Prepare datadir for multiple wallet support

A new field data_directory is introduced in lianad configuration, it
resolves directly to the path of the wallet datadir without the per network
reasoning. The previous data_dir field is kept for backward
compatibility for already generated configuration file.
This commit is contained in:
edouardparis 2025-04-23 17:27:28 +02:00
parent 1213858489
commit 6ccaa0573b
11 changed files with 172 additions and 136 deletions

View File

@ -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);
}

View File

@ -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

View File

@ -60,7 +60,7 @@ pub struct Context {
pub descriptor: Option<LianaDescriptor>,
pub keys: HashMap<bitcoin::bip32::Fingerprint, KeySetting>,
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,

View File

@ -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<Message> {
@ -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<Mutex<Signer>>,
) -> Result<PathBuf, Error> {
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<Mutex<Signer>>,
remote_backend: BackendClient,
) -> Result<PathBuf, Error> {
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<PathBuf, Error> {
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<Config, Error> {
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)]

View File

@ -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() {

View File

@ -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,

View File

@ -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>) -> 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<u8>, bytes_read: usize) -> Vec<u8> {

View File

@ -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<PathBuf>,
/// legacy: An optional custom data directory
data_dir: Option<PathBuf>,
/// Current: A direct point to the current datadir
data_directory: Option<PathBuf>,
/// What messages to log
#[serde(
deserialize_with = "deserialize_fromstr",
@ -169,8 +172,35 @@ pub struct Config {
}
impl Config {
pub fn data_dir(&self) -> Option<PathBuf> {
self.data_dir.clone().or_else(config_folder_path)
pub fn new(
bitcoin_config: BitcoinConfig,
bitcoin_backend: Option<BitcoinBackend>,
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<DataDirectory> {
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)
})
}
}
}

47
lianad/src/datadir.rs Normal file
View File

@ -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
}
}

View File

@ -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<BitcoindError> 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<secp256k1::VerifyOnly>,
bitcoind: &Option<BitcoinD>,
) -> Result<SqliteDb, StartupError> {
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<BitcoinD, StartupError> {
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({

View File

@ -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)