diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 3e5014f63..46c168613 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -308,12 +308,32 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bdk_chain" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c601c4dc7e6c3efa538a0afbb43b964cefab9a9b5e8f352fa0ca38145448a5e7" +dependencies = [ + "bitcoin", + "miniscript", +] + [[package]] name = "bdk_coin_select" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c084bf76f0f67546fc814ffa82044144be1bb4618183a15016c162f8b087ad4" +[[package]] +name = "bdk_electrum" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28906275aeb1f71dc32045670f06c8a26fb17cc62151a99f7425d258f4bda589" +dependencies = [ + "bdk_chain", + "electrum-client", +] + [[package]] name = "bech32" version = "0.10.0-beta" @@ -1143,11 +1163,11 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys 0.4.0", + "dirs-sys 0.4.1", ] [[package]] @@ -1163,13 +1183,14 @@ dependencies = [ [[package]] name = "dirs-sys" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1267,6 +1288,23 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "electrum-client" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89008f106be6f303695522f2f4c1f28b40c3e8367ed8b3bb227f1f882cb52cc2" +dependencies = [ + "bitcoin", + "byteorder", + "libc", + "log", + "rustls", + "serde", + "serde_json", + "webpki-roots", + "winapi", +] + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -2589,12 +2627,13 @@ dependencies = [ [[package]] name = "liana" version = "6.0.0" -source = "git+https://github.com/wizardsardine/liana?branch=master#585bb5b763127f4e0686ce8201fb846fa84137b9" +source = "git+https://github.com/wizardsardine/liana?branch=master#6f7334738360a554d17875b364ccdf2120250315" dependencies = [ "backtrace", "bdk_coin_select", + "bdk_electrum", "bip39", - "dirs 5.0.0", + "dirs 5.0.1", "fern", "getrandom", "jsonrpc 0.17.0", @@ -2936,9 +2975,9 @@ dependencies = [ [[package]] name = "minreq" -version = "2.8.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de406eeb24aba36ed3829532fa01649129677186b44a49debec0ec574ca7da7" +checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0" dependencies = [ "log", "serde", @@ -3260,6 +3299,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orbclient" version = "0.3.47" @@ -3818,9 +3863,9 @@ checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" [[package]] name = "rdrand" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e233b642160555c1aa1ff7a78443c6139342f411b6fa6602af2ebbfee9e166bb" +checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" dependencies = [ "rand_core", ] diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index c222211a2..434153843 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -35,8 +35,8 @@ use state::{ use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, - bitcoind::Bitcoind, daemon::{embedded::EmbeddedDaemon, Daemon, DaemonBackend}, + node::bitcoind::Bitcoind, }; use self::state::SettingsState; diff --git a/gui/src/app/state/settings/bitcoind.rs b/gui/src/app/state/settings/bitcoind.rs index 430b1fe5f..35d36ebc0 100644 --- a/gui/src/app/state/settings/bitcoind.rs +++ b/gui/src/app/state/settings/bitcoind.rs @@ -1,5 +1,5 @@ use std::convert::{From, TryInto}; -use std::net::SocketAddr; +use std::net::{SocketAddr, SocketAddrV4}; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; @@ -9,7 +9,9 @@ use iced::Command; use tracing::info; use liana::{ - config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config}, + config::{ + BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig, + }, miniscript::bitcoin::Network, }; @@ -17,8 +19,11 @@ use liana_ui::{component::form, widget::Element}; use crate::{ app::{cache::Cache, error::Error, message::Message, state::settings::State, view}, - bitcoind::{RpcAuthType, RpcAuthValues}, daemon::Daemon, + node::{ + bitcoind::{RpcAuthType, RpcAuthValues}, + NodeType, + }, }; #[derive(Debug)] @@ -27,6 +32,7 @@ pub struct BitcoindSettingsState { config_updated: bool, node_settings: Option, + electrum_settings: Option, rescan_settings: RescanSetting, } @@ -37,17 +43,52 @@ impl BitcoindSettingsState { daemon_is_external: bool, bitcoind_is_internal: bool, ) -> Self { + let mut configured_node_type = None; + let (bitcoind_config, electrum_config) = + match config.clone().and_then(|c| c.bitcoin_backend) { + Some(BitcoinBackend::Bitcoind(bitcoind_config)) => { + configured_node_type = Some(NodeType::Bitcoind); + let dummy_electrum = ElectrumConfig { + addr: String::default(), + }; + (Some(bitcoind_config), Some(dummy_electrum)) + } + Some(BitcoinBackend::Electrum(electrum_config)) => { + configured_node_type = Some(NodeType::Electrum); + // The dummy values will be ignored. + let dummy_bitcoind = BitcoindConfig { + addr: SocketAddr::V4(SocketAddrV4::from_str("127.0.0.1:10000").unwrap()), + rpc_auth: BitcoindRpcAuth::CookieFile(PathBuf::from_str("").unwrap()), + }; + (Some(dummy_bitcoind), Some(electrum_config)) + } + _ => (None, None), + }; BitcoindSettingsState { warning: None, config_updated: false, - node_settings: config.map(|config| { + node_settings: bitcoind_config.map(|bitcoind_config| { BitcoindSettings::new( - config.bitcoin_config.clone(), - config.bitcoind_config.unwrap(), + configured_node_type, + config + .clone() + .expect("config must exist if bitcoind_config exists") + .bitcoin_config, + bitcoind_config, daemon_is_external, bitcoind_is_internal, ) }), + electrum_settings: electrum_config.map(|electrum_config| { + ElectrumSettings::new( + configured_node_type, + config + .expect("config must exist if electrum_config exists") + .bitcoin_config, + electrum_config, + daemon_is_external, + ) + }), rescan_settings: RescanSetting::new(cache.rescan_progress), } } @@ -73,6 +114,14 @@ impl State for BitcoindSettingsState { )) }); } + if let Some(settings) = &mut self.electrum_settings { + settings.edited(true); + return Command::perform(async {}, |_| { + Message::View(view::Message::Settings( + view::SettingsMessage::EditBitcoindSettings, + )) + }); + } } Err(e) => { self.config_updated = false; @@ -101,6 +150,13 @@ impl State for BitcoindSettingsState { return settings.update(daemon, cache, msg); } } + Message::View(view::Message::Settings(view::SettingsMessage::ElectrumSettings( + msg, + ))) => { + if let Some(settings) = &mut self.electrum_settings { + return settings.update(daemon, cache, msg); + } + } Message::View(view::Message::Settings(view::SettingsMessage::RescanSettings(msg))) => { return self.rescan_settings.update(daemon, cache, msg); } @@ -110,9 +166,17 @@ impl State for BitcoindSettingsState { } fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { - let can_edit_bitcoind_settings = !self.rescan_settings.processing; - let can_do_rescan = !self.rescan_settings.processing - && self.node_settings.as_ref().map(|settings| settings.edit) != Some(true); + let can_edit_bitcoind_settings = + self.node_settings.is_some() && !self.rescan_settings.processing; + let can_edit_electrum_settings = + self.electrum_settings.is_some() && !self.rescan_settings.processing; + let settings_edit = self.node_settings.as_ref().map(|settings| settings.edit) == Some(true) + || self + .electrum_settings + .as_ref() + .map(|settings| settings.edit) + == Some(true); + let can_do_rescan = !self.rescan_settings.processing && !settings_edit; view::settings::bitcoind_settings( cache, self.warning.as_ref(), @@ -123,6 +187,13 @@ impl State for BitcoindSettingsState { .map(move |msg| { view::Message::Settings(view::SettingsMessage::BitcoindSettings(msg)) }), + self.electrum_settings + .as_ref() + .expect("If we have bitcoind, we must also have electrum") + .view(cache, can_edit_electrum_settings) + .map(move |msg| { + view::Message::Settings(view::SettingsMessage::ElectrumSettings(msg)) + }), self.rescan_settings .view(cache, can_do_rescan) .map(move |msg| { @@ -149,6 +220,7 @@ impl From for Box { #[derive(Debug)] pub struct BitcoindSettings { + configured_node_type: Option, bitcoind_config: BitcoindConfig, bitcoin_config: BitcoinConfig, edit: bool, @@ -162,6 +234,7 @@ pub struct BitcoindSettings { impl BitcoindSettings { fn new( + configured_node_type: Option, bitcoin_config: BitcoinConfig, bitcoind_config: BitcoindConfig, daemon_is_external: bool, @@ -194,8 +267,13 @@ impl BitcoindSettings { RpcAuthType::UserPass, ), }; - let addr = bitcoind_config.addr.to_string(); + let addr = if configured_node_type == Some(NodeType::Bitcoind) { + bitcoind_config.addr.to_string() + } else { + String::default() + }; BitcoindSettings { + configured_node_type, daemon_is_external, bitcoind_is_internal, bitcoind_config, @@ -274,10 +352,11 @@ impl BitcoindSettings { if let (true, Some(rpc_auth)) = (self.addr.valid, rpc_auth) { let mut daemon_config = daemon.config().cloned().unwrap(); - daemon_config.bitcoind_config = Some(liana::config::BitcoindConfig { - rpc_auth, - addr: new_addr.unwrap(), - }); + daemon_config.bitcoin_backend = + Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig { + rpc_auth, + addr: new_addr.unwrap(), + })); self.processing = true; return Command::perform(async move { daemon_config }, |cfg| { Message::LoadDaemonConfig(Box::new(cfg)) @@ -289,8 +368,10 @@ impl BitcoindSettings { } fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> { + let is_configured_node_type = self.configured_node_type == Some(NodeType::Bitcoind); if self.edit { view::settings::bitcoind_edit( + is_configured_node_type, self.bitcoin_config.network, cache.blockheight, &self.addr, @@ -300,6 +381,7 @@ impl BitcoindSettings { ) } else { view::settings::bitcoind( + is_configured_node_type, self.bitcoin_config.network, &self.bitcoind_config, cache.blockheight, @@ -310,6 +392,111 @@ impl BitcoindSettings { } } +#[derive(Debug)] +pub struct ElectrumSettings { + configured_node_type: Option, + electrum_config: ElectrumConfig, + bitcoin_config: BitcoinConfig, + edit: bool, + processing: bool, + addr: form::Value, + daemon_is_external: bool, +} + +impl ElectrumSettings { + fn new( + configured_node_type: Option, + bitcoin_config: BitcoinConfig, + electrum_config: ElectrumConfig, + daemon_is_external: bool, + ) -> ElectrumSettings { + let addr = electrum_config.addr.to_string(); + ElectrumSettings { + configured_node_type, + daemon_is_external, + electrum_config, + bitcoin_config, + edit: false, + processing: false, + addr: form::Value { + valid: true, + value: addr, + }, + } + } +} + +impl ElectrumSettings { + fn edited(&mut self, success: bool) { + self.processing = false; + if success { + self.edit = false; + } + } + + fn update( + &mut self, + daemon: Arc, + _cache: &Cache, + message: view::SettingsEditMessage, + ) -> Command { + match message { + view::SettingsEditMessage::Select => { + if !self.processing { + self.edit = true; + } + } + view::SettingsEditMessage::Cancel => { + if !self.processing { + self.edit = false; + } + } + view::SettingsEditMessage::FieldEdited(field, value) => { + if !self.processing && field == "address" { + self.addr.value = value + } + } + view::SettingsEditMessage::Confirm => { + if self.addr.valid { + let mut daemon_config = daemon.config().cloned().unwrap(); + daemon_config.bitcoin_backend = + Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig { + addr: self.addr.value.clone(), + })); + self.processing = true; + return Command::perform(async move { daemon_config }, |cfg| { + Message::LoadDaemonConfig(Box::new(cfg)) + }); + } + } + _ => {} + }; + Command::none() + } + + fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> { + let is_configured_node_type = self.configured_node_type == Some(NodeType::Electrum); + if self.edit { + view::settings::electrum_edit( + is_configured_node_type, + self.bitcoin_config.network, + cache.blockheight, + &self.addr, + self.processing, + ) + } else { + view::settings::electrum( + is_configured_node_type, + self.bitcoin_config.network, + &self.electrum_config, + cache.blockheight, + Some(cache.blockheight != 0), + can_edit && !self.daemon_is_external, + ) + } + } +} + #[derive(Debug, Default)] pub struct RescanSetting { processing: bool, diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index 001e8952a..c726b5fc5 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -1,4 +1,4 @@ -use crate::{app::menu::Menu, bitcoind::RpcAuthType}; +use crate::{app::menu::Menu, node::bitcoind::RpcAuthType}; use liana::miniscript::bitcoin::bip32::Fingerprint; #[derive(Debug, Clone)] @@ -67,6 +67,7 @@ pub enum SpendTxMessage { pub enum SettingsMessage { EditBitcoindSettings, BitcoindSettings(SettingsEditMessage), + ElectrumSettings(SettingsEditMessage), RescanSettings(SettingsEditMessage), EditRemoteBackendSettings, RemoteBackendSettings(RemoteBackendSettingsMessage), diff --git a/gui/src/app/view/settings.rs b/gui/src/app/view/settings.rs index a81bbd295..95782c51a 100644 --- a/gui/src/app/view/settings.rs +++ b/gui/src/app/view/settings.rs @@ -29,8 +29,8 @@ use crate::{ menu::Menu, view::{hw, warning::warn}, }, - bitcoind::{RpcAuthType, RpcAuthValues}, hw::HardwareWallet, + node::bitcoind::{RpcAuthType, RpcAuthValues}, }; pub fn list(cache: &Cache, is_remote_backend: bool) -> Element { @@ -51,7 +51,7 @@ pub fn list(cache: &Cache, is_remote_backend: bool) -> Element { Button::new( Row::new() .push(badge::Badge::new(icon::bitcoin_icon())) - .push(text("Bitcoin Core").bold()) + .push(text("Node").bold()) .padding(10) .spacing(20) .align_items(Alignment::Center) @@ -161,7 +161,7 @@ pub fn bitcoind_settings<'a>( ) .push(icon::chevron_right().size(30)) .push( - Button::new(text("Bitcoin Core").size(30).bold()) + Button::new(text("Node").size(30).bold()) .style(theme::Button::Transparent) .on_press(Message::Settings(SettingsMessage::EditBitcoindSettings)), ), @@ -298,6 +298,7 @@ pub fn remote_backend_section<'a>( } pub fn bitcoind_edit<'a>( + is_configured_node_type: bool, network: Network, blockheight: i32, addr: &form::Value, @@ -306,7 +307,7 @@ pub fn bitcoind_edit<'a>( processing: bool, ) -> Element<'a, SettingsEditMessage> { let mut col = Column::new().spacing(20); - if blockheight != 0 { + if is_configured_node_type && blockheight != 0 { col = col .push( Row::new() @@ -444,6 +445,7 @@ pub fn bitcoind_edit<'a>( } pub fn bitcoind<'a>( + is_configured_node_type: bool, network: Network, config: &liana::config::BitcoindConfig, blockheight: i32, @@ -451,7 +453,7 @@ pub fn bitcoind<'a>( can_edit: bool, ) -> Element<'a, SettingsEditMessage> { let mut col = Column::new().spacing(20); - if blockheight != 0 { + if is_configured_node_type && blockheight != 0 { col = col .push( Row::new() @@ -482,16 +484,18 @@ pub fn bitcoind<'a>( } let mut rows = vec![]; - match &config.rpc_auth { - BitcoindRpcAuth::CookieFile(path) => { - rows.push(("Cookie file path:", path.to_str().unwrap().to_string())); - } - BitcoindRpcAuth::UserPass(user, password) => { - rows.push(("User:", user.clone())); - rows.push(("Password:", password.clone())); + if is_configured_node_type { + match &config.rpc_auth { + BitcoindRpcAuth::CookieFile(path) => { + rows.push(("Cookie file path:", path.to_str().unwrap().to_string())); + } + BitcoindRpcAuth::UserPass(user, password) => { + rows.push(("User:", user.clone())); + rows.push(("Password:", password.clone())); + } } + rows.push(("Socket address:", config.addr.to_string())); } - rows.push(("Socket address:", config.addr.to_string())); let mut col_fields = Column::new(); for (k, v) in rows { @@ -510,7 +514,188 @@ pub fn bitcoind<'a>( Row::new() .push(badge::Badge::new(icon::bitcoin_icon())) .push(text("Bitcoin Core").bold()) - .push(is_running_label(is_running)) + .push_maybe(if is_configured_node_type { + Some(is_running_label(is_running)) + } else { + None + }) + .spacing(20) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .push(if can_edit { + Button::new(icon::pencil_icon()) + .style(theme::Button::TransparentBorder) + .on_press(SettingsEditMessage::Select) + } else { + Button::new(icon::pencil_icon()).style(theme::Button::TransparentBorder) + }) + .align_items(Alignment::Center), + ) + .push(separation().width(Length::Fill)) + .push(col.push(col_fields)) + .spacing(20), + )) + .width(Length::Fill) + .into() +} + +pub fn electrum_edit<'a>( + is_configured_node_type: bool, + network: Network, + blockheight: i32, + addr: &form::Value, + processing: bool, +) -> Element<'a, SettingsEditMessage> { + let mut col = Column::new().spacing(20); + if is_configured_node_type && blockheight != 0 { + col = col + .push( + Row::new() + .push( + Row::new() + .push(badge::Badge::new(icon::network_icon())) + .push( + Column::new() + .push(text("Network:")) + .push(text(network.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ) + .push( + Row::new() + .push(badge::Badge::new(icon::block_icon())) + .push( + Column::new() + .push(text("Block Height:")) + .push(text(blockheight.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ), + ) + .push(separation().width(Length::Fill)); + } + + col = col.push( + Column::new() + .push(text("Address:").bold().small()) + .push( + form::Form::new_trimmed("127:0.0.1:50001", addr, |value| { + SettingsEditMessage::FieldEdited("address", value) + }) + .warning("Please enter a valid address") + .size(P1_SIZE) + .padding(5), + ) + .spacing(5), + ); + + let mut cancel_button = button::transparent(None, " Cancel ").padding(5); + let mut confirm_button = button::primary(None, " Save ").padding(5); + if !processing { + cancel_button = cancel_button.on_press(SettingsEditMessage::Cancel); + confirm_button = confirm_button.on_press(SettingsEditMessage::Confirm); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push(badge::Badge::new(icon::bitcoin_icon())) + .push(text("Electrum").bold()) + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .push(separation().width(Length::Fill)) + .push(col) + .push( + Container::new( + Row::new() + .push(cancel_button) + .push(confirm_button) + .spacing(10) + .align_items(Alignment::Center), + ) + .width(Length::Fill) + .align_x(alignment::Horizontal::Right), + ) + .spacing(20), + )) + .width(Length::Fill) + .into() +} + +pub fn electrum<'a>( + is_configured_node_type: bool, + network: Network, + config: &liana::config::ElectrumConfig, + blockheight: i32, + is_running: Option, + can_edit: bool, +) -> Element<'a, SettingsEditMessage> { + let mut col = Column::new().spacing(20); + if is_configured_node_type && blockheight != 0 { + col = col + .push( + Row::new() + .push( + Row::new() + .push(badge::Badge::new(icon::network_icon())) + .push( + Column::new() + .push(text("Network:")) + .push(text(network.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ) + .push( + Row::new() + .push(badge::Badge::new(icon::block_icon())) + .push( + Column::new() + .push(text("Block Height:")) + .push(text(blockheight.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ), + ) + .push(separation().width(Length::Fill)); + } + + let rows = if is_configured_node_type { + vec![("Address:", config.addr.to_string())] + } else { + vec![] + }; + + let mut col_fields = Column::new(); + for (k, v) in rows { + col_fields = col_fields.push( + Row::new() + .push(Container::new(text(k).bold().small()).width(Length::Fill)) + .push(text(v).small()), + ); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push( + Row::new() + .push(badge::Badge::new(icon::bitcoin_icon())) + .push(text("Electrum").bold()) + .push_maybe(if is_configured_node_type { + Some(is_running_label(is_running)) + } else { + None + }) .spacing(20) .align_items(Alignment::Center) .width(Length::Fill), diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs index 9ee78d80e..06f6203ca 100644 --- a/gui/src/daemon/embedded.rs +++ b/gui/src/daemon/embedded.rs @@ -27,9 +27,9 @@ impl EmbeddedDaemon { pub async fn command(&self, method: F) -> Result where - F: FnOnce(&DaemonControl) -> Result, + F: FnOnce(&mut DaemonControl) -> Result, { - match self.handle.lock().await.as_ref() { + match self.handle.lock().await.as_mut() { Some(DaemonHandle::Controller { control, .. }) => method(control), None => Err(DaemonError::DaemonStopped), } diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs index 38215b646..3f46cbe7a 100644 --- a/gui/src/installer/context.rs +++ b/gui/src/installer/context.rs @@ -4,13 +4,13 @@ use std::time::Duration; use crate::{ app::settings::KeySetting, - bitcoind::{Bitcoind, InternalBitcoindConfig}, lianalite::client::backend::{BackendClient, BackendWalletClient}, + node::bitcoind::{Bitcoind, InternalBitcoindConfig}, signer::Signer, }; use async_hwi::DeviceKind; use liana::{ - config::{BitcoinConfig, BitcoindConfig}, + config::{BitcoinBackend, BitcoinConfig}, descriptors::LianaDescriptor, miniscript::bitcoin, }; @@ -48,7 +48,7 @@ impl RemoteBackend { #[derive(Clone)] pub struct Context { pub bitcoin_config: BitcoinConfig, - pub bitcoind_config: Option, + pub bitcoin_backend: Option, pub descriptor: Option, pub keys: Vec, pub hws: Vec<(DeviceKind, bitcoin::bip32::Fingerprint, Option<[u8; 32]>)>, @@ -77,7 +77,7 @@ impl Context { }, hws: Vec::new(), keys: Vec::new(), - bitcoind_config: None, + bitcoin_backend: None, descriptor: None, data_dir, network, diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index dfeb516bd..690f58dcd 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -6,10 +6,13 @@ use std::path::PathBuf; use super::{context, Error}; use crate::{ - bitcoind::{Bitcoind, ConfigField, RpcAuthType}, download::Progress, hw::HardwareWalletMessage, lianalite::client::{auth::AuthClient, backend::api}, + node::{ + bitcoind::{Bitcoind, ConfigField, RpcAuthType}, + electrum, NodeType, + }, }; use async_hwi::{DeviceKind, Version}; @@ -33,7 +36,7 @@ pub enum Message { ImportRemoteWallet(ImportRemoteWallet), SelectBitcoindType(SelectBitcoindTypeMsg), InternalBitcoind(InternalBitcoindMsg), - DefineBitcoind(DefineBitcoind), + DefineNode(DefineNode), DefineDescriptor(DefineDescriptor), ImportXpub(Fingerprint, Result), HardwareWallets(HardwareWalletMessage), @@ -72,8 +75,20 @@ pub enum ImportRemoteWallet { pub enum DefineBitcoind { ConfigFieldEdited(ConfigField, String), RpcAuthTypeSelected(RpcAuthType), - PingBitcoindResult(Result<(), Error>), - PingBitcoind, +} + +#[derive(Debug, Clone)] +pub enum DefineElectrum { + ConfigFieldEdited(electrum::ConfigField, String), +} + +#[derive(Debug, Clone)] +pub enum DefineNode { + NodeTypeSelected(NodeType), + DefineBitcoind(DefineBitcoind), + DefineElectrum(DefineElectrum), + PingResult((NodeType, Result<(), Error>)), + Ping, } #[derive(Debug, Clone)] diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index f9cdb3393..6464bae7b 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -39,7 +39,7 @@ use crate::{ pub use message::Message; use step::{ - BackupDescriptor, BackupMnemonic, ChooseBackend, DefineBitcoind, DefineDescriptor, Final, + BackupDescriptor, BackupMnemonic, ChooseBackend, DefineDescriptor, DefineNode, Final, ImportDescriptor, ImportRemoteWallet, InternalBitcoindStep, RecoverMnemonic, RegisterDescriptor, RemoteBackendLogin, SelectBitcoindTypeStep, ShareXpubs, Step, }; @@ -126,7 +126,7 @@ impl Installer { RemoteBackendLogin::new(network).into(), SelectBitcoindTypeStep::new().into(), InternalBitcoindStep::new(&context.data_dir).into(), - DefineBitcoind::new().into(), + DefineNode::default().into(), Final::new().into(), ], UserFlow::ShareXpubs => vec![ShareXpubs::new(network, signer.clone()).into()], @@ -139,7 +139,7 @@ impl Installer { RegisterDescriptor::new_import_wallet().into(), SelectBitcoindTypeStep::new().into(), InternalBitcoindStep::new(&context.data_dir).into(), - DefineBitcoind::new().into(), + DefineNode::default().into(), Final::new().into(), ], }, @@ -689,7 +689,7 @@ pub fn extract_daemon_config(ctx: &Context) -> Config { .expect("Context must have a descriptor at this point"), data_dir: Some(ctx.data_dir.clone()), bitcoin_config: ctx.bitcoin_config.clone(), - bitcoind_config: ctx.bitcoind_config.clone(), + bitcoin_backend: ctx.bitcoin_backend.clone(), } } @@ -701,6 +701,7 @@ pub enum Error { Backend(Arc), Settings(SettingsError), Bitcoind(String), + Electrum(String), CannotCreateDatadir(String), CannotCreateFile(String), CannotWriteToFile(String), @@ -752,6 +753,7 @@ impl std::fmt::Display for Error { Self::Backend(e) => write!(f, "Remote backend error: {}", e), Self::Settings(e) => write!(f, "Settings file error: {}", e), Self::Bitcoind(e) => write!(f, "Failed to ping bitcoind: {}", e), + Self::Electrum(e) => write!(f, "Failed to ping Electrum: {}", e), Self::CannotCreateDatadir(e) => write!(f, "Failed to create datadir: {}", e), Self::CannotGetAvailablePort(e) => write!(f, "Failed to get available port: {}", e), Self::CannotWriteToFile(e) => write!(f, "Failed to write to file: {}", e), diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index ac32ed0b1..c45e56c56 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -1,11 +1,12 @@ mod backend; -mod bitcoind; mod descriptor; mod mnemonic; +mod node; mod share_xpubs; -pub use bitcoind::{ - DefineBitcoind, DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep, +pub use node::{ + bitcoind::{DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep}, + DefineNode, }; pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor}; @@ -21,9 +22,9 @@ use iced::{Command, Subscription}; use liana_ui::widget::*; use crate::{ - bitcoind::Bitcoind, hw::HardwareWallets, installer::{context::Context, message::Message, view}, + node::bitcoind::Bitcoind, }; pub trait Step { diff --git a/gui/src/installer/step/bitcoind.rs b/gui/src/installer/step/node/bitcoind.rs similarity index 89% rename from gui/src/installer/step/bitcoind.rs rename to gui/src/installer/step/node/bitcoind.rs index e48637b99..7255cbcd0 100644 --- a/gui/src/installer/step/bitcoind.rs +++ b/gui/src/installer/step/node/bitcoind.rs @@ -9,7 +9,7 @@ use bitcoin_hashes::{sha256, Hash}; use flate2::read::GzDecoder; use iced::{Command, Subscription}; use liana::{ - config::{BitcoindConfig, BitcoindRpcAuth}, + config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth}, miniscript::bitcoin::Network, }; #[cfg(any(target_os = "macos", target_os = "linux"))] @@ -21,12 +21,6 @@ use jsonrpc::{client::Client, simple_http::SimpleHttpTransport}; use liana_ui::{component::form, widget::*}; use crate::{ - bitcoind::{ - self, bitcoind_network_dir, internal_bitcoind_datadir, internal_bitcoind_directory, - Bitcoind, ConfigField, InternalBitcoindConfig, InternalBitcoindConfigError, - InternalBitcoindNetworkConfig, RpcAuth, RpcAuthType, RpcAuthValues, - StartInternalBitcoindError, VERSION, - }, download, hw::HardwareWallets, installer::{ @@ -35,6 +29,12 @@ use crate::{ step::Step, view, Error, }, + node::bitcoind::{ + self, bitcoind_network_dir, internal_bitcoind_datadir, internal_bitcoind_directory, + Bitcoind, ConfigField, InternalBitcoindConfig, InternalBitcoindConfigError, + InternalBitcoindNetworkConfig, RpcAuth, RpcAuthType, RpcAuthValues, + StartInternalBitcoindError, VERSION, + }, }; // The approach for tracking download progress is taken from @@ -320,7 +320,7 @@ impl Step for SelectBitcoindTypeStep { fn apply(&mut self, ctx: &mut Context) -> bool { if !self.use_external { if ctx.internal_bitcoind_config.is_none() { - ctx.bitcoind_config = None; // Ensures internal bitcoind can be restarted in case user has switched selection. + ctx.bitcoin_backend = None; // Ensures internal bitcoind can be restarted in case user has switched selection. } } else { ctx.internal_bitcoind_config = None; @@ -339,11 +339,11 @@ impl Step for SelectBitcoindTypeStep { } } +#[derive(Clone)] pub struct DefineBitcoind { rpc_auth_vals: RpcAuthValues, selected_auth_type: RpcAuthType, address: form::Value, - is_running: Option>, // Internal cache to detect network change. network: Option, @@ -355,47 +355,46 @@ impl DefineBitcoind { rpc_auth_vals: RpcAuthValues::default(), selected_auth_type: RpcAuthType::CookieFile, address: form::Value::default(), - is_running: None, network: None, } } - pub fn ping(&self) -> Command { - let address = self.address.value.to_owned(); - let selected_auth_type = self.selected_auth_type; + pub fn ping(&self) -> Result<(), Error> { let rpc_auth_vals = self.rpc_auth_vals.clone(); - Command::perform( - async move { - let builder = match selected_auth_type { - RpcAuthType::CookieFile => { - let cookie_path = rpc_auth_vals.cookie_path.value; - let cookie = std::fs::read_to_string(cookie_path).map_err(|e| { - Error::Bitcoind(format!("Failed to read cookie file: {}", e)) - })?; - SimpleHttpTransport::builder().cookie_auth(cookie) - } - RpcAuthType::UserPass => { - let user = rpc_auth_vals.user.value; - let password = rpc_auth_vals.password.value; - SimpleHttpTransport::builder().auth(user, Some(password)) - } - }; - let client = Client::with_transport( - builder - .url(&address)? - .timeout(std::time::Duration::from_secs(3)) - .build(), - ); - client.send_request(client.build_request("echo", &[]))?; - Ok(()) - }, - |res| Message::DefineBitcoind(message::DefineBitcoind::PingBitcoindResult(res)), - ) + let builder = match self.selected_auth_type { + RpcAuthType::CookieFile => { + let cookie_path = rpc_auth_vals.cookie_path.value; + let cookie = std::fs::read_to_string(cookie_path) + .map_err(|e| Error::Bitcoind(format!("Failed to read cookie file: {}", e)))?; + SimpleHttpTransport::builder().cookie_auth(cookie) + } + RpcAuthType::UserPass => { + let user = rpc_auth_vals.user.value; + let password = rpc_auth_vals.password.value; + SimpleHttpTransport::builder().auth(user, Some(password)) + } + }; + let client = Client::with_transport( + builder + .url(&self.address.value.to_owned())? + .timeout(std::time::Duration::from_secs(3)) + .build(), + ); + client.send_request(client.build_request("echo", &[]))?; + Ok(()) + } + + pub fn can_try_ping(&self) -> bool { + if let RpcAuthType::UserPass = self.selected_auth_type { + self.address.valid + && !self.rpc_auth_vals.password.value.is_empty() + && !self.rpc_auth_vals.user.value.is_empty() + } else { + self.address.valid && !self.rpc_auth_vals.cookie_path.value.is_empty() + } } -} -impl Step for DefineBitcoind { - fn load_context(&mut self, ctx: &Context) { + pub fn load_context(&mut self, ctx: &Context) { if self.rpc_auth_vals.cookie_path.value.is_empty() // if network changed then the values must be reset to default. || self.network != Some(ctx.bitcoin_config.network) @@ -412,17 +411,12 @@ impl Step for DefineBitcoind { self.network = Some(ctx.bitcoin_config.network); } - fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { - if let Message::DefineBitcoind(msg) = message { + + pub fn update(&mut self, message: message::DefineNode) -> Command { + if let message::DefineNode::DefineBitcoind(msg) = message { match msg { - message::DefineBitcoind::PingBitcoind => { - self.is_running = None; - return self.ping(); - } - message::DefineBitcoind::PingBitcoindResult(res) => self.is_running = Some(res), message::DefineBitcoind::ConfigFieldEdited(field, value) => match field { ConfigField::Address => { - self.is_running = None; self.address.value.clone_from(&value); self.address.valid = false; if let Some((ip, port)) = value.rsplit_once(':') { @@ -434,23 +428,19 @@ impl Step for DefineBitcoind { } } ConfigField::CookieFilePath => { - self.is_running = None; self.rpc_auth_vals.cookie_path.value = value; self.rpc_auth_vals.cookie_path.valid = true; } ConfigField::User => { - self.is_running = None; self.rpc_auth_vals.user.value = value; self.rpc_auth_vals.user.valid = true; } ConfigField::Password => { - self.is_running = None; self.rpc_auth_vals.password.value = value; self.rpc_auth_vals.password.valid = true; } }, message::DefineBitcoind::RpcAuthTypeSelected(auth_type) => { - self.is_running = None; self.selected_auth_type = auth_type; } }; @@ -458,7 +448,7 @@ impl Step for DefineBitcoind { Command::none() } - fn apply(&mut self, ctx: &mut Context) -> bool { + pub fn apply(&mut self, ctx: &mut Context) -> bool { let addr = std::net::SocketAddr::from_str(&self.address.value); let rpc_auth = match self.selected_auth_type { RpcAuthType::CookieFile => { @@ -481,33 +471,18 @@ impl Step for DefineBitcoind { false } (Some(rpc_auth), Ok(addr)) => { - ctx.bitcoind_config = Some(BitcoindConfig { rpc_auth, addr }); + ctx.bitcoin_backend = + Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig { + rpc_auth, + addr, + })); true } } } - fn view( - &self, - _hws: &HardwareWallets, - progress: (usize, usize), - _email: Option<&str>, - ) -> Element { - view::define_bitcoin( - progress, - &self.address, - &self.rpc_auth_vals, - &self.selected_auth_type, - self.is_running.as_ref(), - ) - } - - fn load(&self) -> Command { - self.ping() - } - - fn skip(&self, ctx: &Context) -> bool { - !ctx.bitcoind_is_external || ctx.remote_backend.is_some() + pub fn view(&self) -> Element { + view::define_bitcoind(&self.address, &self.rpc_auth_vals, &self.selected_auth_type) } } @@ -517,12 +492,6 @@ impl Default for DefineBitcoind { } } -impl From for Box { - fn from(s: DefineBitcoind) -> Box { - Box::new(s) - } -} - pub struct InternalBitcoindStep { liana_datadir: PathBuf, bitcoind_datadir: PathBuf, @@ -579,7 +548,7 @@ impl Step for InternalBitcoindStep { } if let Some(Ok(_)) = self.started { // This case can arise if a user switches from internal bitcoind to external and back to internal. - if ctx.bitcoind_config.is_none() { + if ctx.bitcoin_backend.is_none() { self.started = None; // So that internal bitcoind will be restarted. } } @@ -790,7 +759,10 @@ impl Step for InternalBitcoindStep { fn apply(&mut self, ctx: &mut Context) -> bool { // Any errors have been handled as part of `message::InternalBitcoindMsg::Start` if let Some(Ok(_)) = self.started { - ctx.bitcoind_config.clone_from(&self.bitcoind_config); + ctx.bitcoin_backend = self + .bitcoind_config + .as_ref() + .map(|bitcoind_config| BitcoinBackend::Bitcoind(bitcoind_config.clone())); ctx.internal_bitcoind_config .clone_from(&self.internal_bitcoind_config); ctx.internal_bitcoind.clone_from(&self.internal_bitcoind); diff --git a/gui/src/installer/step/node/electrum.rs b/gui/src/installer/step/node/electrum.rs new file mode 100644 index 000000000..8adfbd5a8 --- /dev/null +++ b/gui/src/installer/step/node/electrum.rs @@ -0,0 +1,86 @@ +use iced::Command; +use liana::{ + config::ElectrumConfig, + electrum_client::{self, ElectrumApi}, +}; +use liana_ui::{component::form, widget::*}; + +use crate::{ + installer::{ + context::Context, + message::{self, Message}, + view, Error, + }, + node::electrum::ConfigField, +}; + +#[derive(Clone)] +pub struct DefineElectrum { + address: form::Value, +} + +impl DefineElectrum { + pub fn new() -> Self { + Self { + address: form::Value::default(), + } + } + + pub fn can_try_ping(&self) -> bool { + !self.address.value.is_empty() && self.address.valid + } + + pub fn update(&mut self, message: message::DefineNode) -> Command { + if let message::DefineNode::DefineElectrum(msg) = message { + match msg { + message::DefineElectrum::ConfigFieldEdited(field, value) => match field { + ConfigField::Address => { + let value_noprefix = if value.starts_with("ssl://") { + value.replacen("ssl://", "", 1) + } else { + value.replacen("tcp://", "", 1) + }; + let noprefix_parts: Vec<_> = value_noprefix.split(':').collect(); + self.address.value.clone_from(&value); // save the value including any prefix + self.address.valid = noprefix_parts.len() == 2 + && !noprefix_parts + .first() + .expect("there are two parts") + .is_empty() + && noprefix_parts + .last() + .expect("there are two parts") + .parse::() // check it is a port + .is_ok(); + } + }, + }; + }; + Command::none() + } + + pub fn apply(&mut self, ctx: &mut Context) -> bool { + if self.can_try_ping() { + ctx.bitcoin_backend = Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig { + addr: self.address.value.clone(), + })); + return true; + } + false + } + + pub fn view(&self) -> Element { + view::define_electrum(&self.address) + } + + pub fn ping(&self) -> Result<(), Error> { + let builder = electrum_client::Config::builder(); + let config = builder.timeout(Some(3)).build(); + let client = electrum_client::Client::from_config(&self.address.value, config) + .map_err(|e| Error::Electrum(e.to_string()))?; + client + .raw_call("server.ping", []) + .map_err(|e| Error::Electrum(e.to_string()))?; + Ok(()) + } +} diff --git a/gui/src/installer/step/node/mod.rs b/gui/src/installer/step/node/mod.rs new file mode 100644 index 000000000..cb8b350dc --- /dev/null +++ b/gui/src/installer/step/node/mod.rs @@ -0,0 +1,253 @@ +pub mod bitcoind; +pub mod electrum; + +use crate::{ + hw::HardwareWallets, + installer::{ + context::Context, + message::{self, Message}, + step::{ + node::{bitcoind::DefineBitcoind, electrum::DefineElectrum}, + Step, + }, + view, Error, + }, + node::NodeType, +}; + +use iced::Command; +use liana_ui::widget::Element; + +#[derive(Clone)] +pub enum NodeDefinition { + Bitcoind(DefineBitcoind), + Electrum(DefineElectrum), +} + +impl NodeDefinition { + fn new(node_type: NodeType) -> Self { + match node_type { + NodeType::Bitcoind => NodeDefinition::Bitcoind(DefineBitcoind::new()), + NodeType::Electrum => NodeDefinition::Electrum(DefineElectrum::new()), + } + } + + fn node_type(&self) -> NodeType { + match self { + NodeDefinition::Bitcoind(_) => NodeType::Bitcoind, + NodeDefinition::Electrum(_) => NodeType::Electrum, + } + } + + fn apply(&mut self, ctx: &mut Context) -> bool { + match self { + NodeDefinition::Bitcoind(def) => def.apply(ctx), + NodeDefinition::Electrum(def) => def.apply(ctx), + } + } + + fn can_try_ping(&self) -> bool { + match self { + NodeDefinition::Bitcoind(def) => def.can_try_ping(), + NodeDefinition::Electrum(def) => def.can_try_ping(), + } + } + + fn load_context(&mut self, ctx: &Context) { + match self { + NodeDefinition::Bitcoind(def) => def.load_context(ctx), + NodeDefinition::Electrum(_) => { + // noop for now + } + } + } + + fn update(&mut self, message: message::DefineNode) -> Command { + match self { + NodeDefinition::Bitcoind(def) => def.update(message), + NodeDefinition::Electrum(def) => def.update(message), + } + } + + fn view(&self) -> Element { + match self { + NodeDefinition::Bitcoind(def) => def.view(), + NodeDefinition::Electrum(def) => def.view(), + } + } + + fn ping(&self) -> Result<(), Error> { + match self { + NodeDefinition::Bitcoind(def) => def.ping(), + NodeDefinition::Electrum(def) => def.ping(), + } + } +} + +pub struct Node { + definition: NodeDefinition, + is_running: Option>, + waiting_for_ping_result: bool, +} + +impl Node { + fn new(node_type: NodeType) -> Self { + Node { + definition: NodeDefinition::new(node_type), + is_running: None, + waiting_for_ping_result: false, + } + } +} + +pub struct DefineNode { + nodes: Vec, + selected_node_type: NodeType, +} + +impl From for Box { + fn from(s: DefineNode) -> Box { + Box::new(s) + } +} + +impl DefineNode { + pub fn new(selected_node_type: NodeType) -> Self { + let available_node_types = [ + // This is the order in which the available node types will be shown to the user. + NodeType::Bitcoind, + NodeType::Electrum, + ]; + assert!(available_node_types.contains(&selected_node_type)); + + let nodes = available_node_types + .iter() + .copied() + .map(Node::new) + .collect(); + + Self { + nodes, + selected_node_type, + } + } + + pub fn selected_mut(&mut self) -> &mut Node { + self.get_mut(self.selected_node_type) + .expect("selected type must be present") + } + + pub fn selected(&self) -> &Node { + self.get(self.selected_node_type) + .expect("selected type must be present") + } + + pub fn get_mut(&mut self, node_type: NodeType) -> Option<&mut Node> { + self.nodes + .iter_mut() + .find(|node| node.definition.node_type() == node_type) + } + + pub fn get(&self, node_type: NodeType) -> Option<&Node> { + self.nodes + .iter() + .find(|node| node.definition.node_type() == node_type) + } + + fn update_node( + &mut self, + node_type: NodeType, + message: message::DefineNode, + ) -> Command { + if let Some(node) = self.get_mut(node_type) { + // Don't make changes while waiting for a ping result so that we + // know which values the ping result applies to. + if !node.waiting_for_ping_result { + node.is_running = None; + return node.definition.update(message); + } + } + Command::none() + } +} + +impl Default for DefineNode { + fn default() -> Self { + Self::new(NodeType::Bitcoind) + } +} + +impl Step for DefineNode { + fn load_context(&mut self, ctx: &Context) { + for node in self.nodes.iter_mut() { + node.definition.load_context(ctx); + } + } + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { + if let Message::DefineNode(msg) = message { + match msg { + message::DefineNode::NodeTypeSelected(node_type) => { + self.selected_node_type = node_type; + } + message::DefineNode::Ping => { + let selected = self.selected_mut(); + // Make sure we don't send more than one ping request at a time + // so that we know which values the result applies to. + if !selected.waiting_for_ping_result { + selected.waiting_for_ping_result = true; + selected.is_running = None; + let def = selected.definition.clone(); + let node_type = def.node_type(); + return Command::perform(async move { def.ping() }, move |res| { + Message::DefineNode(message::DefineNode::PingResult((node_type, res))) + }); + } + } + message::DefineNode::PingResult((node_type, res)) => { + // Result may not be for the selected node type. + if let Some(node) = self.get_mut(node_type) { + // Make sure we're expecting the ping result. Otherwise, the user may have changed values + // and so the ping result may not apply to the current values. + if node.waiting_for_ping_result { + node.waiting_for_ping_result = false; + node.is_running = Some(res); + } + } + } + msg @ message::DefineNode::DefineBitcoind(_) => { + return self.update_node(NodeType::Bitcoind, msg); + } + msg @ message::DefineNode::DefineElectrum(_) => { + return self.update_node(NodeType::Electrum, msg); + } + } + } + Command::none() + } + + fn apply(&mut self, ctx: &mut Context) -> bool { + self.selected_mut().definition.apply(ctx) + } + + fn view( + &self, + _hws: &HardwareWallets, + progress: (usize, usize), + _email: Option<&str>, + ) -> Element { + // TODO: Make input fields read-only while waiting for a ping result. + view::define_bitcoin_node( + progress, + self.nodes.iter().map(|node| node.definition.node_type()), + self.selected_node_type, + self.selected().definition.view(), + self.selected().is_running.as_ref(), + self.selected().definition.can_try_ping(), + self.selected().waiting_for_ping_result, + ) + } + + fn skip(&self, ctx: &Context) -> bool { + !ctx.bitcoind_is_external || ctx.remote_backend.is_some() + } +} diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index cae776e00..142c9d480 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -32,14 +32,17 @@ use liana_ui::{ }; use crate::{ - bitcoind::{ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError}, hw::{is_compatible_with_tapminiscript, HardwareWallet, UnsupportedReason}, installer::{ - message::{self, Message}, + message::{self, DefineBitcoind, DefineNode, Message}, prompt, step::{DownloadState, InstallState}, Error, }, + node::{ + bitcoind::{ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError}, + electrum, NodeType, + }, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -524,11 +527,12 @@ pub fn import_descriptor<'a>( "Import the wallet", Column::new() .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.", + "If you are using a Bitcoin Core node, \ + you will need to perform a rescan of \ + the blockchain after creating the wallet \ + in order to see your coins and past \ + transactions. This can be done in \ + Settings > Node.", ))) .push( if imported_descriptor.value.is_empty() || !imported_descriptor.valid { @@ -1159,12 +1163,107 @@ pub fn help_backup<'a>() -> Element<'a, Message> { text(prompt::BACKUP_DESCRIPTOR_HELP).small().into() } -pub fn define_bitcoin<'a>( +pub fn define_bitcoin_node<'a>( progress: (usize, usize), + available_node_types: impl Iterator, + selected_node_type: NodeType, + node_view: Element<'a, Message>, + is_running: Option<&Result<(), Error>>, + can_try_ping: bool, + waiting_for_ping_result: bool, +) -> Element<'a, Message> { + let col = Column::new() + .push( + available_node_types.fold( + Row::new() + .push(text("Node type:").small().bold()) + .spacing(10), + |row, node_type| { + row.push(radio( + match node_type { + NodeType::Bitcoind => "Bitcoin Core", + NodeType::Electrum => "Electrum", + }, + node_type, + Some(selected_node_type), + |new_selection| { + Message::DefineNode(message::DefineNode::NodeTypeSelected( + new_selection, + )) + }, + )) + .spacing(30) + .align_items(Alignment::Center) + }, + ), + ) + .push(node_view) + .push_maybe(if waiting_for_ping_result { + Some(Container::new( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push(text("Checking connection...")), + )) + } else if is_running.is_some() { + is_running.map(|res| { + if res.is_ok() { + Container::new( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push(icon::circle_check_icon().style(color::GREEN)) + .push(text("Connection checked").style(color::GREEN)), + ) + } else { + Container::new( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push(icon::circle_cross_icon().style(color::RED)) + .push(text("Connection failed").style(color::RED)), + ) + } + }) + } else { + Some(Container::new(Space::with_height(Length::Fixed(21.0)))) + }) + .push( + Row::new() + .spacing(10) + .push(Container::new( + button::secondary(None, "Check connection") + .on_press_maybe(if can_try_ping && !waiting_for_ping_result { + Some(Message::DefineNode(DefineNode::Ping)) + } else { + None + }) + .width(Length::Fixed(200.0)), + )) + .push(if is_running.map(|res| res.is_ok()).unwrap_or(false) { + button::primary(None, "Next") + .on_press(Message::Next) + .width(Length::Fixed(200.0)) + } else { + button::primary(None, "Next").width(Length::Fixed(200.0)) + }), + ) + .spacing(50); + + layout( + progress, + None, + "Set up connection to the Bitcoin node", + col, + true, + Some(Message::Previous), + ) +} + +pub fn define_bitcoind<'a>( address: &form::Value, rpc_auth_vals: &RpcAuthValues, selected_auth_type: &RpcAuthType, - is_running: Option<&Result<(), Error>>, ) -> Element<'a, Message> { let is_loopback = if let Some((ip, _port)) = address.value.clone().rsplit_once(':') { let (ipv4, ipv6) = (Ipv4Addr::from_str(ip), Ipv6Addr::from_str(ip)); @@ -1181,9 +1280,8 @@ pub fn define_bitcoin<'a>( .push(text("Address:").bold()) .push( form::Form::new_trimmed("Address", address, |msg| { - Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited( - ConfigField::Address, - msg, + Message::DefineNode(DefineNode::DefineBitcoind( + DefineBitcoind::ConfigFieldEdited(ConfigField::Address, msg), )) }) .warning("Please enter correct address") @@ -1219,9 +1317,9 @@ pub fn define_bitcoin<'a>( *auth_type, Some(*selected_auth_type), |new_selection| { - Message::DefineBitcoind( - message::DefineBitcoind::RpcAuthTypeSelected(new_selection), - ) + Message::DefineNode(DefineNode::DefineBitcoind( + DefineBitcoind::RpcAuthTypeSelected(new_selection), + )) }, )) .spacing(30) @@ -1232,9 +1330,8 @@ pub fn define_bitcoin<'a>( .push(match selected_auth_type { RpcAuthType::CookieFile => Row::new().push( form::Form::new_trimmed("Cookie path", &rpc_auth_vals.cookie_path, |msg| { - Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited( - ConfigField::CookieFilePath, - msg, + Message::DefineNode(DefineNode::DefineBitcoind( + DefineBitcoind::ConfigFieldEdited(ConfigField::CookieFilePath, msg), )) }) .warning("Please enter correct path") @@ -1244,9 +1341,8 @@ pub fn define_bitcoin<'a>( RpcAuthType::UserPass => Row::new() .push( form::Form::new_trimmed("User", &rpc_auth_vals.user, |msg| { - Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited( - ConfigField::User, - msg, + Message::DefineNode(DefineNode::DefineBitcoind( + DefineBitcoind::ConfigFieldEdited(ConfigField::User, msg), )) }) .warning("Please enter correct user") @@ -1255,9 +1351,8 @@ pub fn define_bitcoin<'a>( ) .push( form::Form::new_trimmed("Password", &rpc_auth_vals.password, |msg| { - Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited( - ConfigField::Password, - msg, + Message::DefineNode(DefineNode::DefineBitcoind( + DefineBitcoind::ConfigFieldEdited(ConfigField::Password, msg), )) }) .warning("Please enter correct password") @@ -1268,69 +1363,32 @@ pub fn define_bitcoin<'a>( }) .spacing(10); - let check_connect_enable = if let RpcAuthType::UserPass = selected_auth_type { - address.valid - && !rpc_auth_vals.password.value.is_empty() - && !rpc_auth_vals.user.value.is_empty() - } else { - address.valid && !rpc_auth_vals.cookie_path.value.is_empty() - }; - layout( - progress, - None, - "Set up connection to the Bitcoin full node", - Column::new() - .push(col_address) - .push(col_auth) - .push_maybe(if is_running.is_some() { - is_running.map(|res| { - if res.is_ok() { - Container::new( - Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push(icon::circle_check_icon().style(color::GREEN)) - .push(text("Connection checked").style(color::GREEN)), - ) - } else { - Container::new( - Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push(icon::circle_cross_icon().style(color::RED)) - .push(text("Connection failed").style(color::RED)), - ) - } - }) - } else { - Some(Container::new(Space::with_height(Length::Fixed(25.0)))) + Column::new() + .push(col_address) + .push(col_auth) + .spacing(50) + .into() +} + +pub fn define_electrum<'a>(address: &form::Value) -> Element<'a, Message> { + let col_address = Column::new() + .push(text("Address:").bold()) + .push( + form::Form::new_trimmed("127.0.0.1:50001", address, |msg| { + Message::DefineNode(DefineNode::DefineElectrum( + message::DefineElectrum::ConfigFieldEdited(electrum::ConfigField::Address, msg), + )) }) - .push( - Row::new() - .spacing(10) - .push(Container::new( - button::secondary(None, "Check connection") - .on_press_maybe(if check_connect_enable { - Some(Message::DefineBitcoind( - message::DefineBitcoind::PingBitcoind, - )) - } else { - None - }) - .width(Length::Fixed(200.0)), - )) - .push(if is_running.map(|res| res.is_ok()).unwrap_or(false) { - button::primary(None, "Next") - .on_press(Message::Next) - .width(Length::Fixed(200.0)) - } else { - button::primary(None, "Next").width(Length::Fixed(200.0)) - }), + .warning( + "Please enter correct address (including port), \ + optionally prefixed with tcp:// or ssl://", ) - .spacing(50), - true, - Some(Message::Previous), - ) + .size(text::P1_SIZE) + .padding(10), + ) + .spacing(10); + + Column::new().push(col_address).spacing(50).into() } pub fn select_bitcoind_type<'a>(progress: (usize, usize)) -> Element<'a, Message> { @@ -1338,91 +1396,107 @@ pub fn select_bitcoind_type<'a>(progress: (usize, usize)) -> Element<'a, Message progress, None, "Bitcoin node management", - Column::new().push( - Row::new() - .align_items(Alignment::Start) - .spacing(20) - .push( - Container::new( - Column::new() - .spacing(20) - .width(Length::Fixed(300.0)) - .push(text("Manage your own Bitcoin node").bold()) - ) - .padding(20), - ) - .push( - Container::new( - Column::new() - .spacing(20) - .width(Length::Fixed(300.0)) - .push(text("Have Liana manage and run a dedicated Bitcoin node").bold()) - ) - .padding(20), - ), - ) - .push( - Row::new() - .align_items(Alignment::Start) - .spacing(20) - .push( - Container::new( - Column::new() - .spacing(20) - .width(Length::Fixed(300.0)) - .align_items(Alignment::Start) - .push(text("Liana will connect to your existing instance of Bitcoin Core. You will have to make sure Bitcoin Core is running when you use Liana.\n\n(Use this if you already have a full node on your machine, and don't need a new instance)")) - ) - .padding(20), - ) - .push( - Container::new( - Column::new() - .spacing(20) - .width(Length::Fixed(300.0)) - .align_items(Alignment::Start) - .push(text("Liana will run its own instance of Bitcoin Core. This will use a pruned node, and perform the synchronization in the Liana folder.\n\nIf you select this option, Bitcoin Core will be downloaded, installed and started on the next step.\n\n(Use this if you don't want to deal with Bitcoin Core yourself, or need a new, dedicated instance for Liana)")) + Column::new() + .push( + Row::new() + .align_items(Alignment::Start) + .spacing(20) + .push( + Container::new( + Column::new() + .spacing(20) + .width(Length::Fixed(300.0)) + .push(text("I already have a node").bold()), + ) + .padding(20), ) - .padding(20), - ), - ) - .push( - Row::new() - .align_items(Alignment::End) - .spacing(20) - .push( - Container::new( - Column::new() - .spacing(20) - .width(Length::Fixed(300.0)) - .align_items(Alignment::Center) - .push( - button::primary(None, "Select") + .push( + Container::new( + Column::new().spacing(20).width(Length::Fixed(300.0)).push( + text( + "I want Liana to automatically install \ + a Bitcoin node on my device", + ) + .bold(), + ), + ) + .padding(20), + ), + ) + .push( + Row::new() + .align_items(Alignment::Start) + .spacing(20) + .push( + Container::new( + Column::new() + .spacing(20) .width(Length::Fixed(300.0)) - .on_press(Message::SelectBitcoindType( - message::SelectBitcoindTypeMsg::UseExternal(true), - )), - ) + .align_items(Alignment::Start) + .push(text( + "Select this option if you already have \ + a Bitcoin node running locally or remotely. \ + Liana will connect to it.", + )), + ) + .padding(20), ) - .padding(20), - ) - .push( - Container::new( - Column::new() - .spacing(20) - .width(Length::Fixed(300.0)) - .align_items(Alignment::Center) - .push( - button::primary(None, "Select") - .width(Length::Fixed(300.0)) - .on_press(Message::SelectBitcoindType( - message::SelectBitcoindTypeMsg::UseExternal(false), - )), - ) + .push( + Container::new( + Column::new() + .spacing(20) + .width(Length::Fixed(300.0)) + .align_items(Alignment::Start) + .push(text( + "Liana will install a pruned node \ + on your computer. You won't need to do anything \ + except have some disk space available \ + (~30GB required on mainnet) and \ + wait for the initial synchronization with the \ + network (it can take some days depending on \ + your internet connection speed).", + )), + ) + .padding(20), + ), + ) + .push( + Row::new() + .align_items(Alignment::End) + .spacing(20) + .push( + Container::new( + Column::new() + .spacing(20) + .width(Length::Fixed(300.0)) + .align_items(Alignment::Center) + .push( + button::primary(None, "Select") + .width(Length::Fixed(300.0)) + .on_press(Message::SelectBitcoindType( + message::SelectBitcoindTypeMsg::UseExternal(true), + )), + ), + ) + .padding(20), ) - .padding(20), - ), - ), + .push( + Container::new( + Column::new() + .spacing(20) + .width(Length::Fixed(300.0)) + .align_items(Alignment::Center) + .push( + button::primary(None, "Select") + .width(Length::Fixed(300.0)) + .on_press(Message::SelectBitcoindType( + message::SelectBitcoindTypeMsg::UseExternal(false), + )), + ), + ) + .padding(20), + ), + ), true, Some(Message::Previous), ) @@ -1436,7 +1510,7 @@ pub fn start_internal_bitcoind<'a>( download_state: Option<&DownloadState>, install_state: Option<&InstallState>, ) -> Element<'a, Message> { - let version = crate::bitcoind::VERSION; + let version = crate::node::bitcoind::VERSION; let mut next_button = button::primary(None, "Next").width(Length::Fixed(200.0)); if let Some(Ok(_)) = started { next_button = next_button.on_press(Message::Next); diff --git a/gui/src/lib.rs b/gui/src/lib.rs index b29f7e7c2..9441c85fd 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -1,5 +1,4 @@ pub mod app; -pub mod bitcoind; pub mod daemon; pub mod datadir; pub mod download; @@ -9,6 +8,7 @@ pub mod launcher; pub mod lianalite; pub mod loader; pub mod logger; +pub mod node; pub mod signer; pub mod utils; diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 47e22ac3d..27283a7fa 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -11,7 +11,7 @@ use tracing::{debug, info, warn}; use liana::{ commands::CoinStatus, - config::{Config, ConfigError}, + config::{BitcoinBackend, Config, ConfigError}, miniscript::bitcoin, StartupError, }; @@ -29,10 +29,10 @@ use crate::{ config::Config as GUIConfig, wallet::{Wallet, WalletError}, }, - bitcoind::{ + daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError}, + node::bitcoind::{ internal_bitcoind_debug_log_path, stop_bitcoind, Bitcoind, StartInternalBitcoindError, }, - daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError}, }; const SYNCING_PROGRESS_1: &str = "Bitcoin Core is synchronising the blockchain. A full synchronisation typically take a few days, and is resource intensive. Once the initial synchronisation is done, the next ones will be much faster."; @@ -245,7 +245,7 @@ impl Loader { log::info!("Managed bitcoind stopped."); } else if self.waiting_daemon_bitcoind && self.gui_config.start_internal_bitcoind { if let Ok(config) = Config::from_file(self.gui_config.daemon_config_path.clone()) { - if let Some(bitcoind_config) = &config.bitcoind_config { + if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend { let mut retry = 0; while !stop_bitcoind(bitcoind_config) && retry < 10 { std::thread::sleep(std::time::Duration::from_millis(500)); @@ -500,7 +500,7 @@ pub async fn start_bitcoind_and_daemon( let config = Config::from_file(Some(config_path)).map_err(Error::Config)?; let mut bitcoind: Option = None; if start_internal_bitcoind { - if let Some(bitcoind_config) = &config.bitcoind_config { + if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend { // Check if bitcoind is already running before trying to start it. if liana::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok() { diff --git a/gui/src/bitcoind.rs b/gui/src/node/bitcoind.rs similarity index 100% rename from gui/src/bitcoind.rs rename to gui/src/node/bitcoind.rs diff --git a/gui/src/node/electrum.rs b/gui/src/node/electrum.rs new file mode 100644 index 000000000..97c563a84 --- /dev/null +++ b/gui/src/node/electrum.rs @@ -0,0 +1,14 @@ +use std::fmt; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ConfigField { + Address, +} + +impl fmt::Display for ConfigField { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ConfigField::Address => write!(f, "RPC address"), + } + } +} diff --git a/gui/src/node/mod.rs b/gui/src/node/mod.rs new file mode 100644 index 000000000..293a65268 --- /dev/null +++ b/gui/src/node/mod.rs @@ -0,0 +1,8 @@ +pub mod bitcoind; +pub mod electrum; + +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum NodeType { + Bitcoind, + Electrum, +}