From f3f04a41b00aef014f7f45b3418f5913a4361b8e Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 10 Feb 2023 13:19:57 +0100 Subject: [PATCH 01/11] Use liana hot signer --- gui/Cargo.lock | 86 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 0b91ef6f..eecac727 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -216,6 +216,18 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bip39" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e89470017230c38e52b82b3ee3f530db1856ba1d434e3a67a3456a8a8dec5f" +dependencies = [ + "bitcoin_hashes 0.9.7", + "rand_core 0.4.2", + "serde", + "unicode-normalization", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -239,13 +251,19 @@ checksum = "9cb36de3b18ad25f396f9168302e36fb7e1e8923298ab3127da252d288d5af9d" dependencies = [ "base64", "bech32", - "bitcoin_hashes", + "bitcoin_hashes 0.11.0", "core2", "hashbrown 0.8.2", "secp256k1", "serde", ] +[[package]] +name = "bitcoin_hashes" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1" + [[package]] name = "bitcoin_hashes" version = "0.11.0" @@ -1640,16 +1658,19 @@ dependencies = [ [[package]] name = "liana" version = "0.2.0" -source = "git+https://github.com/wizardsardine/liana?branch=master#88e5977166373437169ea00e54f365085afd03c0" +source = "git+https://github.com/wizardsardine/liana?branch=master#200d3777e0fe89d3b73d296eb31c83b6272dc490" dependencies = [ "backtrace", "base64", + "bip39", "dirs", "fern", + "getrandom", "jsonrpc", "libc", "log", "miniscript", + "rdrand", "rusqlite", "serde", "serde_json", @@ -1840,6 +1861,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "memchr" version = "2.5.0" @@ -2291,7 +2318,7 @@ dependencies = [ "instant", "libc", "redox_syscall", - "smallvec", + "smallvec 1.9.0", "winapi", ] @@ -2304,7 +2331,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec", + "smallvec 1.9.0", "windows-sys 0.42.0", ] @@ -2473,7 +2500,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.3", ] [[package]] @@ -2483,9 +2510,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.3", ] +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.3" @@ -2559,6 +2592,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ae028b272a6e99d9f8260ceefa3caa09300a8d6c8d2b2001316474bc52122e9" +[[package]] +name = "rdrand" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e233b642160555c1aa1ff7a78443c6139342f411b6fa6602af2ebbfee9e166bb" +dependencies = [ + "rand_core 0.6.3", +] + [[package]] name = "redox_syscall" version = "0.2.15" @@ -2648,7 +2690,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "memchr", - "smallvec", + "smallvec 1.9.0", ] [[package]] @@ -2671,7 +2713,7 @@ checksum = "44561062e583c4873162861261f16fd1d85fe927c4904d71329a4fe43dc355ef" dependencies = [ "bitflags", "bytemuck", - "smallvec", + "smallvec 1.9.0", "ttf-parser 0.12.3", "unicode-bidi-mirroring", "unicode-ccc", @@ -2724,7 +2766,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7649a0b3ffb32636e60c7ce0d70511eda9c52c658cd0634e194d5a19943aeff" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.11.0", "secp256k1-sys", "serde", ] @@ -2859,6 +2901,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + [[package]] name = "smallvec" version = "1.9.0" @@ -3180,6 +3231,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +[[package]] +name = "unicode-normalization" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c8070a9942f5e7cfccd93f490fdebd230ee3c3c9f107cb25bad5351ef671cf" +dependencies = [ + "smallvec 0.6.14", +] + [[package]] name = "unicode-script" version = "0.5.4" @@ -3372,7 +3432,7 @@ checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" dependencies = [ "nix 0.22.3", "once_cell", - "smallvec", + "smallvec 1.9.0", "wayland-sys", ] @@ -3453,7 +3513,7 @@ dependencies = [ "naga", "parking_lot 0.12.1", "raw-window-handle 0.5.0", - "smallvec", + "smallvec 1.9.0", "static_assertions", "wasm-bindgen", "wasm-bindgen-futures", @@ -3480,7 +3540,7 @@ dependencies = [ "parking_lot 0.12.1", "profiling", "raw-window-handle 0.5.0", - "smallvec", + "smallvec 1.9.0", "thiserror", "web-sys", "wgpu-hal", @@ -3518,7 +3578,7 @@ dependencies = [ "range-alloc", "raw-window-handle 0.5.0", "renderdoc-sys", - "smallvec", + "smallvec 1.9.0", "thiserror", "wasm-bindgen", "web-sys", From 1f09653198aefd699afd0e2cc5d26235c3193fb8 Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 7 Feb 2023 12:56:54 +0100 Subject: [PATCH 02/11] Add signer module to gui --- gui/src/lib.rs | 1 + gui/src/signer.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 gui/src/signer.rs diff --git a/gui/src/lib.rs b/gui/src/lib.rs index 082e6287..8a377d14 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -4,6 +4,7 @@ pub mod hw; pub mod installer; pub mod launcher; pub mod loader; +pub mod signer; pub mod ui; pub mod utils; diff --git a/gui/src/signer.rs b/gui/src/signer.rs new file mode 100644 index 00000000..50f54861 --- /dev/null +++ b/gui/src/signer.rs @@ -0,0 +1,55 @@ +pub use liana::signer::SignerError; + +use liana::{ + miniscript::bitcoin::{ + secp256k1, + util::{ + bip32::{DerivationPath, ExtendedPubKey, Fingerprint}, + psbt::Psbt, + }, + Network, + }, + signer::HotSigner, +}; + +pub struct Signer { + curve: secp256k1::Secp256k1, + key: HotSigner, + fingerprint: Fingerprint, +} + +impl Signer { + pub fn new(key: HotSigner) -> Self { + let curve = secp256k1::Secp256k1::signing_only(); + let fingerprint = key.fingerprint(&curve); + Self { + key, + curve, + fingerprint, + } + } + + pub fn generate(network: Network) -> Result { + Ok(Self::new(HotSigner::generate(network)?)) + } + + pub fn fingerprint(&self) -> Fingerprint { + self.fingerprint + } + + pub fn get_extended_pubkey(&self, path: &DerivationPath) -> ExtendedPubKey { + self.key.xpub_at(path, &self.curve) + } + + pub fn sign_psbt(&self, psbt: Psbt) -> Result { + self.key.sign_psbt(psbt, &self.curve) + } + + pub fn store( + &self, + datadir_root: &std::path::Path, + network: Network, + ) -> Result<(), SignerError> { + self.key.store(datadir_root, network, &self.curve) + } +} From f8dcd1116dfe820b6de18364b4d7ecaadbe0e7db Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 8 Feb 2023 11:40:41 +0100 Subject: [PATCH 03/11] Add hot signer to create descriptor step --- gui/src/installer/context.rs | 4 + gui/src/installer/message.rs | 1 + gui/src/installer/step/descriptor.rs | 149 ++++++++++++++++++--------- gui/src/installer/view.rs | 44 +++++--- gui/src/signer.rs | 8 ++ 5 files changed, 143 insertions(+), 63 deletions(-) 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)?)) } From 233f9b1886078ad2a94704537df8dacfeb5fe06a Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 10 Feb 2023 13:18:22 +0100 Subject: [PATCH 04/11] installer: add backup mnemonic step --- gui/src/installer/mod.rs | 25 ++++++++++++++-- gui/src/installer/prompt.rs | 1 + gui/src/installer/step/mnemonic.rs | 35 +++++++++++++++++++++++ gui/src/installer/step/mod.rs | 4 +++ gui/src/installer/view.rs | 46 ++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 gui/src/installer/step/mnemonic.rs diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 316f8844..86dfebd7 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -15,8 +15,8 @@ use crate::app::{config as gui_config, settings as gui_settings}; pub use message::Message; use step::{ - BackupDescriptor, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, ParticipateXpub, - RegisterDescriptor, Step, Welcome, + BackupDescriptor, BackupMnemonic, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, + ParticipateXpub, RegisterDescriptor, Step, Welcome, }; pub struct Installer { @@ -91,6 +91,7 @@ impl Installer { self.steps = vec![ Welcome::default().into(), DefineDescriptor::new().into(), + BackupMnemonic::default().into(), BackupDescriptor::default().into(), RegisterDescriptor::default().into(), DefineBitcoind::new().into(), @@ -103,6 +104,7 @@ impl Installer { Welcome::default().into(), ParticipateXpub::new().into(), ImportDescriptor::new(false).into(), + BackupMnemonic::default().into(), BackupDescriptor::default().into(), RegisterDescriptor::default().into(), DefineBitcoind::new().into(), @@ -165,11 +167,28 @@ impl Installer { } } + /// Some steps are skipped because of contextual choice of the user, this + /// code is giving a correct progress summary to the user. + fn progress(&self) -> (usize, usize) { + let mut current = self.current; + let mut total = 0; + for (i, step) in self.steps.iter().enumerate() { + if step.skip(&self.context) { + if i < self.current { + current -= 1; + } + } else { + total += 1 + } + } + (current, total - 1) + } + pub fn view(&self) -> Element { self.steps .get(self.current) .expect("There is always a step") - .view((self.current, self.steps.len() - 1)) + .view(self.progress()) } } diff --git a/gui/src/installer/prompt.rs b/gui/src/installer/prompt.rs index c08e0069..f2055479 100644 --- a/gui/src/installer/prompt.rs +++ b/gui/src/installer/prompt.rs @@ -7,3 +7,4 @@ pub const DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP: &str = pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str = "The alias is applied on all the keys derived from the same seed"; pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a device needs the descriptor. Registration on a device is not a substitute for backing up the descriptor."; +pub const MNEMONIC_HELP: &str = "A hot key generated on this computer was used for creating this wallet. It needs to be backed up. \n Keep it in a safe place. Never share it with anyone."; diff --git a/gui/src/installer/step/mnemonic.rs b/gui/src/installer/step/mnemonic.rs new file mode 100644 index 00000000..de3cb7af --- /dev/null +++ b/gui/src/installer/step/mnemonic.rs @@ -0,0 +1,35 @@ +use crate::installer::{context::Context, message::Message, step::Step, view}; + +use iced::{Command, Element}; + +#[derive(Default)] +pub struct BackupMnemonic { + words: [&'static str; 12], + done: bool, +} + +impl From for Box { + fn from(s: BackupMnemonic) -> Box { + Box::new(s) + } +} + +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; + } + Command::none() + } + fn skip(&self, ctx: &Context) -> bool { + ctx.signer.is_none() + } + fn view(&self, progress: (usize, usize)) -> Element { + view::backup_mnemonic(progress, &self.words, self.done) + } +} diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 198cce20..91efff56 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -1,8 +1,12 @@ mod descriptor; +mod mnemonic; + pub use descriptor::{ BackupDescriptor, DefineDescriptor, ImportDescriptor, ParticipateXpub, RegisterDescriptor, }; +pub use mnemonic::BackupMnemonic; + use std::path::PathBuf; use std::str::FromStr; diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 60d9b833..6d13faae 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -1319,6 +1319,52 @@ fn hw_list_view( .into() } +pub fn backup_mnemonic<'a>( + progress: (usize, usize), + words: &'a [&'static str; 12], + done: bool, +) -> Element<'a, Message> { + layout( + progress, + Column::new() + .push(text("Backup your mnemonic").bold().size(50)) + .push(text(prompt::MNEMONIC_HELP)) + .push( + words + .iter() + .enumerate() + .fold(Column::new().spacing(5), |acc, (i, w)| { + acc.push( + Row::new() + .align_items(Alignment::End) + .push( + Container::new(text(format!("#{}", i + 1)).small()) + .width(Length::Units(50)), + ) + .push(text(*w).bold()), + ) + }), + ) + .push(Checkbox::new( + "I have backed up my mnemonic", + done, + Message::UserActionDone, + )) + .push(if done { + button::primary(None, "Next") + .on_press(Message::Next) + .width(Length::Units(200)) + } else { + button::primary(None, "Next").width(Length::Units(200)) + }) + .width(Length::Fill) + .height(Length::Fill) + .padding(100) + .spacing(50) + .align_items(Alignment::Center), + ) +} + fn layout<'a>( progress: (usize, usize), content: impl Into>, From 037c4d025d651940ef42637ae5a6b8592dae56cb Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 10 Feb 2023 18:23:27 +0100 Subject: [PATCH 05/11] installer: add signer to participate wallet process --- gui/src/installer/message.rs | 2 +- gui/src/installer/step/descriptor.rs | 92 +++++++++++++++++++++++----- gui/src/installer/view.rs | 84 ++++++++++++++++++++++--- 3 files changed, 155 insertions(+), 23 deletions(-) diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 2304e388..784df2c9 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -21,6 +21,7 @@ pub enum Message { Close, Reload, Select(usize), + UseHotSigner, Installed(Result), Network(Network), DefineBitcoind(DefineBitcoind), @@ -45,7 +46,6 @@ 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 1840d856..073d930e 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -658,7 +658,7 @@ impl DescriptorKeyModal for EditXpubModal { self.hws = Vec::new(); return self.load(); } - Message::DefineDescriptor(message::DefineDescriptor::UseHotSigner) => { + Message::UseHotSigner => { self.chosen_hw = None; self.chosen_signer = true; self.form_xpub.valid = true; @@ -841,6 +841,12 @@ impl HardwareWalletXpubs { } } + fn reset(&mut self) { + self.error = None; + self.next_account = ChildNumber::from_hardened_idx(0).unwrap(); + self.xpubs = Vec::new(); + } + fn select(&mut self, i: usize, network: Network) -> Command { if let HardwareWallet::Supported { device, @@ -878,6 +884,42 @@ impl HardwareWalletXpubs { } } +pub struct SignerXpubs { + signer: Arc, + xpubs: Vec, + next_account: ChildNumber, +} + +impl SignerXpubs { + fn new(signer: Arc) -> Self { + Self { + signer, + xpubs: Vec::new(), + next_account: ChildNumber::from_hardened_idx(0).unwrap(), + } + } + + fn reset(&mut self) { + self.xpubs = Vec::new(); + self.next_account = ChildNumber::from_hardened_idx(0).unwrap(); + } + + fn select(&mut self, network: Network) { + let derivation_path = generate_derivation_path(network, self.next_account); + self.next_account = self.next_account.increment().unwrap(); + self.xpubs.push(format!( + "[{}{}]{}/<0;1>/*", + self.signer.fingerprint(), + derivation_path.to_string().trim_start_matches('m'), + self.signer.get_extended_pubkey(&derivation_path) + )); + } + + pub fn view(&self) -> Element { + view::signer_xpubs(&self.xpubs) + } +} + pub struct ParticipateXpub { network: Network, network_valid: bool, @@ -886,6 +928,7 @@ pub struct ParticipateXpub { shared: bool, xpubs_hw: Vec, + xpubs_signer: SignerXpubs, } impl ParticipateXpub { @@ -896,8 +939,30 @@ impl ParticipateXpub { data_dir: None, xpubs_hw: Vec::new(), shared: false, + xpubs_signer: SignerXpubs::new(Arc::new(Signer::generate(Network::Bitcoin).unwrap())), } } + + fn set_network(&mut self, network: Network) { + if network != self.network { + self.xpubs_hw.iter_mut().for_each(|hw| hw.reset()); + self.xpubs_signer.reset(); + } + self.network = network; + if let Some(signer) = Arc::get_mut(&mut self.xpubs_signer.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(); + } + } +} + +impl Default for ParticipateXpub { + fn default() -> Self { + Self::new() + } } impl Step for ParticipateXpub { @@ -906,10 +971,7 @@ impl Step for ParticipateXpub { fn update(&mut self, message: Message) -> Command { match message { 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(); + self.set_network(network); } Message::UserActionDone(shared) => self.shared = shared, Message::ImportXpub(i, res) => { @@ -917,6 +979,9 @@ impl Step for ParticipateXpub { hw.update(res); } } + Message::UseHotSigner => { + self.xpubs_signer.select(self.network); + } Message::Select(i) => { if let Some(hw) = self.xpubs_hw.get_mut(i) { return hw.select(i, self.network); @@ -943,11 +1008,8 @@ impl Step for ParticipateXpub { } 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 load(&self) -> Command { @@ -961,6 +1023,11 @@ 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 } @@ -974,17 +1041,12 @@ impl Step for ParticipateXpub { .enumerate() .map(|(i, hw)| hw.view(i)) .collect(), + self.xpubs_signer.view(), self.shared, ) } } -impl Default for ParticipateXpub { - fn default() -> Self { - Self::new() - } -} - impl From for Box { fn from(s: ParticipateXpub) -> Box { Box::new(s) diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 6d13faae..6e8cede5 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -411,6 +411,76 @@ pub fn import_descriptor<'a>( ) } +pub fn signer_xpubs(xpubs: &Vec) -> Element { + Container::new( + Column::new() + .push( + Button::new( + Row::new().align_items(Alignment::Center).push( + Column::new() + .push(text("This computer").bold()) + .push( + text("Derive a key from a mnemonic stored on this computer") + .small(), + ) + .spacing(5) + .width(Length::Fill), + ), + ) + .on_press(Message::UseHotSigner) + .padding(10) + .style(button::Style::TransparentBorder.into()) + .width(Length::Fill), + ) + .push_maybe(if xpubs.is_empty() { + None + } else { + Some(separation().width(Length::Fill)) + }) + .push_maybe(if xpubs.is_empty() { + None + } else { + Some(xpubs.iter().fold(Column::new().padding(15), |col, xpub| { + col.push( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push( + Container::new( + Scrollable::new(Container::new(text(xpub).small()).padding(10)) + .horizontal_scroll( + Properties::new().width(2).scroller_width(2), + ), + ) + .width(Length::Fill), + ) + .push( + Container::new( + button::border(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clibpboard(xpub.clone())) + .width(Length::Shrink), + ) + .padding(10), + ), + ) + })) + }) + .push_maybe(if !xpubs.is_empty() { + Some( + Container::new( + button::border(Some(icon::plus_icon()), "New public key") + .on_press(Message::UseHotSigner), + ) + .padding(10), + ) + } else { + None + }), + ) + .style(card::SimpleCardStyle) + .into() +} + pub fn hardware_wallet_xpubs<'a>( i: usize, xpubs: &'a Vec, @@ -531,13 +601,14 @@ pub fn hardware_wallet_xpubs<'a>( .into() } -pub fn participate_xpub( +pub fn participate_xpub<'a>( progress: (usize, usize), network: bitcoin::Network, network_valid: bool, - hws: Vec>, + hws: Vec>, + signer: Element<'a, Message>, shared: bool, -) -> Element { +) -> Element<'a, Message> { let row_network = Row::new() .spacing(10) .align_items(Alignment::Center) @@ -583,6 +654,7 @@ pub fn participate_xpub( ) .spacing(10) .push(Column::with_children(hws).spacing(10)) + .push(signer) .width(Length::Fill), ) .push(Checkbox::new( @@ -1138,7 +1210,7 @@ pub fn edit_key_modal<'a>( .spacing(5) .push(text("This computer").bold()) .push( - text("Derive a key from a mnemonic stored on the computer").small(), + text("Derive a key from a mnemonic stored on this computer").small(), ) .width(Length::Fill), ) @@ -1150,9 +1222,7 @@ pub fn edit_key_modal<'a>( .spacing(10), ) .width(Length::Fill) - .on_press(Message::DefineDescriptor( - message::DefineDescriptor::UseHotSigner, - )) + .on_press(Message::UseHotSigner) .style(button::Style::Border.into()), ) .width(Length::Fill), From c70ac114768a6410e01308bbc69ee4db853947e7 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 13 Feb 2023 12:56:17 +0100 Subject: [PATCH 06/11] installer: add recover seed step --- gui/src/installer/message.rs | 3 + gui/src/installer/mod.rs | 3 +- gui/src/installer/prompt.rs | 1 + gui/src/installer/step/mnemonic.rs | 128 ++++++++++++++++++++++++++++- gui/src/installer/step/mod.rs | 2 +- gui/src/installer/view.rs | 106 ++++++++++++++++++++++++ 6 files changed, 240 insertions(+), 3 deletions(-) diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 784df2c9..f403f469 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -16,6 +16,7 @@ pub enum Message { Exit(PathBuf), Clibpboard(String), Next, + Skip, Previous, Install, Close, @@ -29,6 +30,8 @@ pub enum Message { ImportXpub(usize, Result), ConnectedHardwareWallets(Vec), WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>), + MnemonicWord(usize, String), + ImportMnemonic(bool), } #[derive(Debug, Clone)] diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 86dfebd7..2465f1af 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -16,7 +16,7 @@ use crate::app::{config as gui_config, settings as gui_settings}; pub use message::Message; use step::{ BackupDescriptor, BackupMnemonic, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, - ParticipateXpub, RegisterDescriptor, Step, Welcome, + ParticipateXpub, RecoverMnemonic, RegisterDescriptor, Step, Welcome, }; pub struct Installer { @@ -116,6 +116,7 @@ impl Installer { self.steps = vec![ Welcome::default().into(), ImportDescriptor::new(true).into(), + RecoverMnemonic::default().into(), RegisterDescriptor::default().into(), DefineBitcoind::new().into(), Final::new().into(), diff --git a/gui/src/installer/prompt.rs b/gui/src/installer/prompt.rs index f2055479..b9123666 100644 --- a/gui/src/installer/prompt.rs +++ b/gui/src/installer/prompt.rs @@ -8,3 +8,4 @@ pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str = "The alias is applied on all the keys derived from the same seed"; pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a device needs the descriptor. Registration on a device is not a substitute for backing up the descriptor."; pub const MNEMONIC_HELP: &str = "A hot key generated on this computer was used for creating this wallet. It needs to be backed up. \n Keep it in a safe place. Never share it with anyone."; +pub const RECOVER_MNEMONIC_HELP: &str = "If you were using a hot key (a key stored on the computer) in your wallet, you will need to recover it from mnemonics to be able to sign transactions again. Otherwise you can directly go the next step."; diff --git a/gui/src/installer/step/mnemonic.rs b/gui/src/installer/step/mnemonic.rs index de3cb7af..5f88bea9 100644 --- a/gui/src/installer/step/mnemonic.rs +++ b/gui/src/installer/step/mnemonic.rs @@ -1,6 +1,13 @@ -use crate::installer::{context::Context, message::Message, step::Step, view}; +use std::collections::HashSet; +use std::sync::Arc; use iced::{Command, Element}; +use liana::{bip39, signer::HotSigner}; + +use crate::{ + installer::{context::Context, message::Message, step::Step, view}, + signer::Signer, +}; #[derive(Default)] pub struct BackupMnemonic { @@ -33,3 +40,122 @@ impl Step for BackupMnemonic { view::backup_mnemonic(progress, &self.words, self.done) } } + +pub struct RecoverMnemonic { + language: bip39::Language, + words: [(String, bool); 12], + current: usize, + suggestions: Vec, + error: Option, + skip: bool, + recover: bool, +} + +impl Default for RecoverMnemonic { + fn default() -> Self { + Self { + language: bip39::Language::English, + words: Default::default(), + current: 0, + suggestions: Vec::new(), + error: None, + skip: false, + recover: false, + } + } +} + +impl From for Box { + fn from(s: RecoverMnemonic) -> Box { + Box::new(s) + } +} + +impl Step for RecoverMnemonic { + fn update(&mut self, message: Message) -> Command { + match message { + Message::MnemonicWord(index, value) => { + if let Some((word, valid)) = self.words.get_mut(index) { + if value.len() >= 3 { + let suggestions = self.language.words_by_prefix(&value); + if suggestions.contains(&value.as_ref()) { + *valid = true; + self.suggestions = Vec::new(); + } else { + self.suggestions = suggestions.iter().map(|s| s.to_string()).collect(); + *valid = false; + } + } else { + self.suggestions = Vec::new(); + *valid = false; + } + self.current = index; + *word = value; + } + } + Message::ImportMnemonic(recover) => self.recover = recover, + Message::Skip => { + self.skip = true; + return Command::perform(async {}, |_| Message::Next); + } + _ => {} + } + Command::none() + } + + fn apply(&mut self, ctx: &mut Context) -> bool { + 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; + } + + let words: Vec = self + .words + .iter() + .filter_map(|(s, valid)| if *valid { Some(s.clone()) } else { None }) + .collect(); + + let seed = match HotSigner::from_str(ctx.bitcoin_config.network, &words.join(" ")) { + Ok(seed) => seed, + Err(e) => { + self.error = Some(e.to_string()); + return false; + } + }; + + let signer = Signer::new(seed); + let fingerprint = signer.fingerprint(); + + if let Some(descriptor) = &ctx.descriptor { + let info = descriptor.info(); + let mut descriptor_keys = HashSet::new(); + for (fingerprint, _) in info.primary_path().thresh_origins().1.iter() { + descriptor_keys.insert(*fingerprint); + } + for (fingerprint, _) in info.recovery_path().1.thresh_origins().1.iter() { + descriptor_keys.insert(*fingerprint); + } + if !descriptor_keys.contains(&fingerprint) { + self.error = + Some("The descriptor does not use a key derived from this seed".to_string()); + return false; + } + } + + ctx.signer = Some(Arc::new(signer)); + + true + } + fn view(&self, progress: (usize, usize)) -> Element { + view::recover_mnemonic( + progress, + &self.words, + self.current, + &self.suggestions, + self.recover, + self.error.as_ref(), + ) + } +} diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 91efff56..d1119ffa 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -5,7 +5,7 @@ pub use descriptor::{ BackupDescriptor, DefineDescriptor, ImportDescriptor, ParticipateXpub, RegisterDescriptor, }; -pub use mnemonic::BackupMnemonic; +pub use mnemonic::{BackupMnemonic, RecoverMnemonic}; use std::path::PathBuf; use std::str::FromStr; diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 6e8cede5..536fa877 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -1,5 +1,6 @@ use iced::widget::{ scrollable::Properties, Button, Checkbox, Column, Container, PickList, Row, Scrollable, Space, + TextInput, }; use iced::{alignment, Alignment, Element, Length}; @@ -1435,6 +1436,111 @@ pub fn backup_mnemonic<'a>( ) } +pub fn recover_mnemonic<'a>( + progress: (usize, usize), + words: &'a [(String, bool); 12], + current: usize, + suggestions: &'a Vec, + recover: bool, + error: Option<&'a String>, +) -> Element<'a, Message> { + layout( + progress, + Column::new() + .push(text("Mnemonics import").bold().size(50)) + .push(text(prompt::RECOVER_MNEMONIC_HELP)) + .push_maybe(if recover { + Some( + Column::new() + .align_items(Alignment::Center) + .push( + Container::new(if !suggestions.is_empty() { + suggestions.iter().fold(Row::new().spacing(5), |row, sugg| { + row.push( + Button::new(text(sugg)) + .style(button::Style::Border.into()) + .on_press(Message::MnemonicWord( + current, + sugg.to_string(), + )), + ) + }) + } else { + Row::new() + }) + // Fixed height in order to not move words list + .height(Length::Units(50)), + ) + .push(words.iter().enumerate().fold( + Column::new().spacing(5), + |acc, (i, (word, valid))| { + acc.push( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push( + Container::new(text(format!("#{}", i + 1)).small()) + .width(Length::Units(50)), + ) + .push( + Container::new(TextInput::new("", word, move |msg| { + Message::MnemonicWord(i, msg) + })) + .width(Length::Units(100)), + ) + .push_maybe(if *valid { + Some(icon::circle_check_icon().style(color::SUCCESS)) + } else { + None + }), + ) + }, + )) + .push(Space::with_height(Length::Units(50))) + .push_maybe(error.map(|e| card::invalid(text(e).style(color::ALERT)))), + ) + } else { + None + }) + .push(if !recover { + Row::new() + .spacing(10) + .push( + button::border(None, "Import mnemonic") + .on_press(Message::ImportMnemonic(true)) + .width(Length::Units(200)), + ) + .push( + button::primary(None, "Skip") + .on_press(Message::Skip) + .width(Length::Units(200)), + ) + } else { + Row::new() + .spacing(10) + .push( + button::border(None, "Cancel") + .on_press(Message::ImportMnemonic(false)) + .width(Length::Units(200)), + ) + .push( + if words.iter().any(|(_, valid)| !valid) || error.is_some() { + button::primary(None, "Next").width(Length::Units(200)) + } else { + button::primary(None, "Next") + .on_press(Message::Next) + .width(Length::Units(200)) + }, + ) + }) + .width(Length::Fill) + .height(Length::Fill) + .padding(100) + .spacing(50) + .align_items(Alignment::Center), + ) +} + fn layout<'a>( progress: (usize, usize), content: impl Into>, From 7a76754842b7eb688cbc6cd791c412f9006f407d Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 13 Feb 2023 16:51:07 +0100 Subject: [PATCH 07/11] Add title to installer final step --- gui/src/installer/view.rs | 233 +++++++++++++++++++++----------------- 1 file changed, 130 insertions(+), 103 deletions(-) diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 536fa877..a19c9411 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -902,115 +902,142 @@ pub fn install<'a>( config_path: Option<&std::path::PathBuf>, warning: Option<&'a String>, ) -> Element<'a, Message> { - let mut col = Column::new() - .push( - Container::new( - Column::new() - .spacing(10) - .push( - card::simple( - Column::new() - .spacing(5) - .push(text("Descriptor:").small().bold()) - .push(text(descriptor).small()), + layout( + progress, + Column::new() + .push(Space::with_height(Length::Units(50))) + .push(text("Final step").bold().size(50)) + .push(Space::with_height(Length::Units(50))) + .push(text( + "Check your information before finalizing the install process:", + )) + .push( + Container::new( + Column::new() + .spacing(10) + .push( + card::simple( + Column::new() + .spacing(5) + .push(text("Descriptor:").small().bold()) + .push(text(descriptor).small()), + ) + .width(Length::Fill), ) - .width(Length::Fill), - ) - .push( - card::simple( - Column::new() - .spacing(5) - .push(text("Hardware devices:").small().bold()) - .push(context.hws.iter().fold(Column::new(), |acc, hw| { - acc.push( + .push_maybe(if context.hws.is_empty() && context.signer.is_none() { + None + } else { + Some( + card::simple( + Column::new() + .spacing(5) + .push(text("Registered signing devices:").small().bold()) + .push_maybe(if context.hws.is_empty() { + None + } else { + Some(context.hws.iter().fold( + Column::new(), + |acc, hw| { + acc.push( + Row::new() + .spacing(5) + .push(text(hw.0.to_string()).small()) + .push( + text(format!( + "(fingerprint: {})", + hw.1 + )) + .small(), + ), + ) + }, + )) + }) + .push_maybe(context.signer.as_ref().map(|signer| { + Row::new().push(text("This computer").small()).push( + text(format!( + "(fingerprint: {})", + signer.fingerprint() + )) + .small(), + ) + })), + ) + .width(Length::Fill), + ) + }) + .push( + card::simple( + Column::new() + .push(text("Bitcoind:").small().bold()) + .push( Row::new() .spacing(5) - .push(text(hw.0.to_string()).small()) - .push(text(format!("(fingerprint: {})", hw.1)).small()), + .align_items(Alignment::Center) + .push(text("Cookie path:").small()) + .push( + text(format!( + "{}", + context + .bitcoind_config + .as_ref() + .unwrap() + .cookie_path + .to_string_lossy() + )) + .small(), + ), ) - })), - ) - .width(Length::Fill), - ) - .push( - card::simple( - Column::new() - .push(text("Bitcoind:").small().bold()) - .push( - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(text("Cookie path:").small()) - .push( - text(format!( - "{}", - context - .bitcoind_config - .as_ref() - .unwrap() - .cookie_path - .to_string_lossy() - )) - .small(), - ), - ) - .push( - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(text("Address:").small()) - .push( - text(format!( - "{}", - context.bitcoind_config.as_ref().unwrap().addr - )) - .small(), - ), - ), - ) - .width(Length::Fill), - ), + .push( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(text("Address:").small()) + .push( + text(format!( + "{}", + context.bitcoind_config.as_ref().unwrap().addr + )) + .small(), + ), + ), + ) + .width(Length::Fill), + ), + ) + .max_width(1000), ) - .padding(50) - .max_width(1000), - ) - .spacing(50) - .width(Length::Fill) - .height(Length::Fill) - .align_items(Alignment::Center); - - if let Some(error) = warning { - col = col.push(text(error)); - } - - if generating { - col = col.push(button::primary(None, "Installing ...").width(Length::Units(200))) - } else if let Some(path) = config_path { - col = col.push( - Container::new( - Column::new() - .push(Container::new(text("Installed !"))) - .push(Container::new( - button::primary(None, "Start") - .on_press(Message::Exit(path.clone())) - .width(Length::Units(200)), - )) - .align_items(Alignment::Center) - .spacing(20), - ) - .padding(50) + .push(Space::with_height(Length::Units(50))) + .push_maybe(warning.map(|e| card::invalid(text(e)))) + .push(if generating { + Container::new(button::primary(None, "Installing ...").width(Length::Units(200))) + } else if let Some(path) = config_path { + Container::new( + Column::new() + .push(Container::new(text("Installed !"))) + .push(Container::new( + button::primary(None, "Start") + .on_press(Message::Exit(path.clone())) + .width(Length::Units(200)), + )) + .align_items(Alignment::Center) + .spacing(20), + ) + .padding(50) + .width(Length::Fill) + .center_x() + } else { + Container::new( + button::primary(None, "Finalize installation") + .on_press(Message::Install) + .width(Length::Units(200)), + ) + }) + .spacing(10) .width(Length::Fill) - .center_x(), - ); - } else { - col = col.push( - button::primary(None, "Finalize installation") - .on_press(Message::Install) - .width(Length::Units(200)), - ); - } - - layout(progress, col) + .height(Length::Fill) + .align_items(Alignment::Center), + ) } pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> { From ecb3e114865b7e502b2a709f4bd2db4752de8976 Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 14 Feb 2023 11:09:29 +0100 Subject: [PATCH 08/11] installer: store mnemonic in datadir --- gui/src/installer/mod.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 2465f1af..f63ce5fd 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -232,7 +232,18 @@ pub async fn install(ctx: Context) -> Result { daemon_config.to_string().as_bytes(), )?; - log::info!("Daemon config file created"); + log::info!("Daemon configuration file created"); + + if let Some(signer) = &ctx.signer { + signer + .store( + &cfg.data_dir().expect("Already checked"), + cfg.bitcoin_config.network, + ) + .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; + + log::info!("Hot signer mnemonic stored"); + } // create liana GUI configuration file let gui_config_path = create_and_write_file( @@ -247,7 +258,7 @@ pub async fn install(ctx: Context) -> Result { .as_bytes(), )?; - log::info!("Gui config file created"); + log::info!("Gui configuration file created"); // create liana GUI settings file let settings: gui_settings::Settings = ctx.extract_gui_settings(); From e76459d159630068f2c0bd9799ce908c30e42d6e Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 31 Jan 2023 14:01:00 +0100 Subject: [PATCH 09/11] Load hot signer in Wallet --- gui/src/app/error.rs | 2 + gui/src/app/state/spend/detail.rs | 54 ++++++++---- gui/src/app/view/warning.rs | 1 + gui/src/app/wallet.rs | 28 +++++- gui/src/loader.rs | 140 +++++++++++++++++++----------- gui/src/signer.rs | 6 ++ 6 files changed, 161 insertions(+), 70 deletions(-) diff --git a/gui/src/app/error.rs b/gui/src/app/error.rs index f0e767a6..ff6d6457 100644 --- a/gui/src/app/error.rs +++ b/gui/src/app/error.rs @@ -9,6 +9,7 @@ pub enum Error { Daemon(DaemonError), Unexpected(String), HardwareWallet(async_hwi::Error), + HotSigner(String), } impl std::fmt::Display for Error { @@ -40,6 +41,7 @@ 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), } } } diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index 4c6f45aa..2b7066a8 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -25,12 +25,16 @@ trait Action { fn warning(&self) -> Option<&Error> { None } - fn load(&self, _wallet: &Wallet, _daemon: Arc) -> Command { + fn load( + &self, + _wallet: Arc, + _daemon: Arc, + ) -> Command { Command::none() } fn update( &mut self, - _wallet: &Wallet, + _wallet: Arc, _daemon: Arc, _message: Message, _tx: &mut SpendTx, @@ -61,7 +65,7 @@ impl SpendTxState { pub fn load(&self, daemon: Arc) -> Command { if let Some(action) = &self.action { - action.load(&self.wallet, daemon) + action.load(self.wallet.clone(), daemon) } else { Command::none() } @@ -83,13 +87,13 @@ impl SpendTxState { } view::SpendTxMessage::Sign => { let action = SignAction::new(); - let cmd = action.load(&self.wallet, daemon); + let cmd = action.load(self.wallet.clone(), daemon); self.action = Some(Box::new(action)); return cmd; } view::SpendTxMessage::EditPsbt => { let action = UpdateAction::new(self.tx.psbt.to_string()); - let cmd = action.load(&self.wallet, daemon); + let cmd = action.load(self.wallet.clone(), daemon); self.action = Some(Box::new(action)); return cmd; } @@ -101,19 +105,34 @@ impl SpendTxState { } _ => { if let Some(action) = self.action.as_mut() { - return action.update(&self.wallet, daemon.clone(), message, &mut self.tx); + return action.update( + self.wallet.clone(), + daemon.clone(), + message, + &mut self.tx, + ); } } }, Message::Updated(Ok(_)) => { self.saved = true; if let Some(action) = self.action.as_mut() { - return action.update(&self.wallet, daemon.clone(), message, &mut self.tx); + return action.update( + self.wallet.clone(), + daemon.clone(), + message, + &mut self.tx, + ); } } _ => { if let Some(action) = self.action.as_mut() { - return action.update(&self.wallet, daemon.clone(), message, &mut self.tx); + return action.update( + self.wallet.clone(), + daemon.clone(), + message, + &mut self.tx, + ); } } }; @@ -147,7 +166,7 @@ pub struct SaveAction { impl Action for SaveAction { fn update( &mut self, - _wallet: &Wallet, + _wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -183,7 +202,7 @@ pub struct BroadcastAction { impl Action for BroadcastAction { fn update( &mut self, - _wallet: &Wallet, + _wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -227,7 +246,7 @@ pub struct DeleteAction { impl Action for DeleteAction { fn update( &mut self, - _wallet: &Wallet, + _wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -284,13 +303,16 @@ impl Action for SignAction { self.error.as_ref() } - fn load(&self, wallet: &Wallet, _daemon: Arc) -> Command { - let wallet = wallet.clone(); + fn load( + &self, + wallet: Arc, + _daemon: Arc, + ) -> Command { Command::perform(list_hws(wallet), Message::ConnectedHardwareWallets) } fn update( &mut self, - wallet: &Wallet, + wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -365,7 +387,7 @@ impl Action for SignAction { } } -async fn list_hws(wallet: Wallet) -> Vec { +async fn list_hws(wallet: Arc) -> Vec { list_hardware_wallets( &wallet.hardware_wallets, Some((&wallet.name, &wallet.main_descriptor.to_string())), @@ -418,7 +440,7 @@ impl Action for UpdateAction { fn update( &mut self, - wallet: &Wallet, + wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, diff --git a/gui/src/app/view/warning.rs b/gui/src/app/view/warning.rs index b121ec9e..4a2845e0 100644 --- a/gui/src/app/view/warning.rs +++ b/gui/src/app/view/warning.rs @@ -37,6 +37,7 @@ 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 6e8bd4a2..29742695 100644 --- a/gui/src/app/wallet.rs +++ b/gui/src/app/wallet.rs @@ -1,18 +1,19 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; -use crate::hw::HardwareWalletConfig; +use crate::{hw::HardwareWalletConfig, signer::Signer}; use liana::descriptors::MultipathDescriptor; use liana::miniscript::bitcoin::util::bip32::Fingerprint; pub const DEFAULT_WALLET_NAME: &str = "Liana"; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Wallet { pub name: String, pub main_descriptor: MultipathDescriptor, pub keys_aliases: HashMap, pub hardware_wallets: Vec, + pub signer: Option, } impl Wallet { @@ -22,6 +23,7 @@ impl Wallet { main_descriptor, keys_aliases: HashMap::new(), hardware_wallets: Vec::new(), + signer: None, } } @@ -31,6 +33,7 @@ impl Wallet { main_descriptor, keys_aliases: HashMap::new(), hardware_wallets: Vec::new(), + signer: None, } } @@ -39,8 +42,25 @@ impl Wallet { self } - pub fn with_harware_wallets(mut self, hardware_wallets: Vec) -> Self { + pub fn with_hardware_wallets(mut self, hardware_wallets: Vec) -> Self { self.hardware_wallets = hardware_wallets; self } + + pub fn with_signer(mut self, signer: Signer) -> Self { + self.signer = Some(signer); + self + } + + pub fn descriptor_keys(&self) -> HashSet { + let info = self.main_descriptor.info(); + let mut descriptor_keys = HashSet::new(); + for (fingerprint, _) in info.primary_path().thresh_origins().1.iter() { + descriptor_keys.insert(*fingerprint); + } + for (fingerprint, _) in info.recovery_path().1.thresh_origins().1.iter() { + descriptor_keys.insert(*fingerprint); + } + descriptor_keys + } } diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 477bf8e7..67b30a08 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -13,6 +13,7 @@ use log::{debug, info}; use liana::{ config::{Config, ConfigError}, miniscript::bitcoin, + signer::HotSigner, StartupError, }; @@ -24,6 +25,7 @@ use crate::{ wallet::Wallet, }, daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError}, + signer::Signer, ui::{ component::{button, notification, text::*}, icon, @@ -143,47 +145,14 @@ impl Loader { match res { 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)?; - 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)) - }, + load_application( + daemon.clone(), + info, + self.gui_config.clone(), + self.datadir_path.clone(), + self.network, + ), Message::Synced, ); } else { @@ -229,6 +198,10 @@ impl Loader { Message::Started(res) => self.on_start(res), Message::Loaded(res) => self.on_load(res), Message::Syncing(res) => self.on_sync(res), + Message::Synced(Err(e)) => { + self.step = Step::Error(Box::new(e)); + Command::none() + } Message::Failure(_) => { self.daemon_started = false; Command::none() @@ -246,6 +219,71 @@ impl Loader { } } +pub async fn load_application( + daemon: Arc, + info: GetInfoResult, + gui_config: GUIConfig, + datadir_path: Option, + network: bitcoin::Network, +) -> Result<(Arc, Cache, Arc), Error> { + 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 settings_path = settings_path(&datadir_path, network).unwrap(); + 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(&get_datadir_path(&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)); + } + + Ok((Arc::new(wallet), cache, daemon)) +} + #[derive(Clone, Debug)] pub enum ViewMessage { Retry, @@ -368,6 +406,7 @@ pub enum Error { Settings(settings::SettingsError), Config(ConfigError), Daemon(DaemonError), + HotSigner(String), } impl std::fmt::Display for Error { @@ -376,6 +415,7 @@ impl std::fmt::Display for Error { 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), + Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e), } } } @@ -398,16 +438,20 @@ impl From for Error { } } +fn get_datadir_path(datadir_path: &Option) -> Result { + if let Some(ref datadir) = datadir_path { + Ok(datadir.clone()) + } else { + default_datadir().map_err(|_| ConfigError::DatadirNotFound) + } +} + /// default lianad socket path is .liana/bitcoin/lianad_rpc fn socket_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)? - }; + let mut path = get_datadir_path(datadir)?; path.push(network.to_string()); path.push("lianad_rpc"); Ok(path) @@ -418,11 +462,7 @@ 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)? - }; + let mut path = get_datadir_path(datadir)?; path.push(network.to_string()); path.push(settings::DEFAULT_FILE_NAME); Ok(path) diff --git a/gui/src/signer.rs b/gui/src/signer.rs index 039fb683..cad4ffc5 100644 --- a/gui/src/signer.rs +++ b/gui/src/signer.rs @@ -18,6 +18,12 @@ pub struct Signer { fingerprint: Fingerprint, } +impl std::fmt::Debug for Signer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Signer").finish() + } +} + impl Signer { pub fn new(key: HotSigner) -> Self { let curve = secp256k1::Secp256k1::signing_only(); From 9fac6277f42b605200134e6a670cb3511edf80cc Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 14 Feb 2023 13:56:40 +0100 Subject: [PATCH 10/11] Sign with hot signer --- gui/src/app/state/spend/detail.rs | 98 +++++++++++++++++-------------- gui/src/app/view/hw.rs | 11 ++-- gui/src/app/view/message.rs | 1 + gui/src/app/view/spend/detail.rs | 54 ++++++++++++----- 4 files changed, 98 insertions(+), 66 deletions(-) diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index 2b7066a8..b568c6e6 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -25,16 +25,11 @@ trait Action { fn warning(&self) -> Option<&Error> { None } - fn load( - &self, - _wallet: Arc, - _daemon: Arc, - ) -> Command { + fn load(&self, _daemon: Arc) -> Command { Command::none() } fn update( &mut self, - _wallet: Arc, _daemon: Arc, _message: Message, _tx: &mut SpendTx, @@ -65,7 +60,7 @@ impl SpendTxState { pub fn load(&self, daemon: Arc) -> Command { if let Some(action) = &self.action { - action.load(self.wallet.clone(), daemon) + action.load(daemon) } else { Command::none() } @@ -86,14 +81,14 @@ impl SpendTxState { self.action = Some(Box::new(DeleteAction::default())); } view::SpendTxMessage::Sign => { - let action = SignAction::new(); - let cmd = action.load(self.wallet.clone(), daemon); + let action = SignAction::new(self.wallet.clone()); + let cmd = action.load(daemon); self.action = Some(Box::new(action)); return cmd; } view::SpendTxMessage::EditPsbt => { - let action = UpdateAction::new(self.tx.psbt.to_string()); - let cmd = action.load(self.wallet.clone(), daemon); + let action = UpdateAction::new(self.wallet.clone(), self.tx.psbt.to_string()); + let cmd = action.load(daemon); self.action = Some(Box::new(action)); return cmd; } @@ -105,34 +100,19 @@ impl SpendTxState { } _ => { if let Some(action) = self.action.as_mut() { - return action.update( - self.wallet.clone(), - daemon.clone(), - message, - &mut self.tx, - ); + return action.update(daemon.clone(), message, &mut self.tx); } } }, Message::Updated(Ok(_)) => { self.saved = true; if let Some(action) = self.action.as_mut() { - return action.update( - self.wallet.clone(), - daemon.clone(), - message, - &mut self.tx, - ); + return action.update(daemon.clone(), message, &mut self.tx); } } _ => { if let Some(action) = self.action.as_mut() { - return action.update( - self.wallet.clone(), - daemon.clone(), - message, - &mut self.tx, - ); + return action.update(daemon.clone(), message, &mut self.tx); } } }; @@ -166,7 +146,6 @@ pub struct SaveAction { impl Action for SaveAction { fn update( &mut self, - _wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -202,7 +181,6 @@ pub struct BroadcastAction { impl Action for BroadcastAction { fn update( &mut self, - _wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -246,7 +224,6 @@ pub struct DeleteAction { impl Action for DeleteAction { fn update( &mut self, - _wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -279,6 +256,7 @@ impl Action for DeleteAction { } pub struct SignAction { + wallet: Arc, chosen_hw: Option, processing: bool, hws: Vec, @@ -287,8 +265,9 @@ pub struct SignAction { } impl SignAction { - pub fn new() -> Self { + pub fn new(wallet: Arc) -> Self { Self { + wallet, chosen_hw: None, processing: false, hws: Vec::new(), @@ -303,16 +282,14 @@ impl Action for SignAction { self.error.as_ref() } - fn load( - &self, - wallet: Arc, - _daemon: Arc, - ) -> Command { - Command::perform(list_hws(wallet), Message::ConnectedHardwareWallets) + fn load(&self, _daemon: Arc) -> Command { + Command::perform( + list_hws(self.wallet.clone()), + Message::ConnectedHardwareWallets, + ) } fn update( &mut self, - wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -334,6 +311,13 @@ impl Action for SignAction { ); } } + Message::View(view::Message::Spend(view::SpendTxMessage::SelectHotSigner)) => { + self.processing = true; + return Command::perform( + sign_psbt_with_hot_signer(self.wallet.clone(), tx.psbt.clone()), + Message::Signed, + ); + } Message::Signed(res) => match res { Err(e) => self.error = Some(e), Ok((psbt, fingerprint)) => { @@ -350,7 +334,11 @@ impl Action for SignAction { Message::Updated(res) => match res { Ok(()) => { self.processing = false; - tx.sigs = wallet.main_descriptor.partial_spend_info(&tx.psbt).unwrap(); + tx.sigs = self + .wallet + .main_descriptor + .partial_spend_info(&tx.psbt) + .unwrap(); } Err(e) => self.error = Some(e), }, @@ -370,7 +358,7 @@ impl Action for SignAction { self.hws = Vec::new(); self.chosen_hw = None; self.error = None; - return self.load(wallet, daemon); + return self.load(daemon); } _ => {} }; @@ -380,6 +368,7 @@ impl Action for SignAction { view::spend::detail::sign_action( self.error.as_ref(), &self.hws, + self.wallet.signer.as_ref().map(|s| s.fingerprint()), self.processing, self.chosen_hw, &self.signed, @@ -395,6 +384,20 @@ async fn list_hws(wallet: Arc) -> Vec { .await } +async fn sign_psbt_with_hot_signer( + wallet: Arc, + 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)))?; + Ok((psbt, signer.fingerprint())) + } else { + Err(Error::HotSigner("Hot signer not loaded".to_string())) + } +} + async fn sign_psbt( hw: std::sync::Arc, fingerprint: Fingerprint, @@ -405,6 +408,7 @@ async fn sign_psbt( } pub struct UpdateAction { + wallet: Arc, psbt: String, updated: form::Value, processing: bool, @@ -413,8 +417,9 @@ pub struct UpdateAction { } impl UpdateAction { - pub fn new(psbt: String) -> Self { + pub fn new(wallet: Arc, psbt: String) -> Self { Self { + wallet, psbt, updated: form::Value::default(), processing: false, @@ -440,7 +445,6 @@ impl Action for UpdateAction { fn update( &mut self, - wallet: Arc, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -477,7 +481,11 @@ impl Action for UpdateAction { .extend(updated_input.partial_sigs.clone().into_iter()); } } - tx.sigs = wallet.main_descriptor.partial_spend_info(&tx.psbt).unwrap(); + tx.sigs = self + .wallet + .main_descriptor + .partial_spend_info(&tx.psbt) + .unwrap(); } Err(e) => self.error = e.into(), } diff --git a/gui/src/app/view/hw.rs b/gui/src/app/view/hw.rs index c32b8357..1410b1a1 100644 --- a/gui/src/app/view/hw.rs +++ b/gui/src/app/view/hw.rs @@ -74,12 +74,11 @@ pub fn hw_list_view( }) .push_maybe(if signed { Some( - Column::new().push( - Row::new() - .spacing(5) - .push(icon::circle_check_icon().style(color::SUCCESS)) - .push(text("Signed").style(color::SUCCESS)), - ), + Row::new() + .align_items(Alignment::Center) + .spacing(5) + .push(icon::circle_check_icon().style(color::SUCCESS)) + .push(text("Signed").style(color::SUCCESS)), ) } else { None diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index ceb85482..95afe49b 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -40,6 +40,7 @@ pub enum SpendTxMessage { Save, Confirm, Cancel, + SelectHotSigner, SelectHardwareWallet(usize), EditPsbt, PsbtEdited(String), diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs index e28316ee..056012ec 100644 --- a/gui/src/app/view/spend/detail.rs +++ b/gui/src/app/view/spend/detail.rs @@ -714,6 +714,7 @@ pub fn inputs_and_outputs_view<'a>( pub fn sign_action<'a>( warning: Option<&Error>, hws: &'a [HardwareWallet], + signer: Option, processing: bool, chosen_hw: Option, signed: &[Fingerprint], @@ -722,12 +723,12 @@ pub fn sign_action<'a>( .push_maybe(warning.map(|w| warn(Some(w)))) .push(card::simple( Column::new() - .push(if !hws.is_empty() { + .push( Column::new() .push( Row::new() .push( - text("Select hardware wallet to sign with:") + text("Select signing device to sign with:") .bold() .width(Length::Fill), ) @@ -749,19 +750,42 @@ pub fn sign_action<'a>( )) }, )) - .width(Length::Fill) - } else { - Column::new() - .push( - Column::new() - .spacing(15) - .width(Length::Fill) - .push("Please connect a hardware wallet") - .push(button::border(None, "Refresh").on_press(Message::Reload)) - .align_items(Alignment::Center), - ) - .width(Length::Fill) - }) + .push_maybe(signer.map(|fingerprint| { + Button::new( + Row::new() + .align_items(Alignment::Center) + .push( + Column::new() + .width(Length::Fill) + .push(text("This computer").bold()) + .push( + text(format!("fingerprint: {}", fingerprint)) + .small(), + ) + .spacing(5) + .width(Length::Fill), + ) + .push_maybe(if signed.contains(&fingerprint) { + 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 + }), + ) + .on_press(Message::Spend(SpendTxMessage::SelectHotSigner)) + .padding(10) + .style(button::Style::Border.into()) + .width(Length::Fill) + })) + .width(Length::Fill), + ) .spacing(20) .width(Length::Fill) .align_items(Alignment::Center), From 26ad36f34e28e38a7c205343b6e5376a7f580e6f Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 15 Feb 2023 10:17:20 +0100 Subject: [PATCH 11/11] fix register descriptor prompt --- gui/src/installer/prompt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/installer/prompt.rs b/gui/src/installer/prompt.rs index b9123666..4bbd862e 100644 --- a/gui/src/installer/prompt.rs +++ b/gui/src/installer/prompt.rs @@ -6,6 +6,6 @@ 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"; -pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a device needs the descriptor. Registration on a device is not a substitute for backing up the descriptor."; +pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a device needs the descriptor. If the descriptor contains one or more keys imported from an external signing device, the descriptor must be registered on it. Registration on a device is not a substitute for backing up the descriptor."; pub const MNEMONIC_HELP: &str = "A hot key generated on this computer was used for creating this wallet. It needs to be backed up. \n Keep it in a safe place. Never share it with anyone."; pub const RECOVER_MNEMONIC_HELP: &str = "If you were using a hot key (a key stored on the computer) in your wallet, you will need to recover it from mnemonics to be able to sign transactions again. Otherwise you can directly go the next step.";