Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <wallet_name>`

## [4.0.0]

- Added persistance to existing async payjoin integration
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <wallet_name>
```

## Adding new features/command
Expand Down
20 changes: 17 additions & 3 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
55 changes: 54 additions & 1 deletion src/handlers/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -172,3 +173,55 @@ impl AppCommand<AppContext<Init>> 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<AppContext<Init>> for DeleteWalletConfigCommand {
type Output = StatusResult;

fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> {
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<Init>) -> 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()),
}
}
}
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
115 changes: 113 additions & 2 deletions tests/integration/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -157,6 +157,7 @@ mod test_wallets {

cli.build_base_cmd()
.arg("wallets")
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains("wallet_one"))
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");

Expand All @@ -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
Expand Down