From 1afaafd58a2764294a30da2de59e530652aae972 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 16 Jun 2026 19:56:06 -0500 Subject: [PATCH 1/2] feat: expose Wallet create and load params --- bdk-ffi/src/tests/wallet.rs | 96 +++++++++++++++- bdk-ffi/src/wallet.rs | 215 ++++++++++++++++++++++++++++++++---- 2 files changed, 286 insertions(+), 25 deletions(-) diff --git a/bdk-ffi/src/tests/wallet.rs b/bdk-ffi/src/tests/wallet.rs index c8fcd86f..8efbd970 100644 --- a/bdk-ffi/src/tests/wallet.rs +++ b/bdk-ffi/src/tests/wallet.rs @@ -1,7 +1,8 @@ -use crate::bitcoin::{Network, NetworkKind}; +use crate::bitcoin::{BlockHash, Network, NetworkKind}; use crate::descriptor::Descriptor; +use crate::error::LoadWithPersistError; use crate::store::Persister; -use crate::wallet::Wallet; +use crate::wallet::{CreateParams, LoadParams, Wallet}; use bdk_wallet::KeychainKind; @@ -24,6 +25,15 @@ fn two_path_descriptor() -> Arc { Arc::new(Descriptor::new(TWO_PATH_DESCRIPTOR.to_string(), NetworkKind::Test).unwrap()) } +fn custom_genesis_hash() -> Arc { + Arc::new( + BlockHash::from_string( + "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + ) + .unwrap(), + ) +} + fn build_wallet() -> Wallet { Wallet::new( external_descriptor(), @@ -35,6 +45,88 @@ fn build_wallet() -> Wallet { .unwrap() } +#[test] +fn test_create_wallet_with_params_sets_custom_genesis_hash() { + let genesis_hash = custom_genesis_hash(); + let params = CreateParams { + genesis_hash: Some(Arc::clone(&genesis_hash)), + lookahead: 25, + use_spk_cache: true, + }; + + let wallet = Wallet::create_with_params( + external_descriptor(), + internal_descriptor(), + Network::Signet, + Arc::new(Persister::new_in_memory().unwrap()), + params, + ) + .unwrap(); + + assert_eq!(wallet.network(), Network::Signet); + assert_eq!(wallet.latest_checkpoint().hash, genesis_hash); +} + +#[test] +fn test_load_wallet_with_params_checks_network_and_genesis_hash() { + let persister = Arc::new(Persister::new_in_memory().unwrap()); + let genesis_hash = custom_genesis_hash(); + let create_params = CreateParams { + genesis_hash: Some(Arc::clone(&genesis_hash)), + lookahead: 25, + use_spk_cache: true, + }; + + Wallet::create_with_params( + external_descriptor(), + internal_descriptor(), + Network::Signet, + Arc::clone(&persister), + create_params, + ) + .unwrap(); + + let load_params = LoadParams { + check_network: Some(Network::Signet), + check_genesis_hash: Some(Arc::clone(&genesis_hash)), + lookahead: 25, + use_spk_cache: true, + }; + let wallet = Wallet::load_with_params( + external_descriptor(), + internal_descriptor(), + Arc::clone(&persister), + load_params, + ) + .unwrap(); + + assert_eq!(wallet.network(), Network::Signet); + assert_eq!(wallet.latest_checkpoint().hash, genesis_hash); + + let mismatched_params = LoadParams { + check_network: Some(Network::Bitcoin), + check_genesis_hash: Some(custom_genesis_hash()), + lookahead: 25, + use_spk_cache: true, + }; + let error = match Wallet::load_with_params( + external_descriptor(), + internal_descriptor(), + persister, + mismatched_params, + ) { + Ok(_) => panic!("loading with mismatched network should fail"), + Err(error) => error, + }; + + match error { + LoadWithPersistError::InvalidChangeSet { error_message } => { + assert!(error_message.contains("Network mismatch")); + } + error => panic!("expected InvalidChangeSet error, got {:?}", error), + } +} + #[test] fn test_create_wallet() { let wallet = build_wallet(); diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 0a06ccd0..14a203be 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -1,4 +1,6 @@ -use crate::bitcoin::{Amount, FeeRate, OutPoint, Psbt, Script, Transaction, TxOut, Txid}; +use crate::bitcoin::{ + Amount, BlockHash, FeeRate, OutPoint, Psbt, Script, Transaction, TxOut, Txid, +}; use crate::descriptor::Descriptor; use crate::error::{ CalculateFeeError, CannotConnectError, CreateWithPersistError, DescriptorError, @@ -15,7 +17,10 @@ use bdk_wallet::bitcoin::Network; use bdk_wallet::keys::KeyMap; #[allow(deprecated)] use bdk_wallet::signer::SignOptions as BdkSignOptions; -use bdk_wallet::{PersistedWallet, Wallet as BdkWallet}; +use bdk_wallet::{ + CreateParams as BdkCreateParams, LoadParams as BdkLoadParams, PersistedWallet, + Wallet as BdkWallet, +}; use std::ops::DerefMut; use std::sync::{Arc, Mutex, MutexGuard}; @@ -37,6 +42,79 @@ pub struct Wallet { inner_mutex: Mutex>, } +/// Parameters for `Wallet` creation. +#[derive(Clone, Debug, uniffi::Record)] +pub struct CreateParams { + /// Use a custom `genesis_hash`. + pub genesis_hash: Option>, + /// Use a custom `lookahead` value. + pub lookahead: u32, + /// Use a persistent cache of indexed script pubkeys (SPKs). + pub use_spk_cache: bool, +} + +impl CreateParams { + fn with_lookahead(lookahead: u32) -> Self { + Self { + genesis_hash: None, + lookahead, + use_spk_cache: false, + } + } + + fn apply_to(self, params: BdkCreateParams) -> BdkCreateParams { + let mut params = params + .lookahead(self.lookahead) + .use_spk_cache(self.use_spk_cache); + + if let Some(genesis_hash) = self.genesis_hash { + params = params.genesis_hash(genesis_hash.as_ref().0); + } + + params + } +} + +/// Parameters for `Wallet` loading. +#[derive(Clone, Debug, uniffi::Record)] +pub struct LoadParams { + /// Checks that the given network matches the one loaded from persistence. + pub check_network: Option, + /// Checks that the given `genesis_hash` matches the one loaded from persistence. + pub check_genesis_hash: Option>, + /// Use a custom `lookahead` value. + pub lookahead: u32, + /// Use a persistent cache of indexed script pubkeys (SPKs). + pub use_spk_cache: bool, +} + +impl LoadParams { + fn with_lookahead(lookahead: u32) -> Self { + Self { + check_network: None, + check_genesis_hash: None, + lookahead, + use_spk_cache: false, + } + } + + fn apply_to(self, params: BdkLoadParams) -> BdkLoadParams { + let mut params = params + .lookahead(self.lookahead) + .use_spk_cache(self.use_spk_cache); + + if let Some(network) = self.check_network { + params = params.check_network(network); + } + + if let Some(genesis_hash) = self.check_genesis_hash { + params = params.check_genesis_hash(genesis_hash.as_ref().0); + } + + params + } +} + #[uniffi::export] impl Wallet { /// Build a new Wallet. @@ -49,18 +127,38 @@ impl Wallet { network: Network, persister: Arc, lookahead: u32, + ) -> Result { + Self::create_with_params( + descriptor, + change_descriptor, + network, + persister, + CreateParams::with_lookahead(lookahead), + ) + } + + /// Build a new Wallet with explicit create parameters. + /// + /// If you have previously created a wallet, use load instead. + #[uniffi::constructor] + pub fn create_with_params( + descriptor: Arc, + change_descriptor: Arc, + network: Network, + persister: Arc, + params: CreateParams, ) -> Result { let descriptor = descriptor.to_string_with_secret(); let change_descriptor = change_descriptor.to_string_with_secret(); let mut persist_lock = persister.inner.lock().unwrap(); let deref = persist_lock.deref_mut(); - let wallet: PersistedWallet = - BdkWallet::create(descriptor, change_descriptor) - .network(network) - .lookahead(lookahead) - .create_wallet(deref) - .map_err(CreateWithPersistError::from)?; + let bdk_params = BdkWallet::create(descriptor, change_descriptor).network(network); + let bdk_params = params.apply_to(bdk_params); + + let wallet: PersistedWallet = bdk_params + .create_wallet(deref) + .map_err(CreateWithPersistError::from)?; Ok(Wallet { inner_mutex: Mutex::new(wallet), @@ -91,14 +189,33 @@ impl Wallet { network: Network, persister: Arc, lookahead: u32, + ) -> Result { + Self::create_single_with_params( + descriptor, + network, + persister, + CreateParams::with_lookahead(lookahead), + ) + } + + /// Build a new single descriptor `Wallet` with explicit create parameters. + /// + /// If you have previously created a wallet, use `Wallet::load` instead. + #[uniffi::constructor] + pub fn create_single_with_params( + descriptor: Arc, + network: Network, + persister: Arc, + params: CreateParams, ) -> Result { let descriptor = descriptor.to_string_with_secret(); let mut persist_lock = persister.inner.lock().unwrap(); let deref = persist_lock.deref_mut(); - let wallet: PersistedWallet = BdkWallet::create_single(descriptor) - .network(network) - .lookahead(lookahead) + let bdk_params = BdkWallet::create_single(descriptor).network(network); + let bdk_params = params.apply_to(bdk_params); + + let wallet: PersistedWallet = bdk_params .create_wallet(deref) .map_err(CreateWithPersistError::from)?; @@ -127,17 +244,35 @@ impl Wallet { network: Network, persister: Arc, lookahead: u32, + ) -> Result { + Self::create_from_two_path_descriptor_with_params( + two_path_descriptor, + network, + persister, + CreateParams::with_lookahead(lookahead), + ) + } + + /// Build a new `Wallet` from a two-path descriptor with explicit create parameters. + /// + /// If you have previously created a wallet, use load instead. + #[uniffi::constructor] + pub fn create_from_two_path_descriptor_with_params( + two_path_descriptor: Arc, + network: Network, + persister: Arc, + params: CreateParams, ) -> Result { let descriptor = two_path_descriptor.to_string_with_secret(); let mut persist_lock = persister.inner.lock().unwrap(); let deref = persist_lock.deref_mut(); - let wallet: PersistedWallet = - BdkWallet::create_from_two_path_descriptor(descriptor) - .network(network) - .lookahead(lookahead) - .create_wallet(deref) - .map_err(CreateWithPersistError::from)?; + let bdk_params = BdkWallet::create_from_two_path_descriptor(descriptor).network(network); + let bdk_params = params.apply_to(bdk_params); + + let wallet: PersistedWallet = bdk_params + .create_wallet(deref) + .map_err(CreateWithPersistError::from)?; Ok(Wallet { inner_mutex: Mutex::new(wallet), @@ -153,17 +288,37 @@ impl Wallet { change_descriptor: Arc, persister: Arc, lookahead: u32, + ) -> Result { + Self::load_with_params( + descriptor, + change_descriptor, + persister, + LoadParams::with_lookahead(lookahead), + ) + } + + /// Build Wallet by loading from persistence with explicit load parameters. + /// + /// Note that the descriptor secret keys are not persisted to the db. + #[uniffi::constructor] + pub fn load_with_params( + descriptor: Arc, + change_descriptor: Arc, + persister: Arc, + params: LoadParams, ) -> Result { let descriptor = descriptor.to_string_with_secret(); let change_descriptor = change_descriptor.to_string_with_secret(); let mut persist_lock = persister.inner.lock().unwrap(); let deref = persist_lock.deref_mut(); - let wallet: PersistedWallet = BdkWallet::load() + let bdk_params = BdkWallet::load() .descriptor(KeychainKind::External, Some(descriptor)) .descriptor(KeychainKind::Internal, Some(change_descriptor)) - .lookahead(lookahead) - .extract_keys() + .extract_keys(); + let bdk_params = params.apply_to(bdk_params); + + let wallet: PersistedWallet = bdk_params .load_wallet(deref) .map_err(LoadWithPersistError::from)? .ok_or(LoadWithPersistError::CouldNotLoad)?; @@ -209,15 +364,29 @@ impl Wallet { descriptor: Arc, persister: Arc, lookahead: u32, + ) -> Result { + Self::load_single_with_params(descriptor, persister, LoadParams::with_lookahead(lookahead)) + } + + /// Build a single-descriptor Wallet by loading from persistence with explicit load parameters. + /// + /// Note that the descriptor secret keys are not persisted to the db. + #[uniffi::constructor] + pub fn load_single_with_params( + descriptor: Arc, + persister: Arc, + params: LoadParams, ) -> Result { let descriptor = descriptor.to_string_with_secret(); let mut persist_lock = persister.inner.lock().unwrap(); let deref = persist_lock.deref_mut(); - let wallet: PersistedWallet = BdkWallet::load() + let bdk_params = BdkWallet::load() .descriptor(KeychainKind::External, Some(descriptor)) - .lookahead(lookahead) - .extract_keys() + .extract_keys(); + let bdk_params = params.apply_to(bdk_params); + + let wallet: PersistedWallet = bdk_params .load_wallet(deref) .map_err(LoadWithPersistError::from)? .ok_or(LoadWithPersistError::CouldNotLoad)?; From af845a5a2da9f80ca9885eef646fd5f3269e942b Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 28 Jul 2026 16:42:19 -0500 Subject: [PATCH 2/2] feat: add params aware two path wallet loading --- bdk-ffi/src/tests/wallet.rs | 34 ++++++++++++++++++++++++++++++++++ bdk-ffi/src/wallet.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/bdk-ffi/src/tests/wallet.rs b/bdk-ffi/src/tests/wallet.rs index 8efbd970..5515a180 100644 --- a/bdk-ffi/src/tests/wallet.rs +++ b/bdk-ffi/src/tests/wallet.rs @@ -280,3 +280,37 @@ fn test_load_from_two_path_descriptor() { 1 ); } + +#[test] +fn test_load_from_two_path_descriptor_with_params() { + let persister = Arc::new(Persister::new_in_memory().unwrap()); + Wallet::create_from_two_path_descriptor( + two_path_descriptor(), + Network::Signet, + Arc::clone(&persister), + 25, + ) + .unwrap(); + + let params = LoadParams { + check_network: Some(Network::Bitcoin), + check_genesis_hash: None, + lookahead: 25, + use_spk_cache: false, + }; + let error = match Wallet::load_from_two_path_descriptor_with_params( + two_path_descriptor(), + persister, + params, + ) { + Ok(_) => panic!("loading with mismatched network should fail"), + Err(error) => error, + }; + + match error { + LoadWithPersistError::InvalidChangeSet { error_message } => { + assert!(error_message.contains("Network mismatch")); + } + error => panic!("expected InvalidChangeSet error, got {:?}", error), + } +} diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 14a203be..4929d724 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -339,14 +339,35 @@ impl Wallet { two_path_descriptor: Arc, persister: Arc, lookahead: u32, + ) -> Result { + Self::load_from_two_path_descriptor_with_params( + two_path_descriptor, + persister, + LoadParams::with_lookahead(lookahead), + ) + } + + /// Build a two-path descriptor `Wallet` by loading from persistence with explicit load + /// parameters. + /// + /// Checks that the provided two-path descriptor matches exactly what is loaded + /// for both the external and internal keychains. + /// + /// The provided descriptor may only contain extended public keys (`xpub`) with exactly 2 paths. + #[uniffi::constructor] + pub fn load_from_two_path_descriptor_with_params( + two_path_descriptor: Arc, + persister: Arc, + params: LoadParams, ) -> Result { let descriptor = two_path_descriptor.to_string(); let mut persist_lock = persister.inner.lock().unwrap(); let deref = persist_lock.deref_mut(); - let wallet: PersistedWallet = BdkWallet::load() - .two_path_descriptor(descriptor) - .lookahead(lookahead) + let bdk_params = BdkWallet::load().two_path_descriptor(descriptor); + let bdk_params = params.apply_to(bdk_params); + + let wallet: PersistedWallet = bdk_params .load_wallet(deref) .map_err(LoadWithPersistError::from)? .ok_or(LoadWithPersistError::CouldNotLoad)?;