diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index ac83d2fcf..3c127c7bb 100644 --- a/bdk-ffi/Cargo.lock +++ b/bdk-ffi/Cargo.lock @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67f3c4f9526d22374fca5b7ff1d6bf8d921ab56db2dac8df66a2c5561b31d4ef" +checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" dependencies = [ "bdk_chain", "bip39", diff --git a/bdk-ffi/Cargo.toml b/bdk-ffi/Cargo.toml index 12936bfad..77665bc49 100644 --- a/bdk-ffi/Cargo.toml +++ b/bdk-ffi/Cargo.toml @@ -15,7 +15,7 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" [dependencies] -bdk_wallet = { version = "=3.0.0", features = ["all-keys", "keys-bip39", "rusqlite"] } +bdk_wallet = { version = "=3.1.0", features = ["all-keys", "keys-bip39", "rusqlite"] } bdk_esplora = { version = "0.22.2", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] } bdk_electrum = { version = "0.24.0", default-features = false, features = ["use-rustls-ring"] } bdk_kyoto = { version = "0.17.0" } diff --git a/bdk-ffi/src/lib.rs b/bdk-ffi/src/lib.rs index 1693ff35b..8ee2026d8 100644 --- a/bdk-ffi/src/lib.rs +++ b/bdk-ffi/src/lib.rs @@ -6,6 +6,7 @@ mod esplora; mod keys; mod kyoto; mod macros; +mod signer; mod store; mod tx_builder; mod types; diff --git a/bdk-ffi/src/signer.rs b/bdk-ffi/src/signer.rs new file mode 100644 index 000000000..ad7e4cca5 --- /dev/null +++ b/bdk-ffi/src/signer.rs @@ -0,0 +1,58 @@ +use crate::descriptor::Descriptor; + +use bdk_wallet::bitcoin::key::Secp256k1; +use bdk_wallet::signer::SignersContainer as BdkSignersContainer; + +use std::sync::Arc; + +/// Container for multiple signers. +#[derive(Debug, uniffi::Object)] +pub struct SignersContainer { + pub(crate) inner: BdkSignersContainer, +} + +#[uniffi::export] +impl SignersContainer { + /// Build a new signer container from a descriptor's key map. + /// + /// Also looks at the same descriptor to determine the `SignerContext` to attach to the signers. + #[uniffi::constructor] + pub fn from_descriptor(descriptor: Arc) -> Self { + Self::from_descriptor_with_context(Arc::clone(&descriptor), descriptor) + } + + /// Build a new signer container from a signer descriptor's key map. + /// + /// Also looks at the corresponding descriptor to determine the `SignerContext` to attach to + /// the signers. + #[uniffi::constructor] + pub fn from_descriptor_with_context( + signer_descriptor: Arc, + context_descriptor: Arc, + ) -> Self { + let secp = Secp256k1::new(); + let inner = BdkSignersContainer::build( + signer_descriptor.key_map.clone(), + &context_descriptor.extended_descriptor, + &secp, + ); + + Self { inner } + } + + /// Returns the number of signers in the container. + pub fn len(&self) -> u64 { + self.inner.signers().len() as u64 + } + + /// Returns true when the container has no signers. + pub fn is_empty(&self) -> bool { + self.inner.signers().is_empty() + } +} + +impl From for SignersContainer { + fn from(inner: BdkSignersContainer) -> Self { + Self { inner } + } +} diff --git a/bdk-ffi/src/tests/tx_builder.rs b/bdk-ffi/src/tests/tx_builder.rs index 42508f60b..42c83db48 100644 --- a/bdk-ffi/src/tests/tx_builder.rs +++ b/bdk-ffi/src/tests/tx_builder.rs @@ -1,6 +1,6 @@ -use crate::bitcoin::{Amount, Input, Network, NetworkKind, OutPoint, Script, TxOut}; +use crate::bitcoin::{Amount, Input, Network, NetworkKind, OutPoint, Script, Transaction, TxOut}; use crate::descriptor::Descriptor; -use crate::error::SighashParseError; +use crate::error::{AddForeignUtxoError, SighashParseError}; use crate::esplora::EsploraClient; use crate::store::Persister; use crate::tx_builder::TxBuilder; @@ -8,6 +8,10 @@ use crate::types::FullScanScriptInspector; use crate::wallet::Wallet; use bdk_wallet::bitcoin::hashes::hex::FromHex; +use bdk_wallet::bitcoin::{ + absolute, consensus::serialize, transaction, Amount as BdkAmount, ScriptBuf as BdkScriptBuf, + Transaction as BdkTransaction, TxIn as BdkTxIn, TxOut as BdkTxOut, +}; use std::collections::HashMap; use std::sync::Arc; @@ -300,6 +304,94 @@ fn test_add_foreign_utxo_missing_witness_data() { ); } +#[test] +fn test_add_foreign_utxo_validates_non_witness_utxo_when_witness_utxo_is_present() { + let outpoint = OutPoint { + txid: Arc::new( + crate::bitcoin::Txid::from_string( + "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456".to_string(), + ) + .unwrap(), + ), + vout: 0, + }; + + let witness_utxo = TxOut { + value: Arc::new(Amount::from_sat(50_000)), + script_pubkey: Arc::new(Script::new( + Vec::from_hex("0014d85c2b71d0060b09c9886aeb815e50991dda124d").unwrap(), + )), + }; + + let non_witness_tx = BdkTransaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::ZERO, + input: vec![BdkTxIn::default()], + output: vec![BdkTxOut { + value: BdkAmount::from_sat(50_000), + script_pubkey: BdkScriptBuf::new(), + }], + }; + let non_witness_utxo = Arc::new(Transaction::new(serialize(&non_witness_tx)).unwrap()); + + let psbt_input = Input { + non_witness_utxo: Some(non_witness_utxo.clone()), + witness_utxo: Some(witness_utxo.clone()), + partial_sigs: HashMap::new(), + sighash_type: None, + redeem_script: None, + witness_script: None, + bip32_derivation: HashMap::new(), + final_script_sig: None, + final_script_witness: None, + ripemd160_preimages: HashMap::new(), + sha256_preimages: HashMap::new(), + hash160_preimages: HashMap::new(), + hash256_preimages: HashMap::new(), + tap_key_sig: None, + tap_script_sigs: HashMap::new(), + tap_scripts: HashMap::new(), + tap_key_origins: HashMap::new(), + tap_internal_key: None, + tap_merkle_root: None, + proprietary: HashMap::new(), + unknown: HashMap::new(), + }; + + let result = TxBuilder::new().add_foreign_utxo(outpoint.clone(), psbt_input, 68); + + assert!(matches!(result, Err(AddForeignUtxoError::InvalidTxid))); + + let psbt_input_with_sequence = Input { + non_witness_utxo: Some(non_witness_utxo), + witness_utxo: Some(witness_utxo), + partial_sigs: HashMap::new(), + sighash_type: None, + redeem_script: None, + witness_script: None, + bip32_derivation: HashMap::new(), + final_script_sig: None, + final_script_witness: None, + ripemd160_preimages: HashMap::new(), + sha256_preimages: HashMap::new(), + hash160_preimages: HashMap::new(), + hash256_preimages: HashMap::new(), + tap_key_sig: None, + tap_script_sigs: HashMap::new(), + tap_scripts: HashMap::new(), + tap_key_origins: HashMap::new(), + tap_internal_key: None, + tap_merkle_root: None, + proprietary: HashMap::new(), + unknown: HashMap::new(), + }; + + let result = + TxBuilder::new().add_foreign_utxo_with_sequence(outpoint, psbt_input_with_sequence, 68, 0); + + assert!(matches!(result, Err(AddForeignUtxoError::InvalidTxid))); +} + #[test] #[ignore = "requires live MutinyNet Esplora access"] fn test_add_foreign_utxo_with_witness_utxo_succeeds() { diff --git a/bdk-ffi/src/tests/wallet.rs b/bdk-ffi/src/tests/wallet.rs index 01967986e..0bda6ec45 100644 --- a/bdk-ffi/src/tests/wallet.rs +++ b/bdk-ffi/src/tests/wallet.rs @@ -1,8 +1,12 @@ -use crate::bitcoin::{Network, NetworkKind}; +use crate::bitcoin::{BlockHash, Network, NetworkKind, Psbt, Transaction}; use crate::descriptor::Descriptor; +use crate::error::LoadWithPersistError; +use crate::signer::SignersContainer; use crate::store::Persister; -use crate::wallet::Wallet; +use crate::wallet::{CreateParams, LoadParams, Wallet}; +use bdk_wallet::bitcoin::Transaction as BdkTransaction; +use bdk_wallet::bitcoin::{absolute, consensus::serialize, transaction}; use bdk_wallet::KeychainKind; use std::sync::Arc; @@ -24,6 +28,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 +48,100 @@ fn build_wallet() -> Wallet { .unwrap() } +fn empty_psbt() -> Arc { + let tx = BdkTransaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + let tx = Arc::new(Transaction::new(serialize(&tx)).unwrap()); + + Psbt::from_unsigned_tx(tx).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(); @@ -57,6 +164,41 @@ fn test_reveal_next_address() { assert_eq!(address_info.address.to_string(), EXPECTED_FIRST_ADDRESS); } +#[test] +fn test_signers_container_from_descriptor() { + let signers = SignersContainer::from_descriptor(external_descriptor()); + + assert!(!signers.is_empty()); + assert_eq!(signers.len(), 1); +} + +#[test] +fn test_get_signers() { + let wallet = build_wallet(); + + let external_signers = wallet.get_signers(KeychainKind::External); + let internal_signers = wallet.get_signers(KeychainKind::Internal); + + assert!(!external_signers.is_empty()); + assert!(!internal_signers.is_empty()); + assert_eq!(external_signers.len(), 1); + assert_eq!(internal_signers.len(), 1); +} + +#[test] +fn test_sign_with_signers() { + let wallet = build_wallet(); + let psbt = empty_psbt(); + let signers = vec![ + wallet.get_signers(KeychainKind::External), + wallet.get_signers(KeychainKind::Internal), + ]; + + let finalized = wallet.sign_with_signers(psbt, signers, None).unwrap(); + + assert!(finalized); +} + #[test] fn test_create_single_wallet() { let wallet = Wallet::create_single( @@ -105,3 +247,87 @@ fn test_create_two_path_wallet() { assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); assert_eq!(wallet.derivation_index(KeychainKind::Internal), Some(0)); } + +#[test] +fn test_load_from_two_path_descriptor() { + let persister = Arc::new(Persister::new_in_memory().unwrap()); + let wallet = Wallet::create_from_two_path_descriptor( + two_path_descriptor(), + Network::Signet, + Arc::clone(&persister), + 25, + ) + .unwrap(); + + wallet.reveal_next_address(KeychainKind::External); + wallet.reveal_next_address(KeychainKind::Internal); + assert!(wallet.persist(Arc::clone(&persister)).unwrap()); + + let loaded_wallet = + Wallet::load_from_two_path_descriptor(two_path_descriptor(), Arc::clone(&persister), 25) + .unwrap(); + + assert_eq!(loaded_wallet.network(), Network::Signet); + assert_eq!( + loaded_wallet.derivation_index(KeychainKind::External), + Some(0) + ); + assert_eq!( + loaded_wallet.derivation_index(KeychainKind::Internal), + Some(0) + ); + assert_eq!( + loaded_wallet.next_derivation_index(KeychainKind::External), + 1 + ); + assert_eq!( + loaded_wallet.next_derivation_index(KeychainKind::Internal), + 1 + ); +} + +#[test] +fn test_load_from_two_path_descriptor_with_params() { + 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, + }; + let wallet = Wallet::create_from_two_path_descriptor_with_params( + two_path_descriptor(), + Network::Signet, + Arc::clone(&persister), + create_params, + ) + .unwrap(); + + wallet.reveal_next_address(KeychainKind::External); + wallet.reveal_next_address(KeychainKind::Internal); + assert!(wallet.persist(Arc::clone(&persister)).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 loaded_wallet = Wallet::load_from_two_path_descriptor_with_params( + two_path_descriptor(), + Arc::clone(&persister), + load_params, + ) + .unwrap(); + + assert_eq!(loaded_wallet.network(), Network::Signet); + assert_eq!(loaded_wallet.latest_checkpoint().hash, genesis_hash); + assert_eq!( + loaded_wallet.derivation_index(KeychainKind::External), + Some(0) + ); + assert_eq!( + loaded_wallet.derivation_index(KeychainKind::Internal), + Some(0) + ); +} diff --git a/bdk-ffi/src/tx_builder.rs b/bdk-ffi/src/tx_builder.rs index 09ffbd75b..0b28ca0d4 100644 --- a/bdk-ffi/src/tx_builder.rs +++ b/bdk-ffi/src/tx_builder.rs @@ -480,20 +480,17 @@ impl TxBuilder { let bdk_outpoint: BdkOutPoint = outpoint.into(); let bdk_input: BdkInput = psbt_input.try_into()?; - if bdk_input.witness_utxo.is_none() { - match bdk_input.non_witness_utxo.as_ref() { - Some(tx) => { - if tx.compute_txid() != bdk_outpoint.txid { - return Err(AddForeignUtxoError::InvalidTxid); - } - if tx.output.len() <= bdk_outpoint.vout as usize { - return Err(AddForeignUtxoError::InvalidOutpoint { - outpoint: bdk_outpoint.to_string(), - }); - } - } - None => return Err(AddForeignUtxoError::MissingUtxo), + if let Some(tx) = bdk_input.non_witness_utxo.as_ref() { + if tx.compute_txid() != bdk_outpoint.txid { + return Err(AddForeignUtxoError::InvalidTxid); } + if tx.output.len() <= bdk_outpoint.vout as usize { + return Err(AddForeignUtxoError::InvalidOutpoint { + outpoint: bdk_outpoint.to_string(), + }); + } + } else if bdk_input.witness_utxo.is_none() { + return Err(AddForeignUtxoError::MissingUtxo); } let bdk_weight = BdkWeight::from_wu(satisfaction_weight); @@ -519,20 +516,17 @@ impl TxBuilder { let bdk_outpoint: BdkOutPoint = outpoint.into(); let bdk_input: BdkInput = psbt_input.try_into()?; - if bdk_input.witness_utxo.is_none() { - match bdk_input.non_witness_utxo.as_ref() { - Some(tx) => { - if tx.compute_txid() != bdk_outpoint.txid { - return Err(AddForeignUtxoError::InvalidTxid); - } - if tx.output.len() <= bdk_outpoint.vout as usize { - return Err(AddForeignUtxoError::InvalidOutpoint { - outpoint: bdk_outpoint.to_string(), - }); - } - } - None => return Err(AddForeignUtxoError::MissingUtxo), + if let Some(tx) = bdk_input.non_witness_utxo.as_ref() { + if tx.compute_txid() != bdk_outpoint.txid { + return Err(AddForeignUtxoError::InvalidTxid); + } + if tx.output.len() <= bdk_outpoint.vout as usize { + return Err(AddForeignUtxoError::InvalidOutpoint { + outpoint: bdk_outpoint.to_string(), + }); } + } else if bdk_input.witness_utxo.is_none() { + return Err(AddForeignUtxoError::MissingUtxo); } let bdk_weight = BdkWeight::from_wu(satisfaction_weight); diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index becde3c1e..eae57dfa5 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -1,9 +1,12 @@ -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, LoadWithPersistError, PersistenceError, SignerError, TxidParseError, }; +use crate::signer::SignersContainer; use crate::store::{PersistenceType, Persister}; use crate::types::{ AddressInfo, Balance, BlockId, CanonicalTx, ChangeSet, EvictedTx, FullScanRequestBuilder, @@ -14,7 +17,10 @@ use crate::types::{ use bdk_wallet::bitcoin::Network; #[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}; @@ -36,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. @@ -48,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), @@ -90,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)?; @@ -121,17 +239,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), @@ -147,17 +283,89 @@ 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)?; + + Ok(Wallet { + inner_mutex: Mutex::new(wallet), + }) + } + + /// Build a two-path descriptor `Wallet` by loading from persistence. + /// + /// Checks that the provided two-path descriptor matches exactly what is loaded + /// for both the external and internal keychains. + /// + /// Note that descriptor secret keys are not persisted to the db. This method + /// extracts keys from the provided descriptor while loading. + #[uniffi::constructor(default(lookahead = 25))] + pub fn load_from_two_path_descriptor( + 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. + /// + /// Note that descriptor secret keys are not persisted to the db. This method + /// extracts keys from the provided descriptor while loading. + #[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_with_secret(); + let mut persist_lock = persister.inner.lock().unwrap(); + let deref = persist_lock.deref_mut(); + + let bdk_params = BdkWallet::load() + .two_path_descriptor(descriptor) + .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)?; @@ -175,15 +383,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)?; @@ -436,6 +658,13 @@ impl Wallet { self.get_wallet().is_mine(script.0.clone()) } + /// Get the signers. + pub fn get_signers(&self, keychain: KeychainKind) -> Arc { + let signers = self.get_wallet().get_signers(keychain).as_ref().clone(); + + Arc::new(SignersContainer::from(signers)) + } + /// Sign a transaction with all the wallet's signers, in the order specified by every signer's /// [`SignerOrdering`]. This function returns the `Result` type with an encapsulated `bool` that /// has the value true if the PSBT was finalized, or false otherwise. @@ -462,6 +691,40 @@ impl Wallet { .map_err(SignerError::from) } + /// Sign a transaction with the provided signer containers. + /// + /// Signer containers are processed in the order provided. Signers inside each container are + /// processed according to their `SignerOrdering`. + /// + /// The `SignOptions` can be used to tweak the behavior of the software signers, and the way + /// the transaction is finalized at the end. Note that it can't be guaranteed that every signer + /// will follow the options, but the "software signers" (WIF keys and `xprv`) defined in this + /// library will. + /// + /// Returns true if the PSBT was finalized, or false otherwise. + #[uniffi::method(default(sign_options = None))] + #[allow(deprecated)] + pub fn sign_with_signers( + &self, + psbt: Arc, + signers: Vec>, + sign_options: Option, + ) -> Result { + let mut psbt = psbt.0.lock().unwrap(); + let bdk_sign_options: BdkSignOptions = match sign_options { + Some(sign_options) => BdkSignOptions::from(sign_options), + None => BdkSignOptions::default(), + }; + let signers = signers + .iter() + .map(|container| &container.inner) + .collect::>(); + + self.get_wallet() + .sign_with_signers(&mut psbt, &signers, bdk_sign_options) + .map_err(SignerError::from) + } + /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer),