From 765c68b02e87dbea7d14935642ff9a91dcbc9ee2 Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:25:59 +0100 Subject: [PATCH 1/3] installer: allow for different previous messages Clicking the "Previous" button during installation will default to the current behaviour, unless a different message is passed to the function by any given step. --- gui/src/installer/view.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index d8e636cbf..a7c0b8056 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -299,6 +299,7 @@ pub fn define_descriptor<'a>( .push(Space::with_height(Length::Fixed(20.0))) .spacing(50), false, + None, ) } @@ -416,6 +417,7 @@ pub fn import_descriptor<'a>( .push_maybe(error.map(|e| card::error("Invalid descriptor", e.to_string()))) .spacing(50), true, + None, ) } @@ -646,7 +648,8 @@ pub fn participate_xpub<'a>( button::primary(None, "Next").width(Length::Fixed(200.0)) }) .spacing(50), - true + true, + None, ) } @@ -738,6 +741,7 @@ pub fn register_descriptor<'a>( }) .spacing(50), true, + None, ) } @@ -805,6 +809,7 @@ pub fn backup_descriptor<'a>( }) .spacing(50), true, + None, ) } @@ -891,6 +896,7 @@ pub fn define_bitcoin<'a>( ) .spacing(50), true, + None, ) } @@ -1049,6 +1055,7 @@ pub fn install<'a>( .spacing(10) .width(Length::Fill), true, + None, ) } @@ -1558,6 +1565,7 @@ pub fn backup_mnemonic<'a>( }) .spacing(50), true, + None, ) } @@ -1660,6 +1668,7 @@ pub fn recover_mnemonic<'a>( }) .spacing(50), true, + None, ) } @@ -1668,6 +1677,7 @@ fn layout<'a>( title: &'static str, content: impl Into>, padding_left: bool, + previous_message: Option, ) -> Element<'a, Message> { Container::new(scrollable( Column::new() @@ -1679,7 +1689,7 @@ fn layout<'a>( .push( Container::new( button::transparent(Some(icon::previous_icon()), "Previous") - .on_press(Message::Previous), + .on_press(previous_message.unwrap_or(Message::Previous)), ) .width(Length::FillPortion(2)) .center_x(), From 36cf85d84933c0cf5783d0a67fa73ae3050a3ec6 Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:44:43 +0100 Subject: [PATCH 2/3] gui: add option to use internal bitcoind During installation, the user may choose for Liana to configure and start bitcoind for them. This internal bitcoind uses as its data directory the `bitcoind_datadir` folder within the Liana data directory. If the internal bitcoind option has been selected for a network, it will be automatically started when the user returns to Liana and stopped when Liana is closed. --- gui/Cargo.lock | 85 +++++ gui/Cargo.toml | 3 + gui/src/app/config.rs | 17 +- gui/src/app/mod.rs | 8 + gui/src/bitcoind.rs | 77 +++++ gui/src/installer/context.rs | 9 + gui/src/installer/message.rs | 15 + gui/src/installer/mod.rs | 35 +- gui/src/installer/prompt.rs | 1 + gui/src/installer/step/mod.rs | 628 ++++++++++++++++++++++++++++++++-- gui/src/installer/view.rs | 129 +++++++ gui/src/lib.rs | 1 + gui/src/loader.rs | 56 ++- gui/src/utils/mod.rs | 16 + 14 files changed, 1049 insertions(+), 31 deletions(-) create mode 100644 gui/src/bitcoind.rs diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 94090710c..fa0aeb052 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -489,6 +489,28 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] + [[package]] name = "const_panic" version = "0.2.7" @@ -795,6 +817,15 @@ dependencies = [ "libloading", ] +[[package]] +name = "dlv-list" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d529fd73d344663edfd598ccb3f344e46034db51ebd103518eae34338248ad73" +dependencies = [ + "const-random", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1446,6 +1477,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashlink" version = "0.7.0" @@ -1953,12 +1990,14 @@ dependencies = [ "liana", "liana_ui", "log", + "rust-ini", "serde", "serde_json", "tokio", "toml", "tracing", "tracing-subscriber", + "which", ] [[package]] @@ -2553,6 +2592,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "osmesa-sys" version = "0.1.2" @@ -2817,6 +2866,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.64" @@ -3065,6 +3120,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.22" @@ -3532,6 +3597,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tiny-skia" version = "0.7.0" @@ -4170,6 +4244,17 @@ dependencies = [ "wgpu", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "widestring" version = "0.5.1" diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 250ac2615..b1a4ea3b2 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -42,6 +42,9 @@ toml = "0.5" chrono = "0.4" +rust-ini = "0.19.0" +which = "4.4.0" + [dev-dependencies] tokio = {version = "1.9.0", features = ["rt", "macros"]} diff --git a/gui/src/app/config.rs b/gui/src/app/config.rs index 265221f4e..1dfd52d93 100644 --- a/gui/src/app/config.rs +++ b/gui/src/app/config.rs @@ -3,6 +3,15 @@ use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tracing_subscriber::filter; +/// Config required to start internal bitcoind. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct InternalBitcoindExeConfig { + /// Internal bitcoind executable path. + pub exe_path: PathBuf, + /// Internal bitcoind data dir. + pub data_dir: PathBuf, +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { /// Path to lianad configuration file. @@ -16,18 +25,24 @@ pub struct Config { /// hardware wallets config. /// LEGACY: Use Settings module instead. pub hardware_wallets: Option>, + /// Internal bitcoind executable config. + pub internal_bitcoind_exe_config: Option, } pub const DEFAULT_FILE_NAME: &str = "gui.toml"; impl Config { - pub fn new(daemon_config_path: PathBuf) -> Self { + pub fn new( + daemon_config_path: PathBuf, + internal_bitcoind_exe_config: Option, + ) -> Self { Self { daemon_config_path: Some(daemon_config_path), daemon_rpc_path: None, log_level: None, debug: None, hardware_wallets: None, + internal_bitcoind_exe_config, } } diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index d5ce721a4..42f1aec83 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -31,6 +31,7 @@ use state::{ use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, + bitcoind::stop_internal_bitcoind, daemon::{embedded::EmbeddedDaemon, Daemon}, }; @@ -115,6 +116,13 @@ impl App { if !self.daemon.is_external() { self.daemon.stop(); info!("Internal daemon stopped"); + if self.config.internal_bitcoind_exe_config.is_some() { + if let Some(daemon_config) = self.daemon.config() { + if let Some(bitcoind_config) = &daemon_config.bitcoind_config { + stop_internal_bitcoind(bitcoind_config); + } + } + } } } diff --git a/gui/src/bitcoind.rs b/gui/src/bitcoind.rs new file mode 100644 index 000000000..02606f158 --- /dev/null +++ b/gui/src/bitcoind.rs @@ -0,0 +1,77 @@ +use liana::{config::BitcoindConfig, miniscript::bitcoin}; + +use tracing::{info, warn}; + +use crate::app::config::InternalBitcoindExeConfig; + +/// Possible errors when starting bitcoind. +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum StartInternalBitcoindError { + CommandError(String), + CouldNotCanonicalizeDataDir(String), + CouldNotCanonicalizeCookiePath(String), + CookieFileNotFound(String), + BitcoinDError(String), +} + +impl std::fmt::Display for StartInternalBitcoindError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::CommandError(e) => { + write!(f, "Command to start bitcoind returned an error: {}", e) + } + Self::CouldNotCanonicalizeDataDir(e) => { + write!(f, "Failed to canonicalize datadir: {}", e) + } + Self::CouldNotCanonicalizeCookiePath(e) => { + write!(f, "Failed to canonicalize cookie path: {}", e) + } + Self::CookieFileNotFound(path) => { + write!( + f, + "Cookie file was not found at the expected path: {}", + path + ) + } + Self::BitcoinDError(e) => write!(f, "bitcoind connection check failed: {}", e), + } + } +} + +/// Start internal bitcoind for the given network. +pub fn start_internal_bitcoind( + network: &bitcoin::Network, + exe_config: InternalBitcoindExeConfig, +) -> Result { + let args = vec![ + format!("-chain={}", network.to_core_arg()), + format!( + "-datadir={}", + exe_config + .data_dir + .canonicalize() + .map_err(|e| StartInternalBitcoindError::CouldNotCanonicalizeDataDir( + e.to_string() + ))? + .to_string_lossy() + ), + ]; + std::process::Command::new(exe_config.exe_path) + .args(&args) + .stdout(std::process::Stdio::null()) // We still get bitcoind's logs in debug.log. + .spawn() + .map_err(|e| StartInternalBitcoindError::CommandError(e.to_string())) +} + +/// Stop (internal) bitcoind. +pub fn stop_internal_bitcoind(bitcoind_config: &BitcoindConfig) { + match liana::BitcoinD::new(bitcoind_config, "internal_bitcoind_stop".to_string()) { + Ok(bitcoind) => { + info!("Stopping internal bitcoind..."); + bitcoind.stop(); + } + Err(e) => { + warn!("Could not create interface to internal bitcoind: '{}'.", e); + } + } +} diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs index ca28a0a8f..7d1a6bfb8 100644 --- a/gui/src/installer/context.rs +++ b/gui/src/installer/context.rs @@ -4,6 +4,7 @@ use std::time::Duration; use crate::{ app::{ + config::InternalBitcoindExeConfig, settings::{KeySetting, Settings, WalletSetting}, wallet::DEFAULT_WALLET_NAME, }, @@ -18,6 +19,8 @@ use liana::{ miniscript::bitcoin, }; +use super::step::InternalBitcoindConfig; + #[derive(Clone)] pub struct Context { pub bitcoin_config: BitcoinConfig, @@ -30,6 +33,9 @@ pub struct Context { // In case a user entered a mnemonic, // we dont want to override the generated signer with it. pub recovered_signer: Option>, + pub bitcoind_is_external: bool, + pub internal_bitcoind_config: Option, + pub internal_bitcoind_exe_config: Option, } impl Context { @@ -46,6 +52,9 @@ impl Context { data_dir, hw_is_used: false, recovered_signer: None, + bitcoind_is_external: true, + internal_bitcoind_config: None, + internal_bitcoind_exe_config: None, } } diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index ed1fda5ef..f1c0717c5 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -26,6 +26,8 @@ pub enum Message { UseHotSigner, Installed(Result), Network(Network), + SelectBitcoindType(SelectBitcoindTypeMsg), + InternalBitcoind(InternalBitcoindMsg), DefineBitcoind(DefineBitcoind), DefineDescriptor(DefineDescriptor), ImportXpub(usize, Result), @@ -43,6 +45,19 @@ pub enum DefineBitcoind { PingBitcoind, } +#[derive(Debug, Clone)] +pub enum SelectBitcoindTypeMsg { + UseExternal(bool), +} + +#[derive(Debug, Clone)] +pub enum InternalBitcoindMsg { + Previous, + Reload, + DefineConfig, + Start, +} + #[derive(Debug, Clone)] pub enum DefineDescriptor { ImportDescriptor(String), diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 96d2fc156..0d31ccc8e 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -16,14 +16,17 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex}; use crate::{ + app::config::InternalBitcoindExeConfig, app::{config as gui_config, settings as gui_settings}, + bitcoind::stop_internal_bitcoind, signer::Signer, }; pub use message::Message; use step::{ BackupDescriptor, BackupMnemonic, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, - ParticipateXpub, RecoverMnemonic, RegisterDescriptor, Step, Welcome, + InternalBitcoindStep, ParticipateXpub, RecoverMnemonic, RegisterDescriptor, + SelectBitcoindTypeStep, Step, Welcome, }; pub struct Installer { @@ -71,7 +74,19 @@ impl Installer { Subscription::none() } - pub fn stop(&mut self) {} + pub fn stop(&mut self) { + // Use current step's `stop()` method for any changes not yet written to context. + self.steps + .get_mut(self.current) + .expect("There is always a step") + .stop(); + // Now use context to determine what to stop. + if self.context.internal_bitcoind_config.is_some() { + if let Some(bitcoind_config) = &self.context.bitcoind_config { + stop_internal_bitcoind(bitcoind_config); + } + } + } fn next(&mut self) -> Command { let current_step = self @@ -114,6 +129,8 @@ impl Installer { BackupMnemonic::new(self.signer.clone()).into(), BackupDescriptor::default().into(), RegisterDescriptor::new_create_wallet().into(), + SelectBitcoindTypeStep::new().into(), + InternalBitcoindStep::new(&self.context.data_dir).into(), DefineBitcoind::new().into(), Final::new(hot_signer_fingerprint).into(), ]; @@ -127,6 +144,8 @@ impl Installer { BackupMnemonic::new(self.signer.clone()).into(), BackupDescriptor::default().into(), RegisterDescriptor::new_import_wallet().into(), + SelectBitcoindTypeStep::new().into(), + InternalBitcoindStep::new(&self.context.data_dir).into(), DefineBitcoind::new().into(), Final::new(hot_signer_fingerprint).into(), ]; @@ -138,6 +157,8 @@ impl Installer { ImportDescriptor::new(true).into(), RecoverMnemonic::default().into(), RegisterDescriptor::new_import_wallet().into(), + SelectBitcoindTypeStep::new().into(), + InternalBitcoindStep::new(&self.context.data_dir).into(), DefineBitcoind::new().into(), Final::new(hot_signer_fingerprint).into(), ]; @@ -230,6 +251,13 @@ pub fn daemon_check(cfg: liana::config::Config) -> Result<(), Error> { } } +/// Data directory used by internal bitcoind. +pub fn internal_bitcoind_datadir(liana_datadir: &PathBuf) -> PathBuf { + let mut datadir = PathBuf::from(liana_datadir); + datadir.push("bitcoind_datadir"); + datadir +} + pub async fn install(ctx: Context, signer: Arc>) -> Result { let mut cfg: liana::config::Config = ctx.extract_daemon_config(); let data_dir = cfg.data_dir.unwrap(); @@ -295,6 +323,7 @@ pub async fn install(ctx: Context, signer: Arc>) -> Result write!(f, "Failed to ping bitcoind: {}", 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), Self::CannotCreateFile(e) => write!(f, "Failed to create file: {}", e), Self::Unexpected(e) => write!(f, "Unexpected: {}", e), diff --git a/gui/src/installer/prompt.rs b/gui/src/installer/prompt.rs index 6072b2640..2b47a3399 100644 --- a/gui/src/installer/prompt.rs +++ b/gui/src/installer/prompt.rs @@ -9,3 +9,4 @@ pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str = pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a signing device needs the descriptor. If the descriptor contains one or more keys imported from an external signing device, the descriptor must be registered on it. Registration confirms that the device is able to handle the policy. Registration on a device is not a substitute for backing up the descriptor."; pub const MNEMONIC_HELP: &str = "A hot key generated on this computer was used for creating this wallet. It needs to be backed up. \n Keep it in a safe place. Never share it with anyone."; pub const RECOVER_MNEMONIC_HELP: &str = "If you were using a hot key (a key stored on the computer) in your wallet, you will need to recover it from mnemonics to be able to sign transactions again. Otherwise you can directly go the next step."; +pub const SELECT_BITCOIND_TYPE: &str = "Liana requires a Bitcoin node to be running. You can either use your own node that you manage yourself or you can let Liana install and manage a pruned Bitcoin node for use while running Liana."; diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 3bb73589c..0450ce1b2 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -7,7 +7,9 @@ pub use descriptor::{ pub use mnemonic::{BackupMnemonic, RecoverMnemonic}; -use std::path::PathBuf; +use std::collections::BTreeMap; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; +use std::path::{Path, PathBuf}; use std::str::FromStr; use iced::Command; @@ -16,14 +18,21 @@ use liana::{ miniscript::bitcoin::{bip32::Fingerprint, Network}, }; +use tracing::info; + use jsonrpc::{client::Client, simple_http::SimpleHttpTransport}; use liana_ui::{component::form, widget::*}; -use crate::installer::{ - context::Context, - message::{self, Message}, - view, Error, +use crate::{ + bitcoind::{start_internal_bitcoind, stop_internal_bitcoind, StartInternalBitcoindError}, + installer::{ + context::Context, + internal_bitcoind_datadir, + message::{self, Message}, + view, Error, InternalBitcoindExeConfig, + }, + utils::poll_for_file, }; pub trait Step { @@ -41,6 +50,7 @@ pub trait Step { fn apply(&mut self, _ctx: &mut Context) -> bool { true } + fn stop(&self) {} } #[derive(Default)] @@ -64,7 +74,186 @@ pub struct DefineBitcoind { is_running: Option>, } -fn bitcoind_default_cookie_path(network: &Network) -> Option { +pub struct InternalBitcoindStep { + bitcoind_datadir: PathBuf, + network: Network, + started: Option>, + exe_path: Option, + bitcoind_config: Option, + exe_config: Option, + internal_bitcoind_config: Option, + error: Option, +} + +pub struct SelectBitcoindTypeStep { + use_external: bool, +} + +/// Default prune value used by internal bitcoind. +pub const PRUNE_DEFAULT: u32 = 15_000; +/// Default ports used by bitcoind across all networks. +pub const BITCOIND_DEFAULT_PORTS: [u16; 8] = [8332, 8333, 18332, 18333, 18443, 18444, 38332, 38333]; + +/// Represents section for a single network in `bitcoin.conf` file. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct InternalBitcoindNetworkConfig { + rpc_port: u16, + p2p_port: u16, + prune: u32, +} + +/// Represents the `bitcoin.conf` file to be used by internal bitcoind. +#[derive(Debug, Clone)] +pub struct InternalBitcoindConfig { + networks: BTreeMap, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum InternalBitcoindConfigError { + KeyNotFound(String), + CouldNotParseValue(String), + UnexpectedSection(String), + TooManyElements(String), + FileNotFound, + ReadingFile(String), + WritingFile(String), + Unexpected(String), +} + +impl std::fmt::Display for InternalBitcoindConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::KeyNotFound(e) => write!(f, "Config file does not contain expected key: {}", e), + Self::CouldNotParseValue(e) => write!(f, "Value could not be parsed: {}", e), + Self::UnexpectedSection(e) => write!(f, "Unexpected section in file: {}", e), + Self::TooManyElements(section) => { + write!(f, "Section in file contains too many elements: {}", section) + } + Self::FileNotFound => write!(f, "File not found"), + Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e), + Self::WritingFile(e) => write!(f, "Error while writing file: {}", e), + Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), + } + } +} + +impl Default for InternalBitcoindConfig { + fn default() -> Self { + Self::new() + } +} + +impl InternalBitcoindConfig { + pub fn new() -> Self { + Self { + networks: BTreeMap::new(), + } + } + + pub fn from_ini(ini: &ini::Ini) -> Result { + let mut networks = BTreeMap::new(); + for (maybe_sec, prop) in ini { + if let Some(sec) = maybe_sec { + let network = Network::from_core_arg(sec) + .map_err(|e| InternalBitcoindConfigError::UnexpectedSection(e.to_string()))?; + if prop.len() > 3 { + return Err(InternalBitcoindConfigError::TooManyElements( + sec.to_string(), + )); + } + let rpc_port = prop + .get("rpcport") + .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("rpcport".to_string()))? + .parse::() + .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?; + let p2p_port = prop + .get("port") + .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("port".to_string()))? + .parse::() + .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?; + let prune = prop + .get("prune") + .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("prune".to_string()))? + .parse::() + .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?; + networks.insert( + network, + InternalBitcoindNetworkConfig { + rpc_port, + p2p_port, + prune, + }, + ); + } else if !prop.is_empty() { + return Err(InternalBitcoindConfigError::UnexpectedSection( + "General section should be empty".to_string(), + )); + } + } + Ok(Self { networks }) + } + + pub fn from_file(path: &PathBuf) -> Result { + if !path.exists() { + return Err(InternalBitcoindConfigError::FileNotFound); + } + let conf_ini = ini::Ini::load_from_file(path) + .map_err(|e| InternalBitcoindConfigError::ReadingFile(e.to_string()))?; + + Self::from_ini(&conf_ini) + } + + pub fn to_ini(&self) -> ini::Ini { + let mut conf_ini = ini::Ini::new(); + + for (network, network_conf) in &self.networks { + conf_ini + .with_section(Some(network.to_core_arg())) + .set("rpcport", network_conf.rpc_port.to_string()) + .set("port", network_conf.p2p_port.to_string()) + .set("prune", network_conf.prune.to_string()); + } + conf_ini + } + + pub fn to_file(&self, path: &PathBuf) -> Result<(), InternalBitcoindConfigError> { + std::fs::create_dir_all( + path.parent() + .ok_or_else(|| InternalBitcoindConfigError::Unexpected("No parent".to_string()))?, + ) + .map_err(|e| InternalBitcoindConfigError::Unexpected(e.to_string()))?; + info!("Writing to file {}", path.to_string_lossy()); + self.to_ini() + .write_to_file(path) + .map_err(|e| InternalBitcoindConfigError::WritingFile(e.to_string()))?; + + Ok(()) + } +} + +/// Path of the `bitcoin.conf` file used by internal bitcoind. +fn internal_bitcoind_config_path(bitcoind_datadir: &PathBuf) -> PathBuf { + let mut config_path = PathBuf::from(bitcoind_datadir); + config_path.push("bitcoin.conf"); + config_path +} + +/// Path of the cookie file used by internal bitcoind on a given network. +fn internal_bitcoind_cookie_path(bitcoind_datadir: &Path, network: &Network) -> PathBuf { + let mut cookie_path = bitcoind_datadir.to_path_buf(); + if let Some(dir) = bitcoind_network_dir(network) { + cookie_path.push(dir); + } + cookie_path.push(".cookie"); + cookie_path +} + +/// RPC address for internal bitcoind. +fn internal_bitcoind_address(rpc_port: u16) -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), rpc_port) +} + +fn bitcoind_default_datadir() -> Option { #[cfg(target_os = "linux")] let configs_dir = dirs::home_dir(); @@ -78,24 +267,30 @@ fn bitcoind_default_cookie_path(network: &Network) -> Option { #[cfg(not(target_os = "linux"))] path.push("Bitcoin"); - match network { - Network::Bitcoin => { - path.push(".cookie"); - } - Network::Testnet => { - path.push("testnet3/.cookie"); - } - Network::Regtest => { - path.push("regtest/.cookie"); - } - Network::Signet => { - path.push("signet/.cookie"); - } - _ => { - path.push(".cookie"); - } + return Some(path); + } + None +} + +fn bitcoind_network_dir(network: &Network) -> Option { + let dir = match network { + Network::Bitcoin => { + return None; } + Network::Testnet => "testnet3", + Network::Regtest => "regtest", + Network::Signet => "signet", + _ => panic!("Directory required for this network is unknown."), + }; + Some(dir.to_string()) +} +fn bitcoind_default_cookie_path(network: &Network) -> Option { + if let Some(mut path) = bitcoind_default_datadir() { + if let Some(dir) = bitcoind_network_dir(network) { + path.push(dir); + } + path.push(".cookie"); return path.to_str().map(|s| s.to_string()); } None @@ -111,6 +306,86 @@ fn bitcoind_default_address(network: &Network) -> String { } } +/// Looks for bitcoind executable path and returns `None` if not found. +fn bitcoind_exe_path() -> Option { + which::which("bitcoind").ok() +} + +/// Get available port that is valid for use by internal bitcoind. +// Modified from https://github.com/RCasatta/bitcoind/blob/f047740d7d0af935ff7360cf77429c5f294cfd59/src/lib.rs#L435 +pub fn get_available_port() -> Result { + // Perform multiple attempts to get a valid port. + for _ in 0..10 { + // Using 0 as port lets the system assign a port available. + let t = TcpListener::bind(("127.0.0.1", 0)) + .map_err(|e| Error::CannotGetAvailablePort(e.to_string()))?; + let port = t + .local_addr() + .map(|s| s.port()) + .map_err(|e| Error::CannotGetAvailablePort(e.to_string()))?; + if port_is_valid(&port) { + return Ok(port); + } + } + Err(Error::CannotGetAvailablePort( + "Exhausted attempts".to_string(), + )) +} + +/// Checks if port is valid for use by internal bitcoind. +pub fn port_is_valid(port: &u16) -> bool { + !BITCOIND_DEFAULT_PORTS.contains(port) +} + +impl Default for SelectBitcoindTypeStep { + fn default() -> Self { + Self::new() + } +} + +impl From for Box { + fn from(s: SelectBitcoindTypeStep) -> Box { + Box::new(s) + } +} + +impl SelectBitcoindTypeStep { + pub fn new() -> Self { + Self { use_external: true } + } +} + +impl Step for SelectBitcoindTypeStep { + fn update(&mut self, message: Message) -> Command { + if let Message::SelectBitcoindType(msg) = message { + match msg { + message::SelectBitcoindTypeMsg::UseExternal(selected) => { + self.use_external = selected; + } + }; + return Command::perform(async {}, |_| Message::Next); + }; + Command::none() + } + + 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. + } + } else { + ctx.internal_bitcoind_config = None; + ctx.internal_bitcoind_exe_config = None; + } + ctx.bitcoind_is_external = self.use_external; + true + } + + fn view(&self, progress: (usize, usize)) -> Element { + view::select_bitcoind_type(progress) + } +} + impl DefineBitcoind { pub fn new() -> Self { Self { @@ -215,6 +490,10 @@ impl Step for DefineBitcoind { fn load(&self) -> Command { self.ping() } + + fn skip(&self, ctx: &Context) -> bool { + !ctx.bitcoind_is_external + } } impl Default for DefineBitcoind { @@ -229,6 +508,240 @@ impl From for Box { } } +impl From for Box { + fn from(s: InternalBitcoindStep) -> Box { + Box::new(s) + } +} + +impl InternalBitcoindStep { + pub fn new(liana_datadir: &PathBuf) -> Self { + Self { + bitcoind_datadir: internal_bitcoind_datadir(liana_datadir), + network: Network::Bitcoin, + started: None, + exe_path: None, + bitcoind_config: None, + exe_config: None, + internal_bitcoind_config: None, + error: None, + } + } +} + +impl Step for InternalBitcoindStep { + fn load_context(&mut self, ctx: &Context) { + if self.exe_path.is_none() { + self.exe_path = if let Some(exe_config) = ctx.internal_bitcoind_exe_config.clone() { + Some(exe_config.exe_path) + } else { + bitcoind_exe_path() + }; + } + self.network = ctx.bitcoin_config.network; + 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() { + self.started = None; // So that internal bitcoind will be restarted. + } + } + } + fn update(&mut self, message: Message) -> Command { + if let Message::InternalBitcoind(msg) = message { + match msg { + message::InternalBitcoindMsg::Previous => { + if self.internal_bitcoind_config.is_some() { + if let Some(bitcoind_config) = &self.bitcoind_config { + stop_internal_bitcoind(bitcoind_config); + } + } + return Command::perform(async {}, |_| Message::Previous); + } + message::InternalBitcoindMsg::Reload => { + return self.load(); + } + message::InternalBitcoindMsg::DefineConfig => { + let mut conf = match InternalBitcoindConfig::from_file( + &internal_bitcoind_config_path(&self.bitcoind_datadir), + ) { + Ok(conf) => conf, + Err(InternalBitcoindConfigError::FileNotFound) => { + InternalBitcoindConfig::new() + } + Err(e) => { + self.error = Some(e.to_string()); + return Command::none(); + } + }; + // Insert entry for network if not present. + if conf.networks.get(&self.network).is_none() { + let network_conf = match (get_available_port(), get_available_port()) { + (Ok(rpc_port), Ok(p2p_port)) => { + // In case ports are the same, user will need to click button again for another attempt. + if rpc_port == p2p_port { + self.error = Some( + "Could not get distinct ports. Please try again." + .to_string(), + ); + return Command::none(); + } + InternalBitcoindNetworkConfig { + rpc_port, + p2p_port, + prune: PRUNE_DEFAULT, + } + } + (Ok(_), Err(e)) | (Err(e), Ok(_)) => { + self.error = Some(format!("Could not get available port: {}.", e)); + return Command::none(); + } + (Err(e1), Err(e2)) => { + self.error = + Some(format!("Could not get available ports: {}; {}.", e1, e2)); + return Command::none(); + } + }; + conf.networks.insert(self.network, network_conf); + } + if let Err(e) = + conf.to_file(&internal_bitcoind_config_path(&self.bitcoind_datadir)) + { + self.error = Some(e.to_string()); + return Command::none(); + }; + self.error = None; + self.internal_bitcoind_config = Some(conf.clone()); + return Command::perform(async {}, |_| { + Message::InternalBitcoind(message::InternalBitcoindMsg::Reload) + }); + } + message::InternalBitcoindMsg::Start => { + if let Some(path) = &self.exe_path { + let datadir = match self.bitcoind_datadir.canonicalize() { + Ok(datadir) => datadir, + Err(e) => { + self.started = Some(Err( + StartInternalBitcoindError::CouldNotCanonicalizeDataDir( + e.to_string(), + ), + )); + return Command::none(); + } + }; + let exe_config = InternalBitcoindExeConfig { + exe_path: path.to_path_buf(), + data_dir: datadir, + }; + if let Err(e) = start_internal_bitcoind(&self.network, exe_config.clone()) { + self.started = + Some(Err(StartInternalBitcoindError::CommandError(e.to_string()))); + return Command::none(); + } + // Need to wait for cookie file to appear. + let cookie_path = + internal_bitcoind_cookie_path(&self.bitcoind_datadir, &self.network); + if !poll_for_file(&cookie_path, 200, 15) { + self.started = + Some(Err(StartInternalBitcoindError::CookieFileNotFound( + cookie_path.to_string_lossy().into_owned(), + ))); + return Command::none(); + } + let rpc_port = self + .internal_bitcoind_config + .as_ref() + .expect("Already added") + .clone() + .networks + .get(&self.network) + .expect("Already added") + .rpc_port; + let bitcoind_config = match cookie_path.canonicalize() { + Ok(cookie_path) => BitcoindConfig { + cookie_path, + addr: internal_bitcoind_address(rpc_port), + }, + Err(e) => { + self.started = Some(Err( + StartInternalBitcoindError::CouldNotCanonicalizeCookiePath( + e.to_string(), + ), + )); + return Command::none(); + } + }; + match liana::BitcoinD::new( + &bitcoind_config, + "internal_bitcoind_connection_check".to_string(), + ) { + Ok(_) => { + self.error = None; + self.bitcoind_config = Some(bitcoind_config); + self.exe_config = Some(exe_config); + self.started = Some(Ok(())); + } + Err(e) => { + self.started = Some(Err( + StartInternalBitcoindError::BitcoinDError(e.to_string()), + )); + } + } + } + } + }; + }; + Command::none() + } + + fn load(&self) -> Command { + if self.internal_bitcoind_config.is_none() { + return Command::perform(async {}, |_| { + Message::InternalBitcoind(message::InternalBitcoindMsg::DefineConfig) + }); + } + if self.started.is_none() { + return Command::perform(async {}, |_| { + Message::InternalBitcoind(message::InternalBitcoindMsg::Start) + }); + } + Command::none() + } + + 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 = self.bitcoind_config.clone(); + ctx.internal_bitcoind_config = self.internal_bitcoind_config.clone(); + ctx.internal_bitcoind_exe_config = self.exe_config.clone(); + self.error = None; + return true; + } + false + } + + fn view(&self, progress: (usize, usize)) -> Element { + view::start_internal_bitcoind( + progress, + self.exe_path.as_ref(), + self.started.as_ref(), + self.error.as_ref(), + ) + } + + fn stop(&self) { + // In case the installer is closed before changes written to context, stop bitcoind. + if let Some(Ok(_)) = self.started { + if let Some(bitcoind_config) = &self.bitcoind_config { + stop_internal_bitcoind(bitcoind_config); + } + } + } + + fn skip(&self, ctx: &Context) -> bool { + ctx.bitcoind_is_external + } +} + pub struct Final { generating: bool, context: Option, @@ -315,3 +828,74 @@ impl From for Box { Box::new(s) } } + +#[cfg(test)] +mod tests { + use crate::installer::step::{InternalBitcoindConfig, InternalBitcoindNetworkConfig}; + use ini::Ini; + use liana::miniscript::bitcoin::Network; + + // Test the format of the internal bitcoind configuration file. + #[test] + fn internal_bitcoind_config() { + // A valid config + let mut conf_ini = Ini::new(); + conf_ini + .with_section(Some("main")) + .set("rpcport", "43345") + .set("port", "42355") + .set("prune", "15246"); + conf_ini + .with_section(Some("regtest")) + .set("rpcport", "34067") + .set("port", "45175") + .set("prune", "2043"); + let conf = InternalBitcoindConfig::from_ini(&conf_ini).expect("Loading conf from ini"); + let main_conf = InternalBitcoindNetworkConfig { + rpc_port: 43345, + p2p_port: 42355, + prune: 15246, + }; + let regtest_conf = InternalBitcoindNetworkConfig { + rpc_port: 34067, + p2p_port: 45175, + prune: 2043, + }; + assert_eq!(conf.networks.len(), 2); + assert_eq!( + conf.networks.get(&Network::Bitcoin).expect("Missing main"), + &main_conf + ); + assert_eq!( + conf.networks + .get(&Network::Regtest) + .expect("Missing regtest"), + ®test_conf + ); + + let mut conf = InternalBitcoindConfig::new(); + conf.networks.insert(Network::Bitcoin, main_conf); + conf.networks.insert(Network::Regtest, regtest_conf); + for (sec, prop) in &conf.to_ini() { + if let Some(sec) = sec { + assert_eq!(prop.len(), 3); + let rpc_port = prop.get("rpcport").expect("rpcport"); + let p2p_port = prop.get("port").expect("port"); + let prune = prop.get("prune").expect("prune"); + if sec == "main" { + assert_eq!(rpc_port, "43345"); + assert_eq!(p2p_port, "42355"); + assert_eq!(prune, "15246"); + } else if sec == "regtest" { + assert_eq!(rpc_port, "34067"); + assert_eq!(p2p_port, "45175"); + assert_eq!(prune, "2043"); + } else { + panic!("Unexpected section"); + } + } else { + assert!(prop.is_empty()) + } + } + } +} diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index a7c0b8056..836856f5f 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -4,6 +4,7 @@ use iced::widget::{ use iced::{alignment, Alignment, Length}; use async_hwi::DeviceKind; +use std::path::PathBuf; use std::{collections::HashSet, str::FromStr}; use liana::miniscript::bitcoin::{self, bip32::Fingerprint}; @@ -20,6 +21,7 @@ use liana_ui::{ }; use crate::{ + bitcoind::StartInternalBitcoindError, hw::HardwareWallet, installer::{ context::Context, @@ -900,6 +902,133 @@ pub fn define_bitcoin<'a>( ) } +pub fn select_bitcoind_type<'a>(progress: (usize, usize)) -> Element<'a, Message> { + layout( + progress, + "Choose Bitcoin installation type", + Column::new().push(text(prompt::SELECT_BITCOIND_TYPE)).push( + Row::new() + .align_items(Alignment::End) + .spacing(20) + .push( + Container::new( + Column::new() + .spacing(20) + .align_items(Alignment::Center) + .push( + button::primary(None, "I want to manage my own node") + .width(Length::Fixed(300.0)) + .on_press(Message::SelectBitcoindType( + message::SelectBitcoindTypeMsg::UseExternal(true), + )), + ) + .align_items(Alignment::Center), + ) + .padding(20), + ) + .push( + Container::new( + Column::new() + .spacing(20) + .align_items(Alignment::Center) + .push( + button::primary(None, "Let Liana manage my node") + .width(Length::Fixed(300.0)) + .on_press(Message::SelectBitcoindType( + message::SelectBitcoindTypeMsg::UseExternal(false), + )), + ) + .align_items(Alignment::Center), + ) + .padding(20), + ), + ), + true, + None, + ) +} + +pub fn start_internal_bitcoind<'a>( + progress: (usize, usize), + exe_path: Option<&PathBuf>, + started: Option<&Result<(), StartInternalBitcoindError>>, + error: Option<&'a String>, +) -> Element<'a, Message> { + let start_button = button::primary(None, "Start bitcoind").width(Length::Fixed(200.0)); + + 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); + }; + layout( + progress, + "Start Bitcoin full node", + Column::new() + .push(if exe_path.is_some() { + Container::new( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push(icon::circle_check_icon().style(color::GREEN)) + .push(text("bitcoind already installed").style(color::GREEN)), + ) + } else { + Container::new( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push(icon::circle_cross_icon().style(color::RED)) + .push(text("Cannot find bitcoind").style(color::RED)), + ) + }) + .push_maybe(if started.is_some() { + started.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("bitcoind started").style(color::GREEN)), + ) + } else { + Container::new( + Row::new() + .spacing(10) + .align_items(Alignment::Center) + .push(icon::circle_cross_icon().style(color::RED)) + .push( + text(res.as_ref().err().unwrap().to_string()).style(color::RED), + ), + ) + } + }) + } else { + Some(Container::new(Space::with_height(Length::Fixed(25.0)))) + }) + .spacing(50) + .push( + Row::new() + .spacing(10) + .push(Container::new( + if exe_path.is_some() && started.is_none() && error.is_none() { + start_button.on_press(Message::InternalBitcoind( + message::InternalBitcoindMsg::Start, + )) + } else { + start_button + }, + )) + .push(Row::new().spacing(10).push(next_button)), + ) + .push_maybe(error.map(|e| card::invalid(text(e)))), + true, + Some(message::Message::InternalBitcoind( + message::InternalBitcoindMsg::Previous, + )), + ) +} + pub fn install<'a>( progress: (usize, usize), context: &Context, diff --git a/gui/src/lib.rs b/gui/src/lib.rs index 2e0de280f..a01f20f29 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +pub mod bitcoind; pub mod daemon; pub mod hw; pub mod installer; diff --git a/gui/src/loader.rs b/gui/src/loader.rs index fb20aebbe..90ad112e4 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use iced::{Alignment, Command, Length, Subscription}; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use liana::{ config::{Config, ConfigError}, @@ -21,10 +21,12 @@ use liana_ui::{ use crate::{ app::{ cache::Cache, - config::Config as GUIConfig, + config::{Config as GUIConfig, InternalBitcoindExeConfig}, wallet::{Wallet, WalletError}, }, + bitcoind::{start_internal_bitcoind, stop_internal_bitcoind, StartInternalBitcoindError}, daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError}, + utils, }; type Lianad = client::Lianad; @@ -89,6 +91,9 @@ impl Loader { daemon: daemon.clone(), progress: 0.0, }; + if self.gui_config.internal_bitcoind_exe_config.is_some() { + warn!("Ignoring internal bitcoind config because Liana daemon is external."); + } return Command::perform(sync(daemon, false), Message::Syncing); } Err(e) => match e { @@ -102,7 +107,10 @@ impl Loader { self.step = Step::StartingDaemon; self.daemon_started = true; return Command::perform( - start_daemon(daemon_config_path), + start_bitcoind_and_daemon( + daemon_config_path, + self.gui_config.internal_bitcoind_exe_config.clone(), + ), Message::Started, ); } else { @@ -173,6 +181,13 @@ impl Loader { info!("Stopping internal daemon..."); daemon.stop(); info!("Internal daemon stopped"); + if self.gui_config.internal_bitcoind_exe_config.is_some() { + if let Some(daemon_config) = daemon.config() { + if let Some(bitcoind_config) = &daemon_config.bitcoind_config { + stop_internal_bitcoind(bitcoind_config); + } + } + } } } } @@ -327,10 +342,37 @@ async fn connect(socket_path: PathBuf) -> Result, } // Daemon can start only if a config path is given. -pub async fn start_daemon(config_path: PathBuf) -> Result, Error> { - debug!("starting liana daemon"); - +pub async fn start_bitcoind_and_daemon( + config_path: PathBuf, + bitcoind_exe_config: Option, +) -> Result, Error> { let config = Config::from_file(Some(config_path)).map_err(Error::Config)?; + if let Some(exe_config) = bitcoind_exe_config { + if let Some(bitcoind_config) = &config.bitcoind_config { + // Check if bitcoind is already running before trying to start it. + if liana::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok() + { + info!("Internal bitcoind is already running"); + } else { + info!("Starting internal bitcoind"); + start_internal_bitcoind(&config.bitcoin_config.network, exe_config) + .map_err(Error::Bitcoind)?; + if !utils::poll_for_file(&bitcoind_config.cookie_path, 200, 15) { + return Err(Error::Bitcoind( + StartInternalBitcoindError::CookieFileNotFound( + bitcoind_config.cookie_path.to_string_lossy().into_owned(), + ), + )); + } + liana::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()) + .map_err(|e| { + Error::Bitcoind(StartInternalBitcoindError::BitcoinDError(e.to_string())) + })?; + } + } + } + + debug!("starting liana daemon"); let daemon = EmbeddedDaemon::start(config)?; @@ -353,6 +395,7 @@ pub enum Error { Wallet(WalletError), Config(ConfigError), Daemon(DaemonError), + Bitcoind(StartInternalBitcoindError), } impl std::fmt::Display for Error { @@ -361,6 +404,7 @@ impl std::fmt::Display for Error { Self::Config(e) => write!(f, "Config error: {}", e), Self::Wallet(e) => write!(f, "Wallet error: {}", e), Self::Daemon(e) => write!(f, "Liana daemon error: {}", e), + Self::Bitcoind(e) => write!(f, "Bitcoind error: {}", e), } } } diff --git a/gui/src/utils/mod.rs b/gui/src/utils/mod.rs index 084f16d95..55b5a4b2b 100644 --- a/gui/src/utils/mod.rs +++ b/gui/src/utils/mod.rs @@ -1,5 +1,21 @@ +use std::path::Path; + #[cfg(test)] pub mod sandbox; #[cfg(test)] pub mod mock; + +/// Polls for a file's existence at given interval up to a maximum number of polls. +/// Returns `true` once file exists and otherwise `false`. +pub fn poll_for_file(path: &Path, interval_millis: u64, max_polls: u16) -> bool { + for i in 0..max_polls { + if path.exists() { + return true; + } + if i < max_polls.saturating_sub(1) { + std::thread::sleep(std::time::Duration::from_millis(interval_millis)); + } + } + false +} From 698eff7059b1efb9ae92d4a3e3f78113f4c9f5b0 Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:38:26 +0100 Subject: [PATCH 3/3] doc: Add step to choose bitcoind type --- doc/TRY.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/TRY.md b/doc/TRY.md index eeaddfd66..1d9ae9c06 100644 --- a/doc/TRY.md +++ b/doc/TRY.md @@ -147,9 +147,18 @@ mnemonic as well as the descriptor in the next two screens. Otherwise just make ticking the boxes. If you are using a signing device simulator you'll have a step for registering the descriptor on it. -Finally, configure the connection to `bitcoind`. The default should work for what we did in this -guide. Click on continue and finalize the installation. - +You can then decide whether you would like to manage `bitcoind` yourself or let Liana configure +and start/stop it while the GUI is being used: +- If you choose to manage `bitcoind` yourself, the next step will be to configure the connection. +The default should work for what we did in this guide. +- If you choose to let Liana manage `bitcoind`, the next step will search for a `bitcoind` +executable on your computer and start it, using `/bitcoind_datadir` as the +data directory and creating a `bitcoin.conf` file therein. + +Click on continue and finalize the installation. + +In the case of a Liana-managed `bitcoind`, it will be automatically started the next time you +start Liana for the given network. ## Step 3: have fun