From 8ddcece8204d6e0690cc5346a2d94c8f89d052c0 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Wed, 7 May 2025 15:23:36 +0200 Subject: [PATCH] Add wallet id for settings filtering --- liana-gui/src/app/settings.rs | 42 +++-- liana-gui/src/app/state/settings/wallet.rs | 8 +- liana-gui/src/app/wallet.rs | 48 ++++-- liana-gui/src/backup.rs | 7 +- liana-gui/src/installer/mod.rs | 2 + liana-gui/src/launcher.rs | 179 ++++++++++----------- liana-gui/src/loader.rs | 20 ++- liana-gui/src/main.rs | 36 ++--- 8 files changed, 176 insertions(+), 166 deletions(-) diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index eca160d8..0fc72217 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -26,6 +26,24 @@ pub struct Settings { pub wallets: Vec, } +impl Settings { + pub fn from_file(network_dir: &NetworkDirectory) -> Result { + let mut path = network_dir.path().to_path_buf(); + path.push(DEFAULT_FILE_NAME); + + std::fs::read(path) + .map_err(|e| match e.kind() { + std::io::ErrorKind::NotFound => SettingsError::NotFound, + _ => SettingsError::ReadingFile(format!("Reading settings file: {}", e)), + }) + .and_then(|file_content| { + serde_json::from_slice::(&file_content).map_err(|e| { + SettingsError::ReadingFile(format!("Parsing settings file: {}", e)) + }) + }) + } +} + pub async fn update_settings_file( network_dir: &NetworkDirectory, updater: F, @@ -107,6 +125,7 @@ impl AuthConfig { pub struct WalletSettings { pub name: String, pub descriptor_checksum: String, + pub pinned_at: Option, // if wallet is using remote backend, then this information is stored on the remote backend // wallet metadata #[serde(default)] @@ -129,20 +148,7 @@ impl WalletSettings { where F: FnMut(&WalletSettings) -> bool, { - let mut path = network_dir.path().to_path_buf(); - path.push(DEFAULT_FILE_NAME); - - std::fs::read(path) - .map_err(|e| match e.kind() { - std::io::ErrorKind::NotFound => SettingsError::NotFound, - _ => SettingsError::ReadingFile(format!("Reading settings file: {}", e)), - }) - .and_then(|file_content| { - serde_json::from_slice::(&file_content).map_err(|e| { - SettingsError::ReadingFile(format!("Parsing settings file: {}", e)) - }) - }) - .map(|cache| cache.wallets.into_iter().find(selecter)) + Settings::from_file(network_dir).map(|cache| cache.wallets.into_iter().find(selecter)) } pub fn keys_aliases(&self) -> HashMap { @@ -183,6 +189,14 @@ impl WalletSettings { .collect(); } } + + pub fn wallet_id(&self) -> String { + if let Some(t) = self.pinned_at { + format!("{}-{}", self.descriptor_checksum, t) + } else { + self.descriptor_checksum.clone() + } + } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index 92f0f230..8d24ac6b 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -428,12 +428,12 @@ async fn register_wallet( if daemon.backend() != DaemonBackend::RemoteBackend { let network_dir = data_dir.network_directory(network); - let checksum = wallet.descriptor_checksum(); + let wallet_id = wallet.id(); update_settings_file(&network_dir, |mut settings| { if let Some(wallet_setting) = settings .wallets .iter_mut() - .find(|w| w.descriptor_checksum == checksum) + .find(|w| w.wallet_id() == wallet_id) { if let Some(hw_config) = wallet_setting .hardware_wallets @@ -479,12 +479,12 @@ pub async fn update_keys_aliases( ) -> Result, Error> { if daemon.backend() != DaemonBackend::RemoteBackend { let network_dir = data_dir.network_directory(network); - let checksum = wallet.descriptor_checksum(); + let wallet_id = wallet.id(); update_settings_file(&network_dir, |mut settings| { if let Some(wallet_setting) = settings .wallets .iter_mut() - .find(|w| w.descriptor_checksum == checksum) + .find(|w| w.wallet_id() == wallet_id) { wallet_setting.keys = keys_aliases .iter() diff --git a/liana-gui/src/app/wallet.rs b/liana-gui/src/app/wallet.rs index 1cfdcf10..6aae66f9 100644 --- a/liana-gui/src/app/wallet.rs +++ b/liana-gui/src/app/wallet.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use crate::dir::{LianaDirectory, NetworkDirectory}; +use crate::dir::LianaDirectory; use crate::{ app::settings, daemon::DaemonBackend, hw::HardwareWalletConfig, node::NodeType, signer::Signer, }; @@ -11,6 +11,8 @@ use liana::{miniscript::bitcoin, signer::HotSigner}; use liana::descriptors::LianaDescriptor; use liana::miniscript::bitcoin::bip32::Fingerprint; +use super::settings::WalletSettings; + const DEFAULT_WALLET_NAME: &str = "Liana"; pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String { @@ -31,6 +33,8 @@ pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String { pub struct Wallet { pub name: String, pub main_descriptor: LianaDescriptor, + pub descriptor_checksum: String, + pub pinned_at: Option, // TODO: We could replace these two fields with `keys: HashMap`. pub keys_aliases: HashMap, pub provider_keys: HashMap, @@ -42,6 +46,13 @@ impl Wallet { pub fn new(main_descriptor: LianaDescriptor) -> Self { Self { name: wallet_name(&main_descriptor), + descriptor_checksum: main_descriptor + .to_string() + .split_once('#') + .map(|(_, checksum)| checksum) + .unwrap() + .to_string(), + pinned_at: None, main_descriptor, keys_aliases: HashMap::new(), provider_keys: HashMap::new(), @@ -55,6 +66,20 @@ impl Wallet { self } + pub fn with_pin_date(mut self, pinned_at: Option) -> Self { + self.pinned_at = pinned_at; + self + } + + // To match with WalletSettings.wallet_id + pub fn id(&self) -> String { + if let Some(t) = self.pinned_at { + format!("{}-{}", self.descriptor_checksum, t) + } else { + self.descriptor_checksum.clone() + } + } + pub fn with_key_aliases(mut self, aliases: HashMap) -> Self { self.keys_aliases = aliases; self @@ -92,26 +117,15 @@ impl Wallet { descriptor_keys } - pub fn descriptor_checksum(&self) -> String { - self.main_descriptor - .to_string() - .split_once('#') - .map(|(_, checksum)| checksum) - .unwrap() - .to_string() - } - - pub fn load_from_settings(self, dir: &NetworkDirectory) -> Result { - if let Some(wallet_settings) = settings::WalletSettings::from_file(dir, |w| { - w.descriptor_checksum == self.descriptor_checksum() - })? { + pub fn load_from_settings(self, wallet_settings: WalletSettings) -> Result { + if wallet_settings.descriptor_checksum != self.descriptor_checksum { + Err(WalletError::WrongWalletLoaded) + } else { Ok(self .with_key_aliases(wallet_settings.keys_aliases()) .with_provider_keys(wallet_settings.provider_keys()) .with_name(wallet_settings.name) .with_hardware_wallets(wallet_settings.hardware_wallets)) - } else { - Ok(self) } } @@ -172,6 +186,7 @@ impl Wallet { #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum WalletError { + WrongWalletLoaded, Settings(settings::SettingsError), HotSigner(String), } @@ -179,6 +194,7 @@ pub enum WalletError { impl std::fmt::Display for WalletError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { + Self::WrongWalletLoaded => write!(f, "Wrong wallet was loaded"), Self::Settings(e) => write!(f, "Failed to load settings: {}", e), Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e), } diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index 11a9aa61..5ff66f63 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -199,10 +199,9 @@ impl Backup { let keys = wallet.keys(); let network_dir = datadir.network_directory(network); - if let Some(settings) = WalletSettings::from_file(&network_dir, |settings| { - wallet.descriptor_checksum() == settings.descriptor_checksum - }) - .map_err(|_| Error::SettingsFromFile)? + if let Some(settings) = + WalletSettings::from_file(&network_dir, |settings| wallet.id() == settings.wallet_id()) + .map_err(|_| Error::SettingsFromFile)? { if let Ok(settings) = serde_json::to_value(settings) { proprietary.insert(SETTINGS_KEY.to_string(), settings); diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index 2ebce40b..1ca0d102 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -668,6 +668,7 @@ pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletC wallets: vec![WalletSettings { name: wallet_name(descriptor), descriptor_checksum, + pinned_at: Some(chrono::Utc::now().timestamp()), keys: Vec::new(), hardware_wallets: Vec::new(), remote_backend_auth: Some(AuthConfig::new( @@ -704,6 +705,7 @@ pub fn extract_local_gui_settings(ctx: &Context) -> Settings { Settings { wallets: vec![WalletSettings { name: wallet_name(descriptor), + pinned_at: Some(chrono::Utc::now().timestamp()), descriptor_checksum, keys: ctx.keys.values().cloned().collect(), hardware_wallets, diff --git a/liana-gui/src/launcher.rs b/liana-gui/src/launcher.rs index 0fa95e16..3c120a7a 100644 --- a/liana-gui/src/launcher.rs +++ b/liana-gui/src/launcher.rs @@ -13,7 +13,7 @@ use liana_ui::{ use lianad::config::ConfigError; use crate::{ - app, + app::{self, settings::WalletSettings}, dir::{LianaDirectory, NetworkDirectory}, installer::UserFlow, }; @@ -28,11 +28,7 @@ const NETWORKS: [Network; 4] = [ #[derive(Debug, Clone)] pub enum State { Unchecked, - Wallet { - name: Option, - email: Option, - checksum: Option, - }, + Wallets(Vec), NoWallet, } @@ -133,20 +129,26 @@ impl Launcher { Task::none() } }, - Message::View(ViewMessage::Run) => { - if matches!(self.state, State::Wallet { .. }) { - let datadir_path = self.datadir_path.clone(); - let mut path = self - .datadir_path - .network_directory(self.network) - .path() - .to_path_buf(); - path.push(app::config::DEFAULT_FILE_NAME); - let cfg = app::Config::from_file(&path).expect("Already checked"); - let network = self.network; - Task::perform(async move { (datadir_path.clone(), cfg, network) }, |m| { - Message::Run(m.0, m.1, m.2) - }) + Message::View(ViewMessage::Run(index)) => { + if let State::Wallets(wallets) = &self.state { + if let Some(settings) = wallets.get(index) { + let datadir_path = self.datadir_path.clone(); + let mut path = self + .datadir_path + .network_directory(self.network) + .path() + .to_path_buf(); + path.push(app::config::DEFAULT_FILE_NAME); + let cfg = app::Config::from_file(&path).expect("Already checked"); + let network = self.network; + let settings = settings.clone(); + Task::perform( + async move { (datadir_path.clone(), cfg, network, settings) }, + |m| Message::Run(m.0, m.1, m.2, m.3), + ) + } else { + Task::none() + } } else { Task::none() } @@ -191,7 +193,7 @@ impl Launcher { Column::new() .align_x(Alignment::Center) .spacing(30) - .push(if matches!(self.state, State::Wallet { .. }) { + .push(if matches!(self.state, State::Wallets { .. }) { text("Welcome back").size(50).bold() } else { text("Welcome").size(50).bold() @@ -199,63 +201,14 @@ impl Launcher { .push_maybe(self.error.as_ref().map(|e| card::simple(text(e)))) .push(match &self.state { State::Unchecked => Column::new(), - State::Wallet { - email, checksum, .. - } => Column::new().push( - Row::new() - .align_y(Alignment::Center) - .spacing(20) - .push( - Container::new( - Button::new( - Column::new() - .push(p1_bold(format!( - "My Liana {} wallet", - match self.network { - Network::Bitcoin => "Bitcoin", - Network::Signet => "Signet", - Network::Testnet => "Testnet", - Network::Regtest => "Regtest", - _ => "", - } - ))) - .push_maybe(checksum.as_ref().map( - |checksum| { - p1_regular(format!( - "Liana-{}", - checksum - )) - .style(theme::text::secondary) - }, - )) - .push_maybe(email.as_ref().map(|email| { - Row::new() - .push(Space::with_width( - Length::Fill, - )) - .push( - p1_regular(email).style( - theme::text::secondary, - ), - ) - })), - ) - .on_press(ViewMessage::Run) - .padding(15) - .style(theme::button::container_border) - .width(Length::Fill), - ) - .style(theme::card::simple), - ) - .push( - Button::new(icon::trash_icon()) - .style(theme::button::secondary) - .padding(10) - .on_press(ViewMessage::DeleteWallet( - DeleteWalletMessage::ShowModal, - )), - ), - ), + State::Wallets(wallets) => { + Column::new().push(wallets.iter().enumerate().fold( + Column::new().spacing(20), + |col, (i, settings)| { + col.push(wallets_list_item(self.network, settings, i)) + }, + )) + } State::NoWallet => Column::new() .push( Row::new() @@ -338,12 +291,63 @@ impl Launcher { } } +fn wallets_list_item( + network: Network, + settings: &WalletSettings, + i: usize, +) -> Element { + Container::new( + Row::new() + .align_y(Alignment::Center) + .spacing(20) + .push( + Container::new( + Button::new( + Column::new() + .push(p1_bold(format!( + "My Liana {} wallet", + match network { + Network::Bitcoin => "Bitcoin", + Network::Signet => "Signet", + Network::Testnet => "Testnet", + Network::Regtest => "Regtest", + _ => "", + } + ))) + .push( + p1_regular(format!("Liana-{}", settings.descriptor_checksum)) + .style(theme::text::secondary), + ) + .push_maybe(settings.remote_backend_auth.as_ref().map(|auth| { + Row::new() + .push(Space::with_width(Length::Fill)) + .push(p1_regular(&auth.email).style(theme::text::secondary)) + })), + ) + .on_press(ViewMessage::Run(i)) + .padding(15) + .style(theme::button::container_border) + .width(Length::Fill), + ) + .style(theme::card::simple), + ) + .push( + Button::new(icon::trash_icon()) + .style(theme::button::secondary) + .padding(10) + .on_press(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal)), + ), + ) + .into() +} + +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum Message { View(ViewMessage), Install(LianaDirectory, Network, UserFlow), Checked(Result), - Run(LianaDirectory, app::config::Config, Network), + Run(LianaDirectory, app::config::Config, Network, WalletSettings), } #[derive(Debug, Clone)] @@ -354,7 +358,7 @@ pub enum ViewMessage { SelectNetwork(Network), StartInstall(Network), Check, - Run, + Run(usize), DeleteWallet(DeleteWalletMessage), } @@ -513,16 +517,7 @@ async fn check_network_datadir(path: NetworkDirectory) -> Result })?; } - if let Ok(Some(wallet)) = app::settings::WalletSettings::from_file(&path, |_w| true) { - return Ok(State::Wallet { - name: Some(wallet.name), - checksum: Some(wallet.descriptor_checksum), - email: wallet.remote_backend_auth.map(|auth| auth.email), - }); - } - Ok(State::Wallet { - name: None, - checksum: None, - email: None, - }) + app::settings::Settings::from_file(&path) + .map(|s| State::Wallets(s.wallets)) + .map_err(|e| e.to_string()) } diff --git a/liana-gui/src/loader.rs b/liana-gui/src/loader.rs index c34c5dbe..b595f955 100644 --- a/liana-gui/src/loader.rs +++ b/liana-gui/src/loader.rs @@ -59,7 +59,7 @@ pub struct Loader { pub internal_bitcoind: Option, pub waiting_daemon_bitcoind: bool, pub backup: Option, - pub wallet_setting: Option, + pub wallet_settings: WalletSettings, step: Step, } @@ -119,7 +119,7 @@ impl Loader { network: bitcoin::Network, internal_bitcoind: Option, backup: Option, - wallet_setting: Option, + wallet_settings: WalletSettings, ) -> (Self, Task) { let path = socket_path(&datadir_path, network); ( @@ -131,7 +131,7 @@ impl Loader { daemon_started: false, internal_bitcoind, waiting_daemon_bitcoind: false, - wallet_setting, + wallet_settings, backup, }, Task::perform(connect(path), Message::Loaded), @@ -141,11 +141,7 @@ impl Loader { fn start_bitcoind(&self) -> bool { if self.internal_bitcoind.is_some() { false - } else if let Some(start) = self - .wallet_setting - .as_ref() - .and_then(|setting| setting.start_internal_bitcoind) - { + } else if let Some(start) = self.wallet_settings.start_internal_bitcoind { start } else { self.gui_config.start_internal_bitcoind @@ -162,6 +158,7 @@ impl Loader { if info.block_height > 0 { return Task::perform( load_application( + self.wallet_settings.clone(), daemon, info, self.datadir_path.clone(), @@ -256,6 +253,7 @@ impl Loader { if (info.sync - 1.0_f64).abs() < f64::EPSILON { return Task::perform( load_application( + self.wallet_settings.clone(), daemon.clone(), info, self.datadir_path.clone(), @@ -309,7 +307,7 @@ impl Loader { self.network, self.internal_bitcoind.clone(), self.backup.clone(), - self.wallet_setting.clone(), + self.wallet_settings.clone(), ); *self = loader; cmd @@ -406,6 +404,7 @@ fn get_bitcoind_log(log_path: PathBuf) -> impl Stream> { } pub async fn load_application( + wallet_settings: WalletSettings, daemon: Arc, info: GetInfoResult, datadir_path: LianaDirectory, @@ -422,9 +421,8 @@ pub async fn load_application( ), Error, > { - let network_dir = datadir_path.network_directory(network); let wallet = Wallet::new(info.descriptors.main) - .load_from_settings(&network_dir)? + .load_from_settings(wallet_settings)? .load_hotsigners(&datadir_path, network)?; let coins = coins_to_cache(daemon.clone()).await.map(|res| res.coins)?; diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index 42dc9cc8..e1b9445d 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -200,34 +200,21 @@ impl GUI { self.state = State::Installer(Box::new(install)); command.map(|msg| Message::Install(Box::new(msg))) } - launcher::Message::Run(datadir_path, cfg, network) => { + launcher::Message::Run(datadir_path, cfg, network, settings) => { self.logger.set_running_mode( datadir_path.clone(), network, self.log_level .unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)), ); - let network_dir = datadir_path.network_directory(network); - if let Ok(settings) = - app::settings::WalletSettings::from_file(&network_dir, |_w| true) - { - if let Some(setting) = settings - .as_ref() - .and_then(|w| w.remote_backend_auth.clone()) - { - let (login, command) = - login::LianaLiteLogin::new(datadir_path, network, setting); - self.state = State::Login(Box::new(login)); - command.map(|msg| Message::Login(Box::new(msg))) - } else { - let (loader, command) = - Loader::new(datadir_path, cfg, network, None, None, settings); - self.state = State::Loader(Box::new(loader)); - command.map(|msg| Message::Load(Box::new(msg))) - } + if let Some(setting) = settings.remote_backend_auth { + let (login, command) = + login::LianaLiteLogin::new(datadir_path, network, setting); + self.state = State::Login(Box::new(login)); + command.map(|msg| Message::Login(Box::new(msg))) } else { let (loader, command) = - Loader::new(datadir_path, cfg, network, None, None, None); + Loader::new(datadir_path, cfg, network, None, None, settings); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } @@ -281,12 +268,11 @@ impl GUI { (State::Installer(i), Message::Install(msg)) => { if let installer::Message::Exit(path, internal_bitcoind, remove_log) = *msg { let network_dir = i.datadir.network_directory(i.network); + // We get the first one created. let settings = app::settings::WalletSettings::from_file(&network_dir, |_| true) - .expect("A settings file was created"); - if let Some(setting) = settings - .as_ref() - .and_then(|w| w.remote_backend_auth.clone()) - { + .expect("A settings file was created") + .expect("A wallet was created"); + if let Some(setting) = settings.remote_backend_auth { let (login, command) = login::LianaLiteLogin::new(i.datadir.clone(), i.network, setting); self.state = State::Login(Box::new(login));