From bb7777878ea639def09a50f52fdd338467e8a87d Mon Sep 17 00:00:00 2001 From: edouardparis Date: Thu, 24 Apr 2025 16:34:18 +0200 Subject: [PATCH] Use type wrapper instead of PathBuf for directories With the multiplicity of directories, liana directory, network directory and in the future per wallet lianad directories, passing a simple type PathBuf does not transmit to the contributors the information about which directory each module works with. This commit introduces wrappers around PathBuf to give this information and link parent directories to their child directories. --- liana-gui/src/app/cache.rs | 14 +- liana-gui/src/app/config.rs | 29 +---- liana-gui/src/app/mod.rs | 11 +- liana-gui/src/app/settings.rs | 21 ++- liana-gui/src/app/state/psbt.rs | 4 +- liana-gui/src/app/state/receive.rs | 16 ++- liana-gui/src/app/state/settings/mod.rs | 6 +- liana-gui/src/app/state/settings/wallet.rs | 24 ++-- liana-gui/src/app/wallet.rs | 16 +-- liana-gui/src/backup.rs | 8 +- liana-gui/src/daemon/client/mod.rs | 8 +- liana-gui/src/daemon/embedded.rs | 8 +- liana-gui/src/daemon/mod.rs | 7 +- liana-gui/src/datadir.rs | 18 --- liana-gui/src/dir.rs | 123 ++++++++++++++++++ liana-gui/src/export.rs | 20 +-- liana-gui/src/hw.rs | 12 +- liana-gui/src/installer/context.rs | 8 +- liana-gui/src/installer/mod.rs | 91 ++++++------- .../installer/step/descriptor/editor/mod.rs | 11 +- liana-gui/src/installer/step/node/bitcoind.rs | 7 +- liana-gui/src/launcher.rs | 63 ++++----- liana-gui/src/lib.rs | 2 +- liana-gui/src/loader.rs | 34 +++-- liana-gui/src/logger.rs | 11 +- liana-gui/src/main.rs | 51 ++++---- liana-gui/src/node/bitcoind.rs | 24 ++-- .../services/connect/client/backend/mod.rs | 36 ++--- liana-gui/src/services/connect/login.rs | 18 ++- liana-gui/src/signer.rs | 6 +- lianad/src/lib.rs | 4 +- 31 files changed, 420 insertions(+), 291 deletions(-) delete mode 100644 liana-gui/src/datadir.rs create mode 100644 liana-gui/src/dir.rs diff --git a/liana-gui/src/app/cache.rs b/liana-gui/src/app/cache.rs index a6c1f111..a8a64479 100644 --- a/liana-gui/src/app/cache.rs +++ b/liana-gui/src/app/cache.rs @@ -1,15 +1,17 @@ -use crate::daemon::{ - model::{Coin, ListCoinsResult}, - Daemon, DaemonError, +use crate::{ + daemon::{ + model::{Coin, ListCoinsResult}, + Daemon, DaemonError, + }, + dir::LianaDirectory, }; use liana::miniscript::bitcoin::Network; use lianad::commands::CoinStatus; -use std::path::PathBuf; use std::sync::Arc; #[derive(Debug, Clone)] pub struct Cache { - pub datadir_path: PathBuf, + pub datadir_path: LianaDirectory, pub network: Network, pub blockheight: i32, pub coins: Vec, @@ -25,7 +27,7 @@ pub struct Cache { impl std::default::Default for Cache { fn default() -> Self { Self { - datadir_path: std::path::PathBuf::new(), + datadir_path: LianaDirectory::new(std::path::PathBuf::new()), network: Network::Bitcoin, blockheight: 0, coins: Vec::new(), diff --git a/liana-gui/src/app/config.rs b/liana-gui/src/app/config.rs index 0d7b5aaf..def173e8 100644 --- a/liana-gui/src/app/config.rs +++ b/liana-gui/src/app/config.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use std::fs::OpenOptions; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::Path; use tracing_subscriber::filter; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -107,30 +107,3 @@ impl std::fmt::Display for ConfigError { } impl std::error::Error for ConfigError {} - -// Get the absolute path to the liana configuration folder. -/// -/// This a "liana" directory in the XDG standard configuration directory for all OSes but -/// Linux-based ones, for which it's `~/.liana`. -/// Rationale: we want to have the database, RPC socket, etc.. in the same folder as the -/// configuration file but for Linux the XDG specify a data directory (`~/.local/share/`) different -/// from the configuration one (`~/.config/`). -pub fn default_datadir() -> Result> { - #[cfg(target_os = "linux")] - let configs_dir = dirs::home_dir(); - - #[cfg(not(target_os = "linux"))] - let configs_dir = dirs::config_dir(); - - if let Some(mut path) = configs_dir { - #[cfg(target_os = "linux")] - path.push(".liana"); - - #[cfg(not(target_os = "linux"))] - path.push("Liana"); - - return Ok(path); - } - - Err("Failed to get default data directory".into()) -} diff --git a/liana-gui/src/app/mod.rs b/liana-gui/src/app/mod.rs index cdd09c95..894d5e42 100644 --- a/liana-gui/src/app/mod.rs +++ b/liana-gui/src/app/mod.rs @@ -11,7 +11,6 @@ mod error; use std::fs::OpenOptions; use std::io::Write; -use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -37,6 +36,7 @@ use wallet::{sync_status, SyncStatus}; use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, daemon::{embedded::EmbeddedDaemon, Daemon, DaemonBackend}, + dir::LianaDirectory, node::{bitcoind::Bitcoind, NodeType}, }; @@ -58,7 +58,7 @@ impl Panels { fn new( cache: &Cache, wallet: Arc, - data_dir: PathBuf, + data_dir: LianaDirectory, daemon_backend: DaemonBackend, internal_bitcoind: Option<&Bitcoind>, config: Arc, @@ -155,7 +155,7 @@ impl App { wallet: Arc, config: Config, daemon: Arc, - data_dir: PathBuf, + data_dir: LianaDirectory, internal_bitcoind: Option, restored_from_backup: bool, ) -> (App, Task) { @@ -385,15 +385,14 @@ impl App { pub fn load_daemon_config( &mut self, - datadir_path: PathBuf, + datadir_path: LianaDirectory, cfg: DaemonConfig, ) -> Result<(), Error> { Handle::current().block_on(async { self.daemon.stop().await })?; let network = cfg.bitcoin_config.network; let daemon = EmbeddedDaemon::start(cfg)?; self.daemon = Arc::new(daemon); - let mut daemon_config_path = datadir_path; - daemon_config_path.push(network.to_string()); + let mut daemon_config_path = datadir_path.network_directory(network).path().to_path_buf(); daemon_config_path.push("daemon.toml"); let content = diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index 3b10ddc0..8bd0c82a 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -3,14 +3,14 @@ use std::collections::HashMap; use std::fs::OpenOptions; use std::io::Write; -use std::path::PathBuf; -use liana::miniscript::bitcoin::{bip32::Fingerprint, Network}; +use liana::miniscript::bitcoin::bip32::Fingerprint; use liana_ui::component::form; use serde::{Deserialize, Serialize}; use crate::{ backup::{Key, KeyRole, KeyType}, + dir::NetworkDirectory, hw::HardwareWalletConfig, services::{self, connect::client::backend}, }; @@ -23,9 +23,8 @@ pub struct Settings { } impl Settings { - pub fn from_file(datadir: PathBuf, network: Network) -> Result { - let mut path = datadir; - path.push(network.to_string()); + pub fn from_file(network_dir: &NetworkDirectory) -> Result { + let mut path = network_dir.path().to_path_buf(); path.push(DEFAULT_FILE_NAME); let config = std::fs::read(path) @@ -41,9 +40,8 @@ impl Settings { Ok(config) } - pub fn to_file(&self, datadir: PathBuf, network: Network) -> Result<(), SettingsError> { - let mut path = datadir; - path.push(network.to_string()); + pub fn to_file(&self, network_dir: &NetworkDirectory) -> Result<(), SettingsError> { + let mut path = network_dir.path().to_path_buf(); path.push(DEFAULT_FILE_NAME); let content = serde_json::to_string_pretty(&self).map_err(|e| { @@ -257,10 +255,11 @@ impl std::fmt::Display for SettingsError { /// global settings. pub mod global { + use crate::dir::LianaDirectory; use async_hwi::bitbox::{ConfigError, NoiseConfig, NoiseConfigData}; use serde::{Deserialize, Serialize}; use std::io::{Read, Write}; - use std::path::{Path, PathBuf}; + use std::path::PathBuf; pub const DEFAULT_FILE_NAME: &str = "global_settings.json"; @@ -283,9 +282,9 @@ pub mod global { impl PersistedBitboxNoiseConfig { /// Creates a new persisting noise config, which stores the pairing information in "bitbox.json" /// in the provided directory. - pub fn new(global_datadir: &Path) -> PersistedBitboxNoiseConfig { + pub fn new(global_datadir: &LianaDirectory) -> PersistedBitboxNoiseConfig { PersistedBitboxNoiseConfig { - file_path: global_datadir.join(DEFAULT_FILE_NAME), + file_path: global_datadir.path().join(DEFAULT_FILE_NAME), } } } diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index 6f1de0c2..3734ce06 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -1,5 +1,4 @@ use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; use std::sync::Arc; use iced::Subscription; @@ -29,6 +28,7 @@ use crate::{ model::{LabelItem, Labelled, SpendStatus, SpendTx}, Daemon, }, + dir::LianaDirectory, hw::{HardwareWallet, HardwareWallets}, }; @@ -444,7 +444,7 @@ impl SignModal { pub fn new( signed: HashSet, wallet: Arc, - datadir_path: PathBuf, + datadir_path: LianaDirectory, network: Network, is_saved: bool, ) -> Self { diff --git a/liana-gui/src/app/state/receive.rs b/liana-gui/src/app/state/receive.rs index d9dc81ba..9e6c77e7 100644 --- a/liana-gui/src/app/state/receive.rs +++ b/liana-gui/src/app/state/receive.rs @@ -1,5 +1,4 @@ use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; use std::sync::Arc; use iced::{widget::qr_code, Subscription, Task}; @@ -10,6 +9,7 @@ use liana::miniscript::bitcoin::{ use liana_ui::{component::modal, widget::*}; use crate::daemon::model::LabelsLoader; +use crate::dir::LianaDirectory; use crate::{ app::{ cache::Cache, @@ -54,7 +54,7 @@ impl Labelled for Addresses { } pub struct ReceivePanel { - data_dir: PathBuf, + data_dir: LianaDirectory, wallet: Arc, addresses: Addresses, labels_edited: LabelsEdited, @@ -63,7 +63,7 @@ pub struct ReceivePanel { } impl ReceivePanel { - pub fn new(data_dir: PathBuf, wallet: Arc) -> Self { + pub fn new(data_dir: LianaDirectory, wallet: Arc) -> Self { Self { data_dir, wallet, @@ -217,7 +217,7 @@ pub struct VerifyAddressModal { impl VerifyAddressModal { pub fn new( - data_dir: PathBuf, + data_dir: LianaDirectory, wallet: Arc, network: Network, address: Address, @@ -338,7 +338,7 @@ mod tests { use liana::{descriptors::LianaDescriptor, miniscript::bitcoin::Address}; use serde_json::json; - use std::str::FromStr; + use std::{path::PathBuf, str::FromStr}; const DESC: &str = "wsh(or_d(multi(2,[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<0;1>/*,[de6eb005/48'/1'/0'/2']tpubDFGuYfS2JwiUSEXiQuNGdT3R7WTDhbaE6jbUhgYSSdhmfQcSx7ZntMPPv7nrkvAqjpj3jX9wbhSGMeKVao4qAzhbNyBi7iQmv5xxQk6H6jz/<0;1>/*),and_v(v:pkh([ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<2;3>/*),older(3))))#p9ax3xxp"; @@ -356,8 +356,10 @@ mod tests { ))), )]); let wallet = Arc::new(Wallet::new(LianaDescriptor::from_str(DESC).unwrap())); - let sandbox: Sandbox = - Sandbox::new(ReceivePanel::new(PathBuf::new(), wallet.clone())); + let sandbox: Sandbox = Sandbox::new(ReceivePanel::new( + LianaDirectory::new(PathBuf::new()), + wallet.clone(), + )); let client = Arc::new(Lianad::new(daemon.run())); let cache = Cache::default(); let sandbox = sandbox.load(client.clone(), &cache, wallet).await; diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index cefb712d..b5eb1aea 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -2,7 +2,6 @@ mod bitcoind; mod wallet; use std::convert::From; -use std::path::PathBuf; use std::sync::Arc; use iced::Task; @@ -23,13 +22,14 @@ use crate::{ Config, }, daemon::{Daemon, DaemonBackend}, + dir::LianaDirectory, export::{ImportExportMessage, ImportExportType}, }; use super::export::ExportModal; pub struct SettingsState { - data_dir: PathBuf, + data_dir: LianaDirectory, wallet: Arc, setting: Option>, daemon_backend: DaemonBackend, @@ -39,7 +39,7 @@ pub struct SettingsState { impl SettingsState { pub fn new( - data_dir: PathBuf, + data_dir: LianaDirectory, wallet: Arc, daemon_backend: DaemonBackend, internal_bitcoind: bool, diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index d8b24434..e7c59629 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -1,6 +1,5 @@ use std::collections::HashSet; use std::convert::From; -use std::path::PathBuf; use std::sync::Arc; use iced::{Subscription, Task}; @@ -27,6 +26,7 @@ use crate::{ Config, }, daemon::{Daemon, DaemonBackend}, + dir::LianaDirectory, export::{ImportExportMessage, ImportExportType}, hw::{HardwareWallet, HardwareWalletConfig, HardwareWallets}, }; @@ -44,7 +44,7 @@ impl Modal { } pub struct WalletSettingsState { - data_dir: PathBuf, + data_dir: LianaDirectory, warning: Option, descriptor: LianaDescriptor, keys_aliases: Vec<(Fingerprint, form::Value)>, @@ -56,7 +56,7 @@ pub struct WalletSettingsState { } impl WalletSettingsState { - pub fn new(data_dir: PathBuf, wallet: Arc, config: Arc) -> Self { + pub fn new(data_dir: LianaDirectory, wallet: Arc, config: Arc) -> Self { WalletSettingsState { data_dir, descriptor: wallet.main_descriptor.clone(), @@ -296,7 +296,7 @@ impl From for Box { } pub struct RegisterWalletModal { - data_dir: PathBuf, + data_dir: LianaDirectory, wallet: Arc, warning: Option, chosen_hw: Option, @@ -306,7 +306,7 @@ pub struct RegisterWalletModal { } impl RegisterWalletModal { - pub fn new(data_dir: PathBuf, wallet: Arc, network: Network) -> Self { + pub fn new(data_dir: LianaDirectory, wallet: Arc, network: Network) -> Self { let mut registered = HashSet::new(); for hw in &wallet.hardware_wallets { registered.insert(hw.fingerprint); @@ -406,7 +406,7 @@ impl RegisterWalletModal { } async fn register_wallet( - data_dir: PathBuf, + data_dir: LianaDirectory, network: Network, hw: std::sync::Arc, fingerprint: Fingerprint, @@ -427,7 +427,8 @@ async fn register_wallet( }; if daemon.backend() != DaemonBackend::RemoteBackend { - let mut settings = settings::Settings::from_file(data_dir.clone(), network)?; + let network_dir = data_dir.network_directory(network); + let mut settings = settings::Settings::from_file(&network_dir)?; let checksum = wallet.descriptor_checksum(); if let Some(wallet_setting) = settings @@ -446,7 +447,7 @@ async fn register_wallet( } } - settings.to_file(data_dir, network)?; + settings.to_file(&network_dir)?; } let mut wallet = wallet.as_ref().clone(); @@ -469,14 +470,15 @@ async fn register_wallet( } pub async fn update_keys_aliases( - data_dir: PathBuf, + data_dir: LianaDirectory, network: Network, wallet: Arc, keys_aliases: Vec<(Fingerprint, String)>, daemon: Arc, ) -> Result, Error> { if daemon.backend() != DaemonBackend::RemoteBackend { - let mut settings = settings::Settings::from_file(data_dir.clone(), network)?; + let network_dir = data_dir.network_directory(network); + let mut settings = settings::Settings::from_file(&network_dir)?; let checksum = wallet.descriptor_checksum(); if let Some(wallet_setting) = settings .wallets @@ -493,7 +495,7 @@ pub async fn update_keys_aliases( .collect(); } - settings.to_file(data_dir, network)?; + settings.to_file(&network_dir)?; } let mut wallet = wallet.as_ref().clone(); diff --git a/liana-gui/src/app/wallet.rs b/liana-gui/src/app/wallet.rs index 52a095f1..c1f0fb72 100644 --- a/liana-gui/src/app/wallet.rs +++ b/liana-gui/src/app/wallet.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; -use std::path::Path; use std::sync::Arc; +use crate::dir::{LianaDirectory, NetworkDirectory}; use crate::{ app::settings, daemon::DaemonBackend, hw::HardwareWalletConfig, node::NodeType, signer::Signer, }; @@ -101,12 +101,8 @@ impl Wallet { .to_string() } - pub fn load_from_settings( - self, - datadir_path: &Path, - network: bitcoin::Network, - ) -> Result { - let wallet = match settings::Settings::from_file(datadir_path.to_path_buf(), network) { + pub fn load_from_settings(self, dir: &NetworkDirectory) -> Result { + let wallet = match settings::Settings::from_file(dir) { Ok(settings) => { if let Some(wallet_setting) = settings.wallets.first() { self.with_name(wallet_setting.name.clone()) @@ -140,7 +136,7 @@ impl Wallet { }; tracing::info!("Settings file not found, creating one"); - s.to_file(datadir_path.to_path_buf(), network)?; + s.to_file(dir)?; self } Err(e) => return Err(e.into()), @@ -151,10 +147,10 @@ impl Wallet { pub fn load_hotsigners( self, - datadir_path: &Path, + datadir_path: &LianaDirectory, network: bitcoin::Network, ) -> Result { - let hot_signers = match HotSigner::from_datadir(datadir_path, network) { + let hot_signers = match HotSigner::from_datadir(datadir_path.path(), network) { Ok(signers) => signers, Err(e) => match e { liana::signer::SignerError::MnemonicStorage(e) => { diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index e3c477d6..0dc4ae64 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -12,7 +12,6 @@ use serde_json::Value; use std::{ collections::{BTreeMap, HashMap}, fmt::{Debug, Display}, - path::PathBuf, sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; @@ -21,6 +20,7 @@ use tokio::sync::mpsc::UnboundedSender; use crate::{ app::{settings::Settings, wallet::Wallet, Config}, daemon::{model::HistoryTransaction, Daemon, DaemonBackend, DaemonError}, + dir::LianaDirectory, export::Progress, installer::{ extract_daemon_config, extract_local_gui_settings, extract_remote_gui_settings, Context, @@ -180,7 +180,7 @@ impl Backup { /// Create a Backup from the Liana App context pub async fn from_app( - datadir: PathBuf, + datadir: LianaDirectory, network: Network, config: Arc, wallet: Arc, @@ -194,8 +194,8 @@ impl Backup { let descriptor = wallet.main_descriptor.to_string(); let keys = wallet.keys(); - let settings = - Settings::from_file(datadir, network).map_err(|_| Error::SettingsFromFile)?; + let network_dir = datadir.network_directory(network); + let settings = Settings::from_file(&network_dir).map_err(|_| Error::SettingsFromFile)?; if settings.wallets.len() == 1 { if let Ok(settings) = serde_json::to_value(settings.wallets[0].clone()) { proprietary.insert(SETTINGS_KEY.to_string(), settings); diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 266dbb07..376aa3fa 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -1,7 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::iter::FromIterator; -use std::path::Path; use async_trait::async_trait; use lianad::bip329::Labels; @@ -21,6 +20,7 @@ use lianad::{ }; use super::{model::*, Daemon, DaemonBackend, DaemonError}; +use crate::dir::LianaDirectory; pub trait Client { type Error: Into + Debug; @@ -65,7 +65,11 @@ impl Daemon for Lianad { None } - async fn is_alive(&self, _datadir: &Path, _network: Network) -> Result<(), DaemonError> { + async fn is_alive( + &self, + _datadir: &LianaDirectory, + _network: Network, + ) -> Result<(), DaemonError> { Ok(()) } diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index e87bbb24..c86483f4 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,10 +1,10 @@ use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; use std::collections::{HashMap, HashSet}; -use std::path::Path; use tokio::sync::Mutex; use super::{model::*, node, Daemon, DaemonBackend, DaemonError}; +use crate::dir::LianaDirectory; use async_trait::async_trait; use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}; use lianad::{ @@ -67,7 +67,11 @@ impl Daemon for EmbeddedDaemon { Some(&self.config) } - async fn is_alive(&self, _datadir: &Path, _network: Network) -> Result<(), DaemonError> { + async fn is_alive( + &self, + _datadir: &LianaDirectory, + _network: Network, + ) -> Result<(), DaemonError> { let mut handle = self.handle.lock().await; if let Some(h) = handle.as_ref() { if h.is_alive() { diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 5b961c3b..4afea243 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -7,7 +7,6 @@ use std::convert::TryInto; use std::fmt::Debug; use std::io::ErrorKind; use std::iter::FromIterator; -use std::path::Path; use async_trait::async_trait; @@ -97,7 +96,11 @@ impl DaemonBackend { pub trait Daemon: Debug { fn backend(&self) -> DaemonBackend; fn config(&self) -> Option<&Config>; - async fn is_alive(&self, datadir: &Path, network: Network) -> Result<(), DaemonError>; + async fn is_alive( + &self, + datadir: &crate::dir::LianaDirectory, + network: Network, + ) -> Result<(), DaemonError>; async fn stop(&self) -> Result<(), DaemonError>; async fn get_info(&self) -> Result; async fn get_new_address(&self) -> Result; diff --git a/liana-gui/src/datadir.rs b/liana-gui/src/datadir.rs deleted file mode 100644 index 4e906700..00000000 --- a/liana-gui/src/datadir.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub fn create_directory(datadir_path: &std::path::Path) -> Result<(), Box> { - #[cfg(unix)] - return { - use std::fs::DirBuilder; - use std::os::unix::fs::DirBuilderExt; - - let mut builder = DirBuilder::new(); - builder.mode(0o700).recursive(true).create(datadir_path)?; - Ok(()) - }; - - // TODO: permissions on Windows.. - #[cfg(not(unix))] - return { - std::fs::create_dir_all(datadir_path)?; - Ok(()) - }; -} diff --git a/liana-gui/src/dir.rs b/liana-gui/src/dir.rs new file mode 100644 index 00000000..5b498704 --- /dev/null +++ b/liana-gui/src/dir.rs @@ -0,0 +1,123 @@ +use liana::miniscript::bitcoin::Network; +use std::path::{Path, PathBuf}; + +#[derive(Clone, Debug, PartialEq)] +pub struct LianaDirectory(PathBuf); + +impl LianaDirectory { + pub fn new(p: PathBuf) -> Self { + LianaDirectory(p) + } + pub fn new_default() -> Result> { + default_datadir().map(LianaDirectory::new) + } +} + +impl LianaDirectory { + pub fn exists(&self) -> bool { + self.0.as_path().exists() + } + pub fn init(&self) -> Result<(), Box> { + create_directory(self.0.as_path()) + } + pub fn path(&self) -> &Path { + self.0.as_path() + } + + pub fn network_directory(&self, network: Network) -> NetworkDirectory { + let mut path = self.0.clone(); + path.push(network.to_string()); + NetworkDirectory::new(path) + } + + pub fn bitcoind_directory(&self) -> BitcoindDirectory { + let mut path = self.0.clone(); + path.push("bitcoind"); + BitcoindDirectory::new(path) + } +} + +// Get the absolute path to the liana configuration folder. +/// +/// This a "liana" directory in the XDG standard configuration directory for all OSes but +/// Linux-based ones, for which it's `~/.liana`. +/// Rationale: we want to have the database, RPC socket, etc.. in the same folder as the +/// configuration file but for Linux the XDG specify a data directory (`~/.local/share/`) different +/// from the configuration one (`~/.config/`). +fn default_datadir() -> Result> { + #[cfg(target_os = "linux")] + let configs_dir = dirs::home_dir(); + + #[cfg(not(target_os = "linux"))] + let configs_dir = dirs::config_dir(); + + if let Some(mut path) = configs_dir { + #[cfg(target_os = "linux")] + path.push(".liana"); + + #[cfg(not(target_os = "linux"))] + path.push("Liana"); + + return Ok(path); + } + + Err("Failed to get default data directory".into()) +} + +#[derive(Clone, Debug)] +pub struct NetworkDirectory(PathBuf); + +impl NetworkDirectory { + pub fn new(p: PathBuf) -> Self { + NetworkDirectory(p) + } +} + +impl NetworkDirectory { + pub fn exists(&self) -> bool { + self.0.as_path().exists() + } + pub fn init(&self) -> Result<(), Box> { + create_directory(self.0.as_path()) + } + pub fn path(&self) -> &Path { + self.0.as_path() + } +} + +#[derive(Clone, Debug)] +pub struct BitcoindDirectory(PathBuf); + +impl BitcoindDirectory { + pub fn new(p: PathBuf) -> Self { + BitcoindDirectory(p) + } + pub fn exists(&self) -> bool { + self.0.as_path().exists() + } + pub fn init(&self) -> Result<(), Box> { + create_directory(self.0.as_path()) + } + pub fn path(&self) -> &Path { + self.0.as_path() + } +} + +fn create_directory(datadir_path: &std::path::Path) -> Result<(), Box> { + #[cfg(unix)] + return { + use std::fs::DirBuilder; + use std::os::unix::fs::DirBuilderExt; + + let mut builder = DirBuilder::new(); + builder.mode(0o700).recursive(true).create(datadir_path)?; + Ok(()) + }; + + // TODO: permissions on Windows.. + #[cfg(not(unix))] + return { + std::fs::create_dir_all(datadir_path)?; + Ok(()) + }; +} diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 9a605497..0c52209a 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -44,6 +44,7 @@ use crate::{ model::{HistoryTransaction, Labelled}, Daemon, DaemonBackend, DaemonError, }, + dir::{LianaDirectory, NetworkDirectory}, node::bitcoind::Bitcoind, services::connect::client::backend::api::DEFAULT_LIMIT, }; @@ -163,7 +164,7 @@ pub enum ImportExportType { ExportPsbt(String), ExportXpub(String), ExportBackup(String), - ExportProcessBackup(PathBuf, Network, Arc, Arc), + ExportProcessBackup(LianaDirectory, Network, Arc, Arc), ImportBackup( Option>, /*overwrite_labels*/ Option>, /*overwrite_aliases*/ @@ -819,7 +820,8 @@ pub async fn import_backup( let settings = if !account.keys.is_empty() { // TODO: change lianad_datadir is common to gui datadir only for legacy wallet before // multiple wallet - let settings = match Settings::from_file(lianad_datadir.path().to_path_buf(), network) { + let network_dir = NetworkDirectory::new(lianad_datadir.path().to_path_buf()); + let settings = match Settings::from_file(&network_dir) { Ok(s) => s, Err(_) => { return Err(Error::BackupImport("Failed to get App Settings".into())); @@ -938,10 +940,8 @@ pub async fn import_backup( settings.wallets.get_mut(0).expect("already checked").keys = settings_aliases.clone().into_values().collect(); - if settings - .to_file(lianad_datadir.path().to_path_buf(), network) - .is_err() - { + let network_dir = NetworkDirectory::new(lianad_datadir.path().to_path_buf()); + if settings.to_file(&network_dir).is_err() { return Err(Error::BackupImport("Failed to import keys aliases".into())); } else { // Update wallet state @@ -1085,7 +1085,7 @@ pub async fn import_backup_at_launch( wallet: Arc, config: Config, daemon: Arc, - datadir: PathBuf, + datadir: LianaDirectory, internal_bitcoind: Option, backup: Backup, ) -> Result< @@ -1094,7 +1094,7 @@ pub async fn import_backup_at_launch( Arc, Config, Arc, - PathBuf, + LianaDirectory, Option, ), RestoreBackupError, @@ -1231,7 +1231,7 @@ pub async fn get_path(filename: String, write: bool) -> Option { } pub async fn app_backup( - datadir: PathBuf, + datadir: LianaDirectory, network: Network, config: Arc, wallet: Arc, @@ -1243,7 +1243,7 @@ pub async fn app_backup( } pub async fn app_backup_export( - datadir: PathBuf, + datadir: LianaDirectory, network: Network, config: Arc, wallet: Arc, diff --git a/liana-gui/src/hw.rs b/liana-gui/src/hw.rs index 5ed01b60..52b65336 100644 --- a/liana-gui/src/hw.rs +++ b/liana-gui/src/hw.rs @@ -1,11 +1,13 @@ use iced::Task; use std::{ collections::HashMap, - path::PathBuf, sync::{Arc, Mutex}, }; -use crate::app::{settings, wallet::Wallet}; +use crate::{ + app::{settings, wallet::Wallet}, + dir::LianaDirectory, +}; use async_hwi::{ bitbox::{api::runtime, BitBox02, PairingBitbox02}, coldcard, @@ -156,7 +158,7 @@ pub struct HardwareWallets { pub list: Vec, pub aliases: HashMap, wallet: Option>, - datadir_path: PathBuf, + datadir_path: LianaDirectory, } impl std::fmt::Debug for HardwareWallets { @@ -166,7 +168,7 @@ impl std::fmt::Debug for HardwareWallets { } impl HardwareWallets { - pub fn new(datadir_path: PathBuf, network: Network) -> Self { + pub fn new(datadir_path: LianaDirectory, network: Network) -> Self { Self { network, list: Vec::new(), @@ -380,7 +382,7 @@ struct State { wallet: Option>, connected_supported_hws: Vec, api: Option, - datadir_path: PathBuf, + datadir_path: LianaDirectory, } fn refresh(mut state: State) -> impl Stream { diff --git a/liana-gui/src/installer/context.rs b/liana-gui/src/installer/context.rs index 0134e7a1..811985a9 100644 --- a/liana-gui/src/installer/context.rs +++ b/liana-gui/src/installer/context.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; -use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use crate::{ app::settings::KeySetting, backup::Backup, + dir::LianaDirectory, node::bitcoind::{Bitcoind, InternalBitcoindConfig}, services::connect::client::backend::{BackendClient, BackendWalletClient}, signer::Signer, @@ -60,7 +60,7 @@ pub struct Context { pub descriptor: Option, pub keys: HashMap, pub hws: Vec<(DeviceKind, bitcoin::bip32::Fingerprint, Option<[u8; 32]>)>, - pub root_directory: PathBuf, + pub liana_directory: LianaDirectory, pub network: bitcoin::Network, pub hw_is_used: bool, // In case a user entered a mnemonic, @@ -76,7 +76,7 @@ pub struct Context { impl Context { pub fn new( network: bitcoin::Network, - root_directory: PathBuf, + liana_directory: LianaDirectory, remote_backend: RemoteBackend, ) -> Self { Self { @@ -89,7 +89,7 @@ impl Context { keys: HashMap::new(), bitcoin_backend: None, descriptor: None, - root_directory, + liana_directory, network, hw_is_used: false, recovered_signer: None, diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index c69ac611..d1502105 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -27,7 +27,7 @@ use crate::{ }, backup, daemon::DaemonError, - datadir::create_directory, + dir::{LianaDirectory, NetworkDirectory}, hw::{HardwareWalletConfig, HardwareWallets}, services::{ self, @@ -60,7 +60,7 @@ pub enum UserFlow { pub struct Installer { pub network: bitcoin::Network, - pub datadir: PathBuf, + pub datadir: LianaDirectory, current: usize, steps: Vec>, @@ -101,7 +101,7 @@ impl Installer { } pub fn new( - destination_path: PathBuf, + destination_path: LianaDirectory, network: bitcoin::Network, remote_backend: Option, user_flow: UserFlow, @@ -135,7 +135,7 @@ impl Installer { ChooseBackend::new(network).into(), RemoteBackendLogin::new(network).into(), SelectBitcoindTypeStep::new().into(), - InternalBitcoindStep::new(&context.root_directory).into(), + InternalBitcoindStep::new(&context.liana_directory).into(), DefineNode::default().into(), Final::new().into(), ], @@ -148,7 +148,7 @@ impl Installer { RecoverMnemonic::default().into(), RegisterDescriptor::new_import_wallet().into(), SelectBitcoindTypeStep::new().into(), - InternalBitcoindStep::new(&context.root_directory).into(), + InternalBitcoindStep::new(&context.liana_directory).into(), DefineNode::default().into(), Final::new().into(), ], @@ -168,8 +168,8 @@ impl Installer { (installer, command) } - pub fn destination_path(&self) -> PathBuf { - self.context.root_directory.clone() + pub fn destination_path(&self) -> LianaDirectory { + self.context.liana_directory.clone() } pub fn subscription(&self) -> Subscription { @@ -272,21 +272,23 @@ impl Installer { } } Message::Installed(Err(e)) => { - let mut network_directory = self.context.root_directory.clone(); - network_directory.push(self.context.bitcoin_config.network.to_string()); + let network_directory = self + .context + .liana_directory + .network_directory(self.context.bitcoin_config.network); // In case of failure during install, block the thread to // deleted the data_dir/network directory in order to start clean again. warn!("Installation failed. Cleaning up the leftover data directory."); - if let Err(e) = std::fs::remove_dir_all(&network_directory) { + if let Err(e) = std::fs::remove_dir_all(network_directory.path()) { error!( "Failed to completely delete the data directory (path: '{}'): {}", - network_directory.to_string_lossy(), + network_directory.path().to_string_lossy(), e ); } else { warn!( "Successfully deleted data directory at '{}'.", - network_directory.to_string_lossy() + network_directory.path().to_string_lossy() ); }; self.steps @@ -360,24 +362,26 @@ pub async fn install_local_wallet( ctx: Context, signer: Arc>, ) -> Result { + let network_datadir = ctx + .liana_directory + .network_directory(ctx.bitcoin_config.network); + network_datadir + .init() + .map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?; + let cfg: lianad::config::Config = extract_daemon_config(&ctx)?; daemon_check(cfg.clone())?; info!("daemon checked"); - let mut network_datadir_path = ctx.root_directory.clone(); - network_datadir_path.push(cfg.bitcoin_config.network.to_string()); - create_directory(&network_datadir_path) - .map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?; - // Step needed because of ValueAfterTable error in the toml serialize implementation. let daemon_config = toml::Value::try_from(&cfg) .map_err(|e| Error::Unexpected(format!("Failed to serialize daemon config: {}", e)))?; // create lianad configuration file let _daemon_config_path = create_and_write_file( - network_datadir_path.clone(), + &network_datadir, "daemon.toml", daemon_config.to_string().as_bytes(), )?; @@ -392,7 +396,7 @@ pub async fn install_local_wallet( signer .lock() .unwrap() - .store(&ctx.root_directory, cfg.bitcoin_config.network) + .store(&ctx.liana_directory, cfg.bitcoin_config.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Hot signer mnemonic stored"); @@ -400,7 +404,7 @@ pub async fn install_local_wallet( if let Some(signer) = &ctx.recovered_signer { signer - .store(&ctx.root_directory, cfg.bitcoin_config.network) + .store(&ctx.liana_directory, cfg.bitcoin_config.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Recovered signer mnemonic stored"); @@ -408,7 +412,7 @@ pub async fn install_local_wallet( // create liana GUI configuration file let gui_config_path = create_and_write_file( - network_datadir_path.clone(), + &network_datadir, gui_config::DEFAULT_FILE_NAME, toml::to_string(&gui_config::Config::new( // Installer started a bitcoind, it is expected that gui will start it on startup @@ -423,7 +427,7 @@ pub async fn install_local_wallet( // create liana GUI settings file let settings: gui_settings::Settings = extract_local_gui_settings(&ctx); create_and_write_file( - network_datadir_path, + &network_datadir, gui_settings::DEFAULT_FILE_NAME, serde_json::to_string_pretty(&settings) .map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))? @@ -440,9 +444,9 @@ pub async fn create_remote_wallet( signer: Arc>, remote_backend: BackendClient, ) -> Result { - let mut network_datadir_path = ctx.root_directory.clone(); - network_datadir_path.push(ctx.network.to_string()); - create_directory(&network_datadir_path) + let network_datadir = ctx.liana_directory.network_directory(ctx.network); + network_datadir + .init() .map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?; let descriptor = ctx @@ -457,7 +461,7 @@ pub async fn create_remote_wallet( signer .lock() .unwrap() - .store(&ctx.root_directory, ctx.network) + .store(&ctx.liana_directory, ctx.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Hot signer mnemonic stored"); @@ -465,18 +469,15 @@ pub async fn create_remote_wallet( if let Some(signer) = &ctx.recovered_signer { signer - .store(&ctx.root_directory, ctx.network) + .store(&ctx.liana_directory, ctx.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Recovered signer mnemonic stored"); } - let mut network_datadir_path = ctx.root_directory.clone(); - network_datadir_path.push(ctx.network.to_string()); - // create liana GUI configuration file let gui_config_path = create_and_write_file( - network_datadir_path.clone(), + &network_datadir, gui_config::DEFAULT_FILE_NAME, toml::to_string(&gui_config::Config { log_level: Some("info".to_string()), @@ -540,7 +541,7 @@ pub async fn create_remote_wallet( // create liana GUI settings file let settings: gui_settings::Settings = extract_remote_gui_settings(&ctx, &remote_backend).await; create_and_write_file( - network_datadir_path.clone(), + &network_datadir, gui_settings::DEFAULT_FILE_NAME, serde_json::to_string_pretty(&settings) .map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))? @@ -560,21 +561,21 @@ pub async fn import_remote_wallet( if let Some(signer) = &ctx.recovered_signer { signer - .store(&ctx.root_directory, ctx.network) + .store(&ctx.liana_directory, ctx.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Recovered signer mnemonic stored"); } - let mut network_datadir_path = ctx.root_directory.clone(); - network_datadir_path.push(ctx.network.to_string()); - create_directory(&network_datadir_path) + let network_datadir = ctx.liana_directory.network_directory(ctx.network); + network_datadir + .init() .map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?; // create liana GUI settings file let settings: gui_settings::Settings = extract_remote_gui_settings(&ctx, &backend).await; create_and_write_file( - network_datadir_path.clone(), + &network_datadir, gui_settings::DEFAULT_FILE_NAME, serde_json::to_string_pretty(&settings) .map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))? @@ -585,7 +586,7 @@ pub async fn import_remote_wallet( // create liana GUI configuration file let gui_config_path = create_and_write_file( - network_datadir_path.clone(), + &network_datadir, gui_config::DEFAULT_FILE_NAME, toml::to_string(&gui_config::Config { log_level: Some("info".to_string()), @@ -602,12 +603,12 @@ pub async fn import_remote_wallet( } pub fn create_and_write_file( - mut network_datadir: PathBuf, + network_datadir: &NetworkDirectory, file_name: &str, data: &[u8], ) -> Result { - network_datadir.push(file_name); - let path = network_datadir; + let mut path = network_datadir.path().to_path_buf(); + path.push(file_name); let mut file = std::fs::File::create(&path).map_err(|e| Error::CannotCreateFile(e.to_string()))?; file.write_all(data) @@ -681,11 +682,13 @@ pub fn extract_local_gui_settings(ctx: &Context) -> Settings { } pub fn extract_daemon_config(ctx: &Context) -> Result { - let mut data_directory = ctx - .root_directory + let data_directory = ctx + .liana_directory + .network_directory(ctx.bitcoin_config.network) + .path() + .to_path_buf() .canonicalize() .map_err(|e| Error::Unexpected(format!("Failed to canonicalize datadir path: {}", e)))?; - data_directory.push(ctx.bitcoin_config.network.to_string()); Ok(Config::new( ctx.bitcoin_config.clone(), ctx.bitcoin_backend.clone(), diff --git a/liana-gui/src/installer/step/descriptor/editor/mod.rs b/liana-gui/src/installer/step/descriptor/editor/mod.rs index fdd47bf7..45bc6b31 100644 --- a/liana-gui/src/installer/step/descriptor/editor/mod.rs +++ b/liana-gui/src/installer/step/descriptor/editor/mod.rs @@ -627,7 +627,7 @@ mod tests { use std::path::PathBuf; use std::sync::{Arc, Mutex}; - use crate::installer::descriptor::KeySource; + use crate::{dir::LianaDirectory, installer::descriptor::KeySource}; pub struct Sandbox { step: Arc>, @@ -646,7 +646,10 @@ mod tests { } pub async fn update(&self, message: Message) { - let mut hws = HardwareWallets::new(PathBuf::from_str("/").unwrap(), Network::Bitcoin); + let mut hws = HardwareWallets::new( + LianaDirectory::new(PathBuf::from_str("/").unwrap()), + Network::Bitcoin, + ); let cmd = self.step.lock().unwrap().update(&mut hws, message); if let Some(mut stream) = into_stream(cmd) { while let Some(action) = stream.next().await { @@ -665,7 +668,7 @@ mod tests { async fn test_define_descriptor_use_hotkey() { let mut ctx = Context::new( Network::Signet, - PathBuf::from_str("/").unwrap(), + LianaDirectory::new(PathBuf::from_str("/").unwrap()), crate::installer::context::RemoteBackend::None, ); let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new( @@ -746,7 +749,7 @@ mod tests { async fn test_define_descriptor_stores_if_hw_is_used() { let mut ctx = Context::new( Network::Testnet, - PathBuf::from_str("/").unwrap(), + LianaDirectory::new(PathBuf::from_str("/").unwrap()), crate::installer::context::RemoteBackend::None, ); let sandbox: Sandbox = Sandbox::new(DefineDescriptor::new( diff --git a/liana-gui/src/installer/step/node/bitcoind.rs b/liana-gui/src/installer/step/node/bitcoind.rs index 95c111ee..807d7a50 100644 --- a/liana-gui/src/installer/step/node/bitcoind.rs +++ b/liana-gui/src/installer/step/node/bitcoind.rs @@ -18,6 +18,7 @@ use jsonrpc::{client::Client, simple_http::SimpleHttpTransport}; use liana_ui::{component::form, widget::*}; +use crate::dir::LianaDirectory; use crate::{ download, hw::HardwareWallets, @@ -489,7 +490,7 @@ impl Default for DefineBitcoind { } pub struct InternalBitcoindStep { - liana_datadir: PathBuf, + liana_datadir: LianaDirectory, bitcoind_datadir: PathBuf, network: Network, started: Option>, @@ -509,7 +510,7 @@ impl From for Box { } impl InternalBitcoindStep { - pub fn new(liana_datadir: &PathBuf) -> Self { + pub fn new(liana_datadir: &LianaDirectory) -> Self { Self { liana_datadir: liana_datadir.clone(), bitcoind_datadir: internal_bitcoind_datadir(liana_datadir), @@ -531,7 +532,7 @@ impl Step for InternalBitcoindStep { if self.exe_path.is_none() { // Check if current managed bitcoind version is already installed. // For new installations, we ignore any previous managed bitcoind versions that might be installed. - let exe_path = bitcoind::internal_bitcoind_exe_path(&ctx.root_directory, VERSION); + let exe_path = bitcoind::internal_bitcoind_exe_path(&ctx.liana_directory, VERSION); if exe_path.exists() { self.exe_path = Some(exe_path) } else if self.exe_download.is_none() { diff --git a/liana-gui/src/launcher.rs b/liana-gui/src/launcher.rs index 811af462..42e56b95 100644 --- a/liana-gui/src/launcher.rs +++ b/liana-gui/src/launcher.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use iced::{ alignment::Horizontal, widget::{pick_list, scrollable, Button, Space}, @@ -14,7 +12,11 @@ use liana_ui::{ }; use lianad::config::ConfigError; -use crate::{app, installer::UserFlow}; +use crate::{ + app, + dir::{LianaDirectory, NetworkDirectory}, + installer::UserFlow, +}; const NETWORKS: [Network; 4] = [ Network::Bitcoin, @@ -37,20 +39,21 @@ pub enum State { pub struct Launcher { state: State, network: Network, - datadir_path: PathBuf, + datadir_path: LianaDirectory, error: Option, delete_wallet_modal: Option, } impl Launcher { - pub fn new(datadir_path: PathBuf, network: Option) -> (Self, Task) { + pub fn new(datadir_path: LianaDirectory, network: Option) -> (Self, Task) { let network = network.unwrap_or( NETWORKS .iter() - .find(|net| datadir_path.join(net.to_string()).exists()) + .find(|net| datadir_path.path().join(net.to_string()).exists()) .cloned() .unwrap_or(Network::Bitcoin), ); + let network_dir = datadir_path.network_directory(network); ( Self { state: State::Unchecked, @@ -59,10 +62,7 @@ impl Launcher { error: None, delete_wallet_modal: None, }, - Task::perform( - check_network_datadir(datadir_path.clone(), network), - Message::Checked, - ), + Task::perform(check_network_datadir(network_dir), Message::Checked), ) } @@ -96,8 +96,8 @@ impl Launcher { }) } Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal)) => { - let wallet_datadir = self.datadir_path.join(self.network.to_string()); - let config_path = wallet_datadir.join(app::config::DEFAULT_FILE_NAME); + let wallet_datadir = self.datadir_path.network_directory(self.network); + let config_path = wallet_datadir.path().join(app::config::DEFAULT_FILE_NAME); let internal_bitcoind = if let Ok(cfg) = app::Config::from_file(&config_path) { Some(cfg.start_internal_bitcoind) } else { @@ -112,10 +112,8 @@ impl Launcher { } Message::View(ViewMessage::SelectNetwork(network)) => { self.network = network; - Task::perform( - check_network_datadir(self.datadir_path.clone(), self.network), - Message::Checked, - ) + let network_dir = self.datadir_path.network_directory(self.network); + Task::perform(check_network_datadir(network_dir), Message::Checked) } Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Deleted)) => { self.state = State::NoWallet; @@ -138,8 +136,11 @@ impl Launcher { Message::View(ViewMessage::Run) => { if matches!(self.state, State::Wallet { .. }) { let datadir_path = self.datadir_path.clone(); - let mut path = self.datadir_path.clone(); - path.push(self.network.to_string()); + let mut path = self + .datadir_path + .network_directory(self.network) + .path() + .to_path_buf(); path.push(app::config::DEFAULT_FILE_NAME); let cfg = app::Config::from_file(&path).expect("Already checked"); let network = self.network; @@ -340,9 +341,9 @@ impl Launcher { #[derive(Debug, Clone)] pub enum Message { View(ViewMessage), - Install(PathBuf, Network, UserFlow), + Install(LianaDirectory, Network, UserFlow), Checked(Result), - Run(PathBuf, app::config::Config, Network), + Run(LianaDirectory, app::config::Config, Network), } #[derive(Debug, Clone)] @@ -367,7 +368,7 @@ pub enum DeleteWalletMessage { struct DeleteWalletModal { network: Network, - wallet_datadir: PathBuf, + wallet_datadir: NetworkDirectory, warning: Option, deleted: bool, // `None` means we were not able to determine whether wallet uses internal bitcoind. @@ -375,7 +376,11 @@ struct DeleteWalletModal { } impl DeleteWalletModal { - fn new(network: Network, wallet_datadir: PathBuf, internal_bitcoind: Option) -> Self { + fn new( + network: Network, + wallet_datadir: NetworkDirectory, + internal_bitcoind: Option, + ) -> Self { Self { network, wallet_datadir, @@ -388,7 +393,7 @@ impl DeleteWalletModal { fn update(&mut self, message: Message) -> Task { if let Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Confirm)) = message { self.warning = None; - if let Err(e) = std::fs::remove_dir_all(&self.wallet_datadir) { + if let Err(e) = std::fs::remove_dir_all(self.wallet_datadir.path()) { self.warning = Some(e); } else { self.deleted = true; @@ -459,9 +464,8 @@ impl DeleteWalletModal { } } -async fn check_network_datadir(path: PathBuf, network: Network) -> Result { - let mut config_path = path.clone(); - config_path.push(network.to_string()); +async fn check_network_datadir(path: NetworkDirectory) -> Result { + let mut config_path = path.clone().path().to_path_buf(); config_path.push(app::config::DEFAULT_FILE_NAME); if let Err(e) = app::Config::from_file(&config_path) { @@ -470,13 +474,12 @@ async fn check_network_datadir(path: PathBuf, network: Network) -> Result Result; pub struct Loader { - pub datadir_path: PathBuf, + pub datadir_path: LianaDirectory, pub network: bitcoin::Network, pub gui_config: GUIConfig, pub daemon_started: bool, @@ -97,7 +98,7 @@ pub enum Message { Arc, app::Config, Arc, - PathBuf, + LianaDirectory, Option, ), Error, @@ -113,7 +114,7 @@ pub enum Message { impl Loader { pub fn new( - datadir_path: PathBuf, + datadir_path: LianaDirectory, gui_config: GUIConfig, network: bitcoin::Network, internal_bitcoind: Option, @@ -284,8 +285,11 @@ impl Loader { bitcoind.stop(); log::info!("Managed bitcoind stopped."); } else if self.waiting_daemon_bitcoind && self.gui_config.start_internal_bitcoind { - let mut daemon_config_path = self.datadir_path.clone(); - daemon_config_path.push(self.network.to_string()); + let mut daemon_config_path = self + .datadir_path + .network_directory(self.network) + .path() + .to_path_buf(); daemon_config_path.push("daemon.toml"); if let Ok(config) = Config::from_file(Some(daemon_config_path)) { if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend { @@ -406,7 +410,7 @@ fn get_bitcoind_log(log_path: PathBuf) -> impl Stream> { pub async fn load_application( daemon: Arc, info: GetInfoResult, - datadir_path: PathBuf, + datadir_path: LianaDirectory, network: bitcoin::Network, internal_bitcoind: Option, backup: Option, @@ -420,8 +424,9 @@ pub async fn load_application( ), Error, > { + let network_dir = datadir_path.network_directory(network); let wallet = Wallet::new(info.descriptors.main) - .load_from_settings(&datadir_path, network)? + .load_from_settings(&network_dir)? .load_hotsigners(&datadir_path, network)?; let coins = coins_to_cache(daemon.clone()).await.map(|res| res.coins)?; @@ -547,12 +552,14 @@ async fn connect( // Daemon can start only if a config path is given. pub async fn start_bitcoind_and_daemon( - liana_datadir_path: PathBuf, + liana_datadir_path: LianaDirectory, start_internal_bitcoind: bool, network: bitcoin::Network, ) -> StartedResult { - let mut config_path = liana_datadir_path.clone(); - config_path.push(network.to_string()); + let mut config_path = liana_datadir_path + .network_directory(network) + .path() + .to_path_buf(); config_path.push("daemon.toml"); let config = Config::from_file(Some(config_path)).map_err(Error::Config)?; let mut bitcoind: Option = None; @@ -637,9 +644,8 @@ impl From for Error { } /// default lianad socket path is .liana/bitcoin/lianad_rpc -fn socket_path(datadir: &Path, network: bitcoin::Network) -> PathBuf { - let mut path = datadir.to_path_buf(); - path.push(network.to_string()); +fn socket_path(datadir: &LianaDirectory, network: bitcoin::Network) -> PathBuf { + let mut path = datadir.network_directory(network).path().to_path_buf(); path.push("lianad_rpc"); path } diff --git a/liana-gui/src/logger.rs b/liana-gui/src/logger.rs index e37dfd2f..541234e2 100644 --- a/liana-gui/src/logger.rs +++ b/liana-gui/src/logger.rs @@ -9,6 +9,8 @@ use tracing_subscriber::{ reload, Registry, }; +use crate::dir::LianaDirectory; + const INSTALLER_LOG_FILE_NAME: &str = "installer.log"; const GUI_LOG_FILE_NAME: &str = "liana-gui.log"; @@ -77,7 +79,8 @@ impl Logger { } } - pub fn set_installer_mode(&self, mut datadir: PathBuf, log_level: filter::LevelFilter) { + pub fn set_installer_mode(&self, datadir: LianaDirectory, log_level: filter::LevelFilter) { + let mut datadir = datadir.path().to_path_buf(); datadir.push(INSTALLER_LOG_FILE_NAME); if let Err(e) = self.set_layer(datadir, log_level) { error!("Failed to change logger settings: {:#?}", e); @@ -86,10 +89,11 @@ impl Logger { pub fn set_running_mode( &self, - mut datadir: PathBuf, + datadir: LianaDirectory, network: Network, log_level: filter::LevelFilter, ) { + let mut datadir = datadir.path().to_path_buf(); datadir.push(network.to_string()); datadir.push(GUI_LOG_FILE_NAME); if let Err(e) = self.set_layer(datadir, log_level) { @@ -97,7 +101,8 @@ impl Logger { } } - pub fn remove_install_log_file(&self, mut datadir: PathBuf) { + pub fn remove_install_log_file(&self, datadir: LianaDirectory) { + let mut datadir = datadir.path().to_path_buf(); datadir.push(INSTALLER_LOG_FILE_NAME); if let Err(e) = std::fs::remove_file(&datadir) { error!( diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index 29de9eaa..400d1347 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -22,8 +22,8 @@ use liana_ui::{component::text, font, image, theme, widget::Element}; use lianad::commands::ListCoinsResult; use liana_gui::{ - app::{self, cache::Cache, config::default_datadir, wallet::Wallet, App}, - datadir, + app::{self, cache::Cache, wallet::Wallet, App}, + dir::LianaDirectory, export::import_backup_at_launch, hw::HardwareWalletConfig, installer::{self, Installer}, @@ -39,7 +39,7 @@ use liana_gui::{ #[derive(Debug, PartialEq)] enum Arg { - DatadirPath(PathBuf), + DatadirPath(LianaDirectory), Network(bitcoin::Network), } @@ -72,7 +72,7 @@ Options: for (i, arg) in args.iter().enumerate() { if arg == "--datadir" { if let Some(a) = args.get(i + 1) { - res.push(Arg::DatadirPath(PathBuf::from(a))); + res.push(Arg::DatadirPath(LianaDirectory::new(PathBuf::from(a)))); } else { return Err("missing arg to --datadir".into()); } @@ -192,25 +192,25 @@ impl GUI { } } (State::Launcher(l), Message::Launch(msg)) => match *msg { - launcher::Message::Install(datadir_path, network, init) => { - if !datadir_path.exists() { + launcher::Message::Install(datadir, network, init) => { + if !datadir.exists() { // datadir is created right before launching the installer // so logs can go in /installer.log - if let Err(e) = datadir::create_directory(&datadir_path) { + if let Err(e) = datadir.init() { error!("Failed to create datadir: {}", e); } else { info!( "Created a fresh data directory at {}", - &datadir_path.to_string_lossy() + &datadir.path().to_string_lossy() ); } } self.logger.set_installer_mode( - datadir_path.clone(), + datadir.clone(), self.log_level.unwrap_or(LevelFilter::INFO), ); - let (install, command) = Installer::new(datadir_path, network, None, init); + let (install, command) = Installer::new(datadir, network, None, init); self.state = State::Installer(Box::new(install)); command.map(|msg| Message::Install(Box::new(msg))) } @@ -221,9 +221,8 @@ impl GUI { self.log_level .unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)), ); - if let Ok(settings) = - app::settings::Settings::from_file(datadir_path.clone(), network) - { + let network_dir = datadir_path.network_directory(network); + if let Ok(settings) = app::settings::Settings::from_file(&network_dir) { if settings .wallets .first() @@ -267,7 +266,8 @@ impl GUI { login::Message::Run(Ok((backend_client, wallet, coins))) => { let config = app::Config::from_file( &l.datadir - .join(l.network.to_string()) + .network_directory(l.network) + .path() .join(app::config::DEFAULT_FILE_NAME), ) .expect("A gui configuration file must be present"); @@ -293,7 +293,8 @@ impl GUI { }, (State::Installer(i), Message::Install(msg)) => { if let installer::Message::Exit(path, internal_bitcoind, remove_log) = *msg { - let settings = app::settings::Settings::from_file(i.datadir.clone(), i.network) + let network_dir = i.datadir.network_directory(i.network); + let settings = app::settings::Settings::from_file(&network_dir) .expect("A settings file was created"); if settings .wallets @@ -451,7 +452,7 @@ pub fn create_app_with_remote_backend( remote_backend: BackendWalletClient, wallet: api::Wallet, coins: ListCoinsResult, - datadir: PathBuf, + datadir: LianaDirectory, network: bitcoin::Network, config: app::Config, ) -> (app::App, iced::Task) { @@ -513,18 +514,17 @@ pub fn create_app_with_remote_backend( } pub enum Config { - Run(PathBuf, app::Config, bitcoin::Network), - Launcher(PathBuf), + Run(LianaDirectory, app::Config, bitcoin::Network), + Launcher(LianaDirectory), } impl Config { pub fn new( - datadir_path: PathBuf, + datadir_path: LianaDirectory, network: Option, ) -> Result> { if let Some(network) = network { - let mut path = datadir_path.clone(); - path.push(network.to_string()); + let mut path = datadir_path.network_directory(network).path().to_path_buf(); path.push(app::config::DEFAULT_FILE_NAME); match app::Config::from_file(&path) { Ok(cfg) => Ok(Config::Run(datadir_path, cfg, network)), @@ -540,11 +540,11 @@ fn main() -> Result<(), Box> { let args = parse_args(std::env::args().collect())?; let config = match args.as_slice() { [] => { - let datadir_path = default_datadir().unwrap(); + let datadir_path = LianaDirectory::new_default().unwrap(); Config::new(datadir_path, None) } [Arg::Network(network)] => { - let datadir_path = default_datadir().unwrap(); + let datadir_path = LianaDirectory::new_default().unwrap(); Config::new(datadir_path, Some(*network)) } [Arg::DatadirPath(datadir_path)] => Config::new(datadir_path.clone(), None), @@ -640,6 +640,7 @@ fn setup_panic_hook() { #[cfg(test)] mod tests { use super::*; + use liana_gui::dir::LianaDirectory; #[test] fn test_parse_args() { @@ -651,7 +652,7 @@ mod tests { ); assert_eq!( Some(vec![ - Arg::DatadirPath(PathBuf::from("hello")), + Arg::DatadirPath(LianaDirectory::new(PathBuf::from("hello"))), Arg::Network(bitcoin::Network::Testnet) ]), parse_args( @@ -665,7 +666,7 @@ mod tests { assert_eq!( Some(vec![ Arg::Network(bitcoin::Network::Testnet), - Arg::DatadirPath(PathBuf::from("hello")) + Arg::DatadirPath(LianaDirectory::new(PathBuf::from("hello"))), ]), parse_args( "--testnet --datadir hello" diff --git a/liana-gui/src/node/bitcoind.rs b/liana-gui/src/node/bitcoind.rs index 59831507..151b4e1e 100644 --- a/liana-gui/src/node/bitcoind.rs +++ b/liana-gui/src/node/bitcoind.rs @@ -19,6 +19,8 @@ use tracing::{info, warn}; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; +use crate::dir::LianaDirectory; + #[cfg(target_os = "windows")] const CREATE_NO_WINDOW: u32 = 0x08000000; @@ -68,21 +70,22 @@ pub fn download_url() -> String { ) } -pub fn internal_bitcoind_directory(liana_datadir: &PathBuf) -> PathBuf { - let mut datadir = PathBuf::from(liana_datadir); - datadir.push("bitcoind"); - datadir +pub fn internal_bitcoind_directory(liana_datadir: &LianaDirectory) -> PathBuf { + liana_datadir.bitcoind_directory().path().to_path_buf() } /// Data directory used by internal bitcoind. -pub fn internal_bitcoind_datadir(liana_datadir: &PathBuf) -> PathBuf { +pub fn internal_bitcoind_datadir(liana_datadir: &LianaDirectory) -> PathBuf { let mut datadir = internal_bitcoind_directory(liana_datadir); datadir.push("datadir"); datadir } /// Internal bitcoind executable path. -pub fn internal_bitcoind_exe_path(liana_datadir: &PathBuf, bitcoind_version: &str) -> PathBuf { +pub fn internal_bitcoind_exe_path( + liana_datadir: &LianaDirectory, + bitcoind_version: &str, +) -> PathBuf { internal_bitcoind_directory(liana_datadir) .join(format!("bitcoin-{}", bitcoind_version)) .join("bin") @@ -94,7 +97,7 @@ pub fn internal_bitcoind_exe_path(liana_datadir: &PathBuf, bitcoind_version: &st } /// Path of the `bitcoin.conf` file used by internal bitcoind. -pub fn internal_bitcoind_config_path(bitcoind_datadir: &PathBuf) -> PathBuf { +pub fn internal_bitcoind_config_path(bitcoind_datadir: &Path) -> PathBuf { let mut config_path = PathBuf::from(bitcoind_datadir); config_path.push("bitcoin.conf"); config_path @@ -111,7 +114,10 @@ pub fn internal_bitcoind_cookie_path(bitcoind_datadir: &Path, network: &Network) } /// Path of the cookie file used by internal bitcoind on a given network. -pub fn internal_bitcoind_debug_log_path(lianad_datadir: &PathBuf, network: Network) -> PathBuf { +pub fn internal_bitcoind_debug_log_path( + lianad_datadir: &LianaDirectory, + network: Network, +) -> PathBuf { let mut debug_log_path = internal_bitcoind_datadir(lianad_datadir); if let Some(dir) = bitcoind_network_dir(&network) { debug_log_path.push(dir); @@ -400,7 +406,7 @@ impl Bitcoind { pub fn start( network: &bitcoin::Network, config: BitcoindConfig, - liana_datadir: &PathBuf, + liana_datadir: &LianaDirectory, ) -> Result { let bitcoind_datadir = internal_bitcoind_datadir(liana_datadir); // Find most recent bitcoind version available. diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index db0cf3d9..f81daf3c 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -2,7 +2,6 @@ pub mod api; use std::{ collections::{HashMap, HashSet}, - path::Path, sync::Arc, }; @@ -23,6 +22,7 @@ use tokio::sync::RwLock; use crate::{ app::settings::{AuthConfig, Settings}, daemon::{model::*, Daemon, DaemonBackend, DaemonError}, + dir::LianaDirectory, hw::HardwareWalletConfig, }; @@ -519,7 +519,11 @@ impl Daemon for BackendWalletClient { } /// refresh the token if close to expiration. - async fn is_alive(&self, datadir: &Path, network: Network) -> Result<(), DaemonError> { + async fn is_alive( + &self, + datadir: &LianaDirectory, + network: Network, + ) -> Result<(), DaemonError> { let auth = self.auth().await; if auth.expires_at < Utc::now().timestamp() + 60 { match self.inner.auth.try_write() { @@ -534,13 +538,13 @@ impl Daemon for BackendWalletClient { .refresh_token(&auth.refresh_token) .await?; - let mut settings = Settings::from_file(datadir.to_path_buf(), network) - .map_err(|e| { - DaemonError::Unexpected(format!( - "Cannot access to settings.json file: {}", - e - )) - })?; + let network_dir = datadir.network_directory(network); + let mut settings = Settings::from_file(&network_dir).map_err(|e| { + DaemonError::Unexpected(format!( + "Cannot access to settings.json file: {}", + e + )) + })?; if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| { if let Some(auth) = &w.remote_backend_auth { @@ -558,14 +562,12 @@ impl Daemon for BackendWalletClient { tracing::info!("Wallet id was not found in the settings"); } - settings - .to_file(datadir.to_path_buf(), network) - .map_err(|e| { - DaemonError::Unexpected(format!( - "Cannot access to settings.json file: {}", - e - )) - })?; + settings.to_file(&network_dir).map_err(|e| { + DaemonError::Unexpected(format!( + "Cannot access to settings.json file: {}", + e + )) + })?; *old = new; tracing::info!("Liana backend access was refreshed"); diff --git a/liana-gui/src/services/connect/login.rs b/liana-gui/src/services/connect/login.rs index 06366681..e1dce45b 100644 --- a/liana-gui/src/services/connect/login.rs +++ b/liana-gui/src/services/connect/login.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, sync::Arc}; +use std::sync::Arc; use iced::{Alignment, Length, Task}; @@ -16,6 +16,7 @@ use crate::{ settings::{AuthConfig, Settings, SettingsError, WalletSetting}, }, daemon::DaemonError, + dir::LianaDirectory, }; use super::client::{ @@ -100,7 +101,7 @@ pub enum BackendState { } pub struct LianaLiteLogin { - pub datadir: PathBuf, + pub datadir: LianaDirectory, pub network: Network, wallet_id: String, @@ -128,7 +129,11 @@ pub enum ConnectionStep { } impl LianaLiteLogin { - pub fn new(datadir: PathBuf, network: Network, settings: Settings) -> (Self, Task) { + pub fn new( + datadir: LianaDirectory, + network: Network, + settings: Settings, + ) -> (Self, Task) { match settings .wallets .first() @@ -490,13 +495,14 @@ impl LianaLiteLogin { } async fn update_wallet_auth_settings( - datadir: PathBuf, + datadir: LianaDirectory, network: Network, wallet: api::Wallet, email: String, refresh_token: String, ) -> Result<(), Error> { - let mut settings = Settings::from_file(datadir.clone(), network)?; + let network_dir = datadir.network_directory(network); + let mut settings = Settings::from_file(&network_dir)?; let descriptor_checksum = wallet .descriptor @@ -534,7 +540,7 @@ async fn update_wallet_auth_settings( ); } - settings.to_file(datadir, network).map_err(|e| { + settings.to_file(&network_dir).map_err(|e| { DaemonError::Unexpected(format!("Cannot access to settings.json file: {}", e)) })?; diff --git a/liana-gui/src/signer.rs b/liana-gui/src/signer.rs index dad45dab..be237ead 100644 --- a/liana-gui/src/signer.rs +++ b/liana-gui/src/signer.rs @@ -9,6 +9,8 @@ use liana::{ signer::HotSigner, }; +use crate::dir::LianaDirectory; + pub struct Signer { curve: secp256k1::Secp256k1, key: HotSigner, @@ -58,9 +60,9 @@ impl Signer { pub fn store( &self, - datadir_root: &std::path::Path, + datadir_root: &LianaDirectory, network: Network, ) -> Result<(), SignerError> { - self.key.store(datadir_root, network, &self.curve) + self.key.store(datadir_root.path(), network, &self.curve) } } diff --git a/lianad/src/lib.rs b/lianad/src/lib.rs index 2c3ea839..7189bfe1 100644 --- a/lianad/src/lib.rs +++ b/lianad/src/lib.rs @@ -405,8 +405,8 @@ impl DaemonHandle { let data_dir = config .data_directory() .ok_or(StartupError::DefaultDataDirNotFound)?; - let fresh_data_dir = !data_dir.exists(); - if fresh_data_dir { + let fresh_data_dir = !data_dir.exists() || !data_dir.sqlite_db_file_path().exists(); + if !data_dir.exists() { data_dir .init() .map_err(|e| StartupError::DatadirCreation(data_dir.path().to_path_buf(), e))?;