From ae8df0dd4ca8e521207edc08bd630661b559cd66 Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 15 Mar 2023 18:29:01 +0100 Subject: [PATCH] gui: separate settings panels, add wallet settings --- gui/src/app/error.rs | 25 +- gui/src/app/message.rs | 7 +- gui/src/app/mod.rs | 30 +- gui/src/app/settings.rs | 55 ++- gui/src/app/state/mod.rs | 10 +- .../{settings.rs => settings/bitcoind.rs} | 82 ++--- gui/src/app/state/settings/mod.rs | 158 +++++++++ gui/src/app/state/settings/wallet.rs | 258 ++++++++++++++ gui/src/app/state/spend/detail.rs | 17 +- gui/src/app/view/hw.rs | 28 +- gui/src/app/view/message.rs | 21 +- gui/src/app/view/settings.rs | 320 +++++++++++++++--- gui/src/app/view/spend/detail.rs | 10 +- gui/src/app/view/warning.rs | 2 +- gui/src/app/wallet.rs | 103 +++++- gui/src/hw.rs | 10 +- gui/src/installer/context.rs | 2 +- gui/src/loader.rs | 71 +--- gui/src/main.rs | 8 +- gui/src/ui/icon.rs | 4 + 20 files changed, 982 insertions(+), 239 deletions(-) rename gui/src/app/state/{settings.rs => settings/bitcoind.rs} (80%) create mode 100644 gui/src/app/state/settings/mod.rs create mode 100644 gui/src/app/state/settings/wallet.rs diff --git a/gui/src/app/error.rs b/gui/src/app/error.rs index ff6d6457..76b05858 100644 --- a/gui/src/app/error.rs +++ b/gui/src/app/error.rs @@ -1,21 +1,27 @@ -use crate::daemon::DaemonError; -use liana::config::ConfigError; use std::convert::From; use std::io::ErrorKind; +use liana::config::ConfigError; + +use crate::{ + app::{settings::SettingsError, wallet::WalletError}, + daemon::DaemonError, +}; + #[derive(Debug)] pub enum Error { Config(String), + Wallet(WalletError), Daemon(DaemonError), Unexpected(String), HardwareWallet(async_hwi::Error), - HotSigner(String), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Config(e) => write!(f, "{}", e), + Self::Wallet(e) => write!(f, "{}", e), Self::Daemon(e) => match e { DaemonError::Unexpected(e) => write!(f, "{}", e), DaemonError::NoAnswer => write!(f, "Daemon did not answer"), @@ -41,7 +47,6 @@ impl std::fmt::Display for Error { }, Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), Self::HardwareWallet(e) => write!(f, "{}", e), - Self::HotSigner(e) => write!(f, "{}", e), } } } @@ -52,6 +57,18 @@ impl From for Error { } } +impl From for Error { + fn from(error: WalletError) -> Self { + Error::Wallet(error) + } +} + +impl From for Error { + fn from(error: SettingsError) -> Self { + Error::Wallet(WalletError::Settings(error)) + } +} + impl From for Error { fn from(error: DaemonError) -> Self { Error::Daemon(error) diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index bffe8e7c..e2c7d461 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use liana::{ config::Config as DaemonConfig, miniscript::bitcoin::{ @@ -7,7 +9,7 @@ use liana::{ }; use crate::{ - app::{error::Error, view}, + app::{error::Error, view, wallet::Wallet}, daemon::model::*, hw::HardwareWallet, }; @@ -18,6 +20,8 @@ pub enum Message { View(view::Message), LoadDaemonConfig(Box), DaemonConfigLoaded(Result<(), Error>), + LoadWallet, + WalletLoaded(Result, Error>), Info(Result), ReceiveAddress(Result), Coins(Result, Error>), @@ -25,6 +29,7 @@ pub enum Message { Psbt(Result), Recovery(Result), Signed(Result<(Psbt, Fingerprint), Error>), + WalletRegistered(Result), Updated(Result<(), Error>), Saved(Result<(), Error>), StartRescan(Result<(), Error>), diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 7b46bcf4..1d31edca 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -18,7 +18,7 @@ use std::time::Duration; use iced::{clipboard, time, Command, Element, Subscription}; use tracing::{info, warn}; -pub use liana::config::Config as DaemonConfig; +pub use liana::{config::Config as DaemonConfig, miniscript::bitcoin}; pub use config::Config; pub use message::Message; @@ -31,6 +31,7 @@ use crate::{ }; pub struct App { + data_dir: PathBuf, state: Box, cache: Cache, config: Config, @@ -44,11 +45,13 @@ impl App { wallet: Arc, config: Config, daemon: Arc, + data_dir: PathBuf, ) -> (App, Command) { let state: Box = Home::new(wallet.clone(), &cache.coins).into(); let cmd = state.load(daemon.clone()); ( Self { + data_dir, state, cache, config, @@ -61,12 +64,9 @@ impl App { fn load_state(&mut self, menu: &Menu) -> Command { self.state = match menu { - menu::Menu::Settings => state::SettingsState::new( - self.daemon.config().cloned(), - &self.cache, - self.daemon.is_external(), - ) - .into(), + menu::Menu::Settings => { + state::SettingsState::new(self.data_dir.clone(), self.wallet.clone()).into() + } menu::Menu::Home => Home::new(self.wallet.clone(), &self.cache.coins).into(), menu::Menu::Coins => CoinsPanel::new( &self.cache.coins, @@ -145,6 +145,10 @@ impl App { let res = self.load_daemon_config(&path, *cfg); self.update(Message::DaemonConfigLoaded(res)) } + Message::LoadWallet => { + let res = self.load_wallet(); + self.update(Message::WalletLoaded(res)) + } Message::View(view::Message::Menu(menu)) => self.load_state(&menu), Message::View(view::Message::Clipboard(text)) => clipboard::write(text), _ => self.state.update(self.daemon.clone(), &self.cache, message), @@ -181,6 +185,18 @@ impl App { Ok(()) } + pub fn load_wallet(&mut self) -> Result, Error> { + let wallet = Wallet::new(self.wallet.main_descriptor.clone()).load_settings( + &self.config, + &self.data_dir, + self.cache.network, + )?; + + self.wallet = Arc::new(wallet); + + Ok(self.wallet.clone()) + } + pub fn view(&self) -> Element { self.state.view(&self.cache).map(Message::View) } diff --git a/gui/src/app/settings.rs b/gui/src/app/settings.rs index 3f917c92..e76dc2f7 100644 --- a/gui/src/app/settings.rs +++ b/gui/src/app/settings.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; -use std::path::Path; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::PathBuf; -use liana::miniscript::bitcoin::util::bip32::Fingerprint; +use liana::miniscript::bitcoin::{util::bip32::Fingerprint, Network}; use serde::{Deserialize, Serialize}; -use crate::hw::HardwareWalletConfig; +use crate::{app::wallet::Wallet, hw::HardwareWalletConfig}; ///! Settings is the module to handle the GUI settings file. ///! The settings file is used by the GUI to store useful information. @@ -16,7 +18,11 @@ pub struct Settings { } impl Settings { - pub fn from_file(path: &Path) -> Result { + pub fn from_file(datadir: PathBuf, network: Network) -> Result { + let mut path = datadir; + path.push(network.to_string()); + path.push(DEFAULT_FILE_NAME); + let config = std::fs::read(path) .map_err(|e| match e.kind() { std::io::ErrorKind::NotFound => SettingsError::NotFound, @@ -29,6 +35,26 @@ impl Settings { })?; Ok(config) } + + pub fn to_file(&self, datadir: PathBuf, network: Network) -> Result<(), SettingsError> { + let mut path = datadir; + path.push(network.to_string()); + path.push(DEFAULT_FILE_NAME); + + let content = serde_json::to_string_pretty(&self).map_err(|e| { + SettingsError::WritingFile(format!("Failed to serialize settings: {}", e)) + })?; + + let mut settings_file = OpenOptions::new() + .write(true) + .open(path) + .map_err(|e| SettingsError::WritingFile(e.to_string()))?; + + settings_file.write_all(content.as_bytes()).map_err(|e| { + tracing::warn!("failed to write to file: {:?}", e); + SettingsError::WritingFile(e.to_string()) + }) + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -51,6 +77,25 @@ impl WalletSetting { } } +impl From<&Wallet> for WalletSetting { + fn from(w: &Wallet) -> WalletSetting { + Self { + name: w.name.clone(), + hardware_wallets: w.hardware_wallets.clone(), + keys: w + .keys_aliases + .clone() + .into_iter() + .map(|(master_fingerprint, name)| KeySetting { + name, + master_fingerprint, + }) + .collect(), + descriptor_checksum: w.descriptor_checksum(), + } + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct KeySetting { pub name: String, @@ -61,6 +106,7 @@ pub struct KeySetting { pub enum SettingsError { NotFound, ReadingFile(String), + WritingFile(String), Unexpected(String), } @@ -69,6 +115,7 @@ impl std::fmt::Display for SettingsError { match self { Self::NotFound => write!(f, "Settings file not found"), Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e), + Self::WritingFile(e) => write!(f, "Error while writing 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 a6fc78dc..37c8e83c 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -26,10 +26,12 @@ pub trait State { fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>; fn update( &mut self, - daemon: Arc, - cache: &Cache, - message: Message, - ) -> Command; + _daemon: Arc, + _cache: &Cache, + _message: Message, + ) -> Command { + Command::none() + } fn subscription(&self) -> Subscription { Subscription::none() } diff --git a/gui/src/app/state/settings.rs b/gui/src/app/state/settings/bitcoind.rs similarity index 80% rename from gui/src/app/state/settings.rs rename to gui/src/app/state/settings/bitcoind.rs index 24fd3c26..de5fe3e1 100644 --- a/gui/src/app/state/settings.rs +++ b/gui/src/app/state/settings/bitcoind.rs @@ -11,34 +11,22 @@ use tracing::info; use liana::config::{BitcoinConfig, BitcoindConfig, Config}; use crate::{ - app::{cache::Cache, error::Error, message::Message, state::State, view}, + app::{cache::Cache, error::Error, message::Message, state::settings::Setting, view, State}, daemon::Daemon, ui::component::form, }; -trait Setting: std::fmt::Debug { - fn edited(&mut self, success: bool); - fn update( - &mut self, - daemon: Arc, - cache: &Cache, - message: view::SettingsMessage, - ) -> Command; - fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsMessage>; -} - #[derive(Debug)] -pub struct SettingsState { +pub struct BitcoindSettingsState { warning: Option, config_updated: bool, daemon_is_external: bool, - daemon_version: Option, settings: Vec>, current: Option, } -impl SettingsState { +impl BitcoindSettingsState { pub fn new(config: Option, cache: &Cache, daemon_is_external: bool) -> Self { let settings = if let Some(config) = &config { vec![ @@ -53,12 +41,7 @@ impl SettingsState { vec![RescanSetting::new(cache.rescan_progress).into()] }; - SettingsState { - daemon_version: if !daemon_is_external { - Some(liana::VERSION.to_string()) - } else { - None - }, + BitcoindSettingsState { daemon_is_external, warning: None, config_updated: false, @@ -69,7 +52,7 @@ impl SettingsState { } } -impl State for SettingsState { +impl State for BitcoindSettingsState { fn update( &mut self, daemon: Arc, @@ -101,17 +84,16 @@ impl State for SettingsState { Message::Info(res) => match res { Err(e) => self.warning = Some(e), Ok(info) => { - self.daemon_version = Some(info.version); if info.rescan_progress == Some(1.0) { self.settings[1].edited(true); } } }, - Message::View(view::Message::Settings(i, msg)) => { + Message::View(view::Message::Settings(view::SettingsMessage::Edit(i, msg))) => { if let Some(setting) = self.settings.get_mut(i) { match msg { - view::SettingsMessage::Edit => self.current = Some(i), - view::SettingsMessage::CancelEdit => self.current = None, + view::SettingsEditMessage::Select => self.current = Some(i), + view::SettingsEditMessage::Cancel => self.current = None, _ => {} }; return setting.update(daemon, cache, msg); @@ -124,36 +106,24 @@ impl State for SettingsState { fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { let can_edit = self.current.is_none() && !self.daemon_is_external; - view::settings::list( - self.daemon_version.as_ref(), + view::settings::bitcoind_settings( cache, self.warning.as_ref(), self.settings .iter() .enumerate() .map(|(i, setting)| { - setting - .view(cache, can_edit) - .map(move |msg| view::Message::Settings(i, msg)) + setting.view(cache, can_edit).map(move |msg| { + view::Message::Settings(view::SettingsMessage::Edit(i, msg)) + }) }) .collect(), ) } - - fn load(&self, daemon: Arc) -> Command { - if self.daemon_version.is_none() { - Command::perform( - async move { daemon.get_info().map_err(|e| e.into()) }, - Message::Info, - ) - } else { - Command::none() - } - } } -impl From for Box { - fn from(s: SettingsState) -> Box { +impl From for Box { + fn from(s: BitcoindSettingsState) -> Box { Box::new(s) } } @@ -207,20 +177,20 @@ impl Setting for BitcoindSettings { &mut self, daemon: Arc, _cache: &Cache, - message: view::SettingsMessage, + message: view::SettingsEditMessage, ) -> Command { match message { - view::SettingsMessage::Edit => { + view::SettingsEditMessage::Select => { if !self.processing { self.edit = true; } } - view::SettingsMessage::CancelEdit => { + view::SettingsEditMessage::Cancel => { if !self.processing { self.edit = false; } } - view::SettingsMessage::FieldEdited(field, value) => { + view::SettingsEditMessage::FieldEdited(field, value) => { if !self.processing { match field { "socket_address" => self.addr.value = value, @@ -229,7 +199,7 @@ impl Setting for BitcoindSettings { } } } - view::SettingsMessage::ConfirmEdit => { + view::SettingsEditMessage::Confirm => { let new_addr = SocketAddr::from_str(&self.addr.value); self.addr.valid = new_addr.is_ok(); let new_path = PathBuf::from_str(&self.cookie_path.value); @@ -251,7 +221,7 @@ impl Setting for BitcoindSettings { Command::none() } - fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsMessage> { + fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> { if self.edit { view::settings::bitcoind_edit( self.bitcoin_config.network, @@ -307,20 +277,20 @@ impl Setting for RescanSetting { &mut self, daemon: Arc, _cache: &Cache, - message: view::SettingsMessage, + message: view::SettingsEditMessage, ) -> Command { match message { - view::SettingsMessage::Edit => { + view::SettingsEditMessage::Select => { if !self.processing { self.edit = true; } } - view::SettingsMessage::CancelEdit => { + view::SettingsEditMessage::Cancel => { if !self.processing { self.edit = false; } } - view::SettingsMessage::FieldEdited(field, value) => { + view::SettingsEditMessage::FieldEdited(field, value) => { if !self.processing && (value.is_empty() || u32::from_str(&value).is_ok()) { match field { "rescan_year" => self.year.value = value, @@ -330,7 +300,7 @@ impl Setting for RescanSetting { } } } - view::SettingsMessage::ConfirmEdit => { + view::SettingsEditMessage::Confirm => { let date_time = NaiveDate::from_ymd( i32::from_str(&self.year.value).unwrap_or(1), u32::from_str(&self.month.value).unwrap_or(1), @@ -349,7 +319,7 @@ impl Setting for RescanSetting { Command::none() } - fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsMessage> { + fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> { view::settings::rescan( &self.year, &self.month, diff --git a/gui/src/app/state/settings/mod.rs b/gui/src/app/state/settings/mod.rs new file mode 100644 index 00000000..1dfe931b --- /dev/null +++ b/gui/src/app/state/settings/mod.rs @@ -0,0 +1,158 @@ +mod bitcoind; +mod wallet; + +use std::convert::From; +use std::path::PathBuf; +use std::sync::Arc; + +use iced::{Command, Element}; + +use bitcoind::BitcoindSettingsState; +use wallet::WalletSettingsState; + +use crate::{ + app::{cache::Cache, error::Error, message::Message, state::State, view, wallet::Wallet}, + daemon::Daemon, +}; + +trait Setting: std::fmt::Debug { + fn edited(&mut self, success: bool); + fn update( + &mut self, + daemon: Arc, + cache: &Cache, + message: view::SettingsEditMessage, + ) -> Command; + fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage>; +} + +pub struct SettingsState { + data_dir: PathBuf, + wallet: Arc, + setting: Option>, +} + +impl SettingsState { + pub fn new(data_dir: PathBuf, wallet: Arc) -> Self { + Self { + data_dir, + wallet, + setting: None, + } + } +} + +impl State for SettingsState { + fn update( + &mut self, + daemon: Arc, + cache: &Cache, + message: Message, + ) -> Command { + match &message { + Message::View(view::Message::Settings(view::SettingsMessage::EditBitcoindSettings)) => { + self.setting = Some( + BitcoindSettingsState::new( + daemon.config().cloned(), + cache, + daemon.is_external(), + ) + .into(), + ); + self.setting + .as_mut() + .map(|s| s.load(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)) + .unwrap_or_else(Command::none) + } + Message::View(view::Message::Settings(view::SettingsMessage::EditWalletSettings)) => { + self.setting = Some( + WalletSettingsState::new(self.data_dir.clone(), self.wallet.clone()).into(), + ); + self.setting + .as_mut() + .map(|s| s.load(daemon)) + .unwrap_or_else(Command::none) + } + _ => self + .setting + .as_mut() + .map(|s| s.update(daemon, cache, message)) + .unwrap_or_else(Command::none), + } + } + + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { + if let Some(setting) = &self.setting { + setting.view(cache) + } else { + view::settings::list(cache) + } + } +} + +impl From for Box { + fn from(s: SettingsState) -> Box { + Box::new(s) + } +} + +#[derive(Default)] +pub struct AboutSettingsState { + daemon_version: Option, + warning: Option, +} + +impl AboutSettingsState { + pub fn new(daemon_is_external: bool) -> Self { + AboutSettingsState { + daemon_version: if !daemon_is_external { + Some(liana::VERSION.to_string()) + } else { + None + }, + warning: None, + } + } +} + +impl State for AboutSettingsState { + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { + view::settings::about_section(cache, self.warning.as_ref(), self.daemon_version.as_ref()) + } + + fn update( + &mut self, + _daemon: Arc, + _cache: &Cache, + message: Message, + ) -> Command { + if let Message::Info(res) = message { + match res { + Ok(info) => self.daemon_version = Some(info.version), + Err(e) => self.warning = Some(e), + } + } + + Command::none() + } + + fn load(&self, daemon: Arc) -> Command { + Command::perform( + async move { daemon.get_info().map_err(|e| e.into()) }, + Message::Info, + ) + } +} + +impl From for Box { + fn from(s: AboutSettingsState) -> Box { + Box::new(s) + } +} diff --git a/gui/src/app/state/settings/wallet.rs b/gui/src/app/state/settings/wallet.rs new file mode 100644 index 00000000..5b6dac60 --- /dev/null +++ b/gui/src/app/state/settings/wallet.rs @@ -0,0 +1,258 @@ +use std::collections::HashSet; +use std::convert::From; +use std::path::PathBuf; +use std::sync::Arc; + +use iced::{Command, Element}; + +use liana::miniscript::bitcoin::{hashes::hex::ToHex, util::bip32::Fingerprint, Network}; + +use crate::{ + app::{ + cache::Cache, error::Error, message::Message, settings, state::State, view, wallet::Wallet, + }, + daemon::Daemon, + hw::{list_hardware_wallets, HardwareWallet, HardwareWalletConfig}, + ui::component::modal, +}; + +pub struct WalletSettingsState { + data_dir: PathBuf, + warning: Option, + descriptor: String, + wallet: Arc, + modal: Option, +} + +impl WalletSettingsState { + pub fn new(data_dir: PathBuf, wallet: Arc) -> Self { + WalletSettingsState { + data_dir, + descriptor: wallet.main_descriptor.to_string(), + wallet, + warning: None, + modal: None, + } + } +} + +impl State for WalletSettingsState { + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { + let content = + view::settings::wallet_settings(cache, self.warning.as_ref(), &self.descriptor); + if let Some(m) = &self.modal { + modal::Modal::new(content, m.view()) + .on_blur(Some(view::Message::Close)) + .into() + } else { + content + } + } + + fn update( + &mut self, + daemon: Arc, + cache: &Cache, + message: Message, + ) -> Command { + match message { + Message::WalletLoaded(res) => { + match res { + Ok(wallet) => { + if let Some(modal) = &mut self.modal { + modal.wallet = wallet.clone(); + } + self.wallet = wallet; + } + Err(e) => self.warning = Some(e), + }; + Command::none() + } + Message::View(view::Message::Close) => { + self.modal = None; + Command::none() + } + Message::View(view::Message::Settings(view::SettingsMessage::RegisterWallet)) => { + self.modal = Some(RegisterWalletModal::new( + self.data_dir.clone(), + self.wallet.clone(), + )); + self.modal + .as_ref() + .map(|m| m.load(daemon)) + .unwrap_or_else(Command::none) + } + _ => self + .modal + .as_mut() + .map(|m| m.update(daemon, cache, message)) + .unwrap_or_else(Command::none), + } + } + + fn load(&self, daemon: Arc) -> Command { + Command::perform( + async move { daemon.get_info().map_err(|e| e.into()) }, + Message::Info, + ) + } +} + +impl From for Box { + fn from(s: WalletSettingsState) -> Box { + Box::new(s) + } +} + +pub struct RegisterWalletModal { + data_dir: PathBuf, + wallet: Arc, + warning: Option, + chosen_hw: Option, + hws: Vec, + registered: HashSet, + processing: bool, +} + +impl RegisterWalletModal { + pub fn new(data_dir: PathBuf, wallet: Arc) -> Self { + let mut registered = HashSet::new(); + for hw in &wallet.hardware_wallets { + registered.insert(hw.fingerprint); + } + Self { + data_dir, + wallet, + warning: None, + chosen_hw: None, + hws: Vec::new(), + processing: false, + registered, + } + } +} + +impl RegisterWalletModal { + fn view(&self) -> Element { + view::settings::register_wallet_modal( + self.warning.as_ref(), + &self.hws, + self.processing, + self.chosen_hw, + &self.registered, + ) + } + + fn update( + &mut self, + daemon: Arc, + cache: &Cache, + message: Message, + ) -> Command { + match message { + Message::View(view::Message::Reload) => { + self.hws = Vec::new(); + self.chosen_hw = None; + self.warning = None; + self.load(daemon) + } + Message::ConnectedHardwareWallets(hws) => { + self.hws = hws; + Command::none() + } + Message::WalletRegistered(res) => { + self.processing = false; + self.chosen_hw = None; + match res { + Ok(fingerprint) => { + self.registered.insert(fingerprint); + return Command::perform(async {}, |_| Message::LoadWallet); + } + Err(e) => self.warning = Some(e), + } + Command::none() + } + Message::View(view::Message::SelectHardwareWallet(i)) => { + if let Some(HardwareWallet::Supported { + fingerprint, + device, + .. + }) = self.hws.get(i) + { + self.chosen_hw = Some(i); + self.processing = true; + Command::perform( + register_wallet( + self.data_dir.clone(), + cache.network, + device.clone(), + *fingerprint, + self.wallet.clone(), + ), + Message::WalletRegistered, + ) + } else { + Command::none() + } + } + _ => Command::none(), + } + } + + fn load(&self, _daemon: Arc) -> Command { + Command::perform( + list_hws(self.wallet.clone()), + Message::ConnectedHardwareWallets, + ) + } +} + +async fn register_wallet( + data_dir: PathBuf, + network: Network, + hw: std::sync::Arc, + fingerprint: Fingerprint, + wallet: Arc, +) -> Result { + let hmac = hw + .register_wallet(&wallet.name, &wallet.main_descriptor.to_string()) + .await + .map_err(Error::from)?; + + if let Some(hmac) = hmac { + let mut settings = settings::Settings::from_file(data_dir.clone(), network)?; + let checksum = wallet.descriptor_checksum(); + if let Some(wallet_setting) = settings + .wallets + .iter_mut() + .find(|w| w.descriptor_checksum == checksum) + { + let kind = hw.device_kind().to_string(); + if let Some(hw_config) = wallet_setting + .hardware_wallets + .iter_mut() + .find(|cfg| cfg.kind == kind && cfg.fingerprint == fingerprint) + { + hw_config.token = hmac.to_hex(); + } else { + wallet_setting.hardware_wallets.push(HardwareWalletConfig { + kind, + token: hmac.to_hex(), + fingerprint, + }) + } + } + + settings.to_file(data_dir, network)?; + } + + Ok(fingerprint) +} + +async fn list_hws(wallet: Arc) -> Vec { + list_hardware_wallets( + &wallet.hardware_wallets, + Some((&wallet.name, &wallet.main_descriptor.to_string())), + ) + .await +} diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index b568c6e6..1c40fe98 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -11,7 +11,12 @@ use liana::{ use crate::{ app::{ - cache::Cache, error::Error, message::Message, view, view::spend::detail, wallet::Wallet, + cache::Cache, + error::Error, + message::Message, + view, + view::spend::detail, + wallet::{Wallet, WalletError}, }, daemon::{ model::{SpendStatus, SpendTx}, @@ -295,7 +300,7 @@ impl Action for SignAction { tx: &mut SpendTx, ) -> Command { match message { - Message::View(view::Message::Spend(view::SpendTxMessage::SelectHardwareWallet(i))) => { + Message::View(view::Message::SelectHardwareWallet(i)) => { if let Some(HardwareWallet::Supported { fingerprint, device, @@ -389,12 +394,12 @@ async fn sign_psbt_with_hot_signer( psbt: Psbt, ) -> Result<(Psbt, Fingerprint), Error> { if let Some(signer) = &wallet.signer { - let psbt = signer - .sign_psbt(psbt) - .map_err(|e| Error::HotSigner(format!("Hot signer failed to sign psbt: {}", e)))?; + let psbt = signer.sign_psbt(psbt).map_err(|e| { + WalletError::HotSigner(format!("Hot signer failed to sign psbt: {}", e)) + })?; Ok((psbt, signer.fingerprint())) } else { - Err(Error::HotSigner("Hot signer not loaded".to_string())) + Err(WalletError::HotSigner("Hot signer not loaded".to_string()).into()) } } diff --git a/gui/src/app/view/hw.rs b/gui/src/app/view/hw.rs index 1410b1a1..064c5e8c 100644 --- a/gui/src/app/view/hw.rs +++ b/gui/src/app/view/hw.rs @@ -17,13 +17,13 @@ use crate::{ }, }; -pub fn hw_list_view( +pub fn hw_list_view<'a>( i: usize, - hw: &HardwareWallet, + hw: &'a HardwareWallet, chosen: bool, processing: bool, - signed: bool, -) -> Element { + status: Option<&'a str>, +) -> Element<'a, Message> { let mut bttn = Button::new( Row::new() .push( @@ -72,17 +72,13 @@ pub fn hw_list_view( } else { None }) - .push_maybe(if signed { - Some( - Row::new() - .align_items(Alignment::Center) - .spacing(5) - .push(icon::circle_check_icon().style(color::SUCCESS)) - .push(text("Signed").style(color::SUCCESS)), - ) - } else { - None - }) + .push_maybe(status.map(|v| { + Row::new() + .align_items(Alignment::Center) + .spacing(5) + .push(icon::circle_check_icon().style(color::SUCCESS)) + .push(text(v).style(color::SUCCESS)) + })) .align_items(Alignment::Center) .width(Length::Fill), ) @@ -90,7 +86,7 @@ pub fn hw_list_view( .style(button::Style::Border.into()) .width(Length::Fill); if !processing && hw.is_supported() { - bttn = bttn.on_press(Message::Spend(SpendTxMessage::SelectHardwareWallet(i))); + bttn = bttn.on_press(Message::SelectHardwareWallet(i)); } Container::new(bttn) .width(Length::Fill) diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index 95afe49b..ca14b267 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -7,12 +7,13 @@ pub enum Message { Menu(Menu), Close, Select(usize), - Settings(usize, SettingsMessage), + Settings(SettingsMessage), CreateSpend(CreateSpendMessage), ImportSpend(ImportSpendMessage), Spend(SpendTxMessage), Next, Previous, + SelectHardwareWallet(usize), } #[derive(Debug, Clone)] @@ -41,7 +42,6 @@ pub enum SpendTxMessage { Confirm, Cancel, SelectHotSigner, - SelectHardwareWallet(usize), EditPsbt, PsbtEdited(String), Next, @@ -49,8 +49,17 @@ pub enum SpendTxMessage { #[derive(Debug, Clone)] pub enum SettingsMessage { - Edit, - FieldEdited(&'static str, String), - CancelEdit, - ConfirmEdit, + EditBitcoindSettings, + EditWalletSettings, + AboutSection, + RegisterWallet, + Edit(usize, SettingsEditMessage), +} + +#[derive(Debug, Clone)] +pub enum SettingsEditMessage { + Select, + FieldEdited(&'static str, String), + Cancel, + Confirm, } diff --git a/gui/src/app/view/settings.rs b/gui/src/app/view/settings.rs index 28246bbb..052e6a4f 100644 --- a/gui/src/app/view/settings.rs +++ b/gui/src/app/view/settings.rs @@ -1,30 +1,120 @@ +use std::collections::HashSet; use std::str::FromStr; use iced::{ alignment, - widget::{self, Column, Container, ProgressBar, Row, Space}, + widget::{self, Button, Column, Container, ProgressBar, Row, Space}, Alignment, Element, Length, }; -use liana::miniscript::bitcoin; +use liana::miniscript::bitcoin::{util::bip32::Fingerprint, Network}; -use super::{ - dashboard, - message::{Message, SettingsMessage}, -}; +use super::{dashboard, message::*}; use crate::{ - app::{cache::Cache, error::Error, menu::Menu}, + app::{ + cache::Cache, + error::Error, + menu::Menu, + view::{hw, warning::warn}, + }, + hw::HardwareWallet, ui::{ color, - component::{badge, button, card, form, separation, text::*}, + component::{badge, button, card, form, separation, text::*, tooltip::tooltip}, icon, util::Collection, }, }; -pub fn list<'a>( - lianad_version: Option<&'a String>, +pub fn list(cache: &Cache) -> Element { + dashboard( + &Menu::Settings, + cache, + None, + Column::new() + .spacing(20) + .width(Length::Fill) + .push( + Button::new(text("Settings").size(30).bold()) + .style(button::Style::Transparent.into()) + .on_press(Message::Menu(Menu::Settings))) + .push( + Container::new( + Button::new( + Row::new() + .push(badge::Badge::new(icon::bitcoin_icon())) + .push(text("Bitcoind").bold()) + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .width(Length::Fill) + .style(button::Style::Border.into()) + .on_press(Message::Settings(SettingsMessage::EditBitcoindSettings)) + ) + .width(Length::Fill) + .style(card::SimpleCardStyle) + ) + .push( + Container::new( + Button::new( + Row::new() + .push(badge::Badge::new(icon::wallet_icon())) + .push(text("Wallet").bold()) + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .width(Length::Fill) + .style(button::Style::Border.into()) + .on_press(Message::Settings(SettingsMessage::EditWalletSettings)) + ) + .width(Length::Fill) + .style(card::SimpleCardStyle) + ) + .push( + Container::new( + Button::new( + Row::new() + .push(badge::Badge::new(icon::recovery_icon())) + .push(text("Recovery").bold()) + .push(tooltip("In case of loss of the main key, the recovery key can move the funds after a certain time.")) + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .width(Length::Fill) + .style(button::Style::Border.into()) + .on_press(Message::Menu(Menu::Recovery)) + ) + .width(Length::Fill) + .style(card::SimpleCardStyle) + ) + .push( + Container::new( + Button::new( + Row::new() + .push(badge::Badge::new(icon::tooltip_icon())) + .push(text("About").bold()) + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .width(Length::Fill) + .style(button::Style::Border.into()) + .on_press(Message::Settings(SettingsMessage::AboutSection)) + ) + .width(Length::Fill) + .style(card::SimpleCardStyle) + ) + ) +} +pub fn bitcoind_settings<'a>( cache: &'a Cache, warning: Option<&Error>, settings: Vec>, @@ -33,36 +123,62 @@ pub fn list<'a>( &Menu::Settings, cache, warning, - widget::Column::with_children(settings) + Column::new() .spacing(20) - .push(card::simple( - Column::new() + .push( + Row::new() + .spacing(10) + .align_items(Alignment::Center) .push( - Row::new() - .push(badge::Badge::new(icon::recovery_icon())) - .push(text("Recovery").bold()) - .padding(10) - .spacing(20) - .align_items(Alignment::Center) - .width(Length::Fill), + Button::new(text("Settings").size(30).bold()) + .style(button::Style::Transparent.into()) + .on_press(Message::Menu(Menu::Settings)), ) - .push(separation().width(Length::Fill)) - .push(Space::with_height(Length::Units(10))) - .push(text("In case of loss of the main key, the recovery key can move the funds after a certain time.")) - .push(Space::with_height(Length::Units(10))) + .push(icon::chevron_right().size(30)) .push( - Row::new() - .push(Space::with_width(Length::Fill)) - .push(button::primary(None, "Recover funds").on_press(Message::Menu(Menu::Recovery))), + Button::new(text("Bitcoind").size(30).bold()) + .style(button::Style::Transparent.into()) + .on_press(Message::Settings(SettingsMessage::EditBitcoindSettings)), ), - )) + ) + .push(widget::Column::with_children(settings).spacing(20)), + ) +} + +pub fn about_section<'a>( + cache: &'a Cache, + warning: Option<&Error>, + lianad_version: Option<&String>, +) -> Element<'a, Message> { + dashboard( + &Menu::Settings, + cache, + warning, + Column::new() + .spacing(20) + .push( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push( + Button::new(text("Settings").size(30).bold()) + .style(button::Style::Transparent.into()) + .on_press(Message::Menu(Menu::Settings)), + ) + .push(icon::chevron_right().size(30)) + .push( + Button::new(text("About").size(30).bold()) + .style(button::Style::Transparent.into()) + .on_press(Message::Settings(SettingsMessage::AboutSection)), + ), + ) .push( card::simple( Column::new() .push( Row::new() .push(badge::Badge::new(icon::tooltip_icon())) - .push(text("About").bold()) + .push(text("Version").bold()) .padding(10) .spacing(20) .align_items(Alignment::Center) @@ -71,22 +187,28 @@ pub fn list<'a>( .push(separation().width(Length::Fill)) .push(Space::with_height(Length::Units(10))) .push( - Row::new().push(Space::with_width(Length::Fill)).push(Column::new() - .push(text(format!("liana-gui v{}", crate::VERSION))) - .push_maybe(lianad_version.map(|version| text(format!("lianad v{}", version))))) - ) - ).width(Length::Fill) - ) + Row::new().push(Space::with_width(Length::Fill)).push( + Column::new() + .push(text(format!("liana-gui v{}", crate::VERSION))) + .push_maybe( + lianad_version + .map(|version| text(format!("lianad v{}", version))), + ), + ), + ), + ) + .width(Length::Fill), + ), ) } pub fn bitcoind_edit<'a>( - network: bitcoin::Network, + network: Network, blockheight: i32, addr: &form::Value, cookie_path: &form::Value, processing: bool, -) -> Element<'a, SettingsMessage> { +) -> Element<'a, SettingsEditMessage> { let mut col = Column::new().spacing(20); if blockheight != 0 { col = col @@ -124,7 +246,7 @@ pub fn bitcoind_edit<'a>( .push(text("Cookie file path:").bold().small()) .push( form::Form::new("Cookie file path", cookie_path, |value| { - SettingsMessage::FieldEdited("cookie_file_path", value) + SettingsEditMessage::FieldEdited("cookie_file_path", value) }) .warning("Please enter a valid filesystem path") .size(20) @@ -137,7 +259,7 @@ pub fn bitcoind_edit<'a>( .push(text("Socket address:").bold().small()) .push( form::Form::new("Socket address:", addr, |value| { - SettingsMessage::FieldEdited("socket_address", value) + SettingsEditMessage::FieldEdited("socket_address", value) }) .warning("Please enter a valid address") .size(20) @@ -149,8 +271,8 @@ pub fn bitcoind_edit<'a>( let mut cancel_button = button::transparent(None, " Cancel ").padding(5); let mut confirm_button = button::primary(None, " Save ").padding(5); if !processing { - cancel_button = cancel_button.on_press(SettingsMessage::CancelEdit); - confirm_button = confirm_button.on_press(SettingsMessage::ConfirmEdit); + cancel_button = cancel_button.on_press(SettingsEditMessage::Cancel); + confirm_button = confirm_button.on_press(SettingsEditMessage::Confirm); } card::simple(Container::new( @@ -184,12 +306,12 @@ pub fn bitcoind_edit<'a>( } pub fn bitcoind<'a>( - network: bitcoin::Network, + network: Network, config: &liana::config::BitcoindConfig, blockheight: i32, is_running: Option, can_edit: bool, -) -> Element<'a, SettingsMessage> { +) -> Element<'a, SettingsEditMessage> { let mut col = Column::new().spacing(20); if blockheight != 0 { col = col @@ -254,7 +376,7 @@ pub fn bitcoind<'a>( .push(if can_edit { widget::Button::new(icon::pencil_icon()) .style(button::Style::TransparentBorder.into()) - .on_press(SettingsMessage::Edit) + .on_press(SettingsEditMessage::Select) } else { widget::Button::new(icon::pencil_icon()) .style(button::Style::TransparentBorder.into()) @@ -299,7 +421,7 @@ pub fn rescan<'a>( success: bool, processing: bool, can_edit: bool, -) -> Element<'a, SettingsMessage> { +) -> Element<'a, SettingsEditMessage> { card::simple(Container::new( Column::new() .push( @@ -332,7 +454,7 @@ pub fn rescan<'a>( .push(text("Year:").bold().small()) .push( form::Form::new("2022", year, |value| { - SettingsMessage::FieldEdited("rescan_year", value) + SettingsEditMessage::FieldEdited("rescan_year", value) }) .size(20) .padding(5), @@ -340,7 +462,7 @@ pub fn rescan<'a>( .push(text("Month:").bold().small()) .push( form::Form::new("12", month, |value| { - SettingsMessage::FieldEdited("rescan_month", value) + SettingsEditMessage::FieldEdited("rescan_month", value) }) .size(20) .padding(5), @@ -348,7 +470,7 @@ pub fn rescan<'a>( .push(text("Day:").bold().small()) .push( form::Form::new("31", day, |value| { - SettingsMessage::FieldEdited("rescan_day", value) + SettingsEditMessage::FieldEdited("rescan_day", value) }) .size(20) .padding(5), @@ -367,7 +489,7 @@ pub fn rescan<'a>( { Row::new().push(Column::new().width(Length::Fill)).push( button::primary(None, "Start rescan") - .on_press(SettingsMessage::ConfirmEdit) + .on_press(SettingsEditMessage::Confirm) .width(Length::Shrink), ) } else if processing { @@ -396,3 +518,103 @@ fn is_ok_and(res: &Result, f: impl FnOnce(&T) -> bool) -> bool { false } } + +pub fn wallet_settings<'a>( + cache: &'a Cache, + warning: Option<&Error>, + descriptor: &'a str, +) -> Element<'a, Message> { + dashboard( + &Menu::Settings, + cache, + warning, + Column::new() + .spacing(20) + .push( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push( + Button::new(text("Settings").size(30).bold()) + .style(button::Style::Transparent.into()) + .on_press(Message::Menu(Menu::Settings)), + ) + .push(icon::chevron_right().size(30)) + .push( + Button::new(text("Wallet").size(30).bold()) + .style(button::Style::Transparent.into()) + .on_press(Message::Settings(SettingsMessage::AboutSection)), + ), + ) + .push(card::simple( + Column::new() + .push(text("Wallet descriptor:").small().bold()) + .push(text(descriptor.to_owned()).small()) + .push( + Row::new() + .spacing(10) + .push(Column::new().width(Length::Fill)) + .push( + button::border(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clipboard(descriptor.to_owned())), + ) + .push( + button::primary( + Some(icon::chip_icon()), + "Register on hardware device", + ) + .on_press(Message::Settings(SettingsMessage::RegisterWallet)), + ), + ) + .spacing(10), + )), + ) +} + +pub fn register_wallet_modal<'a>( + warning: Option<&Error>, + hws: &'a [HardwareWallet], + processing: bool, + chosen_hw: Option, + registered: &HashSet, +) -> Element<'a, Message> { + Column::new() + .push_maybe(warning.map(|w| warn(Some(w)))) + .push(card::simple( + Column::new() + .push( + Column::new() + .push( + Row::new() + .push(text("Select device:").bold().width(Length::Fill)) + .push(button::border(None, "Refresh").on_press(Message::Reload)) + .align_items(Alignment::Center), + ) + .spacing(10) + .push(hws.iter().enumerate().fold( + Column::new().spacing(10), + |col, (i, hw)| { + col.push(hw::hw_list_view( + i, + hw, + Some(i) == chosen_hw, + processing, + hw.fingerprint().and_then(|f| { + if registered.contains(&f) { + Some("Registered") + } else { + None + } + }), + )) + }, + )) + .width(Length::Fill), + ) + .spacing(20) + .width(Length::Fill) + .align_items(Alignment::Center), + )) + .width(Length::Units(500)) + .into() +} diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs index 0aaa617a..df9992e5 100644 --- a/gui/src/app/view/spend/detail.rs +++ b/gui/src/app/view/spend/detail.rs @@ -737,9 +737,13 @@ pub fn sign_action<'a>( hw, Some(i) == chosen_hw, processing, - hw.fingerprint() - .map(|f| signed.contains(&f)) - .unwrap_or(false), + hw.fingerprint().and_then(|f| { + if signed.contains(&f) { + Some("Signed") + } else { + None + } + }), )) }, )) diff --git a/gui/src/app/view/warning.rs b/gui/src/app/view/warning.rs index 4a2845e0..c4bb5b3f 100644 --- a/gui/src/app/view/warning.rs +++ b/gui/src/app/view/warning.rs @@ -18,6 +18,7 @@ impl From<&Error> for WarningMessage { fn from(error: &Error) -> WarningMessage { match error { Error::Config(e) => WarningMessage(e.to_owned()), + Error::Wallet(_) => WarningMessage("Wallet error".to_string()), Error::Daemon(e) => match e { DaemonError::Rpc(code, _) => { if *code == RpcErrorCode::JSONRPC2_INVALID_PARAMS as i32 { @@ -37,7 +38,6 @@ impl From<&Error> for WarningMessage { }, Error::Unexpected(_) => WarningMessage("Unknown error".to_string()), Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()), - Error::HotSigner(_) => WarningMessage("Hot signer error".to_string()), } } } diff --git a/gui/src/app/wallet.rs b/gui/src/app/wallet.rs index 29742695..385046bd 100644 --- a/gui/src/app/wallet.rs +++ b/gui/src/app/wallet.rs @@ -1,6 +1,13 @@ use std::collections::{HashMap, HashSet}; +use std::path::Path; -use crate::{hw::HardwareWalletConfig, signer::Signer}; +use crate::{ + app::{config::Config, settings}, + hw::HardwareWalletConfig, + signer::Signer, +}; + +use liana::{miniscript::bitcoin, signer::HotSigner}; use liana::descriptors::MultipathDescriptor; use liana::miniscript::bitcoin::util::bip32::Fingerprint; @@ -17,17 +24,7 @@ pub struct Wallet { } impl Wallet { - pub fn new(name: String, main_descriptor: MultipathDescriptor) -> Self { - Self { - name, - main_descriptor, - keys_aliases: HashMap::new(), - hardware_wallets: Vec::new(), - signer: None, - } - } - - pub fn legacy(main_descriptor: MultipathDescriptor) -> Self { + pub fn new(main_descriptor: MultipathDescriptor) -> Self { Self { name: DEFAULT_WALLET_NAME.to_string(), main_descriptor, @@ -63,4 +60,86 @@ 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_settings( + self, + gui_config: &Config, + datadir_path: &Path, + network: bitcoin::Network, + ) -> Result { + let gui_config_hws = gui_config + .hardware_wallets + .as_ref() + .cloned() + .unwrap_or_default(); + + let mut wallet = match settings::Settings::from_file(datadir_path.to_path_buf(), network) { + Ok(settings) => { + if let Some(wallet_setting) = settings.wallets.first() { + self.with_hardware_wallets(wallet_setting.hardware_wallets.clone()) + .with_key_aliases(wallet_setting.keys_aliases()) + } else { + self.with_hardware_wallets(gui_config_hws) + } + } + Err(settings::SettingsError::NotFound) => self.with_hardware_wallets(gui_config_hws), + Err(e) => return Err(e.into()), + }; + + let hot_signers = match HotSigner::from_datadir(datadir_path, network) { + Ok(signers) => signers, + Err(e) => match e { + liana::signer::SignerError::MnemonicStorage(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Vec::new() + } else { + return Err(WalletError::HotSigner(e.to_string())); + } + } + _ => return Err(WalletError::HotSigner(e.to_string())), + }, + }; + + let curve = bitcoin::secp256k1::Secp256k1::signing_only(); + let keys = wallet.descriptor_keys(); + if let Some(hot_signer) = hot_signers + .into_iter() + .find(|s| keys.contains(&s.fingerprint(&curve))) + { + wallet = wallet.with_signer(Signer::new(hot_signer)); + } + + Ok(wallet) + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum WalletError { + Settings(settings::SettingsError), + HotSigner(String), +} + +impl std::fmt::Display for WalletError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Settings(e) => write!(f, "Failed to load settings: {}", e), + Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e), + } + } +} + +impl From for WalletError { + fn from(error: settings::SettingsError) -> Self { + WalletError::Settings(error) + } } diff --git a/gui/src/hw.rs b/gui/src/hw.rs index b524d3a0..383fe502 100644 --- a/gui/src/hw.rs +++ b/gui/src/hw.rs @@ -58,15 +58,15 @@ impl HardwareWallet { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct HardwareWalletConfig { pub kind: String, - pub fingerprint: String, + pub fingerprint: Fingerprint, pub token: String, } impl HardwareWalletConfig { - pub fn new(kind: &async_hwi::DeviceKind, fingerprint: &Fingerprint, token: &[u8; 32]) -> Self { + pub fn new(kind: &async_hwi::DeviceKind, fingerprint: Fingerprint, token: &[u8; 32]) -> Self { Self { kind: kind.to_string(), - fingerprint: fingerprint.to_string(), + fingerprint, token: token.to_hex(), } } @@ -116,7 +116,7 @@ pub async fn list_hardware_wallets( name, descriptor, cfg.iter() - .find(|cfg| cfg.fingerprint == fingerprint.to_string()) + .find(|cfg| cfg.fingerprint == fingerprint) .map(|cfg| cfg.token()), ) .expect("Configuration must be correct"); @@ -166,7 +166,7 @@ pub async fn list_hardware_wallets( name, descriptor, cfg.iter() - .find(|cfg| cfg.fingerprint == fingerprint.to_string()) + .find(|cfg| cfg.fingerprint == fingerprint) .map(|cfg| cfg.token()), ) .expect("Configuration must be correct"); diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs index e822684a..a54e9041 100644 --- a/gui/src/installer/context.rs +++ b/gui/src/installer/context.rs @@ -56,7 +56,7 @@ impl Context { .filter_map(|(kind, fingerprint, token)| { token .as_ref() - .map(|token| HardwareWalletConfig::new(kind, fingerprint, token)) + .map(|token| HardwareWalletConfig::new(kind, *fingerprint, token)) }) .collect(); Settings { diff --git a/gui/src/loader.rs b/gui/src/loader.rs index a089b2d7..2c8bb2ec 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -13,7 +13,6 @@ use tracing::{debug, info}; use liana::{ config::{Config, ConfigError}, miniscript::bitcoin, - signer::HotSigner, StartupError, }; @@ -21,11 +20,9 @@ use crate::{ app::{ cache::Cache, config::Config as GUIConfig, - settings::{self, Settings}, - wallet::Wallet, + wallet::{Wallet, WalletError}, }, daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError}, - signer::Signer, ui::{ component::{button, notification, text::*}, icon, @@ -237,51 +234,9 @@ pub async fn load_application( spend_txs, ..Default::default() }; - let settings_path = settings_path(&datadir_path, network); - let gui_config_hws = gui_config - .hardware_wallets - .as_ref() - .cloned() - .unwrap_or_default(); - let mut wallet = match Settings::from_file(&settings_path) { - Ok(settings) => { - if let Some(wallet_setting) = settings.wallets.first() { - Wallet::new(wallet_setting.name.clone(), info.descriptors.main) - .with_hardware_wallets(wallet_setting.hardware_wallets.clone()) - .with_key_aliases(wallet_setting.keys_aliases()) - } else { - Wallet::legacy(info.descriptors.main).with_hardware_wallets(gui_config_hws) - } - } - Err(settings::SettingsError::NotFound) => { - Wallet::legacy(info.descriptors.main).with_hardware_wallets(gui_config_hws) - } - Err(e) => return Err(e.into()), - }; - - let hot_signers = match HotSigner::from_datadir(&datadir_path, network) { - Ok(signers) => signers, - Err(e) => match e { - liana::signer::SignerError::MnemonicStorage(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Vec::new() - } else { - return Err(Error::HotSigner(e.to_string())); - } - } - _ => return Err(Error::HotSigner(e.to_string())), - }, - }; - - let curve = bitcoin::secp256k1::Secp256k1::signing_only(); - let keys = wallet.descriptor_keys(); - if let Some(hot_signer) = hot_signers - .into_iter() - .find(|s| keys.contains(&s.fingerprint(&curve))) - { - wallet = wallet.with_signer(Signer::new(hot_signer)); - } + let wallet = + Wallet::new(info.descriptors.main).load_settings(&gui_config, &datadir_path, network)?; Ok((Arc::new(wallet), cache, daemon)) } @@ -402,26 +357,24 @@ async fn sync( #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum Error { - Settings(settings::SettingsError), + Wallet(WalletError), Config(ConfigError), Daemon(DaemonError), - HotSigner(String), } 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::Wallet(e) => write!(f, "Wallet error: {}", e), Self::Daemon(e) => write!(f, "Liana daemon error: {}", e), - Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e), } } } -impl From for Error { - fn from(error: settings::SettingsError) -> Self { - Error::Settings(error) +impl From for Error { + fn from(error: WalletError) -> Self { + Error::Wallet(error) } } @@ -444,11 +397,3 @@ fn socket_path(datadir: &Path, network: bitcoin::Network) -> PathBuf { path.push("lianad_rpc"); path } - -/// default liana settings path is .liana/bitcoin/settings.json -fn settings_path(datadir: &Path, network: bitcoin::Network) -> PathBuf { - let mut path = datadir.to_path_buf(); - path.push(network.to_string()); - path.push(settings::DEFAULT_FILE_NAME); - path -} diff --git a/gui/src/main.rs b/gui/src/main.rs index 8f15ce38..5a38fe0f 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -214,7 +214,13 @@ impl Application for GUI { Command::none() } loader::Message::Synced(Ok((wallet, cache, daemon))) => { - let (app, command) = App::new(cache, wallet, loader.gui_config.clone(), daemon); + let (app, command) = App::new( + cache, + wallet, + loader.gui_config.clone(), + daemon, + loader.datadir_path.clone(), + ); self.state = State::App(app); command.map(|msg| Message::Run(Box::new(msg))) } diff --git a/gui/src/ui/icon.rs b/gui/src/ui/icon.rs index ec082572..3b190a90 100644 --- a/gui/src/ui/icon.rs +++ b/gui/src/ui/icon.rs @@ -17,6 +17,10 @@ pub fn arrow_down() -> Text<'static> { icon('\u{F128}') } +pub fn chevron_right() -> Text<'static> { + icon('\u{F285}') +} + pub fn recovery_icon() -> Text<'static> { icon('\u{F467}') }