Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bdk-ffi/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
96 changes: 94 additions & 2 deletions bdk-ffi/src/tests/tx_builder.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
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;
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;
Expand Down Expand Up @@ -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() {
Expand Down
38 changes: 38 additions & 0 deletions bdk-ffi/src/tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
46 changes: 20 additions & 26 deletions bdk-ffi/src/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
44 changes: 41 additions & 3 deletions bdk-ffi/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Descriptor>,
Expand Down Expand Up @@ -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<Descriptor>,
persister: Arc<Persister>,
lookahead: u32,
) -> Result<Wallet, LoadWithPersistError> {
let descriptor = two_path_descriptor.to_string();
let mut persist_lock = persister.inner.lock().unwrap();
let deref = persist_lock.deref_mut();

let wallet: PersistedWallet<PersistenceType> = 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.
Expand Down
Loading