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))?;