diff --git a/liana-gui/src/app/menu.rs b/liana-gui/src/app/menu.rs index c78cd34b..c1226a3d 100644 --- a/liana-gui/src/app/menu.rs +++ b/liana-gui/src/app/menu.rs @@ -7,9 +7,16 @@ pub enum Menu { Transactions, TransactionPreSelected(Txid), Settings, + SettingsPreSelected(SettingsOption), Coins, CreateSpendTx, Recovery, RefreshCoins(Vec), PsbtPreSelected(Txid), } + +/// Pre-selectable settings options. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SettingsOption { + Node, +} diff --git a/liana-gui/src/app/mod.rs b/liana-gui/src/app/mod.rs index 004a5193..174c0b60 100644 --- a/liana-gui/src/app/mod.rs +++ b/liana-gui/src/app/mod.rs @@ -38,7 +38,7 @@ use wallet::{sync_status, SyncStatus}; use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, daemon::{embedded::EmbeddedDaemon, Daemon, DaemonBackend}, - node::bitcoind::Bitcoind, + node::{bitcoind::Bitcoind, NodeType}, }; use self::state::SettingsState; @@ -63,7 +63,15 @@ impl Panels { daemon_backend: DaemonBackend, internal_bitcoind: Option<&Bitcoind>, config: Arc, + restored_from_backup: bool, ) -> Panels { + let show_rescan_warning = restored_from_backup + && daemon_backend.is_lianad() + && daemon_backend + .node_type() + .map(|nt| nt == NodeType::Bitcoind) + // We don't know the node type for external lianad so assume it's bitcoind. + .unwrap_or(true); Self { current: Menu::Home, home: Home::new( @@ -77,6 +85,7 @@ impl Panels { cache.last_poll_at_startup, ), cache.blockheight, + show_rescan_warning, ), coins: CoinsPanel::new(&cache.coins, wallet.main_descriptor.first_timelock_value()), transactions: TransactionsPanel::new(wallet.clone()), @@ -106,7 +115,7 @@ impl Panels { Menu::PSBTs => &self.psbts, Menu::Transactions => &self.transactions, Menu::TransactionPreSelected(_) => &self.transactions, - Menu::Settings => &self.settings, + Menu::Settings | Menu::SettingsPreSelected(_) => &self.settings, Menu::Coins => &self.coins, Menu::CreateSpendTx => &self.create_spend, Menu::Recovery => &self.recovery, @@ -122,7 +131,7 @@ impl Panels { Menu::PSBTs => &mut self.psbts, Menu::Transactions => &mut self.transactions, Menu::TransactionPreSelected(_) => &mut self.transactions, - Menu::Settings => &mut self.settings, + Menu::Settings | Menu::SettingsPreSelected(_) => &mut self.settings, Menu::Coins => &mut self.coins, Menu::CreateSpendTx => &mut self.create_spend, Menu::Recovery => &mut self.recovery, @@ -150,6 +159,7 @@ impl App { daemon: Arc, data_dir: PathBuf, internal_bitcoind: Option, + restored_from_backup: bool, ) -> (App, Task) { let config = Arc::new(config); let mut panels = Panels::new( @@ -159,6 +169,7 @@ impl App { daemon.backend(), internal_bitcoind.as_ref(), config.clone(), + restored_from_backup, ); let cmd = panels.home.reload(daemon.clone(), wallet.clone()); ( @@ -205,6 +216,16 @@ impl App { return Task::none(); }; } + menu::Menu::SettingsPreSelected(setting) => { + self.panels.current = menu.clone(); + return self.panels.current_mut().update( + self.daemon.clone(), + &self.cache, + Message::View(view::Message::Settings(match setting { + &menu::SettingsOption::Node => view::SettingsMessage::EditBitcoindSettings, + })), + ); + } menu::Menu::RefreshCoins(preselected) => { self.panels.create_spend = CreateSpendPanel::new_self_send( self.wallet.clone(), diff --git a/liana-gui/src/app/state/mod.rs b/liana-gui/src/app/state/mod.rs index a54f0397..37950c78 100644 --- a/liana-gui/src/app/state/mod.rs +++ b/liana-gui/src/app/state/mod.rs @@ -135,6 +135,7 @@ pub struct Home { selected_event: Option<(HistoryTransaction, usize)>, labels_edited: LabelsEdited, warning: Option, + show_rescan_warning: bool, } impl Home { @@ -143,6 +144,7 @@ impl Home { coins: &[Coin], sync_status: SyncStatus, tip_height: i32, + show_rescan_warning: bool, ) -> Self { let (balance, unconfirmed_balance, expiring_coins, remaining_seq) = coins_summary( coins, @@ -163,6 +165,7 @@ impl Home { warning: None, is_last_page: false, processing: false, + show_rescan_warning, } } } @@ -191,6 +194,7 @@ impl State for Home { self.is_last_page, self.processing, &self.sync_status, + self.show_rescan_warning, ), ) } @@ -276,6 +280,9 @@ impl State for Home { self.warning = Some(e); } }, + Message::View(view::Message::HideRescanWarning) => { + self.show_rescan_warning = false; + } Message::View(view::Message::SelectPayment(outpoint)) => { return Task::perform( async move { diff --git a/liana-gui/src/app/view/home.rs b/liana-gui/src/app/view/home.rs index 79b9c12b..f28607fe 100644 --- a/liana-gui/src/app/view/home.rs +++ b/liana-gui/src/app/view/home.rs @@ -4,14 +4,16 @@ use std::{collections::HashMap, time::Duration, vec}; use iced::{ alignment, widget::{Container, Row, Space}, - Alignment, Length, + Alignment::{self, Center}, + Length, }; use liana::miniscript::bitcoin; use liana_ui::{ color, component::{amount::*, button, card, event, form, spinner, text::*}, - icon, theme, + icon::{self, cross_icon}, + theme, widget::*, }; @@ -19,13 +21,46 @@ use crate::{ app::{ cache::Cache, error::Error, - menu::Menu, + menu::{self, Menu}, view::{coins, dashboard, label, message::Message}, wallet::SyncStatus, }, daemon::model::{HistoryTransaction, Payment, PaymentKind, TransactionKind}, }; +const RESCAN_WARNING: &str = "As this wallet was restored from a backup, you may need to rescan the blockchain to see past transactions."; + +fn rescan_warning<'a>() -> Element<'a, Message> { + Container::new( + Column::new() + .spacing(10) + .push( + Row::new() + .spacing(5) + .push(icon::warning_icon().style(theme::text::warning)) + .push(text(RESCAN_WARNING).style(theme::text::warning)) + .align_y(Center), + ) + .push( + Row::new() + .spacing(5) + .push(Space::with_width(Length::Fill)) + .push( + button::secondary(None, "Go to rescan").on_press(Message::Menu( + Menu::SettingsPreSelected(menu::SettingsOption::Node), + )), + ) + .push( + button::secondary(Some(cross_icon()), "Dismiss") + .on_press(Message::HideRescanWarning), + ), + ), + ) + .padding(25) + .style(theme::card::border) + .into() +} + #[allow(clippy::too_many_arguments)] pub fn home_view<'a>( balance: &'a bitcoin::Amount, @@ -36,6 +71,7 @@ pub fn home_view<'a>( is_last_page: bool, processing: bool, sync_status: &SyncStatus, + show_rescan_warning: bool, ) -> Element<'a, Message> { Column::new() .push(h3("Balance")) @@ -99,6 +135,7 @@ pub fn home_view<'a>( }, ), ) + .push_maybe(show_rescan_warning.then_some(rescan_warning())) .push_maybe(if expiring_coins.is_empty() { remaining_sequence.map(|sequence| { Container::new( diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 315d5018..d4a2833b 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -24,6 +24,7 @@ pub enum Message { CreateRbf(CreateRbfMessage), ShowQrCode(usize), ImportExport(ImportExportMessage), + HideRescanWarning, } impl Close for Message { diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 6629865f..63a88232 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -76,6 +76,21 @@ impl DaemonBackend { pub fn is_embedded(&self) -> bool { matches!(self, DaemonBackend::EmbeddedLianad(_)) } + + pub fn is_lianad(&self) -> bool { + matches!( + self, + DaemonBackend::EmbeddedLianad(_) | DaemonBackend::ExternalLianad + ) + } + + pub fn node_type(&self) -> Option { + if let DaemonBackend::EmbeddedLianad(node_type) = self { + *node_type + } else { + None + } + } } #[async_trait] diff --git a/liana-gui/src/loader.rs b/liana-gui/src/loader.rs index e785f721..4a86dd4a 100644 --- a/liana-gui/src/loader.rs +++ b/liana-gui/src/loader.rs @@ -103,6 +103,7 @@ pub enum Message { ), Error, >, + /* restored_from_backup */ bool, ), Started(StartedResult), Loaded(Result<(Arc, GetInfoResult), Error>), diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index 476d3ca6..d98decfe 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -369,7 +369,9 @@ impl GUI { }, |r| { let r = r.map_err(loader::Error::RestoreBackup); - Message::Load(Box::new(loader::Message::App(r))) + Message::Load(Box::new(loader::Message::App( + r, /* restored_from_backup */ true, + ))) }, ) } else { @@ -380,17 +382,29 @@ impl GUI { daemon, loader.datadir_path.clone(), bitcoind, + false, ); self.state = State::App(app); command.map(|msg| Message::Run(Box::new(msg))) } } - loader::Message::App(Ok((cache, wallet, config, daemon, datadir, bitcoind))) => { - let (app, command) = App::new(cache, wallet, config, daemon, datadir, bitcoind); + loader::Message::App( + Ok((cache, wallet, config, daemon, datadir, bitcoind)), + restored_from_backup, + ) => { + let (app, command) = App::new( + cache, + wallet, + config, + daemon, + datadir, + bitcoind, + restored_from_backup, + ); self.state = State::App(app); command.map(|msg| Message::Run(Box::new(msg))) } - loader::Message::App(Err(e)) => { + loader::Message::App(Err(e), _) => { tracing::error!("Fail to import backup: {e}"); Task::none() } @@ -506,6 +520,7 @@ pub fn create_app_with_remote_backend( Arc::new(remote_backend), datadir, None, + false, ) }