From 928fd9f7040212723a4190c416953d38006abb82 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Wed, 18 Jun 2025 09:01:44 +0100 Subject: [PATCH 1/4] feat: add initializing wallet config - add config.rs to store and retrieve values - add toml and serde crates for desearilizing and reading values - update utils, commands and handlers files to use values from config.toml -refactor prepare_wallet_db fn - fix clippy issues [Issue: #192] --- CHANGELOG.md | 2 + Cargo.lock | 91 +++++++++++++++++++++++-- Cargo.toml | 2 + Justfile | 2 +- README.md | 39 +++++++++++ src/commands.rs | 59 +++++++++++++++++ src/config.rs | 170 +++++++++++++++++++++++++++++++++++++++++++++++ src/handlers.rs | 156 ++++++++++++++++++++++++++++++++++++++----- src/main.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++- src/utils.rs | 16 +++-- 10 files changed, 679 insertions(+), 31 deletions(-) create mode 100644 src/config.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 437f97e9..48cc7300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changelog info is also documented on the [GitHub releases](https://github.com/bi page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. ## [Unreleased] +- Add wallet configs initialization for initialiazing and saving wallet configs +- Add wallet subcommand `config` to save wallet configs ## [2.0.0] diff --git a/Cargo.lock b/Cargo.lock index 4ff8e29c..614578cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,10 +192,12 @@ dependencies = [ "dirs", "env_logger", "log", + "serde", "serde_json", "shlex", "thiserror", "tokio", + "toml", "tracing", "tracing-subscriber", ] @@ -231,7 +233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f549541116c9f100cd7aa06b5e551e49bcc1f8dda1d0583e014de891aa943329" dependencies = [ "bitcoin", - "hashbrown", + "hashbrown 0.14.5", "serde", ] @@ -789,6 +791,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.13" @@ -1012,13 +1020,19 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + [[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1272,11 +1286,21 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ "bitflags", "cfg-if", @@ -2099,6 +2123,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2323,6 +2356,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -2815,6 +2889,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index d5767f31..e0b39ca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ serde_json = "1.0" thiserror = "2.0.11" tokio = { version = "1", features = ["full"] } cli-table = "0.5.0" +toml = "0.8.23" +serde= {version = "1.0", features = ["derive"]} # Optional dependencies bdk_bitcoind_rpc = { version = "0.21.0", features = ["std"], optional = true } diff --git a/Justfile b/Justfile index 013c4076..48425958 100644 --- a/Justfile +++ b/Justfile @@ -99,4 +99,4 @@ descriptors private wallet=default_wallet: # run any bitcoin-cli rpc command [group('rpc')] rpc command wallet=default_wallet: - bitcoin-cli -datadir={{default_datadir}} -regtest -rpcwallet={{wallet}} -rpcuser={{rpc_user}} -rpcpassword={{rpc_password}} {{command}} \ No newline at end of file + bitcoin-cli -datadir={{default_datadir}} -regtest -rpcwallet={{wallet}} -rpcuser={{rpc_user}} -rpcpassword={{rpc_password}} {{command}} diff --git a/README.md b/README.md index bca4e005..9748c779 100644 --- a/README.md +++ b/README.md @@ -202,3 +202,42 @@ You can optionally return outputs of commands in human-readable, tabular format cargo run --pretty -n signet wallet -w {wallet_name} -d sqlite balance ``` This is available for wallet, key, repl and compile features. When ommitted, outputs default to `JSON`. + +## Initializing Wallet Configurations with `init` Subcommand + +The `wallet init` sub-command simplifies wallet operations by saving configuration parameters to `config.toml` in the data directory (default `~/.bdk-bitcoin/config.toml`). This allows you to run subsequent `bdk-cli wallet` commands without repeatedly specifying configuration details, easing wallet operations. + +To initialize a wallet configuration, use the following command structure: + +```shell +cargo run --features -- -n wallet --wallet --ext-descriptor --int-descriptor --client-type --url [--database-type ] [--rpc-user ] + [--rpc-password ] init +``` + +For example, to initialize a wallet named `my_wallet` with `electrum` as the backend on `signet` network: + +```shell +cargo run --features electrum -- -n signet wallet -w my_wallet -e "tr(tprv8Z.../0/*)#dtdqk3dx" -i "tr(tprv8Z.../1/*)#ulgptya7" -d sqlite -c electrum -u "ssl://mempool.space:60602" init +``` + +To overwrite an existing wallet configuration, use the `--force` flag after the `init` sub-command. + +You can omit the following arguments to use their default values: + +`network`: Defaults to `testnet` + +`database_type`: Defaults to `sqlite` + +#### Using Saved Configuration + +After a wallet is initialized, you can then run `bdk-cli` wallet commands without specifying the parameters, referencing only the wallet subcommand. + +For example, with the wallet `my_wallet` initialized, generate a new address and sync the wallet as follow: + +```shell +cargo run wallet -w my_wallet --use-config new_address + +cargo run --features electrum wallet -w my_wallet --use-config sync +``` + +Note that each wallet has its own configuration, allowing multiple wallets with different configurations. diff --git a/src/commands.rs b/src/commands.rs index d3f2d983..3d8b5353 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,11 +13,14 @@ //! All subcommands are defined in the below enums. #![allow(clippy::large_enum_variant)] +use crate::config::WalletConfig; +use crate::error::BDKCliError as Error; use bdk_wallet::bitcoin::{ Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv}, }; use clap::{Args, Parser, Subcommand, ValueEnum, value_parser}; +use std::path::Path; #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] use crate::utils::parse_proxy_auth; @@ -126,6 +129,12 @@ pub enum CliSubCommand { /// Wallet operation subcommands. #[derive(Debug, Subcommand, Clone, PartialEq)] pub enum WalletSubCommand { + /// Initialize a wallet configuration and save to `config.toml`. + Init { + /// Overwrite existing wallet configuration if it exists. + #[arg(long = "force", default_value_t = false)] + force: bool, + }, #[cfg(any( feature = "electrum", feature = "esplora", @@ -228,6 +237,56 @@ pub struct WalletOpts { pub compactfilter_opts: CompactFilterOpts, } +impl WalletOpts { + /// Merges optional configuration values from config.toml into the current WalletOpts. + pub fn load_config(&mut self, wallet_name: &str, datadir: &Path) -> Result<(), Error> { + if let Some(config) = WalletConfig::load(datadir)? { + if let Ok(config_opts) = config.get_wallet_opts(wallet_name) { + self.verbose = self.verbose || config_opts.verbose; + #[cfg(feature = "electrum")] + { + self.batch_size = if self.batch_size != 10 { + self.batch_size + } else { + config_opts.batch_size + }; + } + #[cfg(feature = "esplora")] + { + self.parallel_requests = if self.parallel_requests != 5 { + self.parallel_requests + } else { + config_opts.parallel_requests + }; + } + #[cfg(feature = "rpc")] + { + self.basic_auth = if self.basic_auth != ("user".into(), "password".into()) { + self.basic_auth.clone() + } else { + config_opts.basic_auth + }; + self.cookie = self.cookie.take().or(config_opts.cookie); + } + #[cfg(feature = "cbf")] + { + if self.compactfilter_opts.conn_count == 2 + && config_opts.compactfilter_opts.conn_count != 2 + { + self.compactfilter_opts.conn_count = + config_opts.compactfilter_opts.conn_count; + } + if self.compactfilter_opts.skip_blocks.is_none() { + self.compactfilter_opts.skip_blocks = + config_opts.compactfilter_opts.skip_blocks; + } + } + } + } + Ok(()) + } +} + /// Options to configure a SOCKS5 proxy for a blockchain client connection. #[cfg(any(feature = "electrum", feature = "esplora"))] #[derive(Debug, Args, Clone, PartialEq, Eq)] diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..4e4f0d2b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,170 @@ +#[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; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WalletConfig { + pub network: Network, + 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 { + let wallet_config = self + .wallets + .get(wallet_name) + .ok_or_else(|| Error::Generic(format!("Wallet {wallet_name} not found in config")))?; + + let _network = match wallet_config.network.as_str() { + "bitcoin" => Network::Bitcoin, + "testnet" => Network::Testnet, + "regtest" => Network::Regtest, + "signet" => Network::Signet, + _ => { + return Err(Error::Generic("Invalid network".to_string())); + } + }; + + #[cfg(any(feature = "sqlite", feature = "redb"))] + let database_type = match wallet_config.database_type.as_str() { + #[cfg(feature = "sqlite")] + "sqlite" => DatabaseType::Sqlite, + #[cfg(feature = "redb")] + "redb" => DatabaseType::Redb, + _ => { + return Err(Error::Generic("Invalid database type".to_string())); + } + }; + + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + let client_type = match wallet_config.client_type.as_deref() { + #[cfg(feature = "electrum")] + Some("electrum") => ClientType::Electrum, + #[cfg(feature = "esplora")] + Some("esplora") => ClientType::Esplora, + #[cfg(feature = "rpc")] + Some("rpc") => ClientType::Rpc, + #[cfg(feature = "cbf")] + Some("cbf") => ClientType::Cbf, + _ => return Err(Error::Generic(format!("Invalid client type"))), + }; + + Ok(WalletOpts { + wallet: Some(wallet_config.wallet.clone()), + verbose: false, + ext_descriptor: Some(wallet_config.ext_descriptor.clone()), + int_descriptor: wallet_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: wallet_config + .server_url + .clone() + .ok_or_else(|| Error::Generic(format!("Server url not found")))?, + #[cfg(feature = "electrum")] + batch_size: 10, + #[cfg(feature = "esplora")] + parallel_requests: 5, + #[cfg(feature = "rpc")] + basic_auth: ( + wallet_config + .rpc_user + .clone() + .unwrap_or_else(|| "user".into()), + wallet_config + .rpc_password + .clone() + .unwrap_or_else(|| "password".into()), + ), + #[cfg(feature = "rpc")] + cookie: wallet_config.cookie.clone(), + #[cfg(feature = "cbf")] + compactfilter_opts: crate::commands::CompactFilterOpts { + conn_count: 2, + skip_blocks: None, + }, + }) + } +} diff --git a/src/handlers.rs b/src/handlers.rs index 6c58a838..d4b2bfc0 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -12,6 +12,7 @@ use crate::commands::OfflineWalletSubCommand::*; use crate::commands::*; +use crate::config::{WalletConfig, WalletConfigInner}; use crate::error::BDKCliError as Error; #[cfg(any(feature = "sqlite", feature = "redb"))] use crate::persister::Persister; @@ -51,12 +52,13 @@ use {crate::utils::BlockchainClient::KyotoClient, bdk_kyoto::LightClient, tokio: #[cfg(feature = "electrum")] use crate::utils::BlockchainClient::Electrum; -use std::collections::BTreeMap; #[cfg(any(feature = "electrum", feature = "esplora"))] use std::collections::HashSet; +use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; #[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))] use std::io::Write; +use std::path::Path; use std::str::FromStr; #[cfg(any(feature = "redb", feature = "compiler"))] use std::sync::Arc; @@ -844,6 +846,107 @@ pub(crate) async fn handle_online_wallet_subcommand( } } +/// Handle wallet init subcommand to create or update config.toml +pub fn handle_init_subcommand( + datadir: &Path, + network: Network, + wallet_opts: &WalletOpts, + force: bool, +) -> Result { + let wallet_name = wallet_opts + .wallet + .as_ref() + .ok_or_else(|| Error::Generic("Wallet name is required".to_string()))?; + + let mut config = WalletConfig::load(datadir)?.unwrap_or(WalletConfig { + network, + wallets: HashMap::new(), + }); + + if config.wallets.contains_key(wallet_name) && !force { + return Err(Error::Generic(format!( + "Wallet '{wallet_name}' already exists in config.toml. Use --force to overwrite." + ))); + } + + let ext_descriptor = wallet_opts + .ext_descriptor + .clone() + .ok_or_else(|| Error::Generic("External descriptor is required".to_string()))?; + let int_descriptor = wallet_opts.int_descriptor.clone(); + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + let client_type = wallet_opts.client_type.clone(); + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + let url = &wallet_opts.url.clone(); + #[cfg(any(feature = "sqlite", feature = "redb"))] + let database_type = match wallet_opts.database_type { + #[cfg(feature = "sqlite")] + DatabaseType::Sqlite => "sqlite".to_string(), + #[cfg(feature = "redb")] + DatabaseType::Redb => "redb".to_string(), + }; + + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + let client_type = match client_type { + #[cfg(feature = "electrum")] + ClientType::Electrum => "electrum".to_string(), + #[cfg(feature = "esplora")] + ClientType::Esplora => "esplora".to_string(), + #[cfg(feature = "rpc")] + ClientType::Rpc => "rpc".to_string(), + #[cfg(feature = "cbf")] + ClientType::Cbf => "cbf".to_string(), + }; + + let wallet_config = WalletConfigInner { + wallet: wallet_name.to_string(), + network: network.to_string(), + ext_descriptor, + int_descriptor, + #[cfg(any(feature = "sqlite", feature = "redb"))] + database_type, + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + client_type: Some(client_type), + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc",))] + server_url: Some(url.to_string()), + #[cfg(feature = "rpc")] + rpc_user: Some(wallet_opts.basic_auth.0.clone()), + #[cfg(feature = "rpc")] + rpc_password: Some(wallet_opts.basic_auth.1.clone()), + #[cfg(feature = "electrum")] + batch_size: Some(wallet_opts.batch_size), + #[cfg(feature = "esplora")] + parallel_requests: Some(wallet_opts.parallel_requests), + #[cfg(feature = "rpc")] + cookie: wallet_opts.cookie.clone(), + }; + + config.network = network; + config + .wallets + .insert(wallet_name.to_string(), wallet_config); + config.save(datadir)?; + + Ok(serde_json::to_string_pretty(&json!({ + "message": format!("Wallet '{wallet_name}' initialized successfully in {:?}", datadir.join("config.toml")) + }))?) +} + /// Determine if PSBT has final script sigs or witnesses for all unsigned tx inputs. #[cfg(any( feature = "electrum", @@ -1048,8 +1151,9 @@ pub(crate) fn handle_compile_subcommand( pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { let network = cli_opts.network; let pretty = cli_opts.pretty; + let subcommand = cli_opts.subcommand.clone(); - let result: Result = match cli_opts.subcommand { + let result: Result = match subcommand { #[cfg(any( feature = "electrum", feature = "esplora", @@ -1057,13 +1161,11 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { feature = "rpc" ))] CliSubCommand::Wallet { - ref wallet_opts, + mut wallet_opts, subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), } => { - // let network = cli_opts.network; let home_dir = prepare_home_dir(cli_opts.datadir)?; - let wallet_name = &wallet_opts.wallet; - let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?; + let database_path = prepare_wallet_db_dir(&home_dir, &mut wallet_opts)?; #[cfg(any(feature = "sqlite", feature = "redb"))] let result = { @@ -1077,6 +1179,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } #[cfg(feature = "redb")] DatabaseType::Redb => { + let wallet_name = &wallet_opts.wallet; let db = Arc::new(bdk_redb::redb::Database::create( home_dir.join("wallet.redb"), )?); @@ -1113,18 +1216,18 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { Ok(result) } CliSubCommand::Wallet { - ref wallet_opts, - subcommand: WalletSubCommand::OfflineWalletSubCommand(ref offline_subcommand), + mut wallet_opts, + subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), } => { let network = cli_opts.network; + let datadir = cli_opts.datadir.clone(); #[cfg(any(feature = "sqlite", feature = "redb"))] let result = { - let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; - let wallet_name = &wallet_opts.wallet; + let home_dir = prepare_home_dir(datadir)?; let mut persister: Persister = match &wallet_opts.database_type { #[cfg(feature = "sqlite")] DatabaseType::Sqlite => { - let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?; + let database_path = prepare_wallet_db_dir(&home_dir, &mut wallet_opts)?; let db_file = database_path.join("wallet.sqlite"); let connection = Connection::open(db_file)?; log::debug!("Sqlite database opened successfully"); @@ -1132,6 +1235,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } #[cfg(feature = "redb")] DatabaseType::Redb => { + let wallet_name = &wallet_opts.wallet; + let db = Arc::new(bdk_redb::redb::Database::create( home_dir.join("wallet.redb"), )?); @@ -1144,11 +1249,11 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } }; - let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?; + let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; let result = handle_offline_wallet_subcommand( &mut wallet, - wallet_opts, + &wallet_opts, &cli_opts, offline_subcommand.clone(), )?; @@ -1167,6 +1272,15 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { }; Ok(result) } + CliSubCommand::Wallet { + wallet_opts, + subcommand: WalletSubCommand::Init { force }, + } => { + let network = cli_opts.network; + let home_dir = prepare_home_dir(cli_opts.datadir)?; + let result = handle_init_subcommand(&home_dir, network, &wallet_opts, force)?; + Ok(result) + } CliSubCommand::Key { subcommand: key_subcommand, } => { @@ -1186,14 +1300,13 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { let network = cli_opts.network; #[cfg(any(feature = "sqlite", feature = "redb"))] let (mut wallet, mut persister) = { - let wallet_name = &wallet_opts.wallet; - let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; let mut persister: Persister = match &wallet_opts.database_type { #[cfg(feature = "sqlite")] DatabaseType::Sqlite => { - let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?; + let database_path = + prepare_wallet_db_dir(&home_dir, &mut wallet_opts.clone())?; let db_file = database_path.join("wallet.sqlite"); let connection = Connection::open(db_file)?; log::debug!("Sqlite database opened successfully"); @@ -1201,6 +1314,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } #[cfg(feature = "redb")] DatabaseType::Redb => { + let wallet_name = &wallet_opts.wallet; let db = Arc::new(bdk_redb::redb::Database::create( home_dir.join("wallet.redb"), )?); @@ -1218,8 +1332,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { #[cfg(not(any(feature = "sqlite", feature = "redb")))] let mut wallet = new_wallet(network, &wallet_opts)?; let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; - let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?; - + let database_path = prepare_wallet_db_dir(&home_dir, &mut wallet_opts.clone())?; loop { let line = readline()?; let line = line.trim(); @@ -1302,6 +1415,13 @@ async fn respond( .map_err(|e| e.to_string())?; Some(value) } + ReplSubCommand::Wallet { + subcommand: WalletSubCommand::Init { force }, + } => { + let value = handle_init_subcommand(&_datadir, network, wallet_opts, force) + .map_err(|e| e.to_string())?; + Some(value) + } ReplSubCommand::Key { subcommand } => { let value = handle_key_subcommand(network, subcommand, cli_opts.pretty) .map_err(|e| e.to_string())?; diff --git a/src/main.rs b/src/main.rs index c69aecc7..f7308e5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ #![warn(missing_docs)] mod commands; +mod config; mod error; mod handlers; #[cfg(any(feature = "sqlite", feature = "redb"))] @@ -19,15 +20,185 @@ mod utils; use bdk_wallet::bitcoin::Network; use log::{debug, error, warn}; +use std::env; +use std::path::PathBuf; use crate::commands::CliOpts; +use crate::config::WalletConfig; +use crate::error::BDKCliError as Error; use crate::handlers::*; +use crate::utils::prepare_home_dir; use clap::Parser; +fn is_value_arg(arg: &str) -> bool { + matches!( + arg, + "-w" | "--wallet" + | "-e" + | "--ext-descriptor" + | "-i" + | "--int-descriptor" + | "-c" + | "--client-type" + | "-d" + | "--database-type" + | "-u" + | "--url" + | "-b" + | "--batch-size" + | "-p" + | "--parallel-requests" + | "-a" + | "--basic-auth" + | "--cookie" + | "-n" + | "--network" + | "--datadir" + ) +} + +/// Inject configuration values from config.toml +/// when --use-config is present, except for the init subcommand. +fn preprocess_args(args: &mut Vec) -> Result<(), Error> { + let use_config = args.iter().any(|arg| arg == "--use-config"); + + let is_init = args.iter().any(|arg| arg == "init"); + + if !use_config || is_init { + return Ok(()); + } + + let mut wallet_name: Option = None; + let mut datadir: Option = None; + + let mut i = 1; + while i < args.len() { + if args[i] == "-w" || args[i] == "--wallet" { + if i + 1 < args.len() { + wallet_name = Some(args[i + 1].clone()); + } + } else if (args[i] == "-d" || args[i] == "--datadir") && i + 1 < args.len() { + datadir = Some(args[i + 1].clone()); + } + i += if is_value_arg(&args[i]) && i + 1 < args.len() { + 2 + } else { + 1 + }; + } + + if let Some(wallet_name) = wallet_name { + let home_dir = prepare_home_dir(datadir.map(PathBuf::from))?; + if let Ok(Some(config)) = WalletConfig::load(&home_dir) { + if let Some(wallet_config) = config.wallets.get(&wallet_name) { + let mut top_level_injections: Vec = Vec::new(); + let mut wallet_injections: Vec = Vec::new(); + + if !args.iter().any(|arg| arg == "-n" || arg == "--network") { + top_level_injections.push("--network".to_string()); + top_level_injections.push(wallet_config.network.clone()); + } + + if !args + .iter() + .any(|arg| arg == "-e" || arg == "--ext-descriptor") + { + wallet_injections.push("--ext-descriptor".to_string()); + wallet_injections.push(wallet_config.ext_descriptor.clone()); + } + if !args + .iter() + .any(|arg| arg == "-i" || arg == "--int-descriptor") + { + if let Some(int_descriptor) = &wallet_config.int_descriptor { + wallet_injections.push("--int-descriptor".to_string()); + wallet_injections.push(int_descriptor.clone()); + } + } + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + if !args.iter().any(|arg| arg == "-c" || arg == "--client-type") { + if let Some(ct) = &wallet_config.client_type { + wallet_injections.push("--client-type".to_string()); + wallet_injections.push(ct.clone()); + } + } + if !args + .iter() + .any(|arg| arg == "-d" || arg == "--database-type") + { + #[cfg(any(feature = "sqlite", feature = "redb"))] + { + wallet_injections.push("--database-type".to_string()); + wallet_injections.push(wallet_config.database_type.clone()); + } + } + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + if !args.iter().any(|arg| arg == "-u" || arg == "--url") { + if let Some(url) = &wallet_config.server_url { + wallet_injections.push("--url".to_string()); + wallet_injections.push(url.clone()); + } + } + + let mut top_level_insert_pos = 1; + while top_level_insert_pos < args.len() + && args[top_level_insert_pos].starts_with('-') + { + if is_value_arg(&args[top_level_insert_pos]) + && top_level_insert_pos + 1 < args.len() + { + top_level_insert_pos += 2; + } else { + top_level_insert_pos += 1; + } + } + args.splice( + top_level_insert_pos..top_level_insert_pos, + top_level_injections, + ); + + let wallet_pos = args + .iter() + .position(|arg| arg == "wallet") + .unwrap_or(args.len()); + let mut wallet_insert_pos = wallet_pos + 1; + while wallet_insert_pos < args.len() && args[wallet_insert_pos].starts_with('-') { + if is_value_arg(&args[wallet_insert_pos]) && wallet_insert_pos + 1 < args.len() + { + wallet_insert_pos += 2; + } else { + wallet_insert_pos += 1; + } + } + args.splice(wallet_insert_pos..wallet_insert_pos, wallet_injections); + } + } + } + + Ok(()) +} + #[tokio::main] async fn main() { env_logger::init(); - let cli_opts: CliOpts = CliOpts::parse(); + + let mut args: Vec = env::args().collect(); + + if let Err(e) = preprocess_args(&mut args) { + error!("Failed to preprocess arguments: {e}"); + std::process::exit(1); + } + + if let Some(pos) = args.iter().position(|arg| arg == "--use-config") { + args.remove(pos); + } + + let cli_opts: CliOpts = CliOpts::parse_from(args); let network = &cli_opts.network; debug!("network: {network:?}"); diff --git a/src/utils.rs b/src/utils.rs index 8a3ee040..5afd4ff8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -121,16 +121,18 @@ pub(crate) fn prepare_home_dir(home_path: Option) -> Result, home_path: &Path, + wallet_opts: &mut WalletOpts, ) -> Result { let mut dir = home_path.to_owned(); + let wallet_name = wallet_opts.wallet.clone(); if let Some(wallet_name) = wallet_name { - dir.push(wallet_name); - } + dir.push(&wallet_name); - if !dir.exists() { - std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; + if !dir.exists() { + std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; + } + wallet_opts.load_config(wallet_name.as_str(), home_path)?; } Ok(dir) @@ -175,7 +177,7 @@ pub(crate) fn new_blockchain_client( _datadir: PathBuf, ) -> Result { #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] - let url = wallet_opts.url.as_str(); + let url = &wallet_opts.url; let client = match wallet_opts.client_type { #[cfg(feature = "electrum")] ClientType::Electrum => { @@ -188,7 +190,7 @@ pub(crate) fn new_blockchain_client( } #[cfg(feature = "esplora")] ClientType::Esplora => { - let client = bdk_esplora::esplora_client::Builder::new(url).build_async()?; + let client = bdk_esplora::esplora_client::Builder::new(&url).build_async()?; BlockchainClient::Esplora { client: Box::new(client), parallel_requests: wallet_opts.parallel_requests, From c18ac56fb65b62448dd7a3d8919a17c19d799646 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Fri, 3 Oct 2025 11:56:51 +0100 Subject: [PATCH 2/4] feat(init-wallet): rename init & add walletopts - rename init to config and move walletopts as config options --- src/commands.rs | 79 ++++++++--------------------------- src/config.rs | 2 +- src/handlers.rs | 109 ++++++++++++++++++++++++++---------------------- src/utils.rs | 61 ++++++++++----------------- 4 files changed, 98 insertions(+), 153 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 3d8b5353..fd7d40bf 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,14 +13,11 @@ //! All subcommands are defined in the below enums. #![allow(clippy::large_enum_variant)] -use crate::config::WalletConfig; -use crate::error::BDKCliError as Error; use bdk_wallet::bitcoin::{ Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv}, }; use clap::{Args, Parser, Subcommand, ValueEnum, value_parser}; -use std::path::Path; #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] use crate::utils::parse_proxy_auth; @@ -72,8 +69,10 @@ pub enum CliSubCommand { /// needs backend like `sync` and `broadcast`, compile the binary with specific backend feature /// and use the configuration options below to configure for that backend. Wallet { - #[command(flatten)] - wallet_opts: WalletOpts, + /// Selects the wallet to use. + #[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)] + wallet: String, + #[command(subcommand)] subcommand: WalletSubCommand, }, @@ -106,6 +105,10 @@ pub enum CliSubCommand { /// REPL command loop can be used to make recurring callbacks to an already loaded wallet. /// This mode is useful for hands on live testing of wallet operations. Repl { + /// Wallet name for this REPL session + #[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)] + wallet: String, + #[command(flatten)] wallet_opts: WalletOpts, }, @@ -129,11 +132,14 @@ pub enum CliSubCommand { /// Wallet operation subcommands. #[derive(Debug, Subcommand, Clone, PartialEq)] pub enum WalletSubCommand { - /// Initialize a wallet configuration and save to `config.toml`. - Init { + /// Save wallet configuration to `config.toml`. + Config { /// Overwrite existing wallet configuration if it exists. - #[arg(long = "force", default_value_t = false)] + #[arg(short = 'f', long = "force", default_value_t = false)] force: bool, + + #[command(flatten)] + wallet_opts: WalletOpts, }, #[cfg(any( feature = "electrum", @@ -179,14 +185,15 @@ pub enum ClientType { #[derive(Debug, Args, Clone, PartialEq, Eq)] pub struct WalletOpts { /// Selects the wallet to use. - #[arg(env = "WALLET_NAME", short = 'w', long = "wallet")] + #[arg(skip)] pub wallet: Option, + // #[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)] /// Adds verbosity, returns PSBT in JSON format alongside serialized, displays expanded objects. #[arg(env = "VERBOSE", short = 'v', long = "verbose")] pub verbose: bool, /// Sets the descriptor to use for the external addresses. - #[arg(env = "EXT_DESCRIPTOR", short = 'e', long)] - pub ext_descriptor: Option, + #[arg(env = "EXT_DESCRIPTOR", short = 'e', long, required = true)] + pub ext_descriptor: String, /// Sets the descriptor to use for internal/change addresses. #[arg(env = "INT_DESCRIPTOR", short = 'i', long)] pub int_descriptor: Option, @@ -237,56 +244,6 @@ pub struct WalletOpts { pub compactfilter_opts: CompactFilterOpts, } -impl WalletOpts { - /// Merges optional configuration values from config.toml into the current WalletOpts. - pub fn load_config(&mut self, wallet_name: &str, datadir: &Path) -> Result<(), Error> { - if let Some(config) = WalletConfig::load(datadir)? { - if let Ok(config_opts) = config.get_wallet_opts(wallet_name) { - self.verbose = self.verbose || config_opts.verbose; - #[cfg(feature = "electrum")] - { - self.batch_size = if self.batch_size != 10 { - self.batch_size - } else { - config_opts.batch_size - }; - } - #[cfg(feature = "esplora")] - { - self.parallel_requests = if self.parallel_requests != 5 { - self.parallel_requests - } else { - config_opts.parallel_requests - }; - } - #[cfg(feature = "rpc")] - { - self.basic_auth = if self.basic_auth != ("user".into(), "password".into()) { - self.basic_auth.clone() - } else { - config_opts.basic_auth - }; - self.cookie = self.cookie.take().or(config_opts.cookie); - } - #[cfg(feature = "cbf")] - { - if self.compactfilter_opts.conn_count == 2 - && config_opts.compactfilter_opts.conn_count != 2 - { - self.compactfilter_opts.conn_count = - config_opts.compactfilter_opts.conn_count; - } - if self.compactfilter_opts.skip_blocks.is_none() { - self.compactfilter_opts.skip_blocks = - config_opts.compactfilter_opts.skip_blocks; - } - } - } - } - Ok(()) - } -} - /// Options to configure a SOCKS5 proxy for a blockchain client connection. #[cfg(any(feature = "electrum", feature = "esplora"))] #[derive(Debug, Args, Clone, PartialEq, Eq)] diff --git a/src/config.rs b/src/config.rs index 4e4f0d2b..c6f5e806 100644 --- a/src/config.rs +++ b/src/config.rs @@ -127,7 +127,7 @@ impl WalletConfig { Ok(WalletOpts { wallet: Some(wallet_config.wallet.clone()), verbose: false, - ext_descriptor: Some(wallet_config.ext_descriptor.clone()), + ext_descriptor: wallet_config.ext_descriptor.clone(), int_descriptor: wallet_config.int_descriptor.clone(), #[cfg(any( feature = "electrum", diff --git a/src/handlers.rs b/src/handlers.rs index d4b2bfc0..1dcaa25e 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -846,33 +846,26 @@ pub(crate) async fn handle_online_wallet_subcommand( } } -/// Handle wallet init subcommand to create or update config.toml -pub fn handle_init_subcommand( +/// Handle wallet config subcommand to create or update config.toml +pub fn handle_config_subcommand( datadir: &Path, network: Network, + wallet: String, wallet_opts: &WalletOpts, force: bool, ) -> Result { - let wallet_name = wallet_opts - .wallet - .as_ref() - .ok_or_else(|| Error::Generic("Wallet name is required".to_string()))?; - let mut config = WalletConfig::load(datadir)?.unwrap_or(WalletConfig { network, wallets: HashMap::new(), }); - if config.wallets.contains_key(wallet_name) && !force { + if config.wallets.contains_key(&wallet) && !force { return Err(Error::Generic(format!( - "Wallet '{wallet_name}' already exists in config.toml. Use --force to overwrite." + "Wallet '{wallet}' already exists in config.toml. Use --force to overwrite." ))); } - let ext_descriptor = wallet_opts - .ext_descriptor - .clone() - .ok_or_else(|| Error::Generic("External descriptor is required".to_string()))?; + let ext_descriptor = wallet_opts.ext_descriptor.clone(); let int_descriptor = wallet_opts.int_descriptor.clone(); #[cfg(any( feature = "electrum", @@ -909,7 +902,7 @@ pub fn handle_init_subcommand( }; let wallet_config = WalletConfigInner { - wallet: wallet_name.to_string(), + wallet: wallet.clone(), network: network.to_string(), ext_descriptor, int_descriptor, @@ -937,13 +930,11 @@ pub fn handle_init_subcommand( }; config.network = network; - config - .wallets - .insert(wallet_name.to_string(), wallet_config); + config.wallets.insert(wallet.clone(), wallet_config); config.save(datadir)?; Ok(serde_json::to_string_pretty(&json!({ - "message": format!("Wallet '{wallet_name}' initialized successfully in {:?}", datadir.join("config.toml")) + "message": format!("Wallet '{wallet}' initialized successfully in {:?}", datadir.join("config.toml")) }))?) } @@ -1161,11 +1152,15 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { feature = "rpc" ))] CliSubCommand::Wallet { - mut wallet_opts, + wallet, subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), } => { let home_dir = prepare_home_dir(cli_opts.datadir)?; - let database_path = prepare_wallet_db_dir(&home_dir, &mut wallet_opts)?; + + let config = WalletConfig::load(&home_dir)? + .ok_or(Error::Generic("No config found".to_string()))?; + let wallet_opts = config.get_wallet_opts(&wallet)?; + let database_path = prepare_wallet_db_dir(&home_dir, &wallet)?; #[cfg(any(feature = "sqlite", feature = "redb"))] let result = { @@ -1216,18 +1211,22 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { Ok(result) } CliSubCommand::Wallet { - mut wallet_opts, + wallet: wallet_name, subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), } => { let network = cli_opts.network; let datadir = cli_opts.datadir.clone(); + let home_dir = prepare_home_dir(datadir)?; + let config = WalletConfig::load(&home_dir)?.ok_or(Error::Generic(format!( + "No config found for wallet '{wallet_name}'" + )))?; + let wallet_opts = config.get_wallet_opts(&wallet_name)?; #[cfg(any(feature = "sqlite", feature = "redb"))] let result = { - let home_dir = prepare_home_dir(datadir)?; let mut persister: Persister = match &wallet_opts.database_type { #[cfg(feature = "sqlite")] DatabaseType::Sqlite => { - let database_path = prepare_wallet_db_dir(&home_dir, &mut wallet_opts)?; + let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; let db_file = database_path.join("wallet.sqlite"); let connection = Connection::open(db_file)?; log::debug!("Sqlite database opened successfully"); @@ -1235,15 +1234,10 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } #[cfg(feature = "redb")] DatabaseType::Redb => { - let wallet_name = &wallet_opts.wallet; - let db = Arc::new(bdk_redb::redb::Database::create( home_dir.join("wallet.redb"), )?); - let store = RedbStore::new( - db, - wallet_name.as_deref().unwrap_or("wallet").to_string(), - )?; + let store = RedbStore::new(db, wallet_name)?; log::debug!("Redb database opened successfully"); Persister::RedbStore(store) } @@ -1273,12 +1267,12 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { Ok(result) } CliSubCommand::Wallet { - wallet_opts, - subcommand: WalletSubCommand::Init { force }, + wallet, + subcommand: WalletSubCommand::Config { force, wallet_opts }, } => { let network = cli_opts.network; let home_dir = prepare_home_dir(cli_opts.datadir)?; - let result = handle_init_subcommand(&home_dir, network, &wallet_opts, force)?; + let result = handle_config_subcommand(&home_dir, network, wallet, &wallet_opts, force)?; Ok(result) } CliSubCommand::Key { @@ -1296,17 +1290,26 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { Ok(result) } #[cfg(feature = "repl")] - CliSubCommand::Repl { ref wallet_opts } => { + CliSubCommand::Repl { + wallet: wallet_name, + mut wallet_opts, + } => { let network = cli_opts.network; + let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; + wallet_opts.wallet = Some(wallet_name.clone()); + + let config = WalletConfig::load(&home_dir)?.ok_or(Error::Generic(format!( + "No config found for wallet {}", + wallet_name.clone() + )))?; + let loaded_wallet_opts = config.get_wallet_opts(&wallet_name)?; + #[cfg(any(feature = "sqlite", feature = "redb"))] let (mut wallet, mut persister) = { - let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; - - let mut persister: Persister = match &wallet_opts.database_type { + let mut persister: Persister = match &loaded_wallet_opts.database_type { #[cfg(feature = "sqlite")] DatabaseType::Sqlite => { - let database_path = - prepare_wallet_db_dir(&home_dir, &mut wallet_opts.clone())?; + let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; let db_file = database_path.join("wallet.sqlite"); let connection = Connection::open(db_file)?; log::debug!("Sqlite database opened successfully"); @@ -1314,25 +1317,21 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } #[cfg(feature = "redb")] DatabaseType::Redb => { - let wallet_name = &wallet_opts.wallet; let db = Arc::new(bdk_redb::redb::Database::create( home_dir.join("wallet.redb"), )?); - let store = RedbStore::new( - db, - wallet_name.as_deref().unwrap_or("wallet").to_string(), - )?; + let store = RedbStore::new(db, wallet_name.clone())?; log::debug!("Redb database opened successfully"); Persister::RedbStore(store) } }; - let wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?; + let wallet = new_persisted_wallet(network, &mut persister, &loaded_wallet_opts)?; (wallet, persister) }; #[cfg(not(any(feature = "sqlite", feature = "redb")))] - let mut wallet = new_wallet(network, &wallet_opts)?; + let mut wallet = new_wallet(network, &loaded_wallet_opts)?; let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; - let database_path = prepare_wallet_db_dir(&home_dir, &mut wallet_opts.clone())?; + let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; loop { let line = readline()?; let line = line.trim(); @@ -1343,7 +1342,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { let result = respond( network, &mut wallet, - wallet_opts, + &wallet_name, + &mut wallet_opts.clone(), line, database_path.clone(), &cli_opts, @@ -1381,7 +1381,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { async fn respond( network: Network, wallet: &mut Wallet, - wallet_opts: &WalletOpts, + wallet_name: &String, + wallet_opts: &mut WalletOpts, line: &str, _datadir: std::path::PathBuf, cli_opts: &CliOpts, @@ -1416,10 +1417,16 @@ async fn respond( Some(value) } ReplSubCommand::Wallet { - subcommand: WalletSubCommand::Init { force }, + subcommand: WalletSubCommand::Config { force, wallet_opts }, } => { - let value = handle_init_subcommand(&_datadir, network, wallet_opts, force) - .map_err(|e| e.to_string())?; + let value = handle_config_subcommand( + &_datadir, + network, + wallet_name.to_string(), + &wallet_opts, + force, + ) + .map_err(|e| e.to_string())?; Some(value) } ReplSubCommand::Key { subcommand } => { diff --git a/src/utils.rs b/src/utils.rs index 5afd4ff8..0318144a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -122,17 +122,13 @@ pub(crate) fn prepare_home_dir(home_path: Option) -> Result Result { let mut dir = home_path.to_owned(); - let wallet_name = wallet_opts.wallet.clone(); - if let Some(wallet_name) = wallet_name { - dir.push(&wallet_name); + dir.push(wallet_name); - if !dir.exists() { - std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; - } - wallet_opts.load_config(wallet_name.as_str(), home_path)?; + if !dir.exists() { + std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; } Ok(dir) @@ -245,17 +241,14 @@ where let int_descriptor = wallet_opts.int_descriptor.clone(); let mut wallet_load_params = Wallet::load(); - if ext_descriptor.is_some() { - wallet_load_params = - wallet_load_params.descriptor(KeychainKind::External, ext_descriptor.clone()); - } + wallet_load_params = + wallet_load_params.descriptor(KeychainKind::External, Some(ext_descriptor.clone())); + if int_descriptor.is_some() { wallet_load_params = wallet_load_params.descriptor(KeychainKind::Internal, int_descriptor.clone()); } - if ext_descriptor.is_some() || int_descriptor.is_some() { - wallet_load_params = wallet_load_params.extract_keys(); - } + wallet_load_params = wallet_load_params.extract_keys(); let wallet_opt = wallet_load_params .check_network(network) @@ -264,25 +257,16 @@ where let wallet = match wallet_opt { Some(wallet) => wallet, - None => match (ext_descriptor, int_descriptor) { - (Some(ext_descriptor), Some(int_descriptor)) => { - let wallet = Wallet::create(ext_descriptor, int_descriptor) - .network(network) - .create_wallet(persister) - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(wallet) - } - (Some(ext_descriptor), None) => { - let wallet = Wallet::create_single(ext_descriptor) - .network(network) - .create_wallet(persister) - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(wallet) - } - _ => Err(Error::Generic( - "An external descriptor is required.".to_string(), - )), - }?, + None => match int_descriptor { + Some(int_descriptor) => Wallet::create(ext_descriptor, int_descriptor) + .network(network) + .create_wallet(persister) + .map_err(|e| Error::Generic(e.to_string()))?, + None => Wallet::create_single(ext_descriptor) + .network(network) + .create_wallet(persister) + .map_err(|e| Error::Generic(e.to_string()))?, + }, }; Ok(wallet) @@ -294,22 +278,19 @@ pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result { + match int_descriptor { + Some(int_descriptor) => { let wallet = Wallet::create(ext_descriptor, int_descriptor) .network(network) .create_wallet_no_persist()?; Ok(wallet) } - (Some(ext_descriptor), None) => { + None => { let wallet = Wallet::create_single(ext_descriptor) .network(network) .create_wallet_no_persist()?; Ok(wallet) } - _ => Err(Error::Generic( - "An external descriptor is required.".to_string(), - )), } } From 2ec001728b12d09d0ec6a962f06fab0efa662daa Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Sat, 4 Oct 2025 02:42:55 +0100 Subject: [PATCH 3/4] feat(wallet-init): add `wallets` command - add wallets command - add warning for using priv descriptors - update readme - add loading network from config - fix review comments --- CHANGELOG.md | 2 +- README.md | 38 ++++----- src/commands.rs | 6 +- src/config.rs | 11 +-- src/handlers.rs | 206 ++++++++++++++++++++++++++++++++++++++++++------ src/main.rs | 172 +--------------------------------------- src/utils.rs | 10 ++- 7 files changed, 220 insertions(+), 225 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48cc7300..55a95b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ Changelog info is also documented on the [GitHub releases](https://github.com/bi page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. ## [Unreleased] -- Add wallet configs initialization for initialiazing and saving wallet configs - Add wallet subcommand `config` to save wallet configs +- Add `wallets` command to list all wallets saved configs ## [2.0.0] diff --git a/README.md b/README.md index 9748c779..3edfe8d5 100644 --- a/README.md +++ b/README.md @@ -203,41 +203,43 @@ cargo run --pretty -n signet wallet -w {wallet_name} -d sqlite balance ``` This is available for wallet, key, repl and compile features. When ommitted, outputs default to `JSON`. -## Initializing Wallet Configurations with `init` Subcommand +## Saving and using wallet configurations -The `wallet init` sub-command simplifies wallet operations by saving configuration parameters to `config.toml` in the data directory (default `~/.bdk-bitcoin/config.toml`). This allows you to run subsequent `bdk-cli wallet` commands without repeatedly specifying configuration details, easing wallet operations. +The `wallet config` sub-command allows you to save wallet settings to a `config.toml` file in the default directory (`~/.bdk-bitcoin/`) or custom directory specified with the `--datadir` flag. This eliminate the need to repeatedly specify descriptors, client types, and other parameters for each command. Once configured, you can use any wallet command by simply specifying the wallet name. All other parameters are automatically loaded from the saved configuration. -To initialize a wallet configuration, use the following command structure: +To save a wallet settings: ```shell -cargo run --features -- -n wallet --wallet --ext-descriptor --int-descriptor --client-type --url [--database-type ] [--rpc-user ] - [--rpc-password ] init +cargo run --features -- -n wallet --wallet config [ -f ] --ext-descriptor --int-descriptor --client-type --url [--database-type ] [--rpc-user ] + [--rpc-password ] ``` For example, to initialize a wallet named `my_wallet` with `electrum` as the backend on `signet` network: ```shell -cargo run --features electrum -- -n signet wallet -w my_wallet -e "tr(tprv8Z.../0/*)#dtdqk3dx" -i "tr(tprv8Z.../1/*)#ulgptya7" -d sqlite -c electrum -u "ssl://mempool.space:60602" init +cargo run --features electrum -- -n signet wallet -w my_wallet config -e "tr(tprv8Z.../0/*)#dtdqk3dx" -i "tr(tprv8Z.../1/*)#ulgptya7" -d sqlite -c electrum -u "ssl://mempool.space:60602" ``` -To overwrite an existing wallet configuration, use the `--force` flag after the `init` sub-command. +To overwrite an existing wallet configuration, use the `--force` flag after the `config` sub-command. -You can omit the following arguments to use their default values: +#### Using a Configured Wallet -`network`: Defaults to `testnet` +Once configured, use any wallet command with just the wallet name: -`database_type`: Defaults to `sqlite` - -#### Using Saved Configuration - -After a wallet is initialized, you can then run `bdk-cli` wallet commands without specifying the parameters, referencing only the wallet subcommand. - -For example, with the wallet `my_wallet` initialized, generate a new address and sync the wallet as follow: ```shell -cargo run wallet -w my_wallet --use-config new_address +cargo run --features electrum wallet -w my_wallet new_address -cargo run --features electrum wallet -w my_wallet --use-config sync +cargo run --features electrum wallet -w my_wallet full_scan ``` Note that each wallet has its own configuration, allowing multiple wallets with different configurations. + +#### View all saved Wallet Configs + +To view all saved wallet configurations: + +```shell +cargo run wallets` +``` +You can also use the `--pretty` flag for a formatted output. diff --git a/src/commands.rs b/src/commands.rs index fd7d40bf..f7e70596 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -108,9 +108,6 @@ pub enum CliSubCommand { /// Wallet name for this REPL session #[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)] wallet: String, - - #[command(flatten)] - wallet_opts: WalletOpts, }, /// Output Descriptors operations. /// @@ -128,6 +125,8 @@ pub enum CliSubCommand { /// Optional key: xprv, xpub, or mnemonic phrase key: Option, }, + /// List all saved wallet configurations. + Wallets, } /// Wallet operation subcommands. #[derive(Debug, Subcommand, Clone, PartialEq)] @@ -187,7 +186,6 @@ pub struct WalletOpts { /// Selects the wallet to use. #[arg(skip)] pub wallet: Option, - // #[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)] /// Adds verbosity, returns PSBT in JSON format alongside serialized, displays expanded objects. #[arg(env = "VERBOSE", short = 'v', long = "verbose")] pub verbose: bool, diff --git a/src/config.rs b/src/config.rs index c6f5e806..f024f0be 100644 --- a/src/config.rs +++ b/src/config.rs @@ -90,6 +90,7 @@ impl WalletConfig { "testnet" => Network::Testnet, "regtest" => Network::Regtest, "signet" => Network::Signet, + "testnet4" => Network::Testnet4, _ => { return Err(Error::Generic("Invalid network".to_string())); } @@ -105,7 +106,6 @@ impl WalletConfig { return Err(Error::Generic("Invalid database type".to_string())); } }; - #[cfg(any( feature = "electrum", feature = "esplora", @@ -144,9 +144,9 @@ impl WalletConfig { .clone() .ok_or_else(|| Error::Generic(format!("Server url not found")))?, #[cfg(feature = "electrum")] - batch_size: 10, + batch_size: wallet_config.batch_size.unwrap_or(10), #[cfg(feature = "esplora")] - parallel_requests: 5, + parallel_requests: wallet_config.parallel_requests.unwrap_or(5), #[cfg(feature = "rpc")] basic_auth: ( wallet_config @@ -161,10 +161,7 @@ impl WalletConfig { #[cfg(feature = "rpc")] cookie: wallet_config.cookie.clone(), #[cfg(feature = "cbf")] - compactfilter_opts: crate::commands::CompactFilterOpts { - conn_count: 2, - skip_blocks: None, - }, + compactfilter_opts: crate::commands::CompactFilterOpts { conn_count: 2 }, }) } } diff --git a/src/handlers.rs b/src/handlers.rs index 1dcaa25e..8fd8a69d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -62,6 +62,14 @@ use std::path::Path; use std::str::FromStr; #[cfg(any(feature = "redb", feature = "compiler"))] use std::sync::Arc; + +#[cfg(feature = "electrum")] +use crate::utils::BlockchainClient::Electrum; +#[cfg(feature = "cbf")] +use bdk_kyoto::LightClient; +use bdk_wallet::bitcoin::base64::prelude::*; +#[cfg(feature = "cbf")] +use tokio::select; #[cfg(any( feature = "electrum", feature = "esplora", @@ -854,6 +862,35 @@ pub fn handle_config_subcommand( wallet_opts: &WalletOpts, force: bool, ) -> Result { + if network == Network::Bitcoin { + eprintln!( + "WARNING: You are configuring a wallet for Bitcoin MAINNET.\n\ + This software is experimental and not recommended for use with real funds.\n\ + Consider using a testnet for testing purposes. \n" + ); + } + + let ext_descriptor = wallet_opts.ext_descriptor.clone(); + let int_descriptor = wallet_opts.int_descriptor.clone(); + + if ext_descriptor.contains("xprv") || ext_descriptor.contains("tprv") { + eprintln!( + "WARNING: Your external descriptor contains PRIVATE KEYS.\n\ + Private keys will be saved in PLAINTEXT in the config file.\n\ + This is a security risk. Consider using public descriptors instead." + ); + } + + if let Some(ref internal_desc) = int_descriptor { + if internal_desc.contains("xprv") || internal_desc.contains("tprv") { + eprintln!( + "WARNING: Your internal descriptor contains PRIVATE KEYS.\n\ + Private keys will be saved in PLAINTEXT in the config file.\n\ + This is a security risk. Consider using public descriptors instead." + ); + } + } + let mut config = WalletConfig::load(datadir)?.unwrap_or(WalletConfig { network, wallets: HashMap::new(), @@ -865,8 +902,6 @@ pub fn handle_config_subcommand( ))); } - let ext_descriptor = wallet_opts.ext_descriptor.clone(); - let int_descriptor = wallet_opts.int_descriptor.clone(); #[cfg(any( feature = "electrum", feature = "esplora", @@ -1138,9 +1173,141 @@ pub(crate) fn handle_compile_subcommand( } } +/// Handle wallets command to show all saved wallet configurations +pub fn handle_wallets_subcommand(datadir: &Path, pretty: bool) -> Result { + let load_config = WalletConfig::load(datadir)?; + + let config = match load_config { + Some(c) if !c.wallets.is_empty() => c, + _ => { + return Ok(if pretty { + "No wallet configurations found.".to_string() + } else { + serde_json::to_string_pretty(&json!({ + "wallets": [] + }))? + }); + } + }; + + if pretty { + let mut rows: Vec> = vec![]; + + for (name, wallet_config) in config.wallets.iter() { + let mut row = vec![name.cell(), wallet_config.network.clone().cell()]; + + #[cfg(any(feature = "sqlite", feature = "redb"))] + row.push(wallet_config.database_type.clone().cell()); + + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + { + let client_str = wallet_config.client_type.as_deref().unwrap_or("N/A"); + row.push(client_str.cell()); + } + + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + { + let url_str = wallet_config.server_url.as_deref().unwrap_or("N/A"); + let display_url = if url_str.len() > 20 { + shorten(url_str, 15, 10) + } else { + url_str.to_string() + }; + row.push(display_url.cell()); + } + + let ext_desc_display = if wallet_config.ext_descriptor.len() > 40 { + shorten(&wallet_config.ext_descriptor, 20, 15) + } else { + wallet_config.ext_descriptor.clone() + }; + row.push(ext_desc_display.cell()); + + let has_int_desc = if wallet_config.int_descriptor.is_some() { + "Yes" + } else { + "No" + }; + row.push(has_int_desc.cell()); + + rows.push(row); + } + + let mut title_cells = vec!["Wallet Name".cell().bold(true), "Network".cell().bold(true)]; + + #[cfg(any(feature = "sqlite", feature = "redb"))] + title_cells.push("Database".cell().bold(true)); + + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + title_cells.push("Client".cell().bold(true)); + + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + title_cells.push("Server URL".cell().bold(true)); + + title_cells.push("External Desc".cell().bold(true)); + title_cells.push("Internal Desc".cell().bold(true)); + + let table = rows + .table() + .title(title_cells) + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } else { + let wallets_summary: Vec<_> = config + .wallets + .iter() + .map(|(name, wallet_config)| { + let mut wallet_json = json!({ + "name": name, + "network": wallet_config.network, + "ext_descriptor": wallet_config.ext_descriptor, + "int_descriptor": wallet_config.int_descriptor, + }); + + #[cfg(any(feature = "sqlite", feature = "redb"))] + { + wallet_json["database_type"] = json!(wallet_config.database_type.clone()); + } + + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + { + wallet_json["client_type"] = json!(wallet_config.client_type.clone()); + } + + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + { + wallet_json["server_url"] = json!(wallet_config.server_url.clone()); + } + + wallet_json + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "wallets": wallets_summary + }))?) + } +} + /// The global top level handler. pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { - let network = cli_opts.network; let pretty = cli_opts.pretty; let subcommand = cli_opts.subcommand.clone(); @@ -1157,9 +1324,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } => { let home_dir = prepare_home_dir(cli_opts.datadir)?; - let config = WalletConfig::load(&home_dir)? - .ok_or(Error::Generic("No config found".to_string()))?; - let wallet_opts = config.get_wallet_opts(&wallet)?; + let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet)?; + let database_path = prepare_wallet_db_dir(&home_dir, &wallet)?; #[cfg(any(feature = "sqlite", feature = "redb"))] @@ -1214,13 +1380,10 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { wallet: wallet_name, subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), } => { - let network = cli_opts.network; let datadir = cli_opts.datadir.clone(); let home_dir = prepare_home_dir(datadir)?; - let config = WalletConfig::load(&home_dir)?.ok_or(Error::Generic(format!( - "No config found for wallet '{wallet_name}'" - )))?; - let wallet_opts = config.get_wallet_opts(&wallet_name)?; + let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?; + #[cfg(any(feature = "sqlite", feature = "redb"))] let result = { let mut persister: Persister = match &wallet_opts.database_type { @@ -1275,9 +1438,15 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { let result = handle_config_subcommand(&home_dir, network, wallet, &wallet_opts, force)?; Ok(result) } + CliSubCommand::Wallets => { + let home_dir = prepare_home_dir(cli_opts.datadir)?; + let result = handle_wallets_subcommand(&home_dir, pretty)?; + Ok(result) + } CliSubCommand::Key { subcommand: key_subcommand, } => { + let network = cli_opts.network; let result = handle_key_subcommand(network, key_subcommand, pretty)?; Ok(result) } @@ -1286,27 +1455,20 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { policy, script_type, } => { + let network = cli_opts.network; let result = handle_compile_subcommand(network, policy, script_type, pretty)?; Ok(result) } #[cfg(feature = "repl")] CliSubCommand::Repl { wallet: wallet_name, - mut wallet_opts, } => { - let network = cli_opts.network; let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; - wallet_opts.wallet = Some(wallet_name.clone()); - - let config = WalletConfig::load(&home_dir)?.ok_or(Error::Generic(format!( - "No config found for wallet {}", - wallet_name.clone() - )))?; - let loaded_wallet_opts = config.get_wallet_opts(&wallet_name)?; + let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?; #[cfg(any(feature = "sqlite", feature = "redb"))] let (mut wallet, mut persister) = { - let mut persister: Persister = match &loaded_wallet_opts.database_type { + let mut persister: Persister = match &wallet_opts.database_type { #[cfg(feature = "sqlite")] DatabaseType::Sqlite => { let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; @@ -1325,7 +1487,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { Persister::RedbStore(store) } }; - let wallet = new_persisted_wallet(network, &mut persister, &loaded_wallet_opts)?; + let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; (wallet, persister) }; #[cfg(not(any(feature = "sqlite", feature = "redb")))] diff --git a/src/main.rs b/src/main.rs index f7308e5a..cb7fc38d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,185 +20,15 @@ mod utils; use bdk_wallet::bitcoin::Network; use log::{debug, error, warn}; -use std::env; -use std::path::PathBuf; use crate::commands::CliOpts; -use crate::config::WalletConfig; -use crate::error::BDKCliError as Error; use crate::handlers::*; -use crate::utils::prepare_home_dir; use clap::Parser; -fn is_value_arg(arg: &str) -> bool { - matches!( - arg, - "-w" | "--wallet" - | "-e" - | "--ext-descriptor" - | "-i" - | "--int-descriptor" - | "-c" - | "--client-type" - | "-d" - | "--database-type" - | "-u" - | "--url" - | "-b" - | "--batch-size" - | "-p" - | "--parallel-requests" - | "-a" - | "--basic-auth" - | "--cookie" - | "-n" - | "--network" - | "--datadir" - ) -} - -/// Inject configuration values from config.toml -/// when --use-config is present, except for the init subcommand. -fn preprocess_args(args: &mut Vec) -> Result<(), Error> { - let use_config = args.iter().any(|arg| arg == "--use-config"); - - let is_init = args.iter().any(|arg| arg == "init"); - - if !use_config || is_init { - return Ok(()); - } - - let mut wallet_name: Option = None; - let mut datadir: Option = None; - - let mut i = 1; - while i < args.len() { - if args[i] == "-w" || args[i] == "--wallet" { - if i + 1 < args.len() { - wallet_name = Some(args[i + 1].clone()); - } - } else if (args[i] == "-d" || args[i] == "--datadir") && i + 1 < args.len() { - datadir = Some(args[i + 1].clone()); - } - i += if is_value_arg(&args[i]) && i + 1 < args.len() { - 2 - } else { - 1 - }; - } - - if let Some(wallet_name) = wallet_name { - let home_dir = prepare_home_dir(datadir.map(PathBuf::from))?; - if let Ok(Some(config)) = WalletConfig::load(&home_dir) { - if let Some(wallet_config) = config.wallets.get(&wallet_name) { - let mut top_level_injections: Vec = Vec::new(); - let mut wallet_injections: Vec = Vec::new(); - - if !args.iter().any(|arg| arg == "-n" || arg == "--network") { - top_level_injections.push("--network".to_string()); - top_level_injections.push(wallet_config.network.clone()); - } - - if !args - .iter() - .any(|arg| arg == "-e" || arg == "--ext-descriptor") - { - wallet_injections.push("--ext-descriptor".to_string()); - wallet_injections.push(wallet_config.ext_descriptor.clone()); - } - if !args - .iter() - .any(|arg| arg == "-i" || arg == "--int-descriptor") - { - if let Some(int_descriptor) = &wallet_config.int_descriptor { - wallet_injections.push("--int-descriptor".to_string()); - wallet_injections.push(int_descriptor.clone()); - } - } - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - if !args.iter().any(|arg| arg == "-c" || arg == "--client-type") { - if let Some(ct) = &wallet_config.client_type { - wallet_injections.push("--client-type".to_string()); - wallet_injections.push(ct.clone()); - } - } - if !args - .iter() - .any(|arg| arg == "-d" || arg == "--database-type") - { - #[cfg(any(feature = "sqlite", feature = "redb"))] - { - wallet_injections.push("--database-type".to_string()); - wallet_injections.push(wallet_config.database_type.clone()); - } - } - #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] - if !args.iter().any(|arg| arg == "-u" || arg == "--url") { - if let Some(url) = &wallet_config.server_url { - wallet_injections.push("--url".to_string()); - wallet_injections.push(url.clone()); - } - } - - let mut top_level_insert_pos = 1; - while top_level_insert_pos < args.len() - && args[top_level_insert_pos].starts_with('-') - { - if is_value_arg(&args[top_level_insert_pos]) - && top_level_insert_pos + 1 < args.len() - { - top_level_insert_pos += 2; - } else { - top_level_insert_pos += 1; - } - } - args.splice( - top_level_insert_pos..top_level_insert_pos, - top_level_injections, - ); - - let wallet_pos = args - .iter() - .position(|arg| arg == "wallet") - .unwrap_or(args.len()); - let mut wallet_insert_pos = wallet_pos + 1; - while wallet_insert_pos < args.len() && args[wallet_insert_pos].starts_with('-') { - if is_value_arg(&args[wallet_insert_pos]) && wallet_insert_pos + 1 < args.len() - { - wallet_insert_pos += 2; - } else { - wallet_insert_pos += 1; - } - } - args.splice(wallet_insert_pos..wallet_insert_pos, wallet_injections); - } - } - } - - Ok(()) -} - #[tokio::main] async fn main() { env_logger::init(); - - let mut args: Vec = env::args().collect(); - - if let Err(e) = preprocess_args(&mut args) { - error!("Failed to preprocess arguments: {e}"); - std::process::exit(1); - } - - if let Some(pos) = args.iter().position(|arg| arg == "--use-config") { - args.remove(pos); - } - - let cli_opts: CliOpts = CliOpts::parse_from(args); + let cli_opts: CliOpts = CliOpts::parse(); let network = &cli_opts.network; debug!("network: {network:?}"); diff --git a/src/utils.rs b/src/utils.rs index 0318144a..5fc195a5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,6 +9,7 @@ //! Utility Tools //! //! This module includes all the utility tools used by the App. +use crate::config::WalletConfig; use crate::error::BDKCliError as Error; use std::{ fmt::Display, @@ -279,13 +280,13 @@ pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result { + Some(int_descriptor) => { let wallet = Wallet::create(ext_descriptor, int_descriptor) .network(network) .create_wallet_no_persist()?; Ok(wallet) } - None => { + None => { let wallet = Wallet::create_single(ext_descriptor) .network(network) .create_wallet_no_persist()?; @@ -363,6 +364,11 @@ pub async fn sync_kyoto_client(wallet: &mut Wallet, client: Box) -> pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { let displayable = displayable.to_string(); + + if displayable.len() <= (start + end) as usize { + return displayable; + } + let start_str: &str = &displayable[0..start as usize]; let end_str: &str = &displayable[displayable.len() - end as usize..]; format!("{start_str}...{end_str}") From 805d0752bcf6d3c5b84d86767193f9f07ec46943 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Tue, 11 Nov 2025 12:35:12 +0100 Subject: [PATCH 4/4] feat(wallet-conf): rebase and merge conflicts --- src/handlers.rs | 32 +++++++++++++------------------- src/utils.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index 8fd8a69d..08ce6f0e 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -63,13 +63,6 @@ use std::str::FromStr; #[cfg(any(feature = "redb", feature = "compiler"))] use std::sync::Arc; -#[cfg(feature = "electrum")] -use crate::utils::BlockchainClient::Electrum; -#[cfg(feature = "cbf")] -use bdk_kyoto::LightClient; -use bdk_wallet::bitcoin::base64::prelude::*; -#[cfg(feature = "cbf")] -use tokio::select; #[cfg(any( feature = "electrum", feature = "esplora", @@ -864,8 +857,8 @@ pub fn handle_config_subcommand( ) -> Result { if network == Network::Bitcoin { eprintln!( - "WARNING: You are configuring a wallet for Bitcoin MAINNET.\n\ - This software is experimental and not recommended for use with real funds.\n\ + "WARNING: You are configuring a wallet for Bitcoin MAINNET. + This software is experimental and not recommended for use with real funds. Consider using a testnet for testing purposes. \n" ); } @@ -875,18 +868,18 @@ pub fn handle_config_subcommand( if ext_descriptor.contains("xprv") || ext_descriptor.contains("tprv") { eprintln!( - "WARNING: Your external descriptor contains PRIVATE KEYS.\n\ - Private keys will be saved in PLAINTEXT in the config file.\n\ - This is a security risk. Consider using public descriptors instead." + "WARNING: Your external descriptor contains PRIVATE KEYS. + Private keys will be saved in PLAINTEXT in the config file. + This is a security risk. Consider using public descriptors instead.\n" ); } if let Some(ref internal_desc) = int_descriptor { if internal_desc.contains("xprv") || internal_desc.contains("tprv") { eprintln!( - "WARNING: Your internal descriptor contains PRIVATE KEYS.\n\ - Private keys will be saved in PLAINTEXT in the config file.\n\ - This is a security risk. Consider using public descriptors instead." + "WARNING: Your internal descriptor contains PRIVATE KEYS. + Private keys will be saved in PLAINTEXT in the config file. + This is a security risk. Consider using public descriptors instead.\n" ); } } @@ -1353,8 +1346,9 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } }; - let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?; - let blockchain_client = new_blockchain_client(wallet_opts, &wallet, database_path)?; + let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; + let blockchain_client = + new_blockchain_client(&wallet_opts, &wallet, database_path)?; let result = handle_online_wallet_subcommand( &mut wallet, @@ -1419,10 +1413,10 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { }; #[cfg(not(any(feature = "sqlite", feature = "redb")))] let result = { - let mut wallet = new_wallet(network, wallet_opts)?; + let mut wallet = new_wallet(network, &wallet_opts)?; handle_offline_wallet_subcommand( &mut wallet, - wallet_opts, + &wallet_opts, &cli_opts, offline_subcommand.clone(), )? diff --git a/src/utils.rs b/src/utils.rs index 5fc195a5..73d34531 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -620,3 +620,31 @@ pub fn format_descriptor_output(result: &Value, pretty: bool) -> Result Result<(WalletOpts, Network), Error> { + let config = WalletConfig::load(home_dir)?.ok_or(Error::Generic(format!( + "No config found for wallet {wallet_name}", + )))?; + + let wallet_opts = config.get_wallet_opts(wallet_name)?; + let wallet_config = config + .wallets + .get(wallet_name) + .ok_or(Error::Generic(format!( + "Wallet '{wallet_name}' not found in config" + )))?; + + let network = match wallet_config.network.as_str() { + "bitcoin" => Ok(Network::Bitcoin), + "testnet" => Ok(Network::Testnet), + "regtest" => Ok(Network::Regtest), + "signet" => Ok(Network::Signet), + "testnet4" => Ok(Network::Testnet4), + _ => Err(Error::Generic("Invalid network in config".to_string())), + }?; + + Ok((wallet_opts, network)) +}