From 768c8f1a12fbfd9a427d4f7cc513b29ed260622e Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Thu, 15 Oct 2026 14:54:46 +0100 Subject: [PATCH 1/4] chore: bump bdk_wallet to 3.2.0 --- .cargo/config.toml | 2 ++ Cargo.lock | 25 ++++++++++++++++++++++--- Cargo.toml | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..3693fc55 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "bdk_wallet_unstable"] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 29bd4886..665d4527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,6 +378,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bdk_coin_select" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43064fa3dd1d3a8b24079cfcb5ede6b785857edc782277f17a1736511ccc1916" + [[package]] name = "bdk_core" version = "0.6.3" @@ -463,13 +469,26 @@ dependencies = [ "electrsd", ] +[[package]] +name = "bdk_tx" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40406c9952f15ed30893826bd83b7d67b6bf11d56574c1efc3db3a5d70cdfcea" +dependencies = [ + "bdk_coin_select", + "miniscript", + "rand 0.8.6", + "rand_core 0.6.4", +] + [[package]] name = "bdk_wallet" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" +checksum = "163ec032e0a5a795ded2a7e47fc40a887c75743326483147e4465159c53abdc4" dependencies = [ "bdk_chain", + "bdk_tx", "bip39", "bitcoin", "miniscript", @@ -4207,7 +4226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 9319864d..d21f4ccd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" license = "MIT" [dependencies] -bdk_wallet = { version = "3.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } +bdk_wallet = { version = "3.2.0", features = ["bdk-tx", "rusqlite", "keys-bip39", "compiler", "std"] } clap = { version = "4.6", features = ["derive","env"] } clap_complete = "4.6" dirs = { version = "6.0.0" } From a48cab65f7202b2f93e1836b9a4ed80f0c3e9c7c Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Thu, 15 Oct 2026 17:34:25 +0100 Subject: [PATCH 2/4] feat: introduce a new WalletSigner struct which will hold signer key materials --- src/handlers/mod.rs | 14 +++++++-- src/handlers/repl.rs | 5 +++- src/main.rs | 6 ++++ src/persister.rs | 70 ++++++++++++++++++++++++++++++++++++++++++-- src/utils/runtime.rs | 6 +++- 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 7d3d7f8c..d072d519 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -25,6 +25,7 @@ pub mod repl; use crate::client::BlockchainClient; use std::path::PathBuf; +use crate::persister::WalletSigners; use crate::{error::BDKCliError as Error, utils::output::FormatOutput}; use bdk_wallet::{Wallet, bitcoin::Network}; @@ -35,6 +36,7 @@ pub struct Init; /// Requires only a wallet. pub struct OfflineOperations<'a> { pub wallet: &'a mut Wallet, + pub signers: &'a WalletSigners, } #[cfg(any( @@ -47,6 +49,7 @@ pub struct OfflineOperations<'a> { /// Requires a wallet and a client. pub struct OnlineOperations<'a> { pub wallet: &'a mut Wallet, + pub signers: &'a WalletSigners, pub client: &'a BlockchainClient, pub wallet_name: String, } @@ -70,11 +73,16 @@ impl AppContext { } impl<'a> AppContext> { - pub fn new_offline_wallet(network: Network, datadir: PathBuf, wallet: &'a mut Wallet) -> Self { + pub fn new_offline_wallet( + network: Network, + datadir: PathBuf, + wallet: &'a mut Wallet, + signers: &'a WalletSigners, + ) -> Self { Self { network, datadir, - state: OfflineOperations { wallet }, + state: OfflineOperations { wallet, signers }, } } } @@ -90,6 +98,7 @@ impl<'a> AppContext> { network: Network, datadir: PathBuf, wallet: &'a mut Wallet, + signers: &'a WalletSigners, client: &'a BlockchainClient, wallet_name: String, ) -> Self { @@ -98,6 +107,7 @@ impl<'a> AppContext> { datadir, state: OnlineOperations { wallet, + signers, client, wallet_name, }, diff --git a/src/handlers/repl.rs b/src/handlers/repl.rs index b93916e9..3c30ae37 100644 --- a/src/handlers/repl.rs +++ b/src/handlers/repl.rs @@ -2,6 +2,7 @@ use { crate::commands::ReplSubCommand, crate::handlers::{AppCommand, AppContext}, + crate::persister::WalletSigners, crate::utils::output::FormatOutput, bdk_wallet::{Wallet, bitcoin::Network}, clap::Parser, @@ -24,6 +25,7 @@ use {crate::commands::WalletSubCommand, crate::error::BDKCliError as Error, std: pub(crate) async fn respond( network: Network, wallet: &mut Wallet, + signers: &WalletSigners, #[cfg(any( feature = "electrum", feature = "esplora", @@ -54,7 +56,7 @@ pub(crate) async fn respond( let response = match repl_subcommand { ReplSubCommand::Wallet { subcommand } => match subcommand { WalletSubCommand::OfflineWalletSubCommand(cmd) => { - let mut ctx = AppContext::new_offline_wallet(network, datadir, wallet); + let mut ctx = AppContext::new_offline_wallet(network, datadir, wallet, signers); cmd.execute(&mut ctx).map_err(|e| e.to_string())?; Some(()) } @@ -70,6 +72,7 @@ pub(crate) async fn respond( network, datadir, wallet, + signers, client_ref, wallet_name.to_string(), ); diff --git a/src/main.rs b/src/main.rs index 7ed73d46..d41bd876 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,12 +68,14 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { WalletSubCommand::OnlineWalletSubCommand(cmd) => { let runtime = WalletRuntime::load(&home_dir, &wallet_name)?; let mut wallet = runtime.build_wallet(true)?; + let signers = runtime.build_signers()?; let client = runtime.build_client(&wallet)?; { let mut ctx = AppContext::new_online_wallet( runtime.network, runtime.home_dir.clone(), &mut wallet, + &signers, &client, runtime.wallet_name.clone(), ); @@ -86,12 +88,14 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { WalletSubCommand::OfflineWalletSubCommand(cmd) => { let runtime = WalletRuntime::load(&home_dir, &wallet_name)?; let mut wallet = runtime.build_wallet(command_requires_db(&cmd))?; + let signers = runtime.build_signers()?; { let mut ctx = AppContext::new_offline_wallet( runtime.network, runtime.home_dir.clone(), &mut wallet, + &signers, ); match cmd { @@ -157,6 +161,7 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { let runtime = WalletRuntime::load(&home_dir, &wallet_name)?; let mut wallet = runtime.build_wallet(true)?; + let signers = runtime.build_signers()?; #[cfg(any( feature = "electrum", @@ -182,6 +187,7 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { let should_exit = crate::handlers::repl::respond( runtime.network, &mut wallet, + &signers, #[cfg(any( feature = "electrum", feature = "esplora", diff --git a/src/persister.rs b/src/persister.rs index 3ff8e61c..63d5d29b 100644 --- a/src/persister.rs +++ b/src/persister.rs @@ -4,10 +4,12 @@ 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; +use bdk_wallet::bitcoin::{Network, NetworkKind, secp256k1::Secp256k1}; +use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::miniscript::descriptor::KeyMap; +use bdk_wallet::{KeychainKind, Wallet}; #[cfg(any(feature = "sqlite", feature = "redb"))] -use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; +use bdk_wallet::{PersistedWallet, WalletPersister}; #[cfg(any(feature = "sqlite", feature = "redb"))] use clap::ValueEnum; @@ -31,6 +33,68 @@ pub(crate) enum Persister { RedbStore(bdk_redb::Store), } +/// Keep the descriptor key maps outside the +/// wallet and construct the signer only when a PSBT is being signed. +pub(crate) struct WalletSigners { + pub external: KeyMap, + pub internal: KeyMap, +} + +impl WalletSigners { + pub(crate) fn new(network: Network, wallet_opts: &WalletOpts) -> Result { + let secp = Secp256k1::new(); + let ext_is_multipath = validate_descriptor_pair( + &wallet_opts.ext_descriptor, + wallet_opts.int_descriptor.as_deref(), + network, + )?; + + if ext_is_multipath { + return Err(Error::Generic( + "Multipath descriptors are not supported by WalletSigners yet.".to_string(), + )); + } + + let (_, external_keymap) = wallet_opts + .ext_descriptor + .clone() + .into_wallet_descriptor(&secp, NetworkKind::from(network))?; + let external = external_keymap; + + let internal = wallet_opts + .int_descriptor + .clone() + .map(|descriptor| { + descriptor + .into_wallet_descriptor(&secp, NetworkKind::from(network)) + .map(|(_, keymap)| keymap) + }) + .transpose()?; + + Ok(Self { + external, + internal: internal.unwrap_or_default(), + }) + } + + pub(crate) fn sign_psbt( + &self, + wallet: &Wallet, + psbt: &mut bdk_wallet::bitcoin::Psbt, + ) -> Result<(), Error> { + let signer = bdk_wallet::bdk_tx::Signer( + self.external + .clone() + .into_iter() + .chain(self.internal.clone()) + .collect(), + ); + psbt.sign(&signer, wallet.secp_ctx()) + .map(|_| ()) + .map_err(|(_, errors)| Error::Generic(format!("Failed to sign PSBT: {errors:?}"))) + } +} + #[cfg(any(feature = "sqlite", feature = "redb"))] impl WalletPersister for Persister { type Error = Error; diff --git a/src/utils/runtime.rs b/src/utils/runtime.rs index 8beb5527..f7739a1d 100644 --- a/src/utils/runtime.rs +++ b/src/utils/runtime.rs @@ -8,7 +8,7 @@ use std::{ use crate::{ error::BDKCliError as Error, - persister::new_wallet, + persister::{WalletSigners, new_wallet}, utils::{load_wallet_config, prepare_wallet_db_dir}, }; #[cfg(any(feature = "sqlite", feature = "redb"))] @@ -113,6 +113,10 @@ impl WalletRuntime { )?))) } + pub fn build_signers(&self) -> Result { + WalletSigners::new(self.network, &self.wallet_opts) + } + #[cfg(any( feature = "electrum", feature = "esplora", From 689a78185a769a488eb6406b502df014b3c760ad Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Wed, 11 Nov 2026 07:55:38 +0100 Subject: [PATCH 3/4] feat: replace the deprecated policy_path with condition --- src/handlers/offline.rs | 43 ++++++++++++++++++++++++++++------------- src/persister.rs | 8 +------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 80a6de47..7658d8a8 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -14,6 +14,9 @@ use bdk_wallet::bitcoin::base64::Engine; use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD; use bdk_wallet::bitcoin::script::PushBytesBuf; use bdk_wallet::bitcoin::{Address, Amount, FeeRate, OutPoint, Psbt, ScriptBuf, Sequence, Txid}; +use bdk_wallet::descriptor::ExtractPolicy; +use bdk_wallet::descriptor::policy::{BuildSatisfaction, Condition}; +use bdk_wallet::signer::SignersContainer; use bdk_wallet::{KeychainKind, SignOptions}; use clap::Parser; use serde_json::json; @@ -255,6 +258,32 @@ impl AppCommand>> for CreateTxCommand { type Output = PsbtResult; fn execute(&self, ctx: &mut AppContext>) -> Result { + let mut condition = Condition::default(); + for (keychain, descriptor) in ctx.state.wallet.keychains() { + let policy = descriptor + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + ctx.state.wallet.secp_ctx(), + )? + .expect("wallet descriptors must have a policy"); + + let policy_path = match keychain { + KeychainKind::External => self.external_policy.as_deref(), + KeychainKind::Internal => self.internal_policy.as_deref(), + } + .map(serde_json::from_str::>>) + .transpose()? + .unwrap_or_default(); + + let policy_condition = policy + .get_condition(&policy_path) + .map_err(|e| Error::Generic(e.to_string()))?; + condition = condition + .merge(&policy_condition) + .map_err(|e| Error::Generic(e.to_string()))?; + } + let mut tx_builder = ctx.state.wallet.build_tx(); if self.send_all { @@ -301,19 +330,7 @@ impl AppCommand>> for CreateTxCommand { tx_builder.add_data(data); } - let policies = vec![ - self.external_policy - .clone() - .map(|p| (p, KeychainKind::External)), - self.internal_policy - .clone() - .map(|p| (p, KeychainKind::Internal)), - ]; - - for (policy, keychain) in policies.into_iter().flatten() { - let policy = serde_json::from_str::>>(&policy)?; - tx_builder.policy_path(policy, keychain); - } + tx_builder.set_condition(condition); let psbt = tx_builder.finish()?; diff --git a/src/persister.rs b/src/persister.rs index 63d5d29b..118e2784 100644 --- a/src/persister.rs +++ b/src/persister.rs @@ -43,18 +43,12 @@ pub(crate) struct WalletSigners { impl WalletSigners { pub(crate) fn new(network: Network, wallet_opts: &WalletOpts) -> Result { let secp = Secp256k1::new(); - let ext_is_multipath = validate_descriptor_pair( + validate_descriptor_pair( &wallet_opts.ext_descriptor, wallet_opts.int_descriptor.as_deref(), network, )?; - if ext_is_multipath { - return Err(Error::Generic( - "Multipath descriptors are not supported by WalletSigners yet.".to_string(), - )); - } - let (_, external_keymap) = wallet_opts .ext_descriptor .clone() From 6206b0587592ce128898b573777ee32062df5c26 Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Wed, 11 Nov 2026 09:30:02 +0100 Subject: [PATCH 4/4] feat: use the new signers in offline commands --- src/handlers/offline.rs | 24 ++++++++++-------------- src/main.rs | 2 ++ src/persister.rs | 4 +++- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 7658d8a8..0c7fe27d 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -489,7 +489,9 @@ impl AppCommand>> for CreateSpTxCommand { let unsigned_psbt = psbt.clone(); - let finalized = ctx.state.wallet.sign(&mut psbt, SignOptions::default())?; + let wallet = &ctx.state.wallet; + ctx.state.signers.sign_psbt(wallet, &mut psbt)?; + let finalized = wallet.finalize_psbt(&mut psbt, SignOptions::default())?; if !finalized { return Err(Error::Generic( @@ -505,16 +507,8 @@ impl AppCommand>> for CreateSpTxCommand { } let secp = Secp256k1::new(); - let mut external_signers = ctx - .state - .wallet - .get_signers(KeychainKind::External) - .as_key_map(&secp); - let internal_signers = ctx - .state - .wallet - .get_signers(KeychainKind::Internal) - .as_key_map(&secp); + let mut external_signers = ctx.state.signers.external.clone(); + let internal_signers = ctx.state.signers.internal.clone(); external_signers.extend(internal_signers); match external_signers.iter().next().expect("not empty") { @@ -543,7 +537,8 @@ impl AppCommand>> for CreateSpTxCommand { psbt_input.final_script_witness = None; } - let _resigned = ctx.state.wallet.sign(&mut psbt, SignOptions::default())?; + ctx.state.signers.sign_psbt(wallet, &mut psbt)?; + let _resigned = wallet.finalize_psbt(&mut psbt, SignOptions::default())?; let raw_tx = psbt.extract_tx()?; @@ -670,7 +665,7 @@ impl AppCommand>> for SignCommand { type Output = PsbtResult; fn execute(&self, ctx: &mut AppContext>) -> Result { - let wallet = &mut ctx.state.wallet; + let wallet = &ctx.state.wallet; let psbt_bytes = BASE64_STANDARD .decode(&self.psbt) .map_err(|e| Error::Generic(e.to_string()))?; @@ -681,7 +676,8 @@ impl AppCommand>> for SignCommand { trust_witness_utxo: self.trust_witness_utxo.unwrap_or(false), ..Default::default() }; - let finalized = wallet.sign(&mut psbt, signopt)?; + ctx.state.signers.sign_psbt(wallet, &mut psbt)?; + let finalized = wallet.finalize_psbt(&mut psbt, signopt)?; Ok(PsbtResult::new(&psbt, Some(finalized))) } } diff --git a/src/main.rs b/src/main.rs index d41bd876..59178b97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -123,11 +123,13 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { WalletSubCommand::Hwi(cmd) => { let runtime = WalletRuntime::load(&home_dir, &wallet_name)?; let mut wallet = runtime.build_wallet(false)?; + let signers = runtime.build_signers()?; let mut ctx = AppContext::new_offline_wallet( runtime.network, runtime.home_dir.clone(), &mut wallet, + &signers, ); cmd.run(&mut ctx, &wallet_name) diff --git a/src/persister.rs b/src/persister.rs index 118e2784..386131bc 100644 --- a/src/persister.rs +++ b/src/persister.rs @@ -4,10 +4,12 @@ use crate::utils::descriptors::validate_descriptor_pair; use crate::utils::path_exists; #[cfg(feature = "redb")] use bdk_redb::redb::TableHandle; +#[cfg(any(feature = "sqlite", feature = "redb"))] +use bdk_wallet::KeychainKind; +use bdk_wallet::Wallet; use bdk_wallet::bitcoin::{Network, NetworkKind, secp256k1::Secp256k1}; use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::miniscript::descriptor::KeyMap; -use bdk_wallet::{KeychainKind, Wallet}; #[cfg(any(feature = "sqlite", feature = "redb"))] use bdk_wallet::{PersistedWallet, WalletPersister}; #[cfg(any(feature = "sqlite", feature = "redb"))]