From 813a9ffee6fbeff64f58d00afc1b879104d5c859 Mon Sep 17 00:00:00 2001 From: yan <102800044+yan-pi@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:21:11 -0300 Subject: [PATCH 1/2] feat(wallet): add wallets --delete to remove saved wallet config --- src/commands.rs | 6 ++-- src/config.rs | 62 ++++++++++++++++++++++++++++++++++ src/handlers/config.rs | 23 +++++++++---- tests/integration/init.rs | 70 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 975f33be..19bdd3a7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -16,7 +16,7 @@ #[cfg(feature = "message_signer")] use crate::handlers::offline::{SignMessageCommand, VerifyMessageCommand}; use crate::handlers::{ - config::{ListWalletsCommand, SaveConfigCommand}, + config::{SaveConfigCommand, WalletsCommand}, descriptor::DescriptorCommand, key::{DeriveKeyCommand, GenerateKeyCommand, RestoreKeyCommand}, offline::{ @@ -141,8 +141,8 @@ pub enum CliSubCommand { /// This feature is intended for development and testing purposes only. Descriptor(DescriptorCommand), - /// List all saved wallet configurations. - Wallets(ListWalletsCommand), + /// List all saved wallet configurations, or delete one with `--delete`. + Wallets(WalletsCommand), /// Generate tab-completion scripts for your shell. /// /// The completion script is output on stdout, allowing you to redirect diff --git a/src/config.rs b/src/config.rs index 60580037..7bc44d4f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -102,6 +102,11 @@ impl WalletConfig { .ok_or_else(|| Error::Generic(format!("Wallet {wallet_name} not found in config")))? .try_into() } + + #[must_use] + pub fn remove_wallet(&mut self, wallet_name: &str) -> Option { + self.wallets.remove(wallet_name) + } } impl TryFrom<&WalletConfigInner> for WalletOpts { @@ -346,4 +351,61 @@ mod tests { let result: Result = (&inner).try_into(); assert!(result.is_err()); } + + fn test_wallet(name: &str) -> WalletConfigInner { + WalletConfigInner { + wallet: name.to_string(), + network: "testnet".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: Some("rpc".to_string()), + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + server_url: Some("http://localhost:18443".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, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_auth: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_retries: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_timeout: None, + #[cfg(feature = "cbf")] + conn_count: None, + } + } + #[test] + fn test_remove_wallet_config() { + let mut config = WalletConfig { + wallets: HashMap::from([ + ("alice".to_string(), test_wallet("alice")), + ("bob".to_string(), test_wallet("bob")), + ]), + }; + + let removed = config.remove_wallet("alice"); + assert!(removed.is_some()); + assert_eq!(removed.unwrap().wallet, "alice"); + assert!(!config.wallets.contains_key("alice")); + assert!(config.wallets.contains_key("bob")); + + assert!(config.remove_wallet("charlie").is_none()); + } } diff --git a/src/handlers/config.rs b/src/handlers/config.rs index 3409bfa3..c3c2f962 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -158,16 +158,27 @@ impl AppCommand> for SaveConfigCommand { } #[derive(Args, Debug, Clone, PartialEq)] -pub struct ListWalletsCommand; +pub struct WalletsCommand { + /// Delete the saved configuration for the given wallet instead of listing. + #[arg(long = "delete", value_name = "WALLET_NAME")] + pub(crate) delete: Option, +} -impl AppCommand> for ListWalletsCommand { +impl AppCommand> for WalletsCommand { type Output = WalletsListResult; fn execute(&self, ctx: &mut AppContext) -> Result { - let config = match WalletConfig::load(&ctx.datadir)? { - Some(cfg) => cfg, - None => return Err(Error::Generic("No wallets configured yet.".into())), - }; + let mut config = WalletConfig::load(&ctx.datadir)? + .ok_or_else(|| Error::Generic("No wallets configured yet.".into()))?; + + if let Some(wallet_name) = &self.delete { + if config.remove_wallet(wallet_name).is_none() { + return Err(Error::Generic(format!( + "Wallet '{wallet_name}' not found in config" + ))); + } + config.save(&ctx.datadir)?; + } Ok(WalletsListResult(config.wallets)) } diff --git a/tests/integration/init.rs b/tests/integration/init.rs index 17fdfce2..810b7491 100644 --- a/tests/integration/init.rs +++ b/tests/integration/init.rs @@ -221,6 +221,36 @@ mod test_config { use super::*; use serde_json::Value; + fn save_wallet(cli: &BdkCli, wallet_name: &str) { + let desc = cli + .cmd("descriptor", &["--type", "tr"]) + .output() + .expect("Command to generate descriptors failed"); + + let desc_values: Value = + serde_json::from_slice(&desc.stdout).expect("Invalid JSON from output descriptor"); + + let pub_desc = &desc_values["public_descriptors"]; + + cli.build_base_cmd() + .arg("wallet") + .arg("--wallet") + .arg(wallet_name) + .arg("config") + .arg("--ext-descriptor") + .arg(pub_desc["external"].as_str().unwrap()) + .arg("--int-descriptor") + .arg(pub_desc["internal"].as_str().unwrap()) + .arg("--client-type") + .arg("rpc") + .arg("--database-type") + .arg("sqlite") + .arg("--url") + .arg("http://localhost:18443") + .assert() + .success(); + } + #[test] fn test_save_and_read_wallet_config() { let temp_dir = TempDir::new().unwrap(); @@ -291,6 +321,46 @@ mod test_config { assert_eq!(config["ext_descriptor"].as_str().unwrap(), ext_desc); assert_eq!(config["int_descriptor"].as_str().unwrap(), int_desc); } + + #[test] + fn test_delete_wallet_config() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + let remove_wallet_name = "test_delete_wallet"; + let keep_wallet_name = "test_keep_wallet"; + + save_wallet(&cli, remove_wallet_name); + save_wallet(&cli, keep_wallet_name); + + // Delete one config: the output is the remaining wallet map + let output = cli + .build_base_cmd() + .arg("wallets") + .arg("--delete") + .arg(remove_wallet_name) + .output() + .expect("Failed to execute wallets --delete command"); + assert!(output.status.success(), "wallets --delete failed"); + + let list: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert!(list.get(remove_wallet_name).is_none()); + assert!(list.get(keep_wallet_name).is_some()); + } + + #[test] + fn test_delete_unknown_wallet_config() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + save_wallet(&cli, "existing_wallet"); + + cli.build_base_cmd() + .arg("wallets") + .arg("--delete") + .arg("ghost_wallet") + .assert() + .failure() + .stderr(predicate::str::contains("not found in config")); + } } // SILENT PAYMENTS From e9878775bfa6ae1530b9ac874e9263fea008cd4a Mon Sep 17 00:00:00 2001 From: yan <102800044+yan-pi@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:21:15 -0300 Subject: [PATCH 2/2] docs: document wallets --delete in README and CHANGELOG --- CHANGELOG.md | 2 ++ README.md | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79da994d..9d79e16f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. ## [Unreleased] +- Added `wallets --delete` to remove a saved wallet configuration + ## [4.0.0] - Added persistance to existing async payjoin integration diff --git a/README.md b/README.md index 94278f3c..4eee9325 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,12 @@ To view all saved wallet configurations: cargo run wallets` ``` +To delete a saved wallet configuration: + +```shell +cargo run wallets --delete +``` + ## Adding new features/command This [guide](./NEW_FEATURE.md) explains how to add a new command/feature to bdk-cli's modular architecture.