diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index 4a272ab0..c980c7e4 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -128,7 +128,7 @@ impl Backup { let mut proprietary = serde_json::Map::new(); proprietary.insert(LIANA_VERSION_KEY.to_string(), liana_version().into()); - let config = extract_daemon_config(&ctx); + let config = extract_daemon_config(&ctx).map_err(|e| Error::Daemon(e.to_string()))?; if let Ok(config) = serde_json::to_value(config) { proprietary.insert(CONFIG_KEY.to_string(), config); } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 52015dc9..e4523742 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -809,22 +809,17 @@ pub async fn import_backup( Vec::new() }; - let datadir = match daemon.config() { - Some(c) => match &c.data_dir { - Some(dd) => dd, - None => { - return Err(Error::BackupImport("Failed to get Daemon config".into())); - } - }, - None => { - return Err(Error::BackupImport("Failed to get Daemon config".into())); - } - }; + let lianad_datadir = daemon + .config() + .and_then(|c| c.data_directory()) + .ok_or(Error::BackupImport("Failed to get Daemon config".into()))?; // check if key aliases can be imported w/o conflict let mut write_aliases = true; let settings = if !account.keys.is_empty() { - let settings = match Settings::from_file(datadir.to_path_buf(), network) { + // 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) { Ok(s) => s, Err(_) => { return Err(Error::BackupImport("Failed to get App Settings".into())); @@ -943,7 +938,10 @@ pub async fn import_backup( settings.wallets.get_mut(0).expect("already checked").keys = settings_aliases.clone().into_values().collect(); - if settings.to_file(datadir.to_path_buf(), network).is_err() { + if settings + .to_file(lianad_datadir.path().to_path_buf(), network) + .is_err() + { return Err(Error::BackupImport("Failed to import keys aliases".into())); } else { // Update wallet state diff --git a/liana-gui/src/installer/context.rs b/liana-gui/src/installer/context.rs index 025349bc..ea3cd74c 100644 --- a/liana-gui/src/installer/context.rs +++ b/liana-gui/src/installer/context.rs @@ -60,7 +60,7 @@ pub struct Context { pub descriptor: Option, pub keys: HashMap, pub hws: Vec<(DeviceKind, bitcoin::bip32::Fingerprint, Option<[u8; 32]>)>, - pub data_dir: PathBuf, + pub root_directory: PathBuf, 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, - data_dir: PathBuf, + root_directory: PathBuf, remote_backend: RemoteBackend, ) -> Self { Self { @@ -89,7 +89,7 @@ impl Context { keys: HashMap::new(), bitcoin_backend: None, descriptor: None, - data_dir, + root_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 b012dead..13540edd 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -133,7 +133,7 @@ impl Installer { ChooseBackend::new(network).into(), RemoteBackendLogin::new(network).into(), SelectBitcoindTypeStep::new().into(), - InternalBitcoindStep::new(&context.data_dir).into(), + InternalBitcoindStep::new(&context.root_directory).into(), DefineNode::default().into(), Final::new().into(), ], @@ -146,7 +146,7 @@ impl Installer { RecoverMnemonic::default().into(), RegisterDescriptor::new_import_wallet().into(), SelectBitcoindTypeStep::new().into(), - InternalBitcoindStep::new(&context.data_dir).into(), + InternalBitcoindStep::new(&context.root_directory).into(), DefineNode::default().into(), Final::new().into(), ], @@ -167,7 +167,7 @@ impl Installer { } pub fn destination_path(&self) -> PathBuf { - self.context.data_dir.clone() + self.context.root_directory.clone() } pub fn subscription(&self) -> Subscription { @@ -270,21 +270,21 @@ impl Installer { } } Message::Installed(Err(e)) => { - let mut data_dir = self.context.data_dir.clone(); - data_dir.push(self.context.bitcoin_config.network.to_string()); + let mut network_directory = self.context.root_directory.clone(); + network_directory.push(self.context.bitcoin_config.network.to_string()); // 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(&data_dir) { + if let Err(e) = std::fs::remove_dir_all(&network_directory) { error!( "Failed to completely delete the data directory (path: '{}'): {}", - data_dir.to_string_lossy(), + network_directory.to_string_lossy(), e ); } else { warn!( "Successfully deleted data directory at '{}'.", - data_dir.to_string_lossy() + network_directory.to_string_lossy() ); }; self.steps @@ -358,19 +358,13 @@ pub async fn install_local_wallet( ctx: Context, signer: Arc>, ) -> Result { - let mut cfg: lianad::config::Config = extract_daemon_config(&ctx); - let data_dir = cfg.data_dir.unwrap(); - - let data_dir = data_dir - .canonicalize() - .map_err(|e| Error::Unexpected(format!("Failed to canonicalize datadir path: {}", e)))?; - cfg.data_dir = Some(data_dir.clone()); + let cfg: lianad::config::Config = extract_daemon_config(&ctx)?; daemon_check(cfg.clone())?; info!("daemon checked"); - let mut network_datadir_path = data_dir; + 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)))?; @@ -396,10 +390,7 @@ pub async fn install_local_wallet( signer .lock() .unwrap() - .store( - &cfg.data_dir().expect("Already checked"), - cfg.bitcoin_config.network, - ) + .store(&ctx.root_directory, cfg.bitcoin_config.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Hot signer mnemonic stored"); @@ -407,10 +398,7 @@ pub async fn install_local_wallet( if let Some(signer) = &ctx.recovered_signer { signer - .store( - &cfg.data_dir().expect("Already checked"), - cfg.bitcoin_config.network, - ) + .store(&ctx.root_directory, cfg.bitcoin_config.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Recovered signer mnemonic stored"); @@ -450,12 +438,7 @@ pub async fn create_remote_wallet( signer: Arc>, remote_backend: BackendClient, ) -> Result { - let data_dir = ctx - .data_dir - .canonicalize() - .map_err(|e| Error::Unexpected(format!("Failed to canonicalize datadir path: {}", e)))?; - - let mut network_datadir_path = data_dir.clone(); + let mut network_datadir_path = ctx.root_directory.clone(); network_datadir_path.push(ctx.network.to_string()); create_directory(&network_datadir_path) .map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?; @@ -472,7 +455,7 @@ pub async fn create_remote_wallet( signer .lock() .unwrap() - .store(&data_dir, ctx.network) + .store(&ctx.root_directory, ctx.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Hot signer mnemonic stored"); @@ -480,13 +463,13 @@ pub async fn create_remote_wallet( if let Some(signer) = &ctx.recovered_signer { signer - .store(&data_dir, ctx.network) + .store(&ctx.root_directory, ctx.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Recovered signer mnemonic stored"); } - let mut network_datadir_path = data_dir; + let mut network_datadir_path = ctx.root_directory.clone(); network_datadir_path.push(ctx.network.to_string()); // create liana GUI configuration file @@ -573,20 +556,15 @@ pub async fn import_remote_wallet( ) -> Result { tracing::info!("Importing wallet from remote backend"); - let data_dir = ctx - .data_dir - .canonicalize() - .map_err(|e| Error::Unexpected(format!("Failed to canonicalize datadir path: {}", e)))?; - if let Some(signer) = &ctx.recovered_signer { signer - .store(&data_dir, ctx.network) + .store(&ctx.root_directory, ctx.network) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; info!("Recovered signer mnemonic stored"); } - let mut network_datadir_path = data_dir; + let mut network_datadir_path = ctx.root_directory.clone(); network_datadir_path.push(ctx.network.to_string()); create_directory(&network_datadir_path) .map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?; @@ -700,17 +678,21 @@ pub fn extract_local_gui_settings(ctx: &Context) -> Settings { } } -pub fn extract_daemon_config(ctx: &Context) -> Config { - Config { - log_level: log::LevelFilter::Info, - main_descriptor: ctx - .descriptor +pub fn extract_daemon_config(ctx: &Context) -> Result { + let mut data_directory = ctx + .root_directory + .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(), + log::LevelFilter::Info, + ctx.descriptor .clone() .expect("Context must have a descriptor at this point"), - data_dir: Some(ctx.data_dir.clone()), - bitcoin_config: ctx.bitcoin_config.clone(), - bitcoin_backend: ctx.bitcoin_backend.clone(), - } + lianad::datadir::DataDirectory::new(data_directory), + )) } #[derive(Debug, Clone)] diff --git a/liana-gui/src/installer/step/node/bitcoind.rs b/liana-gui/src/installer/step/node/bitcoind.rs index c23e6efb..95c111ee 100644 --- a/liana-gui/src/installer/step/node/bitcoind.rs +++ b/liana-gui/src/installer/step/node/bitcoind.rs @@ -531,7 +531,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.data_dir, VERSION); + let exe_path = bitcoind::internal_bitcoind_exe_path(&ctx.root_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/main.rs b/liana-gui/src/main.rs index ec867a14..790fa10f 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -317,6 +317,7 @@ impl GUI { if remove_log { self.logger.remove_install_log_file(i.datadir.clone()); } + let (loader, command) = Loader::new( i.datadir.clone(), cfg, diff --git a/lianad/src/bin/cli.rs b/lianad/src/bin/cli.rs index 60ae9937..66cf3d76 100644 --- a/lianad/src/bin/cli.rs +++ b/lianad/src/bin/cli.rs @@ -1,6 +1,6 @@ #![cfg(not(target_os = "windows"))] -use lianad::config::{config_folder_path, Config}; +use lianad::config::Config; use std::{ env, @@ -89,17 +89,9 @@ fn socket_file(conf_file: Option) -> PathBuf { process::exit(1); }); let data_dir = config - .data_dir - .unwrap_or_else(|| config_folder_path().unwrap()); - let data_dir = data_dir.to_str().expect("Datadir is valid unicode"); - - [ - data_dir, - config.bitcoin_config.network.to_string().as_str(), - "lianad_rpc", - ] - .iter() - .collect() + .data_directory() + .expect("Wallet datadir is not properly defined"); + data_dir.lianad_rpc_socket_path() } fn trimmed(mut vec: Vec, bytes_read: usize) -> Vec { diff --git a/lianad/src/config.rs b/lianad/src/config.rs index bd8d1994..12c37815 100644 --- a/lianad/src/config.rs +++ b/lianad/src/config.rs @@ -2,6 +2,7 @@ use liana::descriptors::LianaDescriptor; use std::{fmt, net::SocketAddr, path::PathBuf, str::FromStr, time::Duration}; +use crate::datadir::DataDirectory; use miniscript::bitcoin::Network; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -146,8 +147,10 @@ pub struct BitcoinConfig { /// Static informations we require to operate #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { - /// An optional custom data directory - pub data_dir: Option, + /// legacy: An optional custom data directory + data_dir: Option, + /// Current: A direct point to the current datadir + data_directory: Option, /// What messages to log #[serde( deserialize_with = "deserialize_fromstr", @@ -169,8 +172,35 @@ pub struct Config { } impl Config { - pub fn data_dir(&self) -> Option { - self.data_dir.clone().or_else(config_folder_path) + pub fn new( + bitcoin_config: BitcoinConfig, + bitcoin_backend: Option, + log_level: log::LevelFilter, + main_descriptor: LianaDescriptor, + data_directory: DataDirectory, + ) -> Self { + Self { + bitcoin_config, + bitcoin_backend, + log_level, + main_descriptor, + data_directory: Some(data_directory.path().to_path_buf()), + data_dir: None, + } + } + + pub fn data_directory(&self) -> Option { + if self.data_directory.is_some() { + self.data_directory.clone().map(DataDirectory::new) + } else if let Some(mut dir) = self.data_dir.clone() { + dir.push(self.bitcoin_config.network.to_string()); + Some(DataDirectory::new(dir)) + } else { + config_folder_path().map(|mut dir| { + dir.push(self.bitcoin_config.network.to_string()); + DataDirectory::new(dir) + }) + } } } diff --git a/lianad/src/datadir.rs b/lianad/src/datadir.rs new file mode 100644 index 00000000..c87f1b16 --- /dev/null +++ b/lianad/src/datadir.rs @@ -0,0 +1,47 @@ +use std::path::{Path, PathBuf}; + +pub struct DataDirectory(PathBuf); + +impl DataDirectory { + pub fn new(p: PathBuf) -> Self { + DataDirectory(p) + } +} + +impl DataDirectory { + pub fn exists(&self) -> bool { + self.0.as_path().exists() + } + pub fn init(&self) -> Result<(), std::io::Error> { + #[cfg(unix)] + return { + use std::fs::DirBuilder; + use std::os::unix::fs::DirBuilderExt; + + let mut builder = DirBuilder::new(); + builder.mode(0o700).recursive(true).create(self.path()) + }; + + // TODO: permissions on Windows.. + #[cfg(not(unix))] + return { std::fs::create_dir_all(self.path()) }; + } + pub fn path(&self) -> &Path { + self.0.as_path() + } + pub fn sqlite_db_file_path(&self) -> PathBuf { + let mut dir = self.0.clone(); + dir.push("lianad.sqlite3"); + dir + } + pub fn lianad_watchonly_wallet_path(&self) -> PathBuf { + let mut dir = self.0.clone(); + dir.push("lianad_watchonly_wallet"); + dir + } + pub fn lianad_rpc_socket_path(&self) -> PathBuf { + let mut dir = self.0.clone(); + dir.push("lianad_rpc"); + dir + } +} diff --git a/lianad/src/lib.rs b/lianad/src/lib.rs index 84a63dcf..2c3ea839 100644 --- a/lianad/src/lib.rs +++ b/lianad/src/lib.rs @@ -2,6 +2,7 @@ mod bitcoin; pub mod commands; pub mod config; mod database; +pub mod datadir; mod jsonrpc; #[cfg(test)] mod testutils; @@ -9,6 +10,7 @@ mod testutils; pub use bdk_electrum::electrum_client; pub use bip329; use bitcoin::electrum; +use datadir::DataDirectory; pub use miniscript; pub use crate::bitcoin::{ @@ -27,7 +29,7 @@ use crate::{ }; use std::{ - error, fmt, fs, io, path, + error, fmt, io, path, sync::{self, mpsc}, thread, }; @@ -166,40 +168,16 @@ impl From for StartupError { } } -fn create_datadir(datadir_path: &path::Path) -> Result<(), StartupError> { - #[cfg(unix)] - return { - use fs::DirBuilder; - use std::os::unix::fs::DirBuilderExt; - - let mut builder = DirBuilder::new(); - builder - .mode(0o700) - .recursive(true) - .create(datadir_path) - .map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e)) - }; - - // TODO: permissions on Windows.. - #[cfg(not(unix))] - return { - fs::create_dir_all(datadir_path) - .map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e)) - }; -} - // Connect to the SQLite database. Create it if starting fresh, and do some sanity checks. // If all went well, returns the interface to the SQLite database. fn setup_sqlite( config: &Config, - data_dir: &path::Path, + data_dir: &DataDirectory, fresh_data_dir: bool, secp: &secp256k1::Secp256k1, bitcoind: &Option, ) -> Result { - let db_path: path::PathBuf = [data_dir, path::Path::new("lianad.sqlite3")] - .iter() - .collect(); + let db_path = data_dir.sqlite_db_file_path(); let options = if fresh_data_dir { Some(FreshDbOptions::new( config.bitcoin_config.network, @@ -242,12 +220,10 @@ fn setup_sqlite( // If all went well, returns the interface to bitcoind. fn setup_bitcoind( config: &Config, - data_dir: &path::Path, + data_dir: &DataDirectory, fresh_data_dir: bool, ) -> Result { - let wo_path: path::PathBuf = [data_dir, path::Path::new("lianad_watchonly_wallet")] - .iter() - .collect(); + let wo_path: path::PathBuf = data_dir.lianad_watchonly_wallet_path(); let wo_path_str = wo_path.to_str().expect("Must be valid unicode").to_string(); // NOTE: On Windows, paths are canonicalized with a "\\?\" prefix to tell Windows to interpret // the string "as is" and to ignore the maximum size of a path. HOWEVER this is not properly @@ -426,14 +402,18 @@ impl DaemonHandle { let secp = secp256k1::Secp256k1::verification_only(); // First, check the data directory - let mut data_dir = config - .data_dir() + let data_dir = config + .data_directory() .ok_or(StartupError::DefaultDataDirNotFound)?; - data_dir.push(config.bitcoin_config.network.to_string()); - let fresh_data_dir = !data_dir.as_path().exists(); + let fresh_data_dir = !data_dir.exists(); if fresh_data_dir { - create_datadir(&data_dir)?; - log::info!("Created a new data directory at '{}'", data_dir.display()); + data_dir + .init() + .map_err(|e| StartupError::DatadirCreation(data_dir.path().to_path_buf(), e))?; + log::info!( + "Created a new data directory at '{}'", + data_dir.path().to_string_lossy() + ); } // Set up the connection to bitcoind (if using it) first as we may need it for the database @@ -501,9 +481,7 @@ impl DaemonHandle { .spawn({ let shutdown = rpcserver_shutdown.clone(); move || { - let mut rpc_socket = data_dir; - rpc_socket.push("lianad_rpc"); - server::run(&rpc_socket, control, shutdown)?; + server::run(&data_dir.lianad_rpc_socket_path(), control, shutdown)?; Ok(()) } }) @@ -776,6 +754,7 @@ mod tests { let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("datadir")] .iter() .collect(); + fs::create_dir_all(&data_dir).unwrap(); let wo_path: path::PathBuf = [ data_dir.as_path(), path::Path::new("bitcoin"), @@ -815,13 +794,15 @@ mod tests { let desc = LianaDescriptor::from_str(desc_str).unwrap(); let receive_desc = desc.receive_descriptor().clone(); let change_desc = desc.change_descriptor().clone(); - let config = Config { + let mut data_directory = data_dir.clone(); + data_directory.push("bitcoin"); + let config = Config::new( bitcoin_config, - bitcoin_backend: Some(config::BitcoinBackend::Bitcoind(bitcoind_config)), - data_dir: Some(data_dir), - log_level: log::LevelFilter::Debug, - main_descriptor: desc, - }; + Some(config::BitcoinBackend::Bitcoind(bitcoind_config)), + log::LevelFilter::Debug, + desc, + DataDirectory::new(data_directory), + ); // Start the daemon in a new thread so the current one acts as the bitcoind server. let t = thread::spawn({ diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 3d783f8c..2f4a11ba 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -4,6 +4,7 @@ use crate::{ database::{ BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet, }, + datadir::DataDirectory, DaemonControl, DaemonHandle, }; use liana::descriptors; @@ -563,7 +564,11 @@ impl DummyLiana { let tmp_dir = tmp_dir(); fs::create_dir_all(&tmp_dir).unwrap(); // Use a shorthand for 'datadir', to avoid overflowing SUN_LEN on MacOS. - let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("d")].iter().collect(); + let root_directory: path::PathBuf = + [tmp_dir.as_path(), path::Path::new("d")].iter().collect(); + fs::create_dir_all(&root_directory).unwrap(); + let mut data_directory = root_directory.clone(); + data_directory.push("bitcoin"); let network = bitcoin::Network::Bitcoin; let bitcoin_config = BitcoinConfig { @@ -579,13 +584,13 @@ impl DummyLiana { ) .unwrap(); let desc = descriptors::LianaDescriptor::new(policy); - let config = Config { + let config = Config::new( bitcoin_config, - bitcoin_backend: None, - data_dir: Some(data_dir), - log_level: log::LevelFilter::Debug, - main_descriptor: desc, - }; + None, + log::LevelFilter::Debug, + desc, + DataDirectory::new(data_directory), + ); let handle = DaemonHandle::start(config, Some(bitcoin_interface), Some(database), rpc_server)