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..6d8ec736 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -6,6 +6,8 @@ use crate::hw::HardwareWallet; #[derive(Debug, Clone)] pub enum Message { + CreateWallet, + ImportWallet, Event(iced_native::Event), Exit(PathBuf), Clibpboard(String), diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 8f4f9e0e..b95563d4 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -16,7 +16,10 @@ use crate::{ }; pub use message::Message; -use step::{Context, DefineBitcoind, DefineDescriptor, Final, RegisterDescriptor, Step, Welcome}; +use step::{ + Context, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, RegisterDescriptor, Step, + Welcome, +}; pub struct Installer { should_exit: bool, @@ -28,12 +31,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 +45,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 +64,61 @@ 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(), + 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 +131,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. diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index 22c6ec49..ff3710a7 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,9 +154,8 @@ impl Step for DefineDescriptor { } }; - ctx.descriptor = Some(desc); - true - } + ctx.descriptor = Some(desc); + true } fn view(&self) -> Element { @@ -197,7 +164,7 @@ impl Step for DefineDescriptor { } else { view::define_descriptor( self.network, - &self.imported_descriptor, + self.network_valid, &self.user_xpub, &self.heir_xpub, &self.sequence, @@ -341,6 +308,92 @@ 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) -> Element { + view::import_descriptor( + 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, diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 52c243d6..00e56977 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::{DefineDescriptor, ImportDescriptor, RegisterDescriptor}; use std::path::PathBuf; use std::str::FromStr; @@ -43,11 +43,11 @@ pub struct Context { pub bitcoind_config: Option, pub descriptor: Option, pub hw_tokens: Vec<(DeviceKind, bitcoin::util::bip32::Fingerprint, [u8; 32])>, - pub data_dir: Option, + 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, @@ -61,36 +61,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()) + view::welcome() } } diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index c9962002..bf578a97 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -68,24 +68,41 @@ const NETWORKS: [Network; 4] = [ 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::from(*network)), |net| { - Message::Network(net.into()) - }) - .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) @@ -101,26 +118,32 @@ pub fn welcome(network: &bitcoin::Network, valid: bool) -> Element { pub fn define_descriptor<'a>( 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( @@ -144,7 +167,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( @@ -168,7 +191,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| { @@ -184,21 +207,19 @@ pub fn define_descriptor<'a>( layout( 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 { @@ -216,6 +237,65 @@ pub fn define_descriptor<'a>( ) } +pub fn import_descriptor<'a>( + 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( + 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>( descriptor: String, hws: &[(HardwareWallet, Option<[u8; 32]>)], @@ -385,9 +465,9 @@ 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), diff --git a/gui/src/ui/icon.rs b/gui/src/ui/icon.rs index 537ee82d..892f76b9 100644 --- a/gui/src/ui/icon.rs +++ b/gui/src/ui/icon.rs @@ -13,6 +13,14 @@ fn icon(unicode: char) -> Text<'static> { .size(20) } +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}') }