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.
This commit is contained in:
parent
765c68b02e
commit
36cf85d849
85
gui/Cargo.lock
generated
85
gui/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"]}
|
||||
|
||||
|
||||
@ -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<Vec<HardwareWalletConfig>>,
|
||||
/// Internal bitcoind executable config.
|
||||
pub internal_bitcoind_exe_config: Option<InternalBitcoindExeConfig>,
|
||||
}
|
||||
|
||||
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<InternalBitcoindExeConfig>,
|
||||
) -> 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
gui/src/bitcoind.rs
Normal file
77
gui/src/bitcoind.rs
Normal file
@ -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<std::process::Child, StartInternalBitcoindError> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<Arc<Signer>>,
|
||||
pub bitcoind_is_external: bool,
|
||||
pub internal_bitcoind_config: Option<InternalBitcoindConfig>,
|
||||
pub internal_bitcoind_exe_config: Option<InternalBitcoindExeConfig>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,8 @@ pub enum Message {
|
||||
UseHotSigner,
|
||||
Installed(Result<PathBuf, Error>),
|
||||
Network(Network),
|
||||
SelectBitcoindType(SelectBitcoindTypeMsg),
|
||||
InternalBitcoind(InternalBitcoindMsg),
|
||||
DefineBitcoind(DefineBitcoind),
|
||||
DefineDescriptor(DefineDescriptor),
|
||||
ImportXpub(usize, Result<DescriptorPublicKey, Error>),
|
||||
@ -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),
|
||||
|
||||
@ -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<Message> {
|
||||
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<Mutex<Signer>>) -> Result<PathBuf, Error> {
|
||||
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<Mutex<Signer>>) -> Result<PathBuf
|
||||
daemon_config_path.canonicalize().map_err(|e| {
|
||||
Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e))
|
||||
})?,
|
||||
ctx.internal_bitcoind_exe_config.clone(),
|
||||
))
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
@ -337,6 +366,7 @@ pub enum Error {
|
||||
CannotCreateDatadir(String),
|
||||
CannotCreateFile(String),
|
||||
CannotWriteToFile(String),
|
||||
CannotGetAvailablePort(String),
|
||||
Unexpected(String),
|
||||
HardwareWallet(async_hwi::Error),
|
||||
}
|
||||
@ -364,6 +394,7 @@ impl std::fmt::Display for Error {
|
||||
match self {
|
||||
Self::Bitcoind(e) => 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),
|
||||
|
||||
@ -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.";
|
||||
|
||||
@ -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<Result<(), Error>>,
|
||||
}
|
||||
|
||||
fn bitcoind_default_cookie_path(network: &Network) -> Option<String> {
|
||||
pub struct InternalBitcoindStep {
|
||||
bitcoind_datadir: PathBuf,
|
||||
network: Network,
|
||||
started: Option<Result<(), StartInternalBitcoindError>>,
|
||||
exe_path: Option<PathBuf>,
|
||||
bitcoind_config: Option<BitcoindConfig>,
|
||||
exe_config: Option<InternalBitcoindExeConfig>,
|
||||
internal_bitcoind_config: Option<InternalBitcoindConfig>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
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<Network, InternalBitcoindNetworkConfig>,
|
||||
}
|
||||
|
||||
#[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<Self, InternalBitcoindConfigError> {
|
||||
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::<u16>()
|
||||
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
|
||||
let p2p_port = prop
|
||||
.get("port")
|
||||
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("port".to_string()))?
|
||||
.parse::<u16>()
|
||||
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
|
||||
let prune = prop
|
||||
.get("prune")
|
||||
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("prune".to_string()))?
|
||||
.parse::<u32>()
|
||||
.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<Self, InternalBitcoindConfigError> {
|
||||
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<PathBuf> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let configs_dir = dirs::home_dir();
|
||||
|
||||
@ -78,24 +267,30 @@ fn bitcoind_default_cookie_path(network: &Network) -> Option<String> {
|
||||
#[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<String> {
|
||||
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<String> {
|
||||
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<PathBuf> {
|
||||
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<u16, Error> {
|
||||
// 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<SelectBitcoindTypeStep> for Box<dyn Step> {
|
||||
fn from(s: SelectBitcoindTypeStep) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectBitcoindTypeStep {
|
||||
pub fn new() -> Self {
|
||||
Self { use_external: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for SelectBitcoindTypeStep {
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
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<Message> {
|
||||
view::select_bitcoind_type(progress)
|
||||
}
|
||||
}
|
||||
|
||||
impl DefineBitcoind {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@ -215,6 +490,10 @@ impl Step for DefineBitcoind {
|
||||
fn load(&self) -> Command<Message> {
|
||||
self.ping()
|
||||
}
|
||||
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
!ctx.bitcoind_is_external
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DefineBitcoind {
|
||||
@ -229,6 +508,240 @@ impl From<DefineBitcoind> for Box<dyn Step> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InternalBitcoindStep> for Box<dyn Step> {
|
||||
fn from(s: InternalBitcoindStep) -> Box<dyn Step> {
|
||||
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<Message> {
|
||||
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<Message> {
|
||||
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<Message> {
|
||||
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<Context>,
|
||||
@ -315,3 +828,74 @@ impl From<Final> for Box<dyn Step> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod app;
|
||||
pub mod bitcoind;
|
||||
pub mod daemon;
|
||||
pub mod hw;
|
||||
pub mod installer;
|
||||
|
||||
@ -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<client::jsonrpc::JsonRPCClient>;
|
||||
@ -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<Arc<dyn Daemon + Sync + Send>,
|
||||
}
|
||||
|
||||
// Daemon can start only if a config path is given.
|
||||
pub async fn start_daemon(config_path: PathBuf) -> Result<Arc<dyn Daemon + Sync + Send>, Error> {
|
||||
debug!("starting liana daemon");
|
||||
|
||||
pub async fn start_bitcoind_and_daemon(
|
||||
config_path: PathBuf,
|
||||
bitcoind_exe_config: Option<InternalBitcoindExeConfig>,
|
||||
) -> Result<Arc<dyn Daemon + Sync + Send>, 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user