From ea39339a6de6291e09ab2e269b32e592c3defa55 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Mon, 23 Jun 2025 10:58:36 -0400 Subject: [PATCH] test: refactor wallet tests --- wallet/tests/add_foreign_utxo.rs | 292 +++++ wallet/tests/build_fee_bump.rs | 946 +++++++++++++++ wallet/tests/common.rs | 117 ++ wallet/tests/persisted_wallet.rs | 359 ++++++ wallet/tests/wallet.rs | 1839 +----------------------------- 5 files changed, 1763 insertions(+), 1790 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 create mode 100644 wallet/tests/persisted_wallet.rs diff --git a/wallet/tests/add_foreign_utxo.rs b/wallet/tests/add_foreign_utxo.rs new file mode 100644 index 00000000..1dd0a8c9 --- /dev/null +++ b/wallet/tests/add_foreign_utxo.rs @@ -0,0 +1,292 @@ +use std::str::FromStr; + +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, Address, Amount}; + +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), + Amount::from_sat(10_000) + fee, + "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, + Amount::from_sat(10_000) + fee, + "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..d81b8d99 --- /dev/null +++ b/wallet/tests/build_fee_bump.rs @@ -0,0 +1,946 @@ +use std::str::FromStr; + +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, ScriptBuf, Sequence, Transaction, + TxOut, +}; + +mod common; +use 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, original_sent_received.0); + assert_eq!(received + fee, original_sent_received.1 + original_fee); + assert!(fee > original_fee); + + 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, + received + ); + + assert_fee_rate!(psbt, fee, 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, original_sent_received.0); + assert_eq!(received + fee, original_sent_received.1 + original_fee); + assert!(fee > original_fee, "{} > {}", fee, original_fee); + + 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, + received + ); + + assert_eq!(fee, 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, original_sent_received.0); + assert!(fee > original_fee); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 1); + assert_eq!(tx.output[0].value + fee, sent); + + assert_fee_rate!(psbt, fee, 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, original_sent_received.0); + assert!(fee > original_fee); + + assert_eq!(tx.output.len(), 1); + assert_eq!(tx.output[0].value + fee, sent); + + assert_eq!(fee, 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, 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, original_details.0 + Amount::from_sat(25_000)); + assert_eq!(fee + received, 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, + received + ); + + assert_fee_rate!(psbt, fee, 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, Amount::from_sat(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, original_sent_received.0 + Amount::from_sat(25_000)); + assert_eq!(fee + received, 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, + received + ); + + assert_eq!(fee, 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, Amount::from_sat(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; + assert_eq!(sent, original_sent_received.0 + Amount::from_sat(50_000)); + assert_eq!( + received, + Amount::from_sat(75_000) - original_send_all_amount - fee + ); + + 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 + ); + + assert_fee_rate!(psbt, fee, FeeRate::from_sat_per_vb_unchecked(50), @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, Amount::from_sat(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_SIG_SIZE]); // sig (72) + txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) + } + 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, original_sent_received.0 + Amount::from_sat(25_000)); + assert_eq!(fee + received, 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, + received + ); + + assert_fee_rate!(psbt, fee, 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, Amount::from_sat(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_SIG_SIZE]); // sig (72) + txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) + } + 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, original_sent_received.0 + Amount::from_sat(25_000)); + assert_eq!(fee + received, 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, + received + ); + + assert_eq!(fee, 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, Amount::from_sat(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_SIG_SIZE]); // sig (72) + txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) + } + 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, Amount::from_sat(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_SIG_SIZE]); // sig (72) + txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) + } + 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] +#[should_panic(expected = "FeeTooLow")] +fn test_legacy_bump_fee_zero_abs() { + let (mut wallet, _) = get_funded_wallet_single(get_test_pkh()); + 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_legacy_bump_fee_drain_wallet() { + let (mut wallet, _) = get_funded_wallet_single(get_test_pkh()); + // 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 { + value: Amount::from_sat(25_000), + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .script_pubkey(), + }], + }; + 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, Amount::from_sat(75_000)); +} + +#[test] +fn test_legacy_bump_fee_add_input() { + let (mut wallet, _) = get_funded_wallet_single(get_test_pkh()); + 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, original_details.0 + Amount::from_sat(25_000)); + assert_eq!(fee + received, 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, + received + ); + + assert_fee_rate_legacy!(psbt, fee, FeeRate::from_sat_per_vb_unchecked(50), @add_signature); +} + +#[test] +fn test_legacy_bump_fee_absolute_add_input() { + let (mut wallet, _) = get_funded_wallet_single(get_test_pkh()); + receive_output_in_latest_block(&mut wallet, Amount::from_sat(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, _original_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, original_sent + Amount::from_sat(25_000)); + assert_eq!(fee + received, 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, + received + ); + + assert_eq!(fee, Amount::from_sat(6_000)); +} diff --git a/wallet/tests/common.rs b/wallet/tests/common.rs new file mode 100644 index 00000000..c7a9b9c5 --- /dev/null +++ b/wallet/tests/common.rs @@ -0,0 +1,117 @@ +#![allow(unused)] + +use bitcoin::secp256k1::Secp256k1; +use miniscript::{descriptor::KeyMap, Descriptor, DescriptorPublicKey}; + +/// The satisfaction size of P2WPKH is 108 WU = +/// 1 (elements in witness) + 1 (size) + 72 (signature + sighash) + 1 (size) + 33 (pubkey). +pub const P2WPKH_FAKE_PK_SIZE: usize = 33; +pub const P2WPKH_FAKE_SIG_SIZE: usize = 72; + +/// The satisfaction size of P2PKH is 107 = +/// 1 (OP_PUSH) + 72 (signature + sighash) + 1 (OP_PUSH) + 33 (pubkey). +pub const P2PKH_FAKE_SCRIPT_SIG_SIZE: usize = 107; + +pub fn parse_descriptor(s: &str) -> (Descriptor, KeyMap) { + >::parse_descriptor(&Secp256k1::new(), s) + .expect("failed to parse descriptor") +} + +/// Validate and return the transaction fee from a PSBT. +/// Panics if extraction fails, fee calculation fails, or if calculated fee doesn't match PSBT's +/// fee. +#[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).expect("failed to calculate fee"); + assert_eq!(Some(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_SIG_SIZE]); // sig (72) + txin.witness.push([0x00; common::P2WPKH_FAKE_PK_SIZE]); // pk (33) + } + )* + + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut dust_change = false; + $( + $( $dust_change )* + dust_change = true; + )* + + let fee_amount = psbt.fee().expect("failed to calculate fee"); + + 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_export] +macro_rules! assert_fee_rate_legacy { + ($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.script_sig = ScriptBuf::from_bytes([0x00; common::P2PKH_FAKE_SCRIPT_SIG_SIZE].to_vec()); // fake signature + } + )* + + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut dust_change = false; + $( + $( $dust_change )* + dust_change = true; + )* + + let fee_amount = psbt.fee().expect("failed to calculate fee"); + 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/persisted_wallet.rs b/wallet/tests/persisted_wallet.rs new file mode 100644 index 00000000..7715d7ad --- /dev/null +++ b/wallet/tests/persisted_wallet.rs @@ -0,0 +1,359 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use anyhow::Context; +use assert_matches::assert_matches; +use bdk_chain::{ + keychain_txout::DEFAULT_LOOKAHEAD, ChainPosition, ConfirmationBlockTime, DescriptorExt, +}; +use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::test_utils::*; +use bdk_wallet::{ + ChangeSet, KeychainKind, LoadError, LoadMismatch, LoadWithPersistError, Wallet, WalletPersister, +}; +use bitcoin::constants::ChainHash; +use bitcoin::hashes::Hash; +use bitcoin::key::Secp256k1; +use bitcoin::{absolute, transaction, Amount, BlockHash, Network, ScriptBuf, Transaction, TxOut}; +use miniscript::{Descriptor, DescriptorPublicKey}; + +mod common; +use common::*; + +const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; + +#[test] +fn wallet_is_persisted() -> anyhow::Result<()> { + fn run( + filename: &str, + create_db: CreateDb, + open_db: OpenDb, + ) -> anyhow::Result<()> + where + CreateDb: Fn(&Path) -> anyhow::Result, + OpenDb: Fn(&Path) -> anyhow::Result, + Db: WalletPersister, + Db::Error: std::error::Error + Send + Sync + 'static, + { + let temp_dir = tempfile::tempdir().expect("must create tempdir"); + let file_path = temp_dir.path().join(filename); + let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_and_change_desc(); + + // create new wallet + let wallet_spk_index = { + let mut db = create_db(&file_path)?; + let mut wallet = Wallet::create(external_desc, internal_desc) + .network(Network::Testnet) + .use_spk_cache(true) + .create_wallet(&mut db)?; + wallet.reveal_next_address(KeychainKind::External); + + // persist new wallet changes + assert!(wallet.persist(&mut db)?, "must write"); + wallet.spk_index().clone() + }; + + // recover wallet + { + let mut db = open_db(&file_path).context("failed to recover db")?; + let wallet = Wallet::load() + .descriptor(KeychainKind::External, Some(external_desc)) + .descriptor(KeychainKind::Internal, Some(internal_desc)) + .check_network(Network::Testnet) + .load_wallet(&mut db)? + .expect("wallet must exist"); + + assert_eq!(wallet.network(), Network::Testnet); + assert_eq!( + wallet.spk_index().keychains().collect::>(), + wallet_spk_index.keychains().collect::>() + ); + assert_eq!( + wallet.spk_index().last_revealed_indices(), + wallet_spk_index.last_revealed_indices() + ); + let secp = Secp256k1::new(); + assert_eq!( + *wallet.public_descriptor(KeychainKind::External), + external_desc + .into_wallet_descriptor(&secp, wallet.network()) + .unwrap() + .0 + ); + } + // Test SPK cache + { + let mut db = open_db(&file_path).context("failed to recover db")?; + let mut wallet = Wallet::load() + .check_network(Network::Testnet) + .use_spk_cache(true) + .load_wallet(&mut db)? + .expect("wallet must exist"); + + let external_did = wallet + .public_descriptor(KeychainKind::External) + .descriptor_id(); + let internal_did = wallet + .public_descriptor(KeychainKind::Internal) + .descriptor_id(); + + assert!(wallet.staged().is_none()); + + let _addr = wallet.reveal_next_address(KeychainKind::External); + let cs = wallet.staged().expect("we should have staged a changeset"); + assert!(!cs.indexer.spk_cache.is_empty(), "failed to cache spks"); + assert_eq!(cs.indexer.spk_cache.len(), 2, "we persisted two keychains"); + let spk_cache: &BTreeMap = + cs.indexer.spk_cache.get(&external_did).unwrap(); + assert_eq!(spk_cache.len() as u32, 1 + 1 + DEFAULT_LOOKAHEAD); + assert_eq!(spk_cache.keys().last(), Some(&26)); + let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap(); + assert_eq!(spk_cache.len() as u32, DEFAULT_LOOKAHEAD); + assert_eq!(spk_cache.keys().last(), Some(&24)); + // Clear the stage + let _ = wallet.take_staged(); + let _addr = wallet.reveal_next_address(KeychainKind::Internal); + let cs = wallet.staged().unwrap(); + assert_eq!(cs.indexer.spk_cache.len(), 1); + let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap(); + assert_eq!(spk_cache.len(), 1); + assert_eq!(spk_cache.keys().next(), Some(&25)); + } + // SPK cache requires load params + { + let mut db = open_db(&file_path).context("failed to recover db")?; + let mut wallet = Wallet::load() + .check_network(Network::Testnet) + // .use_spk_cache(false) + .load_wallet(&mut db)? + .expect("wallet must exist"); + + let internal_did = wallet + .public_descriptor(KeychainKind::Internal) + .descriptor_id(); + + assert!(wallet.staged().is_none()); + + let _addr = wallet.reveal_next_address(KeychainKind::Internal); + let cs = wallet.staged().expect("we should have staged a changeset"); + assert_eq!(cs.indexer.last_revealed.get(&internal_did), Some(&0)); + assert!( + cs.indexer.spk_cache.is_empty(), + "we didn't set `use_spk_cache`" + ); + } + + Ok(()) + } + + run( + "store.db", + |path| Ok(bdk_file_store::Store::create(DB_MAGIC, path)?), + |path| Ok(bdk_file_store::Store::load(DB_MAGIC, path)?.0), + )?; + run::( + "store.sqlite", + |path| Ok(bdk_chain::rusqlite::Connection::open(path)?), + |path| Ok(bdk_chain::rusqlite::Connection::open(path)?), + )?; + + Ok(()) +} + +#[test] +fn wallet_load_checks() -> anyhow::Result<()> { + fn run( + filename: &str, + create_db: CreateDb, + open_db: OpenDb, + ) -> anyhow::Result<()> + where + CreateDb: Fn(&Path) -> anyhow::Result, + OpenDb: Fn(&Path) -> anyhow::Result, + Db: WalletPersister + std::fmt::Debug, + Db::Error: std::error::Error + Send + Sync + 'static, + { + let temp_dir = tempfile::tempdir().expect("must create tempdir"); + let file_path = temp_dir.path().join(filename); + let network = Network::Testnet; + let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_and_change_desc(); + + // create new wallet + let _ = Wallet::create(external_desc, internal_desc) + .network(network) + .create_wallet(&mut create_db(&file_path)?)?; + + assert_matches!( + Wallet::load() + .check_network(Network::Regtest) + .load_wallet(&mut open_db(&file_path)?), + Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( + LoadMismatch::Network { + loaded: Network::Testnet, + expected: Network::Regtest, + } + ))), + "unexpected network check result: Regtest (check) is not Testnet (loaded)", + ); + let mainnet_hash = BlockHash::from_byte_array(ChainHash::BITCOIN.to_bytes()); + assert_matches!( + Wallet::load().check_genesis_hash(mainnet_hash).load_wallet(&mut open_db(&file_path)?), + Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Genesis { .. }))), + "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)", + ); + assert_matches!( + Wallet::load() + .descriptor(KeychainKind::External, Some(internal_desc)) + .load_wallet(&mut open_db(&file_path)?), + Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( + LoadMismatch::Descriptor { .. } + ))), + "unexpected descriptors check result", + ); + assert_matches!( + Wallet::load() + .descriptor(KeychainKind::External, Option::<&str>::None) + .load_wallet(&mut open_db(&file_path)?), + Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( + LoadMismatch::Descriptor { .. } + ))), + "unexpected descriptors check result", + ); + // check setting keymaps + let (_, external_keymap) = parse_descriptor(external_desc); + let (_, internal_keymap) = parse_descriptor(internal_desc); + let wallet = Wallet::load() + .keymap(KeychainKind::External, external_keymap) + .keymap(KeychainKind::Internal, internal_keymap) + .load_wallet(&mut open_db(&file_path)?) + .expect("db should not fail") + .expect("wallet was persisted"); + for keychain in [KeychainKind::External, KeychainKind::Internal] { + let keymap = wallet.get_signers(keychain).as_key_map(wallet.secp_ctx()); + assert!( + !keymap.is_empty(), + "load should populate keymap for keychain {keychain:?}" + ); + } + Ok(()) + } + + run( + "store.db", + |path| Ok(bdk_file_store::Store::::create(DB_MAGIC, path)?), + |path| Ok(bdk_file_store::Store::::load(DB_MAGIC, path)?.0), + )?; + run( + "store.sqlite", + |path| Ok(bdk_chain::rusqlite::Connection::open(path)?), + |path| Ok(bdk_chain::rusqlite::Connection::open(path)?), + )?; + + Ok(()) +} + +#[test] +fn wallet_should_persist_anchors_and_recover() { + use bdk_chain::rusqlite; + let temp_dir = tempfile::tempdir().unwrap(); + let db_path = temp_dir.path().join("wallet.db"); + let mut db = rusqlite::Connection::open(db_path).unwrap(); + + let desc = get_test_tr_single_sig_xprv(); + let mut wallet = Wallet::create_single(desc) + .network(Network::Testnet) + .create_wallet(&mut db) + .unwrap(); + let small_output_tx = Transaction { + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + version: transaction::Version::non_standard(0), + lock_time: absolute::LockTime::ZERO, + }; + let txid = small_output_tx.compute_txid(); + insert_tx(&mut wallet, small_output_tx); + let expected_anchor = ConfirmationBlockTime { + block_id: wallet.latest_checkpoint().block_id(), + confirmation_time: 200, + }; + insert_anchor(&mut wallet, txid, expected_anchor); + assert!(wallet.persist(&mut db).unwrap()); + + // should recover persisted wallet + let secp = wallet.secp_ctx(); + let (_, keymap) = >::parse_descriptor(secp, desc).unwrap(); + assert!(!keymap.is_empty()); + let wallet = Wallet::load() + .descriptor(KeychainKind::External, Some(desc)) + .extract_keys() + .load_wallet(&mut db) + .unwrap() + .expect("must have loaded changeset"); + // stored anchor should be retrieved in the same condition it was persisted + if let ChainPosition::Confirmed { + anchor: obtained_anchor, + .. + } = wallet + .get_tx(txid) + .expect("should retrieve stored tx") + .chain_position + { + assert_eq!(obtained_anchor, expected_anchor) + } else { + panic!("Should have got ChainPosition::Confirmed)"); + } +} + +#[test] +fn single_descriptor_wallet_persist_and_recover() { + use bdk_chain::miniscript::Descriptor; + use bdk_chain::miniscript::DescriptorPublicKey; + use bdk_chain::rusqlite; + let temp_dir = tempfile::tempdir().unwrap(); + let db_path = temp_dir.path().join("wallet.db"); + let mut db = rusqlite::Connection::open(db_path).unwrap(); + + let desc = get_test_tr_single_sig_xprv(); + let mut wallet = Wallet::create_single(desc) + .network(Network::Testnet) + .create_wallet(&mut db) + .unwrap(); + let _ = wallet.reveal_addresses_to(KeychainKind::External, 2); + assert!(wallet.persist(&mut db).unwrap()); + + // should recover persisted wallet + let secp = wallet.secp_ctx(); + let (_, keymap) = >::parse_descriptor(secp, desc).unwrap(); + assert!(!keymap.is_empty()); + let wallet = Wallet::load() + .descriptor(KeychainKind::External, Some(desc)) + .extract_keys() + .load_wallet(&mut db) + .unwrap() + .expect("must have loaded changeset"); + assert_eq!(wallet.derivation_index(KeychainKind::External), Some(2)); + // should have private key + assert_eq!( + wallet.get_signers(KeychainKind::External).as_key_map(secp), + keymap, + ); + + // should error on wrong internal params + let desc = get_test_wpkh(); + let (exp_desc, _) = >::parse_descriptor(secp, desc).unwrap(); + let err = Wallet::load() + .descriptor(KeychainKind::Internal, Some(desc)) + .extract_keys() + .load_wallet(&mut db); + assert_matches!( + err, + Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Descriptor { keychain, loaded, expected }))) + if keychain == KeychainKind::Internal && loaded.is_none() && expected == Some(exp_desc), + "single descriptor wallet should refuse change descriptor param" + ); +} diff --git a/wallet/tests/wallet.rs b/wallet/tests/wallet.rs index 8ed28159..fd47856a 100644 --- a/wallet/tests/wallet.rs +++ b/wallet/tests/wallet.rs @@ -1,392 +1,29 @@ -use std::collections::BTreeMap; -use std::path::Path; use std::str::FromStr; use std::sync::Arc; -use anyhow::Context; use assert_matches::assert_matches; -use bdk_chain::{ - keychain_txout::DEFAULT_LOOKAHEAD, BlockId, CanonicalizationParams, ChainPosition, - ConfirmationBlockTime, DescriptorExt, -}; -use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; -use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; +use bdk_chain::{BlockId, CanonicalizationParams, ConfirmationBlockTime}; +use bdk_wallet::coin_selection; +use bdk_wallet::descriptor::{calc_checksum, DescriptorError}; 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, -}; -use bdk_wallet::{KeychainKind, LoadError, LoadMismatch, LoadWithPersistError}; -use bitcoin::constants::{ChainHash, COINBASE_MATURITY}; +use bdk_wallet::KeychainKind; +use bdk_wallet::{AddressInfo, Balance, PersistedWallet, Update, Wallet, WalletTx}; +use bitcoin::constants::COINBASE_MATURITY; use bitcoin::hashes::Hash; -use bitcoin::key::Secp256k1; 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, SignedAmount, Transaction, TxIn, TxOut, Txid, }; -use bitcoin::{psbt, SignedAmount}; -use miniscript::{descriptor::KeyMap, Descriptor, DescriptorPublicKey}; use rand::rngs::StdRng; use rand::SeedableRng; -fn parse_descriptor(s: &str) -> (Descriptor, KeyMap) { - >::parse_descriptor(&Secp256k1::new(), s) - .expect("failed to parse descriptor") -} - -/// The satisfaction size of P2WPKH is 108 WU = -/// 1 (elements in witness) + 1 (size) -/// + 72 (signature + sighash) + 1 (size) + 33 (pubkey). -const P2WPKH_FAKE_PK_SIZE: usize = 33; -const P2WPKH_FAKE_SIG_SIZE: usize = 72; - -/// The satisfaction size of P2PKH is 107 = -/// 1 (OP_PUSH) + 72 (signature + sighash) + 1 (OP_PUSH) + 33 (pubkey). -const P2PKH_FAKE_SCRIPT_SIG_SIZE: usize = 107; - -const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; - -#[test] -fn wallet_is_persisted() -> anyhow::Result<()> { - fn run( - filename: &str, - create_db: CreateDb, - open_db: OpenDb, - ) -> anyhow::Result<()> - where - CreateDb: Fn(&Path) -> anyhow::Result, - OpenDb: Fn(&Path) -> anyhow::Result, - Db: WalletPersister, - Db::Error: std::error::Error + Send + Sync + 'static, - { - let temp_dir = tempfile::tempdir().expect("must create tempdir"); - let file_path = temp_dir.path().join(filename); - let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - - // create new wallet - let wallet_spk_index = { - let mut db = create_db(&file_path)?; - let mut wallet = Wallet::create(external_desc, internal_desc) - .network(Network::Testnet) - .use_spk_cache(true) - .create_wallet(&mut db)?; - wallet.reveal_next_address(KeychainKind::External); - - // persist new wallet changes - assert!(wallet.persist(&mut db)?, "must write"); - wallet.spk_index().clone() - }; - - // recover wallet - { - let mut db = open_db(&file_path).context("failed to recover db")?; - let wallet = Wallet::load() - .descriptor(KeychainKind::External, Some(external_desc)) - .descriptor(KeychainKind::Internal, Some(internal_desc)) - .check_network(Network::Testnet) - .load_wallet(&mut db)? - .expect("wallet must exist"); - - assert_eq!(wallet.network(), Network::Testnet); - assert_eq!( - wallet.spk_index().keychains().collect::>(), - wallet_spk_index.keychains().collect::>() - ); - assert_eq!( - wallet.spk_index().last_revealed_indices(), - wallet_spk_index.last_revealed_indices() - ); - let secp = Secp256k1::new(); - assert_eq!( - *wallet.public_descriptor(KeychainKind::External), - external_desc - .into_wallet_descriptor(&secp, wallet.network()) - .unwrap() - .0 - ); - } - // Test SPK cache - { - let mut db = open_db(&file_path).context("failed to recover db")?; - let mut wallet = Wallet::load() - .check_network(Network::Testnet) - .use_spk_cache(true) - .load_wallet(&mut db)? - .expect("wallet must exist"); - - let external_did = wallet - .public_descriptor(KeychainKind::External) - .descriptor_id(); - let internal_did = wallet - .public_descriptor(KeychainKind::Internal) - .descriptor_id(); - - assert!(wallet.staged().is_none()); - - let _addr = wallet.reveal_next_address(KeychainKind::External); - let cs = wallet.staged().expect("we should have staged a changeset"); - assert!(!cs.indexer.spk_cache.is_empty(), "failed to cache spks"); - assert_eq!(cs.indexer.spk_cache.len(), 2, "we persisted two keychains"); - let spk_cache: &BTreeMap = - cs.indexer.spk_cache.get(&external_did).unwrap(); - assert_eq!(spk_cache.len() as u32, 1 + 1 + DEFAULT_LOOKAHEAD); - assert_eq!(spk_cache.keys().last(), Some(&26)); - let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap(); - assert_eq!(spk_cache.len() as u32, DEFAULT_LOOKAHEAD); - assert_eq!(spk_cache.keys().last(), Some(&24)); - // Clear the stage - let _ = wallet.take_staged(); - let _addr = wallet.reveal_next_address(KeychainKind::Internal); - let cs = wallet.staged().unwrap(); - assert_eq!(cs.indexer.spk_cache.len(), 1); - let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap(); - assert_eq!(spk_cache.len(), 1); - assert_eq!(spk_cache.keys().next(), Some(&25)); - } - // SPK cache requires load params - { - let mut db = open_db(&file_path).context("failed to recover db")?; - let mut wallet = Wallet::load() - .check_network(Network::Testnet) - // .use_spk_cache(false) - .load_wallet(&mut db)? - .expect("wallet must exist"); - - let internal_did = wallet - .public_descriptor(KeychainKind::Internal) - .descriptor_id(); - - assert!(wallet.staged().is_none()); - - let _addr = wallet.reveal_next_address(KeychainKind::Internal); - let cs = wallet.staged().expect("we should have staged a changeset"); - assert_eq!(cs.indexer.last_revealed.get(&internal_did), Some(&0)); - assert!( - cs.indexer.spk_cache.is_empty(), - "we didn't set `use_spk_cache`" - ); - } - - Ok(()) - } - - run( - "store.db", - |path| Ok(bdk_file_store::Store::create(DB_MAGIC, path)?), - |path| Ok(bdk_file_store::Store::load(DB_MAGIC, path)?.0), - )?; - run::( - "store.sqlite", - |path| Ok(bdk_chain::rusqlite::Connection::open(path)?), - |path| Ok(bdk_chain::rusqlite::Connection::open(path)?), - )?; - - Ok(()) -} - -#[test] -fn wallet_load_checks() -> anyhow::Result<()> { - fn run( - filename: &str, - create_db: CreateDb, - open_db: OpenDb, - ) -> anyhow::Result<()> - where - CreateDb: Fn(&Path) -> anyhow::Result, - OpenDb: Fn(&Path) -> anyhow::Result, - Db: WalletPersister + std::fmt::Debug, - Db::Error: std::error::Error + Send + Sync + 'static, - { - let temp_dir = tempfile::tempdir().expect("must create tempdir"); - let file_path = temp_dir.path().join(filename); - let network = Network::Testnet; - let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - - // create new wallet - let _ = Wallet::create(external_desc, internal_desc) - .network(network) - .create_wallet(&mut create_db(&file_path)?)?; - - assert_matches!( - Wallet::load() - .check_network(Network::Regtest) - .load_wallet(&mut open_db(&file_path)?), - Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( - LoadMismatch::Network { - loaded: Network::Testnet, - expected: Network::Regtest, - } - ))), - "unexpected network check result: Regtest (check) is not Testnet (loaded)", - ); - let mainnet_hash = BlockHash::from_byte_array(ChainHash::BITCOIN.to_bytes()); - assert_matches!( - Wallet::load().check_genesis_hash(mainnet_hash).load_wallet(&mut open_db(&file_path)?), - Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Genesis { .. }))), - "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)", - ); - assert_matches!( - Wallet::load() - .descriptor(KeychainKind::External, Some(internal_desc)) - .load_wallet(&mut open_db(&file_path)?), - Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( - LoadMismatch::Descriptor { .. } - ))), - "unexpected descriptors check result", - ); - assert_matches!( - Wallet::load() - .descriptor(KeychainKind::External, Option::<&str>::None) - .load_wallet(&mut open_db(&file_path)?), - Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( - LoadMismatch::Descriptor { .. } - ))), - "unexpected descriptors check result", - ); - // check setting keymaps - let (_, external_keymap) = parse_descriptor(external_desc); - let (_, internal_keymap) = parse_descriptor(internal_desc); - let wallet = Wallet::load() - .keymap(KeychainKind::External, external_keymap) - .keymap(KeychainKind::Internal, internal_keymap) - .load_wallet(&mut open_db(&file_path)?) - .expect("db should not fail") - .expect("wallet was persisted"); - for keychain in [KeychainKind::External, KeychainKind::Internal] { - let keymap = wallet.get_signers(keychain).as_key_map(wallet.secp_ctx()); - assert!( - !keymap.is_empty(), - "load should populate keymap for keychain {keychain:?}" - ); - } - Ok(()) - } - - run( - "store.db", - |path| Ok(bdk_file_store::Store::::create(DB_MAGIC, path)?), - |path| Ok(bdk_file_store::Store::::load(DB_MAGIC, path)?.0), - )?; - run( - "store.sqlite", - |path| Ok(bdk_chain::rusqlite::Connection::open(path)?), - |path| Ok(bdk_chain::rusqlite::Connection::open(path)?), - )?; - - Ok(()) -} - -#[test] -fn wallet_should_persist_anchors_and_recover() { - use bdk_chain::rusqlite; - let temp_dir = tempfile::tempdir().unwrap(); - let db_path = temp_dir.path().join("wallet.db"); - let mut db = rusqlite::Connection::open(db_path).unwrap(); - - let desc = get_test_tr_single_sig_xprv(); - let mut wallet = Wallet::create_single(desc) - .network(Network::Testnet) - .create_wallet(&mut db) - .unwrap(); - let small_output_tx = Transaction { - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - version: transaction::Version::non_standard(0), - lock_time: absolute::LockTime::ZERO, - }; - let txid = small_output_tx.compute_txid(); - insert_tx(&mut wallet, small_output_tx); - let expected_anchor = ConfirmationBlockTime { - block_id: wallet.latest_checkpoint().block_id(), - confirmation_time: 200, - }; - insert_anchor(&mut wallet, txid, expected_anchor); - assert!(wallet.persist(&mut db).unwrap()); - - // should recover persisted wallet - let secp = wallet.secp_ctx(); - let (_, keymap) = >::parse_descriptor(secp, desc).unwrap(); - assert!(!keymap.is_empty()); - let wallet = Wallet::load() - .descriptor(KeychainKind::External, Some(desc)) - .extract_keys() - .load_wallet(&mut db) - .unwrap() - .expect("must have loaded changeset"); - // stored anchor should be retrieved in the same condition it was persisted - if let ChainPosition::Confirmed { - anchor: obtained_anchor, - .. - } = wallet - .get_tx(txid) - .expect("should retrieve stored tx") - .chain_position - { - assert_eq!(obtained_anchor, expected_anchor) - } else { - panic!("Should have got ChainPosition::Confirmed)"); - } -} - -#[test] -fn single_descriptor_wallet_persist_and_recover() { - use bdk_chain::miniscript::Descriptor; - use bdk_chain::miniscript::DescriptorPublicKey; - use bdk_chain::rusqlite; - let temp_dir = tempfile::tempdir().unwrap(); - let db_path = temp_dir.path().join("wallet.db"); - let mut db = rusqlite::Connection::open(db_path).unwrap(); - - let desc = get_test_tr_single_sig_xprv(); - let mut wallet = Wallet::create_single(desc) - .network(Network::Testnet) - .create_wallet(&mut db) - .unwrap(); - let _ = wallet.reveal_addresses_to(KeychainKind::External, 2); - assert!(wallet.persist(&mut db).unwrap()); - - // should recover persisted wallet - let secp = wallet.secp_ctx(); - let (_, keymap) = >::parse_descriptor(secp, desc).unwrap(); - assert!(!keymap.is_empty()); - let wallet = Wallet::load() - .descriptor(KeychainKind::External, Some(desc)) - .extract_keys() - .load_wallet(&mut db) - .unwrap() - .expect("must have loaded changeset"); - assert_eq!(wallet.derivation_index(KeychainKind::External), Some(2)); - // should have private key - assert_eq!( - wallet.get_signers(KeychainKind::External).as_key_map(secp), - keymap, - ); - - // should error on wrong internal params - let desc = get_test_wpkh(); - let (exp_desc, _) = >::parse_descriptor(secp, desc).unwrap(); - let err = Wallet::load() - .descriptor(KeychainKind::Internal, Some(desc)) - .extract_keys() - .load_wallet(&mut db); - assert_matches!( - err, - Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Descriptor { keychain, loaded, expected }))) - if keychain == KeychainKind::Internal && loaded.is_none() && expected == Some(exp_desc), - "single descriptor wallet should refuse change descriptor param" - ); -} +mod common; #[test] fn test_error_external_and_internal_are_the_same() { @@ -535,90 +172,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_SIG_SIZE]); // sig (72) - txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) - } - )* - - #[allow(unused_mut)] - #[allow(unused_assignments)] - let mut dust_change = false; - $( - $( $dust_change )* - dust_change = true; - )* - - let fee_amount = psbt.fee().expect("failed to calculate fee"); - - 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! assert_fee_rate_legacy { - ($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.script_sig = ScriptBuf::from_bytes([0x00; P2PKH_FAKE_SCRIPT_SIG_SIZE].to_vec()); // fake signature - } - )* - - #[allow(unused_mut)] - #[allow(unused_assignments)] - let mut dust_change = false; - $( - $( $dust_change )* - dust_change = true; - )* - - let fee_amount = psbt.fee().expect("failed to calculate fee"); - 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; @@ -893,18 +446,6 @@ fn test_create_tx_default_sequence() { assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); } -/// Validate and return the transaction fee from a PSBT. -/// Panics if extraction fails, fee calculation fails, or if calculated fee doesn't match PSBT's -/// fee. -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).expect("failed to calculate fee"); - assert_eq!(Some(tx_fee), $psbt.fee_amount()); - tx_fee - }}; -} - #[test] fn test_create_tx_drain_wallet_and_drain_to() { let (mut wallet, _) = get_funded_wallet_wpkh(); @@ -1745,242 +1286,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), - Amount::from_sat(10_000) + fee, - "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 @@ -2027,1068 +1332,68 @@ 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, Amount::from_sat(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, 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(); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.compute_txid(); - - insert_tx(&mut wallet, tx); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); - let anchor = ConfirmationBlockTime { - block_id: wallet.latest_checkpoint().get(42).unwrap().block_id(), - confirmation_time: 42_000, - }; - insert_anchor(&mut wallet, txid, anchor); + 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_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.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); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); - 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 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(); - 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] -#[should_panic(expected = "FeeTooLow")] -fn test_legacy_bump_fee_zero_abs() { - let (mut wallet, _) = get_funded_wallet_single(get_test_pkh()); - 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, original_sent_received.0); - assert_eq!(received + fee, original_sent_received.1 + original_fee); - assert!(fee > original_fee); - - 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, - received - ); - - assert_fee_rate!(psbt, fee, 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, original_sent_received.0); - assert_eq!(received + fee, original_sent_received.1 + original_fee); - assert!(fee > original_fee, "{} > {}", fee, original_fee); - - 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, - received - ); - - assert_eq!(fee, 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, original_sent_received.0); - assert!(fee > original_fee); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 1); - assert_eq!(tx.output[0].value + fee, sent); - - assert_fee_rate!(psbt, fee, 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, original_sent_received.0); - assert!(fee > original_fee); - - assert_eq!(tx.output.len(), 1); - assert_eq!(tx.output[0].value + fee, sent); - - assert_eq!(fee, 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, Amount::from_sat(75_000)); -} - -#[test] -fn test_legacy_bump_fee_drain_wallet() { - let (mut wallet, _) = get_funded_wallet_single(get_test_pkh()); - // 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 { - value: Amount::from_sat(25_000), - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .script_pubkey(), - }], - }; - 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, 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, original_details.0 + Amount::from_sat(25_000)); - assert_eq!(fee + received, 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, - received - ); - - assert_fee_rate!(psbt, fee, FeeRate::from_sat_per_vb_unchecked(50), @add_signature); -} - -#[test] -fn test_legacy_bump_fee_add_input() { - let (mut wallet, _) = get_funded_wallet_single(get_test_pkh()); - 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, original_details.0 + Amount::from_sat(25_000)); - assert_eq!(fee + received, 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, - received - ); - - assert_fee_rate_legacy!(psbt, fee, 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, Amount::from_sat(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, original_sent_received.0 + Amount::from_sat(25_000)); - assert_eq!(fee + received, 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, - received - ); - - assert_eq!(fee, Amount::from_sat(6_000)); -} - -#[test] -fn test_legacy_bump_fee_absolute_add_input() { - let (mut wallet, _) = get_funded_wallet_single(get_test_pkh()); - receive_output_in_latest_block(&mut wallet, Amount::from_sat(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, _original_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, original_sent + Amount::from_sat(25_000)); - assert_eq!(fee + received, 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, - received - ); - - assert_eq!(fee, 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, Amount::from_sat(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; - assert_eq!(sent, original_sent_received.0 + Amount::from_sat(50_000)); - assert_eq!( - received, - Amount::from_sat(75_000) - original_send_all_amount - fee - ); - - 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 - ); - - assert_fee_rate!(psbt, fee, 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, Amount::from_sat(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_SIG_SIZE]); // sig (72) - txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) - } - 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 - ); - - assert_eq!(sent, original_sent_received.0 + Amount::from_sat(25_000)); - assert_eq!(fee, Amount::from_sat(30_000)); - assert_eq!(received, 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, 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, Amount::from_sat(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_SIG_SIZE]); // sig (72) - txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) - } - 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, original_sent_received.0 + Amount::from_sat(25_000)); - assert_eq!(fee + received, 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, - received - ); - - assert_fee_rate!(psbt, fee, 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, Amount::from_sat(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_SIG_SIZE]); // sig (72) - txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) - } - 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, original_sent_received.0 + Amount::from_sat(25_000)); - assert_eq!(fee + received, 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, - received - ); - - assert_eq!(fee, 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, Amount::from_sat(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_SIG_SIZE]); // sig (72) - txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) - } - 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, Amount::from_sat(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_SIG_SIZE]); // sig (72) - txin.witness.push([0x00; P2WPKH_FAKE_PK_SIZE]); // pk (33) - } - 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, Amount::from_sat(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, 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/*)"); +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(); @@ -3851,52 +2156,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, - Amount::from_sat(10_000) + fee, - "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);