diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index 438d0977..fa622ec8 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -26,6 +26,7 @@ pub enum Message { Psbt(Result), Signed(Result<(Psbt, Fingerprint), Error>), Updated(Result<(), Error>), + Saved(Result<(), Error>), StartRescan(Result<(), Error>), ConnectedHardwareWallets(Vec), HistoryTransactions(Result, Error>), diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index 709a5a73..49567f85 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -12,15 +12,13 @@ use crate::{ Daemon, }, hw::{list_hardware_wallets, HardwareWallet}, + ui::component::modal, }; trait Action { fn warning(&self) -> Option<&Error> { None } - fn updated(&self) -> bool { - false - } fn load(&self, _daemon: Arc) -> Command { Command::none() } @@ -40,13 +38,13 @@ pub struct SpendTxState { config: Config, tx: SpendTx, saved: bool, - action: Box, + action: Option>, } impl SpendTxState { pub fn new(config: Config, tx: SpendTx, saved: bool) -> Self { Self { - action: choose_action(&config, saved, &tx), + action: None, config, tx, saved, @@ -54,7 +52,11 @@ impl SpendTxState { } pub fn load(&self, daemon: Arc) -> Command { - self.action.load(daemon) + if let Some(action) = &self.action { + action.load(daemon) + } else { + Command::none() + } } pub fn update( @@ -63,60 +65,57 @@ impl SpendTxState { cache: &Cache, message: Message, ) -> Command { - let cmd = match &message { + match &message { Message::View(view::Message::Spend(msg)) => match msg { view::SpendTxMessage::Cancel => { - self.action = choose_action(&self.config, self.saved, &self.tx); - self.action.load(daemon.clone()) + self.action = None; } view::SpendTxMessage::Delete => { - self.action = Box::new(DeleteAction::default()); - self.action.load(daemon.clone()) + self.action = Some(Box::new(DeleteAction::default())); + } + view::SpendTxMessage::Sign => { + let action = SignAction::new(self.config.clone()); + let cmd = action.load(daemon); + self.action = Some(Box::new(action)); + return cmd; + } + view::SpendTxMessage::Broadcast => { + self.action = Some(Box::new(BroadcastAction::default())); + } + view::SpendTxMessage::Save => { + self.action = Some(Box::new(SaveAction::default())); + } + _ => { + if let Some(action) = self.action.as_mut() { + return action.update(daemon.clone(), cache, message, &mut self.tx); + } } - _ => self - .action - .update(daemon.clone(), cache, message, &mut self.tx), }, - _ => self - .action - .update(daemon.clone(), cache, message, &mut self.tx), + Message::Updated(Ok(_)) => { + self.saved = true; + if let Some(action) = self.action.as_mut() { + return action.update(daemon.clone(), cache, message, &mut self.tx); + } + } + _ => { + if let Some(action) = self.action.as_mut() { + return action.update(daemon.clone(), cache, message, &mut self.tx); + } + } }; - if self.action.updated() { - self.saved = true; - self.action = choose_action(&self.config, self.saved, &self.tx); - self.action.load(daemon) - } else { - cmd - } + Command::none() } pub fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { - detail::spend_view( - self.action.warning(), - &self.tx, - self.action.view(), - self.saved, - cache.network, - ) - } -} - -fn choose_action(config: &Config, saved: bool, tx: &SpendTx) -> Box { - if saved { - match tx.status { - SpendStatus::Deprecated | SpendStatus::Broadcasted => { - return Box::new(NoAction::default()); - } - _ => {} - } - - if !tx.psbt.inputs.first().unwrap().partial_sigs.is_empty() { - return Box::new(BroadcastAction::default()); + let content = detail::spend_view(&self.tx, self.saved, cache.network); + if let Some(action) = &self.action { + modal::Modal::new(content, action.view()) + .on_blur(view::Message::Spend(view::SpendTxMessage::Cancel)) + .into() } else { - return Box::new(SignAction::new(config.clone())); + content } } - Box::new(SaveAction::default()) } #[derive(Default)] @@ -126,14 +125,6 @@ pub struct SaveAction { } impl Action for SaveAction { - fn warning(&self) -> Option<&Error> { - self.error.as_ref() - } - - fn updated(&self) -> bool { - self.saved - } - fn update( &mut self, daemon: Arc, @@ -159,20 +150,17 @@ impl Action for SaveAction { Command::none() } fn view(&self) -> Element { - detail::save_action(self.saved) + detail::save_action(self.error.as_ref(), self.saved) } } #[derive(Default)] pub struct BroadcastAction { - broadcasted: bool, + broadcast: bool, error: Option, } impl Action for BroadcastAction { - fn warning(&self) -> Option<&Error> { - self.error.as_ref() - } fn update( &mut self, daemon: Arc, @@ -195,7 +183,10 @@ impl Action for BroadcastAction { ); } Message::Updated(res) => match res { - Ok(()) => self.broadcasted = true, + Ok(()) => { + tx.status = SpendStatus::Broadcast; + self.broadcast = true; + } Err(e) => self.error = Some(e), }, _ => {} @@ -203,7 +194,7 @@ impl Action for BroadcastAction { Command::none() } fn view(&self) -> Element { - detail::broadcast_action(self.broadcasted) + detail::broadcast_action(self.error.as_ref(), self.broadcast) } } @@ -214,10 +205,6 @@ pub struct DeleteAction { } impl Action for DeleteAction { - fn warning(&self) -> Option<&Error> { - self.error.as_ref() - } - fn update( &mut self, daemon: Arc, @@ -248,7 +235,7 @@ impl Action for DeleteAction { Command::none() } fn view(&self) -> Element { - detail::delete_action(self.deleted) + detail::delete_action(self.error.as_ref(), self.deleted) } } @@ -259,7 +246,6 @@ pub struct SignAction { hws: Vec, error: Option, signed: Vec, - updated: bool, } impl SignAction { @@ -271,7 +257,6 @@ impl SignAction { hws: Vec::new(), error: None, signed: Vec::new(), - updated: false, } } } @@ -281,10 +266,6 @@ impl Action for SignAction { self.error.as_ref() } - fn updated(&self) -> bool { - self.updated - } - fn load(&self, daemon: Arc) -> Command { let config = self.config.clone(); let desc = daemon.config().main_descriptor.to_string(); @@ -327,7 +308,7 @@ impl Action for SignAction { } }, Message::Updated(res) => match res { - Ok(()) => self.updated = true, + Ok(()) => self.processing = false, Err(e) => self.error = Some(e), }, // We add the new hws without dropping the reference of the previous ones. @@ -346,7 +327,13 @@ impl Action for SignAction { Command::none() } fn view(&self) -> Element { - view::spend::detail::sign_action(&self.hws, self.processing, self.chosen_hw, &self.signed) + view::spend::detail::sign_action( + self.error.as_ref(), + &self.hws, + self.processing, + self.chosen_hw, + &self.signed, + ) } } @@ -362,12 +349,3 @@ async fn sign_psbt( hw.sign_tx(&mut psbt).await.map_err(Error::from)?; Ok((psbt, fingerprint)) } - -#[derive(Default)] -pub struct NoAction {} - -impl Action for NoAction { - fn view(&self) -> Element { - iced::widget::Column::new().into() - } -} diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index 21affcad..8cf2bc0e 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -27,6 +27,9 @@ pub enum CreateSpendMessage { #[derive(Debug, Clone)] pub enum SpendTxMessage { Delete, + Sign, + Broadcast, + Save, Confirm, Cancel, SelectHardwareWallet(usize), diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs index b1b342ac..c7966a9c 100644 --- a/gui/src/app/view/spend/detail.rs +++ b/gui/src/app/view/spend/detail.rs @@ -24,21 +24,14 @@ use crate::{ }, }; -pub fn spend_view<'a, T: Into>>( - warning: Option<&Error>, - tx: &'a SpendTx, - action: T, - show_delete: bool, - network: Network, -) -> Element<'a, Message> { +pub fn spend_view(tx: &SpendTx, saved: bool, network: Network) -> Element { spend_modal( - show_delete, - warning, + saved, + None, Column::new() .align_items(Alignment::Center) .spacing(20) .push(spend_header(tx)) - .push(action) .push(spend_overview_view(tx)) .push(inputs_and_outputs_view( &tx.coins, @@ -50,36 +43,44 @@ pub fn spend_view<'a, T: Into>>( ) } -pub fn save_action<'a>(saved: bool) -> Element<'a, Message> { +pub fn save_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Message> { if saved { card::simple(text("Transaction is saved")) - .width(Length::Fill) + .width(Length::Units(400)) .align_x(iced::alignment::Horizontal::Center) .into() } else { card::simple( Column::new() .spacing(10) - .push(text("Save the transaction")) - .push(Row::new().push(Column::new().width(Length::Fill)).push( - button::primary(None, "Save").on_press(Message::Spend(SpendTxMessage::Confirm)), - )), + .push_maybe(warning.map(|w| warn(Some(w)))) + .push(text("Save the transaction as draft")) + .push( + Row::new() + .push(Column::new().width(Length::Fill)) + .push(button::alert(None, "Ignore").on_press(Message::Close)) + .push( + button::primary(None, "Save") + .on_press(Message::Spend(SpendTxMessage::Confirm)), + ), + ), ) - .width(Length::Fill) + .width(Length::Units(400)) .into() } } -pub fn broadcast_action<'a>(saved: bool) -> Element<'a, Message> { +pub fn broadcast_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Message> { if saved { - card::simple(text("Transaction is broadcasted")) - .width(Length::Fill) + card::simple(text("Transaction is broadcast")) + .width(Length::Units(400)) .align_x(iced::alignment::Horizontal::Center) .into() } else { card::simple( Column::new() .spacing(10) + .push_maybe(warning.map(|w| warn(Some(w)))) .push(text("Broadcast the transaction")) .push( Row::new().push(Column::new().width(Length::Fill)).push( @@ -88,21 +89,28 @@ pub fn broadcast_action<'a>(saved: bool) -> Element<'a, Message> { ), ), ) - .width(Length::Fill) + .width(Length::Units(400)) .into() } } -pub fn delete_action<'a>(deleted: bool) -> Element<'a, Message> { +pub fn delete_action<'a>(warning: Option<&Error>, deleted: bool) -> Element<'a, Message> { if deleted { - card::simple(text("Transaction is deleted")) - .align_x(iced::alignment::Horizontal::Center) - .width(Length::Fill) - .into() + card::simple( + Column::new() + .spacing(20) + .align_items(Alignment::Center) + .push(text("Transaction is deleted")) + .push(button::primary(None, "Go back to drafts").on_press(Message::Close)), + ) + .align_x(iced::alignment::Horizontal::Center) + .width(Length::Units(400)) + .into() } else { card::simple( Column::new() .spacing(10) + .push_maybe(warning.map(|w| warn(Some(w)))) .push(text("Delete the transaction draft")) .push( Row::new() @@ -117,13 +125,13 @@ pub fn delete_action<'a>(deleted: bool) -> Element<'a, Message> { ), ), ) - .width(Length::Fill) + .width(Length::Units(400)) .into() } } pub fn spend_modal<'a, T: Into>>( - show_delete: bool, + saved: bool, warning: Option<&Error>, content: T, ) -> Element<'a, Message> { @@ -132,7 +140,7 @@ pub fn spend_modal<'a, T: Into>>( .push( Container::new( Row::new() - .push(if show_delete { + .push(if saved { Column::new() .push( button::alert(Some(icon::trash_icon()), "Delete") @@ -147,9 +155,12 @@ pub fn spend_modal<'a, T: Into>>( .width(Length::Fill) }) .align_items(iced::Alignment::Center) - .push( - button::primary(Some(icon::cross_icon()), "Close").on_press(Message::Close), - ), + .push(if saved { + button::primary(Some(icon::cross_icon()), "Close").on_press(Message::Close) + } else { + button::primary(Some(icon::cross_icon()), "Close") + .on_press(Message::Spend(SpendTxMessage::Save)) + }), ) .padding(10) .style(container::Style::Background), @@ -185,8 +196,8 @@ fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> { .padding(3) .style(badge::PillStyle::Simple), ), - SpendStatus::Broadcasted => Some( - Container::new(text(" Broadcasted ").small()) + SpendStatus::Broadcast => Some( + Container::new(text(" Broadcast ").small()) .padding(3) .style(badge::PillStyle::Success), ), @@ -225,8 +236,25 @@ fn spend_overview_view<'a>(tx: &SpendTx) -> Element<'a, Message> { .push(text(format!( "{}", tx.psbt.inputs[0].partial_sigs.len(), - ))), + ))) + .width(Length::Fill), ) + .push_maybe(if tx.status == SpendStatus::Pending { + if !tx.is_signed() { + Some( + button::primary(None, "Sign") + .on_press(Message::Spend(SpendTxMessage::Sign)), + ) + } else { + Some( + button::primary(None, "Broadcast").on_press( + Message::Spend(SpendTxMessage::Broadcast), + ), + ) + } + } else { + None + }) .align_items(Alignment::Center) .spacing(20), ) @@ -471,6 +499,7 @@ pub fn inputs_and_outputs_view<'a>( } pub fn sign_action<'a>( + warning: Option<&Error>, hws: &[HardwareWallet], processing: bool, chosen_hw: Option, @@ -478,6 +507,7 @@ pub fn sign_action<'a>( ) -> Element<'a, Message> { card::simple( Column::new() + .push_maybe(warning.map(|w| warn(Some(w)))) .push(if !hws.is_empty() { Column::new() .push( @@ -520,6 +550,6 @@ pub fn sign_action<'a>( .width(Length::Fill) .align_items(Alignment::Center), ) - .width(Length::Fill) + .width(Length::Units(500)) .into() } diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs index d7f3e0df..49fdea25 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -62,8 +62,8 @@ fn spend_tx_list_view<'a>(i: usize, tx: &SpendTx) -> Element<'a, Message> { .padding(3) .style(badge::PillStyle::Simple), ), - SpendStatus::Broadcasted => Some( - Container::new(text(" Broadcasted ").small()) + SpendStatus::Broadcast => Some( + Container::new(text(" Broadcast ").small()) .padding(3) .style(badge::PillStyle::Success), ), diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index d64230fe..ec14e83d 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -30,11 +30,11 @@ pub struct SpendTx { pub status: SpendStatus, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum SpendStatus { Pending, Deprecated, - Broadcasted, + Broadcast, } impl SpendTx { @@ -58,7 +58,7 @@ impl SpendTx { inputs_amount += coin.amount; if let Some(info) = coin.spend_info { if info.txid == psbt.unsigned_tx.txid() { - status = SpendStatus::Broadcasted + status = SpendStatus::Broadcast } else { status = SpendStatus::Deprecated } @@ -74,6 +74,10 @@ impl SpendTx { status, } } + + pub fn is_signed(&self) -> bool { + !self.psbt.inputs.first().unwrap().partial_sigs.is_empty() + } } #[derive(Debug, Clone)]