diff --git a/gui/src/installer/config.rs b/gui/src/installer/config.rs index cc92bcb5..696ab269 100644 --- a/gui/src/installer/config.rs +++ b/gui/src/installer/config.rs @@ -18,7 +18,7 @@ impl TryFrom for LianaConfig { daemon: false, log_level: log::LevelFilter::Info, main_descriptor: ctx.descriptor.unwrap(), - data_dir: ctx.data_dir, + data_dir: Some(ctx.data_dir), bitcoin_config: ctx.bitcoin_config, bitcoind_config: ctx.bitcoind_config, }) diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 5d28bb2d..d9baf202 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -6,6 +6,9 @@ use crate::hw::HardwareWallet; #[derive(Debug, Clone)] pub enum Message { + CreateWallet, + ImportWallet, + BackupDone(bool), Event(iced_native::Event), Exit(PathBuf), Clibpboard(String), diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 8f4f9e0e..3f8596fd 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -1,5 +1,6 @@ mod config; mod message; +mod prompt; mod step; mod view; @@ -16,7 +17,10 @@ use crate::{ }; pub use message::Message; -use step::{Context, DefineBitcoind, DefineDescriptor, Final, RegisterDescriptor, Step, Welcome}; +use step::{ + BackupDescriptor, Context, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, + RegisterDescriptor, Step, Welcome, +}; pub struct Installer { should_exit: bool, @@ -28,12 +32,6 @@ pub struct Installer { } impl Installer { - fn next(&mut self) { - if self.current < self.steps.len() - 1 { - self.current += 1; - } - } - fn previous(&mut self) { if self.current > 0 { self.current -= 1; @@ -48,14 +46,8 @@ impl Installer { Installer { should_exit: false, current: 0, - steps: vec![ - Welcome::new(network, destination_path.clone()).into(), - DefineDescriptor::new().into(), - RegisterDescriptor::default().into(), - DefineBitcoind::new().into(), - Final::new().into(), - ], - context: Context::new(network, Some(destination_path)), + steps: vec![Welcome::default().into()], + context: Context::new(network, destination_path), }, Command::none(), ) @@ -73,35 +65,62 @@ impl Installer { self.should_exit = true; } + fn next(&mut self) -> Command { + let current_step = self + .steps + .get_mut(self.current) + .expect("There is always a step"); + if current_step.apply(&mut self.context) { + if self.current < self.steps.len() - 1 { + self.current += 1; + } + // skip the step according to the current context. + while self + .steps + .get(self.current) + .expect("There is always a step") + .skip(&self.context) + { + if self.current < self.steps.len() - 1 { + self.current += 1; + } + } + // calculate new current_step. + let current_step = self + .steps + .get_mut(self.current) + .expect("There is always a step"); + current_step.load_context(&self.context); + return current_step.load(); + } + Command::none() + } + pub fn update(&mut self, message: Message) -> Command { match message { - Message::Clibpboard(s) => clipboard::write(s), - Message::Next => { - let current_step = self - .steps - .get_mut(self.current) - .expect("There is always a step"); - if current_step.apply(&mut self.context) { - self.next(); - // skip the step according to the current context. - while self - .steps - .get(self.current) - .expect("There is always a step") - .skip(&self.context) - { - self.next(); - } - // calculate new current_step. - let current_step = self - .steps - .get_mut(self.current) - .expect("There is always a step"); - current_step.load_context(&self.context); - return current_step.load(); - } - Command::none() + Message::CreateWallet => { + self.steps = vec![ + Welcome::default().into(), + DefineDescriptor::new().into(), + BackupDescriptor::default().into(), + RegisterDescriptor::default().into(), + DefineBitcoind::new().into(), + Final::new().into(), + ]; + self.next() } + Message::ImportWallet => { + self.steps = vec![ + Welcome::default().into(), + ImportDescriptor::new().into(), + RegisterDescriptor::default().into(), + DefineBitcoind::new().into(), + Final::new().into(), + ]; + self.next() + } + Message::Clibpboard(s) => clipboard::write(s), + Message::Next => self.next(), Message::Previous => { self.previous(); Command::none() @@ -114,7 +133,7 @@ impl Installer { Command::perform(install(self.context.clone()), Message::Installed) } Message::Installed(Err(e)) => { - let mut data_dir = self.context.data_dir.clone().unwrap(); + let mut data_dir = self.context.data_dir.clone(); data_dir.push(self.context.bitcoin_config.network.to_string()); // In case of failure during install, block the thread to // deleted the data_dir/network directory in order to start clean again. @@ -141,15 +160,19 @@ impl Installer { self.steps .get(self.current) .expect("There is always a step") - .view() + .view((self.current, self.steps.len() - 1)) } } pub async fn install(ctx: Context) -> Result { let hardware_wallets = ctx - .hw_tokens + .hws .iter() - .map(|(kind, fingerprint, token)| HardwareWalletConfig::new(kind, fingerprint, token)) + .filter_map(|(kind, fingerprint, token)| { + token + .as_ref() + .map(|token| HardwareWalletConfig::new(kind, fingerprint, token)) + }) .collect(); let mut cfg: liana::config::Config = ctx diff --git a/gui/src/installer/prompt.rs b/gui/src/installer/prompt.rs new file mode 100644 index 00000000..c84211c9 --- /dev/null +++ b/gui/src/installer/prompt.rs @@ -0,0 +1,2 @@ +pub const BACKUP_DESCRIPTOR_MESSAGE: &str = "The descriptor is necessary to recover your funds. The backup of your key (via mnemonics, sometimes called 'seed words') is not enough. Please make sure you have backed up both your private key and your descriptor."; +pub const BACKUP_DESCRIPTOR_HELP: &str = "In Bitcoin, the coins are locked using a Script (related to the 'address'). In order to recover your funds you need both to know the Scripts you have participated in (your 'addresses'), and be able to sign a transaction that spends from those. For the ability to sign you backup your private key, this is your mnemonics ('seed words'). For finding the coins that belongs to you you backup a template of your Script ( / 'addresses'), this is your descriptor. Note however the descriptor needs not be as securely stored as the private key. A thief that steals your descriptor but not your private key will not be able to steal your funds."; diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index 22c6ec49..45cd5632 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::str::FromStr; use iced::{Command, Element}; @@ -8,7 +9,7 @@ use liana::{ util::bip32::{DerivationPath, Fingerprint}, Network, }, - descriptor::{Descriptor, DescriptorMultiXKey, DescriptorPublicKey, Wildcard}, + descriptor::{DescriptorMultiXKey, DescriptorPublicKey, Wildcard}, }, }; @@ -24,7 +25,8 @@ use crate::{ pub struct DefineDescriptor { network: Network, - imported_descriptor: form::Value, + network_valid: bool, + data_dir: Option, user_xpub: form::Value, heir_xpub: form::Value, sequence: form::Value, @@ -37,7 +39,8 @@ impl DefineDescriptor { pub fn new() -> Self { Self { network: Network::Bitcoin, - imported_descriptor: form::Value::default(), + data_dir: None, + network_valid: true, user_xpub: form::Value::default(), heir_xpub: form::Value::default(), sequence: form::Value::default(), @@ -55,12 +58,14 @@ impl Step for DefineDescriptor { Message::Close => { self.modal = None; } + Message::Network(network) => { + self.network = network; + let mut network_datadir = self.data_dir.clone().unwrap(); + network_datadir.push(self.network.to_string()); + self.network_valid = !network_datadir.exists(); + } Message::DefineDescriptor(msg) => { match msg { - message::DefineDescriptor::ImportDescriptor(desc) => { - self.imported_descriptor.value = desc; - self.imported_descriptor.valid = true; - } message::DefineDescriptor::UserXpubEdited(xpub) => { self.user_xpub.value = xpub; self.user_xpub.valid = true; @@ -107,78 +112,41 @@ impl Step for DefineDescriptor { fn load_context(&mut self, ctx: &Context) { self.network = ctx.bitcoin_config.network; + self.data_dir = Some(ctx.data_dir.clone()); + let mut network_datadir = ctx.data_dir.clone(); + network_datadir.push(self.network.to_string()); + self.network_valid = !network_datadir.exists(); } fn apply(&mut self, ctx: &mut Context) -> bool { + ctx.bitcoin_config.network = self.network; // descriptor forms for import or creation cannot be both empty or filled. - if self.imported_descriptor.value.is_empty() - == (self.user_xpub.value.is_empty() - || self.heir_xpub.value.is_empty() - || self.sequence.value.is_empty()) + let user_key = DescriptorPublicKey::from_str(&self.user_xpub.value); + self.user_xpub.valid = user_key.is_ok(); + if let Ok(key) = &user_key { + self.user_xpub.valid = check_key_network(key, self.network); + } + + let heir_key = DescriptorPublicKey::from_str(&self.heir_xpub.value); + self.heir_xpub.valid = heir_key.is_ok(); + if let Ok(key) = &heir_key { + self.heir_xpub.valid = check_key_network(key, self.network); + } + + let sequence = self.sequence.value.parse::(); + self.sequence.valid = sequence.is_ok(); + + if !self.network_valid + || !self.user_xpub.valid + || !self.heir_xpub.valid + || !self.sequence.valid { - if !self.user_xpub.value.is_empty() { - let key = DescriptorPublicKey::from_str(&self.user_xpub.value); - self.user_xpub.valid = key.is_ok(); - // Check the Network - if let Ok(key) = &key { - self.user_xpub.valid = check_key_network(key, ctx.bitcoin_config.network); - } - } + return false; + } - if !self.heir_xpub.value.is_empty() { - let key = DescriptorPublicKey::from_str(&self.heir_xpub.value); - self.heir_xpub.valid = key.is_ok(); - // Check the Network - if let Ok(key) = &key { - self.heir_xpub.valid = check_key_network(key, ctx.bitcoin_config.network); - } - } - - if !self.sequence.value.is_empty() { - self.sequence.valid = self.sequence.value.parse::().is_ok(); - } else { - self.sequence.valid = false; - } - - if !self.imported_descriptor.value.is_empty() { - self.imported_descriptor.valid = - Descriptor::::from_str(&self.imported_descriptor.value) - .is_ok(); - } - false - } else if !self.imported_descriptor.value.is_empty() { - if let Ok(desc) = MultipathDescriptor::from_str(&self.imported_descriptor.value) { - ctx.descriptor = Some(desc); - true - } else { - self.imported_descriptor.valid = false; - false - } - } else { - let user_key = DescriptorPublicKey::from_str(&self.user_xpub.value); - self.user_xpub.valid = user_key.is_ok(); - if let Ok(key) = &user_key { - self.user_xpub.valid = check_key_network(key, ctx.bitcoin_config.network); - } - - let heir_key = DescriptorPublicKey::from_str(&self.heir_xpub.value); - self.heir_xpub.valid = heir_key.is_ok(); - if let Ok(key) = &heir_key { - self.heir_xpub.valid = check_key_network(key, ctx.bitcoin_config.network); - } - - let sequence = self.sequence.value.parse::(); - self.sequence.valid = sequence.is_ok(); - - if !self.user_xpub.valid || !self.heir_xpub.valid || !self.sequence.valid { - return false; - } - - let desc = match MultipathDescriptor::new( - user_key.unwrap(), - heir_key.unwrap(), - sequence.unwrap(), - ) { + let desc = + match MultipathDescriptor::new(user_key.unwrap(), heir_key.unwrap(), sequence.unwrap()) + { Ok(desc) => desc, Err(e) => { self.error = Some(e.to_string()); @@ -186,18 +154,18 @@ impl Step for DefineDescriptor { } }; - ctx.descriptor = Some(desc); - true - } + ctx.descriptor = Some(desc); + true } - fn view(&self) -> Element { + fn view(&self, progress: (usize, usize)) -> Element { if let Some(modal) = &self.modal { modal.view() } else { view::define_descriptor( + progress, self.network, - &self.imported_descriptor, + self.network_valid, &self.user_xpub, &self.heir_xpub, &self.sequence, @@ -341,12 +309,99 @@ async fn get_extended_pubkey( })) } +pub struct ImportDescriptor { + network: Network, + network_valid: bool, + data_dir: Option, + imported_descriptor: form::Value, + error: Option, +} + +impl ImportDescriptor { + pub fn new() -> Self { + Self { + network: Network::Bitcoin, + network_valid: true, + data_dir: None, + imported_descriptor: form::Value::default(), + error: None, + } + } +} + +impl Step for ImportDescriptor { + // 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, message: Message) -> Command { + match message { + Message::Network(network) => { + self.network = network; + let mut network_datadir = self.data_dir.clone().unwrap(); + network_datadir.push(self.network.to_string()); + self.network_valid = !network_datadir.exists(); + } + Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) => { + self.imported_descriptor.value = desc; + self.imported_descriptor.valid = true; + } + _ => {} + }; + Command::none() + } + + fn load_context(&mut self, ctx: &Context) { + self.network = ctx.bitcoin_config.network; + self.data_dir = Some(ctx.data_dir.clone()); + let mut network_datadir = ctx.data_dir.clone(); + network_datadir.push(self.network.to_string()); + self.network_valid = !network_datadir.exists(); + } + + fn apply(&mut self, ctx: &mut Context) -> bool { + ctx.bitcoin_config.network = self.network; + // descriptor forms for import or creation cannot be both empty or filled. + if !self.imported_descriptor.value.is_empty() { + if let Ok(desc) = MultipathDescriptor::from_str(&self.imported_descriptor.value) { + ctx.descriptor = Some(desc); + true + } else { + self.imported_descriptor.valid = false; + false + } + } else { + false + } + } + + fn view(&self, progress: (usize, usize)) -> Element { + view::import_descriptor( + progress, + self.network, + self.network_valid, + &self.imported_descriptor, + self.error.as_ref(), + ) + } +} + +impl Default for ImportDescriptor { + fn default() -> Self { + Self::new() + } +} + +impl From for Box { + fn from(s: ImportDescriptor) -> Box { + Box::new(s) + } +} + #[derive(Default)] pub struct RegisterDescriptor { descriptor: Option, processing: bool, chosen_hw: Option, - hws: Vec<(HardwareWallet, Option<[u8; 32]>)>, + hws: Vec<(HardwareWallet, Option<[u8; 32]>, bool)>, error: Option, } @@ -357,7 +412,7 @@ impl Step for RegisterDescriptor { fn update(&mut self, message: Message) -> Command { match message { Message::Select(i) => { - if let Some((hw, hmac)) = self.hws.get(i) { + if let Some((hw, hmac, _)) = self.hws.get(i) { if hmac.is_none() { let device = hw.device.clone(); let descriptor = self.descriptor.as_ref().unwrap().to_string(); @@ -381,7 +436,8 @@ impl Step for RegisterDescriptor { .iter_mut() .find(|hw_h| hw_h.0.fingerprint == fingerprint) { - hw_h.1 = Some(hmac.unwrap_or([0x00; 32])); + hw_h.1 = hmac; + hw_h.2 = true; } } Err(e) => self.error = Some(e), @@ -392,9 +448,9 @@ impl Step for RegisterDescriptor { if !self .hws .iter() - .any(|(h, _)| h.fingerprint == hw.fingerprint) + .any(|(h, _, _)| h.fingerprint == hw.fingerprint) { - self.hws.push((hw, None)); + self.hws.push((hw, None, false)); } } } @@ -406,11 +462,9 @@ impl Step for RegisterDescriptor { Command::none() } fn apply(&mut self, ctx: &mut Context) -> bool { - for (hw, token) in &self.hws { - if let Some(token) = token { - if *token != [0x00; 32] { - ctx.hw_tokens.push((hw.kind, hw.fingerprint, *token)); - } + for (hw, token, registered) in &self.hws { + if *registered { + ctx.hws.push((hw.kind, hw.fingerprint, *token)); } } true @@ -421,9 +475,10 @@ impl Step for RegisterDescriptor { Message::ConnectedHardwareWallets, ) } - fn view(&self) -> Element { + fn view(&self, progress: (usize, usize)) -> Element { let desc = self.descriptor.as_ref().unwrap(); view::register_descriptor( + progress, desc.to_string(), &self.hws, self.error.as_ref(), @@ -450,3 +505,31 @@ impl From for Box { Box::new(s) } } + +#[derive(Default)] +pub struct BackupDescriptor { + done: bool, + descriptor: Option, +} + +impl Step for BackupDescriptor { + fn update(&mut self, message: Message) -> Command { + if let Message::BackupDone(done) = message { + self.done = done; + } + Command::none() + } + fn load_context(&mut self, ctx: &Context) { + self.descriptor = ctx.descriptor.clone(); + } + fn view(&self, progress: (usize, usize)) -> Element { + let desc = self.descriptor.as_ref().unwrap(); + view::backup_descriptor(progress, desc.to_string(), 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 52c243d6..b3a20ff1 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -1,5 +1,5 @@ mod descriptor; -pub use descriptor::{DefineDescriptor, RegisterDescriptor}; +pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor}; use std::path::PathBuf; use std::str::FromStr; @@ -24,7 +24,7 @@ pub trait Step { fn update(&mut self, _message: Message) -> Command { Command::none() } - fn view(&self) -> Element; + fn view(&self, progress: (usize, usize)) -> Element; fn load_context(&mut self, _ctx: &Context) {} fn load(&self) -> Command { Command::none() @@ -42,18 +42,22 @@ pub struct Context { pub bitcoin_config: BitcoinConfig, pub bitcoind_config: Option, pub descriptor: Option, - pub hw_tokens: Vec<(DeviceKind, bitcoin::util::bip32::Fingerprint, [u8; 32])>, - pub data_dir: Option, + pub hws: Vec<( + DeviceKind, + bitcoin::util::bip32::Fingerprint, + Option<[u8; 32]>, + )>, + pub data_dir: PathBuf, } impl Context { - pub fn new(network: bitcoin::Network, data_dir: Option) -> Self { + pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self { Self { bitcoin_config: BitcoinConfig { network, poll_interval_secs: Duration::from_secs(30), }, - hw_tokens: Vec::new(), + hws: Vec::new(), bitcoind_config: None, descriptor: None, data_dir, @@ -61,36 +65,12 @@ impl Context { } } -pub struct Welcome { - network: bitcoin::Network, - data_dir: PathBuf, -} - -impl Welcome { - pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self { - Self { network, data_dir } - } - - fn valid(&self) -> bool { - let mut network_datadir = self.data_dir.clone(); - network_datadir.push(self.network.to_string()); - !network_datadir.exists() - } -} +#[derive(Default)] +pub struct Welcome {} impl Step for Welcome { - fn update(&mut self, message: Message) -> Command { - if let message::Message::Network(network) = message { - self.network = network; - } - Command::none() - } - fn apply(&mut self, ctx: &mut Context) -> bool { - ctx.bitcoin_config.network = self.network; - true - } - fn view(&self) -> Element { - view::welcome(&self.network, self.valid()) + fn view(&self, _progress: (usize, usize)) -> Element { + view::welcome() } } @@ -211,8 +191,8 @@ impl Step for DefineBitcoind { } } - fn view(&self) -> Element { - view::define_bitcoin(&self.address, &self.cookie_path) + fn view(&self, progress: (usize, usize)) -> Element { + view::define_bitcoin(progress, &self.address, &self.cookie_path) } } @@ -230,6 +210,7 @@ impl From for Box { pub struct Final { generating: bool, + context: Option, warning: Option, config_path: Option, } @@ -237,6 +218,7 @@ pub struct Final { impl Final { pub fn new() -> Self { Self { + context: None, generating: false, warning: None, config_path: None, @@ -245,6 +227,9 @@ impl Final { } impl Step for Final { + fn load_context(&mut self, ctx: &Context) { + self.context = Some(ctx.clone()); + } fn update(&mut self, message: Message) -> Command { match message { Message::Installed(res) => { @@ -267,8 +252,13 @@ impl Step for Final { Command::none() } - fn view(&self) -> Element { + fn view(&self, progress: (usize, usize)) -> Element { + let ctx = self.context.as_ref().unwrap(); + let desc = ctx.descriptor.as_ref().unwrap().to_string(); view::install( + progress, + ctx, + desc, self.generating, self.config_path.as_ref(), self.warning.as_ref(), diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 2efb2677..713d0d71 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -1,4 +1,4 @@ -use iced::widget::{Button, Column, Container, PickList, Row, Scrollable}; +use iced::widget::{Button, Checkbox, Column, Container, PickList, Row, Scrollable}; use iced::{Alignment, Element, Length}; use liana::miniscript::bitcoin; @@ -7,12 +7,13 @@ use crate::{ hw::HardwareWallet, installer::{ message::{self, Message}, + step::Context, Error, }, ui::{ color, component::{ - button, card, container, form, + button, card, collapse, container, form, text::{text, Text}, }, icon, @@ -20,28 +21,89 @@ use crate::{ }, }; -const NETWORKS: [bitcoin::Network; 4] = [ - bitcoin::Network::Bitcoin, - bitcoin::Network::Testnet, - bitcoin::Network::Signet, - bitcoin::Network::Regtest, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Network { + Mainnet, + Testnet, + Regtest, + Signet, +} + +impl From for Network { + fn from(n: bitcoin::Network) -> Self { + match n { + bitcoin::Network::Bitcoin => Network::Mainnet, + bitcoin::Network::Testnet => Network::Testnet, + bitcoin::Network::Regtest => Network::Regtest, + bitcoin::Network::Signet => Network::Signet, + } + } +} + +impl From for bitcoin::Network { + fn from(network: Network) -> bitcoin::Network { + match network { + Network::Mainnet => bitcoin::Network::Bitcoin, + Network::Testnet => bitcoin::Network::Testnet, + Network::Regtest => bitcoin::Network::Regtest, + Network::Signet => bitcoin::Network::Signet, + } + } +} + +impl std::fmt::Display for Network { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Mainnet => write!(f, "Bitcoin mainnet"), + Self::Testnet => write!(f, "Bitcoin testnet"), + Self::Regtest => write!(f, "Bitcoin regtest"), + Self::Signet => write!(f, "Bitcoin signet"), + } + } +} + +const NETWORKS: [Network; 4] = [ + Network::Mainnet, + Network::Testnet, + Network::Signet, + Network::Regtest, ]; -pub fn welcome(network: &bitcoin::Network, valid: bool) -> Element { +pub fn welcome<'a>() -> Element<'a, Message> { Container::new(Container::new( Column::new() - .push(Container::new( - PickList::new(&NETWORKS[..], Some(*network), message::Message::Network).padding(10), - )) - .push(if valid { - Container::new( - button::primary(None, "Start the install") - .on_press(Message::Next) - .width(Length::Units(200)), - ) - } else { - card::warning("A data directory already exists for this network".to_string()) - }) + .push( + Row::new() + .spacing(20) + .push( + Button::new( + Container::new( + Column::new() + .width(Length::Units(200)) + .push(icon::wallet_icon().size(50).width(Length::Units(100))) + .push(text("Create new wallet")) + .align_items(Alignment::Center), + ) + .padding(50), + ) + .style(button::Style::Border.into()) + .on_press(Message::CreateWallet), + ) + .push( + Button::new( + Container::new( + Column::new() + .width(Length::Units(200)) + .push(icon::import_icon().size(50).width(Length::Units(100))) + .push(text("Import wallet")) + .align_items(Alignment::Center), + ) + .padding(50), + ) + .style(button::Style::Border.into()) + .on_press(Message::ImportWallet), + ), + ) .width(Length::Fill) .height(Length::Fill) .padding(100) @@ -56,27 +118,34 @@ pub fn welcome(network: &bitcoin::Network, valid: bool) -> Element { } pub fn define_descriptor<'a>( + progress: (usize, usize), network: bitcoin::Network, - imported_descriptor: &form::Value, + network_valid: bool, user_xpub: &form::Value, heir_xpub: &form::Value, sequence: &form::Value, error: Option<&String>, ) -> Element<'a, Message> { - let col_descriptor = Column::new() - .push(text("Descriptor:").bold()) - .push( - form::Form::new("Descriptor", imported_descriptor, |msg| { - Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg)) + let row_network = Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push(text("Network:").bold()) + .push(Container::new( + PickList::new(&NETWORKS[..], Some(Network::from(network)), |net| { + Message::Network(net.into()) }) - .warning("Please enter correct descriptor") - .size(20) .padding(10), - ) - .spacing(10); + )) + .push_maybe(if network_valid { + None + } else { + Some(card::warning( + "A data directory already exists for this network".to_string(), + )) + }); let col_user_xpub = Column::new() - .push(text("Your xpub:").bold()) + .push(text("Your public key:").bold()) .push( Row::new() .push( @@ -100,7 +169,7 @@ pub fn define_descriptor<'a>( .spacing(10); let col_heir_xpub = Column::new() - .push(text("Heir xpub:").bold()) + .push(text("Public key of the recovery key:").bold()) .push( Row::new() .push( @@ -124,7 +193,7 @@ pub fn define_descriptor<'a>( .spacing(10); let col_sequence = Column::new() - .push(text("Number of block:").bold()) + .push(text("Number of block before enabling recovery:").bold()) .push( Container::new( form::Form::new("Number of block", sequence, |msg| { @@ -139,22 +208,21 @@ pub fn define_descriptor<'a>( .spacing(10); layout( + progress, Column::new() - .push(text("Create the descriptor").bold().size(50)) + .push(text("Create the wallet").bold().size(50)) .push( Column::new() + .push(row_network) .push(col_user_xpub) .push(col_sequence) .push(col_heir_xpub) .spacing(20), ) - .push(text("or import it").bold().size(25)) - .push(col_descriptor) .push( - if !imported_descriptor.value.is_empty() - && (!user_xpub.value.is_empty() - || !heir_xpub.value.is_empty() - || !sequence.value.is_empty()) + if user_xpub.value.is_empty() + && heir_xpub.value.is_empty() + && sequence.value.is_empty() { button::primary(None, "Next").width(Length::Units(200)) } else { @@ -172,30 +240,111 @@ pub fn define_descriptor<'a>( ) } +pub fn import_descriptor<'a>( + progress: (usize, usize), + network: bitcoin::Network, + network_valid: bool, + imported_descriptor: &form::Value, + error: Option<&String>, +) -> Element<'a, Message> { + let row_network = Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push(text("Network:").bold()) + .push(Container::new( + PickList::new(&NETWORKS[..], Some(Network::from(network)), |net| { + Message::Network(net.into()) + }) + .padding(10), + )) + .push_maybe(if network_valid { + None + } else { + Some(card::warning( + "A data directory already exists for this network".to_string(), + )) + }); + let col_descriptor = Column::new() + .push(text("Descriptor:").bold()) + .push( + form::Form::new("Descriptor", imported_descriptor, |msg| { + Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg)) + }) + .warning("Please enter correct descriptor") + .size(20) + .padding(10), + ) + .spacing(10); + layout( + progress, + Column::new() + .push(text("Import the wallet").bold().size(50)) + .push( + Column::new() + .spacing(20) + .push(row_network) + .push(col_descriptor), + ) + .push(if !imported_descriptor.value.is_empty() { + button::primary(None, "Next").width(Length::Units(200)) + } else { + button::primary(None, "Next") + .width(Length::Units(200)) + .on_press(Message::Next) + }) + .push_maybe(error.map(|e| card::error("Invalid descriptor", e.to_string()))) + .width(Length::Fill) + .height(Length::Fill) + .padding(100) + .spacing(50) + .align_items(Alignment::Center), + ) +} + pub fn register_descriptor<'a>( + progress: (usize, usize), descriptor: String, - hws: &[(HardwareWallet, Option<[u8; 32]>)], + hws: &[(HardwareWallet, Option<[u8; 32]>, bool)], error: Option<&Error>, processing: bool, chosen_hw: Option, ) -> Element<'a, Message> { layout( + progress, Column::new() .push(text("Register descriptor").bold().size(50)) - .push( + .push(card::simple( Column::new() + .push(text("The descriptor:").small().bold()) .push(text(descriptor.clone()).small()) .push( - button::transparent_border(Some(icon::clipboard_icon()), "Copy") - .on_press(Message::Clibpboard(descriptor)), + Row::new().push(Column::new().width(Length::Fill)).push( + button::transparent_border(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clibpboard(descriptor)), + ), ) .spacing(10) - .align_items(Alignment::Center), - ) + .max_width(1000), + )) .push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string()))) - .push(if !hws.is_empty() { + .push( Column::new() - .push(text(format!("{} hardware wallets connected", hws.len())).bold()) + .push( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push( + Container::new( + text(format!("{} hardware wallets connected", hws.len())) + .bold(), + ) + .width(Length::Fill), + ) + .push( + button::border(Some(icon::reload_icon()), "Refresh") + .on_press(Message::Reload), + ), + ) .spacing(10) .push( hws.iter() @@ -206,26 +355,19 @@ pub fn register_descriptor<'a>( &hw.0, Some(i) == chosen_hw, processing, - hw.1.is_some(), + hw.2, )) }), ) - .width(Length::Fill) + .width(Length::Fill), + ) + .push(if processing { + button::primary(None, "Next").width(Length::Units(200)) } else { - Column::new().push(card::simple( - Column::new() - .spacing(20) - .push("No hardware wallet connected") - .push(button::primary(None, "Refresh").on_press(Message::Reload)) - .align_items(Alignment::Center) - .width(Length::Fill), - )) - }) - .push( button::primary(None, "Next") .on_press(Message::Next) - .width(Length::Units(200)), - ) + .width(Length::Units(200)) + }) .width(Length::Fill) .height(Length::Fill) .padding(100) @@ -234,7 +376,86 @@ pub fn register_descriptor<'a>( ) } +pub fn backup_descriptor<'a>( + progress: (usize, usize), + descriptor: String, + done: bool, +) -> Element<'a, Message> { + layout( + progress, + Column::new() + .push( + text("Did you backup your wallet descriptor ?") + .bold() + .size(50), + ) + .push( + Column::new() + .push(text(super::prompt::BACKUP_DESCRIPTOR_MESSAGE)) + .push(collapse::Collapse::new( + || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Learn more").small().bold()) + .push(icon::collapse_icon()), + ) + .style(button::Style::Transparent.into()) + }, + || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Learn more").small().bold()) + .push(icon::collapsed_icon()), + ) + .style(button::Style::Transparent.into()) + }, + help_backup, + )) + .max_width(1000), + ) + .push(card::simple( + Column::new() + .push(text("The descriptor:").small().bold()) + .push(text(descriptor.clone()).small()) + .push( + Row::new().push(Column::new().width(Length::Fill)).push( + button::transparent_border(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clibpboard(descriptor)), + ), + ) + .spacing(10) + .max_width(1000), + )) + .push(Checkbox::new( + done, + "I have backed up my descriptor", + Message::BackupDone, + )) + .push(if done { + button::primary(None, "Next") + .on_press(Message::Next) + .width(Length::Units(200)) + } else { + button::primary(None, "Next").width(Length::Units(200)) + }) + .width(Length::Fill) + .height(Length::Fill) + .padding(100) + .spacing(50) + .align_items(Alignment::Center), + ) +} + +pub fn help_backup<'a>() -> Element<'a, Message> { + text(super::prompt::BACKUP_DESCRIPTOR_HELP).small().into() +} + pub fn define_bitcoin<'a>( + progress: (usize, usize), address: &form::Value, cookie_path: &form::Value, ) -> Element<'a, Message> { @@ -263,6 +484,7 @@ pub fn define_bitcoin<'a>( .spacing(10); layout( + progress, Column::new() .push( text("Set up connection to the Bitcoin full node") @@ -285,15 +507,88 @@ pub fn define_bitcoin<'a>( } pub fn install<'a>( + progress: (usize, usize), + context: &Context, + descriptor: String, generating: bool, config_path: Option<&std::path::PathBuf>, warning: Option<&'a String>, ) -> Element<'a, Message> { let mut col = Column::new() + .push( + Container::new( + Column::new() + .spacing(10) + .push( + card::simple( + Column::new() + .spacing(5) + .push(text("Descriptor:").small().bold()) + .push(text(descriptor).small()), + ) + .width(Length::Fill), + ) + .push( + card::simple( + Column::new() + .spacing(5) + .push(text("Hardware devices:").small().bold()) + .push(context.hws.iter().fold(Column::new(), |acc, hw| { + acc.push( + Row::new() + .spacing(5) + .push(text(hw.0.to_string()).small()) + .push(text(format!("(fingerprint: {})", hw.1)).small()), + ) + })), + ) + .width(Length::Fill), + ) + .push( + card::simple( + Column::new() + .push(text("Bitcoind:").small().bold()) + .push( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(text("Cookie path:").small()) + .push( + text(format!( + "{}", + context + .bitcoind_config + .as_ref() + .unwrap() + .cookie_path + .to_string_lossy() + )) + .small(), + ), + ) + .push( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(text("Address:").small()) + .push( + text(format!( + "{}", + context.bitcoind_config.as_ref().unwrap().addr + )) + .small(), + ), + ), + ) + .width(Length::Fill), + ), + ) + .padding(50) + .max_width(1000), + ) + .spacing(50) .width(Length::Fill) .height(Length::Fill) - .padding(100) - .spacing(50) .align_items(Alignment::Center); if let Some(error) = warning { @@ -327,7 +622,7 @@ pub fn install<'a>( ); } - layout(col) + layout(progress, col) } pub fn hardware_wallet_xpubs_modal<'a>( @@ -341,17 +636,32 @@ pub fn hardware_wallet_xpubs_modal<'a>( Column::new() .push( text(if is_heir { - "Import the Heir xpub" + "Import the recovery public key" } else { - "Import the user xpub" + "Import the user public key" }) .bold() .size(50), ) .push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string()))) - .push(if !hws.is_empty() { + .push( Column::new() - .push(text(format!("{} hardware wallets connected", hws.len())).bold()) + .push( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push( + Container::new( + text(format!("{} hardware wallets connected", hws.len())) + .bold(), + ) + .width(Length::Fill), + ) + .push( + button::border(Some(icon::reload_icon()), "Refresh") + .on_press(Message::Reload), + ), + ) .spacing(10) .push( hws.iter() @@ -366,22 +676,8 @@ pub fn hardware_wallet_xpubs_modal<'a>( )) }), ) - .width(Length::Fill) - } else { - Column::new() - .push( - card::simple( - Column::new() - .spacing(20) - .width(Length::Fill) - .push("Please connect a hardware wallet") - .push(button::primary(None, "Refresh").on_press(Message::Reload)) - .align_items(Alignment::Center), - ) - .width(Length::Fill), - ) - .width(Length::Fill) - }) + .width(Length::Fill), + ) .width(Length::Fill) .height(Length::Fill) .padding(100) @@ -435,13 +731,21 @@ fn hw_list_view<'a>( .into() } -fn layout<'a>(content: impl Into>) -> Element<'a, Message> { +fn layout<'a>( + progress: (usize, usize), + content: impl Into>, +) -> Element<'a, Message> { Container::new(Scrollable::new( Column::new() .push( Container::new(button::transparent(None, "< Previous").on_press(Message::Previous)) .padding(5), ) + .push( + Container::new(text(format!("{}/{}", progress.0, progress.1))) + .width(Length::Fill) + .center_x(), + ) .push(Container::new(content).width(Length::Fill).center_x()), )) .center_x() diff --git a/gui/src/ui/component/button.rs b/gui/src/ui/component/button.rs index 7fb50fbf..8f537114 100644 --- a/gui/src/ui/component/button.rs +++ b/gui/src/ui/component/button.rs @@ -16,6 +16,10 @@ pub fn transparent<'a, T: 'a>(icon: Option>, t: &'static str) -> button button::Button::new(content(icon, t)).style(Style::Transparent.into()) } +pub fn border<'a, T: 'a>(icon: Option>, t: &'static str) -> button::Button<'a, T> { + button::Button::new(content(icon, t)).style(Style::Border.into()) +} + pub fn transparent_border<'a, T: 'a>( icon: Option>, t: &'static str, diff --git a/gui/src/ui/component/collapse.rs b/gui/src/ui/component/collapse.rs index 480e6c38..839cbd81 100644 --- a/gui/src/ui/component/collapse.rs +++ b/gui/src/ui/component/collapse.rs @@ -5,41 +5,42 @@ use iced::{ use iced_lazy::{self, Component}; use std::marker::PhantomData; -use super::button::Style; +pub struct Collapse<'a, M, H, F, C> { + before: H, + after: F, + content: C, + phantom: PhantomData<&'a M>, +} -pub fn collapse< - 'a, +impl<'a, Message, T, H, F, C> Collapse<'a, Message, H, F, C> +where Message: 'a, T: Into + Clone + 'a, - H: Fn() -> Element<'a, T> + 'a, + H: Fn() -> Button<'a, Event> + 'a, + F: Fn() -> Button<'a, Event> + 'a, C: Fn() -> Element<'a, T> + 'a, ->( - header: H, - content: C, -) -> impl Into> { - Collapse { - header, - content, - phantom: PhantomData, +{ + pub fn new(before: H, after: F, content: C) -> Self { + Collapse { + before, + after, + content, + phantom: PhantomData, + } } } -struct Collapse<'a, H, C> { - header: H, - content: C, - phantom: PhantomData<&'a H>, -} - #[derive(Debug, Clone, Copy)] -enum Event { +pub enum Event { Internal(T), Collapse(bool), } -impl<'a, Message, T, H, C> Component for Collapse<'a, H, C> +impl<'a, Message, T, H, F, C> Component for Collapse<'a, Message, H, F, C> where T: Into + Clone + 'a, - H: Fn() -> Element<'a, T>, + H: Fn() -> Button<'a, Event>, + F: Fn() -> Button<'a, Event>, C: Fn() -> Element<'a, T>, { type State = bool; @@ -58,35 +59,27 @@ where fn view(&self, state: &Self::State) -> Element { if *state { Column::new() - .push( - Button::new((self.header)().map(Event::Internal)) - .style(Style::TransparentBorder.into()) - .padding(10) - .on_press(Event::Collapse(false)), - ) + .push((self.after)().on_press(Event::Collapse(false))) .push((self.content)().map(Event::Internal)) .into() } else { Column::new() - .push( - Button::new((self.header)().map(Event::Internal)) - .style(Style::TransparentBorder.into()) - .padding(10) - .on_press(Event::Collapse(true)), - ) + .push((self.before)().on_press(Event::Collapse(true))) .into() } } } -impl<'a, Message, T, H: 'a, C: 'a> From> for Element<'a, Message> +impl<'a, Message, T, H: 'a, F: 'a, C: 'a> From> + for Element<'a, Message> where Message: 'a, T: Into + Clone + 'a, - H: Fn() -> Element<'a, T, iced::Renderer>, + H: Fn() -> Button<'a, Event, iced::Renderer>, + F: Fn() -> Button<'a, Event, iced::Renderer>, C: Fn() -> Element<'a, T, iced::Renderer>, { - fn from(c: Collapse<'a, H, C>) -> Self { + fn from(c: Collapse<'a, Message, H, F, C>) -> Self { iced_lazy::component(c) } } diff --git a/gui/src/ui/icon.rs b/gui/src/ui/icon.rs index 537ee82d..50b7aee6 100644 --- a/gui/src/ui/icon.rs +++ b/gui/src/ui/icon.rs @@ -13,6 +13,18 @@ fn icon(unicode: char) -> Text<'static> { .size(20) } +pub fn reload_icon() -> Text<'static> { + icon('\u{F130}') +} + +pub fn import_icon() -> Text<'static> { + icon('\u{F30A}') +} + +pub fn wallet_icon() -> Text<'static> { + icon('\u{F615}') +} + pub fn hourglass_icon() -> Text<'static> { icon('\u{F41F}') } @@ -178,3 +190,11 @@ pub fn done_icon() -> Text<'static> { pub fn todo_icon() -> Text<'static> { icon('\u{F28A}') } + +pub fn collapse_icon() -> Text<'static> { + icon('\u{F284}') +} + +pub fn collapsed_icon() -> Text<'static> { + icon('\u{F282}') +}