From 22e28b2f8d4a2011ea97deac90771a27b82acb53 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Wed, 19 Mar 2025 20:28:40 -0400 Subject: [PATCH] test: Move bump fee + foreign utxo tests These files added to wallet/tests directory - add_foreign_utxo.rs - build_fee_bump.rs - common.rs --- wallet/tests/add_foreign_utxo.rs | 292 +++++++ wallet/tests/build_fee_bump.rs | 866 +++++++++++++++++++ wallet/tests/common.rs | 69 ++ wallet/tests/wallet.rs | 1394 +++--------------------------- 4 files changed, 1325 insertions(+), 1296 deletions(-) create mode 100644 wallet/tests/add_foreign_utxo.rs create mode 100644 wallet/tests/build_fee_bump.rs create mode 100644 wallet/tests/common.rs diff --git a/wallet/tests/add_foreign_utxo.rs b/wallet/tests/add_foreign_utxo.rs new file mode 100644 index 00000000..438d6788 --- /dev/null +++ b/wallet/tests/add_foreign_utxo.rs @@ -0,0 +1,292 @@ +use bdk_wallet::psbt::PsbtUtils; +use bdk_wallet::signer::SignOptions; +use bdk_wallet::test_utils::*; +use bdk_wallet::tx_builder::AddForeignUtxoError; +use bdk_wallet::KeychainKind; +use bitcoin::psbt; +use bitcoin::{Address, Amount}; +use std::str::FromStr; + +mod common; + +#[test] +fn test_add_foreign_utxo() { + let (mut wallet1, _) = get_funded_wallet_wpkh(); + let (wallet2, _) = + get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let utxo = wallet2.list_unspent().next().expect("must take!"); + let foreign_utxo_satisfaction = wallet2 + .public_descriptor(KeychainKind::External) + .max_weight_to_satisfy() + .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(), Amount::from_sat(60_000)) + .only_witness_utxo() + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let mut psbt = builder.finish().unwrap(); + wallet1.insert_txout(utxo.outpoint, utxo.txout); + let fee = check_fee!(wallet1, psbt); + let sent_received = + wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + + assert_eq!( + (sent_received.0 - sent_received.1), + Amount::from_sat(10_000) + fee.unwrap_or(Amount::ZERO), + "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] +fn test_calculate_fee_with_missing_foreign_utxo() { + use bdk_chain::tx_graph::CalculateFeeError; + let (mut wallet1, _) = get_funded_wallet_wpkh(); + let (wallet2, _) = + get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let utxo = wallet2.list_unspent().next().expect("must take!"); + let foreign_utxo_satisfaction = wallet2 + .public_descriptor(KeychainKind::External) + .max_weight_to_satisfy() + .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(), Amount::from_sat(60_000)) + .only_witness_utxo() + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let res = wallet1.calculate_fee(&tx); + assert!( + matches!(res, Err(CalculateFeeError::MissingTxOut(outpoints)) if outpoints[0] == utxo.outpoint) + ); +} + +#[test] +fn test_add_foreign_utxo_invalid_psbt_input() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let outpoint = wallet.list_unspent().next().expect("must exist").outpoint; + let foreign_utxo_satisfaction = wallet + .public_descriptor(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + let mut builder = wallet.build_tx(); + let result = + builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction); + assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo))); +} + +#[test] +fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { + let (mut wallet1, txid1) = get_funded_wallet_wpkh(); + let (wallet2, txid2) = + get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + + let utxo2 = wallet2.list_unspent().next().unwrap(); + let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); + let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); + + let satisfaction_weight = wallet2 + .public_descriptor(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + let mut builder = wallet1.build_tx(); + assert!( + builder + .add_foreign_utxo( + utxo2.outpoint, + psbt::Input { + non_witness_utxo: Some(tx1.as_ref().clone()), + ..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.as_ref().clone()), + ..Default::default() + }, + satisfaction_weight + ) + .is_ok(), + "should be ok when outpoint does match psbt_input" + ); +} + +#[test] +fn test_add_foreign_utxo_only_witness_utxo() { + let (mut wallet1, _) = get_funded_wallet_wpkh(); + let (wallet2, txid2) = + get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let utxo2 = wallet2.list_unspent().next().unwrap(); + + let satisfaction_weight = wallet2 + .public_descriptor(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + { + let mut builder = wallet1.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); + + 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 = wallet1.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); + + 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 = wallet1.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); + + let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx; + let psbt_input = psbt::Input { + non_witness_utxo: Some(tx2.as_ref().clone()), + ..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_taproot_foreign_utxo() { + let (mut wallet1, _) = get_funded_wallet_wpkh(); + let (wallet2, _) = get_funded_wallet_single(get_test_tr_single_sig()); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let utxo = wallet2.list_unspent().next().unwrap(); + let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); + let foreign_utxo_satisfaction = wallet2 + .public_descriptor(KeychainKind::External) + .max_weight_to_satisfy() + .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(), Amount::from_sat(60_000)) + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + wallet1.insert_txout(utxo.outpoint, utxo.txout); + let fee = check_fee!(wallet1, psbt); + + assert_eq!( + sent_received.0 - sent_received.1, + Amount::from_sat(10_000) + fee.unwrap_or(Amount::ZERO), + "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" + ); +} diff --git a/wallet/tests/build_fee_bump.rs b/wallet/tests/build_fee_bump.rs new file mode 100644 index 00000000..9a59a19f --- /dev/null +++ b/wallet/tests/build_fee_bump.rs @@ -0,0 +1,866 @@ +use assert_matches::assert_matches; +use bdk_chain::{ChainPosition, ConfirmationBlockTime}; +use bdk_wallet::coin_selection::LargestFirstCoinSelection; +use bdk_wallet::error::CreateTxError; +use bdk_wallet::psbt::PsbtUtils; +use bdk_wallet::test_utils::*; +use bdk_wallet::KeychainKind; +use bitcoin::{ + absolute, transaction, Address, Amount, FeeRate, OutPoint, Sequence, Transaction, TxOut, Weight, +}; +use common::P2WPKH_FAKE_WITNESS_SIZE; +use std::str::FromStr; + +mod common; + +#[test] +#[should_panic(expected = "IrreplaceableTransaction")] +fn test_bump_fee_irreplaceable_tx() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + builder.set_exact_sequence(Sequence(0xFFFFFFFE)); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + 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_wpkh(); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + + insert_tx(&mut wallet, tx); + + let anchor = ConfirmationBlockTime { + block_id: wallet.latest_checkpoint().get(42).unwrap().block_id(), + confirmation_time: 42_000, + }; + insert_anchor(&mut wallet, txid, anchor); + + wallet.build_fee_bump(txid).unwrap().finish().unwrap(); +} + +#[test] +fn test_bump_fee_low_fee_rate() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + + let psbt = builder.finish().unwrap(); + let feerate = psbt.fee_rate().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::BROADCAST_MIN); + let res = builder.finish(); + assert_matches!( + res, + Err(CreateTxError::FeeRateTooLow { .. }), + "expected FeeRateTooLow error" + ); + + let required = feerate.to_sat_per_kwu() + 250; // +1 sat/vb + let sat_vb = required as f64 / 250.0; + let expect = format!("Fee rate too low: required {} sat/vb", sat_vb); + assert_eq!(res.unwrap_err().to_string(), expect); +} + +#[test] +#[should_panic(expected = "FeeTooLow")] +fn test_bump_fee_low_abs() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(Amount::from_sat(10)); + builder.finish().unwrap(); +} + +#[test] +#[should_panic(expected = "FeeTooLow")] +fn test_bump_fee_zero_abs() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(Amount::ZERO); + builder.finish().unwrap(); +} + +#[test] +fn test_bump_fee_reduce_change() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + let original_sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let original_fee = check_fee!(wallet, psbt); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(feerate); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0); + assert_eq!( + sent_received.1 + fee.unwrap_or(Amount::ZERO), + original_sent_received.1 + original_fee.unwrap_or(Amount::ZERO) + ); + assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); + + 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, + Amount::from_sat(25_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), feerate, @add_signature); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(Amount::from_sat(200)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0); + assert_eq!( + sent_received.1 + fee.unwrap_or(Amount::ZERO), + original_sent_received.1 + original_fee.unwrap_or(Amount::ZERO) + ); + assert!( + fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO), + "{} > {}", + fee.unwrap_or(Amount::ZERO), + original_fee.unwrap_or(Amount::ZERO) + ); + + 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, + Amount::from_sat(25_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(200)); +} + +#[test] +fn test_bump_fee_reduce_single_recipient() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + let tx = psbt.clone().extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let original_fee = check_fee!(wallet, psbt); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .fee_rate(feerate) + // remove original tx drain_to address and amount + .set_recipients(Vec::new()) + // set back original drain_to address + .drain_to(addr.script_pubkey()) + // drain wallet output amount will be re-calculated with new fee rate + .drain_wallet(); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0); + assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 1); + assert_eq!( + tx.output[0].value + fee.unwrap_or(Amount::ZERO), + sent_received.0 + ); + + assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), feerate, @add_signature); +} + +#[test] +fn test_bump_fee_absolute_reduce_single_recipient() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + let original_fee = check_fee!(wallet, psbt); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .fee_absolute(Amount::from_sat(300)) + // remove original tx drain_to address and amount + .set_recipients(Vec::new()) + // set back original drain_to address + .drain_to(addr.script_pubkey()) + // drain wallet output amount will be re-calculated with new fee rate + .drain_wallet(); + let psbt = builder.finish().unwrap(); + let tx = &psbt.unsigned_tx; + let sent_received = wallet.sent_and_received(tx); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0); + assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); + + assert_eq!(tx.output.len(), 1); + assert_eq!( + tx.output[0].value + fee.unwrap_or(Amount::ZERO), + sent_received.0 + ); + + assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(300)); +} + +#[test] +fn test_bump_fee_drain_wallet() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + // receive an extra tx so that our wallet has two utxos. + let tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + }; + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx.clone()); + let anchor = ConfirmationBlockTime { + block_id: wallet.latest_checkpoint().block_id(), + confirmation_time: 42_000, + }; + insert_anchor(&mut wallet, txid, anchor); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(OutPoint { + txid: tx.compute_txid(), + vout: 0, + }) + .unwrap() + .manually_selected_only(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + assert_eq!(original_sent_received.0, Amount::from_sat(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() + .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.extract_tx().expect("failed to extract tx")); + + assert_eq!(sent_received.0, Amount::from_sat(75_000)); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_bump_fee_remove_output_manually_selected_only() { + let (mut wallet, _) = get_funded_wallet_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 there is a change output. + let init_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + }; + + let position: ChainPosition = + wallet.transactions().last().unwrap().chain_position; + insert_tx(&mut wallet, init_tx.clone()); + match position { + ChainPosition::Confirmed { anchor, .. } => { + insert_anchor(&mut wallet, init_tx.compute_txid(), anchor) + } + other => panic!("all wallet txs must be confirmed: {:?}", other), + } + + let outpoint = OutPoint { + txid: init_tx.compute_txid(), + vout: 0, + }; + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(outpoint) + .unwrap() + .manually_selected_only(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + assert_eq!(original_sent_received.0, Amount::from_sat(25_000)); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .manually_selected_only() + .fee_rate(FeeRate::from_sat_per_vb_unchecked(255)); + builder.finish().unwrap(); +} + +#[test] +fn test_bump_fee_add_input() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let init_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + }; + let txid = init_tx.compute_txid(); + let pos: ChainPosition = + wallet.transactions().last().unwrap().chain_position; + insert_tx(&mut wallet, init_tx); + match pos { + ChainPosition::Confirmed { anchor, .. } => insert_anchor(&mut wallet, txid, anchor), + other => panic!("all wallet txs must be confirmed: {:?}", other), + } + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_details = wallet.sent_and_received(&tx); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + assert_eq!( + sent_received.0, + original_details.0 + Amount::from_sat(25_000) + ); + assert_eq!( + fee.unwrap_or(Amount::ZERO) + sent_received.1, + Amount::from_sat(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, + Amount::from_sat(45_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); +} + +#[test] +fn test_bump_fee_absolute_add_input() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + receive_output_in_latest_block(&mut wallet, 25_000); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(Amount::from_sat(6_000)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(25_000) + ); + assert_eq!( + fee.unwrap_or(Amount::ZERO) + sent_received.1, + Amount::from_sat(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, + Amount::from_sat(45_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(6_000)); +} + +#[test] +fn test_bump_fee_no_change_add_input_and_change() { + let (mut wallet, _) = get_funded_wallet_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() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(op) + .unwrap() + .manually_selected_only(); + let psbt = builder.finish().unwrap(); + let original_sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let original_fee = check_fee!(wallet, psbt); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + // Now bump the fees, 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_unchecked(50)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + let original_send_all_amount = original_sent_received.0 - original_fee.unwrap_or(Amount::ZERO); + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(50_000) + ); + assert_eq!( + sent_received.1, + Amount::from_sat(75_000) - original_send_all_amount - fee.unwrap_or(Amount::ZERO) + ); + + 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, + Amount::from_sat(75_000) - original_send_all_amount - fee.unwrap_or(Amount::ZERO) + ); + + assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); +} + +#[test] +fn test_bump_fee_add_input_change_dust() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + receive_output_in_latest_block(&mut wallet, 25_000); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); + let psbt = builder.finish().unwrap(); + let original_sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let original_fee = check_fee!(wallet, psbt); + + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realistic weight + } + let original_tx_weight = tx.weight(); + assert_eq!(tx.input.len(), 1); + assert_eq!(tx.output.len(), 2); + let txid = tx.compute_txid(); + insert_tx(&mut wallet, tx); + + 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 + Weight::from_wu(160) + Weight::from_wu(112) - Weight::from_wu(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(Amount::from_sat(fee_abs) / new_tx_weight); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!( + original_sent_received.1, + Amount::from_sat(5_000) - original_fee.unwrap_or(Amount::ZERO) + ); + + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(25_000) + ); + assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(30_000)); + assert_eq!(sent_received.1, Amount::ZERO); + + 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, + Amount::from_sat(45_000) + ); + + assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature); +} + +#[test] +fn test_bump_fee_force_add_input() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); + let psbt = builder.finish().unwrap(); + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.compute_txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + insert_tx(&mut wallet, tx.clone()); + // 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_unchecked(5)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(25_000) + ); + assert_eq!( + fee.unwrap_or(Amount::ZERO) + sent_received.1, + Amount::from_sat(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, + Amount::from_sat(45_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); +} + +#[test] +fn test_bump_fee_absolute_force_add_input() { + let (mut wallet, _) = get_funded_wallet_wpkh(); + let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); + let psbt = builder.finish().unwrap(); + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.compute_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 + } + insert_tx(&mut wallet, tx.clone()); + + // 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(Amount::from_sat(250)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(25_000) + ); + assert_eq!( + fee.unwrap_or(Amount::ZERO) + sent_received.1, + Amount::from_sat(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, + Amount::from_sat(45_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(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_wpkh(); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder.drain_wallet().drain_to(addr.script_pubkey()); + let psbt = 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, ReceiveTo::Mempool(0)); + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + insert_tx(&mut wallet, tx); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25)); + 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_wpkh(); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + // We receive a tx with 0 confirmations, which will be used as an input + // in the drain tx. + receive_output(&mut wallet, 25_000, ReceiveTo::Mempool(0)); + let mut builder = wallet.build_tx(); + builder.drain_wallet().drain_to(addr.script_pubkey()); + let psbt = builder.finish().unwrap(); + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.compute_txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + insert_tx(&mut wallet, tx); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .fee_rate(FeeRate::from_sat_per_vb_unchecked(15)) + // remove original tx drain_to address and amount + .set_recipients(Vec::new()) + // set back original drain_to address + .drain_to(addr.script_pubkey()) + // drain wallet output amount will be re-calculated with new fee rate + .drain_wallet(); + builder.finish().unwrap(); +} diff --git a/wallet/tests/common.rs b/wallet/tests/common.rs new file mode 100644 index 00000000..420e2790 --- /dev/null +++ b/wallet/tests/common.rs @@ -0,0 +1,69 @@ +//! Common test helpers and macros + +#![allow(unused)] + +/// 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. +pub const P2WPKH_FAKE_WITNESS_SIZE: usize = 106; + +#[macro_export] +macro_rules! check_fee { + ($wallet:expr, $psbt: expr) => {{ + let tx = $psbt.clone().extract_tx().expect("failed to extract tx"); + let tx_fee = $wallet.calculate_fee(&tx).ok(); + assert_eq!(tx_fee, $psbt.fee_amount()); + tx_fee + }}; +} + +#[macro_export] +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().expect("failed to extract tx"); + $( + $( $add_signature )* + for txin in &mut tx.input { + txin.witness.push([0x00; common::P2WPKH_FAKE_WITNESS_SIZE]); + } + )* + + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut dust_change = false; + $( + $( $dust_change )* + dust_change = true; + )* + + let fee_amount = psbt + .inputs + .iter() + .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) + - psbt + .unsigned_tx + .output + .iter() + .fold(Amount::ZERO, |acc, o| acc + o.value); + + assert_eq!(fee_amount, $fees); + + let tx_fee_rate = (fee_amount / tx.weight()) + .to_sat_per_kwu(); + let fee_rate = $fee_rate.to_sat_per_kwu(); + let half_default = FeeRate::BROADCAST_MIN.checked_div(2) + .unwrap() + .to_sat_per_kwu(); + + if !dust_change { + assert!(tx_fee_rate >= fee_rate && tx_fee_rate - fee_rate < half_default, "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); + } + }); +} diff --git a/wallet/tests/wallet.rs b/wallet/tests/wallet.rs index 35cbd85d..2ae55496 100644 --- a/wallet/tests/wallet.rs +++ b/wallet/tests/wallet.rs @@ -5,13 +5,12 @@ use std::sync::Arc; use anyhow::Context; use assert_matches::assert_matches; use bdk_chain::{BlockId, ChainPosition, ConfirmationBlockTime}; -use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; +use bdk_wallet::coin_selection; use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; use bdk_wallet::error::CreateTxError; use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::signer::{SignOptions, SignerError}; use bdk_wallet::test_utils::*; -use bdk_wallet::tx_builder::AddForeignUtxoError; use bdk_wallet::{ AddressInfo, Balance, ChangeSet, PersistedWallet, Update, Wallet, WalletPersister, WalletTx, }; @@ -19,33 +18,26 @@ use bdk_wallet::{KeychainKind, LoadError, LoadMismatch, LoadWithPersistError}; use bitcoin::constants::{ChainHash, COINBASE_MATURITY}; use bitcoin::hashes::Hash; use bitcoin::key::Secp256k1; -use bitcoin::psbt; use bitcoin::script::PushBytesBuf; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::taproot::TapNodeHash; use bitcoin::{ absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, - Sequence, Transaction, TxIn, TxOut, Txid, Weight, + Sequence, Transaction, TxIn, TxOut, Txid, }; use miniscript::{descriptor::KeyMap, Descriptor, DescriptorPublicKey}; use rand::rngs::StdRng; use rand::SeedableRng; +mod common; + +const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; + fn parse_descriptor(s: &str) -> (Descriptor, KeyMap) { >::parse_descriptor(&Secp256k1::new(), s) .expect("failed to parse descriptor") } -// 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; - -const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; - #[test] fn wallet_is_persisted() -> anyhow::Result<()> { fn run( @@ -458,53 +450,6 @@ fn test_list_output() { } } -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().expect("failed to 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(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) - - psbt - .unsigned_tx - .output - .iter() - .fold(Amount::ZERO, |acc, o| acc + o.value); - - assert_eq!(fee_amount, $fees); - - let tx_fee_rate = (fee_amount / tx.weight()) - .to_sat_per_kwu(); - let fee_rate = $fee_rate.to_sat_per_kwu(); - let half_default = FeeRate::BROADCAST_MIN.checked_div(2) - .unwrap() - .to_sat_per_kwu(); - - if !dust_change { - assert!(tx_fee_rate >= fee_rate && tx_fee_rate - fee_rate < half_default, "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; @@ -779,15 +724,6 @@ fn test_create_tx_default_sequence() { assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); } -macro_rules! check_fee { - ($wallet:expr, $psbt: expr) => {{ - let tx = $psbt.clone().extract_tx().expect("failed to extract tx"); - let tx_fee = $wallet.calculate_fee(&tx).ok(); - assert_eq!(tx_fee, $psbt.fee_amount()); - tx_fee - }}; -} - #[test] fn test_create_tx_drain_wallet_and_drain_to() { let (mut wallet, _) = get_funded_wallet_wpkh(); @@ -1537,242 +1473,6 @@ fn test_create_tx_increment_change_index() { }); } -#[test] -fn test_add_foreign_utxo() { - let (mut wallet1, _) = get_funded_wallet_wpkh(); - let (wallet2, _) = - get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let utxo = wallet2.list_unspent().next().expect("must take!"); - let foreign_utxo_satisfaction = wallet2 - .public_descriptor(KeychainKind::External) - .max_weight_to_satisfy() - .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(), Amount::from_sat(60_000)) - .only_witness_utxo() - .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) - .unwrap(); - let mut psbt = builder.finish().unwrap(); - wallet1.insert_txout(utxo.outpoint, utxo.txout); - let fee = check_fee!(wallet1, psbt); - let sent_received = - wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - - assert_eq!( - (sent_received.0 - sent_received.1), - Amount::from_sat(10_000) + fee.unwrap_or(Amount::ZERO), - "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] -fn test_calculate_fee_with_missing_foreign_utxo() { - use bdk_chain::tx_graph::CalculateFeeError; - let (mut wallet1, _) = get_funded_wallet_wpkh(); - let (wallet2, _) = - get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let utxo = wallet2.list_unspent().next().expect("must take!"); - let foreign_utxo_satisfaction = wallet2 - .public_descriptor(KeychainKind::External) - .max_weight_to_satisfy() - .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(), Amount::from_sat(60_000)) - .only_witness_utxo() - .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) - .unwrap(); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let res = wallet1.calculate_fee(&tx); - assert!( - matches!(res, Err(CalculateFeeError::MissingTxOut(outpoints)) if outpoints[0] == utxo.outpoint) - ); -} - -#[test] -fn test_add_foreign_utxo_invalid_psbt_input() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - let outpoint = wallet.list_unspent().next().expect("must exist").outpoint; - let foreign_utxo_satisfaction = wallet - .public_descriptor(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - let mut builder = wallet.build_tx(); - let result = - builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction); - assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo))); -} - -#[test] -fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { - let (mut wallet1, txid1) = get_funded_wallet_wpkh(); - let (wallet2, txid2) = - get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - - let utxo2 = wallet2.list_unspent().next().unwrap(); - let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); - let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); - - let satisfaction_weight = wallet2 - .public_descriptor(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - let mut builder = wallet1.build_tx(); - assert!( - builder - .add_foreign_utxo( - utxo2.outpoint, - psbt::Input { - non_witness_utxo: Some(tx1.as_ref().clone()), - ..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.as_ref().clone()), - ..Default::default() - }, - satisfaction_weight - ) - .is_ok(), - "should be ok when outpoint does match psbt_input" - ); -} - -#[test] -fn test_add_foreign_utxo_only_witness_utxo() { - let (mut wallet1, _) = get_funded_wallet_wpkh(); - let (wallet2, txid2) = - get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let utxo2 = wallet2.list_unspent().next().unwrap(); - - let satisfaction_weight = wallet2 - .public_descriptor(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - { - let mut builder = wallet1.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); - - 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 = wallet1.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); - - 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 = wallet1.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); - - let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx; - let psbt_input = psbt::Input { - non_witness_utxo: Some(tx2.as_ref().clone()), - ..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 @@ -1819,993 +1519,141 @@ fn test_create_tx_global_xpubs_master_without_origin() { } #[test] -#[should_panic(expected = "IrreplaceableTransaction")] -fn test_bump_fee_irreplaceable_tx() { +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_wpkh(); - let addr = wallet.next_unused_address(KeychainKind::External); + let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") + .unwrap() + .assume_checked(); + let fee_rate = FeeRate::from_sat_per_kwu(500); + let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); + let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - builder.set_exact_sequence(Sequence(0xFFFFFFFE)); + builder + .add_recipient(send_to.script_pubkey(), Amount::from_sat(8630)) + .add_utxo(incoming_op) + .unwrap() + .fee_rate(fee_rate); let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - wallet.build_fee_bump(txid).unwrap().finish().unwrap(); + assert_eq!(psbt.inputs.len(), 1); + assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), fee_rate, @add_signature); } #[test] -#[should_panic(expected = "TransactionConfirmed")] -fn test_bump_fee_confirmed_tx() { - let (mut wallet, _) = get_funded_wallet_wpkh(); +fn test_sign_single_xprv() { + let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); - insert_tx(&mut wallet, tx); + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); +} - let anchor = ConfirmationBlockTime { - block_id: wallet.latest_checkpoint().get(42).unwrap().block_id(), - confirmation_time: 42_000, - }; - insert_anchor(&mut wallet, txid, anchor); +#[test] +fn test_sign_single_xprv_with_master_fingerprint_and_path() { + let (mut wallet, _) = get_funded_wallet_single("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.next_unused_address(KeychainKind::External); + 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); - wallet.build_fee_bump(txid).unwrap().finish().unwrap(); + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); } #[test] -fn test_bump_fee_low_fee_rate() { - let (mut wallet, _) = get_funded_wallet_wpkh(); +fn test_sign_single_xprv_bip44_path() { + let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); - let psbt = builder.finish().unwrap(); - let feerate = psbt.fee_rate().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); +} - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::BROADCAST_MIN); - let res = builder.finish(); - assert_matches!( - res, - Err(CreateTxError::FeeRateTooLow { .. }), - "expected FeeRateTooLow error" - ); +#[test] +fn test_sign_single_xprv_sh_wpkh() { + let (mut wallet, _) = get_funded_wallet_single("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); + let addr = wallet.next_unused_address(KeychainKind::External); + 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 required = feerate.to_sat_per_kwu() + 250; // +1 sat/vb - let sat_vb = required as f64 / 250.0; - let expect = format!("Fee rate too low: required {} sat/vb", sat_vb); - assert_eq!(res.unwrap_err().to_string(), expect); + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); } #[test] -#[should_panic(expected = "FeeTooLow")] -fn test_bump_fee_low_abs() { - let (mut wallet, _) = get_funded_wallet_wpkh(); +fn test_sign_single_wif() { + let (mut wallet, _) = + get_funded_wallet_single("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(Amount::from_sat(10)); - builder.finish().unwrap(); + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); } #[test] -#[should_panic(expected = "FeeTooLow")] -fn test_bump_fee_zero_abs() { - let (mut wallet, _) = get_funded_wallet_wpkh(); +fn test_sign_single_xprv_no_hd_keypaths() { + let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); + psbt.inputs[0].bip32_derivation.clear(); + assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(Amount::ZERO); - builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); } #[test] -fn test_bump_fee_reduce_change() { - let (mut wallet, _) = get_funded_wallet_wpkh(); +fn test_include_output_redeem_witness_script() { + let desc = get_test_wpkh(); + let change_desc = "sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"; + let (mut wallet, _) = get_funded_wallet(desc, change_desc); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() .assume_checked(); let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - let original_sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let original_fee = check_fee!(wallet, psbt); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - - let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(feerate); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!(sent_received.0, original_sent_received.0); - assert_eq!( - sent_received.1 + fee.unwrap_or(Amount::ZERO), - original_sent_received.1 + original_fee.unwrap_or(Amount::ZERO) - ); - assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); - - 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, - Amount::from_sat(25_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), feerate, @add_signature); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(Amount::from_sat(200)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!(sent_received.0, original_sent_received.0); - assert_eq!( - sent_received.1 + fee.unwrap_or(Amount::ZERO), - original_sent_received.1 + original_fee.unwrap_or(Amount::ZERO) - ); - assert!( - fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO), - "{} > {}", - fee.unwrap_or(Amount::ZERO), - original_fee.unwrap_or(Amount::ZERO) - ); - - 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, - Amount::from_sat(25_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(200)); -} - -#[test] -fn test_bump_fee_reduce_single_recipient() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - let tx = psbt.clone().extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let original_fee = check_fee!(wallet, psbt); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - - let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .fee_rate(feerate) - // remove original tx drain_to address and amount - .set_recipients(Vec::new()) - // set back original drain_to address - .drain_to(addr.script_pubkey()) - // drain wallet output amount will be re-calculated with new fee rate - .drain_wallet(); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!(sent_received.0, original_sent_received.0); - assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 1); - assert_eq!( - tx.output[0].value + fee.unwrap_or(Amount::ZERO), - sent_received.0 - ); - - assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), feerate, @add_signature); -} - -#[test] -fn test_bump_fee_absolute_reduce_single_recipient() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - let original_fee = check_fee!(wallet, psbt); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .fee_absolute(Amount::from_sat(300)) - // remove original tx drain_to address and amount - .set_recipients(Vec::new()) - // set back original drain_to address - .drain_to(addr.script_pubkey()) - // drain wallet output amount will be re-calculated with new fee rate - .drain_wallet(); - let psbt = builder.finish().unwrap(); - let tx = &psbt.unsigned_tx; - let sent_received = wallet.sent_and_received(tx); - let fee = check_fee!(wallet, psbt); - - assert_eq!(sent_received.0, original_sent_received.0); - assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); - - assert_eq!(tx.output.len(), 1); - assert_eq!( - tx.output[0].value + fee.unwrap_or(Amount::ZERO), - sent_received.0 - ); - - assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(300)); -} - -#[test] -fn test_bump_fee_drain_wallet() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - // receive an extra tx so that our wallet has two utxos. - let tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - }; - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx.clone()); - let anchor = ConfirmationBlockTime { - block_id: wallet.latest_checkpoint().block_id(), - confirmation_time: 42_000, - }; - insert_anchor(&mut wallet, txid, anchor); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(OutPoint { - txid: tx.compute_txid(), - vout: 0, - }) - .unwrap() - .manually_selected_only(); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - assert_eq!(original_sent_received.0, Amount::from_sat(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() - .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); - let psbt = builder.finish().unwrap(); - let sent_received = wallet.sent_and_received(&psbt.extract_tx().expect("failed to extract tx")); - - assert_eq!(sent_received.0, Amount::from_sat(75_000)); -} - -#[test] -#[should_panic(expected = "InsufficientFunds")] -fn test_bump_fee_remove_output_manually_selected_only() { - let (mut wallet, _) = get_funded_wallet_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 there is a change output. - let init_tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - }; - - let position: ChainPosition = - wallet.transactions().last().unwrap().chain_position; - insert_tx(&mut wallet, init_tx.clone()); - match position { - ChainPosition::Confirmed { anchor, .. } => { - insert_anchor(&mut wallet, init_tx.compute_txid(), anchor) - } - other => panic!("all wallet txs must be confirmed: {:?}", other), - } - - let outpoint = OutPoint { - txid: init_tx.compute_txid(), - vout: 0, - }; - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(outpoint) - .unwrap() - .manually_selected_only(); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - assert_eq!(original_sent_received.0, Amount::from_sat(25_000)); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .manually_selected_only() - .fee_rate(FeeRate::from_sat_per_vb_unchecked(255)); - builder.finish().unwrap(); -} - -#[test] -fn test_bump_fee_add_input() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - let init_tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - }; - let txid = init_tx.compute_txid(); - let pos: ChainPosition = - wallet.transactions().last().unwrap().chain_position; - insert_tx(&mut wallet, init_tx); - match pos { - ChainPosition::Confirmed { anchor, .. } => insert_anchor(&mut wallet, txid, anchor), - other => panic!("all wallet txs must be confirmed: {:?}", other), - } - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_details = wallet.sent_and_received(&tx); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - assert_eq!( - sent_received.0, - original_details.0 + Amount::from_sat(25_000) - ); - assert_eq!( - fee.unwrap_or(Amount::ZERO) + sent_received.1, - Amount::from_sat(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, - Amount::from_sat(45_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); -} - -#[test] -fn test_bump_fee_absolute_add_input() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - receive_output_in_latest_block(&mut wallet, 25_000); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(Amount::from_sat(6_000)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(25_000) - ); - assert_eq!( - fee.unwrap_or(Amount::ZERO) + sent_received.1, - Amount::from_sat(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, - Amount::from_sat(45_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(6_000)); -} - -#[test] -fn test_bump_fee_no_change_add_input_and_change() { - let (mut wallet, _) = get_funded_wallet_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() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(op) - .unwrap() - .manually_selected_only(); - let psbt = builder.finish().unwrap(); - let original_sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let original_fee = check_fee!(wallet, psbt); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - - // Now bump the fees, 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_unchecked(50)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - let original_send_all_amount = original_sent_received.0 - original_fee.unwrap_or(Amount::ZERO); - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(50_000) - ); - assert_eq!( - sent_received.1, - Amount::from_sat(75_000) - original_send_all_amount - fee.unwrap_or(Amount::ZERO) - ); - - 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, - Amount::from_sat(75_000) - original_send_all_amount - fee.unwrap_or(Amount::ZERO) - ); - - assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); -} - -#[test] -fn test_bump_fee_add_input_change_dust() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - receive_output_in_latest_block(&mut wallet, 25_000); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); - let psbt = builder.finish().unwrap(); - let original_sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let original_fee = check_fee!(wallet, psbt); - - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realistic weight - } - let original_tx_weight = tx.weight(); - assert_eq!(tx.input.len(), 1); - assert_eq!(tx.output.len(), 2); - let txid = tx.compute_txid(); - insert_tx(&mut wallet, tx); - - 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 + Weight::from_wu(160) + Weight::from_wu(112) - Weight::from_wu(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(Amount::from_sat(fee_abs) / new_tx_weight); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!( - original_sent_received.1, - Amount::from_sat(5_000) - original_fee.unwrap_or(Amount::ZERO) - ); - - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(25_000) - ); - assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(30_000)); - assert_eq!(sent_received.1, Amount::ZERO); - - 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, - Amount::from_sat(45_000) - ); - - assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature); -} - -#[test] -fn test_bump_fee_force_add_input() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); - let psbt = builder.finish().unwrap(); - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.compute_txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - insert_tx(&mut wallet, tx.clone()); - // 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_unchecked(5)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(25_000) - ); - assert_eq!( - fee.unwrap_or(Amount::ZERO) + sent_received.1, - Amount::from_sat(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, - Amount::from_sat(45_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); -} - -#[test] -fn test_bump_fee_absolute_force_add_input() { - let (mut wallet, _) = get_funded_wallet_wpkh(); - let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); - let psbt = builder.finish().unwrap(); - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.compute_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 - } - insert_tx(&mut wallet, tx.clone()); - - // 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(Amount::from_sat(250)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(25_000) - ); - assert_eq!( - fee.unwrap_or(Amount::ZERO) + sent_received.1, - Amount::from_sat(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, - Amount::from_sat(45_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(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_wpkh(); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder.drain_wallet().drain_to(addr.script_pubkey()); - let psbt = 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, ReceiveTo::Mempool(0)); - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - insert_tx(&mut wallet, tx); - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25)); - 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_wpkh(); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - // We receive a tx with 0 confirmations, which will be used as an input - // in the drain tx. - receive_output(&mut wallet, 25_000, ReceiveTo::Mempool(0)); - let mut builder = wallet.build_tx(); - builder.drain_wallet().drain_to(addr.script_pubkey()); - let psbt = builder.finish().unwrap(); - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - insert_tx(&mut wallet, tx); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .fee_rate(FeeRate::from_sat_per_vb_unchecked(15)) - // remove original tx drain_to address and amount - .set_recipients(Vec::new()) - // set back original drain_to address - .drain_to(addr.script_pubkey()) - // drain wallet output amount will be re-calculated with new fee rate - .drain_wallet(); - 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_wpkh(); - let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") - .unwrap() - .assume_checked(); - let fee_rate = FeeRate::from_sat_per_kwu(500); - let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); - - let mut builder = wallet.build_tx(); - builder - .add_recipient(send_to.script_pubkey(), Amount::from_sat(8630)) - .add_utxo(incoming_op) - .unwrap() - .fee_rate(fee_rate); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(psbt.inputs.len(), 1); - assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), fee_rate, @add_signature); -} - -#[test] -fn test_sign_single_xprv() { - let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External); - 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().expect("failed to 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_single("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External); - 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().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_sign_single_xprv_bip44_path() { - let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); - let addr = wallet.next_unused_address(KeychainKind::External); - 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().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_sign_single_xprv_sh_wpkh() { - let (mut wallet, _) = get_funded_wallet_single("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); - let addr = wallet.next_unused_address(KeychainKind::External); - 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().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_sign_single_wif() { - let (mut wallet, _) = - get_funded_wallet_single("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); - let addr = wallet.next_unused_address(KeychainKind::External); - 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().expect("failed to 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_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External); - 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().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_include_output_redeem_witness_script() { - let desc = get_test_wpkh(); - let change_desc = "sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"; - let (mut wallet, _) = get_funded_wallet(desc, change_desc); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .include_output_redeem_witness_script(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) + .include_output_redeem_witness_script(); let psbt = builder.finish().unwrap(); // p2sh-p2wsh transaction should contain both witness and redeem scripts @@ -3495,52 +2343,6 @@ fn test_taproot_sign_using_non_witness_utxo() { ); } -#[test] -fn test_taproot_foreign_utxo() { - let (mut wallet1, _) = get_funded_wallet_wpkh(); - let (wallet2, _) = get_funded_wallet_single(get_test_tr_single_sig()); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let utxo = wallet2.list_unspent().next().unwrap(); - let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); - let foreign_utxo_satisfaction = wallet2 - .public_descriptor(KeychainKind::External) - .max_weight_to_satisfy() - .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(), Amount::from_sat(60_000)) - .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) - .unwrap(); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - wallet1.insert_txout(utxo.outpoint, utxo.txout); - let fee = check_fee!(wallet1, psbt); - - assert_eq!( - sent_received.0 - sent_received.1, - Amount::from_sat(10_000) + fee.unwrap_or(Amount::ZERO), - "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.next_unused_address(KeychainKind::External);