diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs index 7b019531..8d8a370a 100644 --- a/gui/src/installer/context.rs +++ b/gui/src/installer/context.rs @@ -30,8 +30,10 @@ pub struct Context { Option<[u8; 32]>, )>, pub data_dir: PathBuf, - pub signer: Option>, pub hw_is_used: bool, + // In case a user entered a mnemonic, + // we dont want to override the generated signer with it. + pub recovered_signer: Option>, } impl Context { @@ -46,8 +48,8 @@ impl Context { bitcoind_config: None, descriptor: None, data_dir, - signer: None, hw_is_used: false, + recovered_signer: None, } } diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 9f5719a8..9fc7cc2a 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -10,10 +10,15 @@ use liana_ui::widget::Element; use tracing::{error, info, warn}; use context::Context; + use std::io::Write; use std::path::PathBuf; +use std::sync::{Arc, Mutex}; -use crate::app::{config as gui_config, settings as gui_settings}; +use crate::{ + app::{config as gui_config, settings as gui_settings}, + signer::Signer, +}; pub use message::Message; use step::{ @@ -24,6 +29,7 @@ use step::{ pub struct Installer { current: usize, steps: Vec>, + signer: Arc>, /// Context is data passed through each step. context: Context, @@ -55,6 +61,7 @@ impl Installer { current: 0, steps: vec![Welcome::default().into()], context: Context::new(network, destination_path), + signer: Arc::new(Mutex::new(Signer::generate(network).unwrap())), }, Command::none(), ) @@ -98,29 +105,30 @@ impl Installer { } pub fn update(&mut self, message: Message) -> Command { + let hot_signer_fingerprint = self.signer.lock().unwrap().fingerprint(); match message { Message::CreateWallet => { self.steps = vec![ Welcome::default().into(), - DefineDescriptor::new().into(), - BackupMnemonic::default().into(), + DefineDescriptor::new(self.signer.clone()).into(), + BackupMnemonic::new(self.signer.clone()).into(), BackupDescriptor::default().into(), RegisterDescriptor::default().into(), DefineBitcoind::new().into(), - Final::new().into(), + Final::new(hot_signer_fingerprint).into(), ]; self.next() } Message::ParticipateWallet => { self.steps = vec![ Welcome::default().into(), - ParticipateXpub::new().into(), + ParticipateXpub::new(self.signer.clone()).into(), ImportDescriptor::new(false).into(), - BackupMnemonic::default().into(), + BackupMnemonic::new(self.signer.clone()).into(), BackupDescriptor::default().into(), RegisterDescriptor::default().into(), DefineBitcoind::new().into(), - Final::new().into(), + Final::new(hot_signer_fingerprint).into(), ]; self.next() } @@ -131,7 +139,7 @@ impl Installer { RecoverMnemonic::default().into(), RegisterDescriptor::default().into(), DefineBitcoind::new().into(), - Final::new().into(), + Final::new(hot_signer_fingerprint).into(), ]; self.next() } @@ -146,7 +154,10 @@ impl Installer { .get_mut(self.current) .expect("There is always a step") .update(message); - Command::perform(install(self.context.clone()), Message::Installed) + Command::perform( + install(self.context.clone(), self.signer.clone()), + Message::Installed, + ) } Message::Installed(Err(e)) => { let mut data_dir = self.context.data_dir.clone(); @@ -219,7 +230,7 @@ pub fn daemon_check(cfg: liana::config::Config) -> Result<(), Error> { } } -pub async fn install(ctx: Context) -> Result { +pub async fn install(ctx: Context, signer: Arc>) -> Result { let mut cfg: liana::config::Config = ctx.extract_daemon_config(); let data_dir = cfg.data_dir.unwrap(); @@ -248,8 +259,14 @@ pub async fn install(ctx: Context) -> Result { info!("Daemon configuration file created"); - if let Some(signer) = &ctx.signer { + if cfg + .main_descriptor + .to_string() + .contains(&signer.lock().unwrap().fingerprint().to_string()) + { signer + .lock() + .unwrap() .store( &cfg.data_dir().expect("Already checked"), cfg.bitcoin_config.network, @@ -259,6 +276,17 @@ pub async fn install(ctx: Context) -> Result { info!("Hot signer mnemonic stored"); } + if let Some(signer) = &ctx.recovered_signer { + signer + .store( + &cfg.data_dir().expect("Already checked"), + cfg.bitcoin_config.network, + ) + .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; + + info!("Recovered signer mnemonic stored"); + } + // create liana GUI configuration file let gui_config_path = create_and_write_file( network_datadir_path.clone(), diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index f3260a7d..296da64e 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::PathBuf; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use iced::Command; use liana::{ @@ -97,13 +97,13 @@ pub struct DefineDescriptor { recovery_paths: Vec, modal: Option>, - signer: Arc, + signer: Arc>, error: Option, } impl DefineDescriptor { - pub fn new() -> Self { + pub fn new(signer: Arc>) -> Self { Self { network: Network::Bitcoin, data_dir: None, @@ -112,7 +112,7 @@ impl DefineDescriptor { spending_threshold: 1, recovery_paths: vec![RecoveryPath::new()], modal: None, - signer: Arc::new(Signer::generate(Network::Bitcoin).unwrap()), + signer, error: None, } } @@ -125,9 +125,7 @@ impl DefineDescriptor { fn set_network(&mut self, network: Network) { self.network = network; - if let Some(signer) = Arc::get_mut(&mut self.signer) { - signer.set_network(network); - } + self.signer.lock().unwrap().set_network(network); if let Some(mut network_datadir) = self.data_dir.clone() { network_datadir.push(self.network.to_string()); self.network_valid = !network_datadir.exists(); @@ -448,7 +446,6 @@ impl Step for DefineDescriptor { ctx.bitcoin_config.network = self.network; ctx.keys = Vec::new(); let mut hw_is_used = false; - let mut signer_is_used = false; let mut spending_keys: Vec = Vec::new(); for spending_key in self.spending_keys.iter().clone() { if let Some(DescriptorPublicKey::XPub(xpub)) = spending_key.key.as_ref() { @@ -457,9 +454,6 @@ impl Step for DefineDescriptor { master_fingerprint, name: spending_key.name.clone(), }); - if master_fingerprint == self.signer.fingerprint() { - signer_is_used = true; - } if spending_key.device_kind.is_some() { hw_is_used = true; } @@ -489,9 +483,6 @@ impl Step for DefineDescriptor { master_fingerprint, name: recovery_key.name.clone(), }); - if master_fingerprint == self.signer.fingerprint() { - signer_is_used = true; - } if recovery_key.device_kind.is_some() { hw_is_used = true; } @@ -539,9 +530,6 @@ impl Step for DefineDescriptor { ctx.descriptor = Some(LianaDescriptor::new(policy)); ctx.hw_is_used = hw_is_used; - if signer_is_used { - ctx.signer = Some(self.signer.clone()); - } true } @@ -650,12 +638,6 @@ fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool { } } -impl Default for DefineDescriptor { - fn default() -> Self { - Self::new() - } -} - impl From for Box { fn from(s: DefineDescriptor) -> Box { Box::new(s) @@ -738,7 +720,8 @@ pub struct EditXpubModal { edit_name: bool, hws: Vec, - hot_signer: Arc, + hot_signer: Arc>, + hot_signer_fingerprint: Fingerprint, chosen_signer: Option<(Fingerprint, Option)>, } @@ -752,8 +735,9 @@ impl EditXpubModal { network: Network, account_indexes: HashMap, keys_aliases: HashMap, - hot_signer: Arc, + hot_signer: Arc>, ) -> Self { + let hot_signer_fingerprint = hot_signer.lock().unwrap().fingerprint(); Self { form_name: form::Value { valid: true, @@ -773,6 +757,7 @@ impl EditXpubModal { network, edit_name: false, chosen_signer: key.map(|k| (k.master_fingerprint(), None)), + hot_signer_fingerprint, hot_signer, } } @@ -838,7 +823,7 @@ impl DescriptorEditModal for EditXpubModal { return self.load(); } Message::UseHotSigner => { - let fingerprint = self.hot_signer.fingerprint(); + let fingerprint = self.hot_signer.lock().unwrap().fingerprint(); self.chosen_signer = Some((fingerprint, None)); self.form_xpub.valid = true; if let Some(alias) = self.keys_aliases.get(&fingerprint) { @@ -859,7 +844,10 @@ impl DescriptorEditModal for EditXpubModal { "[{}{}]{}", fingerprint, derivation_path.to_string().trim_start_matches('m'), - self.hot_signer.get_extended_pubkey(&derivation_path) + self.hot_signer + .lock() + .unwrap() + .get_extended_pubkey(&derivation_path) ); } Message::DefineDescriptor(message::DefineDescriptor::KeyModal(msg)) => match msg { @@ -957,8 +945,8 @@ impl DescriptorEditModal for EditXpubModal { self.error.as_ref(), self.processing, self.chosen_signer.map(|s| s.0), - &self.hot_signer, - self.keys_aliases.get(&self.hot_signer.fingerprint), + &self.hot_signer_fingerprint, + self.keys_aliases.get(&self.hot_signer_fingerprint), &self.form_xpub, &self.form_name, self.edit_name, @@ -1073,13 +1061,13 @@ impl HardwareWalletXpubs { } pub struct SignerXpubs { - signer: Arc, + signer: Arc>, xpubs: Vec, next_account: ChildNumber, } impl SignerXpubs { - fn new(signer: Arc) -> Self { + fn new(signer: Arc>) -> Self { Self { signer, xpubs: Vec::new(), @@ -1095,11 +1083,12 @@ impl SignerXpubs { fn select(&mut self, network: Network) { let derivation_path = generate_derivation_path(network, self.next_account); self.next_account = self.next_account.increment().unwrap(); + let signer = self.signer.lock().unwrap(); self.xpubs.push(format!( "[{}{}]{}", - self.signer.fingerprint(), + signer.fingerprint(), derivation_path.to_string().trim_start_matches('m'), - self.signer.get_extended_pubkey(&derivation_path) + signer.get_extended_pubkey(&derivation_path) )); } @@ -1120,14 +1109,14 @@ pub struct ParticipateXpub { } impl ParticipateXpub { - pub fn new() -> Self { + pub fn new(signer: Arc>) -> Self { Self { network: Network::Bitcoin, network_valid: true, data_dir: None, xpubs_hw: Vec::new(), shared: false, - xpubs_signer: SignerXpubs::new(Arc::new(Signer::generate(Network::Bitcoin).unwrap())), + xpubs_signer: SignerXpubs::new(signer), } } @@ -1137,9 +1126,11 @@ impl ParticipateXpub { self.xpubs_signer.reset(); } self.network = network; - if let Some(signer) = Arc::get_mut(&mut self.xpubs_signer.signer) { - signer.set_network(network); - } + self.xpubs_signer + .signer + .lock() + .unwrap() + .set_network(network); if let Some(mut network_datadir) = self.data_dir.clone() { network_datadir.push(self.network.to_string()); self.network_valid = !network_datadir.exists(); @@ -1147,12 +1138,6 @@ impl ParticipateXpub { } } -impl Default for ParticipateXpub { - fn default() -> Self { - Self::new() - } -} - impl Step for ParticipateXpub { // form value is set as valid each time it is edited. // Verification of the values is happening when the user click on Next button. @@ -1211,11 +1196,6 @@ impl Step for ParticipateXpub { ctx.bitcoin_config.network = self.network; // Drop connections to hardware wallets. self.xpubs_hw = Vec::new(); - if !self.xpubs_signer.xpubs.is_empty() { - ctx.signer = Some(self.xpubs_signer.signer.clone()); - } else { - ctx.signer = None; - } true } @@ -1515,7 +1495,9 @@ mod tests { #[tokio::test] async fn test_define_descriptor_use_hotkey() { let mut ctx = Context::new(Network::Signet, PathBuf::from_str("/").unwrap()); - let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new()); + let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new(Arc::new( + Mutex::new(Signer::generate(Network::Bitcoin).unwrap()), + ))); // Edit primary key sandbox @@ -1582,14 +1564,21 @@ mod tests { sandbox.check(|step| { assert!(step.modal.is_none()); assert!((step).apply(&mut ctx)); - assert!(ctx.signer.is_some()); + assert!(ctx + .descriptor + .as_ref() + .unwrap() + .to_string() + .contains(&step.signer.lock().unwrap().fingerprint().to_string())); }); } #[tokio::test] async fn test_define_descriptor_stores_if_hw_is_used() { let mut ctx = Context::new(Network::Signet, PathBuf::from_str("/").unwrap()); - let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new()); + let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new(Arc::new( + Mutex::new(Signer::generate(Network::Bitcoin).unwrap()), + ))); let specter_key = message::DefinePath::Key( 0, diff --git a/gui/src/installer/step/mnemonic.rs b/gui/src/installer/step/mnemonic.rs index 1f0b7c4a..7198b834 100644 --- a/gui/src/installer/step/mnemonic.rs +++ b/gui/src/installer/step/mnemonic.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use iced::Command; use liana::{bip39, signer::HotSigner}; @@ -11,10 +11,21 @@ use crate::{ signer::Signer, }; -#[derive(Default)] pub struct BackupMnemonic { words: [&'static str; 12], done: bool, + signer: Arc>, +} + +impl BackupMnemonic { + pub fn new(signer: Arc>) -> Self { + let words = signer.lock().unwrap().mnemonic(); + Self { + done: false, + words, + signer, + } + } } impl From for Box { @@ -24,11 +35,6 @@ impl From for Box { } impl Step for BackupMnemonic { - fn load_context(&mut self, ctx: &Context) { - if let Some(signer) = &ctx.signer { - self.words = signer.mnemonic(); - } - } fn update(&mut self, message: Message) -> Command { if let Message::UserActionDone(done) = message { self.done = done; @@ -36,7 +42,13 @@ impl Step for BackupMnemonic { Command::none() } fn skip(&self, ctx: &Context) -> bool { - ctx.signer.is_none() + if let Some(descriptor) = &ctx.descriptor { + !descriptor + .to_string() + .contains(&self.signer.lock().unwrap().fingerprint().to_string()) + } else { + false + } } fn view(&self, progress: (usize, usize)) -> Element { view::backup_mnemonic(progress, &self.words, self.done) @@ -109,7 +121,6 @@ impl Step for RecoverMnemonic { if self.skip { // If the user click previous, we dont want the skip to be set to true. self.skip = false; - ctx.signer = None; return true; } @@ -148,8 +159,7 @@ impl Step for RecoverMnemonic { } } - ctx.signer = Some(Arc::new(signer)); - + ctx.recovered_signer = Some(Arc::new(signer)); true } fn view(&self, progress: (usize, usize)) -> Element { diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 7a3bb93e..5768c18b 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -11,7 +11,10 @@ use std::path::PathBuf; use std::str::FromStr; use iced::Command; -use liana::{config::BitcoindConfig, miniscript::bitcoin}; +use liana::{ + config::BitcoindConfig, + miniscript::bitcoin::{util::bip32::Fingerprint, Network}, +}; use jsonrpc::{client::Client, simple_http::SimpleHttpTransport}; @@ -61,7 +64,7 @@ pub struct DefineBitcoind { is_running: Option>, } -fn bitcoind_default_cookie_path(network: &bitcoin::Network) -> Option { +fn bitcoind_default_cookie_path(network: &Network) -> Option { #[cfg(target_os = "linux")] let configs_dir = dirs::home_dir(); @@ -76,16 +79,16 @@ fn bitcoind_default_cookie_path(network: &bitcoin::Network) -> Option { path.push("Bitcoin"); match network { - bitcoin::Network::Bitcoin => { + Network::Bitcoin => { path.push(".cookie"); } - bitcoin::Network::Testnet => { + Network::Testnet => { path.push("testnet3/.cookie"); } - bitcoin::Network::Regtest => { + Network::Regtest => { path.push("regtest/.cookie"); } - bitcoin::Network::Signet => { + Network::Signet => { path.push("signet/.cookie"); } } @@ -95,12 +98,12 @@ fn bitcoind_default_cookie_path(network: &bitcoin::Network) -> Option { None } -fn bitcoind_default_address(network: &bitcoin::Network) -> String { +fn bitcoind_default_address(network: &Network) -> String { match network { - bitcoin::Network::Bitcoin => "127.0.0.1:8332".to_string(), - bitcoin::Network::Testnet => "127.0.0.1:18332".to_string(), - bitcoin::Network::Regtest => "127.0.0.1:18443".to_string(), - bitcoin::Network::Signet => "127.0.0.1:38332".to_string(), + Network::Bitcoin => "127.0.0.1:8332".to_string(), + Network::Testnet => "127.0.0.1:18332".to_string(), + Network::Regtest => "127.0.0.1:18443".to_string(), + Network::Signet => "127.0.0.1:38332".to_string(), } } @@ -227,15 +230,19 @@ pub struct Final { context: Option, warning: Option, config_path: Option, + hot_signer_fingerprint: Fingerprint, + hot_signer_is_not_used: bool, } impl Final { - pub fn new() -> Self { + pub fn new(hot_signer_fingerprint: Fingerprint) -> Self { Self { context: None, generating: false, warning: None, config_path: None, + hot_signer_fingerprint, + hot_signer_is_not_used: false, } } } @@ -243,6 +250,20 @@ impl Final { impl Step for Final { fn load_context(&mut self, ctx: &Context) { self.context = Some(ctx.clone()); + if let Some(signer) = &ctx.recovered_signer { + self.hot_signer_fingerprint = signer.fingerprint(); + self.hot_signer_is_not_used = false; + } else if ctx + .descriptor + .as_ref() + .unwrap() + .to_string() + .contains(&self.hot_signer_fingerprint.to_string()) + { + self.hot_signer_is_not_used = false; + } else { + self.hot_signer_is_not_used = true; + } } fn update(&mut self, message: Message) -> Command { match message { @@ -276,16 +297,15 @@ impl Step for Final { self.generating, self.config_path.as_ref(), self.warning.as_ref(), + if self.hot_signer_is_not_used { + None + } else { + Some(self.hot_signer_fingerprint) + }, ) } } -impl Default for Final { - fn default() -> Self { - Self::new() - } -} - impl From for Box { fn from(s: Final) -> Box { Box::new(s) diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 476fbdf4..cb4e4de2 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -25,7 +25,6 @@ use crate::{ message::{self, Message}, prompt, Error, }, - signer::Signer, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -888,6 +887,7 @@ pub fn install<'a>( generating: bool, config_path: Option<&std::path::PathBuf>, warning: Option<&'a String>, + signer: Option, ) -> Element<'a, Message> { layout( progress, @@ -911,7 +911,7 @@ pub fn install<'a>( ) .width(Length::Fill), ) - .push_maybe(if context.hws.is_empty() && context.signer.is_none() { + .push_maybe(if context.hws.is_empty() && signer.is_none() { None } else { Some( @@ -950,21 +950,17 @@ pub fn install<'a>( }, )) }) - .push_maybe(context.signer.as_ref().map(|signer| { + .push_maybe(signer.as_ref().map(|fingerprint| { Row::new() .spacing(5) .push_maybe(context.keys.iter().find_map(|k| { - if k.master_fingerprint == signer.fingerprint() - { + if k.master_fingerprint == *fingerprint { Some(text(k.name.clone()).small().bold()) } else { None } })) - .push( - text(format!("#{}", signer.fingerprint())) - .small(), - ) + .push(text(format!("#{}", fingerprint)).small()) .push(text("This computer").small()) })), ) @@ -1245,7 +1241,7 @@ pub fn edit_key_modal<'a>( error: Option<&Error>, processing: bool, chosen_signer: Option, - signer: &Signer, + hot_signer_fingerprint: &Fingerprint, signer_alias: Option<&'a String>, form_xpub: &form::Value, form_name: &'a form::Value, @@ -1288,10 +1284,10 @@ pub fn edit_key_modal<'a>( }, )) .push( - Button::new(if Some(signer.fingerprint) == chosen_signer { - hw::selected_hot_signer(signer.fingerprint, signer_alias) + Button::new(if Some(*hot_signer_fingerprint) == chosen_signer { + hw::selected_hot_signer(hot_signer_fingerprint, signer_alias) } else { - hw::unselected_hot_signer(signer.fingerprint, signer_alias) + hw::unselected_hot_signer(hot_signer_fingerprint, signer_alias) }) .width(Length::Fill) .on_press(Message::UseHotSigner)