From 0040568521235da48668792f565e2210d5cc46a0 Mon Sep 17 00:00:00 2001 From: Muawiya Abubakar Date: Sun, 8 Nov 2026 01:57:09 +0100 Subject: [PATCH 1/4] docs: explain which commands use --network Wallet commands load the network saved in the wallet configuration, so --network only applies to `wallet config` and to commands that do not load a wallet, such as `key` and `descriptor`. Say so in the --network help text. --- src/commands.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands.rs b/src/commands.rs index 2190f5d9..aa22f5a5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -78,6 +78,8 @@ use crate::handlers::hwi::{HwiCommand, WalletHwiCommand}; #[command(version, about, long_about = None)] pub struct CliOpts { /// Sets the network: bitcoin, testnet, testnet4, signet, or regtest. + /// Wallet commands other than `wallet config` use the network saved in + /// the wallet configuration. #[arg( env = "NETWORK", short = 'n', From 6cc1bf98c592e1f4a6fb56d14a0620f685fdbacd Mon Sep 17 00:00:00 2001 From: Muawiya Abubakar Date: Sun, 8 Nov 2026 02:03:11 +0100 Subject: [PATCH 2/4] fix(config): reject network changes in wallet config --force Running `wallet config --force` without the original --network saved the default network (testnet) over the existing one. The config then no longer matched the wallet database, and the wallet failed to load with a network mismatch error. - Reject a --force that would change an existing wallet network, and say which --network to pass instead - Still allow --force to update other settings on the same network - Add tests for both cases --- src/handlers/config.rs | 10 +++++++ tests/integration/init.rs | 62 +++++++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/handlers/config.rs b/src/handlers/config.rs index dc1ffb9f..13a1f1d1 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -78,6 +78,16 @@ impl AppCommand> for SaveConfigCommand { ))); }; + if let Some(existing) = config.wallets.get(wallet_name.as_str()) + && existing.network != ctx.network.to_string() + { + return Err(Error::Generic(format!( + "Wallet '{}' is configured for network '{}', and --force cannot change it to '{}'. \ + To update other settings, pass --network {}.", + wallet_name, existing.network, ctx.network, existing.network + ))); + } + #[cfg(any( feature = "electrum", feature = "esplora", diff --git a/tests/integration/init.rs b/tests/integration/init.rs index 34548828..e34da5d5 100644 --- a/tests/integration/init.rs +++ b/tests/integration/init.rs @@ -702,7 +702,7 @@ mod test_config { use serde_json::Value; use std::fs; - fn save_wallet(cli: &BdkCli, wallet_name: &str) { + fn save_wallet_cmd(cli: &BdkCli, wallet_name: &str, force: bool) -> assert_cmd::Command { let desc = cli .cmd("descriptor", &["--type", "tr"]) .output() @@ -718,7 +718,13 @@ mod test_config { .arg("wallet") .arg("--wallet") .arg(wallet_name) - .arg("config") + .arg("config"); + + if force { + command.arg("--force"); + } + + command .arg("--ext-descriptor") .arg(pub_desc["external"].as_str().unwrap()) .arg("--int-descriptor") @@ -731,11 +737,13 @@ mod test_config { .arg("--url") .arg("http://localhost:18443"); + command.arg("--database-type").arg("sqlite"); + command - .arg("--database-type") - .arg("sqlite") - .assert() - .success(); + } + + fn save_wallet(cli: &BdkCli, wallet_name: &str) { + save_wallet_cmd(cli, wallet_name, false).assert().success(); } #[cfg(feature = "rpc")] @@ -1030,6 +1038,48 @@ rpc_password = "preserved-password" Some("preserved-password") ); } + + #[test] + fn test_config_force_rejects_network_change() { + let temp_dir = TempDir::new().unwrap(); + let regtest = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + let signet = BdkCli::new("signet", Some(temp_dir.path().to_path_buf())); + let wallet_name = "network_wallet"; + let config_path = temp_dir.path().join("config.toml"); + + save_wallet(®test, wallet_name); + let before = fs::read_to_string(&config_path).unwrap(); + + save_wallet_cmd(&signet, wallet_name, true) + .assert() + .failure() + .stderr(predicate::str::contains( + "Wallet 'network_wallet' is configured for network 'regtest', and --force cannot change it to 'signet'", + )); + + let after = fs::read_to_string(&config_path).unwrap(); + assert_eq!( + before, after, + "rejected --force must not change config.toml" + ); + } + + #[test] + fn test_config_force_same_network_updates_settings() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + let wallet_name = "network_wallet"; + let config_path = temp_dir.path().join("config.toml"); + + save_wallet(&cli, wallet_name); + let before = fs::read_to_string(&config_path).unwrap(); + + save_wallet_cmd(&cli, wallet_name, true).assert().success(); + + let after = fs::read_to_string(&config_path).unwrap(); + assert_ne!(before, after, "--force should save the new descriptors"); + assert!(after.contains("network = \"regtest\"")); + } } // SILENT PAYMENTS From ec3dc451ecaf7e468e75e81b7b3920d6b3d450bf Mon Sep 17 00:00:00 2001 From: Muawiya Abubakar Date: Sun, 8 Nov 2026 02:04:03 +0100 Subject: [PATCH 3/4] fix(wallet): explain network mismatch between config and database A network mismatch between config.toml and the wallet database was wrapped in a generic error that named neither the wallet nor the file to fix. - Add a `NetworkMismatch` error naming the wallet, the network in its database and the network in config.toml, and how to fix it - Pass the wallet name to `new_persisted_wallet` - Add a test for the new error Fixes #359 --- src/error.rs | 10 ++++++++++ src/persister.rs | 16 ++++++++++++++-- src/utils/runtime.rs | 7 ++++++- tests/integration/init.rs | 31 +++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/error.rs b/src/error.rs index 04d1ce6b..e472b47b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -67,6 +67,16 @@ pub enum BDKCliError { #[error("Miniscript compiler error: {0}")] MiniscriptCompilerError(#[from] bdk_wallet::miniscript::policy::compiler::CompilerError), + #[cfg(any(feature = "sqlite", feature = "redb"))] + #[error( + "Network mismatch for wallet '{wallet_name}': its database uses '{db_network}', but config.toml has '{config_network}'. Set network = \"{db_network}\" for this wallet in config.toml." + )] + NetworkMismatch { + wallet_name: String, + db_network: bdk_wallet::bitcoin::Network, + config_network: bdk_wallet::bitcoin::Network, + }, + #[error( "Transaction can weigh up to {weight:#} ({} vB) once signed, over the {:#} ({} vB) \ standard limit, so Bitcoin Core nodes will not relay it", diff --git a/src/persister.rs b/src/persister.rs index 3ff8e61c..bc848dcd 100644 --- a/src/persister.rs +++ b/src/persister.rs @@ -7,7 +7,9 @@ use bdk_redb::redb::TableHandle; use bdk_wallet::Wallet; use bdk_wallet::bitcoin::Network; #[cfg(any(feature = "sqlite", feature = "redb"))] -use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; +use bdk_wallet::{ + KeychainKind, LoadError, LoadMismatch, LoadWithPersistError, PersistedWallet, WalletPersister, +}; #[cfg(any(feature = "sqlite", feature = "redb"))] use clap::ValueEnum; @@ -62,6 +64,7 @@ impl WalletPersister for Persister { #[cfg(any(feature = "sqlite", feature = "redb"))] pub(crate) fn new_persisted_wallet( + wallet_name: &str, network: Network, persister: &mut P, wallet_opts: &WalletOpts, @@ -92,7 +95,16 @@ where let wallet_opt = wallet_load_params .check_network(network) .load_wallet(persister) - .map_err(|e| Error::Generic(e.to_string()))?; + .map_err(|e| match e { + LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( + LoadMismatch::Network { loaded, expected }, + )) => Error::NetworkMismatch { + wallet_name: wallet_name.to_owned(), + db_network: loaded, + config_network: expected, + }, + other => Error::Generic(other.to_string()), + })?; let wallet = match wallet_opt { Some(wallet) => wallet, diff --git a/src/utils/runtime.rs b/src/utils/runtime.rs index 8beb5527..b9d33060 100644 --- a/src/utils/runtime.rs +++ b/src/utils/runtime.rs @@ -100,7 +100,12 @@ impl WalletRuntime { #[cfg(any(feature = "sqlite", feature = "redb"))] if require_db { let mut persister = self.create_persister()?; - let wallet = new_persisted_wallet(self.network, &mut persister, &self.wallet_opts)?; + let wallet = new_persisted_wallet( + &self.wallet_name, + self.network, + &mut persister, + &self.wallet_opts, + )?; return Ok(RuntimeWallet::Persisted( Box::new(wallet), Box::new(persister), diff --git a/tests/integration/init.rs b/tests/integration/init.rs index e34da5d5..f3aaf9bc 100644 --- a/tests/integration/init.rs +++ b/tests/integration/init.rs @@ -1080,6 +1080,37 @@ rpc_password = "preserved-password" assert_ne!(before, after, "--force should save the new descriptors"); assert!(after.contains("network = \"regtest\"")); } + + #[test] + fn test_network_mismatch_error_names_wallet_and_networks() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + let wallet_name = "mismatch_wallet"; + let config_path = temp_dir.path().join("config.toml"); + + save_wallet(&cli, wallet_name); + + // Create the wallet database on regtest. + cli.wallet_cmd(&["--wallet", wallet_name, "new_address"]) + .assert() + .success(); + + // Make config.toml disagree with the database. + let config = fs::read_to_string(&config_path).unwrap(); + assert!(config.contains("network = \"regtest\"")); + fs::write( + &config_path, + config.replace("network = \"regtest\"", "network = \"signet\""), + ) + .unwrap(); + + cli.wallet_cmd(&["--wallet", wallet_name, "new_address"]) + .assert() + .failure() + .stderr(predicate::str::contains( + "Network mismatch for wallet 'mismatch_wallet': its database uses 'regtest', but config.toml has 'signet'", + )); + } } // SILENT PAYMENTS From ee925f505923f671bb00427c36a36b0b2c760405 Mon Sep 17 00:00:00 2001 From: Muawiya Abubakar Date: Sun, 8 Nov 2026 02:04:38 +0100 Subject: [PATCH 4/4] docs(changelog): note network config and mismatch fixes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc29bdb..876abb98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. - Updated dependencies to resolve RUSTSEC advisories in rustls, rustls-webpki, webpki-roots, quinn-proto, h2 and zstd-sys; bumped async-hwi to 0.0.35. - Fixed `create_tx`, `create_dns_tx`, `create_sp_tx` and `bump_fee` building transactions over Bitcoin Core's 400,000 wu (100,000 vB) standard weight limit, which no default node relays - Fixed `key generate` silently producing a 24-word mnemonic for any `--entropy` value other than 12; values other than 12 and 24 are now rejected +- Fixed `wallet config --force` silently changing an existing wallet's network, which left the wallet unable to load; changing the network is now rejected +- Improved the network mismatch error to name the wallet, the network in its database and the network in `config.toml`, and to say how to fix it +- Clarified the `--network` help text: wallet commands other than `wallet config` use the network saved in the wallet configuration ## [4.0.0]