diff --git a/CHANGELOG.md b/CHANGELOG.md index 79da994d..0bfba5a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. ## [Unreleased] +- Split saved wallet configuration operations into `wallets list` and `wallets delete ` + ## [4.0.0] - Added persistance to existing async payjoin integration diff --git a/README.md b/README.md index 94278f3c..c297da79 100644 --- a/README.md +++ b/README.md @@ -328,12 +328,18 @@ 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 +#### Manage saved Wallet Configs To view all saved wallet configurations: ```shell -cargo run wallets` +cargo run -- wallets list +``` + +To delete a saved wallet configuration: + +```shell +cargo run -- wallets delete ``` ## Adding new features/command diff --git a/src/commands.rs b/src/commands.rs index 975f33be..87d4f372 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::{DeleteWalletConfigCommand, ListWalletsCommand, SaveConfigCommand}, descriptor::DescriptorCommand, key::{DeriveKeyCommand, GenerateKeyCommand, RestoreKeyCommand}, offline::{ @@ -141,8 +141,12 @@ pub enum CliSubCommand { /// This feature is intended for development and testing purposes only. Descriptor(DescriptorCommand), - /// List all saved wallet configurations. - Wallets(ListWalletsCommand), + /// Saved wallet configuration operations. + Wallets { + #[command(subcommand)] + subcommand: WalletsSubCommand, + }, + /// Generate tab-completion scripts for your shell. /// /// The completion script is output on stdout, allowing you to redirect @@ -208,6 +212,16 @@ pub enum CliSubCommand { ResolveDnsRecipient(ResolveDnsRecipientCommand), } +/// Saved wallet configuration subcommands. +#[derive(Debug, Subcommand, Clone, PartialEq)] +pub enum WalletsSubCommand { + /// List saved wallet configurations. + List(ListWalletsCommand), + + /// Delete a saved wallet configuration. + Delete(DeleteWalletConfigCommand), +} + /// Wallet operation subcommands. #[derive(Debug, Subcommand, Clone, PartialEq)] pub enum WalletSubCommand { diff --git a/src/handlers/config.rs b/src/handlers/config.rs index 3409bfa3..0d238996 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -7,13 +7,14 @@ use std::collections::HashMap; feature = "cbf" ))] use crate::client::ClientType; -use crate::commands::WalletOpts; +use crate::commands::{WalletOpts, WalletsSubCommand}; use crate::config::{WalletConfig, WalletConfigInner}; use crate::error::BDKCliError as Error; use crate::handlers::Init; use crate::handlers::{AppCommand, AppContext}; #[cfg(any(feature = "sqlite", feature = "redb"))] use crate::persister::DatabaseType; +use crate::utils::output::FormatOutput; use crate::utils::types::{StatusResult, WalletsListResult}; use bdk_wallet::bitcoin::Network; use clap::Args; @@ -172,3 +173,55 @@ impl AppCommand> for ListWalletsCommand { Ok(WalletsListResult(config.wallets)) } } + +#[derive(Args, Debug, Clone, PartialEq)] +pub struct DeleteWalletConfigCommand { + /// Name of the saved wallet configuration to delete. + #[arg(value_name = "WALLET_NAME")] + pub(crate) wallet_name: String, +} + +impl AppCommand> for DeleteWalletConfigCommand { + type Output = StatusResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let mut config = match WalletConfig::load(&ctx.datadir)? { + Some(config) => config, + None => return Err(Error::Generic("No wallets configured yet.".into())), + }; + + if config.wallets.remove(&self.wallet_name).is_none() { + return Err(Error::Generic(format!( + "Wallet '{}' not found in config", + self.wallet_name + ))); + } + + if config.wallets.is_empty() { + let config_path = ctx.datadir.join("config.toml"); + std::fs::remove_file(&config_path).map_err(|error| { + Error::Generic(format!( + "Failed to remove config at {config_path:?}: {error}" + )) + })?; + } else { + config.save(&ctx.datadir)?; + } + + Ok(StatusResult { + message: format!( + "Wallet configuration '{}' deleted successfully", + self.wallet_name + ), + }) + } +} + +impl WalletsSubCommand { + pub fn execute(&self, ctx: &mut AppContext) -> Result<(), Error> { + match self { + Self::List(command) => command.execute(ctx)?.write_out(std::io::stdout()), + Self::Delete(command) => command.execute(ctx)?.write_out(std::io::stdout()), + } + } +} diff --git a/src/main.rs b/src/main.rs index 06e3ea24..047e8f9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -129,10 +129,10 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; } - CliSubCommand::Wallets(cmd) => { + CliSubCommand::Wallets { subcommand } => { let mut ctx = AppContext::new(cli_opts.network, home_dir); - cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; + subcommand.execute(&mut ctx)?; } #[cfg(feature = "repl")] diff --git a/tests/integration/init.rs b/tests/integration/init.rs index 17fdfce2..4810a51b 100644 --- a/tests/integration/init.rs +++ b/tests/integration/init.rs @@ -113,7 +113,7 @@ mod test_wallets { let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); let mut cmd = cli.build_base_cmd(); - cmd.arg("wallets"); + cmd.arg("wallets").arg("list"); cmd.assert() .failure() @@ -157,6 +157,7 @@ mod test_wallets { cli.build_base_cmd() .arg("wallets") + .arg("list") .assert() .success() .stdout(predicate::str::contains("wallet_one")) @@ -221,6 +222,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(); @@ -264,7 +295,7 @@ mod test_config { // verify saved config let mut cmd = cli.build_base_cmd(); - cmd.arg("wallets"); + cmd.arg("wallets").arg("list"); let output = cmd.output().expect("Failed to execute wallets command"); @@ -291,6 +322,86 @@ 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 a confirmation message + 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 json: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!( + json["message"].as_str().unwrap(), + "Wallet configuration 'test_delete_wallet' deleted successfully" + ); + + // Re-listing no longer contains the deleted wallet + let output = cli + .build_base_cmd() + .arg("wallets") + .arg("list") + .output() + .expect("Failed to execute wallets list command"); + + 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")); + } + + #[test] + fn test_delete_last_wallet_config() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + let config_path = temp_dir.path().join("config.toml"); + + save_wallet(&cli, "last_wallet"); + assert!(config_path.exists()); + + cli.build_base_cmd() + .arg("wallets") + .arg("delete") + .arg("last_wallet") + .assert() + .success(); + + assert!(!config_path.exists()); + + cli.build_base_cmd() + .arg("wallets") + .arg("list") + .assert() + .failure() + .stderr(predicate::str::contains("No wallets configured yet.")); + } } // SILENT PAYMENTS