diff --git a/Cargo.toml b/Cargo.toml index b20ef222d..78ed61df1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ "example-crates/wallet_esplora_blocking", "example-crates/wallet_esplora_async", "nursery/tmp_plan", - "nursery/coin_select" + "nursery/coin_select", ] [workspace.package] diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs index ee8ed74e1..d7a963d23 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -108,6 +108,12 @@ pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { get_funded_wallet_with_change(descriptor, None) } +// This PKH WIF was taken from example 5 in +// https://github.com/libbitcoin/libbitcoin-explorer/wiki/bx-ec-to-wif +pub fn get_test_pkh() -> &'static str { + "pkh(cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm)" +} + pub fn get_test_wpkh() -> &'static str { "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" } diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index aad8c2db2..a7ba105a4 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -139,6 +139,22 @@ fn test_get_funded_wallet_tx_fee_rate() { assert_eq!(tx_fee_rate.as_sat_per_vb(), 8.849558); } +#[test] +fn test_legacy_get_funded_wallet_tx_fee_rate() { + let (wallet, txid) = get_funded_wallet(get_test_pkh()); + + let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx_fee_rate = wallet.calculate_fee_rate(tx).expect("transaction fee rate"); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + + // tx weight = 464 bytes, as vbytes = (464)/4 = 116 + // fee rate (sats per vbyte) = fee / vbytes = 1000 / 116 = 8.6206896551724137931 rounded to 8.620689 + assert_eq!(tx_fee_rate.as_sat_per_vb(), 8.620689); +} + 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(); @@ -151,7 +167,7 @@ macro_rules! assert_fee_rate { } )* - #[allow(unused_mut)] + #[allow(unused_mut)] #[allow(unused_assignments)] let mut dust_change = false; $( @@ -445,6 +461,16 @@ macro_rules! check_fee { }}; } +/// A floating point assert! that takes an additional third argument `delta` +/// as a tolerance value when test for equality the first and second argument. +macro_rules! assert_delta { + ($x:expr, $y:expr, $d:expr) => { + if !($x - $y < $d || $y - $x < $d) { + panic!(); + } + }; +} + #[test] fn test_create_tx_drain_wallet_and_drain_to() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -526,6 +552,26 @@ fn test_create_tx_default_fee_rate() { assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::default(), @add_signature); } +#[test] +fn test_legacy_create_tx_custom_fee_rate() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + let mut psbt = builder.finish().unwrap(); + + // sign the transaction + wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + + assert_delta!( + psbt.fee_rate().unwrap(), + FeeRate::from_sat_per_vb(5.0), + FeeRate::from_sat_per_vb(0.6) + ) +} + #[test] fn test_create_tx_custom_fee_rate() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -540,6 +586,23 @@ fn test_create_tx_custom_fee_rate() { assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); } +#[test] +fn test_legacy_create_tx_absolute_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(100); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(fee.unwrap_or(0), 100); + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); +} + #[test] fn test_create_tx_absolute_fee() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -557,6 +620,23 @@ fn test_create_tx_absolute_fee() { assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); } +#[test] +fn test_legacy_create_tx_absolute_zero_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(0); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(fee.unwrap_or(0), 0); + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); +} + #[test] fn test_create_tx_absolute_zero_fee() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -587,6 +667,19 @@ fn test_create_tx_absolute_high_fee() { let _ = builder.finish().unwrap(); } +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_legacy_create_tx_absolute_high_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(60_000); + let _ = builder.finish().unwrap(); +} + #[test] fn test_create_tx_add_change() { use bdk::wallet::tx_builder::TxOrdering; @@ -651,6 +744,17 @@ fn test_create_tx_ordering_respected() { assert_eq!(psbt.unsigned_tx.output[2].value, 30_000); } +#[test] +fn test_legacy_create_tx_default_sighash() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 30_000); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.inputs[0].sighash_type, None); +} + #[test] fn test_create_tx_default_sighash() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -662,6 +766,22 @@ fn test_create_tx_default_sighash() { assert_eq!(psbt.inputs[0].sighash_type, None); } +#[test] +fn test_legacy_create_tx_custom_sighash() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .sighash(EcdsaSighashType::Single.into()); + let psbt = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0].sighash_type, + Some(EcdsaSighashType::Single.into()) + ); +} + #[test] fn test_create_tx_custom_sighash() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -1197,7 +1317,7 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { satisfaction_weight ) .is_ok(), - "shoulld be ok when outpoint does match psbt_input" + "should be ok when outpoint does match psbt_input" ); } @@ -1403,6 +1523,28 @@ fn test_bump_fee_low_abs() { builder.finish().unwrap(); } +#[test] +#[should_panic(expected = "FeeTooLow")] +fn test_legacy_bump_fee_zero_abs() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx(); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(0); + builder.finish().unwrap(); +} + #[test] #[should_panic(expected = "FeeTooLow")] fn test_bump_fee_zero_abs() { @@ -1598,6 +1740,66 @@ fn test_bump_fee_absolute_reduce_single_recipient() { assert_eq!(fee.unwrap_or(0), 300); } +#[test] +fn test_legacy_bump_fee_drain_wallet() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + // receive an extra tx so that our wallet has two utxos. + let tx = Transaction { + version: 1, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: 25_000, + script_pubkey: wallet.get_address(New).script_pubkey(), + }], + }; + wallet + .insert_tx( + tx.clone(), + ConfirmationTime::Confirmed { + height: wallet.latest_checkpoint().unwrap().height(), + time: 42_000, + }, + ) + .unwrap(); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(OutPoint { + txid: tx.txid(), + vout: 0, + }) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); + + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + assert_eq!(original_sent_received.0, 25_000); + + // for the new feerate, it should be enough to reduce the output, but since we specify + // `drain_wallet` we expect to spend everything + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .drain_wallet() + .allow_shrinking(addr.script_pubkey()) + .unwrap() + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.extract_tx()); + + assert_eq!(sent_received.0, 75_000); +} + #[test] fn test_bump_fee_drain_wallet() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -1718,6 +1920,80 @@ fn test_bump_fee_remove_output_manually_selected_only() { builder.finish().unwrap(); } +#[test] +fn test_legacy_bump_fee_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + let init_tx = Transaction { + version: 1, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: wallet.get_address(New).script_pubkey(), + value: 25_000, + }], + }; + let pos = wallet + .transactions() + .last() + .unwrap() + .chain_position + .cloned() + .into(); + wallet.insert_tx(init_tx, pos).unwrap(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let original_details = wallet.sent_and_received(&tx); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); + let mut psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = check_fee!(wallet, psbt); + assert_eq!(sent_received.0, original_details.0 + 25_000); + assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + // sign the transaction + wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + + assert_delta!( + psbt.fee_rate().unwrap(), + FeeRate::from_sat_per_vb(50.0), + FeeRate::from_sat_per_vb(1.0) + ); +} + #[test] fn test_bump_fee_add_input() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -1785,6 +2061,57 @@ fn test_bump_fee_add_input() { assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); } +#[test] +fn test_legacy_bump_fee_absolute_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_pkh()); + receive_output_in_latest_block(&mut wallet, 25_000); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(6_000); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0 + 25_000); + assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_eq!(fee.unwrap_or(0), 6_000); +} + #[test] fn test_bump_fee_absolute_add_input() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh());