From d0ec811bef457bfcb266e6d39c7aaec07e34c8e0 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 1 Oct 2024 15:46:37 +0200 Subject: [PATCH] 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) - } - } -}