From 9b4b6fef1bee12e3e370a34c9d9c30eb437bf404 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Fri, 2 Feb 2024 15:43:26 +0100 Subject: [PATCH 01/12] gui: refac app, keep all states --- gui/src/app/mod.rs | 168 +++++++++++++++++++++++++++++++-------------- 1 file changed, 117 insertions(+), 51 deletions(-) diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 0f3443c3..7df2a417 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -35,14 +35,89 @@ use crate::{ daemon::{embedded::EmbeddedDaemon, Daemon}, }; +use self::state::SettingsState; + +struct Panels { + current: Menu, + home: Home, + coins: CoinsPanel, + transactions: TransactionsPanel, + psbts: PsbtsPanel, + recovery: RecoveryPanel, + receive: ReceivePanel, + create_spend: CreateSpendPanel, + settings: SettingsState, +} + +impl Panels { + fn new( + cache: &Cache, + wallet: Arc, + data_dir: PathBuf, + internal_bitcoind: Option<&Bitcoind>, + ) -> Panels { + Self { + current: Menu::Home, + home: Home::new(wallet.clone(), &cache.coins), + coins: CoinsPanel::new(&cache.coins, wallet.main_descriptor.first_timelock_value()), + transactions: TransactionsPanel::new(), + psbts: PsbtsPanel::new(wallet.clone(), &cache.spend_txs), + recovery: RecoveryPanel::new(wallet.clone(), &cache.coins, cache.blockheight), + receive: ReceivePanel::new(data_dir.clone(), wallet.clone()), + create_spend: CreateSpendPanel::new( + wallet.clone(), + &cache.coins, + cache.blockheight as u32, + cache.network, + ), + settings: state::SettingsState::new( + data_dir.clone(), + wallet.clone(), + internal_bitcoind.is_some(), + ), + } + } + + fn current(&self) -> &dyn State { + match self.current { + Menu::Home => &self.home, + Menu::Receive => &self.receive, + Menu::PSBTs => &self.psbts, + Menu::Transactions => &self.transactions, + Menu::Settings => &self.settings, + Menu::Coins => &self.coins, + Menu::CreateSpendTx => &self.create_spend, + Menu::Recovery => &self.recovery, + Menu::RefreshCoins(_) => &self.create_spend, + Menu::PsbtPreSelected(_) => &self.psbts, + } + } + + fn current_mut(&mut self) -> &mut dyn State { + match self.current { + Menu::Home => &mut self.home, + Menu::Receive => &mut self.receive, + Menu::PSBTs => &mut self.psbts, + Menu::Transactions => &mut self.transactions, + Menu::Settings => &mut self.settings, + Menu::Coins => &mut self.coins, + Menu::CreateSpendTx => &mut self.create_spend, + Menu::Recovery => &mut self.recovery, + Menu::RefreshCoins(_) => &mut self.create_spend, + Menu::PsbtPreSelected(_) => &mut self.psbts, + } + } +} + pub struct App { data_dir: PathBuf, - state: Box, cache: Cache, config: Config, wallet: Arc, daemon: Arc, internal_bitcoind: Option, + + panels: Panels, } impl App { @@ -54,12 +129,17 @@ impl App { data_dir: PathBuf, internal_bitcoind: Option, ) -> (App, Command) { - let state: Box = Home::new(wallet.clone(), &cache.coins).into(); - let cmd = state.load(daemon.clone()); + let panels = Panels::new( + &cache, + wallet.clone(), + data_dir.clone(), + internal_bitcoind.as_ref(), + ); + let cmd = panels.home.load(daemon.clone()); ( Self { + panels, data_dir, - state, cache, config, daemon, @@ -70,37 +150,14 @@ impl App { ) } - fn load_state(&mut self, menu: &Menu) -> Command { - self.state = match menu { - menu::Menu::Settings => state::SettingsState::new( - self.data_dir.clone(), - self.wallet.clone(), - self.internal_bitcoind.is_some(), - ) - .into(), - menu::Menu::Home => Home::new(self.wallet.clone(), &self.cache.coins).into(), - menu::Menu::Coins => CoinsPanel::new( - &self.cache.coins, - self.wallet.main_descriptor.first_timelock_value(), - ) - .into(), - menu::Menu::Recovery => RecoveryPanel::new( - self.wallet.clone(), - &self.cache.coins, - self.cache.blockheight, - ) - .into(), - menu::Menu::Receive => { - ReceivePanel::new(self.data_dir.clone(), self.wallet.clone()).into() - } - menu::Menu::Transactions => TransactionsPanel::new().into(), - menu::Menu::PSBTs => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(), + fn set_current_panel(&mut self, menu: Menu) -> Command { + match &menu { menu::Menu::PsbtPreSelected(txid) => { // Get preselected spend from DB in case it's not yet in the cache. // We only need this single spend as we will go straight to its view and not show the PSBTs list. // In case of any error loading the spend or if it doesn't exist, fall back to using the cache // and load PSBTs list in usual way. - match self + self.panels.psbts = match self .daemon .list_spend_transactions(Some(&[*txid])) .map(|txs| txs.first().cloned()) @@ -109,31 +166,37 @@ impl App { PsbtsPanel::new_preselected(self.wallet.clone(), spend_tx).into() } _ => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(), - } + }; } - menu::Menu::CreateSpendTx => CreateSpendPanel::new( - self.wallet.clone(), - &self.cache.coins, - self.cache.blockheight as u32, - self.cache.network, - ) - .into(), - menu::Menu::RefreshCoins(preselected) => CreateSpendPanel::new_self_send( - self.wallet.clone(), - &self.cache.coins, - self.cache.blockheight as u32, - preselected, - self.cache.network, - ) - .into(), + menu::Menu::CreateSpendTx => { + self.panels.create_spend = CreateSpendPanel::new( + self.wallet.clone(), + &self.cache.coins, + self.cache.blockheight as u32, + self.cache.network, + ) + .into(); + } + menu::Menu::RefreshCoins(preselected) => { + self.panels.create_spend = CreateSpendPanel::new_self_send( + self.wallet.clone(), + &self.cache.coins, + self.cache.blockheight as u32, + preselected, + self.cache.network, + ) + .into(); + } + _ => {} }; - self.state.load(self.daemon.clone()) + self.panels.current = menu; + self.panels.current().load(self.daemon.clone()) } pub fn subscription(&self) -> Subscription { Subscription::batch(vec![ time::every(Duration::from_secs(5)).map(|_| Message::Tick), - self.state.subscription(), + self.panels.current().subscription(), ]) } @@ -187,9 +250,12 @@ impl App { let res = self.load_wallet(); self.update(Message::WalletLoaded(res)) } - Message::View(view::Message::Menu(menu)) => self.load_state(&menu), + Message::View(view::Message::Menu(menu)) => self.set_current_panel(menu), Message::View(view::Message::Clipboard(text)) => clipboard::write(text), - _ => self.state.update(self.daemon.clone(), &self.cache, message), + _ => self + .panels + .current_mut() + .update(self.daemon.clone(), &self.cache, message), } } @@ -230,6 +296,6 @@ impl App { } pub fn view(&self) -> Element { - self.state.view(&self.cache).map(Message::View) + self.panels.current().view(&self.cache).map(Message::View) } } From c268c3a093594b76d424d935840c8c5ef6c83482 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Fri, 2 Feb 2024 17:12:15 +0100 Subject: [PATCH 02/12] gui: load receive panel only once --- gui/src/app/state/receive.rs | 44 +++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/gui/src/app/state/receive.rs b/gui/src/app/state/receive.rs index cd4bfca0..0d34c475 100644 --- a/gui/src/app/state/receive.rs +++ b/gui/src/app/state/receive.rs @@ -34,6 +34,12 @@ pub struct Addresses { labels: HashMap, } +impl Addresses { + fn is_empty(&self) -> bool { + self.list.is_empty() + } +} + impl Labelled for Addresses { fn labelled(&self) -> Vec { self.list @@ -154,7 +160,18 @@ impl State for ReceivePanel { )); Command::none() } - Message::View(view::Message::Next) => self.load(daemon), + Message::View(view::Message::Next) => { + let daemon = daemon.clone(); + Command::perform( + async move { + daemon + .get_new_address() + .map(|res| (res.address, res.derivation_index)) + .map_err(|e| e.into()) + }, + Message::ReceiveAddress, + ) + } _ => self .modal .as_mut() @@ -164,16 +181,21 @@ impl State for ReceivePanel { } fn load(&self, daemon: Arc) -> Command { - let daemon = daemon.clone(); - Command::perform( - async move { - daemon - .get_new_address() - .map(|res| (res.address, res.derivation_index)) - .map_err(|e| e.into()) - }, - Message::ReceiveAddress, - ) + // Fill at least with one address, user will then use the generate button. + if self.addresses.is_empty() { + let daemon = daemon.clone(); + Command::perform( + async move { + daemon + .get_new_address() + .map(|res| (res.address, res.derivation_index)) + .map_err(|e| e.into()) + }, + Message::ReceiveAddress, + ) + } else { + Command::none() + } } } From fa4483a4b71ceb17463e89a27d1337c3cfc4d20d Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 6 Feb 2024 12:56:58 +0100 Subject: [PATCH 03/12] gui: add reload cycle to reset state When user clicks on the sidebar menu of the current panel, he expects the view to reset to its initial state without modal of transaction detail for example. --- gui/src/app/mod.rs | 9 --------- gui/src/app/state/mod.rs | 4 ++++ gui/src/app/state/psbts.rs | 10 ++++++++++ gui/src/app/state/transactions.rs | 4 ++++ gui/src/app/view/mod.rs | 6 +++--- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 7df2a417..818de204 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -168,15 +168,6 @@ impl App { _ => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(), }; } - menu::Menu::CreateSpendTx => { - self.panels.create_spend = CreateSpendPanel::new( - self.wallet.clone(), - &self.cache.coins, - self.cache.blockheight as u32, - self.cache.network, - ) - .into(); - } menu::Menu::RefreshCoins(preselected) => { self.panels.create_spend = CreateSpendPanel::new_self_send( self.wallet.clone(), diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index 1126f62f..7abf1741 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -210,6 +210,10 @@ impl State for Home { } }; } + Message::View(view::Message::Reload) => { + self.selected_event = None; + return self.load(daemon); + } Message::View(view::Message::Close) => { self.selected_event = None; } diff --git a/gui/src/app/state/psbts.rs b/gui/src/app/state/psbts.rs index 943eb6b2..a87ece79 100644 --- a/gui/src/app/state/psbts.rs +++ b/gui/src/app/state/psbts.rs @@ -79,6 +79,16 @@ impl State for PsbtsPanel { message: Message, ) -> Command { match message { + Message::View(view::Message::Reload) => { + if self.selected_tx.is_some() { + self.selected_tx = None; + return self.load(daemon); + } + if self.import_tx.is_some() { + self.import_tx = None; + return self.load(daemon); + } + } Message::SpendTxs(res) => match res { Err(e) => self.warning = Some(e), Ok(txs) => { diff --git a/gui/src/app/state/transactions.rs b/gui/src/app/state/transactions.rs index c8349754..cc52a7ed 100644 --- a/gui/src/app/state/transactions.rs +++ b/gui/src/app/state/transactions.rs @@ -121,6 +121,10 @@ impl State for TransactionsPanel { self.warning = e.into(); } }, + Message::View(view::Message::Reload) => { + self.selected_tx = None; + return self.load(daemon); + } Message::View(view::Message::Close) => { self.selected_tx = None; } diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs index 73887fa4..71879700 100644 --- a/gui/src/app/view/mod.rs +++ b/gui/src/app/view/mod.rs @@ -45,7 +45,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { let home_button = if *menu == Menu::Home { row!( button::menu_active(Some(home_icon()), "Home") - .on_press(Message::Menu(Menu::Home)) + .on_press(Message::Reload) .width(iced::Length::Fill), menu_green_bar(), ) @@ -58,7 +58,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { let transactions_button = if *menu == Menu::Transactions { row!( button::menu_active(Some(history_icon()), "Transactions") - .on_press(Message::Menu(Menu::Transactions)) + .on_press(Message::Reload) .width(iced::Length::Fill), menu_green_bar() ) @@ -85,7 +85,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { let psbt_button = if *menu == Menu::PSBTs { row!( button::menu_active(Some(history_icon()), "PSBTs") - .on_press(Message::Menu(Menu::PSBTs)) + .on_press(Message::Reload) .width(iced::Length::Fill), menu_green_bar() ) From c15424abe5a3255b63d45903f14b1044af859e6a Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 6 Feb 2024 13:13:36 +0100 Subject: [PATCH 04/12] fix clippy --- gui/src/app/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 818de204..d37142ba 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -71,7 +71,7 @@ impl Panels { cache.network, ), settings: state::SettingsState::new( - data_dir.clone(), + data_dir, wallet.clone(), internal_bitcoind.is_some(), ), @@ -163,9 +163,9 @@ impl App { .map(|txs| txs.first().cloned()) { Ok(Some(spend_tx)) => { - PsbtsPanel::new_preselected(self.wallet.clone(), spend_tx).into() + PsbtsPanel::new_preselected(self.wallet.clone(), spend_tx) } - _ => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(), + _ => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs), }; } menu::Menu::RefreshCoins(preselected) => { @@ -175,8 +175,7 @@ impl App { self.cache.blockheight as u32, preselected, self.cache.network, - ) - .into(); + ); } _ => {} }; From ed363963b34235d49291a0da7719fd655ff7a8dc Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 26 Feb 2024 14:53:28 +0100 Subject: [PATCH 05/12] Change state load method for reload --- gui/src/app/mod.rs | 6 +++--- gui/src/app/state/coins.rs | 2 +- gui/src/app/state/mod.rs | 8 ++++---- gui/src/app/state/psbts.rs | 25 +++++-------------------- gui/src/app/state/receive.rs | 2 +- gui/src/app/state/recovery.rs | 2 +- gui/src/app/state/settings/bitcoind.rs | 2 +- gui/src/app/state/settings/mod.rs | 13 +++++++++---- gui/src/app/state/settings/wallet.rs | 2 +- gui/src/app/state/spend/mod.rs | 2 +- gui/src/app/state/transactions.rs | 11 ++++------- gui/src/utils/sandbox.rs | 2 +- 12 files changed, 32 insertions(+), 45 deletions(-) diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index d37142ba..82720a95 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -129,13 +129,13 @@ impl App { data_dir: PathBuf, internal_bitcoind: Option, ) -> (App, Command) { - let panels = Panels::new( + let mut panels = Panels::new( &cache, wallet.clone(), data_dir.clone(), internal_bitcoind.as_ref(), ); - let cmd = panels.home.load(daemon.clone()); + let cmd = panels.home.reload(daemon.clone()); ( Self { panels, @@ -180,7 +180,7 @@ impl App { _ => {} }; self.panels.current = menu; - self.panels.current().load(self.daemon.clone()) + self.panels.current_mut().reload(self.daemon.clone()) } pub fn subscription(&self) -> Subscription { diff --git a/gui/src/app/state/coins.rs b/gui/src/app/state/coins.rs index 12342fb1..fc9b49d7 100644 --- a/gui/src/app/state/coins.rs +++ b/gui/src/app/state/coins.rs @@ -150,7 +150,7 @@ impl State for CoinsPanel { Command::none() } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { let daemon1 = daemon.clone(); let daemon2 = daemon.clone(); Command::batch(vec![ diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index 7abf1741..df2c9b54 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -44,7 +44,7 @@ pub trait State { fn subscription(&self) -> Subscription { Subscription::none() } - fn load(&self, _daemon: Arc) -> Command { + fn reload(&mut self, _daemon: Arc) -> Command { Command::none() } } @@ -211,8 +211,7 @@ impl State for Home { }; } Message::View(view::Message::Reload) => { - self.selected_event = None; - return self.load(daemon); + return self.reload(daemon); } Message::View(view::Message::Close) => { self.selected_event = None; @@ -263,7 +262,8 @@ impl State for Home { Command::none() } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { + self.selected_event = None; let daemon1 = daemon.clone(); let daemon2 = daemon.clone(); let daemon3 = daemon.clone(); diff --git a/gui/src/app/state/psbts.rs b/gui/src/app/state/psbts.rs index a87ece79..7a385580 100644 --- a/gui/src/app/state/psbts.rs +++ b/gui/src/app/state/psbts.rs @@ -79,15 +79,8 @@ impl State for PsbtsPanel { message: Message, ) -> Command { match message { - Message::View(view::Message::Reload) => { - if self.selected_tx.is_some() { - self.selected_tx = None; - return self.load(daemon); - } - if self.import_tx.is_some() { - self.import_tx = None; - return self.load(daemon); - } + Message::View(view::Message::Reload) | Message::View(view::Message::Close) => { + return self.reload(daemon); } Message::SpendTxs(res) => match res { Err(e) => self.warning = Some(e), @@ -101,16 +94,6 @@ impl State for PsbtsPanel { self.import_tx = Some(ImportPsbtModal::new()); } } - Message::View(view::Message::Close) => { - if self.selected_tx.is_some() { - self.selected_tx = None; - return self.load(daemon); - } - if self.import_tx.is_some() { - self.import_tx = None; - return self.load(daemon); - } - } Message::View(view::Message::Select(i)) => { if let Some(tx) = self.spend_txs.get(i) { let tx = psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true); @@ -140,7 +123,9 @@ impl State for PsbtsPanel { } } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { + self.selected_tx = None; + self.import_tx = None; let daemon = daemon.clone(); Command::perform( async move { daemon.list_spend_transactions(None).map_err(|e| e.into()) }, diff --git a/gui/src/app/state/receive.rs b/gui/src/app/state/receive.rs index 0d34c475..e5dac8ce 100644 --- a/gui/src/app/state/receive.rs +++ b/gui/src/app/state/receive.rs @@ -180,7 +180,7 @@ impl State for ReceivePanel { } } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { // Fill at least with one address, user will then use the generate button. if self.addresses.is_empty() { let daemon = daemon.clone(); diff --git a/gui/src/app/state/recovery.rs b/gui/src/app/state/recovery.rs index 2e7811e8..087d32e3 100644 --- a/gui/src/app/state/recovery.rs +++ b/gui/src/app/state/recovery.rs @@ -192,7 +192,7 @@ impl State for RecoveryPanel { Command::none() } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { let daemon = daemon.clone(); Command::perform( async move { diff --git a/gui/src/app/state/settings/bitcoind.rs b/gui/src/app/state/settings/bitcoind.rs index cf944660..83b13b9b 100644 --- a/gui/src/app/state/settings/bitcoind.rs +++ b/gui/src/app/state/settings/bitcoind.rs @@ -16,7 +16,7 @@ use liana::{ use liana_ui::{component::form, widget::Element}; use crate::{ - app::{cache::Cache, error::Error, message::Message, view, State}, + app::{cache::Cache, error::Error, message::Message, state::settings::State, view}, bitcoind::{RpcAuthType, RpcAuthValues}, daemon::Daemon, }; diff --git a/gui/src/app/state/settings/mod.rs b/gui/src/app/state/settings/mod.rs index 21cf2820..0788b459 100644 --- a/gui/src/app/state/settings/mod.rs +++ b/gui/src/app/state/settings/mod.rs @@ -55,14 +55,14 @@ impl State for SettingsState { ); self.setting .as_mut() - .map(|s| s.load(daemon)) + .map(|s| s.reload(daemon)) .unwrap_or_else(Command::none) } Message::View(view::Message::Settings(view::SettingsMessage::AboutSection)) => { self.setting = Some(AboutSettingsState::default().into()); self.setting .as_mut() - .map(|s| s.load(daemon)) + .map(|s| s.reload(daemon)) .unwrap_or_else(Command::none) } Message::View(view::Message::Settings(view::SettingsMessage::EditWalletSettings)) => { @@ -71,7 +71,7 @@ impl State for SettingsState { ); self.setting .as_mut() - .map(|s| s.load(daemon)) + .map(|s| s.reload(daemon)) .unwrap_or_else(Command::none) } _ => self @@ -97,6 +97,11 @@ impl State for SettingsState { view::settings::list(cache) } } + + fn reload(&mut self, _daemon: Arc) -> Command { + self.setting = None; + Command::none() + } } impl From for Box { @@ -145,7 +150,7 @@ impl State for AboutSettingsState { Command::none() } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { Command::perform( async move { daemon.get_info().map_err(|e| e.into()) }, Message::Info, diff --git a/gui/src/app/state/settings/wallet.rs b/gui/src/app/state/settings/wallet.rs index 85128297..7af6a837 100644 --- a/gui/src/app/state/settings/wallet.rs +++ b/gui/src/app/state/settings/wallet.rs @@ -180,7 +180,7 @@ impl State for WalletSettingsState { } } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { Command::perform( async move { daemon.get_info().map_err(|e| e.into()) }, Message::Info, diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index 4c39ef34..6a01a821 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -108,7 +108,7 @@ impl State for CreateSpendPanel { Command::none() } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { let daemon1 = daemon.clone(); let daemon2 = daemon.clone(); Command::batch(vec![ diff --git a/gui/src/app/state/transactions.rs b/gui/src/app/state/transactions.rs index cc52a7ed..621c95d7 100644 --- a/gui/src/app/state/transactions.rs +++ b/gui/src/app/state/transactions.rs @@ -121,12 +121,8 @@ impl State for TransactionsPanel { self.warning = e.into(); } }, - Message::View(view::Message::Reload) => { - self.selected_tx = None; - return self.load(daemon); - } - Message::View(view::Message::Close) => { - self.selected_tx = None; + Message::View(view::Message::Reload) | Message::View(view::Message::Close) => { + return self.reload(daemon); } Message::View(view::Message::Select(i)) => { self.selected_tx = Some(i); @@ -229,7 +225,8 @@ impl State for TransactionsPanel { Command::none() } - fn load(&self, daemon: Arc) -> Command { + fn reload(&mut self, daemon: Arc) -> Command { + self.selected_tx = None; let daemon1 = daemon.clone(); let daemon2 = daemon.clone(); let daemon3 = daemon.clone(); diff --git a/gui/src/utils/sandbox.rs b/gui/src/utils/sandbox.rs index 7a408e0d..7a9b45f4 100644 --- a/gui/src/utils/sandbox.rs +++ b/gui/src/utils/sandbox.rs @@ -38,7 +38,7 @@ impl Sandbox { } pub async fn load(mut self, daemon: Arc, cache: &Cache) -> Self { - let cmd = self.state.load(daemon.clone()); + let cmd = self.state.reload(daemon.clone()); for action in cmd.actions() { if let Action::Future(f) = action { let msg = f.await; From 2d2cd12bda47feff6faa01cb5a1338d7f0f9f7a0 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 26 Feb 2024 15:13:23 +0100 Subject: [PATCH 06/12] Restart new spending process on user demand --- gui/src/app/mod.rs | 11 +++++++++++ gui/src/app/state/spend/mod.rs | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 82720a95..8ebaa766 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -177,6 +177,17 @@ impl App { self.cache.network, ); } + menu::Menu::CreateSpendTx => { + // redo the process of spending only if user want to start a new one. + if !self.panels.create_spend.is_first_step() { + self.panels.create_spend = CreateSpendPanel::new( + self.wallet.clone(), + &self.cache.coins, + self.cache.blockheight as u32, + self.cache.network, + ); + } + } _ => {} }; self.panels.current = menu; diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index 6a01a821..6bdf833b 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -63,6 +63,10 @@ impl CreateSpendPanel { ], } } + + pub fn is_first_step(&self) -> bool { + self.current == 0 + } } impl State for CreateSpendPanel { From dc3d29e3f06b7cb2aa0d502edd6f7fcf0aadf458 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 26 Feb 2024 15:33:20 +0100 Subject: [PATCH 07/12] Remove spend_txs from cache --- gui/src/app/cache.rs | 4 +--- gui/src/app/mod.rs | 29 +++++++++++++---------------- gui/src/app/state/psbts.rs | 4 ++-- gui/src/loader.rs | 2 -- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/gui/src/app/cache.rs b/gui/src/app/cache.rs index cc714a6f..05944514 100644 --- a/gui/src/app/cache.rs +++ b/gui/src/app/cache.rs @@ -1,4 +1,4 @@ -use crate::daemon::model::{Coin, SpendTx}; +use crate::daemon::model::Coin; use liana::miniscript::bitcoin::Network; use std::path::PathBuf; @@ -8,7 +8,6 @@ pub struct Cache { pub network: Network, pub blockheight: i32, pub coins: Vec, - pub spend_txs: Vec, pub rescan_progress: Option, } @@ -20,7 +19,6 @@ impl std::default::Default for Cache { network: Network::Bitcoin, blockheight: 0, coins: Vec::new(), - spend_txs: Vec::new(), rescan_progress: None, } } diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 8ebaa766..657cf053 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -32,7 +32,7 @@ use state::{ use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, bitcoind::Bitcoind, - daemon::{embedded::EmbeddedDaemon, Daemon}, + daemon::{embedded::EmbeddedDaemon, model::Coin, Daemon}, }; use self::state::SettingsState; @@ -52,21 +52,22 @@ struct Panels { impl Panels { fn new( cache: &Cache, + coins: &[Coin], wallet: Arc, data_dir: PathBuf, internal_bitcoind: Option<&Bitcoind>, ) -> Panels { Self { current: Menu::Home, - home: Home::new(wallet.clone(), &cache.coins), - coins: CoinsPanel::new(&cache.coins, wallet.main_descriptor.first_timelock_value()), + home: Home::new(wallet.clone(), coins), + coins: CoinsPanel::new(coins, wallet.main_descriptor.first_timelock_value()), transactions: TransactionsPanel::new(), - psbts: PsbtsPanel::new(wallet.clone(), &cache.spend_txs), - recovery: RecoveryPanel::new(wallet.clone(), &cache.coins, cache.blockheight), + psbts: PsbtsPanel::new(wallet.clone()), + recovery: RecoveryPanel::new(wallet.clone(), &coins, cache.blockheight), receive: ReceivePanel::new(data_dir.clone(), wallet.clone()), create_spend: CreateSpendPanel::new( wallet.clone(), - &cache.coins, + &coins, cache.blockheight as u32, cache.network, ), @@ -131,6 +132,7 @@ impl App { ) -> (App, Command) { let mut panels = Panels::new( &cache, + &cache.coins, wallet.clone(), data_dir.clone(), internal_bitcoind.as_ref(), @@ -155,17 +157,15 @@ impl App { menu::Menu::PsbtPreSelected(txid) => { // Get preselected spend from DB in case it's not yet in the cache. // We only need this single spend as we will go straight to its view and not show the PSBTs list. - // In case of any error loading the spend or if it doesn't exist, fall back to using the cache - // and load PSBTs list in usual way. - self.panels.psbts = match self + // In case of any error loading the spend or if it doesn't exist, load PSBTs list in usual way. + if let Ok(Some(spend_tx)) = self .daemon .list_spend_transactions(Some(&[*txid])) .map(|txs| txs.first().cloned()) { - Ok(Some(spend_tx)) => { - PsbtsPanel::new_preselected(self.wallet.clone(), spend_tx) - } - _ => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs), + self.panels.psbts = PsbtsPanel::new_preselected(self.wallet.clone(), spend_tx); + self.panels.current = menu; + return Command::none(); }; } menu::Menu::RefreshCoins(preselected) => { @@ -219,9 +219,6 @@ impl App { Message::Coins(Ok(coins)) => { self.cache.coins = coins.clone(); } - Message::SpendTxs(Ok(txs)) => { - self.cache.spend_txs = txs.clone(); - } Message::Info(Ok(info)) => { self.cache.blockheight = info.block_height; self.cache.rescan_progress = info.rescan_progress; diff --git a/gui/src/app/state/psbts.rs b/gui/src/app/state/psbts.rs index 7a385580..6ba8a8ca 100644 --- a/gui/src/app/state/psbts.rs +++ b/gui/src/app/state/psbts.rs @@ -24,10 +24,10 @@ pub struct PsbtsPanel { } impl PsbtsPanel { - pub fn new(wallet: Arc, spend_txs: &[SpendTx]) -> Self { + pub fn new(wallet: Arc) -> Self { Self { wallet, - spend_txs: spend_txs.to_vec(), + spend_txs: Vec::new(), warning: None, selected_tx: None, import_tx: None, diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 19f43033..da9f8da8 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -368,14 +368,12 @@ pub async fn load_application( Wallet::new(info.descriptors.main).load_settings(&gui_config, &datadir_path, network)?; let coins = daemon.list_coins().map(|res| res.coins)?; - let spend_txs = daemon.list_spend_transactions(None)?; let cache = Cache { datadir_path, network: info.network, blockheight: info.block_height, coins, - spend_txs, ..Default::default() }; From a91fdd791aedc997e4b43e23db8af8c5382cab17 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 26 Feb 2024 16:09:51 +0100 Subject: [PATCH 08/12] Encapsulate cache with its own message --- gui/src/app/message.rs | 3 ++- gui/src/app/mod.rs | 53 +++++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index 5f3488c0..abfd5b99 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -11,7 +11,7 @@ use liana::{ }; use crate::{ - app::{error::Error, view, wallet::Wallet}, + app::{cache::Cache, error::Error, view, wallet::Wallet}, daemon::model::*, hw::HardwareWalletMessage, }; @@ -19,6 +19,7 @@ use crate::{ #[derive(Debug)] pub enum Message { Tick, + UpdateCache(Result), View(view::Message), LoadDaemonConfig(Box), DaemonConfigLoaded(Result<(), Error>), diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 657cf053..2fc2e9ef 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -32,7 +32,7 @@ use state::{ use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, bitcoind::Bitcoind, - daemon::{embedded::EmbeddedDaemon, model::Coin, Daemon}, + daemon::{embedded::EmbeddedDaemon, Daemon}, }; use self::state::SettingsState; @@ -52,22 +52,21 @@ struct Panels { impl Panels { fn new( cache: &Cache, - coins: &[Coin], wallet: Arc, data_dir: PathBuf, internal_bitcoind: Option<&Bitcoind>, ) -> Panels { Self { current: Menu::Home, - home: Home::new(wallet.clone(), coins), - coins: CoinsPanel::new(coins, wallet.main_descriptor.first_timelock_value()), + home: Home::new(wallet.clone(), &cache.coins), + coins: CoinsPanel::new(&cache.coins, wallet.main_descriptor.first_timelock_value()), transactions: TransactionsPanel::new(), psbts: PsbtsPanel::new(wallet.clone()), - recovery: RecoveryPanel::new(wallet.clone(), &coins, cache.blockheight), + recovery: RecoveryPanel::new(wallet.clone(), &cache.coins, cache.blockheight), receive: ReceivePanel::new(data_dir.clone(), wallet.clone()), create_spend: CreateSpendPanel::new( wallet.clone(), - &coins, + &cache.coins, cache.blockheight as u32, cache.network, ), @@ -132,7 +131,6 @@ impl App { ) -> (App, Command) { let mut panels = Panels::new( &cache, - &cache.coins, wallet.clone(), data_dir.clone(), internal_bitcoind.as_ref(), @@ -196,7 +194,7 @@ impl App { pub fn subscription(&self) -> Subscription { Subscription::batch(vec![ - time::every(Duration::from_secs(5)).map(|_| Message::Tick), + time::every(Duration::from_secs(10)).map(|_| Message::Tick), self.panels.current().subscription(), ]) } @@ -213,30 +211,33 @@ impl App { } pub fn update(&mut self, message: Message) -> Command { - // Update cache when values are passing by. - // State will handle the error case. - match &message { - Message::Coins(Ok(coins)) => { - self.cache.coins = coins.clone(); - } - Message::Info(Ok(info)) => { - self.cache.blockheight = info.block_height; - self.cache.rescan_progress = info.rescan_progress; - } - Message::StartRescan(Ok(())) => { - self.cache.rescan_progress = Some(0.0); - } - _ => {} - }; - match message { Message::Tick => { let daemon = self.daemon.clone(); + let datadir_path = self.cache.datadir_path.clone(); Command::perform( - async move { daemon.get_info().map_err(|e| e.into()) }, - Message::Info, + async move { + let info = daemon.get_info()?; + // todo: filter coins to only have current coins. + let coins = daemon.list_coins()?; + Ok(Cache { + datadir_path, + coins: coins.coins, + network: info.network, + blockheight: info.block_height, + rescan_progress: info.rescan_progress, + }) + }, + Message::UpdateCache, ) } + Message::UpdateCache(res) => { + match res { + Ok(cache) => self.cache = cache, + Err(e) => tracing::error!("Failed to update cache: {}", e), + } + Command::none() + } Message::LoadDaemonConfig(cfg) => { let path = self.config.daemon_config_path.clone().expect( "Application config must have a daemon configuration file path at this point.", From 710883b6a2467e4c8fab9b2382e8227b602dfa42 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 26 Feb 2024 16:10:12 +0100 Subject: [PATCH 09/12] Restart spend process according to current state Spend process keeps its state if it is the first step. User can click on Clear to reset if he wants. --- gui/src/app/state/spend/step.rs | 14 ++++++++++++++ gui/src/app/view/message.rs | 1 + gui/src/app/view/mod.rs | 2 +- gui/src/app/view/spend/mod.rs | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index f6ef9c9c..0066bcc7 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -2,6 +2,7 @@ use std::{cmp::Ordering, collections::HashMap, str::FromStr, sync::Arc}; use iced::{Command, Subscription}; use liana::{ + commands::ListCoinsEntry, descriptors::LianaDescriptor, miniscript::bitcoin::{ address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint, @@ -399,6 +400,19 @@ impl Step for DefineSpend { self.batch_label.valid = label.len() <= 100; self.batch_label.value = label; } + view::CreateSpendMessage::Clear => { + *self = Self::new( + self.network, + self.descriptor.clone(), + self.coins + .iter() + .map(|(c, _)| c.clone()) + .collect::>() + .as_slice(), + self.timelock, + ); + return Command::none(); + } view::CreateSpendMessage::AddRecipient => { self.recipients.push(Recipient::default()); } diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index bde8a7c0..36d9511a 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -38,6 +38,7 @@ pub enum CreateSpendMessage { SelectPath(usize), Generate, SendMaxToRecipient(usize), + Clear, } #[derive(Debug, Clone)] diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs index 71879700..e760f58b 100644 --- a/gui/src/app/view/mod.rs +++ b/gui/src/app/view/mod.rs @@ -98,7 +98,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { let spend_button = if *menu == Menu::CreateSpendTx { row!( button::menu_active(Some(send_icon()), "Send") - .on_press(Message::Menu(Menu::CreateSpendTx)) + .on_press(Message::Reload) .width(iced::Length::Fill), menu_green_bar() ) diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs index b41a2dad..2bcabbb8 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -294,7 +294,7 @@ pub fn create_spend_tx<'a>( .push(Space::with_width(Length::Fill)) .push( button::primary(None, "Clear") - .on_press(Message::Menu(Menu::CreateSpendTx)) + .on_press(Message::CreateSpend(CreateSpendMessage::Clear)) .width(Length::Fixed(100.0)), ) .push( From a6832ad0b7a881889e26c10c65f4d0ecd6e8c098 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Fri, 1 Mar 2024 12:10:15 +0100 Subject: [PATCH 10/12] fix send panel: reload coins When going back to send panel, coins are fetched again and passed to DefineSpendStep. A redraft is triggered because some coins may have been removed from the list and new coins were deposited. --- gui/src/app/state/spend/step.rs | 43 +++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 0066bcc7..6efe4647 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -1,4 +1,10 @@ -use std::{cmp::Ordering, collections::HashMap, str::FromStr, sync::Arc}; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, + iter::FromIterator, + str::FromStr, + sync::Arc, +}; use iced::{Command, Subscription}; use liana::{ @@ -143,6 +149,11 @@ impl DefineSpend { } pub fn with_coins_sorted(mut self, blockheight: u32) -> Self { + self.sort_coins(blockheight); + self + } + + fn sort_coins(&mut self, blockheight: u32) { let timelock = self.timelock; self.coins.sort_by(|(a, a_selected), (b, b_selected)| { if *a_selected && !b_selected || !a_selected && *b_selected { @@ -157,7 +168,6 @@ impl DefineSpend { a.block_height.cmp(&b.block_height) } }); - self } pub fn self_send(mut self) -> Self { @@ -542,6 +552,35 @@ impl Step for DefineSpend { } Err(e) => self.warning = Some(e), }, + Message::Coins(res) => match res { + Ok(coins) => { + let selected: HashSet = + HashSet::from_iter(self.coins.iter().filter_map(|(c, selected)| { + if *selected { + Some(c.outpoint) + } else { + None + } + })); + self.coins = coins + .into_iter() + .filter_map(|coin| { + if coin.spend_info.is_none() && !coin.is_immature { + let selected = selected.contains(&coin.outpoint); + Some((coin, selected)) + } else { + None + } + }) + .collect(); + self.sort_coins(cache.blockheight as u32); + // In case some selected coins are not spendable anymore and + // new coins make more sense to be selected. A redraft is triggered + // if all forms are valid (checked in the redraft method) + self.redraft(daemon); + } + Err(e) => self.warning = Some(e), + }, _ => {} }; Command::none() From 2995df870f711966d7d275f67de70719dd1498ce Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 5 Mar 2024 12:00:40 +0100 Subject: [PATCH 11/12] fix psbts panel: keep list of psbts in background fix this comment: https://github.com/wizardsardine/liana/pull/959#issuecomment-1964811858 new_preselected is changed for a preselect method that keeps the current state of the psbts list panel and open the modal with the selected psbt. --- gui/src/app/mod.rs | 2 +- gui/src/app/state/psbts.rs | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 2fc2e9ef..0586cec4 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -161,7 +161,7 @@ impl App { .list_spend_transactions(Some(&[*txid])) .map(|txs| txs.first().cloned()) { - self.panels.psbts = PsbtsPanel::new_preselected(self.wallet.clone(), spend_tx); + self.panels.psbts.preselect(spend_tx); self.panels.current = menu; return Command::none(); }; diff --git a/gui/src/app/state/psbts.rs b/gui/src/app/state/psbts.rs index 6ba8a8ca..414e639e 100644 --- a/gui/src/app/state/psbts.rs +++ b/gui/src/app/state/psbts.rs @@ -34,16 +34,11 @@ impl PsbtsPanel { } } - pub fn new_preselected(wallet: Arc, spend_tx: SpendTx) -> Self { - let psbt_state = psbt::PsbtState::new(wallet.clone(), spend_tx.clone(), true); - - Self { - wallet, - spend_txs: vec![spend_tx], - warning: None, - selected_tx: Some(psbt_state), - import_tx: None, - } + pub fn preselect(&mut self, spend_tx: SpendTx) { + let psbt_state = psbt::PsbtState::new(self.wallet.clone(), spend_tx, true); + self.selected_tx = Some(psbt_state); + self.warning = None; + self.import_tx = None; } } From 18e040e51ff835f5f6c8218392496865b6f8737a Mon Sep 17 00:00:00 2001 From: edouardparis Date: Wed, 6 Mar 2024 10:36:41 +0100 Subject: [PATCH 12/12] fix: override unpaginated pending events and txs pending events and txs are passed unpaginated and in full list through PendingTransactions and PendingPayments. It is useless and armful to append to them to an existing list of pending events as a new rbf replacement event should override the previous one and only one of them must be displayed. --- gui/src/app/state/mod.rs | 6 +----- gui/src/app/state/transactions.rs | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index df2c9b54..0a0bc154 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -186,11 +186,7 @@ impl State for Home { Err(e) => self.warning = Some(e), Ok(events) => { self.warning = None; - for event in events { - if !self.pending_events.iter().any(|other| other.tx == event.tx) { - self.pending_events.push(event); - } - } + self.pending_events = events; } }, Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => { diff --git a/gui/src/app/state/transactions.rs b/gui/src/app/state/transactions.rs index 621c95d7..e0da0740 100644 --- a/gui/src/app/state/transactions.rs +++ b/gui/src/app/state/transactions.rs @@ -105,11 +105,7 @@ impl State for TransactionsPanel { Err(e) => self.warning = Some(e), Ok(txs) => { self.warning = None; - for tx in txs { - if !self.pending_txs.iter().any(|other| other.tx == tx.tx) { - self.pending_txs.push(tx); - } - } + self.pending_txs = txs; } }, Message::RbfModal(tx, is_cancel, res) => match res {