From 9ae33408cb0c0446e5ce729d46aa23dce3eb614d Mon Sep 17 00:00:00 2001 From: edouardparis Date: Wed, 29 May 2024 12:19:35 +0200 Subject: [PATCH 1/5] Move all network setup in launcher --- gui/src/installer/message.rs | 6 +- gui/src/installer/mod.rs | 10 +- gui/src/installer/step/descriptor.rs | 186 +++++------------- gui/src/installer/view.rs | 212 +++------------------ gui/src/launcher.rs | 269 +++++++++++++++++++++------ gui/src/main.rs | 12 +- 6 files changed, 293 insertions(+), 402 deletions(-) diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 4c298101..e60fe0c0 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -1,7 +1,4 @@ -use liana::miniscript::{ - bitcoin::{bip32::Fingerprint, Network}, - DescriptorPublicKey, -}; +use liana::miniscript::{bitcoin::bip32::Fingerprint, DescriptorPublicKey}; use std::path::PathBuf; use super::Error; @@ -29,7 +26,6 @@ pub enum Message { Select(usize), UseHotSigner, Installed(Result), - Network(Network), CreateTaprootDescriptor(bool), SelectBitcoindType(SelectBitcoindTypeMsg), InternalBitcoind(InternalBitcoindMsg), diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index aa842d9b..e507f049 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -29,6 +29,7 @@ use step::{ }; pub struct Installer { + network: bitcoin::Network, current: usize, steps: Vec>, hws: HardwareWallets, @@ -61,6 +62,7 @@ impl Installer { ) -> (Installer, Command) { ( Installer { + network, current: 0, hws: HardwareWallets::new(destination_path.clone(), network), steps: vec![Welcome::default().into()], @@ -135,7 +137,7 @@ impl Installer { Message::CreateWallet => { self.steps = vec![ Welcome::default().into(), - DefineDescriptor::new(self.signer.clone()).into(), + DefineDescriptor::new(self.network, self.signer.clone()).into(), BackupMnemonic::new(self.signer.clone()).into(), BackupDescriptor::default().into(), RegisterDescriptor::new_create_wallet().into(), @@ -149,8 +151,8 @@ impl Installer { Message::ParticipateWallet => { self.steps = vec![ Welcome::default().into(), - ParticipateXpub::new(self.signer.clone()).into(), - ImportDescriptor::new(false).into(), + ParticipateXpub::new(self.network, self.signer.clone()).into(), + ImportDescriptor::new(self.network).into(), BackupMnemonic::new(self.signer.clone()).into(), RegisterDescriptor::new_import_wallet().into(), SelectBitcoindTypeStep::new().into(), @@ -163,7 +165,7 @@ impl Installer { Message::ImportWallet => { self.steps = vec![ Welcome::default().into(), - ImportDescriptor::new(true).into(), + ImportDescriptor::new(self.network).into(), RecoverMnemonic::default().into(), RegisterDescriptor::new_import_wallet().into(), SelectBitcoindTypeStep::new().into(), diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index 40b13fb0..d91f6303 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -1,6 +1,5 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::iter::FromIterator; -use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -196,11 +195,9 @@ impl Setup { } pub struct DefineDescriptor { - data_dir: Option, - setup: HashMap, + setup: Setup, network: Network, - network_valid: bool, use_taproot: bool, modal: Option>, @@ -210,13 +207,11 @@ pub struct DefineDescriptor { } impl DefineDescriptor { - pub fn new(signer: Arc>) -> Self { + pub fn new(network: Network, signer: Arc>) -> Self { Self { - network: Network::Bitcoin, + network, use_taproot: false, - setup: HashMap::from([(Network::Bitcoin, Setup::new())]), - data_dir: None, - network_valid: true, + setup: Setup::new(), modal: None, signer, error: None, @@ -224,25 +219,10 @@ impl DefineDescriptor { } fn valid(&self) -> bool { - self.setup[&self.network].valid() + self.setup.valid() } fn setup_mut(&mut self) -> &mut Setup { - self.setup - .get_mut(&self.network) - .expect("There is always one") - } - - fn set_network(&mut self, network: Network) { - self.network = network; - if self.setup.get(&self.network).is_none() { - self.setup.insert(self.network, Setup::new()); - } - self.signer.lock().unwrap().set_network(network); - if let Some(mut network_datadir) = self.data_dir.clone() { - network_datadir.push(self.network.to_string()); - self.network_valid = !network_datadir.exists(); - } - self.check_setup() + &mut self.setup } fn check_setup(&mut self) { @@ -257,16 +237,11 @@ impl Step for DefineDescriptor { // form value is set as valid each time it is edited. // Verification of the values is happening when the user click on Next button. fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { - let network = self.network; self.error = None; match message { Message::Close => { self.modal = None; } - Message::Network(network) => { - hws.set_network(network); - self.set_network(network) - } Message::CreateTaprootDescriptor(use_taproot) => { self.use_taproot = use_taproot; self.check_setup(); @@ -313,6 +288,7 @@ impl Step for DefineDescriptor { } message::DefineKey::Edit => { let use_taproot = self.use_taproot; + let network = self.network; let setup = self.setup_mut(); let modal = EditXpubModal::new( use_taproot, @@ -424,7 +400,7 @@ impl Step for DefineDescriptor { j, self.network, self.signer.clone(), - self.setup[&self.network].keys.clone(), + self.setup.keys.clone(), ); let cmd = modal.load(); self.modal = Some(Box::new(modal)); @@ -459,11 +435,6 @@ impl Step for DefineDescriptor { Command::none() } - fn load_context(&mut self, ctx: &Context) { - self.data_dir = Some(ctx.data_dir.clone()); - self.set_network(ctx.bitcoin_config.network) - } - fn subscription(&self, hws: &HardwareWallets) -> Subscription { if let Some(modal) = &self.modal { modal.subscription(hws) @@ -478,9 +449,10 @@ impl Step for DefineDescriptor { 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[&self.network].spending_keys.iter().clone() { + for spending_key in self.setup.spending_keys.iter().clone() { let fingerprint = spending_key.expect("Must be present at this step"); - let key = self.setup[&self.network] + let key = self + .setup .keys .iter() .find(|key| key.key.master_fingerprint() == fingerprint) @@ -506,11 +478,12 @@ impl Step for DefineDescriptor { let mut recovery_paths = BTreeMap::new(); - for path in &self.setup[&self.network].recovery_paths { + for path in &self.setup.recovery_paths { 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[&self.network] + let key = self + .setup .keys .iter() .find(|key| key.key.master_fingerprint() == fingerprint) @@ -544,14 +517,14 @@ impl Step for DefineDescriptor { recovery_paths.insert(path.sequence, recovery_keys); } - if !self.network_valid || spending_keys.is_empty() { + if spending_keys.is_empty() { return false; } let spending_keys = if spending_keys.len() == 1 { PathInfo::Single(spending_keys[0].clone()) } else { - PathInfo::Multi(self.setup[&self.network].spending_threshold, spending_keys) + PathInfo::Multi(self.setup.spending_threshold, spending_keys) }; let policy = match if self.use_taproot { @@ -576,13 +549,11 @@ impl Step for DefineDescriptor { hws: &'a HardwareWallets, progress: (usize, usize), ) -> Element<'a, Message> { - let aliases = self.setup[&self.network].keys_aliases(); + let aliases = self.setup.keys_aliases(); let content = view::define_descriptor( progress, - self.network, - self.network_valid, self.use_taproot, - self.setup[&self.network] + self.setup .spending_keys .iter() .enumerate() @@ -590,10 +561,8 @@ impl Step for DefineDescriptor { if let Some(key) = key { view::defined_descriptor_key( aliases.get(key).unwrap().to_string(), - self.setup[&self.network].duplicate_name.contains(key), - self.setup[&self.network] - .incompatible_with_tapminiscript - .contains(key), + self.setup.duplicate_name.contains(key), + self.setup.incompatible_with_tapminiscript.contains(key), ) } else { view::undefined_descriptor_key() @@ -605,16 +574,16 @@ impl Step for DefineDescriptor { }) }) .collect(), - self.setup[&self.network].spending_threshold, - self.setup[&self.network] + self.setup.spending_threshold, + self.setup .recovery_paths .iter() .enumerate() .map(|(i, path)| { path.view( &aliases, - &self.setup[&self.network].duplicate_name, - &self.setup[&self.network].incompatible_with_tapminiscript, + &self.setup.duplicate_name, + &self.setup.incompatible_with_tapminiscript, ) .map(move |msg| { Message::DefineDescriptor(message::DefineDescriptor::RecoveryPath(i, msg)) @@ -1133,13 +1102,6 @@ pub struct HardwareWalletXpubs { error: Option, } -impl HardwareWalletXpubs { - fn reset(&mut self) { - self.error = None; - self.xpubs = Vec::new(); - } -} - pub struct SignerXpubs { signer: Arc>, xpubs: Vec, @@ -1155,11 +1117,6 @@ impl SignerXpubs { } } - fn reset(&mut self) { - self.xpubs = Vec::new(); - self.next_account = ChildNumber::from_hardened_idx(0).unwrap(); - } - fn select(&mut self, network: Network) { self.next_account = self.next_account.increment().unwrap(); let signer = self.signer.lock().unwrap(); @@ -1180,8 +1137,6 @@ impl SignerXpubs { pub struct ParticipateXpub { network: Network, - network_valid: bool, - data_dir: Option, shared: bool, @@ -1190,33 +1145,14 @@ pub struct ParticipateXpub { } impl ParticipateXpub { - pub fn new(signer: Arc>) -> Self { + pub fn new(network: Network, signer: Arc>) -> Self { Self { - network: Network::Bitcoin, - network_valid: true, - data_dir: None, + network, hw_xpubs: Vec::new(), shared: false, xpubs_signer: SignerXpubs::new(signer), } } - - fn set_network(&mut self, network: Network) { - if network != self.network { - self.hw_xpubs.iter_mut().for_each(|hw| hw.reset()); - self.xpubs_signer.reset(); - } - self.network = network; - self.xpubs_signer - .signer - .lock() - .unwrap() - .set_network(network); - if let Some(mut network_datadir) = self.data_dir.clone() { - network_datadir.push(self.network.to_string()); - self.network_valid = !network_datadir.exists(); - } - } } impl Step for ParticipateXpub { @@ -1224,10 +1160,6 @@ impl Step for ParticipateXpub { // Verification of the values is happening when the user click on Next button. fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { match message { - Message::Network(network) => { - hws.set_network(network); - self.set_network(network); - } Message::UserActionDone(shared) => self.shared = shared, Message::ImportXpub(fg, res) => { if let Some(hw_xpubs) = self.hw_xpubs.iter_mut().find(|x| x.fingerprint == fg) { @@ -1292,11 +1224,6 @@ impl Step for ParticipateXpub { hws.refresh().map(Message::HardwareWallets) } - fn load_context(&mut self, ctx: &Context) { - self.data_dir = Some(ctx.data_dir.clone()); - self.set_network(ctx.bitcoin_config.network); - } - fn apply(&mut self, ctx: &mut Context) -> bool { ctx.bitcoin_config.network = self.network; // Drop connections to hardware wallets. @@ -1307,8 +1234,6 @@ impl Step for ParticipateXpub { fn view<'a>(&'a self, hws: &'a HardwareWallets, progress: (usize, usize)) -> Element { view::participate_xpub( progress, - self.network, - self.network_valid, hws.list .iter() .enumerate() @@ -1344,21 +1269,15 @@ impl From for Box { pub struct ImportDescriptor { network: Network, - network_valid: bool, - change_network: bool, - data_dir: Option, imported_descriptor: form::Value, wrong_network: bool, error: Option, } impl ImportDescriptor { - pub fn new(change_network: bool) -> Self { + pub fn new(network: Network) -> Self { Self { - change_network, - network: Network::Bitcoin, - network_valid: true, - data_dir: None, + network, imported_descriptor: form::Value::default(), wrong_network: false, error: None, @@ -1397,32 +1316,13 @@ 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, _hws: &mut HardwareWallets, message: Message) -> Command { - match message { - Message::Network(network) => { - self.network = network; - let mut network_datadir = self.data_dir.clone().unwrap(); - network_datadir.push(self.network.to_string()); - self.network_valid = !network_datadir.exists(); - self.check_descriptor(self.network); - } - Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) => { - self.imported_descriptor.value = desc; - self.check_descriptor(self.network); - } - _ => {} - }; - Command::none() - } - - fn load_context(&mut self, ctx: &Context) { - if ctx.bitcoin_config.network != self.network { - self.check_descriptor(ctx.bitcoin_config.network); + if let Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) = + message + { + self.imported_descriptor.value = desc; + self.check_descriptor(self.network); } - 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(); + Command::none() } fn apply(&mut self, ctx: &mut Context) -> bool { @@ -1441,9 +1341,6 @@ impl Step for ImportDescriptor { fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { view::import_descriptor( progress, - self.change_network, - self.network, - self.network_valid, &self.imported_descriptor, self.wrong_network, self.error.as_ref(), @@ -1650,6 +1547,7 @@ impl From for Box { mod tests { use super::*; use iced_runtime::command::Action; + use std::path::PathBuf; use std::sync::{Arc, Mutex}; pub struct Sandbox { @@ -1686,9 +1584,10 @@ mod tests { #[tokio::test] async fn test_define_descriptor_use_hotkey() { let mut ctx = Context::new(Network::Signet, PathBuf::from_str("/").unwrap()); - let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new(Arc::new( - Mutex::new(Signer::generate(Network::Bitcoin).unwrap()), - ))); + let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new( + Network::Bitcoin, + Arc::new(Mutex::new(Signer::generate(Network::Bitcoin).unwrap())), + )); // Edit primary key sandbox @@ -1767,9 +1666,10 @@ mod tests { #[tokio::test] async fn test_define_descriptor_stores_if_hw_is_used() { let mut ctx = Context::new(Network::Testnet, PathBuf::from_str("/").unwrap()); - let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new(Arc::new( - Mutex::new(Signer::generate(Network::Testnet).unwrap()), - ))); + let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new( + Network::Testnet, + Arc::new(Mutex::new(Signer::generate(Network::Testnet).unwrap())), + )); sandbox.load(&ctx).await; let specter_key = message::DefinePath::Key( diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 606d5f69..461092f6 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -32,55 +32,6 @@ use crate::{ }, }; -#[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, - _ => Network::Mainnet, - } - } -} - -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<'a>() -> Element<'a, Message> { Container::new( Column::new() @@ -191,31 +142,7 @@ impl std::fmt::Display for DescriptorKind { } #[allow(clippy::too_many_arguments)] -pub fn define_descriptor_advanced_settings<'a>( - network: bitcoin::Network, - network_valid: bool, - use_taproot: bool, -) -> Element<'a, Message> { - let col_network = Column::new() - .spacing(10) - .push(text("Network").bold()) - .push(container( - pick_list(&NETWORKS[..], Some(Network::from(network)), |net| { - Message::Network(net.into()) - }) - .style(if network_valid { - theme::PickList::Secondary - } else { - theme::PickList::Invalid - }) - .padding(10), - )) - .push_maybe(if network_valid { - None - } else { - Some(text("A data directory already exists for this network").style(color::RED)) - }); - +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()) @@ -238,12 +165,7 @@ pub fn define_descriptor_advanced_settings<'a>( .spacing(20) .push(Space::with_height(0)) .push(separation().width(500)) - .push( - Row::new() - .push(col_network) - .push(Space::with_width(100)) - .push(col_wallet), - ) + .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") @@ -259,8 +181,6 @@ pub fn define_descriptor_advanced_settings<'a>( #[allow(clippy::too_many_arguments)] pub fn define_descriptor<'a>( progress: (usize, usize), - network: bitcoin::Network, - network_valid: bool, use_taproot: bool, spending_keys: Vec>, spending_threshold: usize, @@ -329,34 +249,29 @@ pub fn define_descriptor<'a>( progress, "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(network, network_valid, use_taproot) - }, - ) - .collapsed(!network_valid), - ) + .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) @@ -384,7 +299,7 @@ pub fn define_descriptor<'a>( )) .width(Length::Fixed(200.0)), ) - .push(if !valid || !network_valid { + .push(if !valid { button::primary(None, "Next").width(Length::Fixed(200.0)) } else { button::primary(None, "Next") @@ -455,33 +370,10 @@ pub fn recovery_path_view( pub fn import_descriptor<'a>( progress: (usize, usize), - change_network: bool, - network: bitcoin::Network, - network_valid: bool, imported_descriptor: &form::Value, wrong_network: bool, error: Option<&String>, ) -> Element<'a, Message> { - let row_network = Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push(text("Network:").bold()) - .push(Container::new( - pick_list(&NETWORKS[..], Some(Network::from(network)), |net| { - Message::Network(net.into()) - }) - .style(if network_valid { - theme::PickList::Simple - } else { - theme::PickList::Invalid - }) - .padding(10), - )) - .push_maybe(if network_valid { - None - } else { - Some(text("A data directory already exists for this network").style(color::RED)) - }); let col_descriptor = Column::new() .push(text("Descriptor:").bold()) .push( @@ -501,28 +393,13 @@ pub fn import_descriptor<'a>( progress, "Import the wallet", Column::new() - .push( - Column::new() - .spacing(20) - .push_maybe(if change_network { - Some(row_network) - } else { - None - }) - .push(col_descriptor) - .push_maybe(if change_network { - // only show message when importing a descriptor - Some(text( - "After creating the wallet, \ + .push(Column::new().spacing(20).push(col_descriptor).push(text( + "After creating the wallet, \ you will need to perform a rescan of \ the blockchain in order to see your \ coins and past transactions. This can \ be done in Settings > Bitcoin Core.", - )) - } else { - None - }), - ) + ))) .push( if imported_descriptor.value.is_empty() || !imported_descriptor.valid { button::primary(None, "Next").width(Length::Fixed(200.0)) @@ -684,43 +561,14 @@ pub fn hardware_wallet_xpubs<'a>( pub fn participate_xpub<'a>( progress: (usize, usize), - network: bitcoin::Network, - network_valid: bool, hws: Vec>, signer: Element<'a, Message>, shared: bool, ) -> Element<'a, Message> { - let row_network = Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push(text("Network:").bold()) - .push(Container::new( - pick_list(&NETWORKS[..], Some(Network::from(network)), |net| { - Message::Network(net.into()) - }) - .style(if network_valid { - theme::PickList::Simple - } else { - theme::PickList::Invalid - }) - .padding(10), - )) - .push_maybe(if network_valid { - None - } else { - Some(text("A data directory already exists for this network").style(color::RED)) - }); - layout( progress, "Share your public keys", Column::new() - .push( - Column::new() - .spacing(20) - .width(Length::Fill) - .push(row_network), - ) .push( Column::new() .push( @@ -739,7 +587,7 @@ pub fn participate_xpub<'a>( checkbox("I have shared my extended public key", shared) .on_toggle(Message::UserActionDone), ) - .push(if shared && network_valid { + .push(if shared { button::primary(None, "Next") .width(Length::Fixed(200.0)) .on_press(Message::Next) diff --git a/gui/src/launcher.rs b/gui/src/launcher.rs index f2dc329d..b47441a1 100644 --- a/gui/src/launcher.rs +++ b/gui/src/launcher.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use iced::{ alignment::Horizontal, - widget::{tooltip, Space}, + widget::{scrollable, tooltip}, Alignment, Command, Length, Subscription, }; @@ -28,33 +28,37 @@ fn wallet_name(network: &Network) -> String { } pub struct Launcher { - choices: Vec, + // true if installed + choices: Vec<(Network, bool)>, datadir_path: PathBuf, error: Option, delete_wallet_modal: Option, + collapsed: bool, } impl Launcher { pub fn new(datadir_path: PathBuf) -> Self { - let mut choices = Vec::new(); - for network in [ - Network::Bitcoin, - Network::Testnet, - Network::Signet, - Network::Regtest, - ] { - if datadir_path.join(network.to_string()).exists() { - choices.push(network) - } - } Self { + choices: [ + Network::Bitcoin, + Network::Testnet, + Network::Signet, + Network::Regtest, + ] + .iter() + .map(|net| (*net, datadir_path.join(net.to_string()).exists())) + .collect(), datadir_path, - choices, error: None, delete_wallet_modal: None, + collapsed: false, } } + fn is_fresh_install(&self) -> bool { + !self.choices.iter().any(|(_, installed)| *installed) + } + pub fn stop(&mut self) {} pub fn subscription(&self) -> Subscription { @@ -63,9 +67,15 @@ impl Launcher { pub fn update(&mut self, message: Message) -> Command { match message { - Message::View(ViewMessage::StartInstall) => { + Message::View(ViewMessage::ShowUninstalledNetworks) => { + self.collapsed = true; + Command::none() + } + Message::View(ViewMessage::StartInstall(net)) => { let datadir_path = self.datadir_path.clone(); - Command::perform(async move { datadir_path }, Message::Install) + Command::perform(async move { (datadir_path, net) }, |(d, n)| { + Message::Install(d, n) + }) } Message::View(ViewMessage::Check(network)) => Command::perform( check_network_datadir(self.datadir_path.clone(), network), @@ -88,11 +98,9 @@ impl Launcher { } Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Deleted)) => { if let Some(modal) = &self.delete_wallet_modal { - let choices = self.choices.clone(); - self.choices = choices - .into_iter() - .filter(|c| c != &modal.network) - .collect(); + if let Some(choice) = self.choices.iter_mut().find(|c| c.0 == modal.network) { + choice.1 = false; + } } Command::none() } @@ -126,7 +134,7 @@ impl Launcher { } pub fn view(&self) -> Element { - let content = Into::>::into( + let content = Into::>::into(scrollable( Column::new() .push( Container::new(image::liana_brand_grey().width(Length::Fixed(200.0))) @@ -136,17 +144,107 @@ impl Launcher { Container::new( Column::new() .spacing(30) - .push(text("Welcome back").size(50).bold()) + .push(if !self.is_fresh_install() { + text("Welcome back").size(50).bold() + } else { + text("Welcome").size(50).bold() + }) .push_maybe(self.error.as_ref().map(|e| card::simple(text(e)))) - .push( - self.choices - .iter() - .fold( - Column::new() - .push(text("Select network:").small().bold()) - .spacing(10), - |col, choice| { - col.push( + .push(if self.is_fresh_install() { + Column::new() + .spacing(10) + .push( + Button::new( + Row::new() + .spacing(20) + .align_items(Alignment::Center) + .push( + badge::Badge::new(icon::bitcoin_icon()) + .style(theme::Badge::Bitcoin), + ) + .push(text(format!( + "Create wallet on {}", + wallet_name(&Network::Bitcoin) + ))), + ) + .on_press(ViewMessage::StartInstall(Network::Bitcoin)) + .padding(10) + .width(Length::Fixed(400.0)) + .style(theme::Button::Border), + ) + .push(if !self.collapsed { + Column::new().push( + Button::new( + Row::new() + .spacing(20) + .align_items(Alignment::Center) + .push(badge::Badge::new(icon::plus_icon())) + .push(text("Create wallet on another network")), + ) + .on_press(ViewMessage::ShowUninstalledNetworks) + .padding(10) + .width(Length::Fixed(400.0)) + .style(theme::Button::TransparentBorder), + ) + } else { + self.choices + .iter() + .filter_map(|(net, installed)| { + if *installed || *net == Network::Bitcoin { + None + } else { + Some(net) + } + }) + .fold(Column::new().spacing(10), |col, choice| { + col.push( + Button::new( + Row::new() + .spacing(20) + .align_items(Alignment::Center) + .push( + badge::Badge::new( + icon::bitcoin_icon(), + ) + .style(theme::Badge::Standard), + ) + .push(text(format!( + "Create wallet on {}", + wallet_name(choice) + ))), + ) + .on_press(ViewMessage::StartInstall(*choice)) + .padding(10) + .width(Length::Fixed(400.0)) + .style(theme::Button::Border), + ) + }) + }) + } else { + Column::new() + .spacing(10) + .push( + self.choices + .iter() + .filter_map( + |(net, installed)| { + if *installed { + Some(net) + } else { + None + } + }, + ) + .fold( + Column::new() + .push( + text("Open an existing wallet:") + .small() + .bold(), + ) + .spacing(10), + |col, choice| { + col.push( Row::new() .spacing(10) .push( @@ -169,7 +267,7 @@ impl Launcher { ) .on_press(ViewMessage::Check(*choice)) .padding(10) - .width(Length::Fill) + .width(Length::Fixed(400.0)) .style(theme::Button::Border), ) .push(tooltip::Tooltip::new( @@ -187,35 +285,89 @@ impl Launcher { )) .align_items(Alignment::Center), ) - }, + }, + ), ) .push( - Button::new( - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(badge::Badge::new(icon::plus_icon())) - .push(text("Install Liana on another network")), - ) - .on_press(ViewMessage::StartInstall) - .padding(10) - .width(Length::Fill) - .style(theme::Button::TransparentBorder), - ), - ) - .max_width(500) - .align_items(Alignment::Center), + if !self.collapsed + && self.choices.iter().any(|(_, installed)| !installed) + { + Column::new().push( + Button::new( + Row::new() + .spacing(20) + .align_items(Alignment::Center) + .push(badge::Badge::new(icon::plus_icon())) + .push(text("Create a new wallet")), + ) + .on_press(ViewMessage::ShowUninstalledNetworks) + .padding(10) + .width(Length::Fixed(400.0)) + .style(theme::Button::TransparentBorder), + ) + } else if self.collapsed { + self.choices + .iter() + .filter_map(|(net, installed)| { + if *installed { + None + } else { + Some(net) + } + }) + .fold( + Column::new() + .push( + text("Create a new wallet:") + .small() + .bold(), + ) + .spacing(10), + |col, choice| { + col.push( + Button::new( + Row::new() + .spacing(20) + .align_items(Alignment::Center) + .push( + badge::Badge::new( + icon::bitcoin_icon(), + ) + .style(match choice { + Network::Bitcoin => { + theme::Badge::Bitcoin + } + _ => theme::Badge::Standard, + }), + ) + .push(text(wallet_name(choice))), + ) + .on_press(ViewMessage::StartInstall(*choice)) + .padding(10) + .width(Length::Fixed(400.0)) + .style(theme::Button::Border), + ) + }, + ) + } else { + Column::new() + }, + ) + }) + .align_items(if self.is_fresh_install() { + Alignment::Center + } else { + Alignment::Start + }) + .max_width(500), ) .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y(), - ) - .push(Space::with_height(Length::Fixed(100.0))), - ) + .center_x(), + ), + )) .map(Message::View); if let Some(modal) = &self.delete_wallet_modal { - Modal::new(content, modal.view()) + Modal::new(Container::new(content).height(Length::Fill), modal.view()) .on_blur(Some(Message::View(ViewMessage::DeleteWallet( DeleteWalletMessage::CloseModal, )))) @@ -229,14 +381,15 @@ impl Launcher { #[derive(Debug, Clone)] pub enum Message { View(ViewMessage), - Install(PathBuf), + Install(PathBuf, Network), Checked(Result), Run(PathBuf, app::config::Config, Network), } #[derive(Debug, Clone)] pub enum ViewMessage { - StartInstall, + StartInstall(Network), + ShowUninstalledNetworks, Check(Network), DeleteWallet(DeleteWalletMessage), } diff --git a/gui/src/main.rs b/gui/src/main.rs index 84a2b712..ae1ab6e5 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -198,13 +198,12 @@ impl Application for GUI { } } (State::Launcher(l), Message::Launch(msg)) => match *msg { - launcher::Message::Install(datadir_path) => { + launcher::Message::Install(datadir_path, network) => { self.logger.set_installer_mode( datadir_path.clone(), self.log_level.unwrap_or(LevelFilter::INFO), ); - let (install, command) = - Installer::new(datadir_path, bitcoin::Network::Bitcoin); + let (install, command) = Installer::new(datadir_path, network); self.state = State::Installer(Box::new(install)); command.map(|msg| Message::Install(Box::new(msg))) } @@ -364,13 +363,6 @@ impl Config { Err(ConfigError::NotFound) => Ok(Config::Install(datadir_path, network)), Err(e) => Err(format!("Failed to read configuration file: {}", e).into()), } - } else if !datadir_path.exists() - || (!datadir_path.join("bitcoin").exists() - && !datadir_path.join("testnet").exists() - && !datadir_path.join("signet").exists() - && !datadir_path.join("regtest").exists()) - { - Ok(Config::Install(datadir_path, bitcoin::Network::Bitcoin)) } else { Ok(Config::Launcher(datadir_path)) } From be343aec37d6a8d6249285cf64759e23673e85e8 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Wed, 29 May 2024 12:30:56 +0200 Subject: [PATCH 2/5] Add previous button to installer first page --- gui/src/installer/message.rs | 1 + gui/src/installer/mod.rs | 4 ++++ gui/src/installer/view.rs | 5 +++++ gui/src/main.rs | 4 ++++ 4 files changed, 14 insertions(+) diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index e60fe0c0..a0be0322 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -20,6 +20,7 @@ pub enum Message { Next, Skip, Previous, + BackToLauncher, Install, Close, Reload, diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index e507f049..fe375207 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -73,6 +73,10 @@ impl Installer { ) } + pub fn destination_path(&self) -> PathBuf { + self.context.data_dir.clone() + } + pub fn subscription(&self) -> Subscription { if self.current > 0 { self.steps diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 461092f6..6f5665c4 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -111,6 +111,11 @@ pub fn welcome<'a>() -> Element<'a, Message> { .padding(20), ), ) + .push( + button::secondary(Some(icon::previous_icon()), "Change network") + .width(Length::Fixed(200.0)) + .on_press(Message::BackToLauncher), + ) .push(Space::with_height(Length::Fixed(100.0))) .spacing(50) .align_items(Alignment::Center), diff --git a/gui/src/main.rs b/gui/src/main.rs index ae1ab6e5..087fd48a 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -246,6 +246,10 @@ impl Application for GUI { ); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) + } else if let installer::Message::BackToLauncher = *msg { + let launcher = Launcher::new(i.destination_path()); + self.state = State::Launcher(Box::new(launcher)); + Command::none() } else { i.update(*msg).map(|msg| Message::Install(Box::new(msg))) } From dee083c6d1b12eff07edef2f888bc1899c57bb72 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Wed, 29 May 2024 13:04:05 +0200 Subject: [PATCH 3/5] Add blue banner for testing network --- gui/src/app/mod.rs | 12 ++++++++++-- gui/src/installer/mod.rs | 18 ++++++++++++++---- gui/ui/src/color.rs | 6 ++++++ gui/ui/src/component/mod.rs | 29 ++++++++++++++++++++++++++++- gui/ui/src/theme.rs | 21 +++++++++++++++++++++ 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 6205db8e..acae0d6c 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -20,7 +20,10 @@ use tokio::runtime::Handle; use tracing::{error, info, warn}; pub use liana::{commands::CoinStatus, config::Config as DaemonConfig, miniscript::bitcoin}; -use liana_ui::widget::Element; +use liana_ui::{ + component::network_banner, + widget::{Column, Element}, +}; pub use config::Config; pub use message::Message; @@ -318,6 +321,11 @@ impl App { } pub fn view(&self) -> Element { - self.panels.current().view(&self.cache).map(Message::View) + let content = self.panels.current().view(&self.cache).map(Message::View); + if self.cache.network != bitcoin::Network::Bitcoin { + Column::with_children(vec![network_banner(self.cache.network).into(), content]).into() + } else { + content + } } } diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index fe375207..29fd1cc4 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -5,8 +5,11 @@ mod step; mod view; use iced::{clipboard, Command, Subscription}; -use liana::miniscript::bitcoin; -use liana_ui::widget::Element; +use liana::miniscript::bitcoin::{self, Network}; +use liana_ui::{ + component::network_banner, + widget::{Column, Element}, +}; use tracing::{error, info, warn}; use context::Context; @@ -252,10 +255,17 @@ impl Installer { } pub fn view(&self) -> Element { - self.steps + let content = self + .steps .get(self.current) .expect("There is always a step") - .view(&self.hws, self.progress()) + .view(&self.hws, self.progress()); + + if self.network != Network::Bitcoin { + Column::with_children(vec![network_banner(self.network).into(), content]).into() + } else { + content + } } } diff --git a/gui/ui/src/color.rs b/gui/ui/src/color.rs index dcd7726d..54b9166e 100644 --- a/gui/ui/src/color.rs +++ b/gui/ui/src/color.rs @@ -55,3 +55,9 @@ pub const RED: Color = Color::from_rgb( pub const ORANGE: Color = Color::from_rgb(0xFF as f32 / 255.0, 0xa7 as f32 / 255.0, 0x0 as f32 / 255.0); + +pub const BLUE: Color = Color::from_rgb( + 0x7D as f32 / 255.0, + 0xD3 as f32 / 255.0, + 0xFC as f32 / 255.0, +); diff --git a/gui/ui/src/component/mod.rs b/gui/ui/src/component/mod.rs index 19e24b37..06ec126f 100644 --- a/gui/ui/src/component/mod.rs +++ b/gui/ui/src/component/mod.rs @@ -12,14 +12,41 @@ pub mod text; pub mod toast; pub mod tooltip; +use bitcoin::Network; pub use tooltip::tooltip; use iced::Length; use crate::{theme, widget::*}; +use self::text::Text; + pub fn separation<'a, T: 'a>() -> Container<'a, T> { - Container::new(Column::new().push(Text::new(" "))) + Container::new(Column::new().push(text::text(" "))) .style(theme::Container::Border) .height(Length::Fixed(1.0)) } + +pub fn network_banner<'a, T: 'a>(network: Network) -> Container<'a, T> { + Container::new( + Row::new() + .push(super::icon::warning_icon()) + .push(text::text("THIS IS A ")) + .push( + text::text(match network { + Network::Signet => "SIGNET WALLET", + Network::Testnet => "TESTNET WALLET", + Network::Regtest => "REGTEST WALLET", + _ => unreachable!(), + }) + .bold(), + ) + .push(text::text(", COINS HAVE ")) + .push(text::text("NO VALUE").bold()) + .align_items(iced::Alignment::Center), + ) + .padding(5) + .width(Length::Fill) + .center_x() + .style(theme::Container::Banner) +} diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs index d6c8096f..32c35ce6 100644 --- a/gui/ui/src/theme.rs +++ b/gui/ui/src/theme.rs @@ -91,6 +91,7 @@ pub enum Container { Background, Foreground, Border, + Banner, Card(Card), Badge(Badge), Pill(Pill), @@ -142,6 +143,16 @@ impl container::StyleSheet for Theme { }, ..container::Appearance::default() }, + Container::Banner => container::Appearance { + background: Some(color::WHITE.into()), + border: iced::Border { + color: color::TRANSPARENT, + width: 0.0, + radius: 0.0.into(), + }, + text_color: color::LIGHT_BLACK.into(), + ..container::Appearance::default() + }, }, Theme::Dark => match style { Container::Transparent => container::Appearance { @@ -182,6 +193,16 @@ impl container::StyleSheet for Theme { }, ..container::Appearance::default() }, + Container::Banner => container::Appearance { + background: Some(color::BLUE.into()), + border: iced::Border { + color: color::TRANSPARENT, + width: 0.0, + radius: 0.0.into(), + }, + text_color: color::LIGHT_BLACK.into(), + ..container::Appearance::default() + }, }, } } From 977d8fa2dda00b45178b18ee3120082c0780b5ad Mon Sep 17 00:00:00 2001 From: edouardparis Date: Wed, 29 May 2024 17:34:16 +0200 Subject: [PATCH 4/5] Create datadir before install to store installer.log Launcher is not skipped anymore if no datadir exists the code in main.rs line 138 to create the datadir needs to be duplicated in this case. --- gui/src/main.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gui/src/main.rs b/gui/src/main.rs index 087fd48a..a3bc0c8f 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -199,6 +199,18 @@ impl Application for GUI { } (State::Launcher(l), Message::Launch(msg)) => match *msg { launcher::Message::Install(datadir_path, network) => { + if !datadir_path.exists() { + // datadir is created right before launching the installer + // so logs can go in /installer.log + if let Err(e) = create_datadir(&datadir_path) { + error!("Failed to create datadir: {}", e); + } else { + info!( + "Created a fresh data directory at {}", + &datadir_path.to_string_lossy() + ); + } + } self.logger.set_installer_mode( datadir_path.clone(), self.log_level.unwrap_or(LevelFilter::INFO), From c0e7b63809fd60f4fc6f5e9cd5223c138e25cfde Mon Sep 17 00:00:00 2001 From: edouardparis Date: Thu, 30 May 2024 10:44:38 +0200 Subject: [PATCH 5/5] fix launcher wording --- gui/src/launcher.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/gui/src/launcher.rs b/gui/src/launcher.rs index b47441a1..85c31b6d 100644 --- a/gui/src/launcher.rs +++ b/gui/src/launcher.rs @@ -237,11 +237,6 @@ impl Launcher { ) .fold( Column::new() - .push( - text("Open an existing wallet:") - .small() - .bold(), - ) .spacing(10), |col, choice| { col.push( @@ -263,7 +258,7 @@ impl Launcher { _ => theme::Badge::Standard, }), ) - .push(text(wallet_name(choice))), + .push(text(format!("Open wallet on {}", choice))), ) .on_press(ViewMessage::Check(*choice)) .padding(10) @@ -317,11 +312,6 @@ impl Launcher { }) .fold( Column::new() - .push( - text("Create a new wallet:") - .small() - .bold(), - ) .spacing(10), |col, choice| { col.push( @@ -340,7 +330,7 @@ impl Launcher { _ => theme::Badge::Standard, }), ) - .push(text(wallet_name(choice))), + .push(text(format!("Create wallet on {}", wallet_name(choice)))), ) .on_press(ViewMessage::StartInstall(*choice)) .padding(10)