diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs index 91b0d687..e822684a 100644 --- a/gui/src/installer/context.rs +++ b/gui/src/installer/context.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use std::sync::Arc; use std::time::Duration; use crate::{ @@ -7,6 +8,7 @@ use crate::{ wallet::DEFAULT_WALLET_NAME, }, hw::HardwareWalletConfig, + signer::Signer, }; use async_hwi::DeviceKind; use liana::{ @@ -28,6 +30,7 @@ pub struct Context { Option<[u8; 32]>, )>, pub data_dir: PathBuf, + pub signer: Option>, } impl Context { @@ -42,6 +45,7 @@ impl Context { bitcoind_config: None, descriptor: None, data_dir, + signer: None, } } diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index ea2388a4..2304e388 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -45,6 +45,7 @@ pub enum DefineDescriptor { HWXpubImported(Result), XPubEdited(String), EditName, + UseHotSigner, NameEdited(String), SequenceEdited(String), ThresholdEdited(bool, usize), diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index d9bdd17e..1840d856 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::str::FromStr; +use std::sync::Arc; use iced::{Command, Element}; use liana::{ @@ -24,6 +25,7 @@ use crate::{ step::{Context, Step}, view, Error, }, + signer::Signer, ui::component::{form, modal::Modal}, }; @@ -47,6 +49,7 @@ pub struct DefineDescriptor { recovery_threshold: usize, sequence: form::Value, modal: Option>, + signer: Arc, error: Option, } @@ -63,6 +66,7 @@ impl DefineDescriptor { recovery_threshold: 1, sequence: form::Value::default(), modal: None, + signer: Arc::new(Signer::generate(Network::Bitcoin).unwrap()), error: None, } } @@ -75,6 +79,23 @@ impl DefineDescriptor { && !self.spending_keys.iter().any(|k| k.key.is_none()) } + fn set_network(&mut self, network: Network) { + self.network = network; + if let Some(signer) = Arc::get_mut(&mut self.signer) { + signer.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(); + } + for key in self.spending_keys.iter_mut() { + key.check_network(self.network); + } + for key in self.recovery_keys.iter_mut() { + key.check_network(self.network); + } + } + // 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. @@ -201,18 +222,7 @@ impl Step for DefineDescriptor { Message::Close => { self.modal = None; } - Message::Network(network) => { - self.network = network; - let mut network_datadir = self.data_dir.clone().unwrap(); - network_datadir.push(self.network.to_string()); - self.network_valid = !network_datadir.exists(); - for key in self.spending_keys.iter_mut() { - key.check_network(self.network); - } - for key in self.recovery_keys.iter_mut() { - key.check_network(self.network); - } - } + Message::Network(network) => self.set_network(network), Message::DefineDescriptor(msg) => { match msg { message::DefineDescriptor::ThresholdEdited(is_recovery, value) => { @@ -263,42 +273,30 @@ impl Step for DefineDescriptor { message::DefineKey::Edit => { if is_recovery { if let Some(recovery_key) = self.recovery_keys.get(i) { - let name = recovery_key.name.clone(); - let key = recovery_key - .key - .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, + recovery_key.name.clone(), + recovery_key.key.as_ref(), i, is_recovery, self.network, self.fingerprint_account_index_mappping(), self.keys_aliases(), + self.signer.clone(), ); let cmd = modal.load(); self.modal = Some(Box::new(modal)); return cmd; } } else if let Some(spending_key) = self.spending_keys.get(i) { - let name = spending_key.name.clone(); - let key = spending_key - .key - .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, + spending_key.name.clone(), + spending_key.key.as_ref(), i, is_recovery, self.network, self.fingerprint_account_index_mappping(), self.keys_aliases(), + self.signer.clone(), ); let cmd = modal.load(); self.modal = Some(Box::new(modal)); @@ -337,16 +335,14 @@ impl Step for DefineDescriptor { } fn load_context(&mut self, ctx: &Context) { - self.network = ctx.bitcoin_config.network; self.data_dir = Some(ctx.data_dir.clone()); - let mut network_datadir = ctx.data_dir.clone(); - network_datadir.push(self.network.to_string()); - self.network_valid = !network_datadir.exists(); + self.set_network(ctx.bitcoin_config.network) } fn apply(&mut self, ctx: &mut Context) -> bool { ctx.bitcoin_config.network = self.network; ctx.keys = Vec::new(); + 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(key) = spending_key.key.as_ref() { @@ -371,6 +367,9 @@ impl Step for DefineDescriptor { master_fingerprint, name: recovery_key.name.clone(), }); + if master_fingerprint == self.signer.fingerprint() { + signer_is_used = true; + } } } recovery_keys.push(key.clone()); @@ -421,6 +420,9 @@ impl Step for DefineDescriptor { }; ctx.descriptor = Some(desc); + if signer_is_used { + ctx.signer = Some(self.signer.clone()); + } true } @@ -555,17 +557,22 @@ pub struct EditXpubModal { chosen_hw: Option, hws: Vec, + + signer: Arc, + chosen_signer: bool, } impl EditXpubModal { + #[allow(clippy::too_many_arguments)] fn new( name: String, - key: String, + key: Option<&DescriptorPublicKey>, key_index: usize, is_recovery: bool, network: Network, account_indexes: HashMap, keys_aliases: HashMap, + signer: Arc, ) -> Self { Self { form_name: form::Value { @@ -574,7 +581,9 @@ impl EditXpubModal { }, form_xpub: form::Value { valid: true, - value: key, + value: key + .map(|k| k.to_string().trim_end_matches("/<0;1>/*").to_string()) + .unwrap_or_else(|| "".to_string()), }, keys_aliases, account_indexes, @@ -586,6 +595,8 @@ impl EditXpubModal { error: None, network, edit_name: false, + chosen_signer: Some(signer.fingerprint()) == key.map(|k| k.master_fingerprint()), + signer, } } fn load(&self) -> Command { @@ -622,8 +633,7 @@ impl DescriptorKeyModal for EditXpubModal { get_extended_pubkey( device.clone(), *fingerprint, - self.network, - account_index, + generate_derivation_path(self.network, account_index), ), |res| { Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported( @@ -635,11 +645,45 @@ impl DescriptorKeyModal for EditXpubModal { } Message::ConnectedHardwareWallets(hws) => { self.hws = hws; + if let Ok(key) = + DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", self.form_xpub.value)) + { + self.chosen_hw = self + .hws + .iter() + .position(|hw| hw.fingerprint() == Some(key.master_fingerprint())); + } } Message::Reload => { self.hws = Vec::new(); return self.load(); } + Message::DefineDescriptor(message::DefineDescriptor::UseHotSigner) => { + self.chosen_hw = None; + self.chosen_signer = true; + self.form_xpub.valid = true; + let fingerprint = self.signer.fingerprint(); + 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; + self.form_name.value = String::new(); + } + let account_index = self + .account_indexes + .get(&fingerprint) + .map(|account_index| account_index.increment().unwrap()) + .unwrap_or_else(|| ChildNumber::from_hardened_idx(0).unwrap()); + let derivation_path = generate_derivation_path(self.network, account_index); + self.form_xpub.value = format!( + "[{}{}]{}", + fingerprint, + derivation_path.to_string().trim_start_matches('m'), + self.signer.get_extended_pubkey(&derivation_path) + ); + } Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported(res)) => { self.processing = false; match res { @@ -651,11 +695,13 @@ impl DescriptorKeyModal for EditXpubModal { } else { self.edit_name = true; } + self.chosen_signer = false; self.form_xpub.valid = true; self.form_xpub.value = key.to_string().trim_end_matches("/<0;1>/*").to_string(); } Err(e) => { + self.chosen_hw = None; self.error = Some(e); } } @@ -719,6 +765,7 @@ impl DescriptorKeyModal for EditXpubModal { self.error.as_ref(), self.processing, self.chosen_hw, + self.chosen_signer, &self.form_xpub, &self.form_name, self.edit_name, @@ -726,22 +773,24 @@ impl DescriptorKeyModal for EditXpubModal { } } -/// 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(&{ +fn generate_derivation_path(network: Network, account_index: ChildNumber) -> DerivationPath { + DerivationPath::from_str(&{ if network == Network::Bitcoin { format!("m/48'/0'/{}/2'", account_index) } else { format!("m/48'/1'/{}/2'", account_index) } }) - .unwrap(); + .unwrap() +} + +/// 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, + derivation_path: DerivationPath, +) -> Result { let xkey = hw .get_extended_pubkey(&derivation_path, false) .await @@ -803,12 +852,12 @@ impl HardwareWalletXpubs { let fingerprint = *fingerprint; self.processing = true; self.error = None; - let next_account = self.next_account; + let derivation_path = generate_derivation_path(network, self.next_account); Command::perform( async move { ( i, - get_extended_pubkey(device, fingerprint, network, next_account).await, + get_extended_pubkey(device, fingerprint, derivation_path).await, ) }, |(i, res)| Message::ImportXpub(i, res), diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 406fda19..60d9b833 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -1086,6 +1086,7 @@ pub fn edit_key_modal<'a>( error: Option<&Error>, processing: bool, chosen_hw: Option, + chosen_signer: bool, form_xpub: &form::Value, form_name: &'a form::Value, edit_name: bool, @@ -1095,14 +1096,14 @@ pub fn edit_key_modal<'a>( .push(card::simple( Column::new() .spacing(25) - .push(if !hws.is_empty() { + .push( Column::new() .push( Row::new() .spacing(10) .align_items(Alignment::Center) .push( - Container::new(text("Select a hardware wallet:").bold()) + Container::new(text("Select a signing device:").bold()) .width(Length::Fill), ) .push( @@ -1126,19 +1127,36 @@ pub fn edit_key_modal<'a>( )) }, )) - .width(Length::Fill) - } else { - Column::new() .push( - Row::new() - .spacing(15) - .width(Length::Fill) - .push(text("Connect a hardware wallet").bold().width(Length::Fill)) - .push(button::border(None, "Refresh").on_press(Message::Reload)) - .align_items(Alignment::Center), + Button::new( + Row::new() + .padding(5) + .width(Length::Fill) + .align_items(Alignment::Center) + .push( + Column::new() + .spacing(5) + .push(text("This computer").bold()) + .push( + text("Derive a key from a mnemonic stored on the computer").small(), + ) + .width(Length::Fill), + ) + .push_maybe(if chosen_signer { + Some(icon::circle_check_icon().style(color::SUCCESS)) + } else { + None + }) + .spacing(10), + ) + .width(Length::Fill) + .on_press(Message::DefineDescriptor( + message::DefineDescriptor::UseHotSigner, + )) + .style(button::Style::Border.into()), ) - .width(Length::Fill) - }) + .width(Length::Fill), + ) .push( Column::new() .spacing(5) diff --git a/gui/src/signer.rs b/gui/src/signer.rs index 50f54861..039fb683 100644 --- a/gui/src/signer.rs +++ b/gui/src/signer.rs @@ -29,6 +29,14 @@ impl Signer { } } + pub fn set_network(&mut self, network: Network) { + self.key.set_network(network) + } + + pub fn mnemonic(&self) -> [&'static str; 12] { + self.key.words() + } + pub fn generate(network: Network) -> Result { Ok(Self::new(HotSigner::generate(network)?)) }