#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
use crate::commands::ClientType;
#[cfg(feature = "sqlite")]
use crate::commands::DatabaseType;
use crate::commands::WalletOpts;
use crate::error::BDKCliError as Error;
use bdk_wallet::bitcoin::Network;
#[cfg(any(feature = "sqlite", feature = "redb"))]
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::str::FromStr;
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletConfig {
pub wallets: HashMap<String, WalletConfigInner>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletConfigInner {
pub wallet: String,
pub network: String,
pub ext_descriptor: String,
pub int_descriptor: Option<String>,
#[cfg(any(feature = "sqlite", feature = "redb"))]
pub database_type: String,
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
pub client_type: Option<String>,
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
pub server_url: Option<String>,
#[cfg(feature = "rpc")]
pub rpc_user: Option<String>,
#[cfg(feature = "rpc")]
pub rpc_password: Option<String>,
#[cfg(feature = "electrum")]
pub batch_size: Option<usize>,
#[cfg(feature = "esplora")]
pub parallel_requests: Option<usize>,
#[cfg(feature = "rpc")]
pub cookie: Option<String>,
}
impl WalletConfig {
pub fn load(datadir: &Path) -> Result<Option<WalletConfig>, Error> {
let config_path = datadir.join("config.toml");
if !config_path.exists() {
return Ok(None);
}
let config_content = fs::read_to_string(&config_path)
.map_err(|e| Error::Generic(format!("Failed to read config file: {e}")))?;
let config: WalletConfig = toml::from_str(&config_content)
.map_err(|e| Error::Generic(format!("Failed to parse config file: {e}")))?;
Ok(Some(config))
}
pub fn save(&self, datadir: &Path) -> Result<(), Error> {
let config_path = datadir.join("config.toml");
let config_content = toml::to_string_pretty(self)
.map_err(|e| Error::Generic(format!("Failed to serialize config: {e}")))?;
fs::create_dir_all(datadir)
.map_err(|e| Error::Generic(format!("Failed to create directory {datadir:?}: {e}")))?;
fs::write(&config_path, config_content).map_err(|e| {
Error::Generic(format!("Failed to write config file {config_path:?}: {e}"))
})?;
log::debug!("Saved config to {config_path:?}");
Ok(())
}
pub fn get_wallet_opts(&self, wallet_name: &str) -> Result<WalletOpts, Error> {
self.wallets
.get(wallet_name)
.ok_or_else(|| Error::Generic(format!("Wallet {wallet_name} not found in config")))?
.try_into()
}
}
impl TryFrom<&WalletConfigInner> for WalletOpts {
type Error = Error;
fn try_from(config: &WalletConfigInner) -> Result<Self, Self::Error> {
let _network = Network::from_str(&config.network)
.map_err(|_| Error::Generic("Invalid network".to_string()))?;
#[cfg(any(feature = "sqlite", feature = "redb"))]
let database_type = DatabaseType::from_str(&config.database_type, true)
.map_err(|_| Error::Generic("Invalid database type".to_string()))?;
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
let client_type = config
.client_type
.as_deref()
.ok_or_else(|| Error::Generic("Client type missing".into()))
.and_then(|s| {
ClientType::from_str(s, true)
.map_err(|_| Error::Generic("Invalid client type".into()))
})?;
Ok(WalletOpts {
wallet: Some(config.wallet.clone()),
verbose: false,
ext_descriptor: config.ext_descriptor.clone(),
int_descriptor: config.int_descriptor.clone(),
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
client_type,
#[cfg(any(feature = "sqlite", feature = "redb"))]
database_type,
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
url: config
.server_url
.clone()
.ok_or_else(|| Error::Generic("Server url not found".into()))?,
#[cfg(feature = "electrum")]
batch_size: config.batch_size.unwrap_or(10),
#[cfg(feature = "esplora")]
parallel_requests: config.parallel_requests.unwrap_or(5),
#[cfg(feature = "rpc")]
basic_auth: (
config.rpc_user.clone().unwrap_or_else(|| "user".into()),
config
.rpc_password
.clone()
.unwrap_or_else(|| "password".into()),
),
#[cfg(feature = "rpc")]
cookie: config.cookie.clone(),
#[cfg(feature = "cbf")]
compactfilter_opts: crate::commands::CompactFilterOpts { conn_count: 2 },
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryInto;
const EXT_DESCRIPTOR: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/0/*)#429nsxmg";
const INT_DESCRIPTOR: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/1/*)#y7qjdnts";
#[test]
fn test_wallet_config_inner_to_opts_conversion() {
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
let client_type = {
if cfg!(feature = "esplora") {
Some("esplora".to_string())
} else if cfg!(feature = "rpc") {
Some("rpc".to_string())
} else if cfg!(feature = "electrum") {
Some("electrum".to_string())
} else if cfg!(feature = "cbf") {
Some("cbf".to_string())
} else {
None
}
};
let wallet_config = WalletConfigInner {
wallet: "test_wallet".to_string(),
network: "testnet4".to_string(),
ext_descriptor: EXT_DESCRIPTOR.to_string(),
int_descriptor: Some(INT_DESCRIPTOR.to_string()),
#[cfg(any(feature = "sqlite", feature = "redb"))]
database_type: "sqlite".to_string(),
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
client_type,
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
server_url: Some("https://example.com/testnet/api".to_string()),
#[cfg(feature = "electrum")]
batch_size: None,
#[cfg(feature = "esplora")]
parallel_requests: None,
#[cfg(feature = "rpc")]
rpc_user: None,
#[cfg(feature = "rpc")]
rpc_password: None,
#[cfg(feature = "rpc")]
cookie: None,
};
let opts: WalletOpts = (&wallet_config)
.try_into()
.expect("Conversion should succeed");
assert_eq!(opts.wallet, Some("test_wallet".to_string()));
#[cfg(all(
feature = "esplora",
not(any(feature = "electrum", feature = "rpc", feature = "cbf"))
))]
assert_eq!(opts.client_type, ClientType::Esplora);
#[cfg(all(
feature = "rpc",
not(any(feature = "esplora", feature = "electrum", feature = "cbf"))
))]
assert_eq!(opts.client_type, ClientType::Rpc);
#[cfg(all(feature = "electrum", not(any(feature = "esplora", feature = "rpc"))))]
assert_eq!(opts.client_type, ClientType::Electrum);
#[cfg(all(
feature = "cbf",
not(any(feature = "esplora", feature = "rpc", feature = "electrum"))
))]
assert_eq!(opts.client_type, ClientType::Cbf);
#[cfg(feature = "sqlite")]
assert_eq!(opts.database_type, DatabaseType::Sqlite);
assert_eq!(opts.ext_descriptor, EXT_DESCRIPTOR);
#[cfg(feature = "electrum")]
assert_eq!(opts.batch_size, 10);
#[cfg(feature = "esplora")]
assert_eq!(opts.parallel_requests, 5);
}
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
#[test]
fn test_invalid_client_type_fails() {
let inner = WalletConfigInner {
wallet: "test".to_string(),
network: "regtest".to_string(),
ext_descriptor: "desc".to_string(),
int_descriptor: None,
#[cfg(any(feature = "sqlite", feature = "redb"))]
database_type: "sqlite".to_string(),
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
client_type: Some("invalid_backend".to_string()),
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
server_url: Some("url".to_string()),
#[cfg(feature = "electrum")]
batch_size: None,
#[cfg(feature = "esplora")]
parallel_requests: None,
#[cfg(feature = "rpc")]
rpc_user: None,
#[cfg(feature = "rpc")]
rpc_password: None,
#[cfg(feature = "rpc")]
cookie: None,
};
let result: Result<WalletOpts, Error> = (&inner).try_into();
assert!(result.is_err());
}
}