From: Vihiga Tyonum Date: Tue, 21 Apr 2026 10:03:00 +0000 (+0100) Subject: ref(config): move config into config subdir X-Git-Tag: v4.0.0~8^2~35 X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script-amount/static/FromScriptException.UnrecognizedScript.html?a=commitdiff_plain;h=739dec3f3f46330416d2395bf1f992f1abd531c4;p=bdk-cli ref(config): move config into config subdir --- diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 197dc07..0000000 --- a/src/config.rs +++ /dev/null @@ -1,302 +0,0 @@ -#[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, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct WalletConfigInner { - pub wallet: String, - pub network: String, - pub ext_descriptor: String, - pub int_descriptor: Option, - #[cfg(any(feature = "sqlite", feature = "redb"))] - pub database_type: String, - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - pub client_type: Option, - #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] - pub server_url: Option, - #[cfg(feature = "rpc")] - pub rpc_user: Option, - #[cfg(feature = "rpc")] - pub rpc_password: Option, - #[cfg(feature = "electrum")] - pub batch_size: Option, - #[cfg(feature = "esplora")] - pub parallel_requests: Option, - #[cfg(feature = "rpc")] - pub cookie: Option, -} - -impl WalletConfig { - /// Load configuration from a TOML file in the wallet's data directory - pub fn load(datadir: &Path) -> Result, 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)) - } - - /// Save configuration to a TOML file - 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(()) - } - - /// Get config for a wallet - pub fn get_wallet_opts(&self, wallet_name: &str) -> Result { - 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 { - 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 = (&inner).try_into(); - assert!(result.is_err()); - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..197dc07 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,302 @@ +#[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, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WalletConfigInner { + pub wallet: String, + pub network: String, + pub ext_descriptor: String, + pub int_descriptor: Option, + #[cfg(any(feature = "sqlite", feature = "redb"))] + pub database_type: String, + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + pub client_type: Option, + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + pub server_url: Option, + #[cfg(feature = "rpc")] + pub rpc_user: Option, + #[cfg(feature = "rpc")] + pub rpc_password: Option, + #[cfg(feature = "electrum")] + pub batch_size: Option, + #[cfg(feature = "esplora")] + pub parallel_requests: Option, + #[cfg(feature = "rpc")] + pub cookie: Option, +} + +impl WalletConfig { + /// Load configuration from a TOML file in the wallet's data directory + pub fn load(datadir: &Path) -> Result, 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)) + } + + /// Save configuration to a TOML file + 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(()) + } + + /// Get config for a wallet + pub fn get_wallet_opts(&self, wallet_name: &str) -> Result { + 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 { + 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 = (&inner).try_into(); + assert!(result.is_err()); + } +}