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/message.rs b/gui/src/installer/message.rs index 4c298101..a0be0322 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; @@ -23,13 +20,13 @@ pub enum Message { Next, Skip, Previous, + BackToLauncher, Install, Close, Reload, 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..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; @@ -29,6 +32,7 @@ use step::{ }; pub struct Installer { + network: bitcoin::Network, current: usize, steps: Vec>, hws: HardwareWallets, @@ -61,6 +65,7 @@ impl Installer { ) -> (Installer, Command) { ( Installer { + network, current: 0, hws: HardwareWallets::new(destination_path.clone(), network), steps: vec![Welcome::default().into()], @@ -71,6 +76,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 @@ -135,7 +144,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 +158,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 +172,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(), @@ -246,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/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..6f5665c4 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() @@ -160,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), @@ -191,31 +147,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 +170,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 +186,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 +254,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 +304,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 +375,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 +398,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 +566,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 +592,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..85c31b6d 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,102 @@ 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() + .spacing(10), + |col, choice| { + col.push( Row::new() .spacing(10) .push( @@ -165,11 +258,11 @@ impl Launcher { _ => theme::Badge::Standard, }), ) - .push(text(wallet_name(choice))), + .push(text(format!("Open wallet on {}", choice))), ) .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 +280,84 @@ 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() + .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(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() + }, + ) + }) + .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 +371,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..a3bc0c8f 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -198,13 +198,24 @@ impl Application for GUI { } } (State::Launcher(l), Message::Launch(msg)) => match *msg { - launcher::Message::Install(datadir_path) => { + 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), ); - 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))) } @@ -247,6 +258,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))) } @@ -364,13 +379,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)) } 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() + }, }, } }