]> Untitled Git - bdk-cli/commitdiff
feat(wallet): add saved config list and delete commands
authoryan <102800044+yan-pi@users.noreply.github.com>
Tue, 11 Aug 2026 19:08:48 +0000 (16:08 -0300)
committeryan <102800044+yan-pi@users.noreply.github.com>
Thu, 22 Oct 2026 13:22:39 +0000 (10:22 -0300)
src/commands.rs
src/config.rs
src/error.rs
src/handlers/config.rs
src/main.rs
src/persister.rs
src/utils/common.rs
tests/integration/init.rs

index a9abb731bafe0590a123b14a8461dcfe40b2b3e1..ec624d0635d1d93329538b79ffa8bd29c3b01a1b 100644 (file)
@@ -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::{
@@ -145,8 +145,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
@@ -218,6 +222,19 @@ pub enum CliSubCommand {
     Hwi(HwiCommand),
 }
 
+/// Saved wallet configuration subcommands.
+#[derive(Debug, Subcommand, Clone, PartialEq)]
+pub enum WalletsSubCommand {
+    /// List saved wallet configurations.
+    List(ListWalletsCommand),
+
+    /// Delete an unused saved wallet configuration.
+    ///
+    /// The command refuses deletion once persistent wallet data exists.
+    /// Wallet database files are never deleted.
+    Delete(DeleteWalletConfigCommand),
+}
+
 /// Wallet operation subcommands.
 #[derive(Debug, Subcommand, Clone, PartialEq)]
 pub enum WalletSubCommand {
index 5e95dc5ea311be09b5e10a355fab9a91a00ebb24..8e9e5d2070836f1a6e0a14b982cffc7980e88dff 100644 (file)
@@ -7,7 +7,7 @@
 use crate::client::ClientType;
 use crate::commands::WalletOpts;
 use crate::error::BDKCliError as Error;
-#[cfg(feature = "sqlite")]
+#[cfg(any(feature = "sqlite", feature = "redb"))]
 use crate::persister::DatabaseType;
 use crate::utils::{FILE_MODE, create_restricted_dir, limit_access, write_file_content};
 use bdk_wallet::bitcoin::Network;
@@ -197,6 +197,40 @@ impl TryFrom<&WalletConfigInner> for WalletOpts {
     }
 }
 
+pub(crate) fn remove_wallet_from_config_file(
+    datadir: &std::path::Path,
+    wallet_name: &str,
+) -> Result<(), Error> {
+    let config_path = datadir.join("config.toml");
+    let config_content = std::fs::read_to_string(&config_path)
+        .map_err(|error| Error::Generic(format!("Failed to read config file: {error}")))?;
+    let mut raw_config: toml::Table = toml::from_str(&config_content)
+        .map_err(|error| Error::Generic(format!("Failed to parse config file: {error}")))?;
+    let wallets = raw_config
+        .get_mut("wallets")
+        .and_then(toml::Value::as_table_mut)
+        .ok_or_else(|| Error::Generic("Config does not contain a wallets table".into()))?;
+
+    if wallets.remove(wallet_name).is_none() {
+        return Err(Error::Generic(format!(
+            "Wallet '{wallet_name}' not found in config"
+        )));
+    }
+
+    if wallets.is_empty() {
+        return fs::remove_file(&config_path).map_err(|error| {
+            Error::Generic(format!(
+                "Failed to remove config at {config_path:?}: {error}"
+            ))
+        });
+    }
+
+    let updated_config = toml::to_string_pretty(&raw_config)
+        .map_err(|error| Error::Generic(format!("Failed to serialize config: {error}")))?;
+    write_file_content(&config_path, &updated_config)
+        .map_err(|error| Error::Generic(format!("Failed to write config file: {error}")))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -231,8 +265,10 @@ mod tests {
             network: "testnet4".to_string(),
             ext_descriptor: EXT_DESCRIPTOR.to_string(),
             int_descriptor: Some(INT_DESCRIPTOR.to_string()),
-            #[cfg(any(feature = "sqlite", feature = "redb"))]
+            #[cfg(feature = "sqlite")]
             database_type: "sqlite".to_string(),
+            #[cfg(all(feature = "redb", not(feature = "sqlite")))]
+            database_type: "redb".to_string(),
 
             #[cfg(any(
                 feature = "electrum",
@@ -318,8 +354,10 @@ mod tests {
             network: "regtest".to_string(),
             ext_descriptor: "desc".to_string(),
             int_descriptor: None,
-            #[cfg(any(feature = "sqlite", feature = "redb"))]
+            #[cfg(feature = "sqlite")]
             database_type: "sqlite".to_string(),
+            #[cfg(all(feature = "redb", not(feature = "sqlite")))]
+            database_type: "redb".to_string(),
             #[cfg(any(
                 feature = "electrum",
                 feature = "esplora",
index 3b1008c4841d51d5d806a7b41667469c42c93626..76b31fd0f2bc6bbba6043eae1e2ad5b2aefaaecc 100644 (file)
@@ -94,6 +94,17 @@ pub enum BDKCliError {
     #[error("Redb dabtabase error: {0}")]
     RedbDatabaseError(Box<bdk_redb::redb::DatabaseError>),
 
+    #[cfg(feature = "redb")]
+    #[error(
+        "Failed to inspect shared Redb store while determining whether wallet '{wallet_name}' has persisted data at {db_path:?}: {source}"
+    )]
+    RedbInspection {
+        wallet_name: String,
+        db_path: std::path::PathBuf,
+        #[source]
+        source: Box<bdk_redb::redb::Error>,
+    },
+
     #[error("Serde json error: {0}")]
     SerdeJson(#[from] serde_json::Error),
 
index 905011a616b5c1ce2f1facc58eed2d08b7cac2b6..dec2b8c489ebf60334b0f267bc5da6b3871b01d8 100644 (file)
@@ -7,14 +7,16 @@ use std::collections::HashMap;
     feature = "cbf"
 ))]
 use crate::client::ClientType;
-use crate::commands::WalletOpts;
-use crate::config::{WalletConfig, WalletConfigInner};
+use crate::commands::{WalletOpts, WalletsSubCommand};
+use crate::config::{WalletConfig, WalletConfigInner, remove_wallet_from_config_file};
 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::persister::wallet_data_exists;
 use crate::utils::descriptors::validate_descriptor_pair;
+use crate::utils::output::FormatOutput;
 use crate::utils::types::{StatusResult, WalletsListResult};
 use bdk_wallet::bitcoin::Network;
 use clap::Args;
@@ -175,3 +177,71 @@ 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 config = match WalletConfig::load(&ctx.datadir)? {
+            Some(config) => config,
+            None => return Err(Error::Generic("No wallets configured yet.".into())),
+        };
+
+        #[cfg(any(feature = "sqlite", feature = "redb"))]
+        let data_exists = {
+            let wallet_config = config.wallets.get(&self.wallet_name).ok_or_else(|| {
+                Error::Generic(format!("Wallet '{}' not found in config", self.wallet_name))
+            })?;
+
+            wallet_data_exists(
+                &ctx.datadir,
+                &self.wallet_name,
+                &wallet_config.database_type,
+            )?
+        };
+
+        #[cfg(not(any(feature = "sqlite", feature = "redb")))]
+        let data_exists = {
+            if !config.wallets.contains_key(&self.wallet_name) {
+                return Err(Error::Generic(format!(
+                    "Wallet '{}' not found in config",
+                    self.wallet_name
+                )));
+            }
+
+            wallet_data_exists(&ctx.datadir, &self.wallet_name)?
+        };
+
+        if data_exists {
+            return Err(Error::Generic(format!(
+                "Wallet data exists for configuration '{}'; the saved configuration was not deleted",
+                self.wallet_name
+            )));
+        }
+
+        remove_wallet_from_config_file(&ctx.datadir, &self.wallet_name)?;
+
+        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()),
+        }
+    }
+}
index ebba66b301e321c633e3fdd0fb2e8fdc4c909fd8..7ed73d466c869e8269e571191cd8fe8518489498 100644 (file)
@@ -144,10 +144,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")]
index 2a1d1ee3d9de165403a7e31922fcfcf9873518b1..3ff8e61c5feced91702bb9886a2a10c276a7bdfe 100644 (file)
@@ -1,6 +1,9 @@
 use crate::commands::WalletOpts;
 use crate::error::BDKCliError as Error;
 use crate::utils::descriptors::validate_descriptor_pair;
+use crate::utils::path_exists;
+#[cfg(feature = "redb")]
+use bdk_redb::redb::TableHandle;
 use bdk_wallet::Wallet;
 use bdk_wallet::bitcoin::Network;
 #[cfg(any(feature = "sqlite", feature = "redb"))]
@@ -112,6 +115,109 @@ where
     Ok(wallet)
 }
 
+#[cfg(feature = "redb")]
+fn redb_wallet_data_exists(datadir: &std::path::Path, wallet_name: &str) -> Result<bool, Error> {
+    let db_path = datadir.join("wallet.redb");
+    if !path_exists(&db_path)? {
+        return Ok(false);
+    }
+
+    let database =
+        bdk_redb::redb::Database::open(&db_path).map_err(|source| Error::RedbInspection {
+            wallet_name: wallet_name.to_owned(),
+            db_path: db_path.clone(),
+            source: Box::new(source.into()),
+        })?;
+
+    let read_transactions = database
+        .begin_read()
+        .map_err(|source| Error::RedbInspection {
+            wallet_name: wallet_name.to_owned(),
+            db_path: db_path.clone(),
+            source: Box::new(source.into()),
+        })?;
+
+    let mut tables = read_transactions
+        .list_tables()
+        .map_err(|source| Error::RedbInspection {
+            wallet_name: wallet_name.to_owned(),
+            db_path: db_path.clone(),
+            source: Box::new(source.into()),
+        })?;
+
+    // bdk_redb creates this per-wallet table in the first committed table batch for a
+    // persisted wallet, so it is the sentinel for an initialized wallet store.
+    let keychain_table_name = format!("{wallet_name}_keychain");
+
+    Ok(tables.any(|table| table.name() == keychain_table_name.as_str()))
+}
+
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+pub(crate) fn wallet_data_exists(
+    datadir: &std::path::Path,
+    wallet_name: &str,
+    database_type: &str,
+) -> Result<bool, Error> {
+    // Validate the persisted configuration before inspecting any data. In particular, an
+    // unknown database type must never turn a delete into a silent success.
+    match database_type {
+        // Both markers are valid persisted formats even when this binary only
+        // supports one of them; the opposite backend is checked conservatively below.
+        "sqlite" | "redb" => {}
+        _ => {
+            return Err(Error::Generic(format!(
+                "Unsupported database type: {database_type}"
+            )));
+        }
+    }
+
+    // A configuration can be changed to another backend without moving the original data, so
+    // inspect every persistence backend marker rather than only the configured backend.
+    let sqlite_path = datadir.join(wallet_name).join("wallet.sqlite");
+    let sqlite_data_exists = path_exists(&sqlite_path)?;
+
+    #[cfg(feature = "redb")]
+    let redb_data_exists = {
+        // Redb is shared by wallets, so only block deletion when this wallet has its sentinel
+        // table.
+        redb_wallet_data_exists(datadir, wallet_name)?
+    };
+
+    #[cfg(not(feature = "redb"))]
+    let redb_data_exists = {
+        // Without Redb support, conservatively treat the shared marker as data because this
+        // build cannot inspect its wallet-specific tables.
+        path_exists(&datadir.join("wallet.redb"))?
+    };
+
+    Ok(sqlite_data_exists || redb_data_exists)
+}
+
+#[cfg(not(any(feature = "sqlite", feature = "redb")))]
+pub(crate) fn wallet_data_exists(
+    datadir: &std::path::Path,
+    wallet_name: &str,
+) -> Result<bool, Error> {
+    // The typed config intentionally omits `database_type` in this build, so inspect the raw
+    // wallet entry as well. A config carrying that field may have been created by a build with a
+    // database backend and must not be silently deleted just because its marker is absent.
+    let config_path = datadir.join("config.toml");
+    let config_content = std::fs::read_to_string(&config_path)
+        .map_err(|error| Error::Generic(format!("Failed to read config file: {error}")))?;
+    let raw_config: toml::Table = toml::from_str(&config_content)
+        .map_err(|error| Error::Generic(format!("Failed to parse config file: {error}")))?;
+    let database_type_present = raw_config
+        .get("wallets")
+        .and_then(toml::Value::as_table)
+        .and_then(|wallets| wallets.get(wallet_name))
+        .and_then(toml::Value::as_table)
+        .is_some_and(|wallet| wallet.contains_key("database_type"));
+
+    Ok(database_type_present
+        || path_exists(&datadir.join(wallet_name).join("wallet.sqlite"))?
+        || path_exists(&datadir.join("wallet.redb"))?)
+}
+
 pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result<Wallet, Error> {
     let ext_descriptor = wallet_opts.ext_descriptor.clone();
     let int_descriptor = wallet_opts.int_descriptor.clone();
index ae86dab867e7af02298895fb39b4d4f2878a7c18..7a2d8dba98fbb17f36f8a5e13c981a9ab85ce187 100644 (file)
@@ -437,6 +437,16 @@ pub(crate) fn write_file_content(path: &Path, contents: &str) -> std::io::Result
     std::fs::write(path, contents)
 }
 
+pub(crate) fn path_exists(path: &std::path::Path) -> Result<bool, Error> {
+    match std::fs::symlink_metadata(path) {
+        Ok(_) => Ok(true),
+        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
+        Err(error) => Err(Error::Generic(format!(
+            "Failed to inspect wallet data at {path:?}: {error}"
+        ))),
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
index 12086ff67ab8b55e9dbf92069588dc0d0de4a411..c5907e13e04a607101c2bf840d843c1ea0a4a79e 100644 (file)
@@ -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()
@@ -125,7 +125,7 @@ mod test_wallets {
         let temp_dir = TempDir::new().unwrap();
         let cli = BdkCli::new("bitcoin", Some(temp_dir.path().to_path_buf()));
 
-        cli.cmd("wallets", &[])
+        cli.cmd("wallets", &["list"])
             .env_remove("RUST_LOG")
             .assert()
             .stderr(predicate::str::contains(
@@ -138,7 +138,7 @@ mod test_wallets {
         let temp_dir = TempDir::new().unwrap();
         let cli = BdkCli::new("bitcoin", Some(temp_dir.path().to_path_buf()));
 
-        cli.cmd("wallets", &[])
+        cli.cmd("wallets", &["list"])
             .env("RUST_LOG", "error")
             .assert()
             .stderr(predicate::str::contains("not currently recommended").not());
@@ -175,6 +175,7 @@ mod test_wallets {
 
         cli.build_base_cmd()
             .arg("wallets")
+            .arg("list")
             .assert()
             .success()
             .stdout(predicate::str::contains("wallet_one"))
@@ -182,6 +183,290 @@ mod test_wallets {
     }
 }
 
+// --- REDB WALLET CONFIGURATION TESTS ---
+#[cfg(feature = "redb")]
+mod test_redb_wallet_config {
+    use super::*;
+    use serde_json::Value;
+
+    fn save_wallet(cli: &BdkCli, wallet_name: &str) {
+        let descriptor = cli
+            .cmd("descriptor", &["--type", "tr"])
+            .output()
+            .expect("Command to generate descriptors failed");
+        assert!(descriptor.status.success());
+
+        let descriptor_json: Value =
+            serde_json::from_slice(&descriptor.stdout).expect("Invalid descriptor JSON");
+        let public_descriptors = &descriptor_json["public_descriptors"];
+
+        let mut command = cli.build_base_cmd();
+        command
+            .arg("wallet")
+            .arg("--wallet")
+            .arg(wallet_name)
+            .arg("config")
+            .arg("--ext-descriptor")
+            .arg(public_descriptors["external"].as_str().unwrap())
+            .arg("--int-descriptor")
+            .arg(public_descriptors["internal"].as_str().unwrap());
+
+        #[cfg(feature = "rpc")]
+        command
+            .arg("--client-type")
+            .arg("rpc")
+            .arg("--url")
+            .arg("http://localhost:18443");
+
+        #[cfg(all(not(feature = "rpc"), feature = "esplora"))]
+        command
+            .arg("--client-type")
+            .arg("esplora")
+            .arg("--url")
+            .arg("http://localhost:3000");
+
+        #[cfg(all(not(feature = "rpc"), not(feature = "esplora"), feature = "electrum"))]
+        command
+            .arg("--client-type")
+            .arg("electrum")
+            .arg("--url")
+            .arg("tcp://localhost:50001");
+
+        #[cfg(all(
+            not(feature = "rpc"),
+            not(feature = "esplora"),
+            not(feature = "electrum"),
+            feature = "cbf"
+        ))]
+        command.arg("--client-type").arg("cbf");
+
+        command
+            .arg("--database-type")
+            .arg("redb")
+            .assert()
+            .success();
+    }
+
+    #[test]
+    fn test_delete_redb_configs_preserves_shared_database() {
+        let temp_dir = TempDir::new().unwrap();
+        let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf()));
+        let persisted_wallet = "persisted_redb_wallet";
+        let unused_wallet = "unused_redb_wallet";
+        let config_path = temp_dir.path().join("config.toml");
+        let database_path = temp_dir.path().join("wallet.redb");
+
+        save_wallet(&cli, persisted_wallet);
+        cli.wallet_cmd(&["--wallet", persisted_wallet, "new_address"])
+            .assert()
+            .success();
+        assert!(database_path.is_file());
+
+        save_wallet(&cli, unused_wallet);
+        cli.build_base_cmd()
+            .args(["wallets", "delete", unused_wallet])
+            .assert()
+            .success();
+
+        assert!(config_path.is_file());
+        assert!(database_path.is_file());
+        cli.build_base_cmd()
+            .args(["wallets", "list"])
+            .assert()
+            .success()
+            .stdout(predicate::str::contains(persisted_wallet))
+            .stdout(predicate::str::contains(unused_wallet).not());
+
+        cli.build_base_cmd()
+            .args(["wallets", "delete", persisted_wallet])
+            .assert()
+            .failure()
+            .stderr(predicate::str::contains(
+                "Wallet data exists for configuration 'persisted_redb_wallet'; the saved configuration was not deleted",
+            ));
+
+        assert!(config_path.is_file());
+        assert!(database_path.is_file());
+    }
+}
+
+// --- REDB INSPECTION ERROR TESTS ---
+#[cfg(all(feature = "sqlite", feature = "redb", feature = "rpc"))]
+mod test_redb_inspection_errors {
+    use super::*;
+    use serde_json::Value;
+    use std::fs;
+
+    #[test]
+    fn test_delete_reports_shared_redb_inspection_failure() {
+        let temp_dir = TempDir::new().unwrap();
+        let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf()));
+        let wallet_name = "sqlite_wallet_with_unreadable_redb";
+        let config_path = temp_dir.path().join("config.toml");
+        let database_path = temp_dir.path().join("wallet.redb");
+
+        let descriptor = cli
+            .cmd("descriptor", &["--type", "tr"])
+            .output()
+            .expect("Command to generate descriptors failed");
+        let descriptor_json: Value =
+            serde_json::from_slice(&descriptor.stdout).expect("Invalid descriptor JSON");
+        let public_descriptors = &descriptor_json["public_descriptors"];
+
+        cli.build_base_cmd()
+            .arg("wallet")
+            .arg("--wallet")
+            .arg(wallet_name)
+            .arg("config")
+            .arg("--ext-descriptor")
+            .arg(public_descriptors["external"].as_str().unwrap())
+            .arg("--int-descriptor")
+            .arg(public_descriptors["internal"].as_str().unwrap())
+            .arg("--client-type")
+            .arg("rpc")
+            .arg("--database-type")
+            .arg("sqlite")
+            .arg("--url")
+            .arg("http://localhost:18443")
+            .assert()
+            .success();
+
+        fs::write(&database_path, b"not a redb database").unwrap();
+
+        cli.build_base_cmd()
+            .args(["wallets", "delete", wallet_name])
+            .assert()
+            .failure()
+            .stderr(predicate::str::contains(
+                "Failed to inspect shared Redb store while determining whether wallet",
+            ));
+
+        assert!(config_path.is_file());
+    }
+}
+
+// --- SINGLE-BACKEND CROSS-BACKEND MARKER TESTS ---
+#[cfg(all(feature = "sqlite", not(feature = "redb")))]
+mod test_sqlite_only_cross_backend_marker {
+    use super::*;
+    use std::fs;
+
+    #[test]
+    fn test_delete_rejects_foreign_redb_file_with_sqlite_marker() {
+        let temp_dir = TempDir::new().unwrap();
+        let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf()));
+        let wallet_name = "sqlite_only_foreign_redb_data";
+        let config_path = temp_dir.path().join("config.toml");
+        let redb_path = temp_dir.path().join("wallet.redb");
+
+        fs::write(
+            &config_path,
+            format!(
+                "[wallets.{wallet_name}]\nwallet = \"{wallet_name}\"\nnetwork = \"regtest\"\next_descriptor = \"wpkh(test)\"\nint_descriptor = \"wpkh(test)\"\ndatabase_type = \"sqlite\"\n"
+            ),
+        )
+        .unwrap();
+        fs::write(&redb_path, []).unwrap();
+
+        cli.build_base_cmd()
+            .args(["wallets", "delete", wallet_name])
+            .assert()
+            .failure()
+            .stderr(predicate::str::contains(
+                "Wallet data exists for configuration 'sqlite_only_foreign_redb_data'; the saved configuration was not deleted",
+            ));
+
+        assert!(config_path.is_file());
+        assert!(redb_path.is_file());
+    }
+}
+
+#[cfg(all(feature = "redb", not(feature = "sqlite")))]
+mod test_redb_only_cross_backend_marker {
+    use super::*;
+    use std::fs;
+
+    #[test]
+    fn test_delete_rejects_foreign_sqlite_file_with_redb_marker() {
+        let temp_dir = TempDir::new().unwrap();
+        let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf()));
+        let wallet_name = "redb_only_foreign_sqlite_data";
+        let config_path = temp_dir.path().join("config.toml");
+        let sqlite_path = temp_dir.path().join(wallet_name).join("wallet.sqlite");
+
+        fs::write(
+            &config_path,
+            format!(
+                "[wallets.{wallet_name}]\nwallet = \"{wallet_name}\"\nnetwork = \"regtest\"\next_descriptor = \"wpkh(test)\"\nint_descriptor = \"wpkh(test)\"\ndatabase_type = \"redb\"\n"
+            ),
+        )
+        .unwrap();
+        fs::create_dir_all(sqlite_path.parent().unwrap()).unwrap();
+        fs::write(&sqlite_path, []).unwrap();
+
+        cli.build_base_cmd()
+            .args(["wallets", "delete", wallet_name])
+            .assert()
+            .failure()
+            .stderr(predicate::str::contains(
+                "Wallet data exists for configuration 'redb_only_foreign_sqlite_data'; the saved configuration was not deleted",
+            ));
+
+        assert!(config_path.is_file());
+        assert!(sqlite_path.is_file());
+    }
+}
+
+// --- DATABASE-DISABLED WALLET CONFIGURATION TESTS ---
+#[cfg(not(any(feature = "sqlite", feature = "redb")))]
+mod test_database_disabled_wallet_config {
+    use super::*;
+    use std::fs;
+
+    fn write_config(datadir: &std::path::Path, wallet_name: &str, database_type: Option<&str>) {
+        let database_type = database_type
+            .map(|database_type| format!("\ndatabase_type = \"{database_type}\""))
+            .unwrap_or_default();
+
+        fs::write(
+            datadir.join("config.toml"),
+            format!(
+                "[wallets.{wallet_name}]\nwallet = \"{wallet_name}\"\nnetwork = \"regtest\"\next_descriptor = \"wpkh(test)\"\nint_descriptor = \"wpkh(test)\"{database_type}\n"
+            ),
+        )
+        .unwrap();
+    }
+
+    #[test]
+    fn test_delete_wallet_config_without_database_support() {
+        for (wallet_name, database_type) in [
+            ("marker_wallet", Some("sqlite")),
+            ("database_free_wallet", None),
+        ] {
+            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");
+
+            write_config(temp_dir.path(), wallet_name, database_type);
+
+            let assertion = cli
+                .build_base_cmd()
+                .args(["wallets", "delete", wallet_name])
+                .assert();
+
+            if database_type.is_some() {
+                assertion.failure().stderr(predicate::str::contains(
+                    "Wallet data exists for configuration",
+                ));
+                assert!(config_path.is_file());
+            } else {
+                assertion.success();
+                assert!(!config_path.exists());
+            }
+        }
+    }
+}
+
 // --- DESCRIPTOR COMMAND TESTS ---
 mod test_descriptor {
     use super::*;
@@ -280,11 +565,49 @@ mod test_compile {
 }
 
 // --- CONFIG COMMAND TESTS ---
-#[cfg(feature = "rpc")]
+#[cfg(any(feature = "rpc", feature = "sqlite"))]
 mod test_config {
     use super::*;
     use serde_json::Value;
+    use std::fs;
+
+    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"];
 
+        let mut command = cli.build_base_cmd();
+        command
+            .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());
+
+        #[cfg(feature = "rpc")]
+        command
+            .arg("--client-type")
+            .arg("rpc")
+            .arg("--url")
+            .arg("http://localhost:18443");
+
+        command
+            .arg("--database-type")
+            .arg("sqlite")
+            .assert()
+            .success();
+    }
+
+    #[cfg(feature = "rpc")]
     #[test]
     fn test_save_and_read_wallet_config() {
         let temp_dir = TempDir::new().unwrap();
@@ -322,7 +645,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");
 
@@ -350,7 +673,7 @@ mod test_config {
         assert_eq!(config["int_descriptor"].as_str().unwrap(), int_desc);
     }
 
-    #[cfg(unix)]
+    #[cfg(all(unix, feature = "rpc"))]
     #[test]
     fn test_config_with_private_keys_is_unreadable_by_other_users() {
         use std::os::unix::fs::PermissionsExt;
@@ -391,7 +714,194 @@ mod test_config {
             "config.toml holds a private descriptor and must not be readable by other users"
         );
     }
+
+    #[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."));
+    }
+
+    #[test]
+    fn test_delete_wallet_config_with_persisted_data_fails() {
+        let temp_dir = TempDir::new().unwrap();
+        let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf()));
+        let wallet_name = "persisted_wallet";
+
+        save_wallet(&cli, wallet_name);
+
+        let config_path = temp_dir.path().join("config.toml");
+        let database_path = temp_dir.path().join(wallet_name).join("wallet.sqlite");
+
+        assert!(config_path.is_file());
+        assert!(
+            !database_path.exists(),
+            "saving a configuration alone should not create wallet data"
+        );
+
+        cli.wallet_cmd(&["--wallet", wallet_name, "new_address"])
+            .assert()
+            .success();
+
+        assert!(
+            database_path.is_file(),
+            "new_address should initialize the wallet database"
+        );
+
+        cli.build_base_cmd()
+            .arg("wallets")
+            .arg("delete")
+            .arg(wallet_name)
+            .assert()
+            .failure()
+            .stderr(predicate::str::contains(
+                "Wallet data exists for configuration 'persisted_wallet'",
+            ));
+
+        assert!(
+            config_path.is_file(),
+            "failed deletion should preserve config.toml"
+        );
+
+        assert!(
+            database_path.is_file(),
+            "failed deletion should preserve wallet data"
+        );
+
+        cli.build_base_cmd()
+            .arg("wallets")
+            .arg("list")
+            .assert()
+            .success()
+            .stdout(predicate::str::contains(wallet_name));
+    }
+
+    #[cfg(feature = "sqlite")]
+    #[test]
+    fn test_delete_preserves_fields_unknown_to_current_build() {
+        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");
+
+        fs::write(
+            &config_path,
+            r#"[wallets.remove_wallet]
+wallet = "remove_wallet"
+network = "regtest"
+ext_descriptor = "wpkh(test)"
+int_descriptor = "wpkh(test)"
+database_type = "sqlite"
+
+[wallets.preserved_wallet]
+wallet = "preserved_wallet"
+network = "regtest"
+ext_descriptor = "wpkh(test)"
+int_descriptor = "wpkh(test)"
+database_type = "sqlite"
+client_type = "rpc"
+server_url = "http://localhost:18443"
+rpc_user = "preserved-user"
+rpc_password = "preserved-password"
+"#,
+        )
+        .unwrap();
+
+        cli.build_base_cmd()
+            .args(["wallets", "delete", "remove_wallet"])
+            .assert()
+            .success();
+
+        let raw_config: toml::Table =
+            toml::from_str(&fs::read_to_string(&config_path).unwrap()).unwrap();
+        let preserved = raw_config["wallets"]["preserved_wallet"]
+            .as_table()
+            .unwrap();
+        assert_eq!(preserved["client_type"].as_str(), Some("rpc"));
+        assert_eq!(
+            preserved["server_url"].as_str(),
+            Some("http://localhost:18443")
+        );
+        assert_eq!(preserved["rpc_user"].as_str(), Some("preserved-user"));
+        assert_eq!(
+            preserved["rpc_password"].as_str(),
+            Some("preserved-password")
+        );
+    }
 }
+
 //  SILENT PAYMENTS
 #[cfg(feature = "silent-payments")]
 mod test_silent_payments {