diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index 6da4c677..5cd2a13e 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 60d02c82..3f9fdc8f 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/tests/tx_builder.rs b/bdk-ffi/src/tests/tx_builder.rs index 76880f15..902fbb5d 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; @@ -301,6 +305,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 6219fa7b..c8fcd86f 100644 --- a/bdk-ffi/src/tests/wallet.rs +++ b/bdk-ffi/src/tests/wallet.rs @@ -150,3 +150,41 @@ 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 + ); +} diff --git a/bdk-ffi/src/tx_builder.rs b/bdk-ffi/src/tx_builder.rs index d242f82d..c5e5a608 100644 --- a/bdk-ffi/src/tx_builder.rs +++ b/bdk-ffi/src/tx_builder.rs @@ -496,20 +496,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); @@ -535,20 +532,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 acd73c6a..0fa524fa 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -109,13 +109,18 @@ impl Wallet { /// Build a new `Wallet` from a two-path descriptor. /// - /// This function parses a multipath descriptor with exactly 2 paths and creates a wallet using the existing receive and change wallet creation logic. + /// This function parses a multipath descriptor with exactly 2 paths and creates a wallet + /// using the existing receive and change wallet creation logic. Use this method with public + /// extended keys (`xpub` prefix) to create watch-only wallets. /// - /// Multipath descriptors follow [BIP-389](https://github.com/bitcoin/bips/blob/master/bip-0389.mediawiki) and allow defining both receive and change derivation paths in a single descriptor using the <0;1> syntax. + /// Multipath descriptors follow [BIP-389](https://github.com/bitcoin/bips/blob/master/bip-0389.mediawiki) + /// and allow defining both receive and change derivation paths in a single descriptor using + /// the `<0;1>` syntax. /// /// If you have previously created a wallet, use load instead. /// - /// Returns an error if the descriptor is invalid or not a 2-path multipath descriptor. + /// Returns an error if the descriptor is not a 2-path multipath descriptor. Private multipath + /// descriptors cannot be constructed as `Descriptor` values for this API. #[uniffi::constructor(default(lookahead = 25))] pub fn create_from_two_path_descriptor( two_path_descriptor: Arc, @@ -168,6 +173,39 @@ impl 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. + /// + /// Use this method with public extended keys (`xpub` prefix) to load watch-only wallets. + /// Private multipath descriptors cannot be constructed as `Descriptor` values for this API. + /// + /// 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 { + 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) + .extract_keys() + .load_wallet(deref) + .map_err(LoadWithPersistError::from)? + .ok_or(LoadWithPersistError::CouldNotLoad)?; + + Ok(Wallet { + inner_mutex: Mutex::new(wallet), + }) + } + /// Build a single-descriptor Wallet by loading from persistence. /// /// Note that the descriptor secret keys are not persisted to the db.