diff --git a/gui/src/app/cache.rs b/gui/src/app/cache.rs index e75429fc..4c13ed10 100644 --- a/gui/src/app/cache.rs +++ b/gui/src/app/cache.rs @@ -1,6 +1,7 @@ use crate::daemon::model::{Coin, SpendTx}; use liana::miniscript::bitcoin::Network; +#[derive(Debug)] pub struct Cache { pub network: Network, pub blockheight: i32, diff --git a/gui/src/app/config.rs b/gui/src/app/config.rs index 8cf30fc1..459de2d7 100644 --- a/gui/src/app/config.rs +++ b/gui/src/app/config.rs @@ -11,19 +11,19 @@ pub struct Config { /// Use iced debug feature if true. pub debug: Option, /// hardware wallets config. - #[serde(default)] - pub hardware_wallets: Vec, + /// LEGACY: Use Settings module instead. + pub hardware_wallets: Option>, } pub const DEFAULT_FILE_NAME: &str = "gui.toml"; impl Config { - pub fn new(daemon_config_path: PathBuf, hardware_wallets: Vec) -> Self { + pub fn new(daemon_config_path: PathBuf) -> Self { Self { daemon_config_path, log_level: None, debug: None, - hardware_wallets, + hardware_wallets: None, } } @@ -40,14 +40,6 @@ impl Config { })?; Ok(config) } - - pub fn default_path() -> Result { - let mut datadir = default_datadir().map_err(|_| { - ConfigError::Unexpected("Could not locate the default datadir directory.".to_owned()) - })?; - datadir.push(DEFAULT_FILE_NAME); - Ok(datadir) - } } #[derive(PartialEq, Eq, Debug, Clone)] diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 17b08751..acab0d48 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -2,6 +2,7 @@ pub mod cache; pub mod config; pub mod menu; pub mod message; +pub mod settings; pub mod state; pub mod view; pub mod wallet; @@ -31,14 +32,14 @@ pub struct App { state: Box, cache: Cache, config: Config, - wallet: Wallet, + wallet: Arc, daemon: Arc, } impl App { pub fn new( cache: Cache, - wallet: Wallet, + wallet: Arc, config: Config, daemon: Arc, ) -> (App, Command) { @@ -72,22 +73,15 @@ impl App { .into(), menu::Menu::Recovery => RecoveryPanel::new( self.wallet.clone(), - self.config.clone(), &self.cache.coins, self.wallet.main_descriptor.timelock_value(), self.cache.blockheight as u32, ) .into(), menu::Menu::Receive => ReceivePanel::default().into(), - menu::Menu::Spend => SpendPanel::new( - self.wallet.clone(), - self.config.clone(), - &self.cache.spend_txs, - ) - .into(), + menu::Menu::Spend => SpendPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(), menu::Menu::CreateSpendTx => CreateSpendPanel::new( self.wallet.clone(), - self.config.clone(), &self.cache.coins, self.cache.blockheight as u32, ) diff --git a/gui/src/app/settings.rs b/gui/src/app/settings.rs new file mode 100644 index 00000000..3f917c92 --- /dev/null +++ b/gui/src/app/settings.rs @@ -0,0 +1,75 @@ +use std::collections::HashMap; +use std::path::Path; + +use liana::miniscript::bitcoin::util::bip32::Fingerprint; +use serde::{Deserialize, Serialize}; + +use crate::hw::HardwareWalletConfig; + +///! Settings is the module to handle the GUI settings file. +///! The settings file is used by the GUI to store useful information. +pub const DEFAULT_FILE_NAME: &str = "settings.json"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Settings { + pub wallets: Vec, +} + +impl Settings { + pub fn from_file(path: &Path) -> Result { + let config = 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)) + }) + })?; + Ok(config) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WalletSetting { + pub name: String, + pub descriptor_checksum: String, + #[serde(default)] + pub keys: Vec, + #[serde(default)] + pub hardware_wallets: Vec, +} + +impl WalletSetting { + pub fn keys_aliases(&self) -> HashMap { + let mut map = HashMap::new(); + for key in self.keys.clone() { + map.insert(key.master_fingerprint, key.name); + } + map + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct KeySetting { + pub name: String, + pub master_fingerprint: Fingerprint, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum SettingsError { + NotFound, + ReadingFile(String), + Unexpected(String), +} + +impl std::fmt::Display for SettingsError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::NotFound => write!(f, "Settings file not found"), + Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e), + Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), + } + } +} diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index 1c3e648f..a6fc78dc 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -39,7 +39,7 @@ pub trait State { } pub struct Home { - wallet: Wallet, + wallet: Arc, balance: Amount, recovery_warning: Option<(Amount, usize)>, recovery_alert: Option<(Amount, usize)>, @@ -50,7 +50,7 @@ pub struct Home { } impl Home { - pub fn new(wallet: Wallet, coins: &[Coin]) -> Self { + pub fn new(wallet: Arc, coins: &[Coin]) -> Self { Self { wallet, balance: Amount::from_sat( diff --git a/gui/src/app/state/recovery.rs b/gui/src/app/state/recovery.rs index 4933878d..ecf35c26 100644 --- a/gui/src/app/state/recovery.rs +++ b/gui/src/app/state/recovery.rs @@ -6,7 +6,6 @@ use iced::{Command, Element}; use crate::{ app::{ cache::Cache, - config::Config, error::Error, menu::Menu, message::Message, @@ -25,8 +24,7 @@ use crate::{ use liana::miniscript::bitcoin::{Address, Amount, Network}; pub struct RecoveryPanel { - wallet: Wallet, - config: Config, + wallet: Arc, locked_coins: (usize, Amount), recoverable_coins: (usize, Amount), warning: Option, @@ -38,13 +36,7 @@ pub struct RecoveryPanel { } impl RecoveryPanel { - pub fn new( - wallet: Wallet, - config: Config, - coins: &[Coin], - timelock: u32, - blockheight: u32, - ) -> Self { + pub fn new(wallet: Arc, coins: &[Coin], timelock: u32, blockheight: u32) -> Self { let mut locked_coins = (0, Amount::from_sat(0)); let mut recoverable_coins = (0, Amount::from_sat(0)); for coin in coins { @@ -61,7 +53,6 @@ impl RecoveryPanel { } Self { wallet, - config, locked_coins, recoverable_coins, warning: None, @@ -123,12 +114,7 @@ impl State for RecoveryPanel { }, Message::Recovery(res) => match res { Ok(tx) => { - self.generated = Some(detail::SpendTxState::new( - self.wallet.clone(), - self.config.clone(), - tx, - false, - )) + self.generated = Some(detail::SpendTxState::new(self.wallet.clone(), tx, false)) } Err(e) => self.warning = Some(e), }, diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index c6a34d64..6dd2f21c 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -8,8 +8,7 @@ use liana::miniscript::bitcoin::{ use crate::{ app::{ - cache::Cache, config::Config, error::Error, message::Message, view, view::spend::detail, - wallet::Wallet, + cache::Cache, error::Error, message::Message, view, view::spend::detail, wallet::Wallet, }, daemon::{ model::{SpendStatus, SpendTx}, @@ -39,19 +38,17 @@ trait Action { } pub struct SpendTxState { - wallet: Wallet, - config: Config, + wallet: Arc, tx: SpendTx, saved: bool, action: Option>, } impl SpendTxState { - pub fn new(wallet: Wallet, config: Config, tx: SpendTx, saved: bool) -> Self { + pub fn new(wallet: Arc, tx: SpendTx, saved: bool) -> Self { Self { wallet, action: None, - config, tx, saved, } @@ -80,7 +77,7 @@ impl SpendTxState { self.action = Some(Box::new(DeleteAction::default())); } view::SpendTxMessage::Sign => { - let action = SignAction::new(self.config.clone()); + let action = SignAction::new(); let cmd = action.load(&self.wallet, daemon); self.action = Some(Box::new(action)); return cmd; @@ -252,7 +249,6 @@ impl Action for DeleteAction { } pub struct SignAction { - config: Config, chosen_hw: Option, processing: bool, hws: Vec, @@ -261,9 +257,8 @@ pub struct SignAction { } impl SignAction { - pub fn new(config: Config) -> Self { + pub fn new() -> Self { Self { - config, chosen_hw: None, processing: false, hws: Vec::new(), @@ -279,13 +274,8 @@ impl Action for SignAction { } fn load(&self, wallet: &Wallet, _daemon: Arc) -> Command { - let config = self.config.clone(); - let desc = wallet.main_descriptor.to_string(); - let name = wallet.name.clone(); - Command::perform( - list_hws(config, name, desc), - Message::ConnectedHardwareWallets, - ) + let wallet = wallet.clone(); + Command::perform(list_hws(wallet), Message::ConnectedHardwareWallets) } fn update( &mut self, @@ -350,8 +340,12 @@ impl Action for SignAction { } } -async fn list_hws(config: Config, wallet_name: String, descriptor: String) -> Vec { - list_hardware_wallets(&config.hardware_wallets, Some((&wallet_name, &descriptor))).await +async fn list_hws(wallet: Wallet) -> Vec { + list_hardware_wallets( + &wallet.hardware_wallets, + Some((&wallet.name, &wallet.main_descriptor.to_string())), + ) + .await } async fn sign_psbt( diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index 84dad9d0..230167f5 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -8,10 +8,7 @@ use liana::miniscript::bitcoin::{consensus, util::psbt::Psbt}; use super::{redirect, State}; use crate::{ - app::{ - cache::Cache, config::Config, error::Error, menu::Menu, message::Message, view, - wallet::Wallet, - }, + app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet}, daemon::{ model::{Coin, SpendTx}, Daemon, @@ -20,8 +17,7 @@ use crate::{ }; pub struct SpendPanel { - wallet: Wallet, - config: Config, + wallet: Arc, selected_tx: Option, spend_txs: Vec, warning: Option, @@ -29,10 +25,9 @@ pub struct SpendPanel { } impl SpendPanel { - pub fn new(wallet: Wallet, config: Config, spend_txs: &[SpendTx]) -> Self { + pub fn new(wallet: Arc, spend_txs: &[SpendTx]) -> Self { Self { wallet, - config, spend_txs: spend_txs.to_vec(), warning: None, selected_tx: None, @@ -97,12 +92,7 @@ impl State for SpendPanel { } Message::View(view::Message::Select(i)) => { if let Some(tx) = self.spend_txs.get(i) { - let tx = detail::SpendTxState::new( - self.wallet.clone(), - self.config.clone(), - tx.clone(), - true, - ); + let tx = detail::SpendTxState::new(self.wallet.clone(), tx.clone(), true); let cmd = tx.load(daemon); self.selected_tx = Some(tx); return cmd; @@ -143,7 +133,7 @@ pub struct CreateSpendPanel { } impl CreateSpendPanel { - pub fn new(wallet: Wallet, config: Config, coins: &[Coin], blockheight: u32) -> Self { + pub fn new(wallet: Arc, coins: &[Coin], blockheight: u32) -> Self { let descriptor = wallet.main_descriptor.clone(); let timelock = descriptor.timelock_value(); Self { @@ -157,7 +147,7 @@ impl CreateSpendPanel { timelock, blockheight, )), - Box::new(step::SaveSpend::new(wallet, config)), + Box::new(step::SaveSpend::new(wallet)), ], } } diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 77b2529d..0280b990 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -12,8 +12,7 @@ use liana::{ use crate::{ app::{ - cache::Cache, config::Config, error::Error, message::Message, state::spend::detail, view, - wallet::Wallet, + cache::Cache, error::Error, message::Message, state::spend::detail, view, wallet::Wallet, }, daemon::{ model::{remaining_sequence, Coin, SpendTx}, @@ -430,16 +429,14 @@ impl Step for ChooseCoins { } pub struct SaveSpend { - wallet: Wallet, - config: Config, + wallet: Arc, spend: Option, } impl SaveSpend { - pub fn new(wallet: Wallet, config: Config) -> Self { + pub fn new(wallet: Arc) -> Self { Self { wallet, - config, spend: None, } } @@ -455,7 +452,6 @@ impl Step for SaveSpend { .unwrap(); self.spend = Some(detail::SpendTxState::new( self.wallet.clone(), - self.config.clone(), SpendTx::new(psbt, draft.inputs.clone(), sigs), false, )); diff --git a/gui/src/app/wallet.rs b/gui/src/app/wallet.rs index 7fd91089..6e8bd4a2 100644 --- a/gui/src/app/wallet.rs +++ b/gui/src/app/wallet.rs @@ -1,16 +1,46 @@ -use liana::descriptors::MultipathDescriptor; +use std::collections::HashMap; -#[derive(Clone)] +use crate::hw::HardwareWalletConfig; + +use liana::descriptors::MultipathDescriptor; +use liana::miniscript::bitcoin::util::bip32::Fingerprint; + +pub const DEFAULT_WALLET_NAME: &str = "Liana"; + +#[derive(Debug, Clone)] pub struct Wallet { pub name: String, pub main_descriptor: MultipathDescriptor, + pub keys_aliases: HashMap, + pub hardware_wallets: Vec, } impl Wallet { - pub fn new(main_descriptor: MultipathDescriptor) -> Self { + pub fn new(name: String, main_descriptor: MultipathDescriptor) -> Self { Self { - name: "Liana".to_string(), + name, main_descriptor, + keys_aliases: HashMap::new(), + hardware_wallets: Vec::new(), } } + + pub fn legacy(main_descriptor: MultipathDescriptor) -> Self { + Self { + name: DEFAULT_WALLET_NAME.to_string(), + main_descriptor, + keys_aliases: HashMap::new(), + hardware_wallets: Vec::new(), + } + } + + pub fn with_key_aliases(mut self, aliases: HashMap) -> Self { + self.keys_aliases = aliases; + self + } + + pub fn with_harware_wallets(mut self, hardware_wallets: Vec) -> Self { + self.hardware_wallets = hardware_wallets; + self + } } diff --git a/gui/src/installer/config.rs b/gui/src/installer/config.rs index 696ab269..a4dd53af 100644 --- a/gui/src/installer/config.rs +++ b/gui/src/installer/config.rs @@ -2,7 +2,7 @@ use std::convert::TryFrom; use liana::config::Config as LianaConfig; -use super::step::Context; +use super::Context; pub const DEFAULT_FILE_NAME: &str = "daemon.toml"; diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs new file mode 100644 index 00000000..91b0d687 --- /dev/null +++ b/gui/src/installer/context.rs @@ -0,0 +1,87 @@ +use std::path::PathBuf; +use std::time::Duration; + +use crate::{ + app::{ + settings::{KeySetting, Settings, WalletSetting}, + wallet::DEFAULT_WALLET_NAME, + }, + hw::HardwareWalletConfig, +}; +use async_hwi::DeviceKind; +use liana::{ + config::Config, + config::{BitcoinConfig, BitcoindConfig}, + descriptors::MultipathDescriptor, + miniscript::bitcoin, +}; + +#[derive(Clone)] +pub struct Context { + pub bitcoin_config: BitcoinConfig, + pub bitcoind_config: Option, + pub descriptor: Option, + pub keys: Vec, + pub hws: Vec<( + DeviceKind, + bitcoin::util::bip32::Fingerprint, + Option<[u8; 32]>, + )>, + pub data_dir: PathBuf, +} + +impl Context { + pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self { + Self { + bitcoin_config: BitcoinConfig { + network, + poll_interval_secs: Duration::from_secs(30), + }, + hws: Vec::new(), + keys: Vec::new(), + bitcoind_config: None, + descriptor: None, + data_dir, + } + } + + pub fn extract_gui_settings(&self) -> Settings { + let hardware_wallets = self + .hws + .iter() + .filter_map(|(kind, fingerprint, token)| { + token + .as_ref() + .map(|token| HardwareWalletConfig::new(kind, fingerprint, token)) + }) + .collect(); + Settings { + wallets: vec![WalletSetting { + name: DEFAULT_WALLET_NAME.to_string(), + descriptor_checksum: self + .descriptor + .as_ref() + .unwrap() + .to_string() + .split_once('#') + .map(|(_, checksum)| checksum) + .unwrap() + .to_string(), + keys: self.keys.clone(), + hardware_wallets, + }], + } + } + + pub fn extract_daemon_config(&self) -> Config { + Config { + #[cfg(unix)] + daemon: false, + log_level: log::LevelFilter::Info, + main_descriptor: self.descriptor.clone().unwrap(), + data_dir: Some(self.data_dir.clone()), + bitcoin_config: self.bitcoin_config.clone(), + bitcoind_config: self.bitcoind_config.clone(), + } + } +} diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 2517ea5c..390c4a5e 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -44,6 +44,7 @@ pub enum DefineDescriptor { Key(bool, usize, DefineKey), HWXpubImported(Result), XPubEdited(String), + EditName, NameEdited(String), SequenceEdited(String), ThresholdEdited(bool, usize), diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 0c0df440..dc87d294 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -1,4 +1,4 @@ -mod config; +mod context; mod message; mod prompt; mod step; @@ -7,18 +7,16 @@ mod view; use iced::{clipboard, Command, Element, Subscription}; use liana::miniscript::bitcoin; -use std::convert::TryInto; +use context::Context; use std::io::Write; use std::path::PathBuf; -use crate::{ - app::config as gui_config, hw::HardwareWalletConfig, installer::config::DEFAULT_FILE_NAME, -}; +use crate::app::{config as gui_config, settings as gui_settings}; pub use message::Message; use step::{ - BackupDescriptor, Context, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, - ParticipateXpub, RegisterDescriptor, Step, Welcome, + BackupDescriptor, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, ParticipateXpub, + RegisterDescriptor, Step, Welcome, }; pub struct Installer { @@ -164,19 +162,7 @@ impl Installer { } pub async fn install(ctx: Context) -> Result { - let hardware_wallets = ctx - .hws - .iter() - .filter_map(|(kind, fingerprint, token)| { - token - .as_ref() - .map(|token| HardwareWalletConfig::new(kind, fingerprint, token)) - }) - .collect(); - - let mut cfg: liana::config::Config = ctx - .try_into() - .expect("Everything should be checked at this point"); + let mut cfg: liana::config::Config = ctx.extract_daemon_config(); // Start Daemon to check correctness of installation let daemon = liana::DaemonHandle::start_default(cfg.clone()).map_err(|e| { Error::Unexpected(format!("Failed to start daemon with entered config: {}", e)) @@ -191,42 +177,55 @@ pub async fn install(ctx: Context) -> Result { let mut datadir_path = cfg.data_dir.clone().unwrap(); datadir_path.push(cfg.bitcoin_config.network.to_string()); - // create lianad configuration file - let mut daemon_config_path = datadir_path.clone(); - daemon_config_path.push(DEFAULT_FILE_NAME); - let mut daemon_config_file = std::fs::File::create(&daemon_config_path) - .map_err(|e| Error::CannotCreateFile(e.to_string()))?; - // Step needed because of ValueAfterTable error in the toml serialize implementation. let daemon_config = toml::Value::try_from(&cfg).expect("daemon::Config has a proper Serialize implementation"); - daemon_config_file - .write_all(daemon_config.to_string().as_bytes()) - .map_err(|e| Error::CannotWriteToFile(e.to_string()))?; + // create lianad configuration file + let daemon_config_path = create_and_write_file( + datadir_path.clone(), + "daemon.toml", + daemon_config.to_string().as_bytes(), + )?; // create liana GUI configuration file - let mut gui_config_path = datadir_path; - gui_config_path.push(gui_config::DEFAULT_FILE_NAME); - let mut gui_config_file = std::fs::File::create(&gui_config_path) - .map_err(|e| Error::CannotCreateFile(e.to_string()))?; + let gui_config_path = create_and_write_file( + datadir_path.clone(), + gui_config::DEFAULT_FILE_NAME, + toml::to_string(&gui_config::Config::new( + daemon_config_path.canonicalize().map_err(|e| { + Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e)) + })?, + )) + .unwrap() + .as_bytes(), + )?; - gui_config_file - .write_all( - toml::to_string(&gui_config::Config::new( - daemon_config_path.canonicalize().map_err(|e| { - Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e)) - })?, - hardware_wallets, - )) - .unwrap() - .as_bytes(), - ) - .map_err(|e| Error::CannotWriteToFile(e.to_string()))?; + // create liana GUI settings file + let settings: gui_settings::Settings = ctx.extract_gui_settings(); + create_and_write_file( + datadir_path, + gui_settings::DEFAULT_FILE_NAME, + serde_json::to_string_pretty(&settings).unwrap().as_bytes(), + )?; Ok(gui_config_path) } +pub fn create_and_write_file( + mut network_datadir: PathBuf, + file_name: &str, + data: &[u8], +) -> Result { + network_datadir.push(file_name); + let path = network_datadir; + let mut file = + std::fs::File::create(&path).map_err(|e| Error::CannotCreateFile(e.to_string()))?; + file.write_all(data) + .map_err(|e| Error::CannotWriteToFile(e.to_string()))?; + Ok(path) +} + #[derive(Debug, Clone)] pub enum Error { CannotCreateDatadir(String), diff --git a/gui/src/installer/prompt.rs b/gui/src/installer/prompt.rs index c7dbf98f..7d91ea48 100644 --- a/gui/src/installer/prompt.rs +++ b/gui/src/installer/prompt.rs @@ -4,3 +4,5 @@ pub const DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP: &str = "This is the keys that can spend received coins immediately,\n with no time restriction."; pub const DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP: &str = "Number of blocks after a coin is received \nfor which the recovery path is not available"; +pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str = + "The alias is applied on all the keys derived from the same seed"; diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index 5a4595e8..44777cb4 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::str::FromStr; @@ -7,7 +7,7 @@ use liana::{ descriptors::{LianaDescKeys, MultipathDescriptor}, miniscript::{ bitcoin::{ - util::bip32::{DerivationPath, Fingerprint}, + util::bip32::{ChildNumber, DerivationPath, Fingerprint}, Network, }, descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard}, @@ -15,6 +15,7 @@ use liana::{ }; use crate::{ + app::settings::KeySetting, hw::{list_hardware_wallets, HardwareWallet}, installer::{ message::{self, Message}, @@ -24,9 +25,6 @@ use crate::{ ui::component::{form, modal::Modal}, }; -const LIANA_STANDARD_PATH: &str = "m/48'/0'/0'/2'"; -const LIANA_TESTNET_STANDARD_PATH: &str = "m/48'/1'/0'/2'"; - pub trait DescriptorKeyModal { fn processing(&self) -> bool { false @@ -48,8 +46,6 @@ pub struct DefineDescriptor { sequence: form::Value, modal: Option>, - name_indexes: (usize, usize), - error: Option, } @@ -59,11 +55,10 @@ impl DefineDescriptor { network: Network::Bitcoin, data_dir: None, network_valid: true, - spending_keys: vec![DescriptorKey::new("Key 1".to_string())], + spending_keys: vec![DescriptorKey::default()], spending_threshold: 1, - recovery_keys: vec![DescriptorKey::new("Recovery key 1".to_string())], + recovery_keys: vec![DescriptorKey::default()], recovery_threshold: 1, - name_indexes: (1, 1), sequence: form::Value::default(), modal: None, error: None, @@ -79,18 +74,22 @@ impl DefineDescriptor { } // TODO: Improve algo + // Mark as duplicate every defined key that have the same name but not the same fingerprint. + // And every undefined_key that have a same name than an other key. fn check_for_duplicate(&mut self) { let mut all_keys = HashSet::new(); let mut duplicate_keys = HashSet::new(); - let mut all_names = HashSet::new(); + let mut all_names: HashMap = HashMap::new(); let mut duplicate_names = HashSet::new(); for spending_key in &self.spending_keys { - if all_names.contains(&spending_key.name) { - duplicate_names.insert(spending_key.name.clone()); - } else { - all_names.insert(spending_key.name.clone()); - } if let Some(key) = &spending_key.key { + if let Some(fg) = all_names.get(&spending_key.name) { + if fg != &key.master_fingerprint() { + duplicate_names.insert(spending_key.name.clone()); + } + } else { + all_names.insert(spending_key.name.clone(), key.master_fingerprint()); + } if all_keys.contains(key) { duplicate_keys.insert(key.clone()); } else { @@ -99,12 +98,14 @@ impl DefineDescriptor { } } for recovery_key in &self.recovery_keys { - if all_names.contains(&recovery_key.name) { - duplicate_names.insert(recovery_key.name.clone()); - } else { - all_names.insert(recovery_key.name.clone()); - } if let Some(key) = &recovery_key.key { + if let Some(fg) = all_names.get(&recovery_key.name) { + if fg != &key.master_fingerprint() { + duplicate_names.insert(recovery_key.name.clone()); + } + } else { + all_names.insert(recovery_key.name.clone(), key.master_fingerprint()); + } if all_keys.contains(key) { duplicate_keys.insert(key.clone()); } else { @@ -124,6 +125,69 @@ impl DefineDescriptor { } } } + + fn edit_alias_for_key_with_same_fingerprint(&mut self, name: String, fingerprint: Fingerprint) { + for spending_key in &mut self.spending_keys { + if spending_key.key.as_ref().map(|k| k.master_fingerprint()) == Some(fingerprint) { + spending_key.name = name.clone(); + } + } + for recovery_key in &mut self.recovery_keys { + if recovery_key.key.as_ref().map(|k| k.master_fingerprint()) == Some(fingerprint) { + recovery_key.name = name.clone(); + } + } + } + + /// Returns the maximum account index per key fingerprint + fn fingerprint_account_index_mappping(&self) -> HashMap { + let mut mapping = HashMap::new(); + let update_mapping = + |keys: &[DescriptorKey], mapping: &mut HashMap| { + for key in keys { + if let Some(DescriptorPublicKey::MultiXPub(key)) = key.key.as_ref() { + if let Some((fingerprint, derivation_path)) = key.origin.as_ref() { + let index = if derivation_path.len() >= 4 { + if derivation_path[0].to_string() == "48'" { + Some(derivation_path[2]) + } else { + None + } + } else { + None + }; + if let Some(index) = index { + if let Some(previous_index) = mapping.get(fingerprint) { + if index > *previous_index { + mapping.insert(*fingerprint, index); + } + } else { + mapping.insert(*fingerprint, index); + } + } + } + } + } + }; + update_mapping(&self.spending_keys, &mut mapping); + update_mapping(&self.recovery_keys, &mut mapping); + mapping + } + + fn keys_aliases(&self) -> HashMap { + let mut map = HashMap::new(); + for spending_key in &self.spending_keys { + if let Some(key) = spending_key.key.as_ref() { + map.insert(key.master_fingerprint(), spending_key.name.clone()); + } + } + for recovery_key in &self.recovery_keys { + if let Some(key) = recovery_key.key.as_ref() { + map.insert(key.master_fingerprint(), recovery_key.name.clone()); + } + } + map + } } impl Step for DefineDescriptor { @@ -164,16 +228,10 @@ impl Step for DefineDescriptor { } message::DefineDescriptor::AddKey(is_recovery) => { if is_recovery { - self.name_indexes.0 += 1; - self.recovery_keys.push(DescriptorKey::new(format!( - "Recovery key {}", - self.name_indexes.0, - ))); + self.recovery_keys.push(DescriptorKey::default()); self.recovery_threshold += 1; } else { - self.name_indexes.1 += 1; - self.spending_keys - .push(DescriptorKey::new(format!("Key {}", self.name_indexes.1,))); + self.spending_keys.push(DescriptorKey::default()); self.spending_threshold += 1; } } @@ -182,6 +240,10 @@ impl Step for DefineDescriptor { return Command::perform(async move { key }, Message::Clibpboard); } message::DefineKey::Edited(name, imported_key) => { + self.edit_alias_for_key_with_same_fingerprint( + name.clone(), + imported_key.master_fingerprint(), + ); if is_recovery { if let Some(recovery_key) = self.recovery_keys.get_mut(i) { recovery_key.name = name; @@ -207,8 +269,15 @@ impl Step for DefineDescriptor { k.to_string().trim_end_matches("/<0;1>/*").to_string() }) .unwrap_or_else(|| "".to_string()); - let modal = - EditXpubModal::new(name, key, i, is_recovery, self.network); + let modal = EditXpubModal::new( + name, + key, + i, + is_recovery, + self.network, + self.fingerprint_account_index_mappping(), + self.keys_aliases(), + ); let cmd = modal.load(); self.modal = Some(Box::new(modal)); return cmd; @@ -220,8 +289,15 @@ impl Step for DefineDescriptor { .as_ref() .map(|k| k.to_string().trim_end_matches("/<0;1>/*").to_string()) .unwrap_or_else(|| "".to_string()); - let modal = - EditXpubModal::new(name, key, i, is_recovery, self.network); + let modal = EditXpubModal::new( + name, + key, + i, + is_recovery, + self.network, + self.fingerprint_account_index_mappping(), + self.keys_aliases(), + ); let cmd = modal.load(); self.modal = Some(Box::new(modal)); return cmd; @@ -268,17 +344,36 @@ impl Step for DefineDescriptor { fn apply(&mut self, ctx: &mut Context) -> bool { ctx.bitcoin_config.network = self.network; - let spending_keys: Vec = self - .spending_keys - .iter() - .filter_map(|k| k.key.clone()) - .collect(); + ctx.keys = Vec::new(); + let mut spending_keys: Vec = Vec::new(); + for spending_key in self.spending_keys.iter().clone() { + if let Some(key) = spending_key.key.as_ref() { + if let DescriptorPublicKey::MultiXPub(xpub) = key { + if let Some((master_fingerprint, _)) = xpub.origin { + ctx.keys.push(KeySetting { + master_fingerprint, + name: spending_key.name.clone(), + }); + } + } + spending_keys.push(key.clone()); + } + } - let recovery_keys: Vec = self - .recovery_keys - .iter() - .filter_map(|k| k.key.clone()) - .collect(); + let mut recovery_keys: Vec = Vec::new(); + for recovery_key in self.recovery_keys.iter().clone() { + if let Some(key) = recovery_key.key.as_ref() { + if let DescriptorPublicKey::MultiXPub(xpub) = key { + if let Some((master_fingerprint, _)) = xpub.origin { + ctx.keys.push(KeySetting { + master_fingerprint, + name: recovery_key.name.clone(), + }); + } + } + recovery_keys.push(key.clone()); + } + } let sequence = self.sequence.value.parse::(); self.sequence.valid = sequence.is_ok(); @@ -378,17 +473,19 @@ pub struct DescriptorKey { pub duplicate_name: bool, } -impl DescriptorKey { - pub fn new(name: String) -> Self { +impl Default for DescriptorKey { + fn default() -> Self { Self { - name, + name: "".to_string(), valid: true, key: None, duplicate_key: false, duplicate_name: false, } } +} +impl DescriptorKey { pub fn check_network(&mut self, network: Network) { if let Some(key) = &self.key { self.valid = check_key_network(key, network); @@ -397,7 +494,7 @@ impl DescriptorKey { pub fn view(&self) -> Element { match &self.key { - None => view::undefined_descriptor_key(&self.name), + None => view::undefined_descriptor_key(), Some(_) => view::defined_descriptor_key( &self.name, self.valid, @@ -447,8 +544,12 @@ pub struct EditXpubModal { error: Option, processing: bool, + keys_aliases: HashMap, + account_indexes: HashMap, + form_name: form::Value, form_xpub: form::Value, + edit_name: bool, chosen_hw: Option, hws: Vec, @@ -461,6 +562,8 @@ impl EditXpubModal { key_index: usize, is_recovery: bool, network: Network, + account_indexes: HashMap, + keys_aliases: HashMap, ) -> Self { Self { form_name: form::Value { @@ -471,6 +574,8 @@ impl EditXpubModal { valid: true, value: key, }, + keys_aliases, + account_indexes, is_recovery, key_index, chosen_hw: None, @@ -478,6 +583,7 @@ impl EditXpubModal { hws: Vec::new(), error: None, network, + edit_name: false, } } fn load(&self) -> Command { @@ -500,8 +606,14 @@ impl DescriptorKeyModal for EditXpubModal { let device = hw.device.clone(); self.chosen_hw = Some(i); self.processing = true; + // If another account n exists, the key is retrieved for the account n+1 + let account_index = self + .account_indexes + .get(&hw.fingerprint) + .map(|account_index| account_index.increment().unwrap()) + .unwrap_or_else(|| ChildNumber::from_hardened_idx(0).unwrap()); return Command::perform( - get_extended_pubkey(device, hw.fingerprint, self.network), + get_extended_pubkey(device, hw.fingerprint, self.network, account_index), |res| { Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported( res, @@ -520,6 +632,14 @@ impl DescriptorKeyModal for EditXpubModal { self.processing = false; match res { Ok(key) => { + if let Some(alias) = self.keys_aliases.get(&key.master_fingerprint()) { + self.form_name.valid = true; + self.form_name.value = alias.clone(); + self.edit_name = false; + } else { + self.edit_name = true; + } + self.form_xpub.valid = true; self.form_xpub.value = key.to_string().trim_end_matches("/<0;1>/*").to_string(); } @@ -528,13 +648,32 @@ impl DescriptorKeyModal for EditXpubModal { } } } + Message::DefineDescriptor(message::DefineDescriptor::EditName) => { + self.edit_name = true; + } Message::DefineDescriptor(message::DefineDescriptor::NameEdited(name)) => { self.form_name.valid = true; self.form_name.value = name; } Message::DefineDescriptor(message::DefineDescriptor::XPubEdited(s)) => { - self.form_xpub.valid = - DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", s)).is_ok(); + if let Ok(DescriptorPublicKey::MultiXPub(key)) = + DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", s)) + { + if let Some((fingerprint, _)) = key.origin { + self.form_xpub.valid = true; + if let Some(alias) = self.keys_aliases.get(&fingerprint) { + self.form_name.valid = true; + self.form_name.value = alias.clone(); + self.edit_name = false; + } else { + self.edit_name = true; + } + } else { + self.form_xpub.valid = false; + } + } else { + self.form_xpub.valid = false; + } self.form_xpub.value = s; } Message::DefineDescriptor(message::DefineDescriptor::ConfirmXpub) => { @@ -570,19 +709,25 @@ impl DescriptorKeyModal for EditXpubModal { self.chosen_hw, &self.form_xpub, &self.form_name, + self.edit_name, ) } } +/// LIANA_STANDARD_PATH: m/48'/0'/0'/2'; +/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2'; async fn get_extended_pubkey( hw: std::sync::Arc, fingerprint: Fingerprint, network: Network, + account_index: ChildNumber, ) -> Result { - let derivation_path = DerivationPath::from_str(if network == Network::Bitcoin { - LIANA_STANDARD_PATH - } else { - LIANA_TESTNET_STANDARD_PATH + let derivation_path = DerivationPath::from_str(&{ + if network == Network::Bitcoin { + format!("m/48'/0'/{}/2'", account_index) + } else { + format!("m/48'/1'/{}/2'", account_index) + } }) .unwrap(); let xkey = hw @@ -667,7 +812,12 @@ impl Step for ParticipateXpub { self.processing = true; self.error = None; return Command::perform( - get_extended_pubkey(device, hw.fingerprint, self.network), + get_extended_pubkey( + device, + hw.fingerprint, + self.network, + ChildNumber::from_hardened_idx(0).unwrap(), + ), Message::ImportXpub, ); } diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index f425d907..198cce20 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -5,19 +5,14 @@ pub use descriptor::{ use std::path::PathBuf; use std::str::FromStr; -use std::time::Duration; -use async_hwi::DeviceKind; use iced::{Command, Element}; -use liana::{ - config::{BitcoinConfig, BitcoindConfig}, - descriptors::MultipathDescriptor, - miniscript::bitcoin, -}; +use liana::{config::BitcoindConfig, miniscript::bitcoin}; use crate::ui::component::form; use crate::installer::{ + context::Context, message::{self, Message}, view, }; @@ -39,34 +34,6 @@ pub trait Step { } } -#[derive(Clone)] -pub struct Context { - pub bitcoin_config: BitcoinConfig, - pub bitcoind_config: Option, - pub descriptor: Option, - pub hws: Vec<( - DeviceKind, - bitcoin::util::bip32::Fingerprint, - Option<[u8; 32]>, - )>, - pub data_dir: PathBuf, -} - -impl Context { - pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self { - Self { - bitcoin_config: BitcoinConfig { - network, - poll_interval_secs: Duration::from_secs(30), - }, - hws: Vec::new(), - bitcoind_config: None, - descriptor: None, - data_dir, - } - } -} - #[derive(Default)] pub struct Welcome {} diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index a22ab1da..18341093 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -8,9 +8,9 @@ use liana::miniscript::bitcoin; use crate::{ hw::HardwareWallet, installer::{ + context::Context, message::{self, Message}, - step::Context, - Error, + prompt, Error, }, ui::{ color, @@ -171,9 +171,7 @@ pub fn define_descriptor<'a>( .spacing(10) .push(Space::with_width(Length::Units(40))) .push(text("Primary path:").bold()) - .push(tooltip( - super::prompt::DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP, - )), + .push(tooltip(prompt::DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP)), ) .push(separation().width(Length::Fill)) .push( @@ -294,7 +292,7 @@ pub fn define_descriptor<'a>( Row::new() .spacing(10) .push(text("Blocks before recovery:").bold()) - .push(tooltip(super::prompt::DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP)), + .push(tooltip(prompt::DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP)), ) .push( Container::new( @@ -319,7 +317,7 @@ pub fn define_descriptor<'a>( layout( progress, Column::new() - .push(Space::with_height(Length::Units(50))) + .push(Space::with_height(Length::Units(30))) .push(text("Create the wallet").bold().size(50)) .push( Column::new() @@ -620,7 +618,7 @@ pub fn backup_descriptor<'a>( ) .push( Column::new() - .push(text(super::prompt::BACKUP_DESCRIPTOR_MESSAGE)) + .push(text(prompt::BACKUP_DESCRIPTOR_MESSAGE)) .push(collapse::Collapse::new( || { Button::new( @@ -680,7 +678,7 @@ pub fn backup_descriptor<'a>( } pub fn help_backup<'a>() -> Element<'a, Message> { - text(super::prompt::BACKUP_DESCRIPTOR_HELP).small().into() + text(prompt::BACKUP_DESCRIPTOR_HELP).small().into() } pub fn define_bitcoin<'a>( @@ -854,7 +852,7 @@ pub fn install<'a>( layout(progress, col) } -pub fn undefined_descriptor_key(name: &str) -> Element { +pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> { card::simple( Column::new() .width(Length::Fill) @@ -875,8 +873,13 @@ pub fn undefined_descriptor_key(name: &str) -> Element { .spacing(15) .align_items(Alignment::Center) .push( - Scrollable::new(text(name).bold()) - .horizontal_scroll(Properties::new().width(2).scroller_width(2)), + Scrollable::new( + icon::key_icon() + .style(color::DARK_GREY) + .size(50) + .width(Length::Units(50)), + ) + .horizontal_scroll(Properties::new().width(2).scroller_width(2)), ) .push(icon::circle_check_icon().style(color::FOREGROUND).size(50)), ) @@ -884,8 +887,7 @@ pub fn undefined_descriptor_key(name: &str) -> Element { .align_y(alignment::Vertical::Center), ) .push( - button::border(Some(icon::pencil_icon()), "Edit") - .on_press(message::DefineKey::Edit), + button::border(Some(icon::pencil_icon()), "Set").on_press(message::DefineKey::Edit), ) .push(Space::with_height(Length::Units(5))), ) @@ -967,7 +969,7 @@ pub fn defined_descriptor_key( .height(Length::Units(200)) .width(Length::Units(200)), ) - .push(text("Key is a duplicate").small().style(color::ALERT)) + .push(text("Duplicate key").small().style(color::ALERT)) .into() } else if duplicate_name { Column::new() @@ -978,7 +980,7 @@ pub fn defined_descriptor_key( .height(Length::Units(200)) .width(Length::Units(200)), ) - .push(text("Name is a duplicate").small().style(color::ALERT)) + .push(text("Duplicate name").small().style(color::ALERT)) .into() } else { card::simple(col) @@ -989,6 +991,7 @@ pub fn defined_descriptor_key( } } +#[allow(clippy::too_many_arguments)] pub fn edit_key_modal<'a>( network: bitcoin::Network, hws: &[HardwareWallet], @@ -996,62 +999,14 @@ pub fn edit_key_modal<'a>( processing: bool, chosen_hw: Option, form_xpub: &form::Value, - form_name: &form::Value, + form_name: &'a form::Value, + edit_name: bool, ) -> Element<'a, Message> { Column::new() .push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string()))) .push(card::simple( Column::new() .spacing(25) - .push( - Container::new( - Row::new() - .spacing(5) - .push(icon::pencil_icon()) - .push(text("Edit")), - ) - .width(Length::Fill) - .align_x(alignment::Horizontal::Center), - ) - .push( - Column::new() - .spacing(5) - .push(text("Edit name:").bold()) - .push( - form::Form::new("Name", form_name, |msg| { - Message::DefineDescriptor(message::DefineDescriptor::NameEdited( - msg, - )) - }) - .warning("Please enter correct name") - .size(20) - .padding(10), - ), - ) - .push( - Column::new() - .spacing(5) - .push(text("Enter an extended public key:").bold()) - .push( - Row::new() - .push( - form::Form::new("Extended public key", form_xpub, |msg| { - Message::DefineDescriptor( - message::DefineDescriptor::XPubEdited(msg), - ) - }) - .warning(if network == bitcoin::Network::Bitcoin { - "Please enter correct xpub" - } else { - "Please enter correct tpub" - }) - .size(20) - .padding(10), - ) - .spacing(10) - .push(Container::new(text("/<0;1>/*")).padding(5)), - ), - ) .push(if !hws.is_empty() { Column::new() .push( @@ -1059,7 +1014,7 @@ pub fn edit_key_modal<'a>( .spacing(10) .align_items(Alignment::Center) .push( - Container::new(text("Or select a hardware wallet:").bold()) + Container::new(text("Select a hardware wallet:").bold()) .width(Length::Fill), ) .push( @@ -1100,6 +1055,75 @@ pub fn edit_key_modal<'a>( ) .width(Length::Fill) }) + .push( + Column::new() + .spacing(5) + .push(text("Or enter an extended public key:").bold()) + .push( + Row::new() + .push( + form::Form::new("Extended public key", form_xpub, |msg| { + Message::DefineDescriptor( + message::DefineDescriptor::XPubEdited(msg), + ) + }) + .warning(if network == bitcoin::Network::Bitcoin { + "Please enter correct xpub with origin" + } else { + "Please enter correct tpub with origin" + }) + .size(20) + .padding(10), + ) + .spacing(10) + .push(Container::new(text("/<0;1>/*")).padding(5)), + ), + ) + .push( + if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid { + Column::new().push( + Row::new() + .push( + Column::new() + .spacing(5) + .width(Length::Fill) + .push( + Row::new() + .spacing(5) + .push(text("Fingerprint alias:").bold()) + .push(tooltip( + prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP, + )), + ) + .push(text(&form_name.value)), + ) + .push(button::border(Some(icon::pencil_icon()), "Edit").on_press( + Message::DefineDescriptor(message::DefineDescriptor::EditName), + )), + ) + } else if !form_xpub.value.is_empty() && form_xpub.valid { + Column::new() + .spacing(5) + .push( + Row::new() + .spacing(5) + .push(text("Fingerprint alias:").bold()) + .push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)), + ) + .push( + form::Form::new("Alias", form_name, |msg| { + Message::DefineDescriptor( + message::DefineDescriptor::NameEdited(msg), + ) + }) + .warning("Please enter correct alias") + .size(20) + .padding(10), + ) + } else { + Column::new() + }, + ) .push( if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty() { diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 558d647a..477bf8e7 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -17,7 +17,12 @@ use liana::{ }; use crate::{ - app::config::{default_datadir, Config as GUIConfig}, + app::{ + cache::Cache, + config::{default_datadir, Config as GUIConfig}, + settings::{self, Settings}, + wallet::Wallet, + }, daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError}, ui::{ component::{button, notification, text::*}, @@ -30,6 +35,7 @@ type Lianad = client::Lianad; pub struct Loader { pub datadir_path: Option, + pub network: bitcoin::Network, pub gui_config: GUIConfig, pub daemon_started: bool, @@ -47,16 +53,12 @@ pub enum Step { Error(Box), } +#[allow(clippy::type_complexity)] #[derive(Debug)] pub enum Message { View(ViewMessage), Syncing(Result), - Synced( - GetInfoResult, - Vec, - Vec, - Arc, - ), + Synced(Result<(Arc, Cache, Arc), Error>), Started(Result, Error>), Loaded(Result, Error>), Failure(DaemonError), @@ -75,6 +77,7 @@ impl Loader { .unwrap(); ( Loader { + network: daemon_config.bitcoin_config.network, datadir_path, daemon_config: daemon_config.clone(), gui_config, @@ -141,18 +144,47 @@ impl Loader { Ok(info) => { if (info.sync - 1.0_f64).abs() < f64::EPSILON { let daemon = daemon.clone(); + let settings_path = + settings_path(&self.datadir_path, self.network).unwrap(); + let gui_config_hws = self + .gui_config + .hardware_wallets + .as_ref() + .cloned() + .unwrap_or_default(); return Command::perform( async move { - let coins = daemon - .list_coins() - .map(|res| res.coins) - .unwrap_or_else(|_| Vec::new()); - let spend_txs = daemon - .list_spend_transactions() - .unwrap_or_else(|_| Vec::new()); - (info, coins, spend_txs, daemon) + let coins = daemon.list_coins().map(|res| res.coins)?; + let spend_txs = daemon.list_spend_transactions()?; + let cache = Cache { + network: info.network, + blockheight: info.block_height, + coins, + spend_txs, + ..Default::default() + }; + let wallet = match Settings::from_file(&settings_path) { + Ok(settings) => { + if let Some(wallet_setting) = settings.wallets.first() { + Wallet::legacy(info.descriptors.main) + .with_harware_wallets( + wallet_setting.hardware_wallets.clone(), + ) + .with_key_aliases(wallet_setting.keys_aliases()) + } else { + Wallet::legacy(info.descriptors.main) + .with_harware_wallets(gui_config_hws) + } + } + Err(settings::SettingsError::NotFound) => { + Wallet::legacy(info.descriptors.main) + .with_harware_wallets(gui_config_hws) + } + Err(e) => return Err(e.into()), + }; + Ok((Arc::new(wallet), cache, daemon)) }, - |res| Message::Synced(res.0, res.1, res.2, res.3), + Message::Synced, ); } else { *progress = info.sync @@ -333,6 +365,7 @@ async fn sync( #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum Error { + Settings(settings::SettingsError), Config(ConfigError), Daemon(DaemonError), } @@ -340,12 +373,19 @@ pub enum Error { impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { + Self::Settings(e) => write!(f, "Settings error: {}", e), Self::Config(e) => write!(f, "Config error: {}", e), Self::Daemon(e) => write!(f, "Liana daemon error: {}", e), } } } +impl From for Error { + fn from(error: settings::SettingsError) -> Self { + Error::Settings(error) + } +} + impl From for Error { fn from(error: ConfigError) -> Self { Error::Config(error) @@ -372,3 +412,18 @@ fn socket_path( path.push("lianad_rpc"); Ok(path) } + +/// default liana settings path is .liana/bitcoin/settings.json +fn settings_path( + datadir: &Option, + network: bitcoin::Network, +) -> Result { + let mut path = if let Some(ref datadir) = datadir { + datadir.clone() + } else { + default_datadir().map_err(|_| ConfigError::DatadirNotFound)? + }; + path.push(network.to_string()); + path.push(settings::DEFAULT_FILE_NAME); + Ok(path) +} diff --git a/gui/src/main.rs b/gui/src/main.rs index 917aaf1b..397891b3 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -11,9 +11,7 @@ use liana::{config::Config as DaemonConfig, miniscript::bitcoin}; use liana_gui::{ app::{ self, - cache::Cache, config::{default_datadir, ConfigError}, - wallet::Wallet, App, }, installer::{self, Installer}, @@ -205,17 +203,7 @@ impl Application for GUI { ))); Command::none() } - loader::Message::Synced(info, coins, spend_txs, daemon) => { - let cache = Cache { - network: info.network, - blockheight: info.block_height, - coins, - spend_txs, - ..Default::default() - }; - - let wallet = Wallet::new(info.descriptors.main); - + loader::Message::Synced(Ok((wallet, cache, daemon))) => { let (app, command) = App::new(cache, wallet, loader.gui_config.clone(), daemon); self.state = State::App(app); command.map(|msg| Message::Run(Box::new(msg)))