From 57538e53e4f834942ba25c72531a9bbf69982284 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Tue, 14 Feb 2023 13:54:07 +1100 Subject: [PATCH] Move tests to /tests To stop my rust-anlayzer from killing me --- src/psbt/mod.rs | 161 -- src/wallet/coin_selection.rs | 5 +- src/wallet/mod.rs | 3322 +--------------------------------- tests/common.rs | 86 + tests/psbt.rs | 158 ++ tests/wallet.rs | 3241 +++++++++++++++++++++++++++++++++ 6 files changed, 3489 insertions(+), 3484 deletions(-) create mode 100644 tests/common.rs create mode 100644 tests/psbt.rs create mode 100644 tests/wallet.rs diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 62ef0a79..87243236 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -77,164 +77,3 @@ impl PsbtUtils for Psbt { }) } } - -#[cfg(test)] -mod test { - use crate::bitcoin::TxIn; - use crate::psbt::Psbt; - use crate::wallet::test::{get_funded_wallet, get_test_wpkh}; - use crate::wallet::AddressIndex; - use crate::wallet::AddressIndex::New; - use crate::{psbt, FeeRate, SignOptions}; - use core::str::FromStr; - - // from bip 174 - const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA"; - - #[test] - #[should_panic(expected = "InputIndexOutOfRange")] - fn test_psbt_malformed_psbt_input_legacy() { - let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = wallet.get_address(AddressIndex::New); - let mut builder = wallet.build_tx(); - builder.add_recipient(send_to.script_pubkey(), 10_000); - let (mut psbt, _) = builder.finish().unwrap(); - psbt.inputs.push(psbt_bip.inputs[0].clone()); - let options = SignOptions { - trust_witness_utxo: true, - ..Default::default() - }; - let _ = wallet.sign(&mut psbt, options).unwrap(); - } - - #[test] - #[should_panic(expected = "InputIndexOutOfRange")] - fn test_psbt_malformed_psbt_input_segwit() { - let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = wallet.get_address(AddressIndex::New); - let mut builder = wallet.build_tx(); - builder.add_recipient(send_to.script_pubkey(), 10_000); - let (mut psbt, _) = builder.finish().unwrap(); - psbt.inputs.push(psbt_bip.inputs[1].clone()); - let options = SignOptions { - trust_witness_utxo: true, - ..Default::default() - }; - let _ = wallet.sign(&mut psbt, options).unwrap(); - } - - #[test] - #[should_panic(expected = "InputIndexOutOfRange")] - fn test_psbt_malformed_tx_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = wallet.get_address(AddressIndex::New); - let mut builder = wallet.build_tx(); - builder.add_recipient(send_to.script_pubkey(), 10_000); - let (mut psbt, _) = builder.finish().unwrap(); - psbt.unsigned_tx.input.push(TxIn::default()); - let options = SignOptions { - trust_witness_utxo: true, - ..Default::default() - }; - let _ = wallet.sign(&mut psbt, options).unwrap(); - } - - #[test] - fn test_psbt_sign_with_finalized() { - let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = wallet.get_address(AddressIndex::New); - let mut builder = wallet.build_tx(); - builder.add_recipient(send_to.script_pubkey(), 10_000); - let (mut psbt, _) = builder.finish().unwrap(); - - // add a finalized input - psbt.inputs.push(psbt_bip.inputs[0].clone()); - psbt.unsigned_tx - .input - .push(psbt_bip.unsigned_tx.input[0].clone()); - - let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); - } - - #[test] - fn test_psbt_fee_rate_with_witness_utxo() { - use psbt::PsbtUtils; - - let expected_fee_rate = 1.2345; - - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); - let (mut psbt, _) = builder.finish().unwrap(); - let fee_amount = psbt.fee_amount(); - assert!(fee_amount.is_some()); - - let unfinalized_fee_rate = psbt.fee_rate().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let finalized_fee_rate = psbt.fee_rate().unwrap(); - assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate); - assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb()); - } - - #[test] - fn test_psbt_fee_rate_with_nonwitness_utxo() { - use psbt::PsbtUtils; - - let expected_fee_rate = 1.2345; - - let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); - let (mut psbt, _) = builder.finish().unwrap(); - let fee_amount = psbt.fee_amount(); - assert!(fee_amount.is_some()); - let unfinalized_fee_rate = psbt.fee_rate().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let finalized_fee_rate = psbt.fee_rate().unwrap(); - assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate); - assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb()); - } - - #[test] - fn test_psbt_fee_rate_with_missing_txout() { - use psbt::PsbtUtils; - - let expected_fee_rate = 1.2345; - - let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wpkh_wallet.get_address(New); - let mut builder = wpkh_wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); - let (mut wpkh_psbt, _) = builder.finish().unwrap(); - - wpkh_psbt.inputs[0].witness_utxo = None; - wpkh_psbt.inputs[0].non_witness_utxo = None; - assert!(wpkh_psbt.fee_amount().is_none()); - assert!(wpkh_psbt.fee_rate().is_none()); - - let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = pkh_wallet.get_address(New); - let mut builder = pkh_wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); - let (mut pkh_psbt, _) = builder.finish().unwrap(); - - pkh_psbt.inputs[0].non_witness_utxo = None; - assert!(pkh_psbt.fee_amount().is_none()); - assert!(pkh_psbt.fee_rate().is_none()); - } -} diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index bfd1fe57..12c89a7b 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -112,10 +112,7 @@ use rand::thread_rng; /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// overridden -#[cfg(not(test))] pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; -#[cfg(test)] -pub type DefaultCoinSelectionAlgorithm = LargestFirstCoinSelection; // make the tests more predictable // Base weight of a Txin, not counting the weight needed for satisfying it. // prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) @@ -373,7 +370,7 @@ impl OutputGroup { /// Branch and bound coin selection /// /// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct BranchAndBoundCoinSelection { size_of_change: u64, } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 8884553e..295a8e38 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -19,11 +19,11 @@ use alloc::{ sync::Arc, vec::Vec, }; -use bdk_chain::{keychain::KeychainTracker, sparse_chain, BlockId, ConfirmationTime, chain_graph}; +use bdk_chain::{chain_graph, keychain::KeychainTracker, sparse_chain, BlockId, ConfirmationTime}; use bitcoin::secp256k1::Secp256k1; +use core::convert::TryInto; use core::fmt; use core::ops::Deref; -use core::convert::TryInto; use bitcoin::consensus::encode::serialize; use bitcoin::util::psbt; @@ -422,7 +422,7 @@ impl Wallet { /// Iterate over the transactions in the wallet in order of ascending confirmation time with /// unconfirmed transactions last. - fn transactions( + pub fn transactions( &self, ) -> impl DoubleEndedIterator + '_ { self.keychain_tracker @@ -1640,3319 +1640,3 @@ where Ok(wallet_name) } - -#[cfg(test)] -pub(crate) mod test { - use crate::types::KeychainKind; - use assert_matches::assert_matches; - use bdk_chain::TxHeight; - use bitcoin::hashes::Hash; - use bitcoin::{util::psbt, Network}; - use bitcoin::{PackedLockTime, TxIn}; - use core::str::FromStr; - - use super::*; - use crate::signer::SignOptions; - use crate::wallet::AddressIndex::*; - - /// Return a fake wallet that appears to be funded for testing. - pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { - let mut wallet = Wallet::new(descriptor, None, Network::Regtest).unwrap(); - let address = wallet.get_address(AddressIndex::New).address; - - let tx = Transaction { - version: 1, - lock_time: bitcoin::PackedLockTime(0), - input: vec![], - output: vec![TxOut { - value: 50_000, - script_pubkey: address.script_pubkey(), - }], - }; - - wallet - .insert_checkpoint(BlockId { - height: 1_000, - hash: BlockHash::all_zeros(), - }) - .unwrap(); - wallet - .insert_tx( - tx.clone(), - ConfirmationTime::Confirmed { - height: 1_000, - time: 100, - }, - ) - .unwrap(); - - (wallet, tx.txid()) - } - - fn receive_output(wallet: &mut Wallet, value: u64, height: TxHeight) -> OutPoint { - let tx = Transaction { - version: 1, - lock_time: PackedLockTime(0), - input: vec![], - output: vec![TxOut { - script_pubkey: wallet.get_address(LastUnused).script_pubkey(), - value, - }], - }; - - wallet - .insert_tx( - tx.clone(), - match height { - TxHeight::Confirmed(height) => ConfirmationTime::Confirmed { - height, - time: 42_000, - }, - TxHeight::Unconfirmed => ConfirmationTime::Unconfirmed, - }, - ) - .unwrap(); - - OutPoint { - txid: tx.txid(), - vout: 0, - } - } - - fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { - let height = wallet.latest_checkpoint().map(|id| id.height).into(); - receive_output(wallet, value, height) - } - - // The satisfaction size of a P2WPKH is 112 WU = - // 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len) - // On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for - // a total of 105 WU. - // Here, we push just once for simplicity, so we have to add an extra byte for the missing - // OP_PUSH. - const P2WPKH_FAKE_WITNESS_SIZE: usize = 106; - - #[test] - fn test_descriptor_checksum() { - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - let checksum = wallet.descriptor_checksum(KeychainKind::External); - assert_eq!(checksum.len(), 8); - - let raw_descriptor = wallet - .keychanins() - .iter() - .next() - .unwrap() - .1 - .to_string() - .split_once('#') - .unwrap() - .0 - .to_string(); - assert_eq!(calc_checksum(&raw_descriptor).unwrap(), checksum); - } - - #[test] - fn test_get_funded_wallet_balance() { - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - assert_eq!(wallet.get_balance().confirmed, 50000); - } - - pub(crate) fn get_test_wpkh() -> &'static str { - "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" - } - - pub(crate) fn get_test_single_sig_csv() -> &'static str { - // and(pk(Alice),older(6)) - "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))" - } - - pub(crate) fn get_test_a_or_b_plus_csv() -> &'static str { - // or(pk(Alice),and(pk(Bob),older(144))) - "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))" - } - - pub(crate) fn get_test_single_sig_cltv() -> &'static str { - // and(pk(Alice),after(100000)) - "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))" - } - - pub(crate) fn get_test_tr_single_sig() -> &'static str { - "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)" - } - - pub(crate) fn get_test_tr_with_taptree() -> &'static str { - "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" - } - - pub(crate) fn get_test_tr_with_taptree_both_priv() -> &'static str { - "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})" - } - - pub(crate) fn get_test_tr_repeated_key() -> &'static str { - "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})" - } - - pub(crate) fn get_test_tr_single_sig_xprv() -> &'static str { - "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)" - } - - pub(crate) fn get_test_tr_with_taptree_xprv() -> &'static str { - "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" - } - - pub(crate) fn get_test_tr_dup_keys() -> &'static str { - "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" - } - - macro_rules! assert_fee_rate { - ($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({ - let psbt = $psbt.clone(); - #[allow(unused_mut)] - let mut tx = $psbt.clone().extract_tx(); - $( - $( $add_signature )* - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - )* - - #[allow(unused_mut)] - #[allow(unused_assignments)] - let mut dust_change = false; - $( - $( $dust_change )* - dust_change = true; - )* - - let fee_amount = psbt - .inputs - .iter() - .fold(0, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) - - psbt - .unsigned_tx - .output - .iter() - .fold(0, |acc, o| acc + o.value); - - assert_eq!(fee_amount, $fees); - - let tx_fee_rate = FeeRate::from_wu($fees, tx.weight()); - let fee_rate = $fee_rate; - - if !dust_change { - assert!(tx_fee_rate >= fee_rate && (tx_fee_rate - fee_rate).as_sat_per_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate); - } else { - assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate); - } - }); - } - - macro_rules! from_str { - ($e:expr, $t:ty) => {{ - use core::str::FromStr; - <$t>::from_str($e).unwrap() - }}; - - ($e:expr) => { - from_str!($e, _) - }; - } - - #[test] - #[should_panic(expected = "NoRecipients")] - fn test_create_tx_empty_recipients() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - wallet.build_tx().finish().unwrap(); - } - - #[test] - #[should_panic(expected = "NoUtxosSelected")] - fn test_create_tx_manually_selected_empty_utxos() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .manually_selected_only(); - builder.finish().unwrap(); - } - - #[test] - #[should_panic(expected = "Invalid version `0`")] - fn test_create_tx_version_0() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .version(0); - builder.finish().unwrap(); - } - - #[test] - #[should_panic( - expected = "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" - )] - fn test_create_tx_version_1_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .version(1); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_custom_version() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .version(42); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.version, 42); - } - - #[test] - fn test_create_tx_default_locktime_is_last_sync_height() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); - - // Since we never synced the wallet we don't have a last_sync_height - // we could use to try to prevent fee sniping. We default to 0. - assert_eq!(psbt.unsigned_tx.lock_time.0, 1_000); - } - - #[test] - fn test_create_tx_fee_sniping_locktime_last_sync() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - - let (psbt, _) = builder.finish().unwrap(); - - // If there's no current_height we're left with using the last sync height - assert_eq!( - psbt.unsigned_tx.lock_time.0, - wallet.latest_checkpoint().unwrap().height - ); - } - - #[test] - fn test_create_tx_default_locktime_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.lock_time.0, 100_000); - } - - #[test] - fn test_create_tx_custom_locktime() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .current_height(630_001) - .nlocktime(LockTime::from_height(630_000).unwrap()); - let (psbt, _) = builder.finish().unwrap(); - - // When we explicitly specify a nlocktime - // we don't try any fee sniping prevention trick - // (we ignore the current_height) - assert_eq!(psbt.unsigned_tx.lock_time.0, 630_000); - } - - #[test] - fn test_create_tx_custom_locktime_compatible_with_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .nlocktime(LockTime::from_height(630_000).unwrap()); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.lock_time.0, 630_000); - } - - #[test] - #[should_panic( - expected = "TxBuilder requested timelock of `Blocks(Height(50000))`, but at least `Blocks(Height(100000))` is required to spend from this script" - )] - fn test_create_tx_custom_locktime_incompatible_with_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .nlocktime(LockTime::from_height(50000).unwrap()); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_no_rbf_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); - } - - #[test] - fn test_create_tx_with_default_rbf_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); - // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). - // It will be set to the OP_CSV value, in this case 6 - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); - } - - #[test] - #[should_panic( - expected = "Cannot enable RBF with nSequence `Sequence(3)` given a required OP_CSV of `Sequence(6)`" - )] - fn test_create_tx_with_custom_rbf_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .enable_rbf_with_sequence(Sequence(3)); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_no_rbf_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); - } - - #[test] - #[should_panic(expected = "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")] - fn test_create_tx_invalid_rbf_sequence() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_custom_rbf_sequence() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .enable_rbf_with_sequence(Sequence(0xDEADBEEF)); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF)); - } - - #[test] - fn test_create_tx_default_sequence() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); - } - - #[test] - #[should_panic( - expected = "The `change_policy` can be set only if the wallet has a change_descriptor" - )] - fn test_create_tx_change_policy_no_internal() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .do_not_spend_change(); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_drain_wallet_and_drain_to() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value, - 50_000 - details.fee.unwrap_or(0) - ); - } - - #[test] - fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); - let drain_addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 20_000) - .drain_to(drain_addr.script_pubkey()) - .drain_wallet(); - let (psbt, details) = builder.finish().unwrap(); - let outputs = psbt.unsigned_tx.output; - - assert_eq!(outputs.len(), 2); - let main_output = outputs - .iter() - .find(|x| x.script_pubkey == addr.script_pubkey()) - .unwrap(); - let drain_output = outputs - .iter() - .find(|x| x.script_pubkey == drain_addr.script_pubkey()) - .unwrap(); - assert_eq!(main_output.value, 20_000,); - assert_eq!(drain_output.value, 30_000 - details.fee.unwrap_or(0)); - } - - #[test] - fn test_create_tx_drain_to_and_utxos() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let utxos: Vec<_> = wallet - .get_available_utxos() - .into_iter() - .map(|(u, _)| u.outpoint) - .collect(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxos(&utxos) - .unwrap(); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value, - 50_000 - details.fee.unwrap_or(0) - ); - } - - #[test] - #[should_panic(expected = "NoRecipients")] - fn test_create_tx_drain_to_no_drain_wallet_no_utxos() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let drain_addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(drain_addr.script_pubkey()); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_default_fee_rate() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, details) = builder.finish().unwrap(); - - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::default(), @add_signature); - } - - #[test] - fn test_create_tx_custom_fee_rate() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .fee_rate(FeeRate::from_sat_per_vb(5.0)); - let (psbt, details) = builder.finish().unwrap(); - - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); - } - - #[test] - fn test_create_tx_absolute_fee() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(100); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.fee.unwrap_or(0), 100); - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value, - 50_000 - details.fee.unwrap_or(0) - ); - } - - #[test] - fn test_create_tx_absolute_zero_fee() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(0); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.fee.unwrap_or(0), 0); - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value, - 50_000 - details.fee.unwrap_or(0) - ); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_create_tx_absolute_high_fee() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(60_000); - let (_psbt, _details) = builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_add_change() { - use super::tx_builder::TxOrdering; - - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .ordering(TxOrdering::Untouched); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.output.len(), 2); - assert_eq!(psbt.unsigned_tx.output[0].value, 25_000); - assert_eq!( - psbt.unsigned_tx.output[1].value, - 25_000 - details.fee.unwrap_or(0) - ); - } - - #[test] - fn test_create_tx_skip_change_dust() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 49_800); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!(psbt.unsigned_tx.output[0].value, 49_800); - assert_eq!(details.fee.unwrap_or(0), 200); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_create_tx_drain_to_dust_amount() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - // very high fee rate, so that the only output would be below dust - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_rate(FeeRate::from_sat_per_vb(453.0)); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_ordering_respected() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 30_000) - .add_recipient(addr.script_pubkey(), 10_000) - .ordering(super::tx_builder::TxOrdering::Bip69Lexicographic); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.output.len(), 3); - assert_eq!( - psbt.unsigned_tx.output[0].value, - 10_000 - details.fee.unwrap_or(0) - ); - assert_eq!(psbt.unsigned_tx.output[1].value, 10_000); - assert_eq!(psbt.unsigned_tx.output[2].value, 30_000); - } - - #[test] - fn test_create_tx_default_sighash() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 30_000); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.inputs[0].sighash_type, None); - } - - #[test] - fn test_create_tx_custom_sighash() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 30_000) - .sighash(bitcoin::EcdsaSighashType::Single.into()); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!( - psbt.inputs[0].sighash_type, - Some(bitcoin::EcdsaSighashType::Single.into()) - ); - } - - #[test] - fn test_create_tx_input_hd_keypaths() { - use bitcoin::util::bip32::{DerivationPath, Fingerprint}; - use core::str::FromStr; - - let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1); - assert_eq!( - psbt.inputs[0].bip32_derivation.values().next().unwrap(), - &( - Fingerprint::from_str("d34db33f").unwrap(), - DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap() - ) - ); - } - - #[test] - fn test_create_tx_output_hd_keypaths() { - use bitcoin::util::bip32::{DerivationPath, Fingerprint}; - use core::str::FromStr; - - let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); - - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1); - let expected_derivation_path = format!("m/44'/0'/0'/0/{}", addr.index); - assert_eq!( - psbt.outputs[0].bip32_derivation.values().next().unwrap(), - &( - Fingerprint::from_str("d34db33f").unwrap(), - DerivationPath::from_str(&expected_derivation_path).unwrap() - ) - ); - } - - #[test] - fn test_create_tx_set_redeem_script_p2sh() { - use bitcoin::hashes::hex::FromHex; - - let (mut wallet, _) = - get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!( - psbt.inputs[0].redeem_script, - Some(Script::from( - Vec::::from_hex( - "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" - ) - .unwrap() - )) - ); - assert_eq!(psbt.inputs[0].witness_script, None); - } - - #[test] - fn test_create_tx_set_witness_script_p2wsh() { - use bitcoin::hashes::hex::FromHex; - - let (mut wallet, _) = - get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.inputs[0].redeem_script, None); - assert_eq!( - psbt.inputs[0].witness_script, - Some(Script::from( - Vec::::from_hex( - "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" - ) - .unwrap() - )) - ); - } - - #[test] - fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() { - use bitcoin::hashes::hex::FromHex; - - let (mut wallet, _) = - get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - let script = Script::from( - Vec::::from_hex( - "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac", - ) - .unwrap(), - ); - - assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_v0_p2wsh())); - assert_eq!(psbt.inputs[0].witness_script, Some(script)); - } - - #[test] - fn test_create_tx_non_witness_utxo() { - let (mut wallet, _) = - get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert!(psbt.inputs[0].non_witness_utxo.is_some()); - assert!(psbt.inputs[0].witness_utxo.is_none()); - } - - #[test] - fn test_create_tx_only_witness_utxo() { - let (mut wallet, _) = - get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .only_witness_utxo() - .drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert!(psbt.inputs[0].non_witness_utxo.is_none()); - assert!(psbt.inputs[0].witness_utxo.is_some()); - } - - #[test] - fn test_create_tx_shwpkh_has_witness_utxo() { - let (mut wallet, _) = - get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert!(psbt.inputs[0].witness_utxo.is_some()); - } - - #[test] - fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() { - let (mut wallet, _) = - get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert!(psbt.inputs[0].non_witness_utxo.is_some()); - assert!(psbt.inputs[0].witness_utxo.is_some()); - } - - #[test] - fn test_create_tx_add_utxo() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let small_output_tx = Transaction { - input: vec![], - output: vec![TxOut { - value: 25_000, - script_pubkey: wallet.get_address(New).address.script_pubkey(), - }], - version: 0, - lock_time: PackedLockTime(0), - }; - wallet - .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed) - .unwrap(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 30_000) - .add_utxo(OutPoint { - txid: small_output_tx.txid(), - vout: 0, - }) - .unwrap(); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!( - psbt.unsigned_tx.input.len(), - 2, - "should add an additional input since 25_000 < 30_000" - ); - assert_eq!(details.sent, 75_000, "total should be sum of both inputs"); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_create_tx_manually_selected_insufficient() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let small_output_tx = Transaction { - input: vec![], - output: vec![TxOut { - value: 25_000, - script_pubkey: wallet.get_address(New).address.script_pubkey(), - }], - version: 0, - lock_time: PackedLockTime(0), - }; - - wallet - .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed) - .unwrap(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 30_000) - .add_utxo(OutPoint { - txid: small_output_tx.txid(), - vout: 0, - }) - .unwrap() - .manually_selected_only(); - builder.finish().unwrap(); - } - - #[test] - #[should_panic(expected = "SpendingPolicyRequired(External)")] - fn test_create_tx_policy_path_required() { - let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 30_000); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_policy_path_no_csv() { - let descriptors = get_test_wpkh(); - let mut wallet = Wallet::new(descriptors, None, Network::Regtest).unwrap(); - - let tx = Transaction { - version: 0, - lock_time: PackedLockTime(0), - input: vec![], - output: vec![TxOut { - value: 50_000, - script_pubkey: wallet.get_address(New).script_pubkey(), - }], - }; - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); - let root_id = external_policy.id; - // child #0 is just the key "A" - let path = vec![(root_id, vec![0])].into_iter().collect(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 30_000) - .policy_path(path, KeychainKind::External); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF)); - } - - #[test] - fn test_create_tx_policy_path_use_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); - - let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); - let root_id = external_policy.id; - // child #1 is or(pk(B),older(144)) - let path = vec![(root_id, vec![1])].into_iter().collect(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 30_000) - .policy_path(path, KeychainKind::External); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144)); - } - - #[test] - fn test_create_tx_global_xpubs_with_origin() { - use bitcoin::hashes::hex::FromHex; - use bitcoin::util::bip32; - - let (mut wallet, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .add_global_xpubs(); - let (psbt, _) = builder.finish().unwrap(); - - let key = bip32::ExtendedPubKey::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap(); - let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap(); - let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap(); - - assert_eq!(psbt.xpub.len(), 1); - assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path))); - } - - #[test] - fn test_add_foreign_utxo() { - let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); - let (wallet2, _) = - get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo = wallet2.list_unspent().remove(0); - let foreign_utxo_satisfaction = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_satisfaction_weight() - .unwrap(); - - let psbt_input = psbt::Input { - witness_utxo: Some(utxo.txout.clone()), - ..Default::default() - }; - - let mut builder = wallet1.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 60_000) - .only_witness_utxo() - .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) - .unwrap(); - let (mut psbt, details) = builder.finish().unwrap(); - - assert_eq!( - details.sent - details.received, - 10_000 + details.fee.unwrap_or(0), - "we should have only net spent ~10_000" - ); - - assert!( - psbt.unsigned_tx - .input - .iter() - .any(|input| input.previous_output == utxo.outpoint), - "foreign_utxo should be in there" - ); - - let finished = wallet1 - .sign( - &mut psbt, - SignOptions { - trust_witness_utxo: true, - ..Default::default() - }, - ) - .unwrap(); - - assert!( - !finished, - "only one of the inputs should have been signed so far" - ); - - let finished = wallet2 - .sign( - &mut psbt, - SignOptions { - trust_witness_utxo: true, - ..Default::default() - }, - ) - .unwrap(); - assert!(finished, "all the inputs should have been signed now"); - } - - #[test] - #[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")] - fn test_add_foreign_utxo_invalid_psbt_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let outpoint = wallet.list_unspent()[0].outpoint; - let foreign_utxo_satisfaction = wallet - .get_descriptor_for_keychain(KeychainKind::External) - .max_satisfaction_weight() - .unwrap(); - - let mut builder = wallet.build_tx(); - builder - .add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction) - .unwrap(); - } - - #[test] - fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { - let (mut wallet1, txid1) = get_funded_wallet(get_test_wpkh()); - let (wallet2, txid2) = - get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - - let utxo2 = wallet2.list_unspent().remove(0); - let tx1 = wallet1.get_tx(txid1, true).unwrap().transaction.unwrap(); - let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); - - let satisfaction_weight = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_satisfaction_weight() - .unwrap(); - - let mut builder = wallet1.build_tx(); - assert!( - builder - .add_foreign_utxo( - utxo2.outpoint, - psbt::Input { - non_witness_utxo: Some(tx1), - ..Default::default() - }, - satisfaction_weight - ) - .is_err(), - "should fail when outpoint doesn't match psbt_input" - ); - assert!( - builder - .add_foreign_utxo( - utxo2.outpoint, - psbt::Input { - non_witness_utxo: Some(tx2), - ..Default::default() - }, - satisfaction_weight - ) - .is_ok(), - "shoulld be ok when outpoint does match psbt_input" - ); - } - - #[test] - fn test_add_foreign_utxo_only_witness_utxo() { - let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); - let (wallet2, txid2) = - get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo2 = wallet2.list_unspent().remove(0); - - let satisfaction_weight = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_satisfaction_weight() - .unwrap(); - - let mut builder = wallet1.build_tx(); - builder.add_recipient(addr.script_pubkey(), 60_000); - - { - let mut builder = builder.clone(); - let psbt_input = psbt::Input { - witness_utxo: Some(utxo2.txout.clone()), - ..Default::default() - }; - builder - .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) - .unwrap(); - assert!( - builder.finish().is_err(), - "psbt_input with witness_utxo should fail with only witness_utxo" - ); - } - - { - let mut builder = builder.clone(); - let psbt_input = psbt::Input { - witness_utxo: Some(utxo2.txout.clone()), - ..Default::default() - }; - builder - .only_witness_utxo() - .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) - .unwrap(); - assert!( - builder.finish().is_ok(), - "psbt_input with just witness_utxo should succeed when `only_witness_utxo` is enabled" - ); - } - - { - let mut builder = builder.clone(); - let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); - let psbt_input = psbt::Input { - non_witness_utxo: Some(tx2), - ..Default::default() - }; - builder - .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) - .unwrap(); - assert!( - builder.finish().is_ok(), - "psbt_input with non_witness_utxo should succeed by default" - ); - } - } - - #[test] - fn test_get_psbt_input() { - // this should grab a known good utxo and set the input - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - for utxo in wallet.list_unspent() { - let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap(); - assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some()); - } - } - - #[test] - #[should_panic( - expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")" - )] - fn test_create_tx_global_xpubs_origin_missing() { - let (mut wallet, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .add_global_xpubs(); - builder.finish().unwrap(); - } - - #[test] - fn test_create_tx_global_xpubs_master_without_origin() { - use bitcoin::hashes::hex::FromHex; - use bitcoin::util::bip32; - - let (mut wallet, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .add_global_xpubs(); - let (psbt, _) = builder.finish().unwrap(); - - let key = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap(); - let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap(); - - assert_eq!(psbt.xpub.len(), 1); - assert_eq!( - psbt.xpub.get(&key), - Some(&(fingerprint, bip32::DerivationPath::default())) - ); - } - - #[test] - #[should_panic(expected = "IrreplaceableTransaction")] - fn test_bump_fee_irreplaceable_tx() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); - - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - wallet.build_fee_bump(txid).unwrap().finish().unwrap(); - } - - #[test] - #[should_panic(expected = "TransactionConfirmed")] - fn test_bump_fee_confirmed_tx() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); - - let tx = psbt.extract_tx(); - let txid = tx.txid(); - - wallet - .insert_tx( - tx, - ConfirmationTime::Confirmed { - height: 42, - time: 42_000, - }, - ) - .unwrap(); - - wallet.build_fee_bump(txid).unwrap().finish().unwrap(); - } - - #[test] - #[should_panic(expected = "FeeRateTooLow")] - fn test_bump_fee_low_fee_rate() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); - - let tx = psbt.extract_tx(); - let txid = tx.txid(); - - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(1.0)); - builder.finish().unwrap(); - } - - #[test] - #[should_panic(expected = "FeeTooLow")] - fn test_bump_fee_low_abs() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); - - let tx = psbt.extract_tx(); - let txid = tx.txid(); - - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(10); - builder.finish().unwrap(); - } - - #[test] - #[should_panic(expected = "FeeTooLow")] - fn test_bump_fee_zero_abs() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); - - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(0); - builder.finish().unwrap(); - } - - #[test] - fn test_bump_fee_reduce_change() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf(); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, original_details.sent); - assert_eq!( - details.received + details.fee.unwrap_or(0), - original_details.received + original_details.fee.unwrap_or(0) - ); - assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - 25_000 - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - details.received - ); - - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(200); - builder.enable_rbf(); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, original_details.sent); - assert_eq!( - details.received + details.fee.unwrap_or(0), - original_details.received + original_details.fee.unwrap_or(0) - ); - assert!( - details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0), - "{} > {}", - details.fee.unwrap_or(0), - original_details.fee.unwrap_or(0) - ); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - 25_000 - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - details.received - ); - - assert_eq!(details.fee.unwrap_or(0), 200); - } - - #[test] - fn test_bump_fee_reduce_single_recipient() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .fee_rate(FeeRate::from_sat_per_vb(2.5)) - .allow_shrinking(addr.script_pubkey()) - .unwrap(); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, original_details.sent); - assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 1); - assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent); - - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); - } - - #[test] - fn test_bump_fee_absolute_reduce_single_recipient() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .allow_shrinking(addr.script_pubkey()) - .unwrap() - .fee_absolute(300); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, original_details.sent); - assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 1); - assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent); - - assert_eq!(details.fee.unwrap_or(0), 300); - } - - #[test] - fn test_bump_fee_drain_wallet() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - // receive an extra tx so that our wallet has two utxos. - let tx = Transaction { - version: 1, - lock_time: PackedLockTime(0), - input: vec![], - output: vec![TxOut { - value: 25_000, - script_pubkey: wallet.get_address(New).script_pubkey(), - }], - }; - wallet - .insert_tx( - tx.clone(), - ConfirmationTime::Confirmed { - height: wallet.latest_checkpoint().unwrap().height, - time: 42_000, - }, - ) - .unwrap(); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(OutPoint { - txid: tx.txid(), - vout: 0, - }) - .unwrap() - .manually_selected_only() - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - assert_eq!(original_details.sent, 25_000); - - // for the new feerate, it should be enough to reduce the output, but since we specify - // `drain_wallet` we expect to spend everything - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .drain_wallet() - .allow_shrinking(addr.script_pubkey()) - .unwrap() - .fee_rate(FeeRate::from_sat_per_vb(5.0)); - let (_, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, 75_000); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_bump_fee_remove_output_manually_selected_only() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - // receive an extra tx so that our wallet has two utxos. then we manually pick only one of - // them, and make sure that `bump_fee` doesn't try to add more. This fails because we've - // told the wallet it's not allowed to add more inputs AND it can't reduce the value of the - // existing output. In other words, bump_fee + manually_selected_only is always an error - // unless you've also set "allow_shrinking" OR there is a change output. - let init_tx = Transaction { - version: 1, - lock_time: PackedLockTime(0), - input: vec![], - output: vec![TxOut { - script_pubkey: wallet.get_address(New).script_pubkey(), - value: 25_000, - }], - }; - wallet - .insert_tx(init_tx.clone(), wallet.transactions().last().unwrap().0) - .unwrap(); - let outpoint = OutPoint { - txid: init_tx.txid(), - vout: 0, - }; - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(outpoint) - .unwrap() - .manually_selected_only() - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - assert_eq!(original_details.sent, 25_000); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .manually_selected_only() - .fee_rate(FeeRate::from_sat_per_vb(255.0)); - builder.finish().unwrap(); - } - - #[test] - fn test_bump_fee_add_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let init_tx = Transaction { - version: 1, - lock_time: PackedLockTime(0), - input: vec![], - output: vec![TxOut { - script_pubkey: wallet.get_address(New).script_pubkey(), - value: 25_000, - }], - }; - wallet - .insert_tx(init_tx.clone(), wallet.transactions().last().unwrap().0) - .unwrap(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 45_000) - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, original_details.sent + 25_000); - assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - 45_000 - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - details.received - ); - - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); - } - - #[test] - fn test_bump_fee_absolute_add_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - receive_output_in_latest_block(&mut wallet, 25_000); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 45_000) - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(6_000); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, original_details.sent + 25_000); - assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - 45_000 - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - details.received - ); - - assert_eq!(details.fee.unwrap_or(0), 6_000); - } - - #[test] - fn test_bump_fee_no_change_add_input_and_change() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let op = receive_output_in_latest_block(&mut wallet, 25_000); - - // initially make a tx without change by using `drain_to` - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(op) - .unwrap() - .manually_selected_only() - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - - let tx = psbt.extract_tx(); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - // now bump the fees without using `allow_shrinking`. the wallet should add an - // extra input and a change output, and leave the original output untouched - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); - let (psbt, details) = builder.finish().unwrap(); - - let original_send_all_amount = original_details.sent - original_details.fee.unwrap_or(0); - assert_eq!(details.sent, original_details.sent + 50_000); - assert_eq!( - details.received, - 75_000 - original_send_all_amount - details.fee.unwrap_or(0) - ); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - original_send_all_amount - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - 75_000 - original_send_all_amount - details.fee.unwrap_or(0) - ); - - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); - } - - #[test] - fn test_bump_fee_add_input_change_dust() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - receive_output_in_latest_block(&mut wallet, 25_000); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 45_000) - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let mut tx = psbt.extract_tx(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realisitc weight - } - let original_tx_weight = tx.weight(); - assert_eq!(tx.input.len(), 1); - assert_eq!(tx.output.len(), 2); - let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - // We set a fee high enough that during rbf we are forced to add - // a new input and also that we have to remove the change - // that we had previously - - // We calculate the new weight as: - // original weight - // + extra input weight: 160 WU = (32 (prevout) + 4 (vout) + 4 (nsequence)) * 4 - // + input satisfaction weight: 112 WU = 106 (witness) + 2 (witness len) + (1 (script len)) * 4 - // - change output weight: 124 WU = (8 (value) + 1 (script len) + 22 (script)) * 4 - let new_tx_weight = original_tx_weight + 160 + 112 - 124; - // two inputs (50k, 25k) and one output (45k) - epsilon - // We use epsilon here to avoid asking for a slightly too high feerate - let fee_abs = 50_000 + 25_000 - 45_000 - 10; - builder.fee_rate(FeeRate::from_wu(fee_abs, new_tx_weight)); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!( - original_details.received, - 5_000 - original_details.fee.unwrap_or(0) - ); - - assert_eq!(details.sent, original_details.sent + 25_000); - assert_eq!(details.fee.unwrap_or(0), 30_000); - assert_eq!(details.received, 0); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 1); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - 45_000 - ); - - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature); - } - - #[test] - fn test_bump_fee_force_add_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 45_000) - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let mut tx = psbt.extract_tx(); - let txid = tx.txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - wallet - .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed) - .unwrap(); - // the new fee_rate is low enough that just reducing the change would be fine, but we force - // the addition of an extra input with `add_utxo()` - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .add_utxo(incoming_op) - .unwrap() - .fee_rate(FeeRate::from_sat_per_vb(5.0)); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, original_details.sent + 25_000); - assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - 45_000 - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - details.received - ); - - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); - } - - #[test] - fn test_bump_fee_absolute_force_add_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 45_000) - .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let mut tx = psbt.extract_tx(); - let txid = tx.txid(); - // skip saving the new utxos, we know they can't be used anyways - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - wallet - .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed) - .unwrap(); - - // the new fee_rate is low enough that just reducing the change would be fine, but we force - // the addition of an extra input with `add_utxo()` - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.add_utxo(incoming_op).unwrap().fee_absolute(250); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!(details.sent, original_details.sent + 25_000); - assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - 45_000 - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - details.received - ); - - assert_eq!(details.fee.unwrap_or(0), 250); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_bump_fee_unconfirmed_inputs_only() { - // We try to bump the fee, but: - // - We can't reduce the change, as we have no change - // - All our UTXOs are unconfirmed - // So, we fail with "InsufficientFunds", as per RBF rule 2: - // The replacement transaction may only include an unconfirmed input - // if that input was included in one of the original transactions. - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_wallet() - .drain_to(addr.script_pubkey()) - .enable_rbf(); - let (psbt, __details) = builder.finish().unwrap(); - // Now we receive one transaction with 0 confirmations. We won't be able to use that for - // fee bumping, as it's still unconfirmed! - receive_output(&mut wallet, 25_000, TxHeight::Unconfirmed); - let mut tx = psbt.extract_tx(); - let txid = tx.txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(25.0)); - builder.finish().unwrap(); - } - - #[test] - fn test_bump_fee_unconfirmed_input() { - // We create a tx draining the wallet and spending one confirmed - // and one unconfirmed UTXO. We check that we can fee bump normally - // (BIP125 rule 2 only apply to newly added unconfirmed input, you can - // always fee bump with an unconfirmed input if it was included in the - // original transaction) - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - // We receive a tx with 0 confirmations, which will be used as an input - // in the drain tx. - receive_output(&mut wallet, 25_000, TxHeight::Unconfirmed); - let mut builder = wallet.build_tx(); - builder - .drain_wallet() - .drain_to(addr.script_pubkey()) - .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); - let mut tx = psbt.extract_tx(); - let txid = tx.txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .fee_rate(FeeRate::from_sat_per_vb(15.0)) - .allow_shrinking(addr.script_pubkey()) - .unwrap(); - builder.finish().unwrap(); - } - - #[test] - fn test_fee_amount_negative_drain_val() { - // While building the transaction, bdk would calculate the drain_value - // as - // current_delta - fee_amount - drain_fee - // using saturating_sub, meaning that if the result would end up negative, - // it'll remain to zero instead. - // This caused a bug in master where we would calculate the wrong fee - // for a transaction. - // See https://github.com/bitcoindevkit/bdk/issues/660 - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap(); - let fee_rate = FeeRate::from_sat_per_vb(2.01); - let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); - - let mut builder = wallet.build_tx(); - builder - .add_recipient(send_to.script_pubkey(), 8630) - .add_utxo(incoming_op) - .unwrap() - .enable_rbf() - .fee_rate(fee_rate); - let (psbt, details) = builder.finish().unwrap(); - - assert!(psbt.inputs.len() == 1); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate, @add_signature); - } - - #[test] - fn test_sign_single_xprv() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx(); - assert_eq!(extracted.input[0].witness.len(), 2); - } - - #[test] - fn test_sign_single_xprv_with_master_fingerprint_and_path() { - let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx(); - assert_eq!(extracted.input[0].witness.len(), 2); - } - - #[test] - fn test_sign_single_xprv_bip44_path() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx(); - assert_eq!(extracted.input[0].witness.len(), 2); - } - - #[test] - fn test_sign_single_xprv_sh_wpkh() { - let (mut wallet, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx(); - assert_eq!(extracted.input[0].witness.len(), 2); - } - - #[test] - fn test_sign_single_wif() { - let (mut wallet, _) = - get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx(); - assert_eq!(extracted.input[0].witness.len(), 2); - } - - #[test] - fn test_sign_single_xprv_no_hd_keypaths() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - psbt.inputs[0].bip32_derivation.clear(); - assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx(); - assert_eq!(extracted.input[0].witness.len(), 2); - } - - #[test] - fn test_include_output_redeem_witness_script() { - let (mut wallet, _) = get_funded_wallet("sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 45_000) - .include_output_redeem_witness_script(); - let (psbt, _) = builder.finish().unwrap(); - - // p2sh-p2wsh transaction should contain both witness and redeem scripts - assert!(psbt - .outputs - .iter() - .any(|output| output.redeem_script.is_some() && output.witness_script.is_some())); - } - - #[test] - fn test_signing_only_one_of_multiple_inputs() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 45_000) - .include_output_redeem_witness_script(); - let (mut psbt, _) = builder.finish().unwrap(); - - // add another input to the psbt that is at least passable. - let dud_input = bitcoin::util::psbt::Input { - witness_utxo: Some(TxOut { - value: 100_000, - script_pubkey: miniscript::Descriptor::::from_str( - "wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)", - ) - .unwrap() - .script_pubkey(), - }), - ..Default::default() - }; - - psbt.inputs.push(dud_input); - psbt.unsigned_tx.input.push(bitcoin::TxIn::default()); - let is_final = wallet - .sign( - &mut psbt, - SignOptions { - trust_witness_utxo: true, - ..Default::default() - }, - ) - .unwrap(); - assert!( - !is_final, - "shouldn't be final since we can't sign one of the inputs" - ); - assert!( - psbt.inputs[0].final_script_witness.is_some(), - "should finalized input it signed" - ) - } - - #[test] - fn test_remove_partial_sigs_after_finalize_sign_option() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - - for remove_partial_sigs in &[true, false] { - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap().0; - - assert!(wallet - .sign( - &mut psbt, - SignOptions { - remove_partial_sigs: *remove_partial_sigs, - ..Default::default() - }, - ) - .unwrap()); - - psbt.inputs.iter().for_each(|input| { - if *remove_partial_sigs { - assert!(input.partial_sigs.is_empty()) - } else { - assert!(!input.partial_sigs.is_empty()) - } - }); - } - } - - #[test] - fn test_try_finalize_sign_option() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - - for try_finalize in &[true, false] { - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap().0; - - let finalized = wallet - .sign( - &mut psbt, - SignOptions { - try_finalize: *try_finalize, - ..Default::default() - }, - ) - .unwrap(); - - psbt.inputs.iter().for_each(|input| { - if *try_finalize { - assert!(finalized); - assert!(input.final_script_sig.is_some()); - assert!(input.final_script_witness.is_some()); - } else { - assert!(!finalized); - assert!(input.final_script_sig.is_none()); - assert!(input.final_script_witness.is_none()); - } - }); - } - } - - #[test] - fn test_sign_nonstandard_sighash() { - let sighash = EcdsaSighashType::NonePlusAnyoneCanPay; - - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .sighash(sighash.into()) - .drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let result = wallet.sign(&mut psbt, Default::default()); - assert!( - result.is_err(), - "Signing should have failed because the TX uses non-standard sighashes" - ); - assert_matches!( - result, - Err(Error::Signer(SignerError::NonStandardSighash)), - "Signing failed with the wrong error type" - ); - - // try again after opting-in - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - assert!(result.is_ok(), "Signing should have worked"); - assert!( - result.unwrap(), - "Should finalize the input since we can produce signatures" - ); - - let extracted = psbt.extract_tx(); - assert_eq!( - *extracted.input[0].witness.to_vec()[0].last().unwrap(), - sighash.to_u32() as u8, - "The signature should have been made with the right sighash" - ); - } - - #[test] - fn test_unused_address() { - let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); - - assert_eq!( - wallet.get_address(LastUnused).to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - assert_eq!( - wallet.get_address(LastUnused).to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - } - - #[test] - fn test_next_unused_address() { - let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; - let mut wallet = Wallet::new(descriptor, None, Network::Testnet).unwrap(); - assert_eq!(wallet.derivation_index(KeychainKind::External), None); - - assert_eq!( - wallet.get_address(LastUnused).to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); - assert_eq!( - wallet.get_address(LastUnused).to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); - - // use the above address - receive_output_in_latest_block(&mut wallet, 25_000); - - assert_eq!( - wallet.get_address(LastUnused).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - assert_eq!(wallet.derivation_index(KeychainKind::External), Some(1)); - } - - #[test] - fn test_peek_address_at_index() { - let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); - - assert_eq!( - wallet.get_address(Peek(1)).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - - assert_eq!( - wallet.get_address(Peek(0)).to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - - assert_eq!( - wallet.get_address(Peek(2)).to_string(), - "tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2" - ); - - // current new address is not affected - assert_eq!( - wallet.get_address(New).to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - - assert_eq!( - wallet.get_address(New).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - } - - #[test] - fn test_peek_address_at_index_not_derivable() { - let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", - None, Network::Testnet).unwrap(); - - assert_eq!( - wallet.get_address(Peek(1)).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - - assert_eq!( - wallet.get_address(Peek(0)).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - - assert_eq!( - wallet.get_address(Peek(2)).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - } - - #[test] - fn test_returns_index_and_address() { - let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); - - // new index 0 - assert_eq!( - wallet.get_address(New), - AddressInfo { - index: 0, - address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a").unwrap(), - keychain: KeychainKind::External, - } - ); - - // new index 1 - assert_eq!( - wallet.get_address(New), - AddressInfo { - index: 1, - address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7").unwrap(), - keychain: KeychainKind::External, - } - ); - - // peek index 25 - assert_eq!( - wallet.get_address(Peek(25)), - AddressInfo { - index: 25, - address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2").unwrap(), - keychain: KeychainKind::External, - } - ); - - // new index 2 - assert_eq!( - wallet.get_address(New), - AddressInfo { - index: 2, - address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2").unwrap(), - keychain: KeychainKind::External, - } - ); - } - - #[test] - fn test_sending_to_bip350_bech32m_address() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = - Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c") - .unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 45_000); - builder.finish().unwrap(); - } - - #[test] - fn test_get_address() { - use crate::descriptor::template::Bip84; - let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let mut wallet = Wallet::new( - Bip84(key, KeychainKind::External), - Some(Bip84(key, KeychainKind::Internal)), - Network::Regtest, - ) - .unwrap(); - - assert_eq!( - wallet.get_address(AddressIndex::New), - AddressInfo { - index: 0, - address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w").unwrap(), - keychain: KeychainKind::External, - } - ); - - assert_eq!( - wallet.get_internal_address(AddressIndex::New), - AddressInfo { - index: 0, - address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79").unwrap(), - keychain: KeychainKind::Internal, - } - ); - - let mut wallet = - Wallet::new(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); - - assert_eq!( - wallet.get_internal_address(AddressIndex::New), - AddressInfo { - index: 0, - address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w").unwrap(), - keychain: KeychainKind::Internal, - }, - "when there's no internal descriptor it should just use external" - ); - } - - #[test] - fn test_get_address_no_reuse_single_descriptor() { - use crate::collections::HashSet; - use crate::descriptor::template::Bip84; - - let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let mut wallet = - Wallet::new(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); - - let mut used_set = HashSet::new(); - - (0..3).for_each(|_| { - let external_addr = wallet.get_address(AddressIndex::New).address; - assert!(used_set.insert(external_addr)); - - let internal_addr = wallet.get_internal_address(AddressIndex::New).address; - assert!(used_set.insert(internal_addr)); - }); - } - - #[test] - fn test_taproot_psbt_populate_tap_key_origins() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); - let addr = wallet.get_address(AddressIndex::New); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!( - psbt.inputs[0] - .tap_key_origins - .clone() - .into_iter() - .collect::>(), - vec![( - from_str!("b96d3a3dc76a4fc74e976511b23aecb78e0754c23c0ed7a6513e18cbbc7178e9"), - (vec![], (from_str!("f6a5cb8b"), from_str!("m/0"))) - )], - "Wrong input tap_key_origins" - ); - assert_eq!( - psbt.outputs[0] - .tap_key_origins - .clone() - .into_iter() - .collect::>(), - vec![( - from_str!("e9b03068cf4a2621d4f81e68f6c4216e6bd260fe6edf6acc55c8d8ae5aeff0a8"), - (vec![], (from_str!("f6a5cb8b"), from_str!("m/1"))) - )], - "Wrong output tap_key_origins" - ); - } - - #[test] - fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_repeated_key()); - let addr = wallet.get_address(AddressIndex::New); - - let path = vec![("e5mmg3xh".to_string(), vec![0])] - .into_iter() - .collect(); - - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 25_000) - .policy_path(path, KeychainKind::External); - let (psbt, _) = builder.finish().unwrap(); - - let mut input_key_origins = psbt.inputs[0] - .tap_key_origins - .clone() - .into_iter() - .collect::>(); - input_key_origins.sort(); - - assert_eq!( - input_key_origins, - vec![ - ( - from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"), - ( - vec![], - (FromStr::from_str("871fd295").unwrap(), vec![].into()) - ) - ), - ( - from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"), - ( - vec![ - from_str!( - "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e" - ), - from_str!( - "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903" - ), - ], - (FromStr::from_str("ece52657").unwrap(), vec![].into()) - ) - ) - ], - "Wrong input tap_key_origins" - ); - - let mut output_key_origins = psbt.outputs[0] - .tap_key_origins - .clone() - .into_iter() - .collect::>(); - output_key_origins.sort(); - - assert_eq!( - input_key_origins, output_key_origins, - "Wrong output tap_key_origins" - ); - } - - #[test] - fn test_taproot_psbt_input_tap_tree() { - use crate::bitcoin::psbt::serialize::Deserialize; - use crate::bitcoin::psbt::TapTree; - use bitcoin::hashes::hex::FromHex; - use bitcoin::util::taproot; - - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); - let addr = wallet.get_address(AddressIndex::Peek(0)); - - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); - - assert_eq!( - psbt.inputs[0].tap_merkle_root, - Some( - FromHex::from_hex( - "61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986" - ) - .unwrap() - ), - ); - assert_eq!( - psbt.inputs[0].tap_scripts.clone().into_iter().collect::>(), - vec![ - (taproot::ControlBlock::from_slice(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (from_str!("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac"), taproot::LeafVersion::TapScript)), - (taproot::ControlBlock::from_slice(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (from_str!("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac"), taproot::LeafVersion::TapScript)), - ], - ); - assert_eq!( - psbt.inputs[0].tap_internal_key, - Some(from_str!( - "b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55" - )) - ); - - // Since we are creating an output to the same address as the input, assert that the - // internal_key is the same - assert_eq!( - psbt.inputs[0].tap_internal_key, - psbt.outputs[0].tap_internal_key - ); - - assert_eq!( - psbt.outputs[0].tap_tree, - Some(TapTree::deserialize(&Vec::::from_hex("01c022208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac01c0222051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",).unwrap()).unwrap()) - ); - } - - #[test] - fn test_taproot_sign_missing_witness_utxo() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - let witness_utxo = psbt.inputs[0].witness_utxo.take(); - - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - assert_matches!( - result, - Err(Error::Signer(SignerError::MissingWitnessUtxo)), - "Signing should have failed with the correct error because the witness_utxo is missing" - ); - - // restore the witness_utxo - psbt.inputs[0].witness_utxo = witness_utxo; - - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - - assert_matches!( - result, - Ok(true), - "Should finalize the input since we can produce signatures" - ); - } - - #[test] - fn test_taproot_sign_using_non_witness_utxo() { - let (mut wallet, prev_txid) = get_funded_wallet(get_test_tr_single_sig()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - psbt.inputs[0].witness_utxo = None; - psbt.inputs[0].non_witness_utxo = wallet.get_tx(prev_txid, true).unwrap().transaction; - assert!( - psbt.inputs[0].non_witness_utxo.is_some(), - "Previous tx should be present in the database" - ); - - let result = wallet.sign(&mut psbt, Default::default()); - assert!(result.is_ok(), "Signing should have worked"); - assert!( - result.unwrap(), - "Should finalize the input since we can produce signatures" - ); - } - - #[test] - fn test_taproot_foreign_utxo() { - let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); - let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig()); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo = wallet2.list_unspent().remove(0); - let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); - let foreign_utxo_satisfaction = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_satisfaction_weight() - .unwrap(); - - assert!( - psbt_input.non_witness_utxo.is_none(), - "`non_witness_utxo` should never be populated for taproot" - ); - - let mut builder = wallet1.build_tx(); - builder - .add_recipient(addr.script_pubkey(), 60_000) - .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) - .unwrap(); - let (psbt, details) = builder.finish().unwrap(); - - assert_eq!( - details.sent - details.received, - 10_000 + details.fee.unwrap_or(0), - "we should have only net spent ~10_000" - ); - - assert!( - psbt.unsigned_tx - .input - .iter() - .any(|input| input.previous_output == utxo.outpoint), - "foreign_utxo should be in there" - ); - } - - fn test_spend_from_wallet(mut wallet: Wallet) { - let addr = wallet.get_address(AddressIndex::New); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); - - assert!( - wallet.sign(&mut psbt, Default::default()).unwrap(), - "Unable to finalize tx" - ); - } - - // #[test] - // fn test_taproot_key_spend() { - // let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); - // test_spend_from_wallet(wallet); - - // let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); - // test_spend_from_wallet(wallet); - // } - - #[test] - fn test_taproot_no_key_spend() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.get_address(AddressIndex::New); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); - - assert!( - wallet - .sign( - &mut psbt, - SignOptions { - sign_with_tap_internal_key: false, - ..Default::default() - }, - ) - .unwrap(), - "Unable to finalize tx" - ); - - assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none())); - } - - #[test] - fn test_taproot_script_spend() { - let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); - test_spend_from_wallet(wallet); - - let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree_xprv()); - test_spend_from_wallet(wallet); - } - - #[test] - fn test_taproot_script_spend_sign_all_leaves() { - use crate::signer::TapLeavesOptions; - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.get_address(AddressIndex::New); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); - - assert!( - wallet - .sign( - &mut psbt, - SignOptions { - tap_leaves_options: TapLeavesOptions::All, - ..Default::default() - }, - ) - .unwrap(), - "Unable to finalize tx" - ); - - assert!(psbt - .inputs - .iter() - .all(|i| i.tap_script_sigs.len() == i.tap_scripts.len())); - } - - #[test] - fn test_taproot_script_spend_sign_include_some_leaves() { - use crate::signer::TapLeavesOptions; - use bitcoin::util::taproot::TapLeafHash; - - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.get_address(AddressIndex::New); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); - let mut script_leaves: Vec<_> = psbt.inputs[0] - .tap_scripts - .clone() - .values() - .map(|(script, version)| TapLeafHash::from_script(script, *version)) - .collect(); - let included_script_leaves = vec![script_leaves.pop().unwrap()]; - let excluded_script_leaves = script_leaves; - - assert!( - wallet - .sign( - &mut psbt, - SignOptions { - tap_leaves_options: TapLeavesOptions::Include( - included_script_leaves.clone() - ), - ..Default::default() - }, - ) - .unwrap(), - "Unable to finalize tx" - ); - - assert!(psbt.inputs[0] - .tap_script_sigs - .iter() - .all(|s| included_script_leaves.contains(&s.0 .1) - && !excluded_script_leaves.contains(&s.0 .1))); - } - - #[test] - fn test_taproot_script_spend_sign_exclude_some_leaves() { - use crate::signer::TapLeavesOptions; - use bitcoin::util::taproot::TapLeafHash; - - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.get_address(AddressIndex::New); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); - let mut script_leaves: Vec<_> = psbt.inputs[0] - .tap_scripts - .clone() - .values() - .map(|(script, version)| TapLeafHash::from_script(script, *version)) - .collect(); - let included_script_leaves = vec![script_leaves.pop().unwrap()]; - let excluded_script_leaves = script_leaves; - - assert!( - wallet - .sign( - &mut psbt, - SignOptions { - tap_leaves_options: TapLeavesOptions::Exclude( - excluded_script_leaves.clone() - ), - ..Default::default() - }, - ) - .unwrap(), - "Unable to finalize tx" - ); - - assert!(psbt.inputs[0] - .tap_script_sigs - .iter() - .all(|s| included_script_leaves.contains(&s.0 .1) - && !excluded_script_leaves.contains(&s.0 .1))); - } - - #[test] - fn test_taproot_script_spend_sign_no_leaves() { - use crate::signer::TapLeavesOptions; - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.get_address(AddressIndex::New); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); - - wallet - .sign( - &mut psbt, - SignOptions { - tap_leaves_options: TapLeavesOptions::None, - ..Default::default() - }, - ) - .unwrap(); - - assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty())); - } - - #[test] - fn test_taproot_sign_derive_index_from_psbt() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); - - let addr = wallet.get_address(AddressIndex::New); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); - - // re-create the wallet with an empty db - let wallet_empty = - Wallet::new(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap(); - - // signing with an empty db means that we will only look at the psbt to infer the - // derivation index - assert!( - wallet_empty.sign(&mut psbt, Default::default()).unwrap(), - "Unable to finalize tx" - ); - } - - #[test] - fn test_taproot_sign_explicit_sighash_all() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .sighash(SchnorrSighashType::All.into()) - .drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let result = wallet.sign(&mut psbt, Default::default()); - assert!( - result.is_ok(), - "Signing should work because SIGHASH_ALL is safe" - ) - } - - #[test] - fn test_taproot_sign_non_default_sighash() { - let sighash = SchnorrSighashType::NonePlusAnyoneCanPay; - - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); - let addr = wallet.get_address(New); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .sighash(sighash.into()) - .drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let witness_utxo = psbt.inputs[0].witness_utxo.take(); - - let result = wallet.sign(&mut psbt, Default::default()); - assert!( - result.is_err(), - "Signing should have failed because the TX uses non-standard sighashes" - ); - assert_matches!( - result, - Err(Error::Signer(SignerError::NonStandardSighash)), - "Signing failed with the wrong error type" - ); - - // try again after opting-in - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - assert!( - result.is_err(), - "Signing should have failed because the witness_utxo is missing" - ); - assert_matches!( - result, - Err(Error::Signer(SignerError::MissingWitnessUtxo)), - "Signing failed with the wrong error type" - ); - - // restore the witness_utxo - psbt.inputs[0].witness_utxo = witness_utxo; - - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - - assert!(result.is_ok(), "Signing should have worked"); - assert!( - result.unwrap(), - "Should finalize the input since we can produce signatures" - ); - - let extracted = psbt.extract_tx(); - assert_eq!( - *extracted.input[0].witness.to_vec()[0].last().unwrap(), - sighash as u8, - "The signature should have been made with the right sighash" - ); - } - - #[test] - fn test_spend_coinbase() { - let descriptor = get_test_wpkh(); - let mut wallet = Wallet::new(descriptor, None, Network::Regtest).unwrap(); - - let confirmation_height = 5; - wallet - .insert_checkpoint(BlockId { - height: confirmation_height, - hash: BlockHash::all_zeros(), - }) - .unwrap(); - let coinbase_tx = Transaction { - version: 1, - lock_time: bitcoin::PackedLockTime(0), - input: vec![TxIn { - previous_output: OutPoint::null(), - ..Default::default() - }], - output: vec![TxOut { - value: 25_000, - script_pubkey: wallet.get_address(New).address.script_pubkey(), - }], - }; - wallet - .insert_tx( - coinbase_tx, - ConfirmationTime::Confirmed { - height: confirmation_height, - time: 30_000, - }, - ) - .unwrap(); - - let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1; - let maturity_time = confirmation_height + COINBASE_MATURITY; - - let balance = wallet.get_balance(); - assert_eq!( - balance, - Balance { - immature: 25_000, - trusted_pending: 0, - untrusted_pending: 0, - confirmed: 0 - } - ); - - // We try to create a transaction, only to notice that all - // our funds are unspendable - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), balance.immature / 2) - .current_height(confirmation_height); - assert!(matches!( - builder.finish(), - Err(Error::InsufficientFunds { - needed: _, - available: 0 - }) - )); - - // Still unspendable... - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), balance.immature / 2) - .current_height(not_yet_mature_time); - assert_matches!( - builder.finish(), - Err(Error::InsufficientFunds { - needed: _, - available: 0 - }) - ); - - wallet - .insert_checkpoint(BlockId { - height: maturity_time, - hash: BlockHash::all_zeros(), - }) - .unwrap(); - let balance = wallet.get_balance(); - assert_eq!( - balance, - Balance { - immature: 0, - trusted_pending: 0, - untrusted_pending: 0, - confirmed: 25_000 - } - ); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), balance.confirmed / 2) - .current_height(maturity_time); - builder.finish().unwrap(); - } - - #[test] - fn test_allow_dust_limit() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - - let addr = wallet.get_address(New); - - let mut builder = wallet.build_tx(); - - builder.add_recipient(addr.script_pubkey(), 0); - - assert_matches!(builder.finish(), Err(Error::OutputBelowDustLimit(0))); - - let mut builder = wallet.build_tx(); - - builder - .allow_dust(true) - .add_recipient(addr.script_pubkey(), 0); - - assert!(builder.finish().is_ok()); - } - - #[test] - fn test_fee_rate_sign_no_grinding_high_r() { - // Our goal is to obtain a transaction with a signature with high-R (71 bytes - // instead of 70). We then check that our fee rate and fee calculation is - // alright. - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.get_address(New); - let fee_rate = FeeRate::from_sat_per_vb(1.0); - let mut builder = wallet.build_tx(); - let mut data = vec![0]; - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_rate(fee_rate) - .add_data(&data); - let (mut psbt, details) = builder.finish().unwrap(); - let (op_return_vout, _) = psbt - .unsigned_tx - .output - .iter() - .enumerate() - .find(|(_n, i)| i.script_pubkey.is_op_return()) - .unwrap(); - - let mut sig_len: usize = 0; - // We try to sign many different times until we find a longer signature (71 bytes) - while sig_len < 71 { - // Changing the OP_RETURN data will make the signature change (but not the fee, until - // data[0] is small enough) - data[0] += 1; - psbt.unsigned_tx.output[op_return_vout].script_pubkey = Script::new_op_return(&data); - // Clearing the previous signature - psbt.inputs[0].partial_sigs.clear(); - // Signing - wallet - .sign( - &mut psbt, - SignOptions { - remove_partial_sigs: false, - try_finalize: false, - allow_grinding: false, - ..Default::default() - }, - ) - .unwrap(); - // We only have one key in the partial_sigs map, this is a trick to retrieve it - let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); - sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len(); - } - // Actually finalizing the transaction... - wallet - .sign( - &mut psbt, - SignOptions { - remove_partial_sigs: false, - allow_grinding: false, - ..Default::default() - }, - ) - .unwrap(); - // ...and checking that everything is fine - assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate); - } - - #[test] - fn test_fee_rate_sign_grinding_low_r() { - // Our goal is to obtain a transaction with a signature with low-R (70 bytes) - // by setting the `allow_grinding` signing option as true. - // We then check that our fee rate and fee calculation is alright and that our - // signature is 70 bytes. - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.get_address(New); - let fee_rate = FeeRate::from_sat_per_vb(1.0); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_rate(fee_rate); - let (mut psbt, details) = builder.finish().unwrap(); - - wallet - .sign( - &mut psbt, - SignOptions { - remove_partial_sigs: false, - allow_grinding: true, - ..Default::default() - }, - ) - .unwrap(); - - let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); - let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len(); - assert_eq!(sig_len, 70); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate); - } - - #[cfg(feature = "test-hardware-signer")] - #[test] - fn test_create_signer() { - use crate::wallet::hardwaresigner::HWISigner; - use hwi::types::HWIChain; - use hwi::HWIClient; - - let mut devices = HWIClient::enumerate().unwrap(); - if devices.is_empty() { - panic!("No devices found!"); - } - let device = devices.remove(0).unwrap(); - let client = HWIClient::get_client(&device, true, HWIChain::Regtest).unwrap(); - let descriptors = client.get_descriptors::(None).unwrap(); - let custom_signer = HWISigner::from_device(&device, HWIChain::Regtest).unwrap(); - - let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]); - wallet.add_signer( - KeychainKind::External, - SignerOrdering(200), - Arc::new(custom_signer), - ); - - let addr = wallet.get_address(LastUnused); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - } - - #[test] - fn test_taproot_load_descriptor_duplicated_keys() { - // Added after issue https://github.com/bitcoindevkit/bdk/issues/760 - // - // Having the same key in multiple taproot leaves is safe and should be accepted by BDK - - let (mut wallet, _) = get_funded_wallet(get_test_tr_dup_keys()); - let addr = wallet.get_address(New); - - assert_eq!( - addr.to_string(), - "bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5" - ); - } -} diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 00000000..cd00db86 --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,86 @@ +#![allow(unused)] +use bdk::{wallet::AddressIndex, Wallet}; +use bdk_chain::{BlockId, ConfirmationTime}; +use bitcoin::hashes::Hash; +use bitcoin::{BlockHash, Network, Transaction, TxOut}; + +/// Return a fake wallet that appears to be funded for testing. +pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { + let mut wallet = Wallet::new(descriptor, None, Network::Regtest).unwrap(); + let address = wallet.get_address(AddressIndex::New).address; + + let tx = Transaction { + version: 1, + lock_time: bitcoin::PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 50_000, + script_pubkey: address.script_pubkey(), + }], + }; + + wallet + .insert_checkpoint(BlockId { + height: 1_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_tx( + tx.clone(), + ConfirmationTime::Confirmed { + height: 1_000, + time: 100, + }, + ) + .unwrap(); + + (wallet, tx.txid()) +} + +pub fn get_test_wpkh() -> &'static str { + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" +} + +pub fn get_test_single_sig_csv() -> &'static str { + // and(pk(Alice),older(6)) + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))" +} + +pub fn get_test_a_or_b_plus_csv() -> &'static str { + // or(pk(Alice),and(pk(Bob),older(144))) + "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))" +} + +pub fn get_test_single_sig_cltv() -> &'static str { + // and(pk(Alice),after(100000)) + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))" +} + +pub fn get_test_tr_single_sig() -> &'static str { + "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)" +} + +pub fn get_test_tr_with_taptree() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" +} + +pub fn get_test_tr_with_taptree_both_priv() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})" +} + +pub fn get_test_tr_repeated_key() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})" +} + +pub fn get_test_tr_single_sig_xprv() -> &'static str { + "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)" +} + +pub fn get_test_tr_with_taptree_xprv() -> &'static str { + "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" +} + +pub fn get_test_tr_dup_keys() -> &'static str { + "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" +} diff --git a/tests/psbt.rs b/tests/psbt.rs new file mode 100644 index 00000000..8d399f5f --- /dev/null +++ b/tests/psbt.rs @@ -0,0 +1,158 @@ +use bdk::bitcoin::TxIn; +use bdk::wallet::AddressIndex; +use bdk::wallet::AddressIndex::New; +use bdk::{psbt, FeeRate, SignOptions}; +use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; +use core::str::FromStr; +mod common; +use common::*; + +// from bip 174 +const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA"; + +#[test] +#[should_panic(expected = "InputIndexOutOfRange")] +fn test_psbt_malformed_psbt_input_legacy() { + let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = wallet.get_address(AddressIndex::New); + let mut builder = wallet.build_tx(); + builder.add_recipient(send_to.script_pubkey(), 10_000); + let (mut psbt, _) = builder.finish().unwrap(); + psbt.inputs.push(psbt_bip.inputs[0].clone()); + let options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + let _ = wallet.sign(&mut psbt, options).unwrap(); +} + +#[test] +#[should_panic(expected = "InputIndexOutOfRange")] +fn test_psbt_malformed_psbt_input_segwit() { + let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = wallet.get_address(AddressIndex::New); + let mut builder = wallet.build_tx(); + builder.add_recipient(send_to.script_pubkey(), 10_000); + let (mut psbt, _) = builder.finish().unwrap(); + psbt.inputs.push(psbt_bip.inputs[1].clone()); + let options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + let _ = wallet.sign(&mut psbt, options).unwrap(); +} + +#[test] +#[should_panic(expected = "InputIndexOutOfRange")] +fn test_psbt_malformed_tx_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = wallet.get_address(AddressIndex::New); + let mut builder = wallet.build_tx(); + builder.add_recipient(send_to.script_pubkey(), 10_000); + let (mut psbt, _) = builder.finish().unwrap(); + psbt.unsigned_tx.input.push(TxIn::default()); + let options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + let _ = wallet.sign(&mut psbt, options).unwrap(); +} + +#[test] +fn test_psbt_sign_with_finalized() { + let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = wallet.get_address(AddressIndex::New); + let mut builder = wallet.build_tx(); + builder.add_recipient(send_to.script_pubkey(), 10_000); + let (mut psbt, _) = builder.finish().unwrap(); + + // add a finalized input + psbt.inputs.push(psbt_bip.inputs[0].clone()); + psbt.unsigned_tx + .input + .push(psbt_bip.unsigned_tx.input[0].clone()); + + let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); +} + +#[test] +fn test_psbt_fee_rate_with_witness_utxo() { + use psbt::PsbtUtils; + + let expected_fee_rate = 1.2345; + + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); + let (mut psbt, _) = builder.finish().unwrap(); + let fee_amount = psbt.fee_amount(); + assert!(fee_amount.is_some()); + + let unfinalized_fee_rate = psbt.fee_rate().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let finalized_fee_rate = psbt.fee_rate().unwrap(); + assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate); + assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb()); +} + +#[test] +fn test_psbt_fee_rate_with_nonwitness_utxo() { + use psbt::PsbtUtils; + + let expected_fee_rate = 1.2345; + + let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); + let (mut psbt, _) = builder.finish().unwrap(); + let fee_amount = psbt.fee_amount(); + assert!(fee_amount.is_some()); + let unfinalized_fee_rate = psbt.fee_rate().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let finalized_fee_rate = psbt.fee_rate().unwrap(); + assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate); + assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb()); +} + +#[test] +fn test_psbt_fee_rate_with_missing_txout() { + use psbt::PsbtUtils; + + let expected_fee_rate = 1.2345; + + let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wpkh_wallet.get_address(New); + let mut builder = wpkh_wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); + let (mut wpkh_psbt, _) = builder.finish().unwrap(); + + wpkh_psbt.inputs[0].witness_utxo = None; + wpkh_psbt.inputs[0].non_witness_utxo = None; + assert!(wpkh_psbt.fee_amount().is_none()); + assert!(wpkh_psbt.fee_rate().is_none()); + + let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = pkh_wallet.get_address(New); + let mut builder = pkh_wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); + let (mut pkh_psbt, _) = builder.finish().unwrap(); + + pkh_psbt.inputs[0].non_witness_utxo = None; + assert!(pkh_psbt.fee_amount().is_none()); + assert!(pkh_psbt.fee_rate().is_none()); +} diff --git a/tests/wallet.rs b/tests/wallet.rs new file mode 100644 index 00000000..88de27a5 --- /dev/null +++ b/tests/wallet.rs @@ -0,0 +1,3241 @@ +use assert_matches::assert_matches; +use bdk::descriptor::calc_checksum; +use bdk::signer::{SignOptions, SignerError}; +use bdk::wallet::coin_selection::LargestFirstCoinSelection; +use bdk::wallet::AddressIndex::*; +use bdk::wallet::AddressInfo; +use bdk::wallet::{AddressIndex, Wallet}; +use bdk::Balance; +use bdk::Error; +use bdk::FeeRate; +use bdk::KeychainKind; +use bdk_chain::BlockId; +use bdk_chain::COINBASE_MATURITY; +use bdk_chain::{ConfirmationTime, TxHeight}; +use bitcoin::hashes::Hash; +use bitcoin::BlockHash; +use bitcoin::Script; +use bitcoin::{util::psbt, Network}; +use bitcoin::{ + Address, EcdsaSighashType, LockTime, OutPoint, PackedLockTime, SchnorrSighashType, Sequence, + Transaction, TxIn, TxOut, +}; +use core::str::FromStr; + +mod common; +use common::*; + +fn receive_output(wallet: &mut Wallet, value: u64, height: TxHeight) -> OutPoint { + let tx = Transaction { + version: 1, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + script_pubkey: wallet.get_address(LastUnused).script_pubkey(), + value, + }], + }; + + wallet + .insert_tx( + tx.clone(), + match height { + TxHeight::Confirmed(height) => ConfirmationTime::Confirmed { + height, + time: 42_000, + }, + TxHeight::Unconfirmed => ConfirmationTime::Unconfirmed, + }, + ) + .unwrap(); + + OutPoint { + txid: tx.txid(), + vout: 0, + } +} + +fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { + let height = wallet.latest_checkpoint().map(|id| id.height).into(); + receive_output(wallet, value, height) +} + +// The satisfaction size of a P2WPKH is 112 WU = +// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len) +// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for +// a total of 105 WU. +// Here, we push just once for simplicity, so we have to add an extra byte for the missing +// OP_PUSH. +const P2WPKH_FAKE_WITNESS_SIZE: usize = 106; + +#[test] +fn test_descriptor_checksum() { + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + let checksum = wallet.descriptor_checksum(KeychainKind::External); + assert_eq!(checksum.len(), 8); + + let raw_descriptor = wallet + .keychanins() + .iter() + .next() + .unwrap() + .1 + .to_string() + .split_once('#') + .unwrap() + .0 + .to_string(); + assert_eq!(calc_checksum(&raw_descriptor).unwrap(), checksum); +} + +#[test] +fn test_get_funded_wallet_balance() { + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + assert_eq!(wallet.get_balance().confirmed, 50000); +} + +macro_rules! assert_fee_rate { + ($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({ + let psbt = $psbt.clone(); + #[allow(unused_mut)] + let mut tx = $psbt.clone().extract_tx(); + $( + $( $add_signature )* + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + )* + + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut dust_change = false; + $( + $( $dust_change )* + dust_change = true; + )* + + let fee_amount = psbt + .inputs + .iter() + .fold(0, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) + - psbt + .unsigned_tx + .output + .iter() + .fold(0, |acc, o| acc + o.value); + + assert_eq!(fee_amount, $fees); + + let tx_fee_rate = FeeRate::from_wu($fees, tx.weight()); + let fee_rate = $fee_rate; + + if !dust_change { + assert!(tx_fee_rate >= fee_rate && (tx_fee_rate - fee_rate).as_sat_per_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate); + } else { + assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate); + } + }); +} + +macro_rules! from_str { + ($e:expr, $t:ty) => {{ + use core::str::FromStr; + <$t>::from_str($e).unwrap() + }}; + + ($e:expr) => { + from_str!($e, _) + }; +} + +#[test] +#[should_panic(expected = "NoRecipients")] +fn test_create_tx_empty_recipients() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + wallet.build_tx().finish().unwrap(); +} + +#[test] +#[should_panic(expected = "NoUtxosSelected")] +fn test_create_tx_manually_selected_empty_utxos() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .manually_selected_only(); + builder.finish().unwrap(); +} + +#[test] +#[should_panic(expected = "Invalid version `0`")] +fn test_create_tx_version_0() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .version(0); + builder.finish().unwrap(); +} + +#[test] +#[should_panic( + expected = "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" +)] +fn test_create_tx_version_1_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .version(1); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_custom_version() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .version(42); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.version, 42); +} + +#[test] +fn test_create_tx_default_locktime_is_last_sync_height() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + // Since we never synced the wallet we don't have a last_sync_height + // we could use to try to prevent fee sniping. We default to 0. + assert_eq!(psbt.unsigned_tx.lock_time.0, 1_000); +} + +#[test] +fn test_create_tx_fee_sniping_locktime_last_sync() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + + let (psbt, _) = builder.finish().unwrap(); + + // If there's no current_height we're left with using the last sync height + assert_eq!( + psbt.unsigned_tx.lock_time.0, + wallet.latest_checkpoint().unwrap().height + ); +} + +#[test] +fn test_create_tx_default_locktime_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.lock_time.0, 100_000); +} + +#[test] +fn test_create_tx_custom_locktime() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .current_height(630_001) + .nlocktime(LockTime::from_height(630_000).unwrap()); + let (psbt, _) = builder.finish().unwrap(); + + // When we explicitly specify a nlocktime + // we don't try any fee sniping prevention trick + // (we ignore the current_height) + assert_eq!(psbt.unsigned_tx.lock_time.0, 630_000); +} + +#[test] +fn test_create_tx_custom_locktime_compatible_with_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .nlocktime(LockTime::from_height(630_000).unwrap()); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.lock_time.0, 630_000); +} + +#[test] +#[should_panic( + expected = "TxBuilder requested timelock of `Blocks(Height(50000))`, but at least `Blocks(Height(100000))` is required to spend from this script" +)] +fn test_create_tx_custom_locktime_incompatible_with_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .nlocktime(LockTime::from_height(50000).unwrap()); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_no_rbf_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); +} + +#[test] +fn test_create_tx_with_default_rbf_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, _) = builder.finish().unwrap(); + // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). + // It will be set to the OP_CSV value, in this case 6 + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); +} + +#[test] +#[should_panic( + expected = "Cannot enable RBF with nSequence `Sequence(3)` given a required OP_CSV of `Sequence(6)`" +)] +fn test_create_tx_with_custom_rbf_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf_with_sequence(Sequence(3)); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_no_rbf_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); +} + +#[test] +#[should_panic(expected = "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")] +fn test_create_tx_invalid_rbf_sequence() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_custom_rbf_sequence() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf_with_sequence(Sequence(0xDEADBEEF)); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF)); +} + +#[test] +fn test_create_tx_default_sequence() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); +} + +#[test] +#[should_panic( + expected = "The `change_policy` can be set only if the wallet has a change_descriptor" +)] +fn test_create_tx_change_policy_no_internal() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .do_not_spend_change(); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_drain_wallet_and_drain_to() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.unsigned_tx.output[0].value, + 50_000 - details.fee.unwrap_or(0) + ); +} + +#[test] +fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); + let drain_addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 20_000) + .drain_to(drain_addr.script_pubkey()) + .drain_wallet(); + let (psbt, details) = builder.finish().unwrap(); + let outputs = psbt.unsigned_tx.output; + + assert_eq!(outputs.len(), 2); + let main_output = outputs + .iter() + .find(|x| x.script_pubkey == addr.script_pubkey()) + .unwrap(); + let drain_output = outputs + .iter() + .find(|x| x.script_pubkey == drain_addr.script_pubkey()) + .unwrap(); + assert_eq!(main_output.value, 20_000,); + assert_eq!(drain_output.value, 30_000 - details.fee.unwrap_or(0)); +} + +#[test] +fn test_create_tx_drain_to_and_utxos() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let utxos: Vec<_> = wallet + .list_unspent() + .into_iter() + .map(|u| u.outpoint) + .collect(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxos(&utxos) + .unwrap(); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.unsigned_tx.output[0].value, + 50_000 - details.fee.unwrap_or(0) + ); +} + +#[test] +#[should_panic(expected = "NoRecipients")] +fn test_create_tx_drain_to_no_drain_wallet_no_utxos() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let drain_addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(drain_addr.script_pubkey()); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_default_fee_rate() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, details) = builder.finish().unwrap(); + + assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::default(), @add_signature); +} + +#[test] +fn test_create_tx_custom_fee_rate() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + let (psbt, details) = builder.finish().unwrap(); + + assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); +} + +#[test] +fn test_create_tx_absolute_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(100); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.fee.unwrap_or(0), 100); + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.unsigned_tx.output[0].value, + 50_000 - details.fee.unwrap_or(0) + ); +} + +#[test] +fn test_create_tx_absolute_zero_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(0); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.fee.unwrap_or(0), 0); + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.unsigned_tx.output[0].value, + 50_000 - details.fee.unwrap_or(0) + ); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_create_tx_absolute_high_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(60_000); + let (_psbt, _details) = builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_add_change() { + use bdk::wallet::tx_builder::TxOrdering; + + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .ordering(TxOrdering::Untouched); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.output.len(), 2); + assert_eq!(psbt.unsigned_tx.output[0].value, 25_000); + assert_eq!( + psbt.unsigned_tx.output[1].value, + 25_000 - details.fee.unwrap_or(0) + ); +} + +#[test] +fn test_create_tx_skip_change_dust() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 49_800); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!(psbt.unsigned_tx.output[0].value, 49_800); + assert_eq!(details.fee.unwrap_or(0), 200); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_create_tx_drain_to_dust_amount() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + // very high fee rate, so that the only output would be below dust + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_rate(FeeRate::from_sat_per_vb(453.0)); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_ordering_respected() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .add_recipient(addr.script_pubkey(), 10_000) + .ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.output.len(), 3); + assert_eq!( + psbt.unsigned_tx.output[0].value, + 10_000 - details.fee.unwrap_or(0) + ); + assert_eq!(psbt.unsigned_tx.output[1].value, 10_000); + assert_eq!(psbt.unsigned_tx.output[2].value, 30_000); +} + +#[test] +fn test_create_tx_default_sighash() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 30_000); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.inputs[0].sighash_type, None); +} + +#[test] +fn test_create_tx_custom_sighash() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .sighash(bitcoin::EcdsaSighashType::Single.into()); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0].sighash_type, + Some(bitcoin::EcdsaSighashType::Single.into()) + ); +} + +#[test] +fn test_create_tx_input_hd_keypaths() { + use bitcoin::util::bip32::{DerivationPath, Fingerprint}; + use core::str::FromStr; + + let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1); + assert_eq!( + psbt.inputs[0].bip32_derivation.values().next().unwrap(), + &( + Fingerprint::from_str("d34db33f").unwrap(), + DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap() + ) + ); +} + +#[test] +fn test_create_tx_output_hd_keypaths() { + use bitcoin::util::bip32::{DerivationPath, Fingerprint}; + use core::str::FromStr; + + let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); + + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1); + let expected_derivation_path = format!("m/44'/0'/0'/0/{}", addr.index); + assert_eq!( + psbt.outputs[0].bip32_derivation.values().next().unwrap(), + &( + Fingerprint::from_str("d34db33f").unwrap(), + DerivationPath::from_str(&expected_derivation_path).unwrap() + ) + ); +} + +#[test] +fn test_create_tx_set_redeem_script_p2sh() { + use bitcoin::hashes::hex::FromHex; + + let (mut wallet, _) = + get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0].redeem_script, + Some(Script::from( + Vec::::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" + ) + .unwrap() + )) + ); + assert_eq!(psbt.inputs[0].witness_script, None); +} + +#[test] +fn test_create_tx_set_witness_script_p2wsh() { + use bitcoin::hashes::hex::FromHex; + + let (mut wallet, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.inputs[0].redeem_script, None); + assert_eq!( + psbt.inputs[0].witness_script, + Some(Script::from( + Vec::::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" + ) + .unwrap() + )) + ); +} + +#[test] +fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() { + use bitcoin::hashes::hex::FromHex; + + let (mut wallet, _) = + get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + let script = Script::from( + Vec::::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac", + ) + .unwrap(), + ); + + assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_v0_p2wsh())); + assert_eq!(psbt.inputs[0].witness_script, Some(script)); +} + +#[test] +fn test_create_tx_non_witness_utxo() { + let (mut wallet, _) = + get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert!(psbt.inputs[0].non_witness_utxo.is_some()); + assert!(psbt.inputs[0].witness_utxo.is_none()); +} + +#[test] +fn test_create_tx_only_witness_utxo() { + let (mut wallet, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .only_witness_utxo() + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert!(psbt.inputs[0].non_witness_utxo.is_none()); + assert!(psbt.inputs[0].witness_utxo.is_some()); +} + +#[test] +fn test_create_tx_shwpkh_has_witness_utxo() { + let (mut wallet, _) = + get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert!(psbt.inputs[0].witness_utxo.is_some()); +} + +#[test] +fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() { + let (mut wallet, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert!(psbt.inputs[0].non_witness_utxo.is_some()); + assert!(psbt.inputs[0].witness_utxo.is_some()); +} + +#[test] +fn test_create_tx_add_utxo() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let small_output_tx = Transaction { + input: vec![], + output: vec![TxOut { + value: 25_000, + script_pubkey: wallet.get_address(New).address.script_pubkey(), + }], + version: 0, + lock_time: PackedLockTime(0), + }; + wallet + .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed) + .unwrap(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .add_utxo(OutPoint { + txid: small_output_tx.txid(), + vout: 0, + }) + .unwrap(); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!( + psbt.unsigned_tx.input.len(), + 2, + "should add an additional input since 25_000 < 30_000" + ); + assert_eq!(details.sent, 75_000, "total should be sum of both inputs"); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_create_tx_manually_selected_insufficient() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let small_output_tx = Transaction { + input: vec![], + output: vec![TxOut { + value: 25_000, + script_pubkey: wallet.get_address(New).address.script_pubkey(), + }], + version: 0, + lock_time: PackedLockTime(0), + }; + + wallet + .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed) + .unwrap(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .add_utxo(OutPoint { + txid: small_output_tx.txid(), + vout: 0, + }) + .unwrap() + .manually_selected_only(); + builder.finish().unwrap(); +} + +#[test] +#[should_panic(expected = "SpendingPolicyRequired(External)")] +fn test_create_tx_policy_path_required() { + let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 30_000); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_policy_path_no_csv() { + let descriptors = get_test_wpkh(); + let mut wallet = Wallet::new(descriptors, None, Network::Regtest).unwrap(); + + let tx = Transaction { + version: 0, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 50_000, + script_pubkey: wallet.get_address(New).script_pubkey(), + }], + }; + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); + let root_id = external_policy.id; + // child #0 is just the key "A" + let path = vec![(root_id, vec![0])].into_iter().collect(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .policy_path(path, KeychainKind::External); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF)); +} + +#[test] +fn test_create_tx_policy_path_use_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); + + let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); + let root_id = external_policy.id; + // child #1 is or(pk(B),older(144)) + let path = vec![(root_id, vec![1])].into_iter().collect(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .policy_path(path, KeychainKind::External); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144)); +} + +#[test] +fn test_create_tx_global_xpubs_with_origin() { + use bitcoin::hashes::hex::FromHex; + use bitcoin::util::bip32; + + let (mut wallet, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .add_global_xpubs(); + let (psbt, _) = builder.finish().unwrap(); + + let key = bip32::ExtendedPubKey::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap(); + let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap(); + let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap(); + + assert_eq!(psbt.xpub.len(), 1); + assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path))); +} + +#[test] +fn test_add_foreign_utxo() { + let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); + let (wallet2, _) = + get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let utxo = wallet2.list_unspent().remove(0); + let foreign_utxo_satisfaction = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_satisfaction_weight() + .unwrap(); + + let psbt_input = psbt::Input { + witness_utxo: Some(utxo.txout.clone()), + ..Default::default() + }; + + let mut builder = wallet1.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 60_000) + .only_witness_utxo() + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let (mut psbt, details) = builder.finish().unwrap(); + + assert_eq!( + details.sent - details.received, + 10_000 + details.fee.unwrap_or(0), + "we should have only net spent ~10_000" + ); + + assert!( + psbt.unsigned_tx + .input + .iter() + .any(|input| input.previous_output == utxo.outpoint), + "foreign_utxo should be in there" + ); + + let finished = wallet1 + .sign( + &mut psbt, + SignOptions { + trust_witness_utxo: true, + ..Default::default() + }, + ) + .unwrap(); + + assert!( + !finished, + "only one of the inputs should have been signed so far" + ); + + let finished = wallet2 + .sign( + &mut psbt, + SignOptions { + trust_witness_utxo: true, + ..Default::default() + }, + ) + .unwrap(); + assert!(finished, "all the inputs should have been signed now"); +} + +#[test] +#[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")] +fn test_add_foreign_utxo_invalid_psbt_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let outpoint = wallet.list_unspent()[0].outpoint; + let foreign_utxo_satisfaction = wallet + .get_descriptor_for_keychain(KeychainKind::External) + .max_satisfaction_weight() + .unwrap(); + + let mut builder = wallet.build_tx(); + builder + .add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction) + .unwrap(); +} + +#[test] +fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { + let (mut wallet1, txid1) = get_funded_wallet(get_test_wpkh()); + let (wallet2, txid2) = + get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + + let utxo2 = wallet2.list_unspent().remove(0); + let tx1 = wallet1.get_tx(txid1, true).unwrap().transaction.unwrap(); + let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); + + let satisfaction_weight = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_satisfaction_weight() + .unwrap(); + + let mut builder = wallet1.build_tx(); + assert!( + builder + .add_foreign_utxo( + utxo2.outpoint, + psbt::Input { + non_witness_utxo: Some(tx1), + ..Default::default() + }, + satisfaction_weight + ) + .is_err(), + "should fail when outpoint doesn't match psbt_input" + ); + assert!( + builder + .add_foreign_utxo( + utxo2.outpoint, + psbt::Input { + non_witness_utxo: Some(tx2), + ..Default::default() + }, + satisfaction_weight + ) + .is_ok(), + "shoulld be ok when outpoint does match psbt_input" + ); +} + +#[test] +fn test_add_foreign_utxo_only_witness_utxo() { + let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); + let (wallet2, txid2) = + get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let utxo2 = wallet2.list_unspent().remove(0); + + let satisfaction_weight = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_satisfaction_weight() + .unwrap(); + + let mut builder = wallet1.build_tx(); + builder.add_recipient(addr.script_pubkey(), 60_000); + + { + let mut builder = builder.clone(); + let psbt_input = psbt::Input { + witness_utxo: Some(utxo2.txout.clone()), + ..Default::default() + }; + builder + .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) + .unwrap(); + assert!( + builder.finish().is_err(), + "psbt_input with witness_utxo should fail with only witness_utxo" + ); + } + + { + let mut builder = builder.clone(); + let psbt_input = psbt::Input { + witness_utxo: Some(utxo2.txout.clone()), + ..Default::default() + }; + builder + .only_witness_utxo() + .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) + .unwrap(); + assert!( + builder.finish().is_ok(), + "psbt_input with just witness_utxo should succeed when `only_witness_utxo` is enabled" + ); + } + + { + let mut builder = builder.clone(); + let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); + let psbt_input = psbt::Input { + non_witness_utxo: Some(tx2), + ..Default::default() + }; + builder + .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) + .unwrap(); + assert!( + builder.finish().is_ok(), + "psbt_input with non_witness_utxo should succeed by default" + ); + } +} + +#[test] +fn test_get_psbt_input() { + // this should grab a known good utxo and set the input + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + for utxo in wallet.list_unspent() { + let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap(); + assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some()); + } +} + +#[test] +#[should_panic( + expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")" +)] +fn test_create_tx_global_xpubs_origin_missing() { + let (mut wallet, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .add_global_xpubs(); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_global_xpubs_master_without_origin() { + use bitcoin::hashes::hex::FromHex; + use bitcoin::util::bip32; + + let (mut wallet, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .add_global_xpubs(); + let (psbt, _) = builder.finish().unwrap(); + + let key = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap(); + let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap(); + + assert_eq!(psbt.xpub.len(), 1); + assert_eq!( + psbt.xpub.get(&key), + Some(&(fingerprint, bip32::DerivationPath::default())) + ); +} + +#[test] +#[should_panic(expected = "IrreplaceableTransaction")] +fn test_bump_fee_irreplaceable_tx() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet.build_fee_bump(txid).unwrap().finish().unwrap(); +} + +#[test] +#[should_panic(expected = "TransactionConfirmed")] +fn test_bump_fee_confirmed_tx() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + let tx = psbt.extract_tx(); + let txid = tx.txid(); + + wallet + .insert_tx( + tx, + ConfirmationTime::Confirmed { + height: 42, + time: 42_000, + }, + ) + .unwrap(); + + wallet.build_fee_bump(txid).unwrap().finish().unwrap(); +} + +#[test] +#[should_panic(expected = "FeeRateTooLow")] +fn test_bump_fee_low_fee_rate() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, _) = builder.finish().unwrap(); + + let tx = psbt.extract_tx(); + let txid = tx.txid(); + + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(1.0)); + builder.finish().unwrap(); +} + +#[test] +#[should_panic(expected = "FeeTooLow")] +fn test_bump_fee_low_abs() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, _) = builder.finish().unwrap(); + + let tx = psbt.extract_tx(); + let txid = tx.txid(); + + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(10); + builder.finish().unwrap(); +} + +#[test] +#[should_panic(expected = "FeeTooLow")] +fn test_bump_fee_zero_abs() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, _) = builder.finish().unwrap(); + + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(0); + builder.finish().unwrap(); +} + +#[test] +fn test_bump_fee_reduce_change() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf(); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, original_details.sent); + assert_eq!( + details.received + details.fee.unwrap_or(0), + original_details.received + original_details.fee.unwrap_or(0) + ); + assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 25_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(200); + builder.enable_rbf(); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, original_details.sent); + assert_eq!( + details.received + details.fee.unwrap_or(0), + original_details.received + original_details.fee.unwrap_or(0) + ); + assert!( + details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0), + "{} > {}", + details.fee.unwrap_or(0), + original_details.fee.unwrap_or(0) + ); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 25_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_eq!(details.fee.unwrap_or(0), 200); +} + +#[test] +fn test_bump_fee_reduce_single_recipient() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .fee_rate(FeeRate::from_sat_per_vb(2.5)) + .allow_shrinking(addr.script_pubkey()) + .unwrap(); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, original_details.sent); + assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 1); + assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent); + + assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); +} + +#[test] +fn test_bump_fee_absolute_reduce_single_recipient() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .allow_shrinking(addr.script_pubkey()) + .unwrap() + .fee_absolute(300); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, original_details.sent); + assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 1); + assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent); + + assert_eq!(details.fee.unwrap_or(0), 300); +} + +#[test] +fn test_bump_fee_drain_wallet() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + // receive an extra tx so that our wallet has two utxos. + let tx = Transaction { + version: 1, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 25_000, + script_pubkey: wallet.get_address(New).script_pubkey(), + }], + }; + wallet + .insert_tx( + tx.clone(), + ConfirmationTime::Confirmed { + height: wallet.latest_checkpoint().unwrap().height, + time: 42_000, + }, + ) + .unwrap(); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(OutPoint { + txid: tx.txid(), + vout: 0, + }) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + assert_eq!(original_details.sent, 25_000); + + // for the new feerate, it should be enough to reduce the output, but since we specify + // `drain_wallet` we expect to spend everything + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .drain_wallet() + .allow_shrinking(addr.script_pubkey()) + .unwrap() + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + let (_, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, 75_000); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_bump_fee_remove_output_manually_selected_only() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + // receive an extra tx so that our wallet has two utxos. then we manually pick only one of + // them, and make sure that `bump_fee` doesn't try to add more. This fails because we've + // told the wallet it's not allowed to add more inputs AND it can't reduce the value of the + // existing output. In other words, bump_fee + manually_selected_only is always an error + // unless you've also set "allow_shrinking" OR there is a change output. + let init_tx = Transaction { + version: 1, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + script_pubkey: wallet.get_address(New).script_pubkey(), + value: 25_000, + }], + }; + wallet + .insert_tx(init_tx.clone(), wallet.transactions().last().unwrap().0) + .unwrap(); + let outpoint = OutPoint { + txid: init_tx.txid(), + vout: 0, + }; + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(outpoint) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + assert_eq!(original_details.sent, 25_000); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .manually_selected_only() + .fee_rate(FeeRate::from_sat_per_vb(255.0)); + builder.finish().unwrap(); +} + +#[test] +fn test_bump_fee_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let init_tx = Transaction { + version: 1, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + script_pubkey: wallet.get_address(New).script_pubkey(), + value: 25_000, + }], + }; + wallet + .insert_tx(init_tx.clone(), wallet.transactions().last().unwrap().0) + .unwrap(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, original_details.sent + 25_000); + assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); +} + +#[test] +fn test_bump_fee_absolute_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + receive_output_in_latest_block(&mut wallet, 25_000); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(6_000); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, original_details.sent + 25_000); + assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_eq!(details.fee.unwrap_or(0), 6_000); +} + +#[test] +fn test_bump_fee_no_change_add_input_and_change() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let op = receive_output_in_latest_block(&mut wallet, 25_000); + + // initially make a tx without change by using `drain_to` + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(op) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + // now bump the fees without using `allow_shrinking`. the wallet should add an + // extra input and a change output, and leave the original output untouched + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); + let (psbt, details) = builder.finish().unwrap(); + + let original_send_all_amount = original_details.sent - original_details.fee.unwrap_or(0); + assert_eq!(details.sent, original_details.sent + 50_000); + assert_eq!( + details.received, + 75_000 - original_send_all_amount - details.fee.unwrap_or(0) + ); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + original_send_all_amount + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + 75_000 - original_send_all_amount - details.fee.unwrap_or(0) + ); + + assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); +} + +#[test] +fn test_bump_fee_add_input_change_dust() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + receive_output_in_latest_block(&mut wallet, 25_000); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let mut tx = psbt.extract_tx(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realisitc weight + } + let original_tx_weight = tx.weight(); + assert_eq!(tx.input.len(), 1); + assert_eq!(tx.output.len(), 2); + let txid = tx.txid(); + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + // We set a fee high enough that during rbf we are forced to add + // a new input and also that we have to remove the change + // that we had previously + + // We calculate the new weight as: + // original weight + // + extra input weight: 160 WU = (32 (prevout) + 4 (vout) + 4 (nsequence)) * 4 + // + input satisfaction weight: 112 WU = 106 (witness) + 2 (witness len) + (1 (script len)) * 4 + // - change output weight: 124 WU = (8 (value) + 1 (script len) + 22 (script)) * 4 + let new_tx_weight = original_tx_weight + 160 + 112 - 124; + // two inputs (50k, 25k) and one output (45k) - epsilon + // We use epsilon here to avoid asking for a slightly too high feerate + let fee_abs = 50_000 + 25_000 - 45_000 - 10; + builder.fee_rate(FeeRate::from_wu(fee_abs, new_tx_weight)); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!( + original_details.received, + 5_000 - original_details.fee.unwrap_or(0) + ); + + assert_eq!(details.sent, original_details.sent + 25_000); + assert_eq!(details.fee.unwrap_or(0), 30_000); + assert_eq!(details.received, 0); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 1); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + + assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature); +} + +#[test] +fn test_bump_fee_force_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + wallet + .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed) + .unwrap(); + // the new fee_rate is low enough that just reducing the change would be fine, but we force + // the addition of an extra input with `add_utxo()` + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .add_utxo(incoming_op) + .unwrap() + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, original_details.sent + 25_000); + assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); +} + +#[test] +fn test_bump_fee_absolute_force_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, original_details) = builder.finish().unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + // skip saving the new utxos, we know they can't be used anyways + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + wallet + .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed) + .unwrap(); + + // the new fee_rate is low enough that just reducing the change would be fine, but we force + // the addition of an extra input with `add_utxo()` + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.add_utxo(incoming_op).unwrap().fee_absolute(250); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!(details.sent, original_details.sent + 25_000); + assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_eq!(details.fee.unwrap_or(0), 250); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_bump_fee_unconfirmed_inputs_only() { + // We try to bump the fee, but: + // - We can't reduce the change, as we have no change + // - All our UTXOs are unconfirmed + // So, we fail with "InsufficientFunds", as per RBF rule 2: + // The replacement transaction may only include an unconfirmed input + // if that input was included in one of the original transactions. + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_wallet() + .drain_to(addr.script_pubkey()) + .enable_rbf(); + let (psbt, __details) = builder.finish().unwrap(); + // Now we receive one transaction with 0 confirmations. We won't be able to use that for + // fee bumping, as it's still unconfirmed! + receive_output(&mut wallet, 25_000, TxHeight::Unconfirmed); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(25.0)); + builder.finish().unwrap(); +} + +#[test] +fn test_bump_fee_unconfirmed_input() { + // We create a tx draining the wallet and spending one confirmed + // and one unconfirmed UTXO. We check that we can fee bump normally + // (BIP125 rule 2 only apply to newly added unconfirmed input, you can + // always fee bump with an unconfirmed input if it was included in the + // original transaction) + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + // We receive a tx with 0 confirmations, which will be used as an input + // in the drain tx. + receive_output(&mut wallet, 25_000, TxHeight::Unconfirmed); + let mut builder = wallet.build_tx(); + builder + .drain_wallet() + .drain_to(addr.script_pubkey()) + .enable_rbf(); + let (psbt, _) = builder.finish().unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .fee_rate(FeeRate::from_sat_per_vb(15.0)) + .allow_shrinking(addr.script_pubkey()) + .unwrap(); + builder.finish().unwrap(); +} + +#[test] +fn test_fee_amount_negative_drain_val() { + // While building the transaction, bdk would calculate the drain_value + // as + // current_delta - fee_amount - drain_fee + // using saturating_sub, meaning that if the result would end up negative, + // it'll remain to zero instead. + // This caused a bug in master where we would calculate the wrong fee + // for a transaction. + // See https://github.com/bitcoindevkit/bdk/issues/660 + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap(); + let fee_rate = FeeRate::from_sat_per_vb(2.01); + let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); + + let mut builder = wallet.build_tx(); + builder + .add_recipient(send_to.script_pubkey(), 8630) + .add_utxo(incoming_op) + .unwrap() + .enable_rbf() + .fee_rate(fee_rate); + let (psbt, details) = builder.finish().unwrap(); + + assert!(psbt.inputs.len() == 1); + assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate, @add_signature); +} + +#[test] +fn test_sign_single_xprv() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx(); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_xprv_with_master_fingerprint_and_path() { + let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx(); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_xprv_bip44_path() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx(); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_xprv_sh_wpkh() { + let (mut wallet, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx(); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_wif() { + let (mut wallet, _) = + get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx(); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_xprv_no_hd_keypaths() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + psbt.inputs[0].bip32_derivation.clear(); + assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx(); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_include_output_redeem_witness_script() { + let (mut wallet, _) = get_funded_wallet("sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .include_output_redeem_witness_script(); + let (psbt, _) = builder.finish().unwrap(); + + // p2sh-p2wsh transaction should contain both witness and redeem scripts + assert!(psbt + .outputs + .iter() + .any(|output| output.redeem_script.is_some() && output.witness_script.is_some())); +} + +#[test] +fn test_signing_only_one_of_multiple_inputs() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .include_output_redeem_witness_script(); + let (mut psbt, _) = builder.finish().unwrap(); + + // add another input to the psbt that is at least passable. + let dud_input = bitcoin::util::psbt::Input { + witness_utxo: Some(TxOut { + value: 100_000, + script_pubkey: miniscript::Descriptor::::from_str( + "wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)", + ) + .unwrap() + .script_pubkey(), + }), + ..Default::default() + }; + + psbt.inputs.push(dud_input); + psbt.unsigned_tx.input.push(bitcoin::TxIn::default()); + let is_final = wallet + .sign( + &mut psbt, + SignOptions { + trust_witness_utxo: true, + ..Default::default() + }, + ) + .unwrap(); + assert!( + !is_final, + "shouldn't be final since we can't sign one of the inputs" + ); + assert!( + psbt.inputs[0].final_script_witness.is_some(), + "should finalized input it signed" + ) +} + +#[test] +fn test_remove_partial_sigs_after_finalize_sign_option() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + + for remove_partial_sigs in &[true, false] { + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap().0; + + assert!(wallet + .sign( + &mut psbt, + SignOptions { + remove_partial_sigs: *remove_partial_sigs, + ..Default::default() + }, + ) + .unwrap()); + + psbt.inputs.iter().for_each(|input| { + if *remove_partial_sigs { + assert!(input.partial_sigs.is_empty()) + } else { + assert!(!input.partial_sigs.is_empty()) + } + }); + } +} + +#[test] +fn test_try_finalize_sign_option() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + + for try_finalize in &[true, false] { + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap().0; + + let finalized = wallet + .sign( + &mut psbt, + SignOptions { + try_finalize: *try_finalize, + ..Default::default() + }, + ) + .unwrap(); + + psbt.inputs.iter().for_each(|input| { + if *try_finalize { + assert!(finalized); + assert!(input.final_script_sig.is_some()); + assert!(input.final_script_witness.is_some()); + } else { + assert!(!finalized); + assert!(input.final_script_sig.is_none()); + assert!(input.final_script_witness.is_none()); + } + }); + } +} + +#[test] +fn test_sign_nonstandard_sighash() { + let sighash = EcdsaSighashType::NonePlusAnyoneCanPay; + + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .sighash(sighash.into()) + .drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!( + result.is_err(), + "Signing should have failed because the TX uses non-standard sighashes" + ); + assert_matches!( + result, + Err(bdk::Error::Signer(SignerError::NonStandardSighash)), + "Signing failed with the wrong error type" + ); + + // try again after opting-in + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + assert!(result.is_ok(), "Signing should have worked"); + assert!( + result.unwrap(), + "Should finalize the input since we can produce signatures" + ); + + let extracted = psbt.extract_tx(); + assert_eq!( + *extracted.input[0].witness.to_vec()[0].last().unwrap(), + sighash.to_u32() as u8, + "The signature should have been made with the right sighash" + ); +} + +#[test] +fn test_unused_address() { + let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + None, Network::Testnet).unwrap(); + + assert_eq!( + wallet.get_address(LastUnused).to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + assert_eq!( + wallet.get_address(LastUnused).to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); +} + +#[test] +fn test_next_unused_address() { + let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; + let mut wallet = Wallet::new(descriptor, None, Network::Testnet).unwrap(); + assert_eq!(wallet.derivation_index(KeychainKind::External), None); + + assert_eq!( + wallet.get_address(LastUnused).to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); + assert_eq!( + wallet.get_address(LastUnused).to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); + + // use the above address + receive_output_in_latest_block(&mut wallet, 25_000); + + assert_eq!( + wallet.get_address(LastUnused).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + assert_eq!(wallet.derivation_index(KeychainKind::External), Some(1)); +} + +#[test] +fn test_peek_address_at_index() { + let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + None, Network::Testnet).unwrap(); + + assert_eq!( + wallet.get_address(Peek(1)).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + + assert_eq!( + wallet.get_address(Peek(0)).to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + + assert_eq!( + wallet.get_address(Peek(2)).to_string(), + "tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2" + ); + + // current new address is not affected + assert_eq!( + wallet.get_address(New).to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + + assert_eq!( + wallet.get_address(New).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); +} + +#[test] +fn test_peek_address_at_index_not_derivable() { + let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", + None, Network::Testnet).unwrap(); + + assert_eq!( + wallet.get_address(Peek(1)).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + + assert_eq!( + wallet.get_address(Peek(0)).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + + assert_eq!( + wallet.get_address(Peek(2)).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); +} + +#[test] +fn test_returns_index_and_address() { + let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + None, Network::Testnet).unwrap(); + + // new index 0 + assert_eq!( + wallet.get_address(New), + AddressInfo { + index: 0, + address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a").unwrap(), + keychain: KeychainKind::External, + } + ); + + // new index 1 + assert_eq!( + wallet.get_address(New), + AddressInfo { + index: 1, + address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7").unwrap(), + keychain: KeychainKind::External, + } + ); + + // peek index 25 + assert_eq!( + wallet.get_address(Peek(25)), + AddressInfo { + index: 25, + address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2").unwrap(), + keychain: KeychainKind::External, + } + ); + + // new index 2 + assert_eq!( + wallet.get_address(New), + AddressInfo { + index: 2, + address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2").unwrap(), + keychain: KeychainKind::External, + } + ); +} + +#[test] +fn test_sending_to_bip350_bech32m_address() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c") + .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 45_000); + builder.finish().unwrap(); +} + +#[test] +fn test_get_address() { + use bdk::descriptor::template::Bip84; + let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let mut wallet = Wallet::new( + Bip84(key, KeychainKind::External), + Some(Bip84(key, KeychainKind::Internal)), + Network::Regtest, + ) + .unwrap(); + + assert_eq!( + wallet.get_address(AddressIndex::New), + AddressInfo { + index: 0, + address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w").unwrap(), + keychain: KeychainKind::External, + } + ); + + assert_eq!( + wallet.get_internal_address(AddressIndex::New), + AddressInfo { + index: 0, + address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79").unwrap(), + keychain: KeychainKind::Internal, + } + ); + + let mut wallet = + Wallet::new(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); + + assert_eq!( + wallet.get_internal_address(AddressIndex::New), + AddressInfo { + index: 0, + address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w").unwrap(), + keychain: KeychainKind::Internal, + }, + "when there's no internal descriptor it should just use external" + ); +} + +#[test] +fn test_get_address_no_reuse_single_descriptor() { + use bdk::descriptor::template::Bip84; + use std::collections::HashSet; + + let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let mut wallet = + Wallet::new(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); + + let mut used_set = HashSet::new(); + + (0..3).for_each(|_| { + let external_addr = wallet.get_address(AddressIndex::New).address; + assert!(used_set.insert(external_addr)); + + let internal_addr = wallet.get_internal_address(AddressIndex::New).address; + assert!(used_set.insert(internal_addr)); + }); +} + +#[test] +fn test_taproot_psbt_populate_tap_key_origins() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); + let addr = wallet.get_address(AddressIndex::New); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(), + vec![( + from_str!("b96d3a3dc76a4fc74e976511b23aecb78e0754c23c0ed7a6513e18cbbc7178e9"), + (vec![], (from_str!("f6a5cb8b"), from_str!("m/0"))) + )], + "Wrong input tap_key_origins" + ); + assert_eq!( + psbt.outputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(), + vec![( + from_str!("e9b03068cf4a2621d4f81e68f6c4216e6bd260fe6edf6acc55c8d8ae5aeff0a8"), + (vec![], (from_str!("f6a5cb8b"), from_str!("m/1"))) + )], + "Wrong output tap_key_origins" + ); +} + +#[test] +fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_repeated_key()); + let addr = wallet.get_address(AddressIndex::New); + + let path = vec![("e5mmg3xh".to_string(), vec![0])] + .into_iter() + .collect(); + + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .policy_path(path, KeychainKind::External); + let (psbt, _) = builder.finish().unwrap(); + + let mut input_key_origins = psbt.inputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(); + input_key_origins.sort(); + + assert_eq!( + input_key_origins, + vec![ + ( + from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"), + ( + vec![], + (FromStr::from_str("871fd295").unwrap(), vec![].into()) + ) + ), + ( + from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"), + ( + vec![ + from_str!( + "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e" + ), + from_str!( + "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903" + ), + ], + (FromStr::from_str("ece52657").unwrap(), vec![].into()) + ) + ) + ], + "Wrong input tap_key_origins" + ); + + let mut output_key_origins = psbt.outputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(); + output_key_origins.sort(); + + assert_eq!( + input_key_origins, output_key_origins, + "Wrong output tap_key_origins" + ); +} + +#[test] +fn test_taproot_psbt_input_tap_tree() { + use bdk::bitcoin::psbt::serialize::Deserialize; + use bdk::bitcoin::psbt::TapTree; + use bitcoin::hashes::hex::FromHex; + use bitcoin::util::taproot; + + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); + let addr = wallet.get_address(AddressIndex::Peek(0)); + + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0].tap_merkle_root, + Some( + FromHex::from_hex("61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986") + .unwrap() + ), + ); + assert_eq!( + psbt.inputs[0].tap_scripts.clone().into_iter().collect::>(), + vec![ + (taproot::ControlBlock::from_slice(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (from_str!("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac"), taproot::LeafVersion::TapScript)), + (taproot::ControlBlock::from_slice(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (from_str!("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac"), taproot::LeafVersion::TapScript)), + ], + ); + assert_eq!( + psbt.inputs[0].tap_internal_key, + Some(from_str!( + "b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55" + )) + ); + + // Since we are creating an output to the same address as the input, assert that the + // internal_key is the same + assert_eq!( + psbt.inputs[0].tap_internal_key, + psbt.outputs[0].tap_internal_key + ); + + assert_eq!( + psbt.outputs[0].tap_tree, + Some(TapTree::deserialize(&Vec::::from_hex("01c022208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac01c0222051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",).unwrap()).unwrap()) + ); +} + +#[test] +fn test_taproot_sign_missing_witness_utxo() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + let witness_utxo = psbt.inputs[0].witness_utxo.take(); + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + assert_matches!( + result, + Err(Error::Signer(SignerError::MissingWitnessUtxo)), + "Signing should have failed with the correct error because the witness_utxo is missing" + ); + + // restore the witness_utxo + psbt.inputs[0].witness_utxo = witness_utxo; + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + + assert_matches!( + result, + Ok(true), + "Should finalize the input since we can produce signatures" + ); +} + +#[test] +fn test_taproot_sign_using_non_witness_utxo() { + let (mut wallet, prev_txid) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + psbt.inputs[0].witness_utxo = None; + psbt.inputs[0].non_witness_utxo = wallet.get_tx(prev_txid, true).unwrap().transaction; + assert!( + psbt.inputs[0].non_witness_utxo.is_some(), + "Previous tx should be present in the database" + ); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!(result.is_ok(), "Signing should have worked"); + assert!( + result.unwrap(), + "Should finalize the input since we can produce signatures" + ); +} + +#[test] +fn test_taproot_foreign_utxo() { + let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); + let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig()); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let utxo = wallet2.list_unspent().remove(0); + let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); + let foreign_utxo_satisfaction = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_satisfaction_weight() + .unwrap(); + + assert!( + psbt_input.non_witness_utxo.is_none(), + "`non_witness_utxo` should never be populated for taproot" + ); + + let mut builder = wallet1.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 60_000) + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!( + details.sent - details.received, + 10_000 + details.fee.unwrap_or(0), + "we should have only net spent ~10_000" + ); + + assert!( + psbt.unsigned_tx + .input + .iter() + .any(|input| input.previous_output == utxo.outpoint), + "foreign_utxo should be in there" + ); +} + +fn test_spend_from_wallet(mut wallet: Wallet) { + let addr = wallet.get_address(AddressIndex::New); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + + assert!( + wallet.sign(&mut psbt, Default::default()).unwrap(), + "Unable to finalize tx" + ); +} + +// #[test] +// fn test_taproot_key_spend() { +// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); +// test_spend_from_wallet(wallet); + +// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); +// test_spend_from_wallet(wallet); +// } + +#[test] +fn test_taproot_no_key_spend() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.get_address(AddressIndex::New); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + + assert!( + wallet + .sign( + &mut psbt, + SignOptions { + sign_with_tap_internal_key: false, + ..Default::default() + }, + ) + .unwrap(), + "Unable to finalize tx" + ); + + assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none())); +} + +#[test] +fn test_taproot_script_spend() { + let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); + test_spend_from_wallet(wallet); + + let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree_xprv()); + test_spend_from_wallet(wallet); +} + +#[test] +fn test_taproot_script_spend_sign_all_leaves() { + use bdk::signer::TapLeavesOptions; + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.get_address(AddressIndex::New); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + + assert!( + wallet + .sign( + &mut psbt, + SignOptions { + tap_leaves_options: TapLeavesOptions::All, + ..Default::default() + }, + ) + .unwrap(), + "Unable to finalize tx" + ); + + assert!(psbt + .inputs + .iter() + .all(|i| i.tap_script_sigs.len() == i.tap_scripts.len())); +} + +#[test] +fn test_taproot_script_spend_sign_include_some_leaves() { + use bdk::signer::TapLeavesOptions; + use bitcoin::util::taproot::TapLeafHash; + + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.get_address(AddressIndex::New); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + let mut script_leaves: Vec<_> = psbt.inputs[0] + .tap_scripts + .clone() + .values() + .map(|(script, version)| TapLeafHash::from_script(script, *version)) + .collect(); + let included_script_leaves = vec![script_leaves.pop().unwrap()]; + let excluded_script_leaves = script_leaves; + + assert!( + wallet + .sign( + &mut psbt, + SignOptions { + tap_leaves_options: TapLeavesOptions::Include(included_script_leaves.clone()), + ..Default::default() + }, + ) + .unwrap(), + "Unable to finalize tx" + ); + + assert!(psbt.inputs[0] + .tap_script_sigs + .iter() + .all(|s| included_script_leaves.contains(&s.0 .1) + && !excluded_script_leaves.contains(&s.0 .1))); +} + +#[test] +fn test_taproot_script_spend_sign_exclude_some_leaves() { + use bdk::signer::TapLeavesOptions; + use bitcoin::util::taproot::TapLeafHash; + + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.get_address(AddressIndex::New); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + let mut script_leaves: Vec<_> = psbt.inputs[0] + .tap_scripts + .clone() + .values() + .map(|(script, version)| TapLeafHash::from_script(script, *version)) + .collect(); + let included_script_leaves = vec![script_leaves.pop().unwrap()]; + let excluded_script_leaves = script_leaves; + + assert!( + wallet + .sign( + &mut psbt, + SignOptions { + tap_leaves_options: TapLeavesOptions::Exclude(excluded_script_leaves.clone()), + ..Default::default() + }, + ) + .unwrap(), + "Unable to finalize tx" + ); + + assert!(psbt.inputs[0] + .tap_script_sigs + .iter() + .all(|s| included_script_leaves.contains(&s.0 .1) + && !excluded_script_leaves.contains(&s.0 .1))); +} + +#[test] +fn test_taproot_script_spend_sign_no_leaves() { + use bdk::signer::TapLeavesOptions; + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.get_address(AddressIndex::New); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + + wallet + .sign( + &mut psbt, + SignOptions { + tap_leaves_options: TapLeavesOptions::None, + ..Default::default() + }, + ) + .unwrap(); + + assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty())); +} + +#[test] +fn test_taproot_sign_derive_index_from_psbt() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); + + let addr = wallet.get_address(AddressIndex::New); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + + // re-create the wallet with an empty db + let wallet_empty = Wallet::new(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap(); + + // signing with an empty db means that we will only look at the psbt to infer the + // derivation index + assert!( + wallet_empty.sign(&mut psbt, Default::default()).unwrap(), + "Unable to finalize tx" + ); +} + +#[test] +fn test_taproot_sign_explicit_sighash_all() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .sighash(SchnorrSighashType::All.into()) + .drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!( + result.is_ok(), + "Signing should work because SIGHASH_ALL is safe" + ) +} + +#[test] +fn test_taproot_sign_non_default_sighash() { + let sighash = SchnorrSighashType::NonePlusAnyoneCanPay; + + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .sighash(sighash.into()) + .drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let witness_utxo = psbt.inputs[0].witness_utxo.take(); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!( + result.is_err(), + "Signing should have failed because the TX uses non-standard sighashes" + ); + assert_matches!( + result, + Err(Error::Signer(SignerError::NonStandardSighash)), + "Signing failed with the wrong error type" + ); + + // try again after opting-in + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + assert!( + result.is_err(), + "Signing should have failed because the witness_utxo is missing" + ); + assert_matches!( + result, + Err(Error::Signer(SignerError::MissingWitnessUtxo)), + "Signing failed with the wrong error type" + ); + + // restore the witness_utxo + psbt.inputs[0].witness_utxo = witness_utxo; + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + + assert!(result.is_ok(), "Signing should have worked"); + assert!( + result.unwrap(), + "Should finalize the input since we can produce signatures" + ); + + let extracted = psbt.extract_tx(); + assert_eq!( + *extracted.input[0].witness.to_vec()[0].last().unwrap(), + sighash as u8, + "The signature should have been made with the right sighash" + ); +} + +#[test] +fn test_spend_coinbase() { + let descriptor = get_test_wpkh(); + let mut wallet = Wallet::new(descriptor, None, Network::Regtest).unwrap(); + + let confirmation_height = 5; + wallet + .insert_checkpoint(BlockId { + height: confirmation_height, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + let coinbase_tx = Transaction { + version: 1, + lock_time: bitcoin::PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut { + value: 25_000, + script_pubkey: wallet.get_address(New).address.script_pubkey(), + }], + }; + wallet + .insert_tx( + coinbase_tx, + ConfirmationTime::Confirmed { + height: confirmation_height, + time: 30_000, + }, + ) + .unwrap(); + + let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1; + let maturity_time = confirmation_height + COINBASE_MATURITY; + + let balance = wallet.get_balance(); + assert_eq!( + balance, + Balance { + immature: 25_000, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 0 + } + ); + + // We try to create a transaction, only to notice that all + // our funds are unspendable + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), balance.immature / 2) + .current_height(confirmation_height); + assert!(matches!( + builder.finish(), + Err(Error::InsufficientFunds { + needed: _, + available: 0 + }) + )); + + // Still unspendable... + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), balance.immature / 2) + .current_height(not_yet_mature_time); + assert_matches!( + builder.finish(), + Err(Error::InsufficientFunds { + needed: _, + available: 0 + }) + ); + + wallet + .insert_checkpoint(BlockId { + height: maturity_time, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + let balance = wallet.get_balance(); + assert_eq!( + balance, + Balance { + immature: 0, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 25_000 + } + ); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), balance.confirmed / 2) + .current_height(maturity_time); + builder.finish().unwrap(); +} + +#[test] +fn test_allow_dust_limit() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + + let addr = wallet.get_address(New); + + let mut builder = wallet.build_tx(); + + builder.add_recipient(addr.script_pubkey(), 0); + + assert_matches!(builder.finish(), Err(Error::OutputBelowDustLimit(0))); + + let mut builder = wallet.build_tx(); + + builder + .allow_dust(true) + .add_recipient(addr.script_pubkey(), 0); + + assert!(builder.finish().is_ok()); +} + +#[test] +fn test_fee_rate_sign_no_grinding_high_r() { + // Our goal is to obtain a transaction with a signature with high-R (71 bytes + // instead of 70). We then check that our fee rate and fee calculation is + // alright. + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.get_address(New); + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let mut builder = wallet.build_tx(); + let mut data = vec![0]; + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_rate(fee_rate) + .add_data(&data); + let (mut psbt, details) = builder.finish().unwrap(); + let (op_return_vout, _) = psbt + .unsigned_tx + .output + .iter() + .enumerate() + .find(|(_n, i)| i.script_pubkey.is_op_return()) + .unwrap(); + + let mut sig_len: usize = 0; + // We try to sign many different times until we find a longer signature (71 bytes) + while sig_len < 71 { + // Changing the OP_RETURN data will make the signature change (but not the fee, until + // data[0] is small enough) + data[0] += 1; + psbt.unsigned_tx.output[op_return_vout].script_pubkey = Script::new_op_return(&data); + // Clearing the previous signature + psbt.inputs[0].partial_sigs.clear(); + // Signing + wallet + .sign( + &mut psbt, + SignOptions { + remove_partial_sigs: false, + try_finalize: false, + allow_grinding: false, + ..Default::default() + }, + ) + .unwrap(); + // We only have one key in the partial_sigs map, this is a trick to retrieve it + let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); + sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len(); + } + // Actually finalizing the transaction... + wallet + .sign( + &mut psbt, + SignOptions { + remove_partial_sigs: false, + allow_grinding: false, + ..Default::default() + }, + ) + .unwrap(); + // ...and checking that everything is fine + assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate); +} + +#[test] +fn test_fee_rate_sign_grinding_low_r() { + // Our goal is to obtain a transaction with a signature with low-R (70 bytes) + // by setting the `allow_grinding` signing option as true. + // We then check that our fee rate and fee calculation is alright and that our + // signature is 70 bytes. + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.get_address(New); + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_rate(fee_rate); + let (mut psbt, details) = builder.finish().unwrap(); + + wallet + .sign( + &mut psbt, + SignOptions { + remove_partial_sigs: false, + allow_grinding: true, + ..Default::default() + }, + ) + .unwrap(); + + let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); + let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len(); + assert_eq!(sig_len, 70); + assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate); +} + +#[cfg(feature = "test-hardware-signer")] +#[test] +fn test_create_signer() { + use std::sync::Arc; + + use bdk::signer::SignerOrdering; + use bdk::wallet::hardwaresigner::HWISigner; + use hwi::types::HWIChain; + use hwi::HWIClient; + + let mut devices = HWIClient::enumerate().unwrap(); + if devices.is_empty() { + panic!("No devices found!"); + } + let device = devices.remove(0).unwrap(); + let client = HWIClient::get_client(&device, true, HWIChain::Regtest).unwrap(); + let descriptors = client.get_descriptors::(None).unwrap(); + let custom_signer = HWISigner::from_device(&device, HWIChain::Regtest).unwrap(); + + let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]); + wallet.add_signer( + KeychainKind::External, + SignerOrdering(200), + Arc::new(custom_signer), + ); + + let addr = wallet.get_address(LastUnused); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); +} + +#[test] +fn test_taproot_load_descriptor_duplicated_keys() { + // Added after issue https://github.com/bitcoindevkit/bdk/issues/760 + // + // Having the same key in multiple taproot leaves is safe and should be accepted by BDK + + let (mut wallet, _) = get_funded_wallet(get_test_tr_dup_keys()); + let addr = wallet.get_address(New); + + assert_eq!( + addr.to_string(), + "bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5" + ); +}