From d73894dfa0143f8bdb7fcb884b9c8c6d2154382b Mon Sep 17 00:00:00 2001 From: edouardparis Date: Thu, 26 Sep 2024 15:54:24 +0200 Subject: [PATCH 1/8] installer: module editor --- .../editor/mod.rs} | 312 +--------------- gui/src/installer/step/descriptor/mod.rs | 336 ++++++++++++++++++ gui/src/installer/step/mod.rs | 4 +- gui/src/installer/step/share_xpubs.rs | 2 +- 4 files changed, 341 insertions(+), 313 deletions(-) rename gui/src/installer/step/{descriptor.rs => descriptor/editor/mod.rs} (83%) create mode 100644 gui/src/installer/step/descriptor/mod.rs diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor/editor/mod.rs similarity index 83% rename from gui/src/installer/step/descriptor.rs rename to gui/src/installer/step/descriptor/editor/mod.rs index 0816db94..4037ef3a 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor/editor/mod.rs @@ -27,7 +27,7 @@ use async_hwi::{DeviceKind, Version}; use crate::hw; use crate::{ - app::{settings::KeySetting, wallet::wallet_name}, + app::settings::KeySetting, hw::{HardwareWallet, HardwareWallets}, installer::{ message::{self, Message}, @@ -1097,316 +1097,6 @@ pub async fn get_extended_pubkey( })) } -pub struct ImportDescriptor { - network: Network, - imported_descriptor: form::Value, - wrong_network: bool, - error: Option, -} - -impl ImportDescriptor { - pub fn new(network: Network) -> Self { - Self { - network, - imported_descriptor: form::Value::default(), - wrong_network: false, - error: None, - } - } - - fn check_descriptor(&mut self, network: Network) -> Option { - if !self.imported_descriptor.value.is_empty() { - if let Ok(desc) = LianaDescriptor::from_str(&self.imported_descriptor.value) { - if network == Network::Bitcoin { - self.imported_descriptor.valid = desc.all_xpubs_net_is(network); - } else { - self.imported_descriptor.valid = desc.all_xpubs_net_is(Network::Testnet); - } - if self.imported_descriptor.valid { - self.wrong_network = false; - Some(desc) - } else { - self.wrong_network = true; - None - } - } else { - self.imported_descriptor.valid = false; - self.wrong_network = false; - None - } - } else { - self.wrong_network = false; - self.imported_descriptor.valid = true; - None - } - } -} - -impl Step for ImportDescriptor { - // ImportRemoteWallet is used instead - fn skip(&self, ctx: &Context) -> bool { - ctx.remote_backend.is_some() - } - // form value is set as valid each time it is edited. - // Verification of the values is happening when the user click on Next button. - fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { - if let Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) = - message - { - self.imported_descriptor.value = desc; - self.check_descriptor(self.network); - } - Command::none() - } - - fn apply(&mut self, ctx: &mut Context) -> bool { - ctx.bitcoin_config.network = self.network; - // Set to true in order to force the registration process to be shown to user. - ctx.hw_is_used = true; - // descriptor forms for import or creation cannot be both empty or filled. - if let Some(desc) = self.check_descriptor(self.network) { - ctx.descriptor = Some(desc); - true - } else { - false - } - } - - fn view<'a>( - &'a self, - _hws: &'a HardwareWallets, - progress: (usize, usize), - email: Option<&'a str>, - ) -> Element { - view::import_descriptor( - progress, - email, - &self.imported_descriptor, - self.wrong_network, - self.error.as_ref(), - ) - } -} - -impl From for Box { - fn from(s: ImportDescriptor) -> Box { - Box::new(s) - } -} - -pub struct RegisterDescriptor { - descriptor: Option, - processing: bool, - chosen_hw: Option, - hmacs: Vec<(Fingerprint, DeviceKind, Option<[u8; 32]>)>, - registered: HashSet, - error: Option, - done: bool, - /// Whether this step is part of the descriptor creation process. This is used to detect when - /// it's instead shown as part of the descriptor *import* process, where we can't detect - /// whether a signing device is used, to explicit this step is not required if the user isn't - /// using a signing device. - created_desc: bool, -} - -impl RegisterDescriptor { - fn new(created_desc: bool) -> Self { - Self { - created_desc, - descriptor: Default::default(), - processing: Default::default(), - chosen_hw: Default::default(), - hmacs: Default::default(), - registered: Default::default(), - error: Default::default(), - done: Default::default(), - } - } - - pub fn new_create_wallet() -> Self { - Self::new(true) - } - - pub fn new_import_wallet() -> Self { - Self::new(false) - } -} - -impl Step for RegisterDescriptor { - fn load_context(&mut self, ctx: &Context) { - // we reset device registered set if the descriptor have changed. - if self.descriptor != ctx.descriptor { - self.registered = Default::default(); - self.done = false; - } - self.descriptor.clone_from(&ctx.descriptor); - let mut map = HashMap::new(); - for key in ctx.keys.iter().filter(|k| !k.name.is_empty()) { - map.insert(key.master_fingerprint, key.name.clone()); - } - } - fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { - match message { - Message::Select(i) => { - if let Some(HardwareWallet::Supported { - device, - fingerprint, - .. - }) = hws.list.get(i) - { - if !self.registered.contains(fingerprint) { - let descriptor = self.descriptor.as_ref().unwrap(); - let name = wallet_name(descriptor); - self.chosen_hw = Some(i); - self.processing = true; - self.error = None; - return Command::perform( - register_wallet( - device.clone(), - *fingerprint, - name, - descriptor.to_string(), - ), - Message::WalletRegistered, - ); - } - } - } - Message::WalletRegistered(res) => { - self.processing = false; - self.chosen_hw = None; - match res { - Ok((fingerprint, hmac)) => { - if let Some(hw_h) = hws - .list - .iter() - .find(|hw_h| hw_h.fingerprint() == Some(fingerprint)) - { - self.registered.insert(fingerprint); - self.hmacs.push((fingerprint, *hw_h.kind(), hmac)); - } - } - Err(e) => { - if !matches!(e, Error::HardwareWallet(async_hwi::Error::UserRefused)) { - self.error = Some(e) - } - } - } - } - Message::Reload => { - return self.load(); - } - Message::UserActionDone(done) => { - self.done = done; - } - _ => {} - }; - Command::none() - } - fn skip(&self, ctx: &Context) -> bool { - !ctx.hw_is_used - } - fn apply(&mut self, ctx: &mut Context) -> bool { - for (fingerprint, kind, token) in &self.hmacs { - ctx.hws.push((*kind, *fingerprint, *token)); - } - true - } - fn subscription(&self, hws: &HardwareWallets) -> Subscription { - hws.refresh().map(Message::HardwareWallets) - } - fn load(&self) -> Command { - Command::none() - } - fn view<'a>( - &'a self, - hws: &'a HardwareWallets, - progress: (usize, usize), - email: Option<&'a str>, - ) -> Element<'a, Message> { - let desc = self.descriptor.as_ref().unwrap(); - view::register_descriptor( - progress, - email, - desc.to_string(), - &hws.list, - &self.registered, - self.error.as_ref(), - self.processing, - self.chosen_hw, - self.done, - self.created_desc, - ) - } -} - -async fn register_wallet( - hw: std::sync::Arc, - fingerprint: Fingerprint, - name: String, - descriptor: String, -) -> Result<(Fingerprint, Option<[u8; 32]>), Error> { - let hmac = hw - .register_wallet(&name, &descriptor) - .await - .map_err(Error::from)?; - Ok((fingerprint, hmac)) -} - -impl From for Box { - fn from(s: RegisterDescriptor) -> Box { - Box::new(s) - } -} - -#[derive(Default)] -pub struct BackupDescriptor { - done: bool, - descriptor: Option, - key_aliases: HashMap, -} - -impl Step for BackupDescriptor { - fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { - if let Message::UserActionDone(done) = message { - self.done = done; - } - Command::none() - } - fn load_context(&mut self, ctx: &Context) { - if self.descriptor != ctx.descriptor { - self.descriptor.clone_from(&ctx.descriptor); - self.done = false; - } - self.key_aliases = ctx - .keys - .iter() - .cloned() - .map(|k| (k.master_fingerprint, k.name)) - .collect() - } - fn view<'a>( - &'a self, - _hws: &'a HardwareWallets, - progress: (usize, usize), - email: Option<&'a str>, - ) -> Element { - view::backup_descriptor( - progress, - email, - self.descriptor.as_ref().expect("Must be a descriptor"), - &self.key_aliases, - self.done, - ) - } -} - -impl From for Box { - fn from(s: BackupDescriptor) -> Box { - Box::new(s) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/gui/src/installer/step/descriptor/mod.rs b/gui/src/installer/step/descriptor/mod.rs new file mode 100644 index 00000000..fe8e27b0 --- /dev/null +++ b/gui/src/installer/step/descriptor/mod.rs @@ -0,0 +1,336 @@ +pub mod editor; + +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; + +use iced::{Command, Subscription}; +use liana::{ + descriptors::LianaDescriptor, + miniscript::bitcoin::{bip32::Fingerprint, Network}, +}; + +use liana_ui::{component::form, widget::Element}; + +use async_hwi::DeviceKind; + +use crate::{ + app::wallet::wallet_name, + hw::{HardwareWallet, HardwareWallets}, + installer::{ + message::{self, Message}, + step::{Context, Step}, + view, Error, + }, +}; + +pub struct ImportDescriptor { + network: Network, + imported_descriptor: form::Value, + wrong_network: bool, + error: Option, +} + +impl ImportDescriptor { + pub fn new(network: Network) -> Self { + Self { + network, + imported_descriptor: form::Value::default(), + wrong_network: false, + error: None, + } + } + + fn check_descriptor(&mut self, network: Network) -> Option { + if !self.imported_descriptor.value.is_empty() { + if let Ok(desc) = LianaDescriptor::from_str(&self.imported_descriptor.value) { + if network == Network::Bitcoin { + self.imported_descriptor.valid = desc.all_xpubs_net_is(network); + } else { + self.imported_descriptor.valid = desc.all_xpubs_net_is(Network::Testnet); + } + if self.imported_descriptor.valid { + self.wrong_network = false; + Some(desc) + } else { + self.wrong_network = true; + None + } + } else { + self.imported_descriptor.valid = false; + self.wrong_network = false; + None + } + } else { + self.wrong_network = false; + self.imported_descriptor.valid = true; + None + } + } +} + +impl Step for ImportDescriptor { + // ImportRemoteWallet is used instead + fn skip(&self, ctx: &Context) -> bool { + ctx.remote_backend.is_some() + } + // form value is set as valid each time it is edited. + // Verification of the values is happening when the user click on Next button. + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { + if let Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) = + message + { + self.imported_descriptor.value = desc; + self.check_descriptor(self.network); + } + Command::none() + } + + fn apply(&mut self, ctx: &mut Context) -> bool { + ctx.bitcoin_config.network = self.network; + // Set to true in order to force the registration process to be shown to user. + ctx.hw_is_used = true; + // descriptor forms for import or creation cannot be both empty or filled. + if let Some(desc) = self.check_descriptor(self.network) { + ctx.descriptor = Some(desc); + true + } else { + false + } + } + + fn view<'a>( + &'a self, + _hws: &'a HardwareWallets, + progress: (usize, usize), + email: Option<&'a str>, + ) -> Element { + view::import_descriptor( + progress, + email, + &self.imported_descriptor, + self.wrong_network, + self.error.as_ref(), + ) + } +} + +impl From for Box { + fn from(s: ImportDescriptor) -> Box { + Box::new(s) + } +} + +pub struct RegisterDescriptor { + descriptor: Option, + processing: bool, + chosen_hw: Option, + hmacs: Vec<(Fingerprint, DeviceKind, Option<[u8; 32]>)>, + registered: HashSet, + error: Option, + done: bool, + /// Whether this step is part of the descriptor creation process. This is used to detect when + /// it's instead shown as part of the descriptor *import* process, where we can't detect + /// whether a signing device is used, to explicit this step is not required if the user isn't + /// using a signing device. + created_desc: bool, +} + +impl RegisterDescriptor { + fn new(created_desc: bool) -> Self { + Self { + created_desc, + descriptor: Default::default(), + processing: Default::default(), + chosen_hw: Default::default(), + hmacs: Default::default(), + registered: Default::default(), + error: Default::default(), + done: Default::default(), + } + } + + pub fn new_create_wallet() -> Self { + Self::new(true) + } + + pub fn new_import_wallet() -> Self { + Self::new(false) + } +} + +impl Step for RegisterDescriptor { + fn load_context(&mut self, ctx: &Context) { + // we reset device registered set if the descriptor have changed. + if self.descriptor != ctx.descriptor { + self.registered = Default::default(); + self.done = false; + } + self.descriptor.clone_from(&ctx.descriptor); + let mut map = HashMap::new(); + for key in ctx.keys.iter().filter(|k| !k.name.is_empty()) { + map.insert(key.master_fingerprint, key.name.clone()); + } + } + fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { + match message { + Message::Select(i) => { + if let Some(HardwareWallet::Supported { + device, + fingerprint, + .. + }) = hws.list.get(i) + { + if !self.registered.contains(fingerprint) { + let descriptor = self.descriptor.as_ref().unwrap(); + let name = wallet_name(descriptor); + self.chosen_hw = Some(i); + self.processing = true; + self.error = None; + return Command::perform( + register_wallet( + device.clone(), + *fingerprint, + name, + descriptor.to_string(), + ), + Message::WalletRegistered, + ); + } + } + } + Message::WalletRegistered(res) => { + self.processing = false; + self.chosen_hw = None; + match res { + Ok((fingerprint, hmac)) => { + if let Some(hw_h) = hws + .list + .iter() + .find(|hw_h| hw_h.fingerprint() == Some(fingerprint)) + { + self.registered.insert(fingerprint); + self.hmacs.push((fingerprint, *hw_h.kind(), hmac)); + } + } + Err(e) => { + if !matches!(e, Error::HardwareWallet(async_hwi::Error::UserRefused)) { + self.error = Some(e) + } + } + } + } + Message::Reload => { + return self.load(); + } + Message::UserActionDone(done) => { + self.done = done; + } + _ => {} + }; + Command::none() + } + fn skip(&self, ctx: &Context) -> bool { + !ctx.hw_is_used + } + fn apply(&mut self, ctx: &mut Context) -> bool { + for (fingerprint, kind, token) in &self.hmacs { + ctx.hws.push((*kind, *fingerprint, *token)); + } + true + } + fn subscription(&self, hws: &HardwareWallets) -> Subscription { + hws.refresh().map(Message::HardwareWallets) + } + fn load(&self) -> Command { + Command::none() + } + fn view<'a>( + &'a self, + hws: &'a HardwareWallets, + progress: (usize, usize), + email: Option<&'a str>, + ) -> Element<'a, Message> { + let desc = self.descriptor.as_ref().unwrap(); + view::register_descriptor( + progress, + email, + desc.to_string(), + &hws.list, + &self.registered, + self.error.as_ref(), + self.processing, + self.chosen_hw, + self.done, + self.created_desc, + ) + } +} + +async fn register_wallet( + hw: std::sync::Arc, + fingerprint: Fingerprint, + name: String, + descriptor: String, +) -> Result<(Fingerprint, Option<[u8; 32]>), Error> { + let hmac = hw + .register_wallet(&name, &descriptor) + .await + .map_err(Error::from)?; + Ok((fingerprint, hmac)) +} + +impl From for Box { + fn from(s: RegisterDescriptor) -> Box { + Box::new(s) + } +} + +#[derive(Default)] +pub struct BackupDescriptor { + done: bool, + descriptor: Option, + key_aliases: HashMap, +} + +impl Step for BackupDescriptor { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { + if let Message::UserActionDone(done) = message { + self.done = done; + } + Command::none() + } + fn load_context(&mut self, ctx: &Context) { + if self.descriptor != ctx.descriptor { + self.descriptor.clone_from(&ctx.descriptor); + self.done = false; + } + self.key_aliases = ctx + .keys + .iter() + .cloned() + .map(|k| (k.master_fingerprint, k.name)) + .collect() + } + fn view<'a>( + &'a self, + _hws: &'a HardwareWallets, + progress: (usize, usize), + email: Option<&'a str>, + ) -> Element { + view::backup_descriptor( + progress, + email, + self.descriptor.as_ref().expect("Must be a descriptor"), + &self.key_aliases, + self.done, + ) + } +} + +impl From for Box { + fn from(s: BackupDescriptor) -> Box { + Box::new(s) + } +} diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index c45e56c5..d47c253a 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -9,7 +9,9 @@ pub use node::{ DefineNode, }; -pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor}; +pub use descriptor::{ + editor::DefineDescriptor, BackupDescriptor, ImportDescriptor, RegisterDescriptor, +}; pub use backend::{ChooseBackend, ImportRemoteWallet, RemoteBackendLogin}; pub use mnemonic::{BackupMnemonic, RecoverMnemonic}; diff --git a/gui/src/installer/step/share_xpubs.rs b/gui/src/installer/step/share_xpubs.rs index 27dabe18..b6c1403d 100644 --- a/gui/src/installer/step/share_xpubs.rs +++ b/gui/src/installer/step/share_xpubs.rs @@ -13,7 +13,7 @@ use crate::{ installer::{ message::Message, step::{ - descriptor::{default_derivation_path, get_extended_pubkey}, + descriptor::editor::{default_derivation_path, get_extended_pubkey}, Context, Step, }, view, Error, From e6f85227ca9020b8390ee5a3e9e07438714bb4d5 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Thu, 26 Sep 2024 16:25:14 +0200 Subject: [PATCH 2/8] installer descriptor editor: add key module --- .../installer/step/descriptor/editor/key.rs | 449 ++++++++++++++++++ .../installer/step/descriptor/editor/mod.rs | 444 +---------------- gui/src/installer/step/share_xpubs.rs | 2 +- 3 files changed, 459 insertions(+), 436 deletions(-) create mode 100644 gui/src/installer/step/descriptor/editor/key.rs diff --git a/gui/src/installer/step/descriptor/editor/key.rs b/gui/src/installer/step/descriptor/editor/key.rs new file mode 100644 index 00000000..c6a910bb --- /dev/null +++ b/gui/src/installer/step/descriptor/editor/key.rs @@ -0,0 +1,449 @@ +use std::collections::HashSet; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +use iced::{Command, Subscription}; +use liana::miniscript::bitcoin::bip32::Xpub; +use liana::miniscript::{ + bitcoin::{ + bip32::{DerivationPath, Fingerprint}, + Network, + }, + descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, DescriptorXKey, Wildcard}, +}; + +use liana_ui::{component::form, widget::Element}; + +use async_hwi::{DeviceKind, Version}; + +use crate::{ + hw::{HardwareWallet, HardwareWallets}, + installer::{ + message::{self, Message}, + view, Error, + }, + signer::Signer, +}; + +pub fn new_multixkey_from_xpub( + xpub: DescriptorXKey, + derivation_index: usize, +) -> DescriptorMultiXKey { + DescriptorMultiXKey { + origin: xpub.origin, + xkey: xpub.xkey, + derivation_paths: DerivPaths::new(vec![ + DerivationPath::from_str(&format!("m/{}", 2 * derivation_index)).unwrap(), + DerivationPath::from_str(&format!("m/{}", 2 * derivation_index + 1)).unwrap(), + ]) + .unwrap(), + wildcard: Wildcard::Unhardened, + } +} + +#[derive(Clone)] +pub struct Key { + pub device_kind: Option, + pub device_version: Option, + pub name: String, + pub fingerprint: Fingerprint, + pub key: DescriptorPublicKey, +} + +pub fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool { + match key { + DescriptorPublicKey::XPub(key) => { + if network == Network::Bitcoin { + key.xkey.network == Network::Bitcoin + } else { + key.xkey.network == Network::Testnet + } + } + DescriptorPublicKey::MultiXPub(key) => { + if network == Network::Bitcoin { + key.xkey.network == Network::Bitcoin + } else { + key.xkey.network == Network::Testnet + } + } + _ => true, + } +} + +pub struct EditXpubModal { + device_must_support_tapminiscript: bool, + /// None if path is primary path + path_index: Option, + key_index: usize, + network: Network, + error: Option, + processing: bool, + + form_name: form::Value, + form_xpub: form::Value, + edit_name: bool, + + other_path_keys: HashSet, + duplicate_master_fg: bool, + + keys: Vec, + hot_signer: Arc>, + hot_signer_fingerprint: Fingerprint, + chosen_signer: Option<(Fingerprint, Option, Option)>, +} + +impl EditXpubModal { + #[allow(clippy::too_many_arguments)] + pub fn new( + device_must_support_tapminiscript: bool, + other_path_keys: HashSet, + key: Option, + path_index: Option, + key_index: usize, + network: Network, + hot_signer: Arc>, + keys: Vec, + ) -> Self { + let hot_signer_fingerprint = hot_signer.lock().unwrap().fingerprint(); + Self { + device_must_support_tapminiscript, + other_path_keys, + form_name: form::Value { + valid: true, + value: key + .map(|fg| { + keys.iter() + .find(|k| k.fingerprint == fg) + .expect("must be stored") + .name + .clone() + }) + .unwrap_or_default(), + }, + form_xpub: form::Value { + valid: true, + value: key + .map(|fg| { + keys.iter() + .find(|k| k.fingerprint == fg) + .expect("must be stored") + .key + .to_string() + }) + .unwrap_or_default(), + }, + keys, + path_index, + key_index, + processing: false, + error: None, + network, + edit_name: false, + chosen_signer: key.map(|k| (k, None, None)), + hot_signer_fingerprint, + hot_signer, + duplicate_master_fg: false, + } + } + + pub fn load(&self) -> Command { + Command::none() + } +} + +impl super::DescriptorEditModal for EditXpubModal { + fn processing(&self) -> bool { + self.processing + } + + fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { + // Reset these fields. + // the fonction will setup them again if something is wrong + self.duplicate_master_fg = false; + self.error = None; + match message { + Message::Select(i) => { + if let Some(HardwareWallet::Supported { + device, + fingerprint, + kind, + version, + .. + }) = hws.list.get(i) + { + self.chosen_signer = Some((*fingerprint, Some(*kind), version.clone())); + self.processing = true; + return Command::perform( + get_extended_pubkey(device.clone(), *fingerprint, self.network), + |res| { + Message::DefineDescriptor(message::DefineDescriptor::KeyModal( + message::ImportKeyModal::HWXpubImported(res), + )) + }, + ); + } + } + Message::Reload => { + return self.load(); + } + Message::UseHotSigner => { + let fingerprint = self.hot_signer.lock().unwrap().fingerprint(); + self.chosen_signer = Some((fingerprint, None, None)); + self.form_xpub.valid = true; + if let Some(alias) = self + .keys + .iter() + .find(|key| key.fingerprint == fingerprint) + .map(|k| k.name.clone()) + { + self.form_name.valid = true; + self.form_name.value = alias; + self.edit_name = false; + } else { + self.edit_name = true; + self.form_name.value = String::new(); + } + let derivation_path = default_derivation_path(self.network); + self.form_xpub.value = format!( + "[{}{}]{}", + fingerprint, + derivation_path.to_string().trim_start_matches('m'), + self.hot_signer + .lock() + .unwrap() + .get_extended_pubkey(&derivation_path) + ); + } + Message::DefineDescriptor(message::DefineDescriptor::KeyModal(msg)) => match msg { + message::ImportKeyModal::HWXpubImported(res) => { + self.processing = false; + match res { + Ok(key) => { + if let Some(alias) = self + .keys + .iter() + .find(|k| k.fingerprint == key.master_fingerprint()) + .map(|k| k.name.clone()) + { + self.form_name.valid = true; + self.form_name.value = alias; + self.edit_name = false; + } else { + self.edit_name = true; + self.form_name.value = String::new(); + } + self.form_xpub.valid = check_key_network(&key, self.network); + self.form_xpub.value = key.to_string(); + } + Err(e) => { + self.chosen_signer = None; + self.error = Some(e); + } + } + } + message::ImportKeyModal::EditName => { + self.edit_name = true; + } + message::ImportKeyModal::NameEdited(name) => { + self.form_name.valid = true; + self.form_name.value = name; + } + message::ImportKeyModal::XPubEdited(s) => { + if let Ok(DescriptorPublicKey::XPub(key)) = DescriptorPublicKey::from_str(&s) { + self.chosen_signer = None; + if !key.derivation_path.is_master() { + self.form_xpub.valid = false; + } else if let Some((fingerprint, _)) = key.origin { + self.form_xpub.valid = if self.network == Network::Bitcoin { + key.xkey.network == Network::Bitcoin + } else { + key.xkey.network == Network::Testnet + }; + if let Some(alias) = self + .keys + .iter() + .find(|k| k.fingerprint == fingerprint) + .map(|k| k.name.clone()) + { + self.form_name.valid = true; + self.form_name.value = alias; + self.edit_name = false; + } else { + self.edit_name = true; + } + } else { + self.form_xpub.valid = false; + } + } else { + self.form_xpub.valid = false; + } + self.form_xpub.value = s; + } + message::ImportKeyModal::ConfirmXpub => { + if let Ok(key) = DescriptorPublicKey::from_str(&self.form_xpub.value) { + let key_index = self.key_index; + let name = self.form_name.value.clone(); + let (device_kind, device_version) = + if let Some((_, kind, version)) = &self.chosen_signer { + (*kind, version.clone()) + } else { + (None, None) + }; + if self.other_path_keys.contains(&key.master_fingerprint()) { + self.duplicate_master_fg = true; + } else if let Some(path_index) = self.path_index { + return Command::perform( + async move { (path_index, key_index, key) }, + move |(path_index, key_index, key)| { + message::DefineDescriptor::RecoveryPath( + path_index, + message::DefinePath::Key( + key_index, + message::DefineKey::Edited( + name, + key, + device_kind, + device_version, + ), + ), + ) + }, + ) + .map(Message::DefineDescriptor); + } else { + return Command::perform( + async move { (key_index, key) }, + move |(key_index, key)| { + message::DefineDescriptor::PrimaryPath( + message::DefinePath::Key( + key_index, + message::DefineKey::Edited( + name, + key, + device_kind, + device_version, + ), + ), + ) + }, + ) + .map(Message::DefineDescriptor); + } + } + } + message::ImportKeyModal::SelectKey(i) => { + if let Some(key) = self.keys.get(i) { + self.chosen_signer = + Some((key.fingerprint, key.device_kind, key.device_version.clone())); + self.form_xpub.value = key.key.to_string(); + self.form_xpub.valid = true; + self.form_name.value.clone_from(&key.name); + self.form_name.valid = true; + } + } + }, + _ => {} + }; + Command::none() + } + + fn subscription(&self, hws: &HardwareWallets) -> Subscription { + hws.refresh().map(Message::HardwareWallets) + } + + fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> { + let chosen_signer = self.chosen_signer.as_ref().map(|s| s.0); + view::edit_key_modal( + self.network, + hws.list + .iter() + .enumerate() + .filter_map(|(i, hw)| { + if self + .keys + .iter() + .any(|k| Some(k.fingerprint) == hw.fingerprint()) + { + None + } else { + Some(view::hw_list_view( + i, + hw, + hw.fingerprint() == chosen_signer, + self.processing, + !self.processing + && hw.fingerprint() == chosen_signer + && self.form_xpub.valid + && !self.form_xpub.value.is_empty(), + self.device_must_support_tapminiscript, + )) + } + }) + .collect(), + self.keys + .iter() + .enumerate() + .filter_map(|(i, key)| { + if key.fingerprint == self.hot_signer_fingerprint { + None + } else { + Some(view::key_list_view( + i, + &key.name, + &key.fingerprint, + key.device_kind.as_ref(), + key.device_version.as_ref(), + Some(key.fingerprint) == chosen_signer, + self.device_must_support_tapminiscript, + )) + } + }) + .collect(), + self.error.as_ref(), + self.chosen_signer.as_ref().map(|s| s.0), + &self.hot_signer_fingerprint, + self.keys.iter().find_map(|k| { + if k.fingerprint == self.hot_signer_fingerprint { + Some(&k.name) + } else { + None + } + }), + &self.form_xpub, + &self.form_name, + self.edit_name, + self.duplicate_master_fg, + ) + } +} + +pub fn default_derivation_path(network: Network) -> DerivationPath { + DerivationPath::from_str({ + if network == Network::Bitcoin { + "m/48'/0'/0'/2'" + } else { + "m/48'/1'/0'/2'" + } + }) + .unwrap() +} + +/// LIANA_STANDARD_PATH: m/48'/0'/0'/2'; +/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2'; +pub async fn get_extended_pubkey( + hw: std::sync::Arc, + fingerprint: Fingerprint, + network: Network, +) -> Result { + let derivation_path = default_derivation_path(network); + let xkey = hw + .get_extended_pubkey(&derivation_path) + .await + .map_err(Error::from)?; + Ok(DescriptorPublicKey::XPub(DescriptorXKey { + origin: Some((fingerprint, derivation_path)), + derivation_path: DerivationPath::master(), + wildcard: Wildcard::None, + xkey, + })) +} diff --git a/gui/src/installer/step/descriptor/editor/mod.rs b/gui/src/installer/step/descriptor/editor/mod.rs index 4037ef3a..233cad63 100644 --- a/gui/src/installer/step/descriptor/editor/mod.rs +++ b/gui/src/installer/step/descriptor/editor/mod.rs @@ -1,20 +1,16 @@ +pub mod key; + use std::collections::{BTreeMap, HashMap, HashSet}; use std::iter::FromIterator; use std::str::FromStr; use std::sync::{Arc, Mutex}; use iced::{Command, Subscription}; -use liana::miniscript::bitcoin::bip32::Xpub; use liana::{ descriptors::{LianaDescriptor, LianaPolicy, PathInfo}, miniscript::{ - bitcoin::{ - bip32::{DerivationPath, Fingerprint}, - Network, - }, - descriptor::{ - DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, DescriptorXKey, Wildcard, - }, + bitcoin::{bip32::Fingerprint, Network}, + descriptor::DescriptorPublicKey, }, }; @@ -23,20 +19,20 @@ use liana_ui::{ widget::Element, }; -use async_hwi::{DeviceKind, Version}; - use crate::hw; use crate::{ app::settings::KeySetting, - hw::{HardwareWallet, HardwareWallets}, + hw::HardwareWallets, installer::{ message::{self, Message}, step::{Context, Step}, - view, Error, + view, }, signer::Signer, }; +use key::{check_key_network, new_multixkey_from_xpub, EditXpubModal, Key}; + pub trait DescriptorEditModal { fn processing(&self) -> bool { false @@ -609,51 +605,6 @@ impl Step for DefineDescriptor { } } -fn new_multixkey_from_xpub( - xpub: DescriptorXKey, - derivation_index: usize, -) -> DescriptorMultiXKey { - DescriptorMultiXKey { - origin: xpub.origin, - xkey: xpub.xkey, - derivation_paths: DerivPaths::new(vec![ - DerivationPath::from_str(&format!("m/{}", 2 * derivation_index)).unwrap(), - DerivationPath::from_str(&format!("m/{}", 2 * derivation_index + 1)).unwrap(), - ]) - .unwrap(), - wildcard: Wildcard::Unhardened, - } -} - -#[derive(Clone)] -pub struct Key { - pub device_kind: Option, - pub device_version: Option, - pub name: String, - pub fingerprint: Fingerprint, - pub key: DescriptorPublicKey, -} - -fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool { - match key { - DescriptorPublicKey::XPub(key) => { - if network == Network::Bitcoin { - key.xkey.network == Network::Bitcoin - } else { - key.xkey.network == Network::Testnet - } - } - DescriptorPublicKey::MultiXPub(key) => { - if network == Network::Bitcoin { - key.xkey.network == Network::Bitcoin - } else { - key.xkey.network == Network::Testnet - } - } - _ => true, - } -} - impl From for Box { fn from(s: DefineDescriptor) -> Box { Box::new(s) @@ -720,383 +671,6 @@ impl DescriptorEditModal for EditSequenceModal { } } -pub struct EditXpubModal { - device_must_support_tapminiscript: bool, - /// None if path is primary path - path_index: Option, - key_index: usize, - network: Network, - error: Option, - processing: bool, - - form_name: form::Value, - form_xpub: form::Value, - edit_name: bool, - - other_path_keys: HashSet, - duplicate_master_fg: bool, - - keys: Vec, - hot_signer: Arc>, - hot_signer_fingerprint: Fingerprint, - chosen_signer: Option<(Fingerprint, Option, Option)>, -} - -impl EditXpubModal { - #[allow(clippy::too_many_arguments)] - fn new( - device_must_support_tapminiscript: bool, - other_path_keys: HashSet, - key: Option, - path_index: Option, - key_index: usize, - network: Network, - hot_signer: Arc>, - keys: Vec, - ) -> Self { - let hot_signer_fingerprint = hot_signer.lock().unwrap().fingerprint(); - Self { - device_must_support_tapminiscript, - other_path_keys, - form_name: form::Value { - valid: true, - value: key - .map(|fg| { - keys.iter() - .find(|k| k.fingerprint == fg) - .expect("must be stored") - .name - .clone() - }) - .unwrap_or_default(), - }, - form_xpub: form::Value { - valid: true, - value: key - .map(|fg| { - keys.iter() - .find(|k| k.fingerprint == fg) - .expect("must be stored") - .key - .to_string() - }) - .unwrap_or_default(), - }, - keys, - path_index, - key_index, - processing: false, - error: None, - network, - edit_name: false, - chosen_signer: key.map(|k| (k, None, None)), - hot_signer_fingerprint, - hot_signer, - duplicate_master_fg: false, - } - } - fn load(&self) -> Command { - Command::none() - } -} - -impl DescriptorEditModal for EditXpubModal { - fn processing(&self) -> bool { - self.processing - } - - fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { - // Reset these fields. - // the fonction will setup them again if something is wrong - self.duplicate_master_fg = false; - self.error = None; - match message { - Message::Select(i) => { - if let Some(HardwareWallet::Supported { - device, - fingerprint, - kind, - version, - .. - }) = hws.list.get(i) - { - self.chosen_signer = Some((*fingerprint, Some(*kind), version.clone())); - self.processing = true; - return Command::perform( - get_extended_pubkey(device.clone(), *fingerprint, self.network), - |res| { - Message::DefineDescriptor(message::DefineDescriptor::KeyModal( - message::ImportKeyModal::HWXpubImported(res), - )) - }, - ); - } - } - Message::Reload => { - return self.load(); - } - Message::UseHotSigner => { - let fingerprint = self.hot_signer.lock().unwrap().fingerprint(); - self.chosen_signer = Some((fingerprint, None, None)); - self.form_xpub.valid = true; - if let Some(alias) = self - .keys - .iter() - .find(|key| key.fingerprint == fingerprint) - .map(|k| k.name.clone()) - { - self.form_name.valid = true; - self.form_name.value = alias; - self.edit_name = false; - } else { - self.edit_name = true; - self.form_name.value = String::new(); - } - let derivation_path = default_derivation_path(self.network); - self.form_xpub.value = format!( - "[{}{}]{}", - fingerprint, - derivation_path.to_string().trim_start_matches('m'), - self.hot_signer - .lock() - .unwrap() - .get_extended_pubkey(&derivation_path) - ); - } - Message::DefineDescriptor(message::DefineDescriptor::KeyModal(msg)) => match msg { - message::ImportKeyModal::HWXpubImported(res) => { - self.processing = false; - match res { - Ok(key) => { - if let Some(alias) = self - .keys - .iter() - .find(|k| k.fingerprint == key.master_fingerprint()) - .map(|k| k.name.clone()) - { - self.form_name.valid = true; - self.form_name.value = alias; - self.edit_name = false; - } else { - self.edit_name = true; - self.form_name.value = String::new(); - } - self.form_xpub.valid = check_key_network(&key, self.network); - self.form_xpub.value = key.to_string(); - } - Err(e) => { - self.chosen_signer = None; - self.error = Some(e); - } - } - } - message::ImportKeyModal::EditName => { - self.edit_name = true; - } - message::ImportKeyModal::NameEdited(name) => { - self.form_name.valid = true; - self.form_name.value = name; - } - message::ImportKeyModal::XPubEdited(s) => { - if let Ok(DescriptorPublicKey::XPub(key)) = DescriptorPublicKey::from_str(&s) { - self.chosen_signer = None; - if !key.derivation_path.is_master() { - self.form_xpub.valid = false; - } else if let Some((fingerprint, _)) = key.origin { - self.form_xpub.valid = if self.network == Network::Bitcoin { - key.xkey.network == Network::Bitcoin - } else { - key.xkey.network == Network::Testnet - }; - if let Some(alias) = self - .keys - .iter() - .find(|k| k.fingerprint == fingerprint) - .map(|k| k.name.clone()) - { - self.form_name.valid = true; - self.form_name.value = alias; - self.edit_name = false; - } else { - self.edit_name = true; - } - } else { - self.form_xpub.valid = false; - } - } else { - self.form_xpub.valid = false; - } - self.form_xpub.value = s; - } - message::ImportKeyModal::ConfirmXpub => { - if let Ok(key) = DescriptorPublicKey::from_str(&self.form_xpub.value) { - let key_index = self.key_index; - let name = self.form_name.value.clone(); - let (device_kind, device_version) = - if let Some((_, kind, version)) = &self.chosen_signer { - (*kind, version.clone()) - } else { - (None, None) - }; - if self.other_path_keys.contains(&key.master_fingerprint()) { - self.duplicate_master_fg = true; - } else if let Some(path_index) = self.path_index { - return Command::perform( - async move { (path_index, key_index, key) }, - move |(path_index, key_index, key)| { - message::DefineDescriptor::RecoveryPath( - path_index, - message::DefinePath::Key( - key_index, - message::DefineKey::Edited( - name, - key, - device_kind, - device_version, - ), - ), - ) - }, - ) - .map(Message::DefineDescriptor); - } else { - return Command::perform( - async move { (key_index, key) }, - move |(key_index, key)| { - message::DefineDescriptor::PrimaryPath( - message::DefinePath::Key( - key_index, - message::DefineKey::Edited( - name, - key, - device_kind, - device_version, - ), - ), - ) - }, - ) - .map(Message::DefineDescriptor); - } - } - } - message::ImportKeyModal::SelectKey(i) => { - if let Some(key) = self.keys.get(i) { - self.chosen_signer = - Some((key.fingerprint, key.device_kind, key.device_version.clone())); - self.form_xpub.value = key.key.to_string(); - self.form_xpub.valid = true; - self.form_name.value.clone_from(&key.name); - self.form_name.valid = true; - } - } - }, - _ => {} - }; - Command::none() - } - - fn subscription(&self, hws: &HardwareWallets) -> Subscription { - hws.refresh().map(Message::HardwareWallets) - } - - fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> { - let chosen_signer = self.chosen_signer.as_ref().map(|s| s.0); - view::edit_key_modal( - self.network, - hws.list - .iter() - .enumerate() - .filter_map(|(i, hw)| { - if self - .keys - .iter() - .any(|k| Some(k.fingerprint) == hw.fingerprint()) - { - None - } else { - Some(view::hw_list_view( - i, - hw, - hw.fingerprint() == chosen_signer, - self.processing, - !self.processing - && hw.fingerprint() == chosen_signer - && self.form_xpub.valid - && !self.form_xpub.value.is_empty(), - self.device_must_support_tapminiscript, - )) - } - }) - .collect(), - self.keys - .iter() - .enumerate() - .filter_map(|(i, key)| { - if key.fingerprint == self.hot_signer_fingerprint { - None - } else { - Some(view::key_list_view( - i, - &key.name, - &key.fingerprint, - key.device_kind.as_ref(), - key.device_version.as_ref(), - Some(key.fingerprint) == chosen_signer, - self.device_must_support_tapminiscript, - )) - } - }) - .collect(), - self.error.as_ref(), - self.chosen_signer.as_ref().map(|s| s.0), - &self.hot_signer_fingerprint, - self.keys.iter().find_map(|k| { - if k.fingerprint == self.hot_signer_fingerprint { - Some(&k.name) - } else { - None - } - }), - &self.form_xpub, - &self.form_name, - self.edit_name, - self.duplicate_master_fg, - ) - } -} - -pub fn default_derivation_path(network: Network) -> DerivationPath { - DerivationPath::from_str({ - if network == Network::Bitcoin { - "m/48'/0'/0'/2'" - } else { - "m/48'/1'/0'/2'" - } - }) - .unwrap() -} - -/// LIANA_STANDARD_PATH: m/48'/0'/0'/2'; -/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2'; -pub async fn get_extended_pubkey( - hw: std::sync::Arc, - fingerprint: Fingerprint, - network: Network, -) -> Result { - let derivation_path = default_derivation_path(network); - let xkey = hw - .get_extended_pubkey(&derivation_path) - .await - .map_err(Error::from)?; - Ok(DescriptorPublicKey::XPub(DescriptorXKey { - origin: Some((fingerprint, derivation_path)), - derivation_path: DerivationPath::master(), - wildcard: Wildcard::None, - xkey, - })) -} - #[cfg(test)] mod tests { use super::*; @@ -1239,7 +813,7 @@ mod tests { message::DefineKey::Edited( "My Specter key".to_string(), DescriptorPublicKey::from_str("[4df3f0e3/84'/0'/0']tpubDDRs9DnRUiJc4hq92PSJKhfzQBgHJUrDo7T2i48smsDfLsQcm3Vh7JhuGqJv8zozVkNFin8YPgpmn2NWNmpRaE3GW2pSxbmAzYf2juy7LeW").unwrap(), - Some(DeviceKind::Specter), + Some(async_hwi::DeviceKind::Specter), None, ), ); diff --git a/gui/src/installer/step/share_xpubs.rs b/gui/src/installer/step/share_xpubs.rs index b6c1403d..a219a71e 100644 --- a/gui/src/installer/step/share_xpubs.rs +++ b/gui/src/installer/step/share_xpubs.rs @@ -13,7 +13,7 @@ use crate::{ installer::{ message::Message, step::{ - descriptor::editor::{default_derivation_path, get_extended_pubkey}, + descriptor::editor::key::{default_derivation_path, get_extended_pubkey}, Context, Step, }, view, Error, From 0b1932c9fac6afc63021f57ae47ee3b42125efae Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 1 Oct 2024 15:04:51 +0200 Subject: [PATCH 3/8] keep key hot signer origin --- gui/src/installer/step/descriptor/editor/key.rs | 3 ++- gui/src/installer/step/descriptor/editor/mod.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gui/src/installer/step/descriptor/editor/key.rs b/gui/src/installer/step/descriptor/editor/key.rs index c6a910bb..8cc6849f 100644 --- a/gui/src/installer/step/descriptor/editor/key.rs +++ b/gui/src/installer/step/descriptor/editor/key.rs @@ -44,6 +44,7 @@ pub fn new_multixkey_from_xpub( #[derive(Clone)] pub struct Key { pub device_kind: Option, + pub is_hot_signer: bool, pub device_version: Option, pub name: String, pub fingerprint: Fingerprint, @@ -102,9 +103,9 @@ impl EditXpubModal { key_index: usize, network: Network, hot_signer: Arc>, + hot_signer_fingerprint: Fingerprint, keys: Vec, ) -> Self { - let hot_signer_fingerprint = hot_signer.lock().unwrap().fingerprint(); Self { device_must_support_tapminiscript, other_path_keys, diff --git a/gui/src/installer/step/descriptor/editor/mod.rs b/gui/src/installer/step/descriptor/editor/mod.rs index 233cad63..6aec613d 100644 --- a/gui/src/installer/step/descriptor/editor/mod.rs +++ b/gui/src/installer/step/descriptor/editor/mod.rs @@ -198,17 +198,20 @@ pub struct DefineDescriptor { modal: Option>, signer: Arc>, + signer_fingerprint: Fingerprint, error: Option, } impl DefineDescriptor { pub fn new(network: Network, signer: Arc>) -> Self { + let signer_fingerprint = signer.lock().unwrap().fingerprint(); Self { network, use_taproot: false, setup: Setup::new(), modal: None, + signer_fingerprint, signer, error: None, } @@ -259,6 +262,7 @@ impl Step for DefineDescriptor { } message::DefineKey::Edited(name, imported_key, kind, version) => { let fingerprint = imported_key.master_fingerprint(); + let is_hot_signer = self.signer_fingerprint == fingerprint; hws.set_alias(fingerprint, name.clone()); if let Some(key) = self .setup_mut() @@ -269,6 +273,7 @@ impl Step for DefineDescriptor { key.name = name; } else { self.setup_mut().keys.push(Key { + is_hot_signer, fingerprint, name, key: imported_key, @@ -300,6 +305,7 @@ impl Step for DefineDescriptor { i, network, self.signer.clone(), + self.signer_fingerprint, self.setup_mut() .keys .iter() @@ -354,6 +360,7 @@ impl Step for DefineDescriptor { } message::DefineKey::Edited(name, imported_key, kind, version) => { let fingerprint = imported_key.master_fingerprint(); + let is_hot_signer = self.signer_fingerprint == fingerprint; hws.set_alias(fingerprint, name.clone()); if let Some(key) = self .setup_mut() @@ -362,9 +369,13 @@ impl Step for DefineDescriptor { .find(|k| k.fingerprint == fingerprint) { key.name = name; + key.is_hot_signer = is_hot_signer; + key.device_kind = kind; + key.device_version = version; } else { self.setup_mut().keys.push(Key { fingerprint, + is_hot_signer, name, key: imported_key, device_kind: kind, @@ -396,6 +407,7 @@ impl Step for DefineDescriptor { j, self.network, self.signer.clone(), + self.signer_fingerprint, self.setup.keys.clone(), ); let cmd = modal.load(); From 5fabd987e8ade7fae38f47d34b986c3ea13718c8 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 1 Oct 2024 15:16:11 +0200 Subject: [PATCH 4/8] move installer view module in a directory --- gui/src/installer/{view.rs => view/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gui/src/installer/{view.rs => view/mod.rs} (100%) diff --git a/gui/src/installer/view.rs b/gui/src/installer/view/mod.rs similarity index 100% rename from gui/src/installer/view.rs rename to gui/src/installer/view/mod.rs From d0ec811bef457bfcb266e6d39c7aaec07e34c8e0 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 1 Oct 2024 15:46:37 +0200 Subject: [PATCH 5/8] installer: refac setup as a list of paths --- gui/src/installer/message.rs | 3 +- .../installer/step/descriptor/editor/key.rs | 30 +- .../installer/step/descriptor/editor/mod.rs | 466 +++++------- gui/src/installer/view/editor.rs | 710 +++++++++++++++++ gui/src/installer/view/mod.rs | 711 +----------------- 5 files changed, 905 insertions(+), 1015 deletions(-) create mode 100644 gui/src/installer/view/editor.rs diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 690f58dc..3e7939cb 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -110,8 +110,7 @@ pub enum InternalBitcoindMsg { #[derive(Debug, Clone)] pub enum DefineDescriptor { ImportDescriptor(String), - PrimaryPath(DefinePath), - RecoveryPath(usize, DefinePath), + Path(usize, DefinePath), AddRecoveryPath, KeyModal(ImportKeyModal), SequenceModal(SequenceModal), diff --git a/gui/src/installer/step/descriptor/editor/key.rs b/gui/src/installer/step/descriptor/editor/key.rs index 8cc6849f..e88195ca 100644 --- a/gui/src/installer/step/descriptor/editor/key.rs +++ b/gui/src/installer/step/descriptor/editor/key.rs @@ -73,8 +73,7 @@ pub fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool { pub struct EditXpubModal { device_must_support_tapminiscript: bool, - /// None if path is primary path - path_index: Option, + path_index: usize, key_index: usize, network: Network, error: Option, @@ -99,7 +98,7 @@ impl EditXpubModal { device_must_support_tapminiscript: bool, other_path_keys: HashSet, key: Option, - path_index: Option, + path_index: usize, key_index: usize, network: Network, hot_signer: Arc>, @@ -292,11 +291,12 @@ impl super::DescriptorEditModal for EditXpubModal { }; if self.other_path_keys.contains(&key.master_fingerprint()) { self.duplicate_master_fg = true; - } else if let Some(path_index) = self.path_index { + } else { + let path_index = self.path_index; return Command::perform( async move { (path_index, key_index, key) }, move |(path_index, key_index, key)| { - message::DefineDescriptor::RecoveryPath( + message::DefineDescriptor::Path( path_index, message::DefinePath::Key( key_index, @@ -311,24 +311,6 @@ impl super::DescriptorEditModal for EditXpubModal { }, ) .map(Message::DefineDescriptor); - } else { - return Command::perform( - async move { (key_index, key) }, - move |(key_index, key)| { - message::DefineDescriptor::PrimaryPath( - message::DefinePath::Key( - key_index, - message::DefineKey::Edited( - name, - key, - device_kind, - device_version, - ), - ), - ) - }, - ) - .map(Message::DefineDescriptor); } } } @@ -354,7 +336,7 @@ impl super::DescriptorEditModal for EditXpubModal { fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> { let chosen_signer = self.chosen_signer.as_ref().map(|s| s.0); - view::edit_key_modal( + view::editor::edit_key_modal( self.network, hws.list .iter() diff --git a/gui/src/installer/step/descriptor/editor/mod.rs b/gui/src/installer/step/descriptor/editor/mod.rs index 6aec613d..493622cc 100644 --- a/gui/src/installer/step/descriptor/editor/mod.rs +++ b/gui/src/installer/step/descriptor/editor/mod.rs @@ -31,7 +31,7 @@ use crate::{ signer::Signer, }; -use key::{check_key_network, new_multixkey_from_xpub, EditXpubModal, Key}; +use key::{new_multixkey_from_xpub, EditXpubModal, Key}; pub trait DescriptorEditModal { fn processing(&self) -> bool { @@ -46,19 +46,28 @@ pub trait DescriptorEditModal { } } -pub struct RecoveryPath { +pub struct Path { keys: Vec>, threshold: usize, - sequence: u16, + sequence: Option, duplicate_sequence: bool, } -impl RecoveryPath { - pub fn new() -> Self { +impl Path { + pub fn new_primary_path() -> Self { Self { keys: vec![None], threshold: 1, - sequence: u16::MAX, + sequence: None, + duplicate_sequence: false, + } + } + + pub fn new_recovery_path() -> Self { + Self { + keys: vec![None], + threshold: 1, + sequence: Some(u16::MAX), duplicate_sequence: false, } } @@ -73,57 +82,92 @@ impl RecoveryPath { duplicate_name: &HashSet, incompatible_with_tapminiscript: &HashSet, ) -> Element { - view::recovery_path_view( - self.sequence, - self.duplicate_sequence, - self.threshold, - self.keys - .iter() - .enumerate() - .map(|(i, key)| { - if let Some(key) = key { - view::defined_descriptor_key( - aliases.get(key).unwrap().to_string(), - duplicate_name.contains(key), - incompatible_with_tapminiscript.contains(key), - ) - } else { - view::undefined_descriptor_key() - } - .map(move |msg| message::DefinePath::Key(i, msg)) - }) - .collect(), - ) + if let Some(sequence) = self.sequence { + view::editor::recovery_path_view( + sequence, + self.duplicate_sequence, + self.threshold, + self.keys + .iter() + .enumerate() + .map(|(i, key)| { + if let Some(key) = key { + view::editor::defined_descriptor_key( + aliases.get(key).unwrap().to_string(), + duplicate_name.contains(key), + incompatible_with_tapminiscript.contains(key), + ) + } else { + view::editor::undefined_descriptor_key() + } + .map(move |msg| message::DefinePath::Key(i, msg)) + }) + .collect(), + ) + } else { + view::editor::primary_path_view( + self.threshold, + self.keys + .iter() + .enumerate() + .map(|(i, key)| { + if let Some(key) = key { + view::editor::defined_descriptor_key( + aliases.get(key).unwrap().to_string(), + duplicate_name.contains(key), + incompatible_with_tapminiscript.contains(key), + ) + } else { + view::editor::undefined_descriptor_key() + } + .map(move |msg| message::DefinePath::Key(i, msg)) + }) + .collect(), + ) + } } } -struct Setup { +pub struct DefineDescriptor { + network: Network, + use_taproot: bool, + + modal: Option>, + signer: Arc>, + signer_fingerprint: Fingerprint, + keys: Vec, duplicate_name: HashSet, incompatible_with_tapminiscript: HashSet, - spending_keys: Vec>, - spending_threshold: usize, - recovery_paths: Vec, + paths: Vec, + + error: Option, } -impl Setup { - fn new() -> Self { +impl DefineDescriptor { + pub fn new(network: Network, signer: Arc>) -> Self { + let signer_fingerprint = signer.lock().unwrap().fingerprint(); Self { + network, + use_taproot: false, + modal: None, + signer_fingerprint, + + signer, + error: None, keys: Vec::new(), duplicate_name: HashSet::new(), incompatible_with_tapminiscript: HashSet::new(), - spending_keys: vec![None], - spending_threshold: 1, - recovery_paths: vec![RecoveryPath::new()], + paths: vec![Path::new_primary_path(), Path::new_recovery_path()], } } - fn valid(&self) -> bool { - !self.spending_keys.is_empty() - && !self.spending_keys.iter().any(|k| k.is_none()) - && !self.recovery_paths.iter().any(|path| !path.valid()) - && self.duplicate_name.is_empty() - && self.incompatible_with_tapminiscript.is_empty() + fn keys_aliases(&self) -> HashMap { + let mut map = HashMap::new(); + for key in &self.keys { + map.insert(key.key.master_fingerprint(), key.name.clone()); + } + map } // Mark as duplicate every defined key that have the same name but not the same fingerprint. @@ -140,17 +184,21 @@ impl Setup { } let mut all_sequence = HashSet::new(); - let mut duplicate_sequence = HashSet::new(); - for path in &mut self.recovery_paths { - if all_sequence.contains(&path.sequence) { - duplicate_sequence.insert(path.sequence); - } else { - all_sequence.insert(path.sequence); + let mut duplicate_sequences = HashSet::new(); + for path in &mut self.paths { + if let Some(sequence) = path.sequence { + if all_sequence.contains(&sequence) { + duplicate_sequences.insert(sequence); + } else { + all_sequence.insert(sequence); + } } } - for path in &mut self.recovery_paths { - path.duplicate_sequence = duplicate_sequence.contains(&path.sequence); + for path in &mut self.paths { + if let Some(sequence) = path.sequence { + path.duplicate_sequence = duplicate_sequences.contains(&sequence); + } } } @@ -160,9 +208,9 @@ impl Setup { for key in &self.keys { // check if key is used by a path if !self - .spending_keys + .paths .iter() - .chain(self.recovery_paths.iter().flat_map(|path| &path.keys)) + .flat_map(|path| &path.keys) .any(|k| *k == Some(key.fingerprint)) { continue; @@ -181,54 +229,17 @@ impl Setup { } } - fn keys_aliases(&self) -> HashMap { - let mut map = HashMap::new(); - for key in &self.keys { - map.insert(key.key.master_fingerprint(), key.name.clone()); - } - map - } -} - -pub struct DefineDescriptor { - setup: Setup, - - network: Network, - use_taproot: bool, - - modal: Option>, - signer: Arc>, - signer_fingerprint: Fingerprint, - - error: Option, -} - -impl DefineDescriptor { - pub fn new(network: Network, signer: Arc>) -> Self { - let signer_fingerprint = signer.lock().unwrap().fingerprint(); - Self { - network, - use_taproot: false, - setup: Setup::new(), - modal: None, - signer_fingerprint, - signer, - error: None, - } - } - fn valid(&self) -> bool { - self.setup.valid() - } - fn setup_mut(&mut self) -> &mut Setup { - &mut self.setup + !self.paths.iter().any(|path| !path.valid()) + && self.duplicate_name.is_empty() + && self.incompatible_with_tapminiscript.is_empty() + && self.paths.len() >= 2 } fn check_setup(&mut self) { - self.setup_mut().check_for_duplicate(); + self.check_for_duplicate(); let use_taproot = self.use_taproot; - self.setup_mut() - .check_for_tapminiscript_support(use_taproot); + self.check_for_tapminiscript_support(use_taproot); } } @@ -246,110 +257,30 @@ impl Step for DefineDescriptor { self.check_setup(); } Message::DefineDescriptor(message::DefineDescriptor::AddRecoveryPath) => { - self.setup_mut().recovery_paths.push(RecoveryPath::new()); + self.paths.push(Path::new_recovery_path()); } - Message::DefineDescriptor(message::DefineDescriptor::PrimaryPath(msg)) => match msg { + Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) => match msg { message::DefinePath::ThresholdEdited(value) => { - self.setup_mut().spending_threshold = value; - } - message::DefinePath::AddKey => { - self.setup_mut().spending_keys.push(None); - self.setup_mut().spending_threshold += 1; - } - message::DefinePath::Key(i, msg) => match msg { - message::DefineKey::Clipboard(key) => { - return Command::perform(async move { key }, Message::Clibpboard); - } - message::DefineKey::Edited(name, imported_key, kind, version) => { - let fingerprint = imported_key.master_fingerprint(); - let is_hot_signer = self.signer_fingerprint == fingerprint; - hws.set_alias(fingerprint, name.clone()); - if let Some(key) = self - .setup_mut() - .keys - .iter_mut() - .find(|k| k.fingerprint == fingerprint) - { - key.name = name; - } else { - self.setup_mut().keys.push(Key { - is_hot_signer, - fingerprint, - name, - key: imported_key, - device_kind: kind, - device_version: version, - }); - } - - self.setup_mut().spending_keys[i] = Some(fingerprint); - - self.modal = None; - self.check_setup(); - } - message::DefineKey::Edit => { - let use_taproot = self.use_taproot; - let network = self.network; - let setup = self.setup_mut(); - let modal = EditXpubModal::new( - use_taproot, - HashSet::from_iter(setup.spending_keys.iter().filter_map(|key| { - if key.is_some() && key != &setup.spending_keys[i] { - *key - } else { - None - } - })), - self.setup_mut().spending_keys[i], - None, - i, - network, - self.signer.clone(), - self.signer_fingerprint, - self.setup_mut() - .keys - .iter() - .filter(|k| check_key_network(&k.key, network)) - .cloned() - .collect(), - ); - let cmd = modal.load(); - self.modal = Some(Box::new(modal)); - return cmd; - } - message::DefineKey::Delete => { - self.setup_mut().spending_keys.remove(i); - if self.setup_mut().spending_threshold - > self.setup_mut().spending_keys.len() - { - self.setup_mut().spending_threshold -= 1; - } - self.check_setup(); - } - }, - _ => {} - }, - Message::DefineDescriptor(message::DefineDescriptor::RecoveryPath(i, msg)) => match msg - { - message::DefinePath::ThresholdEdited(value) => { - if let Some(path) = self.setup_mut().recovery_paths.get_mut(i) { + if let Some(path) = self.paths.get_mut(i) { path.threshold = value; } } message::DefinePath::SequenceEdited(seq) => { self.modal = None; - if let Some(path) = self.setup_mut().recovery_paths.get_mut(i) { - path.sequence = seq; + if let Some(path) = self.paths.get_mut(i) { + path.sequence = Some(seq); } - self.setup_mut().check_for_duplicate(); + self.check_for_duplicate(); } message::DefinePath::EditSequence => { - if let Some(path) = self.setup_mut().recovery_paths.get(i) { - self.modal = Some(Box::new(EditSequenceModal::new(i, path.sequence))); + if let Some(path) = self.paths.get(i) { + if let Some(sequence) = path.sequence { + self.modal = Some(Box::new(EditSequenceModal::new(i, sequence))); + } } } message::DefinePath::AddKey => { - if let Some(path) = self.setup_mut().recovery_paths.get_mut(i) { + if let Some(path) = self.paths.get_mut(i) { path.keys.push(None); path.threshold += 1; } @@ -362,18 +293,15 @@ impl Step for DefineDescriptor { let fingerprint = imported_key.master_fingerprint(); let is_hot_signer = self.signer_fingerprint == fingerprint; hws.set_alias(fingerprint, name.clone()); - if let Some(key) = self - .setup_mut() - .keys - .iter_mut() - .find(|k| k.fingerprint == fingerprint) + if let Some(key) = + self.keys.iter_mut().find(|k| k.fingerprint == fingerprint) { key.name = name; key.is_hot_signer = is_hot_signer; key.device_kind = kind; key.device_version = version; } else { - self.setup_mut().keys.push(Key { + self.keys.push(Key { fingerprint, is_hot_signer, name, @@ -383,52 +311,49 @@ impl Step for DefineDescriptor { }); } - self.setup_mut().recovery_paths[i].keys[j] = Some(fingerprint); + self.paths[i].keys[j] = Some(fingerprint); self.modal = None; self.check_setup(); } message::DefineKey::Edit => { let use_taproot = self.use_taproot; - let setup = self.setup_mut(); + let path = &self.paths[i]; let modal = EditXpubModal::new( use_taproot, - HashSet::from_iter(setup.recovery_paths[i].keys.iter().filter_map( - |key| { - if key.is_some() && key != &setup.recovery_paths[i].keys[j] { - *key - } else { - None - } - }, - )), - setup.recovery_paths[i].keys[j], - Some(i), + HashSet::from_iter(path.keys.iter().filter_map(|key| { + if key.is_some() && key != &path.keys[j] { + *key + } else { + None + } + })), + path.keys[j], + i, j, self.network, self.signer.clone(), self.signer_fingerprint, - self.setup.keys.clone(), + self.keys.clone(), ); let cmd = modal.load(); self.modal = Some(Box::new(modal)); return cmd; } message::DefineKey::Delete => { - if let Some(path) = self.setup_mut().recovery_paths.get_mut(i) { + if let Some(path) = self.paths.get_mut(i) { path.keys.remove(j); if path.threshold > path.keys.len() { path.threshold -= 1; } } if self - .setup_mut() - .recovery_paths + .paths .get(i) .map(|path| path.keys.is_empty()) .unwrap_or(false) { - self.setup_mut().recovery_paths.remove(i); + self.paths.remove(i); } self.check_setup(); } @@ -452,15 +377,18 @@ impl Step for DefineDescriptor { } fn apply(&mut self, ctx: &mut Context) -> bool { + if self.paths.len() < 2 { + return false; + } + ctx.bitcoin_config.network = self.network; ctx.keys = Vec::new(); let mut hw_is_used = false; let mut spending_keys: Vec = Vec::new(); let mut key_derivation_index = HashMap::::new(); - for spending_key in self.setup.spending_keys.iter().clone() { + for spending_key in self.paths[0].keys.iter().clone() { let fingerprint = spending_key.expect("Must be present at this step"); let key = self - .setup .keys .iter() .find(|key| key.key.master_fingerprint() == fingerprint) @@ -486,12 +414,11 @@ impl Step for DefineDescriptor { let mut recovery_paths = BTreeMap::new(); - for path in &self.setup.recovery_paths { + for path in &self.paths[1..] { let mut recovery_keys: Vec = Vec::new(); for recovery_key in path.keys.iter().clone() { let fingerprint = recovery_key.expect("Must be present at this step"); let key = self - .setup .keys .iter() .find(|key| key.key.master_fingerprint() == fingerprint) @@ -522,7 +449,11 @@ impl Step for DefineDescriptor { PathInfo::Multi(path.threshold, recovery_keys) }; - recovery_paths.insert(path.sequence, recovery_keys); + recovery_paths.insert( + path.sequence + .expect("Must be a recovery path with a sequence"), + recovery_keys, + ); } if spending_keys.is_empty() { @@ -532,7 +463,7 @@ impl Step for DefineDescriptor { let spending_keys = if spending_keys.len() == 1 { PathInfo::Single(spending_keys[0].clone()) } else { - PathInfo::Multi(self.setup.spending_threshold, spending_keys) + PathInfo::Multi(self.paths[0].threshold, spending_keys) }; let policy = match if self.use_taproot { @@ -558,45 +489,22 @@ impl Step for DefineDescriptor { progress: (usize, usize), email: Option<&'a str>, ) -> Element<'a, Message> { - let aliases = self.setup.keys_aliases(); - let content = view::define_descriptor( + let aliases = self.keys_aliases(); + let content = view::editor::define_descriptor( progress, email, self.use_taproot, - self.setup - .spending_keys - .iter() - .enumerate() - .map(|(i, key)| { - if let Some(key) = key { - view::defined_descriptor_key( - aliases.get(key).unwrap().to_string(), - self.setup.duplicate_name.contains(key), - self.setup.incompatible_with_tapminiscript.contains(key), - ) - } else { - view::undefined_descriptor_key() - } - .map(move |msg| { - Message::DefineDescriptor(message::DefineDescriptor::PrimaryPath( - message::DefinePath::Key(i, msg), - )) - }) - }) - .collect(), - self.setup.spending_threshold, - self.setup - .recovery_paths + self.paths .iter() .enumerate() .map(|(i, path)| { path.view( &aliases, - &self.setup.duplicate_name, - &self.setup.incompatible_with_tapminiscript, + &self.duplicate_name, + &self.incompatible_with_tapminiscript, ) .map(move |msg| { - Message::DefineDescriptor(message::DefineDescriptor::RecoveryPath(i, msg)) + Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) }) }) .collect(), @@ -663,7 +571,7 @@ impl DescriptorEditModal for EditSequenceModal { return Command::perform( async move { (path_index, sequence) }, |(path_index, sequence)| { - message::DefineDescriptor::RecoveryPath( + message::DefineDescriptor::Path( path_index, message::DefinePath::SequenceEdited(sequence), ) @@ -679,7 +587,7 @@ impl DescriptorEditModal for EditSequenceModal { } fn view(&self, _hws: &HardwareWallets) -> Element { - view::edit_sequence_modal(&self.sequence) + view::editor::edit_sequence_modal(&self.sequence) } } @@ -735,12 +643,10 @@ mod tests { // Edit primary key sandbox - .update(Message::DefineDescriptor( - message::DefineDescriptor::PrimaryPath(message::DefinePath::Key( - 0, - message::DefineKey::Edit, - )), - )) + .update(Message::DefineDescriptor(message::DefineDescriptor::Path( + 0, + message::DefinePath::Key(0, message::DefineKey::Edit), + ))) .await; sandbox.check(|step| assert!(step.modal.is_some())); sandbox.update(Message::UseHotSigner).await; @@ -760,22 +666,18 @@ mod tests { // Edit sequence sandbox - .update(Message::DefineDescriptor( - message::DefineDescriptor::RecoveryPath( - 0, - message::DefinePath::SequenceEdited(1000), - ), - )) + .update(Message::DefineDescriptor(message::DefineDescriptor::Path( + 1, + message::DefinePath::SequenceEdited(1000), + ))) .await; // Edit recovery key sandbox - .update(Message::DefineDescriptor( - message::DefineDescriptor::RecoveryPath( - 0, - message::DefinePath::Key(0, message::DefineKey::Edit), - ), - )) + .update(Message::DefineDescriptor(message::DefineDescriptor::Path( + 1, + message::DefinePath::Key(0, message::DefineKey::Edit), + ))) .await; sandbox.check(|step| assert!(step.modal.is_some())); sandbox.update(Message::DefineDescriptor( @@ -832,19 +734,18 @@ mod tests { // Use Specter device for primary key sandbox - .update(Message::DefineDescriptor( - message::DefineDescriptor::PrimaryPath(specter_key.clone()), - )) + .update(Message::DefineDescriptor(message::DefineDescriptor::Path( + 0, + specter_key.clone(), + ))) .await; // Edit recovery key sandbox - .update(Message::DefineDescriptor( - message::DefineDescriptor::RecoveryPath( - 0, - message::DefinePath::Key(0, message::DefineKey::Edit), - ), - )) + .update(Message::DefineDescriptor(message::DefineDescriptor::Path( + 1, + message::DefinePath::Key(0, message::DefineKey::Edit), + ))) .await; sandbox.check(|step| assert!(step.modal.is_some())); sandbox.update(Message::DefineDescriptor( @@ -872,12 +773,10 @@ mod tests { // Now edit primary key to use hot signer instead of Specter device sandbox - .update(Message::DefineDescriptor( - message::DefineDescriptor::PrimaryPath(message::DefinePath::Key( - 0, - message::DefineKey::Edit, - )), - )) + .update(Message::DefineDescriptor(message::DefineDescriptor::Path( + 0, + message::DefinePath::Key(0, message::DefineKey::Edit), + ))) .await; sandbox.check(|step| assert!(step.modal.is_some())); sandbox.update(Message::UseHotSigner).await; @@ -901,9 +800,10 @@ mod tests { // Now edit the recovery key to use Specter device sandbox - .update(Message::DefineDescriptor( - message::DefineDescriptor::RecoveryPath(0, specter_key.clone()), - )) + .update(Message::DefineDescriptor(message::DefineDescriptor::Path( + 1, + specter_key.clone(), + ))) .await; sandbox.check(|step| { assert!((step).apply(&mut ctx)); diff --git a/gui/src/installer/view/editor.rs b/gui/src/installer/view/editor.rs new file mode 100644 index 00000000..14dda5ee --- /dev/null +++ b/gui/src/installer/view/editor.rs @@ -0,0 +1,710 @@ +use iced::widget::{ + container, pick_list, scrollable, scrollable::Properties, slider, Button, Space, +}; +use iced::{alignment, Alignment, Length}; + +use liana_ui::component::text; +use std::str::FromStr; + +use liana::miniscript::bitcoin::{self, bip32::Fingerprint}; +use liana_ui::{ + color, + component::{ + button, card, collapse, form, hw, separation, + text::{p1_regular, text, Text}, + tooltip, + }, + icon, image, theme, + widget::*, +}; + +use crate::installer::{ + message::{self, Message}, + prompt, + view::{defined_sequence, layout}, + Error, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DescriptorKind { + P2WSH, + Taproot, +} + +const DESCRIPTOR_KINDS: [DescriptorKind; 2] = [DescriptorKind::P2WSH, DescriptorKind::Taproot]; + +impl std::fmt::Display for DescriptorKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::P2WSH => write!(f, "P2WSH"), + Self::Taproot => write!(f, "Taproot"), + } + } +} + +#[allow(clippy::too_many_arguments)] +pub fn define_descriptor_advanced_settings<'a>(use_taproot: bool) -> Element<'a, Message> { + let col_wallet = Column::new() + .spacing(10) + .push(text("Descriptor type").bold()) + .push(container( + pick_list( + &DESCRIPTOR_KINDS[..], + Some(if use_taproot { + DescriptorKind::Taproot + } else { + DescriptorKind::P2WSH + }), + |kind| Message::CreateTaprootDescriptor(kind == DescriptorKind::Taproot), + ) + .style(theme::PickList::Secondary) + .padding(10), + )); + + container( + Column::new() + .spacing(20) + .push(Space::with_height(0)) + .push(separation().width(500)) + .push(Row::new().push(col_wallet)) + .push_maybe(if use_taproot { + Some( + p1_regular("Taproot is only supported by Liana version 5.0 and above") + .style(color::GREY_2), + ) + } else { + None + }), + ) + .into() +} + +#[allow(clippy::too_many_arguments)] +pub fn define_descriptor<'a>( + progress: (usize, usize), + email: Option<&'a str>, + use_taproot: bool, + paths: Vec>, + valid: bool, + error: Option<&String>, +) -> Element<'a, Message> { + layout( + progress, + email, + "Create the wallet", + Column::new() + .push(collapse::Collapse::new( + || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Advanced settings").small().bold()) + .push(icon::collapse_icon()), + ) + .style(theme::Button::Transparent) + }, + || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Advanced settings").small().bold()) + .push(icon::collapsed_icon()), + ) + .style(theme::Button::Transparent) + }, + move || define_descriptor_advanced_settings(use_taproot), + )) + .push( + Column::new() + .width(Length::Fill) + .push( + Column::new() + .spacing(25) + .push(Column::with_children(paths).spacing(10)) + .push(tooltip(prompt::DEFINE_DESCRIPTOR_RECOVERY_PATH_TOOLTIP)), + ) + .spacing(25), + ) + .push( + Row::new() + .spacing(10) + .push( + button::secondary(Some(icon::plus_icon()), "Add a recovery path") + .on_press(Message::DefineDescriptor( + message::DefineDescriptor::AddRecoveryPath, + )) + .width(Length::Fixed(200.0)), + ) + .push(if !valid { + button::primary(None, "Next").width(Length::Fixed(200.0)) + } else { + button::primary(None, "Next") + .width(Length::Fixed(200.0)) + .on_press(Message::Next) + }), + ) + .push_maybe(error.map(|e| card::error("Failed to create descriptor", e.to_string()))) + .push(Space::with_height(Length::Fixed(20.0))) + .spacing(50), + false, + Some(Message::Previous), + ) +} + +pub fn primary_path_view( + primary_threshold: usize, + primary_keys: Vec>, +) -> Element { + Container::new( + Column::new().push( + Row::new() + .align_items(Alignment::Center) + .push_maybe(if primary_keys.len() > 1 { + Some(threshsold_input::threshsold_input( + primary_threshold, + primary_keys.len(), + message::DefinePath::ThresholdEdited, + )) + } else { + None + }) + .push( + scrollable( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(Row::with_children(primary_keys).spacing(5)) + .push( + Button::new( + Container::new(icon::plus_icon().size(50)) + .width(Length::Fixed(150.0)) + .height(Length::Fixed(150.0)) + .align_y(alignment::Vertical::Center) + .align_x(alignment::Horizontal::Center), + ) + .width(Length::Fixed(150.0)) + .height(Length::Fixed(150.0)) + .style(theme::Button::TransparentBorder) + .on_press(message::DefinePath::AddKey), + ) + .padding(5), + ) + .direction(scrollable::Direction::Horizontal( + Properties::new().width(3).scroller_width(3), + )), + ), + ), + ) + .padding(5) + .style(theme::Container::Card(theme::Card::Border)) + .into() +} + +pub fn recovery_path_view( + sequence: u16, + duplicate_sequence: bool, + recovery_threshold: usize, + recovery_keys: Vec>, +) -> Element { + Container::new( + Column::new() + .push(defined_sequence(sequence, duplicate_sequence)) + .push( + Row::new() + .align_items(Alignment::Center) + .push_maybe(if recovery_keys.len() > 1 { + Some(threshsold_input::threshsold_input( + recovery_threshold, + recovery_keys.len(), + message::DefinePath::ThresholdEdited, + )) + } else { + None + }) + .push( + scrollable( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(Row::with_children(recovery_keys).spacing(5)) + .push( + Button::new( + Container::new(icon::plus_icon().size(50)) + .width(Length::Fixed(150.0)) + .height(Length::Fixed(150.0)) + .align_y(alignment::Vertical::Center) + .align_x(alignment::Horizontal::Center), + ) + .width(Length::Fixed(150.0)) + .height(Length::Fixed(150.0)) + .style(theme::Button::TransparentBorder) + .on_press(message::DefinePath::AddKey), + ) + .padding(5), + ) + .direction(scrollable::Direction::Horizontal( + Properties::new().width(3).scroller_width(3), + )), + ), + ), + ) + .padding(5) + .style(theme::Container::Card(theme::Card::Border)) + .into() +} + +pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> { + card::simple( + Column::new() + .width(Length::Fill) + .align_items(Alignment::Center) + .push( + Row::new() + .align_items(Alignment::Center) + .push(Space::with_width(Length::Fill)) + .push( + Button::new(icon::cross_icon()) + .style(theme::Button::Transparent) + .on_press(message::DefineKey::Delete), + ), + ) + .push( + Container::new( + Column::new() + .spacing(15) + .align_items(Alignment::Center) + .push(image::key_mark_icon().width(Length::Fixed(30.0))), + ) + .height(Length::Fill) + .align_y(alignment::Vertical::Center), + ) + .push( + button::secondary(Some(icon::pencil_icon()), "Set") + .on_press(message::DefineKey::Edit), + ) + .push(Space::with_height(Length::Fixed(5.0))), + ) + .padding(5) + .height(Length::Fixed(150.0)) + .width(Length::Fixed(150.0)) + .into() +} + +pub fn defined_descriptor_key<'a>( + name: String, + duplicate_name: bool, + incompatible_with_tapminiscript: bool, +) -> Element<'a, message::DefineKey> { + let col = Column::new() + .width(Length::Fill) + .align_items(Alignment::Center) + .push( + Row::new() + .align_items(Alignment::Center) + .push(Space::with_width(Length::Fill)) + .push( + Button::new(icon::cross_icon()) + .style(theme::Button::Transparent) + .on_press(message::DefineKey::Delete), + ), + ) + .push( + Container::new( + Column::new() + .spacing(10) + .align_items(Alignment::Center) + .push( + scrollable( + Column::new() + .push(text(name).bold()) + .push(Space::with_height(Length::Fixed(5.0))), + ) + .direction(scrollable::Direction::Horizontal( + Properties::new().width(5).scroller_width(5), + )), + ) + .push(image::success_mark_icon().width(Length::Fixed(50.0))) + .push(Space::with_width(Length::Fixed(1.0))), + ) + .height(Length::Fill) + .align_y(alignment::Vertical::Center), + ) + .push( + button::secondary(Some(icon::pencil_icon()), "Edit").on_press(message::DefineKey::Edit), + ) + .push(Space::with_height(Length::Fixed(5.0))); + + if duplicate_name { + Column::new() + .align_items(Alignment::Center) + .push( + card::invalid(col) + .padding(5) + .height(Length::Fixed(150.0)) + .width(Length::Fixed(150.0)), + ) + .push(text("Duplicate name").small().style(color::RED)) + .into() + } else if incompatible_with_tapminiscript { + Column::new() + .align_items(Alignment::Center) + .push( + card::invalid(col) + .padding(5) + .height(Length::Fixed(150.0)) + .width(Length::Fixed(150.0)), + ) + .push( + text("Taproot is not supported\nby this key device") + .small() + .style(color::RED), + ) + .into() + } else { + card::simple(col) + .padding(5) + .height(Length::Fixed(150.0)) + .width(Length::Fixed(150.0)) + .into() + } +} + +#[allow(clippy::too_many_arguments)] +pub fn edit_key_modal<'a>( + network: bitcoin::Network, + hws: Vec>, + keys: Vec>, + error: Option<&Error>, + chosen_signer: Option, + hot_signer_fingerprint: &Fingerprint, + signer_alias: Option<&'a String>, + form_xpub: &form::Value, + form_name: &'a form::Value, + edit_name: bool, + duplicate_master_fg: bool, +) -> Element<'a, Message> { + Column::new() + .push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string()))) + .push(card::simple( + Column::new() + .spacing(25) + .push( + Column::new() + .push( + Container::new(text("Select a signing device:").bold()) + .width(Length::Fill), + ) + .spacing(10) + .push( + Column::with_children(hws).spacing(10) + ) + .push( + Column::with_children(keys).spacing(10) + ) + .push( + Button::new(if Some(*hot_signer_fingerprint) == chosen_signer { + hw::selected_hot_signer(hot_signer_fingerprint, signer_alias) + } else { + hw::unselected_hot_signer(hot_signer_fingerprint, signer_alias) + }) + .width(Length::Fill) + .on_press(Message::UseHotSigner) + .style(theme::Button::Border), + ) + .width(Length::Fill), + ) + .push( + Column::new() + .spacing(5) + .push(text("Or enter an extended public key:").bold()) + .push( + Row::new() + .push( + form::Form::new_trimmed( + &format!( + "[aabbccdd/42'/0']{}pub6DAkq8LWw91WGgUGnkR5Sbzjev5JCsXaTVZQ9MwsPV4BkNFKygtJ8GHodfDVx1udR723nT7JASqGPpKvz7zQ25pUTW6zVEBdiWoaC4aUqik", + if network == bitcoin::Network::Bitcoin { + "x" + } else { + "t" + } + ), + form_xpub, |msg| { + Message::DefineDescriptor( + message::DefineDescriptor::KeyModal( + message::ImportKeyModal::XPubEdited(msg),),) + }) + .warning(if network == bitcoin::Network::Bitcoin { + "Please enter correct xpub with origin and without appended derivation path" + } else { + "Please enter correct tpub with origin and without appended derivation path" + }) + .size(text::P1_SIZE) + .padding(10), + ) + .spacing(10) + ), + ) + .push( + if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid { + Column::new().push( + Row::new() + .push( + Column::new() + .spacing(5) + .width(Length::Fill) + .push( + Row::new() + .spacing(5) + .push(text("Fingerprint alias:").bold()) + .push(tooltip( + prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP, + )), + ) + .push(text(&form_name.value)), + ) + .push( + button::secondary(Some(icon::pencil_icon()), "Edit").on_press( + Message::DefineDescriptor( + message::DefineDescriptor::KeyModal( + message::ImportKeyModal::EditName, + ), + ), + ), + ), + ) + } else if !form_xpub.value.is_empty() && form_xpub.valid { + Column::new() + .spacing(5) + .push( + Row::new() + .spacing(5) + .push(text("Fingerprint alias:").bold()) + .push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)), + ) + .push( + form::Form::new("Alias", form_name, |msg| { + Message::DefineDescriptor(message::DefineDescriptor::KeyModal( + message::ImportKeyModal::NameEdited(msg), + )) + }) + .warning("Please enter correct alias") + .size(text::P1_SIZE) + .padding(10), + ) + } else { + Column::new() + }, + ) + .push_maybe( + if duplicate_master_fg { + Some(text("A single signing device may not be used more than once per path. (It can still be used in other paths.)").style(color::RED)) + } else { + None + } + ) + .push( + if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty() && !duplicate_master_fg + { + button::primary(None, "Apply") + .on_press(Message::DefineDescriptor( + message::DefineDescriptor::KeyModal( + message::ImportKeyModal::ConfirmXpub, + ), + )) + .width(Length::Fixed(200.0)) + } else { + button::primary(None, "Apply").width(Length::Fixed(100.0)) + }, + ) + .align_items(Alignment::Center), + )) + .width(Length::Fixed(600.0)) + .into() +} + +/// returns y,m,d,h,m +pub fn duration_from_sequence(sequence: u16) -> (u32, u32, u32, u32, u32) { + let mut n_minutes = sequence as u32 * 10; + let n_years = n_minutes / 525960; + n_minutes -= n_years * 525960; + let n_months = n_minutes / 43830; + n_minutes -= n_months * 43830; + let n_days = n_minutes / 1440; + n_minutes -= n_days * 1440; + let n_hours = n_minutes / 60; + n_minutes -= n_hours * 60; + + (n_years, n_months, n_days, n_hours, n_minutes) +} + +pub fn edit_sequence_modal<'a>(sequence: &form::Value) -> Element<'a, Message> { + let mut col = Column::new() + .width(Length::Fill) + .spacing(20) + .align_items(Alignment::Center) + .push(text("Activate recovery path after:")) + .push( + Row::new() + .push( + Container::new( + form::Form::new_trimmed("ex: 1000", sequence, |v| { + Message::DefineDescriptor(message::DefineDescriptor::SequenceModal( + message::SequenceModal::SequenceEdited(v), + )) + }) + .warning("Sequence must be superior to 0 and inferior to 65535"), + ) + .width(Length::Fixed(200.0)), + ) + .spacing(10) + .push(text("blocks").bold()), + ); + + if sequence.valid { + if let Ok(sequence) = u16::from_str(&sequence.value) { + let (n_years, n_months, n_days, n_hours, n_minutes) = duration_from_sequence(sequence); + col = col + .push( + [ + (n_years, "year"), + (n_months, "month"), + (n_days, "day"), + (n_hours, "hour"), + (n_minutes, "minute"), + ] + .iter() + .fold(Row::new().spacing(5), |row, (n, unit)| { + row.push_maybe(if *n > 0 { + Some( + text(format!("{} {}{}", n, unit, if *n > 1 { "s" } else { "" })) + .bold(), + ) + } else { + None + }) + }), + ) + .push( + Container::new( + slider(1..=u16::MAX, sequence, |v| { + Message::DefineDescriptor(message::DefineDescriptor::SequenceModal( + message::SequenceModal::SequenceEdited(v.to_string()), + )) + }) + .step(144_u16), // 144 blocks per day + ) + .width(Length::Fixed(500.0)), + ); + } + } + + card::simple(col.push(if sequence.valid { + button::primary(None, "Apply") + .on_press(Message::DefineDescriptor( + message::DefineDescriptor::SequenceModal(message::SequenceModal::ConfirmSequence), + )) + .width(Length::Fixed(200.0)) + } else { + button::primary(None, "Apply").width(Length::Fixed(200.0)) + })) + .width(Length::Fixed(800.0)) + .into() +} + +mod threshsold_input { + use iced::alignment::{self, Alignment}; + use iced::widget::{component, Component}; + use iced::Length; + use liana_ui::{component::text::*, icon, theme, widget::*}; + + pub struct ThresholdInput { + value: usize, + max: usize, + on_change: Box Message>, + } + + pub fn threshsold_input( + value: usize, + max: usize, + on_change: impl Fn(usize) -> Message + 'static, + ) -> ThresholdInput { + ThresholdInput::new(value, max, on_change) + } + + #[derive(Debug, Clone)] + pub enum Event { + IncrementPressed, + DecrementPressed, + } + + impl ThresholdInput { + pub fn new( + value: usize, + max: usize, + on_change: impl Fn(usize) -> Message + 'static, + ) -> Self { + Self { + value, + max, + on_change: Box::new(on_change), + } + } + } + + impl Component for ThresholdInput { + type State = (); + type Event = Event; + + fn update(&mut self, _state: &mut Self::State, event: Event) -> Option { + match event { + Event::IncrementPressed => { + if self.value < self.max { + Some((self.on_change)(self.value.saturating_add(1))) + } else { + None + } + } + Event::DecrementPressed => { + if self.value > 1 { + Some((self.on_change)(self.value.saturating_sub(1))) + } else { + None + } + } + } + } + + fn view(&self, _state: &Self::State) -> Element { + let button = |label, on_press| { + Button::new(label) + .style(theme::Button::Transparent) + .width(Length::Fixed(50.0)) + .on_press(on_press) + }; + + Column::new() + .width(Length::Fixed(150.0)) + .push(button(icon::up_icon().size(30), Event::IncrementPressed)) + .push(text("Threshold:").small().bold()) + .push( + Container::new(text(format!("{}/{}", self.value, self.max)).size(30)) + .align_y(alignment::Vertical::Center), + ) + .push(button(icon::down_icon().size(30), Event::DecrementPressed)) + .align_items(Alignment::Center) + .into() + } + } + + impl<'a, Message> From> for Element<'a, Message> + where + Message: 'a, + { + fn from(numeric_input: ThresholdInput) -> Self { + component(numeric_input) + } + } +} diff --git a/gui/src/installer/view/mod.rs b/gui/src/installer/view/mod.rs index 01fbf7dc..62fcac18 100644 --- a/gui/src/installer/view/mod.rs +++ b/gui/src/installer/view/mod.rs @@ -1,8 +1,7 @@ +pub mod editor; + use async_hwi::utils::extract_keys_and_template; -use iced::widget::{ - checkbox, container, pick_list, radio, scrollable, scrollable::Properties, slider, Button, - Space, TextInput, -}; +use iced::widget::{checkbox, radio, scrollable, scrollable::Properties, Button, Space, TextInput}; use iced::{ alignment, widget::{progress_bar, tooltip as iced_tooltip}, @@ -25,9 +24,8 @@ use liana_ui::{ component::{ button, card, collapse, form, hw, separation, text::{h2, h3, h4_bold, h5_regular, p1_regular, text, Text}, - tooltip, }, - icon, image, theme, + icon, theme, widget::*, }; @@ -37,6 +35,7 @@ use crate::{ message::{self, DefineBitcoind, DefineNode, Message}, prompt, step::{DownloadState, InstallState}, + view::editor::duration_from_sequence, Error, }, node::{ @@ -45,252 +44,6 @@ use crate::{ }, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DescriptorKind { - P2WSH, - Taproot, -} - -const DESCRIPTOR_KINDS: [DescriptorKind; 2] = [DescriptorKind::P2WSH, DescriptorKind::Taproot]; - -impl std::fmt::Display for DescriptorKind { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::P2WSH => write!(f, "P2WSH"), - Self::Taproot => write!(f, "Taproot"), - } - } -} - -#[allow(clippy::too_many_arguments)] -pub fn define_descriptor_advanced_settings<'a>(use_taproot: bool) -> Element<'a, Message> { - let col_wallet = Column::new() - .spacing(10) - .push(text("Descriptor type").bold()) - .push(container( - pick_list( - &DESCRIPTOR_KINDS[..], - Some(if use_taproot { - DescriptorKind::Taproot - } else { - DescriptorKind::P2WSH - }), - |kind| Message::CreateTaprootDescriptor(kind == DescriptorKind::Taproot), - ) - .style(theme::PickList::Secondary) - .padding(10), - )); - - container( - Column::new() - .spacing(20) - .push(Space::with_height(0)) - .push(separation().width(500)) - .push(Row::new().push(col_wallet)) - .push_maybe(if use_taproot { - Some( - p1_regular("Taproot is only supported by Liana version 5.0 and above") - .style(color::GREY_2), - ) - } else { - None - }), - ) - .into() -} - -#[allow(clippy::too_many_arguments)] -pub fn define_descriptor<'a>( - progress: (usize, usize), - email: Option<&'a str>, - use_taproot: bool, - spending_keys: Vec>, - spending_threshold: usize, - recovery_paths: Vec>, - valid: bool, - error: Option<&String>, -) -> Element<'a, Message> { - let col_spending_keys = Column::new() - .push( - Row::new() - .spacing(10) - .push(text("Primary path:").bold()) - .push(tooltip(prompt::DEFINE_DESCRIPTOR_PRIMARY_PATH_TOOLTIP)), - ) - .push(Container::new( - Row::new() - .align_items(Alignment::Center) - .push_maybe(if spending_keys.len() > 1 { - Some(threshsold_input::threshsold_input( - spending_threshold, - spending_keys.len(), - |value| { - Message::DefineDescriptor(message::DefineDescriptor::PrimaryPath( - message::DefinePath::ThresholdEdited(value), - )) - }, - )) - } else { - None - }) - .push( - scrollable( - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(Row::with_children(spending_keys).spacing(5)) - .push( - Button::new( - Container::new(icon::plus_icon().size(50)) - .width(Length::Fixed(150.0)) - .height(Length::Fixed(150.0)) - .align_y(alignment::Vertical::Center) - .align_x(alignment::Horizontal::Center), - ) - .width(Length::Fixed(150.0)) - .height(Length::Fixed(150.0)) - .style(theme::Button::TransparentBorder) - .on_press( - Message::DefineDescriptor( - message::DefineDescriptor::PrimaryPath( - message::DefinePath::AddKey, - ), - ), - ), - ) - .padding(5), - ) - .direction(scrollable::Direction::Horizontal( - Properties::new().width(3).scroller_width(3), - )), - ), - )) - .spacing(10); - - layout( - progress, - email, - "Create the wallet", - Column::new() - .push(collapse::Collapse::new( - || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .spacing(10) - .push(text("Advanced settings").small().bold()) - .push(icon::collapse_icon()), - ) - .style(theme::Button::Transparent) - }, - || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .spacing(10) - .push(text("Advanced settings").small().bold()) - .push(icon::collapsed_icon()), - ) - .style(theme::Button::Transparent) - }, - move || define_descriptor_advanced_settings(use_taproot), - )) - .push( - Column::new() - .width(Length::Fill) - .push( - Column::new() - .spacing(25) - .push(col_spending_keys) - .push( - Row::new() - .spacing(10) - .push(text("Recovery paths:").bold()) - .push(tooltip(prompt::DEFINE_DESCRIPTOR_RECOVERY_PATH_TOOLTIP)), - ) - .push(Column::with_children(recovery_paths).spacing(10)), - ) - .spacing(25), - ) - .push( - Row::new() - .spacing(10) - .push( - button::secondary(Some(icon::plus_icon()), "Add a recovery path") - .on_press(Message::DefineDescriptor( - message::DefineDescriptor::AddRecoveryPath, - )) - .width(Length::Fixed(200.0)), - ) - .push(if !valid { - button::secondary(None, "Next").width(Length::Fixed(200.0)) - } else { - button::secondary(None, "Next") - .width(Length::Fixed(200.0)) - .on_press(Message::Next) - }), - ) - .push_maybe(error.map(|e| card::error("Failed to create descriptor", e.to_string()))) - .push(Space::with_height(Length::Fixed(20.0))) - .spacing(50), - false, - Some(Message::Previous), - ) -} - -pub fn recovery_path_view( - sequence: u16, - duplicate_sequence: bool, - recovery_threshold: usize, - recovery_keys: Vec>, -) -> Element { - Container::new( - Column::new() - .push(defined_sequence(sequence, duplicate_sequence)) - .push( - Row::new() - .align_items(Alignment::Center) - .push_maybe(if recovery_keys.len() > 1 { - Some(threshsold_input::threshsold_input( - recovery_threshold, - recovery_keys.len(), - message::DefinePath::ThresholdEdited, - )) - } else { - None - }) - .push( - scrollable( - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(Row::with_children(recovery_keys).spacing(5)) - .push( - Button::new( - Container::new(icon::plus_icon().size(50)) - .width(Length::Fixed(150.0)) - .height(Length::Fixed(150.0)) - .align_y(alignment::Vertical::Center) - .align_x(alignment::Horizontal::Center), - ) - .width(Length::Fixed(150.0)) - .height(Length::Fixed(150.0)) - .style(theme::Button::TransparentBorder) - .on_press(message::DefinePath::AddKey), - ) - .padding(5), - ) - .direction(scrollable::Direction::Horizontal( - Properties::new().width(3).scroller_width(3), - )), - ), - ), - ) - .padding(5) - .style(theme::Container::Card(theme::Card::Border)) - .into() -} - pub fn import_wallet_or_descriptor<'a>( progress: (usize, usize), email: Option<&'a str>, @@ -1722,365 +1475,6 @@ pub fn defined_sequence<'a>( .into() } -pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> { - card::simple( - Column::new() - .width(Length::Fill) - .align_items(Alignment::Center) - .push( - Row::new() - .align_items(Alignment::Center) - .push(Space::with_width(Length::Fill)) - .push( - Button::new(icon::cross_icon()) - .style(theme::Button::Transparent) - .on_press(message::DefineKey::Delete), - ), - ) - .push( - Container::new( - Column::new() - .spacing(15) - .align_items(Alignment::Center) - .push(image::key_mark_icon().width(Length::Fixed(30.0))), - ) - .height(Length::Fill) - .align_y(alignment::Vertical::Center), - ) - .push( - button::secondary(Some(icon::pencil_icon()), "Set") - .on_press(message::DefineKey::Edit), - ) - .push(Space::with_height(Length::Fixed(5.0))), - ) - .padding(5) - .height(Length::Fixed(150.0)) - .width(Length::Fixed(150.0)) - .into() -} - -pub fn defined_descriptor_key<'a>( - name: String, - duplicate_name: bool, - incompatible_with_tapminiscript: bool, -) -> Element<'a, message::DefineKey> { - let col = Column::new() - .width(Length::Fill) - .align_items(Alignment::Center) - .push( - Row::new() - .align_items(Alignment::Center) - .push(Space::with_width(Length::Fill)) - .push( - Button::new(icon::cross_icon()) - .style(theme::Button::Transparent) - .on_press(message::DefineKey::Delete), - ), - ) - .push( - Container::new( - Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push( - scrollable( - Column::new() - .push(text(name).bold()) - .push(Space::with_height(Length::Fixed(5.0))), - ) - .direction(scrollable::Direction::Horizontal( - Properties::new().width(5).scroller_width(5), - )), - ) - .push(image::success_mark_icon().width(Length::Fixed(50.0))) - .push(Space::with_width(Length::Fixed(1.0))), - ) - .height(Length::Fill) - .align_y(alignment::Vertical::Center), - ) - .push( - button::secondary(Some(icon::pencil_icon()), "Edit").on_press(message::DefineKey::Edit), - ) - .push(Space::with_height(Length::Fixed(5.0))); - - if duplicate_name { - Column::new() - .align_items(Alignment::Center) - .push( - card::invalid(col) - .padding(5) - .height(Length::Fixed(150.0)) - .width(Length::Fixed(150.0)), - ) - .push(text("Duplicate name").small().style(color::RED)) - .into() - } else if incompatible_with_tapminiscript { - Column::new() - .align_items(Alignment::Center) - .push( - card::invalid(col) - .padding(5) - .height(Length::Fixed(150.0)) - .width(Length::Fixed(150.0)), - ) - .push( - text("Taproot is not supported\nby this key device") - .small() - .style(color::RED), - ) - .into() - } else { - card::simple(col) - .padding(5) - .height(Length::Fixed(150.0)) - .width(Length::Fixed(150.0)) - .into() - } -} - -#[allow(clippy::too_many_arguments)] -pub fn edit_key_modal<'a>( - network: bitcoin::Network, - hws: Vec>, - keys: Vec>, - error: Option<&Error>, - chosen_signer: Option, - hot_signer_fingerprint: &Fingerprint, - signer_alias: Option<&'a String>, - form_xpub: &form::Value, - form_name: &'a form::Value, - edit_name: bool, - duplicate_master_fg: bool, -) -> Element<'a, Message> { - Column::new() - .push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string()))) - .push(card::simple( - Column::new() - .spacing(25) - .push( - Column::new() - .push( - Container::new(text("Select a signing device:").bold()) - .width(Length::Fill), - ) - .spacing(10) - .push( - Column::with_children(hws).spacing(10) - ) - .push( - Column::with_children(keys).spacing(10) - ) - .push( - Button::new(if Some(*hot_signer_fingerprint) == chosen_signer { - hw::selected_hot_signer(hot_signer_fingerprint, signer_alias) - } else { - hw::unselected_hot_signer(hot_signer_fingerprint, signer_alias) - }) - .width(Length::Fill) - .on_press(Message::UseHotSigner) - .style(theme::Button::Border), - ) - .width(Length::Fill), - ) - .push( - Column::new() - .spacing(5) - .push(text("Or enter an extended public key:").bold()) - .push( - Row::new() - .push( - form::Form::new_trimmed( - &format!( - "[aabbccdd/42'/0']{}pub6DAkq8LWw91WGgUGnkR5Sbzjev5JCsXaTVZQ9MwsPV4BkNFKygtJ8GHodfDVx1udR723nT7JASqGPpKvz7zQ25pUTW6zVEBdiWoaC4aUqik", - if network == bitcoin::Network::Bitcoin { - "x" - } else { - "t" - } - ), - form_xpub, |msg| { - Message::DefineDescriptor( - message::DefineDescriptor::KeyModal( - message::ImportKeyModal::XPubEdited(msg),),) - }) - .warning(if network == bitcoin::Network::Bitcoin { - "Please enter correct xpub with origin and without appended derivation path" - } else { - "Please enter correct tpub with origin and without appended derivation path" - }) - .size(text::P1_SIZE) - .padding(10), - ) - .spacing(10) - ), - ) - .push( - if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid { - Column::new().push( - Row::new() - .push( - Column::new() - .spacing(5) - .width(Length::Fill) - .push( - Row::new() - .spacing(5) - .push(text("Fingerprint alias:").bold()) - .push(tooltip( - prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP, - )), - ) - .push(text(&form_name.value)), - ) - .push( - button::secondary(Some(icon::pencil_icon()), "Edit").on_press( - Message::DefineDescriptor( - message::DefineDescriptor::KeyModal( - message::ImportKeyModal::EditName, - ), - ), - ), - ), - ) - } else if !form_xpub.value.is_empty() && form_xpub.valid { - Column::new() - .spacing(5) - .push( - Row::new() - .spacing(5) - .push(text("Fingerprint alias:").bold()) - .push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)), - ) - .push( - form::Form::new("Alias", form_name, |msg| { - Message::DefineDescriptor(message::DefineDescriptor::KeyModal( - message::ImportKeyModal::NameEdited(msg), - )) - }) - .warning("Please enter correct alias") - .size(text::P1_SIZE) - .padding(10), - ) - } else { - Column::new() - }, - ) - .push_maybe( - if duplicate_master_fg { - Some(text("A single signing device may not be used more than once per path. (It can still be used in other paths.)").style(color::RED)) - } else { - None - } - ) - .push( - if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty() && !duplicate_master_fg - { - button::secondary(None, "Apply") - .on_press(Message::DefineDescriptor( - message::DefineDescriptor::KeyModal( - message::ImportKeyModal::ConfirmXpub, - ), - )) - .width(Length::Fixed(200.0)) - } else { - button::secondary(None, "Apply").width(Length::Fixed(100.0)) - }, - ) - .align_items(Alignment::Center), - )) - .width(Length::Fixed(600.0)) - .into() -} - -/// returns y,m,d,h,m -fn duration_from_sequence(sequence: u16) -> (u32, u32, u32, u32, u32) { - let mut n_minutes = sequence as u32 * 10; - let n_years = n_minutes / 525960; - n_minutes -= n_years * 525960; - let n_months = n_minutes / 43830; - n_minutes -= n_months * 43830; - let n_days = n_minutes / 1440; - n_minutes -= n_days * 1440; - let n_hours = n_minutes / 60; - n_minutes -= n_hours * 60; - - (n_years, n_months, n_days, n_hours, n_minutes) -} - -pub fn edit_sequence_modal<'a>(sequence: &form::Value) -> Element<'a, Message> { - let mut col = Column::new() - .width(Length::Fill) - .spacing(20) - .align_items(Alignment::Center) - .push(text("Activate recovery path after:")) - .push( - Row::new() - .push( - Container::new( - form::Form::new_trimmed("ex: 1000", sequence, |v| { - Message::DefineDescriptor(message::DefineDescriptor::SequenceModal( - message::SequenceModal::SequenceEdited(v), - )) - }) - .warning("Sequence must be superior to 0 and inferior to 65535"), - ) - .width(Length::Fixed(200.0)), - ) - .spacing(10) - .push(text("blocks").bold()), - ); - - if sequence.valid { - if let Ok(sequence) = u16::from_str(&sequence.value) { - let (n_years, n_months, n_days, n_hours, n_minutes) = duration_from_sequence(sequence); - col = col - .push( - [ - (n_years, "year"), - (n_months, "month"), - (n_days, "day"), - (n_hours, "hour"), - (n_minutes, "minute"), - ] - .iter() - .fold(Row::new().spacing(5), |row, (n, unit)| { - row.push_maybe(if *n > 0 { - Some( - text(format!("{} {}{}", n, unit, if *n > 1 { "s" } else { "" })) - .bold(), - ) - } else { - None - }) - }), - ) - .push( - Container::new( - slider(1..=u16::MAX, sequence, |v| { - Message::DefineDescriptor(message::DefineDescriptor::SequenceModal( - message::SequenceModal::SequenceEdited(v.to_string()), - )) - }) - .step(144_u16), // 144 blocks per day - ) - .width(Length::Fixed(500.0)), - ); - } - } - - card::simple(col.push(if sequence.valid { - button::secondary(None, "Apply") - .on_press(Message::DefineDescriptor( - message::DefineDescriptor::SequenceModal(message::SequenceModal::ConfirmSequence), - )) - .width(Length::Fixed(200.0)) - } else { - button::secondary(None, "Apply").width(Length::Fixed(200.0)) - })) - .width(Length::Fixed(800.0)) - .into() -} - pub fn hw_list_view( i: usize, hw: &HardwareWallet, @@ -2589,98 +1983,3 @@ fn layout<'a>( .style(theme::Container::Background) .into() } - -mod threshsold_input { - use iced::alignment::{self, Alignment}; - use iced::widget::{component, Component}; - use iced::Length; - use liana_ui::{component::text::*, icon, theme, widget::*}; - - pub struct ThresholdInput { - value: usize, - max: usize, - on_change: Box Message>, - } - - pub fn threshsold_input( - value: usize, - max: usize, - on_change: impl Fn(usize) -> Message + 'static, - ) -> ThresholdInput { - ThresholdInput::new(value, max, on_change) - } - - #[derive(Debug, Clone)] - pub enum Event { - IncrementPressed, - DecrementPressed, - } - - impl ThresholdInput { - pub fn new( - value: usize, - max: usize, - on_change: impl Fn(usize) -> Message + 'static, - ) -> Self { - Self { - value, - max, - on_change: Box::new(on_change), - } - } - } - - impl Component for ThresholdInput { - type State = (); - type Event = Event; - - fn update(&mut self, _state: &mut Self::State, event: Event) -> Option { - match event { - Event::IncrementPressed => { - if self.value < self.max { - Some((self.on_change)(self.value.saturating_add(1))) - } else { - None - } - } - Event::DecrementPressed => { - if self.value > 1 { - Some((self.on_change)(self.value.saturating_sub(1))) - } else { - None - } - } - } - } - - fn view(&self, _state: &Self::State) -> Element { - let button = |label, on_press| { - Button::new(label) - .style(theme::Button::Transparent) - .width(Length::Fixed(50.0)) - .on_press(on_press) - }; - - Column::new() - .width(Length::Fixed(150.0)) - .push(button(icon::up_icon().size(30), Event::IncrementPressed)) - .push(text("Threshold:").small().bold()) - .push( - Container::new(text(format!("{}/{}", self.value, self.max)).size(30)) - .align_y(alignment::Vertical::Center), - ) - .push(button(icon::down_icon().size(30), Event::DecrementPressed)) - .align_items(Alignment::Center) - .into() - } - } - - impl<'a, Message> From> for Element<'a, Message> - where - Message: 'a, - { - fn from(numeric_input: ThresholdInput) -> Self { - component(numeric_input) - } - } -} From fab3303147ad1933f9b434502b8cc5465b451dc5 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Thu, 3 Oct 2024 16:58:43 +0200 Subject: [PATCH 6/8] installer step: ChooseDescriptorTemplate --- gui/src/installer/context.rs | 8 +++ gui/src/installer/message.rs | 1 + gui/src/installer/mod.rs | 5 +- .../installer/step/descriptor/editor/mod.rs | 1 + .../step/descriptor/editor/template.rs | 55 +++++++++++++++ gui/src/installer/step/mod.rs | 3 +- .../view/{editor.rs => editor/mod.rs} | 2 + gui/src/installer/view/editor/template/mod.rs | 69 +++++++++++++++++++ 8 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 gui/src/installer/step/descriptor/editor/template.rs rename gui/src/installer/view/{editor.rs => editor/mod.rs} (99%) create mode 100644 gui/src/installer/view/editor/template/mod.rs diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs index 3f46cbe7..0849fc21 100644 --- a/gui/src/installer/context.rs +++ b/gui/src/installer/context.rs @@ -45,10 +45,17 @@ impl RemoteBackend { } } +#[derive(Debug, Clone, Copy)] +pub enum DescriptorTemplate { + SimpleInheritance, + Custom, +} + #[derive(Clone)] pub struct Context { pub bitcoin_config: BitcoinConfig, pub bitcoin_backend: Option, + pub descriptor_template: DescriptorTemplate, pub descriptor: Option, pub keys: Vec, pub hws: Vec<(DeviceKind, bitcoin::bip32::Fingerprint, Option<[u8; 32]>)>, @@ -71,6 +78,7 @@ impl Context { remote_backend: RemoteBackend, ) -> Self { Self { + descriptor_template: DescriptorTemplate::Custom, bitcoin_config: BitcoinConfig { network, poll_interval_secs: Duration::from_secs(30), diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 3e7939cb..87a266c5 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -32,6 +32,7 @@ pub enum Message { UseHotSigner, Installed(Result), CreateTaprootDescriptor(bool), + SelectDescriptorTemplate(context::DescriptorTemplate), SelectBackend(SelectBackend), ImportRemoteWallet(ImportRemoteWallet), SelectBitcoindType(SelectBitcoindTypeMsg), diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 43be9df3..f0f3262a 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -39,8 +39,8 @@ use crate::{ pub use message::Message; use step::{ - BackupDescriptor, BackupMnemonic, ChooseBackend, DefineDescriptor, DefineNode, Final, - ImportDescriptor, ImportRemoteWallet, InternalBitcoindStep, RecoverMnemonic, + BackupDescriptor, BackupMnemonic, ChooseBackend, ChooseDescriptorTemplate, DefineDescriptor, + DefineNode, Final, ImportDescriptor, ImportRemoteWallet, InternalBitcoindStep, RecoverMnemonic, RegisterDescriptor, RemoteBackendLogin, SelectBitcoindTypeStep, ShareXpubs, Step, }; @@ -119,6 +119,7 @@ impl Installer { hws: HardwareWallets::new(destination_path.clone(), network), steps: match user_flow { UserFlow::CreateWallet => vec![ + ChooseDescriptorTemplate::default().into(), DefineDescriptor::new(network, signer.clone()).into(), BackupMnemonic::new(signer.clone()).into(), BackupDescriptor::default().into(), diff --git a/gui/src/installer/step/descriptor/editor/mod.rs b/gui/src/installer/step/descriptor/editor/mod.rs index 493622cc..b6d42d24 100644 --- a/gui/src/installer/step/descriptor/editor/mod.rs +++ b/gui/src/installer/step/descriptor/editor/mod.rs @@ -1,4 +1,5 @@ pub mod key; +pub mod template; use std::collections::{BTreeMap, HashMap, HashSet}; use std::iter::FromIterator; diff --git a/gui/src/installer/step/descriptor/editor/template.rs b/gui/src/installer/step/descriptor/editor/template.rs new file mode 100644 index 00000000..ab9d5e2a --- /dev/null +++ b/gui/src/installer/step/descriptor/editor/template.rs @@ -0,0 +1,55 @@ +use iced::Command; + +use liana_ui::widget::Element; + +use crate::{ + hw::HardwareWallets, + installer::{ + context::DescriptorTemplate, + message::Message, + step::{Context, Step}, + view, + }, +}; + +pub struct ChooseDescriptorTemplate { + template: DescriptorTemplate, +} + +impl Default for ChooseDescriptorTemplate { + fn default() -> Self { + Self { + template: DescriptorTemplate::Custom, + } + } +} + +impl From for Box { + fn from(s: ChooseDescriptorTemplate) -> Box { + Box::new(s) + } +} +impl Step for ChooseDescriptorTemplate { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { + if let Message::SelectDescriptorTemplate(template) = message { + self.template = template; + Command::perform(async move {}, |_| Message::Next) + } else { + Command::none() + } + } + + fn apply(&mut self, ctx: &mut Context) -> bool { + ctx.descriptor_template = self.template; + true + } + + fn view<'a>( + &'a self, + _hws: &'a HardwareWallets, + progress: (usize, usize), + _email: Option<&'a str>, + ) -> Element { + view::editor::template::choose_descriptor_template(progress) + } +} diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index d47c253a..01a8fcf1 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -10,7 +10,8 @@ pub use node::{ }; pub use descriptor::{ - editor::DefineDescriptor, BackupDescriptor, ImportDescriptor, RegisterDescriptor, + editor::template::ChooseDescriptorTemplate, editor::DefineDescriptor, BackupDescriptor, + ImportDescriptor, RegisterDescriptor, }; pub use backend::{ChooseBackend, ImportRemoteWallet, RemoteBackendLogin}; diff --git a/gui/src/installer/view/editor.rs b/gui/src/installer/view/editor/mod.rs similarity index 99% rename from gui/src/installer/view/editor.rs rename to gui/src/installer/view/editor/mod.rs index 14dda5ee..7dc0582e 100644 --- a/gui/src/installer/view/editor.rs +++ b/gui/src/installer/view/editor/mod.rs @@ -1,3 +1,5 @@ +pub mod template; + use iced::widget::{ container, pick_list, scrollable, scrollable::Properties, slider, Button, Space, }; diff --git a/gui/src/installer/view/editor/template/mod.rs b/gui/src/installer/view/editor/template/mod.rs new file mode 100644 index 00000000..486ff5bc --- /dev/null +++ b/gui/src/installer/view/editor/template/mod.rs @@ -0,0 +1,69 @@ +use iced::{alignment, Alignment, Length}; + +use liana_ui::{ + color, + component::{ + button, card, + text::{h3, p1_regular, p2_regular}, + }, + widget::*, +}; + +use crate::installer::context; +use crate::installer::{message::Message, view::layout}; + +pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static, Message> { + layout( + progress, + None, + "Choose Wallet Type", + Column::new() + .align_items(Alignment::Start) + .push(Container::new( + p1_regular("What do you want your wallet for? Also consider this depends on the amount of funds you have, the more funds, higher the security should be. Not sure about the wallet type? We can help you.") + .style(color::GREY_3) + .horizontal_alignment(alignment::Horizontal::Left) + ).align_x(alignment::Horizontal::Left).width(Length::Fill)) + .push( + card::simple( + Row::new() + .align_items(Alignment::Center) + .push( + Column::new() + .align_items(Alignment::Start) + .push(h3("Simple inheritance")) + .push(p2_regular("Two keys required, one for yourself to spend and another for your heir.").style(color::GREY_3)) + .width(Length::Fill) + ) + .push(button::secondary(None, "Select").on_press( + Message::SelectDescriptorTemplate( + context::DescriptorTemplate::SimpleInheritance, + ), + )), + ) + .width(Length::Fill), + ) + .push( + card::simple( + Row::new() + .align_items(Alignment::Center) + .push( + Column::new() + .align_items(Alignment::Start) + .push(h3("Custom (choose your own)")) + .push(p2_regular("Create a custom set up that fits all your need").style(color::GREY_3)) + .width(Length::Fill) + ) + .push(button::secondary(None, "Select").on_press( + Message::SelectDescriptorTemplate( + context::DescriptorTemplate::Custom, + ), + )), + ) + .width(Length::Fill), + ) + .spacing(20), + true, + Some(Message::Previous), + ) +} From 5889e60dc2a3b8894195caca5c9e00f954197523 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Fri, 4 Oct 2024 15:58:41 +0200 Subject: [PATCH 7/8] Add descriptor template description step --- gui/src/installer/context.rs | 2 +- gui/src/installer/mod.rs | 6 ++- .../step/descriptor/editor/template.rs | 37 +++++++++++++++ gui/src/installer/step/mod.rs | 5 ++- .../view/editor/template/inheritance.rs | 45 +++++++++++++++++++ gui/src/installer/view/editor/template/mod.rs | 3 ++ gui/ui/src/image.rs | 8 ++++ .../inheritance_template_description.svg | 29 ++++++++++++ 8 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 gui/src/installer/view/editor/template/inheritance.rs create mode 100644 gui/ui/static/images/inheritance_template_description.svg diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs index 0849fc21..b4417c56 100644 --- a/gui/src/installer/context.rs +++ b/gui/src/installer/context.rs @@ -45,7 +45,7 @@ impl RemoteBackend { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DescriptorTemplate { SimpleInheritance, Custom, diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index f0f3262a..e0adac8f 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -40,8 +40,9 @@ use crate::{ pub use message::Message; use step::{ BackupDescriptor, BackupMnemonic, ChooseBackend, ChooseDescriptorTemplate, DefineDescriptor, - DefineNode, Final, ImportDescriptor, ImportRemoteWallet, InternalBitcoindStep, RecoverMnemonic, - RegisterDescriptor, RemoteBackendLogin, SelectBitcoindTypeStep, ShareXpubs, Step, + DefineNode, DescriptorTemplateDescription, Final, ImportDescriptor, ImportRemoteWallet, + InternalBitcoindStep, RecoverMnemonic, RegisterDescriptor, RemoteBackendLogin, + SelectBitcoindTypeStep, ShareXpubs, Step, }; #[derive(Debug, Clone)] @@ -120,6 +121,7 @@ impl Installer { steps: match user_flow { UserFlow::CreateWallet => vec![ ChooseDescriptorTemplate::default().into(), + DescriptorTemplateDescription::default().into(), DefineDescriptor::new(network, signer.clone()).into(), BackupMnemonic::new(signer.clone()).into(), BackupDescriptor::default().into(), diff --git a/gui/src/installer/step/descriptor/editor/template.rs b/gui/src/installer/step/descriptor/editor/template.rs index ab9d5e2a..391b4382 100644 --- a/gui/src/installer/step/descriptor/editor/template.rs +++ b/gui/src/installer/step/descriptor/editor/template.rs @@ -53,3 +53,40 @@ impl Step for ChooseDescriptorTemplate { view::editor::template::choose_descriptor_template(progress) } } + +pub struct DescriptorTemplateDescription { + template: DescriptorTemplate, +} + +impl Default for DescriptorTemplateDescription { + fn default() -> Self { + Self { + template: DescriptorTemplate::Custom, + } + } +} + +impl From for Box { + fn from(s: DescriptorTemplateDescription) -> Box { + Box::new(s) + } +} + +impl Step for DescriptorTemplateDescription { + fn load_context(&mut self, ctx: &Context) { + self.template = ctx.descriptor_template; + } + + fn skip(&self, ctx: &Context) -> bool { + ctx.descriptor_template == DescriptorTemplate::Custom + } + + fn view<'a>( + &'a self, + _hws: &'a HardwareWallets, + progress: (usize, usize), + _email: Option<&'a str>, + ) -> Element { + view::editor::template::inheritance::inheritance_template_description(progress) + } +} diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 01a8fcf1..690faaaa 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -10,8 +10,9 @@ pub use node::{ }; pub use descriptor::{ - editor::template::ChooseDescriptorTemplate, editor::DefineDescriptor, BackupDescriptor, - ImportDescriptor, RegisterDescriptor, + editor::template::{ChooseDescriptorTemplate, DescriptorTemplateDescription}, + editor::DefineDescriptor, + BackupDescriptor, ImportDescriptor, RegisterDescriptor, }; pub use backend::{ChooseBackend, ImportRemoteWallet, RemoteBackendLogin}; diff --git a/gui/src/installer/view/editor/template/inheritance.rs b/gui/src/installer/view/editor/template/inheritance.rs new file mode 100644 index 00000000..1c1c2f4a --- /dev/null +++ b/gui/src/installer/view/editor/template/inheritance.rs @@ -0,0 +1,45 @@ +use iced::{alignment, widget::Space, Alignment, Length}; + +use liana_ui::{ + color, + component::{ + button, + text::{h3, p1_regular}, + }, + image, + widget::*, +}; + +use crate::installer::{message::Message, view::layout}; + +pub fn inheritance_template_description(progress: (usize, usize)) -> Element<'static, Message> { + layout( + progress, + None, + "Introduction", + Column::new() + .align_items(Alignment::Start) + .push(h3("Inheritance wallet")) + .max_width(800.0) + .push(Container::new( + p1_regular("In this current setup you will need 2 Keys for your wallet. For security reasons, we suggest you to use 2 Hardware Wallets to store them.") + .style(color::GREY_3) + .horizontal_alignment(alignment::Horizontal::Left) + ).align_x(alignment::Horizontal::Left).width(Length::Fill)) + .push(Container::new( + p1_regular("For this Inheritance wallet you will need 2 Keys: Your Primary Key and an Inheritance Key to be given to a chosen relative.") + .style(color::GREY_3) + .horizontal_alignment(alignment::Horizontal::Left) + ).align_x(alignment::Horizontal::Left).width(Length::Fill)) + .push(image::inheritance_template_description().width(Length::Fill)) + .push(Container::new( + p1_regular("Your relative’s Inheritance Key will become active only if you don’t move the coins in your wallet for the defined period of time, enabling him/her to recover your funds while not being able to access them before that.") + .style(color::GREY_3) + .horizontal_alignment(alignment::Horizontal::Left) + ).align_x(alignment::Horizontal::Left).width(Length::Fill)) + .push(Row::new().push(Space::with_width(Length::Fill)).push(button::primary(None, "Select").width(Length::Fixed(200.0)).on_press(Message::Next))) + .spacing(20), + true, + Some(Message::Previous), + ) +} diff --git a/gui/src/installer/view/editor/template/mod.rs b/gui/src/installer/view/editor/template/mod.rs index 486ff5bc..867e3885 100644 --- a/gui/src/installer/view/editor/template/mod.rs +++ b/gui/src/installer/view/editor/template/mod.rs @@ -1,3 +1,5 @@ +pub mod inheritance; + use iced::{alignment, Alignment, Length}; use liana_ui::{ @@ -18,6 +20,7 @@ pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static, None, "Choose Wallet Type", Column::new() + .max_width(800.0) .align_items(Alignment::Start) .push(Container::new( p1_regular("What do you want your wallet for? Also consider this depends on the amount of funds you have, the more funds, higher the security should be. Not sure about the wallet type? We can help you.") diff --git a/gui/ui/src/image.rs b/gui/ui/src/image.rs index db6e542c..96715537 100644 --- a/gui/ui/src/image.rs +++ b/gui/ui/src/image.rs @@ -59,3 +59,11 @@ pub fn key_mark_icon() -> Svg { let h = Handle::from_memory(KEY_MARK_ICON.to_vec()); Svg::new(h) } + +const INHERITANCE_TEMPLATE_DESC: &[u8] = + include_bytes!("../static/images/inheritance_template_description.svg"); + +pub fn inheritance_template_description() -> Svg { + let h = Handle::from_memory(INHERITANCE_TEMPLATE_DESC.to_vec()); + Svg::new(h) +} diff --git a/gui/ui/static/images/inheritance_template_description.svg b/gui/ui/static/images/inheritance_template_description.svg new file mode 100644 index 00000000..8b8f25f0 --- /dev/null +++ b/gui/ui/static/images/inheritance_template_description.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From bd03cc9cfffb7c5c45dd0dd60d54db807d4e02a2 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 7 Oct 2024 16:51:48 +0200 Subject: [PATCH 8/8] Add inheritance and custom templates --- gui/src/installer/context.rs | 5 +- gui/src/installer/message.rs | 23 +- .../installer/step/descriptor/editor/key.rs | 213 +++--- .../installer/step/descriptor/editor/mod.rs | 408 ++++++------ .../step/descriptor/editor/template.rs | 31 +- gui/src/installer/step/mod.rs | 3 +- gui/src/installer/view/editor/mod.rs | 624 +++++++----------- .../installer/view/editor/template/custom.rs | 206 ++++++ .../view/editor/template/inheritance.rs | 143 +++- gui/src/installer/view/editor/template/mod.rs | 13 +- gui/src/installer/view/mod.rs | 152 +++-- gui/ui/src/component/card.rs | 6 + gui/ui/src/icon.rs | 8 + gui/ui/src/image.rs | 8 + gui/ui/src/theme.rs | 25 +- .../images/custom_template_description.svg | 28 + 16 files changed, 1107 insertions(+), 789 deletions(-) create mode 100644 gui/src/installer/view/editor/template/custom.rs create mode 100644 gui/ui/static/images/custom_template_description.svg diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs index b4417c56..2abf2140 100644 --- a/gui/src/installer/context.rs +++ b/gui/src/installer/context.rs @@ -45,8 +45,9 @@ impl RemoteBackend { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum DescriptorTemplate { + #[default] SimpleInheritance, Custom, } @@ -78,7 +79,7 @@ impl Context { remote_backend: RemoteBackend, ) -> Self { Self { - descriptor_template: DescriptorTemplate::Custom, + descriptor_template: DescriptorTemplate::default(), bitcoin_config: BitcoinConfig { network, poll_interval_secs: Duration::from_secs(30), diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 87a266c5..b9c4a63c 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -8,13 +8,13 @@ use super::{context, Error}; use crate::{ download::Progress, hw::HardwareWalletMessage, + installer::step::descriptor::editor::key::Key, lianalite::client::{auth::AuthClient, backend::api}, node::{ bitcoind::{Bitcoind, ConfigField, RpcAuthType}, electrum, NodeType, }, }; -use async_hwi::{DeviceKind, Version}; #[derive(Debug, Clone)] pub enum Message { @@ -108,13 +108,15 @@ pub enum InternalBitcoindMsg { Start, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum DefineDescriptor { + ChangeTemplate(context::DescriptorTemplate), ImportDescriptor(String), Path(usize, DefinePath), AddRecoveryPath, KeyModal(ImportKeyModal), - SequenceModal(SequenceModal), + ThresholdSequenceModal(ThresholdSequenceModal), } #[allow(clippy::large_enum_variant)] @@ -125,6 +127,7 @@ pub enum DefinePath { ThresholdEdited(usize), SequenceEdited(u16), EditSequence, + EditThreshold, } #[allow(clippy::large_enum_variant)] @@ -133,26 +136,22 @@ pub enum DefineKey { Delete, Edit, Clipboard(String), - Edited( - String, - DescriptorPublicKey, - Option, - Option, - ), + Edited(Key), } #[derive(Debug, Clone)] pub enum ImportKeyModal { - HWXpubImported(Result), + FetchedKey(Result), XPubEdited(String), - EditName, NameEdited(String), + ManuallyImportXpub, ConfirmXpub, SelectKey(usize), } #[derive(Debug, Clone)] -pub enum SequenceModal { +pub enum ThresholdSequenceModal { + ThresholdEdited(usize), SequenceEdited(String), - ConfirmSequence, + Confirm, } diff --git a/gui/src/installer/step/descriptor/editor/key.rs b/gui/src/installer/step/descriptor/editor/key.rs index e88195ca..6444e46d 100644 --- a/gui/src/installer/step/descriptor/editor/key.rs +++ b/gui/src/installer/step/descriptor/editor/key.rs @@ -17,7 +17,7 @@ use liana_ui::{component::form, widget::Element}; use async_hwi::{DeviceKind, Version}; use crate::{ - hw::{HardwareWallet, HardwareWallets}, + hw::{is_compatible_with_tapminiscript, HardwareWallet, HardwareWallets}, installer::{ message::{self, Message}, view, Error, @@ -41,7 +41,7 @@ pub fn new_multixkey_from_xpub( } } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct Key { pub device_kind: Option, pub is_hot_signer: bool, @@ -49,6 +49,7 @@ pub struct Key { pub name: String, pub fingerprint: Fingerprint, pub key: DescriptorPublicKey, + pub is_compatible_taproot: bool, } pub fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool { @@ -81,7 +82,7 @@ pub struct EditXpubModal { form_name: form::Value, form_xpub: form::Value, - edit_name: bool, + manually_imported_xpub: bool, other_path_keys: HashSet, duplicate_master_fg: bool, @@ -89,7 +90,7 @@ pub struct EditXpubModal { keys: Vec, hot_signer: Arc>, hot_signer_fingerprint: Fingerprint, - chosen_signer: Option<(Fingerprint, Option, Option)>, + chosen_signer: Option, } impl EditXpubModal { @@ -97,7 +98,7 @@ impl EditXpubModal { pub fn new( device_must_support_tapminiscript: bool, other_path_keys: HashSet, - key: Option, + key: Option, path_index: usize, key_index: usize, network: Network, @@ -105,41 +106,34 @@ impl EditXpubModal { hot_signer_fingerprint: Fingerprint, keys: Vec, ) -> Self { + // The xpub is manually imported if the key is neither from a device or the hot signer. + let manually_imported_xpub = key + .as_ref() + .map(|k| !k.is_hot_signer && k.device_kind.is_none()) + .unwrap_or(false); Self { device_must_support_tapminiscript, other_path_keys, form_name: form::Value { valid: true, - value: key - .map(|fg| { - keys.iter() - .find(|k| k.fingerprint == fg) - .expect("must be stored") - .name - .clone() - }) - .unwrap_or_default(), + value: key.as_ref().map(|k| k.name.clone()).unwrap_or_default(), }, form_xpub: form::Value { valid: true, - value: key - .map(|fg| { - keys.iter() - .find(|k| k.fingerprint == fg) - .expect("must be stored") - .key - .to_string() - }) - .unwrap_or_default(), + value: if manually_imported_xpub { + key.as_ref().map(|k| k.key.to_string()).unwrap_or_default() + } else { + String::new() + }, }, + manually_imported_xpub, keys, path_index, key_index, processing: false, error: None, network, - edit_name: false, - chosen_signer: key.map(|k| (k, None, None)), + chosen_signer: key, hot_signer_fingerprint, hot_signer, duplicate_master_fg: false, @@ -171,13 +165,41 @@ impl super::DescriptorEditModal for EditXpubModal { .. }) = hws.list.get(i) { - self.chosen_signer = Some((*fingerprint, Some(*kind), version.clone())); self.processing = true; + self.manually_imported_xpub = false; + let device_version = version.clone(); + let fingerprint = *fingerprint; + let device_kind = *kind; + let network = self.network; return Command::perform( - get_extended_pubkey(device.clone(), *fingerprint, self.network), - |res| { + get_extended_pubkey(device.clone(), fingerprint, self.network), + move |res| { Message::DefineDescriptor(message::DefineDescriptor::KeyModal( - message::ImportKeyModal::HWXpubImported(res), + message::ImportKeyModal::FetchedKey(match res { + Err(e) => Err(e), + Ok(key) => { + if check_key_network(&key, network) { + Ok(Key { + is_hot_signer: false, + fingerprint, + name: "".to_string(), + key, + is_compatible_taproot: + is_compatible_with_tapminiscript( + &device_kind, + device_version.as_ref(), + ), + device_kind: Some(device_kind), + device_version, + }) + } else { + Err(Error::Unexpected( + "Fetched key does not have the correct network" + .to_string(), + )) + } + } + }), )) }, ); @@ -187,24 +209,10 @@ impl super::DescriptorEditModal for EditXpubModal { return self.load(); } Message::UseHotSigner => { + self.manually_imported_xpub = false; let fingerprint = self.hot_signer.lock().unwrap().fingerprint(); - self.chosen_signer = Some((fingerprint, None, None)); - self.form_xpub.valid = true; - if let Some(alias) = self - .keys - .iter() - .find(|key| key.fingerprint == fingerprint) - .map(|k| k.name.clone()) - { - self.form_name.valid = true; - self.form_name.value = alias; - self.edit_name = false; - } else { - self.edit_name = true; - self.form_name.value = String::new(); - } let derivation_path = default_derivation_path(self.network); - self.form_xpub.value = format!( + let key_str = format!( "[{}{}]{}", fingerprint, derivation_path.to_string().trim_start_matches('m'), @@ -213,27 +221,36 @@ impl super::DescriptorEditModal for EditXpubModal { .unwrap() .get_extended_pubkey(&derivation_path) ); + self.chosen_signer = Some(Key { + is_hot_signer: true, + fingerprint, + name: "".to_string(), + key: DescriptorPublicKey::from_str(&key_str).unwrap(), + is_compatible_taproot: true, + device_kind: None, + device_version: None, + }); + self.form_name.value = self + .keys + .iter() + .find_map(|k| { + if k.fingerprint == fingerprint { + Some(k.name.clone()) + } else { + None + } + }) + .unwrap_or_default(); + self.form_name.valid = true; } Message::DefineDescriptor(message::DefineDescriptor::KeyModal(msg)) => match msg { - message::ImportKeyModal::HWXpubImported(res) => { + message::ImportKeyModal::FetchedKey(res) => { self.processing = false; match res { Ok(key) => { - if let Some(alias) = self - .keys - .iter() - .find(|k| k.fingerprint == key.master_fingerprint()) - .map(|k| k.name.clone()) - { - self.form_name.valid = true; - self.form_name.value = alias; - self.edit_name = false; - } else { - self.edit_name = true; - self.form_name.value = String::new(); - } - self.form_xpub.valid = check_key_network(&key, self.network); - self.form_xpub.value = key.to_string(); + self.form_name.valid = true; + self.form_name.value.clone_from(&key.name); + self.chosen_signer = Some(key); } Err(e) => { self.chosen_signer = None; @@ -241,11 +258,16 @@ impl super::DescriptorEditModal for EditXpubModal { } } } - message::ImportKeyModal::EditName => { - self.edit_name = true; + message::ImportKeyModal::ManuallyImportXpub => { + self.chosen_signer = None; + self.manually_imported_xpub = true; + self.form_xpub = form::Value::default(); } message::ImportKeyModal::NameEdited(name) => { - self.form_name.valid = true; + self.form_name.valid = !self.keys.iter().any(|k| { + Some(&k.fingerprint) != self.chosen_signer.as_ref().map(|s| &s.fingerprint) + && name == k.name + }); self.form_name.value = name; } message::ImportKeyModal::XPubEdited(s) => { @@ -259,17 +281,18 @@ impl super::DescriptorEditModal for EditXpubModal { } else { key.xkey.network == Network::Testnet }; - if let Some(alias) = self - .keys - .iter() - .find(|k| k.fingerprint == fingerprint) - .map(|k| k.name.clone()) - { + if self.form_xpub.valid { + self.chosen_signer = Some(Key { + is_hot_signer: false, + fingerprint, + name: "".to_string(), + key: DescriptorPublicKey::XPub(key), + is_compatible_taproot: true, + device_kind: None, + device_version: None, + }); + self.form_name.value = "".to_string(); self.form_name.valid = true; - self.form_name.value = alias; - self.edit_name = false; - } else { - self.edit_name = true; } } else { self.form_xpub.valid = false; @@ -280,16 +303,10 @@ impl super::DescriptorEditModal for EditXpubModal { self.form_xpub.value = s; } message::ImportKeyModal::ConfirmXpub => { - if let Ok(key) = DescriptorPublicKey::from_str(&self.form_xpub.value) { + if let Some(mut key) = self.chosen_signer.clone() { let key_index = self.key_index; - let name = self.form_name.value.clone(); - let (device_kind, device_version) = - if let Some((_, kind, version)) = &self.chosen_signer { - (*kind, version.clone()) - } else { - (None, None) - }; - if self.other_path_keys.contains(&key.master_fingerprint()) { + key.name.clone_from(&self.form_name.value); + if self.other_path_keys.contains(&key.fingerprint) { self.duplicate_master_fg = true; } else { let path_index = self.path_index; @@ -300,12 +317,7 @@ impl super::DescriptorEditModal for EditXpubModal { path_index, message::DefinePath::Key( key_index, - message::DefineKey::Edited( - name, - key, - device_kind, - device_version, - ), + message::DefineKey::Edited(key), ), ) }, @@ -316,10 +328,7 @@ impl super::DescriptorEditModal for EditXpubModal { } message::ImportKeyModal::SelectKey(i) => { if let Some(key) = self.keys.get(i) { - self.chosen_signer = - Some((key.fingerprint, key.device_kind, key.device_version.clone())); - self.form_xpub.value = key.key.to_string(); - self.form_xpub.valid = true; + self.chosen_signer = Some(key.clone()); self.form_name.value.clone_from(&key.name); self.form_name.valid = true; } @@ -335,8 +344,13 @@ impl super::DescriptorEditModal for EditXpubModal { } fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> { - let chosen_signer = self.chosen_signer.as_ref().map(|s| s.0); + let chosen_signer = self.chosen_signer.as_ref().map(|s| s.fingerprint); view::editor::edit_key_modal( + if self.path_index > 0 { + "Set your key" + } else { + "Set your primary key" + }, self.network, hws.list .iter() @@ -354,10 +368,7 @@ impl super::DescriptorEditModal for EditXpubModal { hw, hw.fingerprint() == chosen_signer, self.processing, - !self.processing - && hw.fingerprint() == chosen_signer - && self.form_xpub.valid - && !self.form_xpub.value.is_empty(), + hw.fingerprint() == chosen_signer, self.device_must_support_tapminiscript, )) } @@ -383,7 +394,7 @@ impl super::DescriptorEditModal for EditXpubModal { }) .collect(), self.error.as_ref(), - self.chosen_signer.as_ref().map(|s| s.0), + self.chosen_signer.as_ref().map(|s| s.fingerprint), &self.hot_signer_fingerprint, self.keys.iter().find_map(|k| { if k.fingerprint == self.hot_signer_fingerprint { @@ -392,9 +403,9 @@ impl super::DescriptorEditModal for EditXpubModal { None } }), - &self.form_xpub, &self.form_name, - self.edit_name, + &self.form_xpub, + self.manually_imported_xpub, self.duplicate_master_fg, ) } diff --git a/gui/src/installer/step/descriptor/editor/mod.rs b/gui/src/installer/step/descriptor/editor/mod.rs index b6d42d24..cef52399 100644 --- a/gui/src/installer/step/descriptor/editor/mod.rs +++ b/gui/src/installer/step/descriptor/editor/mod.rs @@ -20,7 +20,7 @@ use liana_ui::{ widget::Element, }; -use crate::hw; +use crate::installer::context::DescriptorTemplate; use crate::{ app::settings::KeySetting, hw::HardwareWallets, @@ -50,7 +50,8 @@ pub trait DescriptorEditModal { pub struct Path { keys: Vec>, threshold: usize, - sequence: Option, + // sequence is 0 if it is a primary path. + sequence: u16, duplicate_sequence: bool, } @@ -59,7 +60,7 @@ impl Path { Self { keys: vec![None], threshold: 1, - sequence: None, + sequence: 0, duplicate_sequence: false, } } @@ -68,7 +69,7 @@ impl Path { Self { keys: vec![None], threshold: 1, - sequence: Some(u16::MAX), + sequence: u16::MAX, duplicate_sequence: false, } } @@ -76,57 +77,6 @@ impl Path { fn valid(&self) -> bool { !self.keys.is_empty() && !self.keys.iter().any(|k| k.is_none()) && !self.duplicate_sequence } - - fn view( - &self, - aliases: &HashMap, - duplicate_name: &HashSet, - incompatible_with_tapminiscript: &HashSet, - ) -> Element { - if let Some(sequence) = self.sequence { - view::editor::recovery_path_view( - sequence, - self.duplicate_sequence, - self.threshold, - self.keys - .iter() - .enumerate() - .map(|(i, key)| { - if let Some(key) = key { - view::editor::defined_descriptor_key( - aliases.get(key).unwrap().to_string(), - duplicate_name.contains(key), - incompatible_with_tapminiscript.contains(key), - ) - } else { - view::editor::undefined_descriptor_key() - } - .map(move |msg| message::DefinePath::Key(i, msg)) - }) - .collect(), - ) - } else { - view::editor::primary_path_view( - self.threshold, - self.keys - .iter() - .enumerate() - .map(|(i, key)| { - if let Some(key) = key { - view::editor::defined_descriptor_key( - aliases.get(key).unwrap().to_string(), - duplicate_name.contains(key), - incompatible_with_tapminiscript.contains(key), - ) - } else { - view::editor::undefined_descriptor_key() - } - .map(move |msg| message::DefinePath::Key(i, msg)) - }) - .collect(), - ) - } - } } pub struct DefineDescriptor { @@ -137,10 +87,9 @@ pub struct DefineDescriptor { signer: Arc>, signer_fingerprint: Fingerprint, - keys: Vec, - duplicate_name: HashSet, - incompatible_with_tapminiscript: HashSet, + keys: HashMap, paths: Vec, + descriptor_template: DescriptorTemplate, error: Option, } @@ -156,95 +105,78 @@ impl DefineDescriptor { signer, error: None, - keys: Vec::new(), - duplicate_name: HashSet::new(), - incompatible_with_tapminiscript: HashSet::new(), - paths: vec![Path::new_primary_path(), Path::new_recovery_path()], + keys: HashMap::new(), + descriptor_template: DescriptorTemplate::default(), + paths: Vec::new(), } } - fn keys_aliases(&self) -> HashMap { - let mut map = HashMap::new(); - for key in &self.keys { - map.insert(key.key.master_fingerprint(), key.name.clone()); - } - map - } - - // Mark as duplicate every defined key that have the same name but not the same fingerprint. - // And every undefined_key that have a same name than an other key. - fn check_for_duplicate(&mut self) { - self.duplicate_name = HashSet::new(); - for a in &self.keys { - for b in &self.keys { - if a.name == b.name && a.fingerprint != b.fingerprint { - self.duplicate_name.insert(a.fingerprint); - self.duplicate_name.insert(b.fingerprint); + fn path_keys<'a>(&'a self, p: &Path) -> Vec> { + p.keys + .iter() + .map(|f| { + if let Some(f) = f { + self.keys.get(f) + } else { + None } - } - } + }) + .collect() + } + fn check_for_duplicate(&mut self) { let mut all_sequence = HashSet::new(); let mut duplicate_sequences = HashSet::new(); for path in &mut self.paths { - if let Some(sequence) = path.sequence { - if all_sequence.contains(&sequence) { - duplicate_sequences.insert(sequence); - } else { - all_sequence.insert(sequence); - } + if all_sequence.contains(&path.sequence) { + duplicate_sequences.insert(path.sequence); + } else { + all_sequence.insert(path.sequence); } } for path in &mut self.paths { - if let Some(sequence) = path.sequence { - path.duplicate_sequence = duplicate_sequences.contains(&sequence); - } - } - } - - fn check_for_tapminiscript_support(&mut self, must_support_taproot: bool) { - self.incompatible_with_tapminiscript = HashSet::new(); - if must_support_taproot { - for key in &self.keys { - // check if key is used by a path - if !self - .paths - .iter() - .flat_map(|path| &path.keys) - .any(|k| *k == Some(key.fingerprint)) - { - continue; - } - - // device_kind is none only for HotSigner which is compatible. - if let Some(device_kind) = key.device_kind.as_ref() { - if !hw::is_compatible_with_tapminiscript( - device_kind, - key.device_version.as_ref(), - ) { - self.incompatible_with_tapminiscript.insert(key.fingerprint); - } - } - } + path.duplicate_sequence = duplicate_sequences.contains(&path.sequence); } } fn valid(&self) -> bool { - !self.paths.iter().any(|path| !path.valid()) - && self.duplicate_name.is_empty() - && self.incompatible_with_tapminiscript.is_empty() - && self.paths.len() >= 2 + !self.paths.iter().any(|path| { + !path.valid() + || (self.use_taproot + && path.keys.iter().any(|k| { + if let Some(k) = k.and_then(|k| self.keys.get(&k)) { + !k.is_compatible_taproot + } else { + false + } + })) + }) && self.paths.len() >= 2 } fn check_setup(&mut self) { self.check_for_duplicate(); - let use_taproot = self.use_taproot; - self.check_for_tapminiscript_support(use_taproot); + } + + fn load_template(&mut self, template: DescriptorTemplate) { + if self.descriptor_template != template || self.paths.is_empty() { + match template { + DescriptorTemplate::SimpleInheritance => { + self.paths = vec![Path::new_primary_path(), Path::new_recovery_path()]; + } + DescriptorTemplate::Custom => { + self.paths = vec![Path::new_primary_path(), Path::new_recovery_path()]; + } + } + } + self.descriptor_template = template; } } impl Step for DefineDescriptor { + fn load_context(&mut self, ctx: &Context) { + self.load_template(ctx.descriptor_template) + } // form value is set as valid each time it is edited. // Verification of the values is happening when the user click on Next button. fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { @@ -257,29 +189,40 @@ impl Step for DefineDescriptor { self.use_taproot = use_taproot; self.check_setup(); } + Message::DefineDescriptor(message::DefineDescriptor::ChangeTemplate(template)) => { + self.descriptor_template = template; + } Message::DefineDescriptor(message::DefineDescriptor::AddRecoveryPath) => { self.paths.push(Path::new_recovery_path()); } Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) => match msg { - message::DefinePath::ThresholdEdited(value) => { - if let Some(path) = self.paths.get_mut(i) { - path.threshold = value; - } - } message::DefinePath::SequenceEdited(seq) => { self.modal = None; if let Some(path) = self.paths.get_mut(i) { - path.sequence = Some(seq); + path.sequence = seq; } self.check_for_duplicate(); } - message::DefinePath::EditSequence => { - if let Some(path) = self.paths.get(i) { - if let Some(sequence) = path.sequence { - self.modal = Some(Box::new(EditSequenceModal::new(i, sequence))); - } + message::DefinePath::ThresholdEdited(t) => { + self.modal = None; + if let Some(path) = self.paths.get_mut(i) { + path.threshold = t; } } + message::DefinePath::EditSequence => { + if let Some(path) = self.paths.get(i) { + self.modal = Some(Box::new(EditSequenceModal::new(i, path.sequence))); + } + } + message::DefinePath::EditThreshold => { + if let Some(path) = self.paths.get(i) { + self.modal = Some(Box::new(EditThresholdModal::new( + i, + (path.threshold, path.keys.len()), + ))); + } + } + message::DefinePath::AddKey => { if let Some(path) = self.paths.get_mut(i) { path.keys.push(None); @@ -290,30 +233,10 @@ impl Step for DefineDescriptor { message::DefineKey::Clipboard(key) => { return Command::perform(async move { key }, Message::Clibpboard); } - message::DefineKey::Edited(name, imported_key, kind, version) => { - let fingerprint = imported_key.master_fingerprint(); - let is_hot_signer = self.signer_fingerprint == fingerprint; - hws.set_alias(fingerprint, name.clone()); - if let Some(key) = - self.keys.iter_mut().find(|k| k.fingerprint == fingerprint) - { - key.name = name; - key.is_hot_signer = is_hot_signer; - key.device_kind = kind; - key.device_version = version; - } else { - self.keys.push(Key { - fingerprint, - is_hot_signer, - name, - key: imported_key, - device_kind: kind, - device_version: version, - }); - } - - self.paths[i].keys[j] = Some(fingerprint); - + message::DefineKey::Edited(key) => { + hws.set_alias(key.fingerprint, key.name.clone()); + self.paths[i].keys[j] = Some(key.fingerprint); + self.keys.insert(key.fingerprint, key); self.modal = None; self.check_setup(); } @@ -329,13 +252,13 @@ impl Step for DefineDescriptor { None } })), - path.keys[j], + path.keys[j].and_then(|f| self.keys.get(&f)).cloned(), i, j, self.network, self.signer.clone(), self.signer_fingerprint, - self.keys.clone(), + self.keys.values().cloned().collect(), ); let cmd = modal.load(); self.modal = Some(Box::new(modal)); @@ -348,11 +271,13 @@ impl Step for DefineDescriptor { path.threshold -= 1; } } - if self - .paths - .get(i) - .map(|path| path.keys.is_empty()) - .unwrap_or(false) + // Only delete recovery paths. + if i > 0 + && self + .paths + .get(i) + .map(|path| path.keys.is_empty()) + .unwrap_or(false) { self.paths.remove(i); } @@ -391,8 +316,7 @@ impl Step for DefineDescriptor { let fingerprint = spending_key.expect("Must be present at this step"); let key = self .keys - .iter() - .find(|key| key.key.master_fingerprint() == fingerprint) + .get(&fingerprint) .expect("Must be present at this step"); if let DescriptorPublicKey::XPub(xpub) = &key.key { if let Some((master_fingerprint, _)) = xpub.origin { @@ -421,8 +345,7 @@ impl Step for DefineDescriptor { let fingerprint = recovery_key.expect("Must be present at this step"); let key = self .keys - .iter() - .find(|key| key.key.master_fingerprint() == fingerprint) + .get(&fingerprint) .expect("Must be present at this step"); if let DescriptorPublicKey::XPub(xpub) = &key.key { if let Some((master_fingerprint, _)) = xpub.origin { @@ -450,11 +373,7 @@ impl Step for DefineDescriptor { PathInfo::Multi(path.threshold, recovery_keys) }; - recovery_paths.insert( - path.sequence - .expect("Must be a recovery path with a sequence"), - recovery_keys, - ); + recovery_paths.insert(path.sequence, recovery_keys); } if spending_keys.is_empty() { @@ -488,30 +407,43 @@ impl Step for DefineDescriptor { &'a self, hws: &'a HardwareWallets, progress: (usize, usize), - email: Option<&'a str>, + _email: Option<&'a str>, ) -> Element<'a, Message> { - let aliases = self.keys_aliases(); - let content = view::editor::define_descriptor( - progress, - email, - self.use_taproot, - self.paths - .iter() - .enumerate() - .map(|(i, path)| { - path.view( - &aliases, - &self.duplicate_name, - &self.incompatible_with_tapminiscript, - ) - .map(move |msg| { - Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) - }) - }) - .collect(), - self.valid(), - self.error.as_ref(), - ); + let content = match self.descriptor_template { + DescriptorTemplate::SimpleInheritance => { + view::editor::template::inheritance::inheritance_template( + progress, + self.use_taproot, + self.paths[0].keys[0] + .as_ref() + .and_then(|f| self.keys.get(f)), + self.paths[1].keys[0] + .as_ref() + .and_then(|f| self.keys.get(f)), + self.paths[1].sequence, + self.valid(), + ) + } + DescriptorTemplate::Custom => view::editor::template::custom::custom_template( + progress, + self.use_taproot, + view::editor::template::custom::Path { + keys: self.path_keys(&self.paths[0]), + sequence: self.paths[0].sequence, + duplicate_sequence: self.paths[0].duplicate_sequence, + threshold: self.paths[0].threshold, + }, + &mut self.paths[1..] + .iter() + .map(|p| view::editor::template::custom::Path { + sequence: p.sequence, + duplicate_sequence: p.duplicate_sequence, + threshold: p.threshold, + keys: self.path_keys(p), + }), + self.valid(), + ), + }; if let Some(modal) = &self.modal { Modal::new(content, modal.view(hws)) .on_blur(if modal.processing() { @@ -555,9 +487,11 @@ impl DescriptorEditModal for EditSequenceModal { } fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { - if let Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(msg)) = message { + if let Message::DefineDescriptor(message::DefineDescriptor::ThresholdSequenceModal(msg)) = + message + { match msg { - message::SequenceModal::SequenceEdited(seq) => { + message::ThresholdSequenceModal::SequenceEdited(seq) => { if let Ok(s) = u16::from_str(&seq) { self.sequence.valid = s != 0 } else { @@ -565,7 +499,7 @@ impl DescriptorEditModal for EditSequenceModal { } self.sequence.value = seq; } - message::SequenceModal::ConfirmSequence => { + message::ThresholdSequenceModal::Confirm => { if self.sequence.valid { if let Ok(sequence) = u16::from_str(&self.sequence.value) { let path_index = self.path_index; @@ -582,6 +516,7 @@ impl DescriptorEditModal for EditSequenceModal { } } } + _ => {} } } Command::none() @@ -592,6 +527,60 @@ impl DescriptorEditModal for EditSequenceModal { } } +pub struct EditThresholdModal { + threshold: (usize, usize), + path_index: usize, +} + +impl EditThresholdModal { + pub fn new(path_index: usize, threshold: (usize, usize)) -> Self { + Self { + threshold, + path_index, + } + } +} + +impl DescriptorEditModal for EditThresholdModal { + fn processing(&self) -> bool { + false + } + + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { + if let Message::DefineDescriptor(message::DefineDescriptor::ThresholdSequenceModal(msg)) = + message + { + match msg { + message::ThresholdSequenceModal::ThresholdEdited(threshold) => { + if threshold <= self.threshold.1 { + self.threshold.0 = threshold; + } + } + message::ThresholdSequenceModal::Confirm => { + let path_index = self.path_index; + let threshold = self.threshold.0; + return Command::perform( + async move { (path_index, threshold) }, + |(path_index, threshold)| { + message::DefineDescriptor::Path( + path_index, + message::DefinePath::ThresholdEdited(threshold), + ) + }, + ) + .map(Message::DefineDescriptor); + } + _ => {} + } + } + Command::none() + } + + fn view(&self, _hws: &HardwareWallets) -> Element { + view::editor::edit_threshold_modal(self.threshold) + } +} + #[cfg(test)] mod tests { use super::*; @@ -638,9 +627,10 @@ mod tests { crate::installer::context::RemoteBackend::None, ); let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new( - Network::Bitcoin, + Network::Signet, Arc::new(Mutex::new(Signer::generate(Network::Bitcoin).unwrap())), )); + sandbox.load(&ctx).await; // Edit primary key sandbox @@ -723,14 +713,18 @@ mod tests { )); sandbox.load(&ctx).await; + let key = DescriptorPublicKey::from_str("[4df3f0e3/84'/0'/0']tpubDDRs9DnRUiJc4hq92PSJKhfzQBgHJUrDo7T2i48smsDfLsQcm3Vh7JhuGqJv8zozVkNFin8YPgpmn2NWNmpRaE3GW2pSxbmAzYf2juy7LeW").unwrap(); let specter_key = message::DefinePath::Key( 0, - message::DefineKey::Edited( - "My Specter key".to_string(), - DescriptorPublicKey::from_str("[4df3f0e3/84'/0'/0']tpubDDRs9DnRUiJc4hq92PSJKhfzQBgHJUrDo7T2i48smsDfLsQcm3Vh7JhuGqJv8zozVkNFin8YPgpmn2NWNmpRaE3GW2pSxbmAzYf2juy7LeW").unwrap(), - Some(async_hwi::DeviceKind::Specter), - None, - ), + message::DefineKey::Edited(Key { + name: "My Specter key".to_string(), + fingerprint: key.master_fingerprint(), + key, + device_kind: Some(async_hwi::DeviceKind::Specter), + device_version: None, + is_compatible_taproot: false, + is_hot_signer: false, + }), ); // Use Specter device for primary key diff --git a/gui/src/installer/step/descriptor/editor/template.rs b/gui/src/installer/step/descriptor/editor/template.rs index 391b4382..53243fd7 100644 --- a/gui/src/installer/step/descriptor/editor/template.rs +++ b/gui/src/installer/step/descriptor/editor/template.rs @@ -12,18 +12,11 @@ use crate::{ }, }; +#[derive(Default)] pub struct ChooseDescriptorTemplate { template: DescriptorTemplate, } -impl Default for ChooseDescriptorTemplate { - fn default() -> Self { - Self { - template: DescriptorTemplate::Custom, - } - } -} - impl From for Box { fn from(s: ChooseDescriptorTemplate) -> Box { Box::new(s) @@ -54,18 +47,11 @@ impl Step for ChooseDescriptorTemplate { } } +#[derive(Default)] pub struct DescriptorTemplateDescription { template: DescriptorTemplate, } -impl Default for DescriptorTemplateDescription { - fn default() -> Self { - Self { - template: DescriptorTemplate::Custom, - } - } -} - impl From for Box { fn from(s: DescriptorTemplateDescription) -> Box { Box::new(s) @@ -77,16 +63,19 @@ impl Step for DescriptorTemplateDescription { self.template = ctx.descriptor_template; } - fn skip(&self, ctx: &Context) -> bool { - ctx.descriptor_template == DescriptorTemplate::Custom - } - fn view<'a>( &'a self, _hws: &'a HardwareWallets, progress: (usize, usize), _email: Option<&'a str>, ) -> Element { - view::editor::template::inheritance::inheritance_template_description(progress) + match self.template { + DescriptorTemplate::SimpleInheritance => { + view::editor::template::inheritance::inheritance_template_description(progress) + } + DescriptorTemplate::Custom { .. } => { + view::editor::template::custom::custom_template_description(progress) + } + } } } diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 690faaaa..a0562f05 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -1,5 +1,6 @@ +pub mod descriptor; + mod backend; -mod descriptor; mod mnemonic; mod node; mod share_xpubs; diff --git a/gui/src/installer/view/editor/mod.rs b/gui/src/installer/view/editor/mod.rs index 7dc0582e..a2ad333e 100644 --- a/gui/src/installer/view/editor/mod.rs +++ b/gui/src/installer/view/editor/mod.rs @@ -1,32 +1,34 @@ pub mod template; -use iced::widget::{ - container, pick_list, scrollable, scrollable::Properties, slider, Button, Space, -}; -use iced::{alignment, Alignment, Length}; +use iced::widget::{container, pick_list, slider, Button, Space}; +use iced::{Alignment, Length}; -use liana_ui::component::text; +use liana::miniscript::bitcoin::Network; +use liana_ui::component::text::{self, h3, p1_bold, p2_regular, H3_SIZE}; +use liana_ui::image; use std::str::FromStr; use liana::miniscript::bitcoin::{self, bip32::Fingerprint}; use liana_ui::{ color, component::{ - button, card, collapse, form, hw, separation, + button, card, form, hw, separation, text::{p1_regular, text, Text}, tooltip, }, - icon, image, theme, + icon, theme, widget::*, }; use crate::installer::{ message::{self, Message}, prompt, - view::{defined_sequence, layout}, + view::defined_sequence, Error, }; +use super::defined_threshold; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DescriptorKind { P2WSH, @@ -81,300 +83,141 @@ pub fn define_descriptor_advanced_settings<'a>(use_taproot: bool) -> Element<'a, .into() } -#[allow(clippy::too_many_arguments)] -pub fn define_descriptor<'a>( - progress: (usize, usize), - email: Option<&'a str>, - use_taproot: bool, - paths: Vec>, - valid: bool, - error: Option<&String>, -) -> Element<'a, Message> { - layout( - progress, - email, - "Create the wallet", +pub fn path( + color: iced::Color, + title: Option, + sequence: u16, + duplicate_sequence: bool, + threshold: usize, + keys: Vec>, + fixed: bool, +) -> Element { + let keys_len = keys.len(); + Container::new( Column::new() - .push(collapse::Collapse::new( - || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .spacing(10) - .push(text("Advanced settings").small().bold()) - .push(icon::collapse_icon()), - ) - .style(theme::Button::Transparent) - }, - || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .spacing(10) - .push(text("Advanced settings").small().bold()) - .push(icon::collapsed_icon()), - ) - .style(theme::Button::Transparent) - }, - move || define_descriptor_advanced_settings(use_taproot), - )) + .spacing(10) + .push_maybe(title.map(p1_bold)) + .push(defined_sequence(sequence, duplicate_sequence)) + .push( + Column::new() + .spacing(5) + .align_items(Alignment::Center) + .push(Column::with_children(keys).spacing(5)), + ) + .push_maybe(if fixed { + if keys_len == 1 { + None + } else { + Some(Row::new().push(defined_threshold(color, fixed, (threshold, keys_len)))) + } + } else { + Some( + Row::new() + .spacing(10) + .push(defined_threshold(color, fixed, (threshold, keys_len))) + .push( + button::secondary(Some(icon::plus_icon()), "Add key") + .on_press(message::DefinePath::AddKey), + ), + ) + }), + ) + .padding(10) + .style(theme::Container::Card(theme::Card::Border)) + .into() +} + +pub fn defined_key<'a>( + alias: &'a str, + color: iced::Color, + title: &'static str, + warning: Option<&'static str>, + fixed: bool, +) -> Element<'a, message::DefineKey> { + card::simple( + Row::new() + .spacing(10) + .width(Length::Fill) + .align_items(Alignment::Center) + .push(icon::round_key_icon().size(H3_SIZE).style(color)) .push( Column::new() .width(Length::Fill) + .spacing(5) .push( - Column::new() - .spacing(25) - .push(Column::with_children(paths).spacing(10)) - .push(tooltip(prompt::DEFINE_DESCRIPTOR_RECOVERY_PATH_TOOLTIP)), - ) - .spacing(25), - ) - .push( - Row::new() - .spacing(10) - .push( - button::secondary(Some(icon::plus_icon()), "Add a recovery path") - .on_press(Message::DefineDescriptor( - message::DefineDescriptor::AddRecoveryPath, - )) - .width(Length::Fixed(200.0)), - ) - .push(if !valid { - button::primary(None, "Next").width(Length::Fixed(200.0)) - } else { - button::primary(None, "Next") - .width(Length::Fixed(200.0)) - .on_press(Message::Next) - }), - ) - .push_maybe(error.map(|e| card::error("Failed to create descriptor", e.to_string()))) - .push(Space::with_height(Length::Fixed(20.0))) - .spacing(50), - false, - Some(Message::Previous), - ) -} - -pub fn primary_path_view( - primary_threshold: usize, - primary_keys: Vec>, -) -> Element { - Container::new( - Column::new().push( - Row::new() - .align_items(Alignment::Center) - .push_maybe(if primary_keys.len() > 1 { - Some(threshsold_input::threshsold_input( - primary_threshold, - primary_keys.len(), - message::DefinePath::ThresholdEdited, - )) - } else { - None - }) - .push( - scrollable( Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(Row::with_children(primary_keys).spacing(5)) - .push( - Button::new( - Container::new(icon::plus_icon().size(50)) - .width(Length::Fixed(150.0)) - .height(Length::Fixed(150.0)) - .align_y(alignment::Vertical::Center) - .align_x(alignment::Horizontal::Center), - ) - .width(Length::Fixed(150.0)) - .height(Length::Fixed(150.0)) - .style(theme::Button::TransparentBorder) - .on_press(message::DefinePath::AddKey), - ) - .padding(5), + .spacing(10) + .push(p1_regular(title).style(color::GREY_2)) + .push(p1_bold(alias)), ) - .direction(scrollable::Direction::Horizontal( - Properties::new().width(3).scroller_width(3), - )), - ), - ), - ) - .padding(5) - .style(theme::Container::Card(theme::Card::Border)) - .into() -} - -pub fn recovery_path_view( - sequence: u16, - duplicate_sequence: bool, - recovery_threshold: usize, - recovery_keys: Vec>, -) -> Element { - Container::new( - Column::new() - .push(defined_sequence(sequence, duplicate_sequence)) - .push( - Row::new() - .align_items(Alignment::Center) - .push_maybe(if recovery_keys.len() > 1 { - Some(threshsold_input::threshsold_input( - recovery_threshold, - recovery_keys.len(), - message::DefinePath::ThresholdEdited, - )) - } else { - None - }) - .push( - scrollable( - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(Row::with_children(recovery_keys).spacing(5)) - .push( - Button::new( - Container::new(icon::plus_icon().size(50)) - .width(Length::Fixed(150.0)) - .height(Length::Fixed(150.0)) - .align_y(alignment::Vertical::Center) - .align_x(alignment::Horizontal::Center), - ) - .width(Length::Fixed(150.0)) - .height(Length::Fixed(150.0)) - .style(theme::Button::TransparentBorder) - .on_press(message::DefinePath::AddKey), - ) - .padding(5), - ) - .direction(scrollable::Direction::Horizontal( - Properties::new().width(3).scroller_width(3), - )), - ), - ), - ) - .padding(5) - .style(theme::Container::Card(theme::Card::Border)) - .into() -} - -pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> { - card::simple( - Column::new() - .width(Length::Fill) - .align_items(Alignment::Center) - .push( - Row::new() - .align_items(Alignment::Center) - .push(Space::with_width(Length::Fill)) - .push( - Button::new(icon::cross_icon()) - .style(theme::Button::Transparent) - .on_press(message::DefineKey::Delete), - ), + .push_maybe(warning.map(|w| p2_regular(w).style(color::RED))), ) + .push_maybe(if warning.is_none() { + Some(icon::check_icon().style(color::GREEN)) + } else { + None + }) .push( - Container::new( - Column::new() - .spacing(15) - .align_items(Alignment::Center) - .push(image::key_mark_icon().width(Length::Fixed(30.0))), - ) - .height(Length::Fill) - .align_y(alignment::Vertical::Center), - ) - .push( - button::secondary(Some(icon::pencil_icon()), "Set") + button::secondary(Some(icon::pencil_icon()), "Edit") .on_press(message::DefineKey::Edit), ) - .push(Space::with_height(Length::Fixed(5.0))), + .push_maybe(if fixed { + None + } else { + Some( + Button::new(icon::trash_icon()) + .style(theme::Button::Secondary) + .padding(5) + .on_press(message::DefineKey::Delete), + ) + }), ) - .padding(5) - .height(Length::Fixed(150.0)) - .width(Length::Fixed(150.0)) .into() } -pub fn defined_descriptor_key<'a>( - name: String, - duplicate_name: bool, - incompatible_with_tapminiscript: bool, +pub fn undefined_key<'a>( + color: iced::Color, + title: &'static str, + active: bool, + fixed: bool, ) -> Element<'a, message::DefineKey> { - let col = Column::new() - .width(Length::Fill) - .align_items(Alignment::Center) - .push( - Row::new() - .align_items(Alignment::Center) - .push(Space::with_width(Length::Fill)) - .push( - Button::new(icon::cross_icon()) - .style(theme::Button::Transparent) - .on_press(message::DefineKey::Delete), - ), - ) - .push( - Container::new( + card::simple( + Row::new() + .spacing(10) + .width(Length::Fill) + .align_items(Alignment::Center) + .push(icon::round_key_icon().size(H3_SIZE).style(color)) + .push( Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push( - scrollable( - Column::new() - .push(text(name).bold()) - .push(Space::with_height(Length::Fixed(5.0))), - ) - .direction(scrollable::Direction::Horizontal( - Properties::new().width(5).scroller_width(5), - )), - ) - .push(image::success_mark_icon().width(Length::Fixed(50.0))) - .push(Space::with_width(Length::Fixed(1.0))), + .width(Length::Fill) + .spacing(5) + .push(p1_bold(title)), ) - .height(Length::Fill) - .align_y(alignment::Vertical::Center), - ) - .push( - button::secondary(Some(icon::pencil_icon()), "Edit").on_press(message::DefineKey::Edit), - ) - .push(Space::with_height(Length::Fixed(5.0))); - - if duplicate_name { - Column::new() - .align_items(Alignment::Center) - .push( - card::invalid(col) - .padding(5) - .height(Length::Fixed(150.0)) - .width(Length::Fixed(150.0)), - ) - .push(text("Duplicate name").small().style(color::RED)) - .into() - } else if incompatible_with_tapminiscript { - Column::new() - .align_items(Alignment::Center) - .push( - card::invalid(col) - .padding(5) - .height(Length::Fixed(150.0)) - .width(Length::Fixed(150.0)), - ) - .push( - text("Taproot is not supported\nby this key device") - .small() - .style(color::RED), - ) - .into() - } else { - card::simple(col) - .padding(5) - .height(Length::Fixed(150.0)) - .width(Length::Fixed(150.0)) - .into() - } + .push_maybe(if active { + Some( + button::primary(Some(icon::pencil_icon()), "Set") + .on_press(message::DefineKey::Edit), + ) + } else { + None + }) + .push_maybe(if fixed { + None + } else { + Some( + Button::new(icon::trash_icon()) + .style(theme::Button::Secondary) + .padding(5) + .on_press(message::DefineKey::Delete), + ) + }), + ) + .into() } #[allow(clippy::too_many_arguments)] pub fn edit_key_modal<'a>( + title: &'a str, network: bitcoin::Network, hws: Vec>, keys: Vec>, @@ -382,22 +225,24 @@ pub fn edit_key_modal<'a>( chosen_signer: Option, hot_signer_fingerprint: &Fingerprint, signer_alias: Option<&'a String>, - form_xpub: &form::Value, form_name: &'a form::Value, - edit_name: bool, + form_xpub: &form::Value, + manually_imported_xpub: bool, duplicate_master_fg: bool, ) -> Element<'a, Message> { Column::new() + .padding(25) .push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string()))) - .push(card::simple( + .push(card::modal( Column::new() .spacing(25) + .push(Row::new() + .push(Space::with_width(Length::Fill)) + .push(button::transparent(Some(icon::cross_icon()), "").on_press(Message::Close))) .push( Column::new() - .push( - Container::new(text("Select a signing device:").bold()) - .width(Length::Fill), - ) + .push(h3(title)) + .push(p1_regular("Select the signing device for your key")) .spacing(10) .push( Column::with_children(hws).spacing(10) @@ -415,90 +260,79 @@ pub fn edit_key_modal<'a>( .on_press(Message::UseHotSigner) .style(theme::Button::Border), ) + .push(if manually_imported_xpub { + card::simple(Column::new() + .spacing(10) + .push( + Row::new() + .align_items(Alignment::Center) + .push(p1_regular("Enter an extended public key:").width(Length::Fill)) + .push(image::success_mark_icon().width(Length::Fixed(50.0))) + ) + .push( + Row::new() + .push( + form::Form::new_trimmed( + &example_xpub(network), + form_xpub, |msg| { + Message::DefineDescriptor( + message::DefineDescriptor::KeyModal( + message::ImportKeyModal::XPubEdited(msg),),) + }) + .warning(if network == bitcoin::Network::Bitcoin { + "Please enter correct xpub with origin and without appended derivation path" + } else { + "Please enter correct tpub with origin and without appended derivation path" + }) + .size(text::P1_SIZE) + .padding(10), + ) + .spacing(10) + )) + } else { + Container::new( + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(icon::import_icon()) + .push(p1_regular("Enter an extended public key")) + ) + .padding(20) + .width(Length::Fill) + .on_press(Message::DefineDescriptor( + message::DefineDescriptor::KeyModal(message::ImportKeyModal::ManuallyImportXpub) + )) + .style(theme::Button::Secondary), + ) + } + ) .width(Length::Fill), ) - .push( - Column::new() - .spacing(5) - .push(text("Or enter an extended public key:").bold()) - .push( - Row::new() - .push( - form::Form::new_trimmed( - &format!( - "[aabbccdd/42'/0']{}pub6DAkq8LWw91WGgUGnkR5Sbzjev5JCsXaTVZQ9MwsPV4BkNFKygtJ8GHodfDVx1udR723nT7JASqGPpKvz7zQ25pUTW6zVEBdiWoaC4aUqik", - if network == bitcoin::Network::Bitcoin { - "x" - } else { - "t" - } - ), - form_xpub, |msg| { - Message::DefineDescriptor( - message::DefineDescriptor::KeyModal( - message::ImportKeyModal::XPubEdited(msg),),) - }) - .warning(if network == bitcoin::Network::Bitcoin { - "Please enter correct xpub with origin and without appended derivation path" - } else { - "Please enter correct tpub with origin and without appended derivation path" - }) - .size(text::P1_SIZE) - .padding(10), - ) - .spacing(10) - ), - ) - .push( - if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid { - Column::new().push( - Row::new() - .push( - Column::new() - .spacing(5) - .width(Length::Fill) - .push( - Row::new() - .spacing(5) - .push(text("Fingerprint alias:").bold()) - .push(tooltip( - prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP, - )), - ) - .push(text(&form_name.value)), - ) - .push( - button::secondary(Some(icon::pencil_icon()), "Edit").on_press( - Message::DefineDescriptor( - message::DefineDescriptor::KeyModal( - message::ImportKeyModal::EditName, - ), - ), - ), - ), - ) - } else if !form_xpub.value.is_empty() && form_xpub.valid { - Column::new() - .spacing(5) + .push_maybe( + if chosen_signer.is_some() { + Some(card::simple(Column::new() + .spacing(10) .push( Row::new() .spacing(5) - .push(text("Fingerprint alias:").bold()) + .push(text("Key name:").bold()) .push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)), ) + .push(p1_regular("Give this key a friendly name. It helps you identify it later").style(color::GREY_2)) .push( - form::Form::new("Alias", form_name, |msg| { + form::Form::new("Name", form_name, |msg| { Message::DefineDescriptor(message::DefineDescriptor::KeyModal( message::ImportKeyModal::NameEdited(msg), )) }) - .warning("Please enter correct alias") + .warning("Two different keys cannot have the same name") + .padding(10) .size(text::P1_SIZE) - .padding(10), - ) + ))) } else { - Column::new() - }, + None + } ) .push_maybe( if duplicate_master_fg { @@ -508,25 +342,30 @@ pub fn edit_key_modal<'a>( } ) .push( - if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty() && !duplicate_master_fg - { - button::primary(None, "Apply") - .on_press(Message::DefineDescriptor( + button::primary(None, "Apply") + .on_press_maybe(if !duplicate_master_fg + && (!manually_imported_xpub || form_xpub.valid) + && !form_name.value.is_empty() && form_name.valid { + Some(Message::DefineDescriptor( message::DefineDescriptor::KeyModal( message::ImportKeyModal::ConfirmXpub, ), )) - .width(Length::Fixed(200.0)) - } else { - button::primary(None, "Apply").width(Length::Fixed(100.0)) - }, + } else {None}) + .width(Length::Fixed(200.0)) ) .align_items(Alignment::Center), )) - .width(Length::Fixed(600.0)) + .width(Length::Fixed(800.0)) .into() } +fn example_xpub(network: Network) -> String { + format!("[aabbccdd/42'/0']{}pub6DAkq8LWw91WGgUGnkR5Sbzjev5JCsXaTVZQ9MwsPV4BkNFKygtJ8GHodfDVx1udR723nT7JASqGPpKvz7zQ25pUTW6zVEBdiWoaC4aUqik", + if network == bitcoin::Network::Bitcoin { "x" } else { "t" } + ) +} + /// returns y,m,d,h,m pub fn duration_from_sequence(sequence: u16) -> (u32, u32, u32, u32, u32) { let mut n_minutes = sequence as u32 * 10; @@ -547,15 +386,17 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value) -> Element<'a, Me .width(Length::Fill) .spacing(20) .align_items(Alignment::Center) - .push(text("Activate recovery path after:")) + .push(text("Keys can move the funds after inactivity of:")) .push( Row::new() .push( Container::new( form::Form::new_trimmed("ex: 1000", sequence, |v| { - Message::DefineDescriptor(message::DefineDescriptor::SequenceModal( - message::SequenceModal::SequenceEdited(v), - )) + Message::DefineDescriptor( + message::DefineDescriptor::ThresholdSequenceModal( + message::ThresholdSequenceModal::SequenceEdited(v), + ), + ) }) .warning("Sequence must be superior to 0 and inferior to 65535"), ) @@ -592,9 +433,11 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value) -> Element<'a, Me .push( Container::new( slider(1..=u16::MAX, sequence, |v| { - Message::DefineDescriptor(message::DefineDescriptor::SequenceModal( - message::SequenceModal::SequenceEdited(v.to_string()), - )) + Message::DefineDescriptor( + message::DefineDescriptor::ThresholdSequenceModal( + message::ThresholdSequenceModal::SequenceEdited(v.to_string()), + ), + ) }) .step(144_u16), // 144 blocks per day ) @@ -603,10 +446,12 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value) -> Element<'a, Me } } - card::simple(col.push(if sequence.valid { + card::modal(col.push(if sequence.valid { button::primary(None, "Apply") .on_press(Message::DefineDescriptor( - message::DefineDescriptor::SequenceModal(message::SequenceModal::ConfirmSequence), + message::DefineDescriptor::ThresholdSequenceModal( + message::ThresholdSequenceModal::Confirm, + ), )) .width(Length::Fixed(200.0)) } else { @@ -616,6 +461,35 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value) -> Element<'a, Me .into() } +pub fn edit_threshold_modal<'a>(threshold: (usize, usize)) -> Element<'a, Message> { + card::modal( + Column::new() + .width(Length::Fill) + .spacing(20) + .align_items(Alignment::Center) + .push(threshsold_input::threshsold_input( + threshold.0, + threshold.1, + |v| { + Message::DefineDescriptor(message::DefineDescriptor::ThresholdSequenceModal( + message::ThresholdSequenceModal::ThresholdEdited(v), + )) + }, + )) + .push( + button::primary(None, "Apply") + .on_press(Message::DefineDescriptor( + message::DefineDescriptor::ThresholdSequenceModal( + message::ThresholdSequenceModal::Confirm, + ), + )) + .width(Length::Fixed(200.0)), + ), + ) + .width(Length::Fixed(800.0)) + .into() +} + mod threshsold_input { use iced::alignment::{self, Alignment}; use iced::widget::{component, Component}; diff --git a/gui/src/installer/view/editor/template/custom.rs b/gui/src/installer/view/editor/template/custom.rs new file mode 100644 index 00000000..30703929 --- /dev/null +++ b/gui/src/installer/view/editor/template/custom.rs @@ -0,0 +1,206 @@ +use iced::{alignment, widget::Space, Alignment, Length}; + +use liana_ui::{ + color, + component::{ + button, collapse, + text::{h3, p1_regular, text, Text}, + }, + icon, image, theme, + widget::*, +}; + +use crate::installer::{ + message::{self, Message}, + prompt, + step::descriptor::editor::key::Key, + view::{ + editor::{define_descriptor_advanced_settings, defined_key, path, undefined_key}, + layout, + }, +}; + +pub fn custom_template_description(progress: (usize, usize)) -> Element<'static, Message> { + layout( + progress, + None, + "Introduction", + Column::new() + .align_items(Alignment::Start) + .push(h3("Custom wallet")) + .max_width(800.0) + .push(Container::new( + p1_regular("Through this setup you can choose how many keys you want to use. For security reasons, we suggest you use Hardware Wallets to store them.") + .style(color::GREY_2) + .horizontal_alignment(alignment::Horizontal::Left) + ).align_x(alignment::Horizontal::Left).width(Length::Fill)) + .push(Container::new( + p1_regular("For this Custom wallet you will need to define your Primary and Recovery Sets of Keys.") + .style(color::GREY_2) + .horizontal_alignment(alignment::Horizontal::Left) + ).align_x(alignment::Horizontal::Left).width(Length::Fill)) + .push(image::custom_template_description().width(Length::Fill)) + .push(Container::new( + p1_regular("The Primary set of Keys will always be able to spend. Your Recovery set(s) of Keys will activate only after a defined time of wallet inactivity, allowing for secure recovery and advanced spending policies. You can define more than one set of Recovery Keys activating at different times.") + .style(color::GREY_2) + .horizontal_alignment(alignment::Horizontal::Left) + ).align_x(alignment::Horizontal::Left).width(Length::Fill)) + .push(Row::new().push(Space::with_width(Length::Fill)).push(button::primary(None, "Select").width(Length::Fixed(200.0)).on_press(Message::Next))) + .spacing(20), + true, + Some(Message::Previous), + ) +} + +pub struct Path<'a> { + pub keys: Vec>, + pub sequence: u16, + pub duplicate_sequence: bool, + pub threshold: usize, +} + +pub fn custom_template<'a>( + progress: (usize, usize), + use_taproot: bool, + primary_path: Path<'a>, + recovery_paths: &mut dyn Iterator>, + valid: bool, +) -> Element<'a, Message> { + layout( + progress, + None, + "Set keys", + Column::new() + .align_items(Alignment::Start) + .max_width(1000.0) + .push(collapse::Collapse::new( + || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Advanced settings").small().bold()) + .push(icon::collapse_icon()), + ) + .style(theme::Button::Transparent) + }, + || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Advanced settings").small().bold()) + .push(icon::collapsed_icon()), + ) + .style(theme::Button::Transparent) + }, + move || define_descriptor_advanced_settings(use_taproot), + )) + .push(p1_regular(prompt::DEFINE_DESCRIPTOR_PRIMARY_PATH_TOOLTIP).style(color::GREY_2)) + .push( + path( + color::GREEN, + Some("Primary spending option:".to_string()), + primary_path.sequence, + primary_path.duplicate_sequence, + primary_path.threshold, + primary_path + .keys + .iter() + .enumerate() + .map(|(i, primary_key)| { + if let Some(key) = primary_key { + defined_key( + &key.name, + color::GREEN, + "Primary key", + if use_taproot && !key.is_compatible_taproot { + Some("Key is not compatible with taproot") + } else { + None + }, + i == 0, + ) + } else { + undefined_key( + color::GREEN, + "Primary key", + !primary_path.keys[0..i].iter().any(|k| k.is_none()), + i == 0, + ) + } + .map(move |msg| message::DefinePath::Key(i, msg)) + }) + .collect(), + false, + ) + .map(|msg| Message::DefineDescriptor(message::DefineDescriptor::Path(0, msg))), + ) + .push(p1_regular(prompt::DEFINE_DESCRIPTOR_RECOVERY_PATH_TOOLTIP).style(color::GREY_2)) + .push(recovery_paths.into_iter().enumerate().fold( + Column::new().spacing(20), + |col, (i, p)| { + col.push( + path( + color::ORANGE, + Some(format!("Recovery option #{}:", i + 1)), + p.sequence, + p.duplicate_sequence, + p.threshold, + p.keys + .iter() + .enumerate() + .map(|(j, recovery_key)| { + if let Some(key) = recovery_key { + defined_key( + &key.name, + color::ORANGE, + "Recovery key", + if use_taproot && !key.is_compatible_taproot { + Some("Key is not compatible with Taproot") + } else { + None + }, + false, + ) + } else { + undefined_key( + color::ORANGE, + "Recovery key", + !p.keys[0..j].iter().any(|k| k.is_none()), + false, + ) + } + .map(move |msg| message::DefinePath::Key(j, msg)) + }) + .collect(), + false, + ) + .map(move |msg| { + Message::DefineDescriptor(message::DefineDescriptor::Path(i + 1, msg)) + }), + ) + }, + )) + .push( + Row::new() + .push( + button::secondary(Some(icon::plus_icon()), "Add recovery option") + .width(Length::Fixed(200.0)) + .on_press(Message::DefineDescriptor( + message::DefineDescriptor::AddRecoveryPath, + )), + ) + .push(Space::with_width(Length::Fill)) + .push( + button::primary(None, "Continue") + .width(Length::Fixed(200.0)) + .on_press_maybe(if valid { Some(Message::Next) } else { None }), + ), + ) + .push(Space::with_height(100.0)) + .spacing(20), + true, + Some(Message::Previous), + ) +} diff --git a/gui/src/installer/view/editor/template/inheritance.rs b/gui/src/installer/view/editor/template/inheritance.rs index 1c1c2f4a..aab9dc58 100644 --- a/gui/src/installer/view/editor/template/inheritance.rs +++ b/gui/src/installer/view/editor/template/inheritance.rs @@ -3,14 +3,22 @@ use iced::{alignment, widget::Space, Alignment, Length}; use liana_ui::{ color, component::{ - button, - text::{h3, p1_regular}, + button, collapse, + text::{h3, p1_regular, text, Text}, }, - image, + icon, image, theme, widget::*, }; -use crate::installer::{message::Message, view::layout}; +use crate::installer::{ + context, + message::{self, Message}, + step::descriptor::editor::key::Key, + view::{ + editor::{define_descriptor_advanced_settings, defined_key, path, undefined_key}, + layout, + }, +}; pub fn inheritance_template_description(progress: (usize, usize)) -> Element<'static, Message> { layout( @@ -22,19 +30,14 @@ pub fn inheritance_template_description(progress: (usize, usize)) -> Element<'st .push(h3("Inheritance wallet")) .max_width(800.0) .push(Container::new( - p1_regular("In this current setup you will need 2 Keys for your wallet. For security reasons, we suggest you to use 2 Hardware Wallets to store them.") - .style(color::GREY_3) - .horizontal_alignment(alignment::Horizontal::Left) - ).align_x(alignment::Horizontal::Left).width(Length::Fill)) - .push(Container::new( - p1_regular("For this Inheritance wallet you will need 2 Keys: Your Primary Key and an Inheritance Key to be given to a chosen relative.") - .style(color::GREY_3) + p1_regular("For this Inheritance wallet you will need 2 Keys: Your Primary Key and an Inheritance Key to be given to a chosen heir. For security reasons, we suggest you use 2 Hardware Wallets to store them.") + .style(color::GREY_2) .horizontal_alignment(alignment::Horizontal::Left) ).align_x(alignment::Horizontal::Left).width(Length::Fill)) .push(image::inheritance_template_description().width(Length::Fill)) .push(Container::new( p1_regular("Your relative’s Inheritance Key will become active only if you don’t move the coins in your wallet for the defined period of time, enabling him/her to recover your funds while not being able to access them before that.") - .style(color::GREY_3) + .style(color::GREY_2) .horizontal_alignment(alignment::Horizontal::Left) ).align_x(alignment::Horizontal::Left).width(Length::Fill)) .push(Row::new().push(Space::with_width(Length::Fill)).push(button::primary(None, "Select").width(Length::Fixed(200.0)).on_press(Message::Next))) @@ -43,3 +46,119 @@ pub fn inheritance_template_description(progress: (usize, usize)) -> Element<'st Some(Message::Previous), ) } + +pub fn inheritance_template<'a>( + progress: (usize, usize), + use_taproot: bool, + primary_key: Option<&'a Key>, + recovery_key: Option<&'a Key>, + sequence: u16, + valid: bool, +) -> Element<'a, Message> { + layout( + progress, + None, + "Set keys", + Column::new() + .align_items(Alignment::Start) + .max_width(1000.0) + .push(collapse::Collapse::new( + || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Advanced settings").small().bold()) + .push(icon::collapse_icon()), + ) + .style(theme::Button::Transparent) + }, + || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Advanced settings").small().bold()) + .push(icon::collapsed_icon()), + ) + .style(theme::Button::Transparent) + }, + move || define_descriptor_advanced_settings(use_taproot), + )) + .push( + path( + color::GREEN, + None, + 0, + false, + 1, + vec![if let Some(key) = primary_key { + defined_key( + &key.name, + color::GREEN, + "Primary key", + if use_taproot && !key.is_compatible_taproot { + Some("Key is not compatible with Taproot") + } else { + None + }, + true, + ) + } else { + undefined_key(color::GREEN, "Primary key", true, true) + } + .map(|msg| message::DefinePath::Key(0, msg))], + true, + ) + .map(|msg| Message::DefineDescriptor(message::DefineDescriptor::Path(0, msg))), + ) + .push( + path( + color::WHITE, + None, + sequence, + false, + 1, + vec![if let Some(key) = recovery_key { + defined_key( + &key.name, + color::WHITE, + "Inheritance key", + if use_taproot && !key.is_compatible_taproot { + Some("Key is not compatible with taproot") + } else { + None + }, + true, + ) + } else { + undefined_key(color::WHITE, "Inheritance key", primary_key.is_some(), true) + } + .map(|msg| message::DefinePath::Key(0, msg))], + true, + ) + .map(|msg| Message::DefineDescriptor(message::DefineDescriptor::Path(1, msg))), + ) + .push( + Row::new() + .push( + button::secondary(None, "Customize") + .width(Length::Fixed(200.0)) + .on_press(Message::DefineDescriptor( + message::DefineDescriptor::ChangeTemplate( + context::DescriptorTemplate::Custom, + ), + )), + ) + .push(Space::with_width(Length::Fill)) + .push( + button::primary(None, "Continue") + .width(Length::Fixed(200.0)) + .on_press_maybe(if valid { Some(Message::Next) } else { None }), + ), + ) + .spacing(20), + true, + Some(Message::Previous), + ) +} diff --git a/gui/src/installer/view/editor/template/mod.rs b/gui/src/installer/view/editor/template/mod.rs index 867e3885..38482e3f 100644 --- a/gui/src/installer/view/editor/template/mod.rs +++ b/gui/src/installer/view/editor/template/mod.rs @@ -1,3 +1,4 @@ +pub mod custom; pub mod inheritance; use iced::{alignment, Alignment, Length}; @@ -18,13 +19,13 @@ pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static, layout( progress, None, - "Choose Wallet Type", + "Choose wallet type", Column::new() .max_width(800.0) .align_items(Alignment::Start) .push(Container::new( - p1_regular("What do you want your wallet for? Also consider this depends on the amount of funds you have, the more funds, higher the security should be. Not sure about the wallet type? We can help you.") - .style(color::GREY_3) + p1_regular("What do you want your wallet for? This depends on the amount of funds you have, the more funds, the higher the security should be. Not sure about the wallet type? We can help you.") + .style(color::GREY_2) .horizontal_alignment(alignment::Horizontal::Left) ).align_x(alignment::Horizontal::Left).width(Length::Fill)) .push( @@ -35,7 +36,7 @@ pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static, Column::new() .align_items(Alignment::Start) .push(h3("Simple inheritance")) - .push(p2_regular("Two keys required, one for yourself to spend and another for your heir.").style(color::GREY_3)) + .push(p2_regular("Two keys required, one for yourself to spend and another for your heir.").style(color::GREY_2)) .width(Length::Fill) ) .push(button::secondary(None, "Select").on_press( @@ -54,12 +55,12 @@ pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static, Column::new() .align_items(Alignment::Start) .push(h3("Custom (choose your own)")) - .push(p2_regular("Create a custom set up that fits all your need").style(color::GREY_3)) + .push(p2_regular("Create a custom setup that fits all your needs").style(color::GREY_2)) .width(Length::Fill) ) .push(button::secondary(None, "Select").on_press( Message::SelectDescriptorTemplate( - context::DescriptorTemplate::Custom, + context::DescriptorTemplate::Custom , ), )), ) diff --git a/gui/src/installer/view/mod.rs b/gui/src/installer/view/mod.rs index 62fcac18..9f5818ab 100644 --- a/gui/src/installer/view/mod.rs +++ b/gui/src/installer/view/mod.rs @@ -1268,10 +1268,6 @@ pub fn start_internal_bitcoind<'a>( install_state: Option<&InstallState>, ) -> Element<'a, Message> { let version = crate::node::bitcoind::VERSION; - let mut next_button = button::secondary(None, "Next").width(Length::Fixed(200.0)); - if let Some(Ok(_)) = started { - next_button = next_button.on_press(Message::Next); - }; layout( progress, None, @@ -1364,11 +1360,7 @@ pub fn start_internal_bitcoind<'a>( } }) .spacing(50) - .push( - Row::new() - .spacing(10) - .push(Row::new().spacing(10).push(next_button)), - ) + .push(Row::new()) .push_maybe(error.map(|e| card::invalid(text(e)))), true, Some(message::Message::InternalBitcoind( @@ -1415,6 +1407,57 @@ pub fn install<'a>( ) } +pub fn defined_threshold<'a>( + color: iced::Color, + fixed: bool, + threshold: (usize, usize), +) -> Element<'a, message::DefinePath> { + if !fixed && threshold.1 > 1 { + Button::new( + Row::new() + .spacing(10) + .push((0..threshold.1).fold(Row::new(), |row, i| { + if i < threshold.0 { + row.push(icon::round_key_icon().style(color)) + } else { + row.push(icon::round_key_icon()) + } + })) + .push(text(format!( + "{} out of {} key{}", + threshold.0, + threshold.1, + if threshold.0 > 1 { "s" } else { "" }, + ))) + .push(icon::pencil_icon()), + ) + .padding(10) + .on_press(message::DefinePath::EditThreshold) + .style(theme::Button::Secondary) + .into() + } else { + card::simple( + Row::new() + .spacing(10) + .push((0..threshold.1).fold(Row::new(), |row, i| { + if i < threshold.0 { + row.push(icon::round_key_icon().style(color)) + } else { + row.push(icon::round_key_icon()) + } + })) + .push(text(format!( + "{} out of {} key{}", + threshold.0, + threshold.1, + if threshold.0 > 1 { "s" } else { "" }, + ))), + ) + .padding(10) + .into() + } +} + pub fn defined_sequence<'a>( sequence: u16, duplicate_sequence: bool, @@ -1423,53 +1466,66 @@ pub fn defined_sequence<'a>( Container::new( Column::new() .spacing(5) + .push(if sequence != 0 { + Row::new().align_items(Alignment::Center).push( + Container::new( + Row::new() + .align_items(Alignment::Center) + .spacing(5) + .push( + text::p1_regular("Available after inactivity of ~") + .style(color::GREY_2), + ) + .push( + Button::new( + Row::new() + .padding(5) + .spacing(5) + .align_items(Alignment::Center) + .push(text( + [ + (n_years, "y"), + (n_months, "m"), + (n_days, "d"), + (n_hours, "h"), + (n_minutes, "mn"), + ] + .iter() + .filter_map(|(n, unit)| { + if *n > 0 { + Some(format!("{}{}", n, unit)) + } else { + None + } + }) + .collect::>() + .join(" "), + )) + .push(icon::pencil_icon()), + ) + .style(theme::Button::Secondary) + .on_press(message::DefinePath::EditSequence), + ), + ) + .width(Length::Fill) + .padding(5) + .align_y(alignment::Vertical::Center), + ) + } else { + Row::new() + .push(p1_regular("Able to move the funds at any time.").style(color::GREY_2)) + .padding(5) + }) .push_maybe(if duplicate_sequence { Some( - text("No two recovery paths may become available at the very same date.") + text("No two recovery options may become available at the very same date.") .small() .style(color::RED), ) } else { None }) - .push( - Row::new() - .align_items(Alignment::Center) - .push( - Container::new( - Column::new() - .spacing(5) - .push(text(format!("Available after {} blocks", sequence)).bold()) - .push( - [ - (n_years, "y"), - (n_months, "m"), - (n_days, "d"), - (n_hours, "h"), - (n_minutes, "mn"), - ] - .iter() - .fold( - Row::new().spacing(5), - |row, (n, unit)| { - row.push_maybe(if *n > 0 { - Some(text(format!("{}{}", n, unit,))) - } else { - None - }) - }, - ), - ), - ) - .padding(5) - .align_y(alignment::Vertical::Center), - ) - .push( - button::secondary(Some(icon::pencil_icon()), "Edit") - .on_press(message::DefinePath::EditSequence), - ) - .spacing(15), - ), + .spacing(15), ) .padding(5) .into() diff --git a/gui/ui/src/component/card.rs b/gui/ui/src/component/card.rs index d15b7b82..2ccf69bd 100644 --- a/gui/ui/src/component/card.rs +++ b/gui/ui/src/component/card.rs @@ -1,5 +1,11 @@ use crate::{color, component::text::text, icon, theme, widget::*}; +pub fn modal<'a, T: 'a, C: Into>>(content: C) -> Container<'a, T> { + Container::new(content) + .padding(15) + .style(theme::Container::Card(theme::Card::Modal)) +} + pub fn simple<'a, T: 'a, C: Into>>(content: C) -> Container<'a, T> { Container::new(content) .padding(15) diff --git a/gui/ui/src/icon.rs b/gui/ui/src/icon.rs index 1b09f48d..b09d7dc3 100644 --- a/gui/ui/src/icon.rs +++ b/gui/ui/src/icon.rs @@ -115,6 +115,14 @@ pub fn previous_icon() -> Text<'static> { bootstrap_icon('\u{F284}') } +pub fn check_icon() -> Text<'static> { + bootstrap_icon('\u{F633}') +} + +pub fn round_key_icon() -> Text<'static> { + bootstrap_icon('\u{F44E}') +} + const ICONEX_ICONS: Font = Font::with_name("Untitled1"); fn iconex_icon(unicode: char) -> Text<'static> { diff --git a/gui/ui/src/image.rs b/gui/ui/src/image.rs index 96715537..ed4b8bae 100644 --- a/gui/ui/src/image.rs +++ b/gui/ui/src/image.rs @@ -67,3 +67,11 @@ pub fn inheritance_template_description() -> Svg { let h = Handle::from_memory(INHERITANCE_TEMPLATE_DESC.to_vec()); Svg::new(h) } + +const CUSTOM_TEMPLATE_DESC: &[u8] = + include_bytes!("../static/images/custom_template_description.svg"); + +pub fn custom_template_description() -> Svg { + let h = Handle::from_memory(CUSTOM_TEMPLATE_DESC.to_vec()); + Svg::new(h) +} diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs index 85f0cef1..bc04e2fa 100644 --- a/gui/ui/src/theme.rs +++ b/gui/ui/src/theme.rs @@ -288,6 +288,7 @@ impl Notification { pub enum Card { #[default] Simple, + Modal, Border, Invalid, Warning, @@ -302,10 +303,14 @@ impl Card { background: Some(color::GREY_2.into()), ..container::Appearance::default() }, + Card::Modal => container::Appearance { + background: Some(color::GREY_2.into()), + ..container::Appearance::default() + }, Card::Border => container::Appearance { background: Some(iced::Color::TRANSPARENT.into()), border: iced::Border { - color: color::GREY_2, + color: color::GREY_7, width: 1.0, radius: 10.0.into(), }, @@ -347,10 +352,19 @@ impl Card { }, ..container::Appearance::default() }, + Card::Modal => container::Appearance { + background: Some(color::LIGHT_BLACK.into()), + border: iced::Border { + color: color::TRANSPARENT, + width: 0.0, + radius: 25.0.into(), + }, + ..container::Appearance::default() + }, Card::Border => container::Appearance { background: Some(iced::Color::TRANSPARENT.into()), border: iced::Border { - color: color::GREY_5, + color: color::GREY_7, width: 1.0, radius: 25.0.into(), }, @@ -790,8 +804,11 @@ impl button::StyleSheet for Theme { } } fn disabled(&self, style: &Self::Style) -> button::Appearance { - let active = self.active(style); - + let active = if let Button::Primary = style { + self.active(&Button::Secondary) + } else { + self.active(style) + }; button::Appearance { shadow_offset: iced::Vector::default(), background: Some(color::TRANSPARENT.into()), diff --git a/gui/ui/static/images/custom_template_description.svg b/gui/ui/static/images/custom_template_description.svg new file mode 100644 index 00000000..5873d98a --- /dev/null +++ b/gui/ui/static/images/custom_template_description.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +