From 004957dc2969171b7a37ce92d2cdb0e8f22b6da5 Mon Sep 17 00:00:00 2001 From: vmammal Date: Thu, 16 Nov 2023 18:00:58 -0500 Subject: [PATCH 1/5] refactor(bdk)!: drop FeeRate from bdk::types Adopt `bitcoin::FeeRate` throughout --- crates/bdk/src/psbt/mod.rs | 5 +- crates/bdk/src/types.rs | 170 +----------------------- crates/bdk/src/wallet/coin_selection.rs | 92 +++++++------ crates/bdk/src/wallet/error.rs | 10 +- crates/bdk/src/wallet/hardwaresigner.rs | 2 +- crates/bdk/src/wallet/mod.rs | 35 +++-- crates/bdk/src/wallet/tx_builder.rs | 26 ++-- crates/bdk/tests/psbt.rs | 30 +++-- crates/bdk/tests/wallet.rs | 77 ++++++----- crates/hwi/src/lib.rs | 2 +- 10 files changed, 151 insertions(+), 298 deletions(-) diff --git a/crates/bdk/src/psbt/mod.rs b/crates/bdk/src/psbt/mod.rs index 796b8618b..260669175 100644 --- a/crates/bdk/src/psbt/mod.rs +++ b/crates/bdk/src/psbt/mod.rs @@ -11,9 +11,10 @@ //! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure. -use crate::FeeRate; use alloc::vec::Vec; use bitcoin::psbt::PartiallySignedTransaction as Psbt; +use bitcoin::Amount; +use bitcoin::FeeRate; use bitcoin::TxOut; // TODO upstream the functions here to `rust-bitcoin`? @@ -65,7 +66,7 @@ impl PsbtUtils for Psbt { let fee_amount = self.fee_amount(); fee_amount.map(|fee| { let weight = self.clone().extract_tx().weight(); - FeeRate::from_wu(fee, weight) + Amount::from_sat(fee) / weight }) } } diff --git a/crates/bdk/src/types.rs b/crates/bdk/src/types.rs index 5f2173596..3bde290e4 100644 --- a/crates/bdk/src/types.rs +++ b/crates/bdk/src/types.rs @@ -11,11 +11,10 @@ use alloc::boxed::Box; use core::convert::AsRef; -use core::ops::Sub; use bdk_chain::ConfirmationTime; use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut}; -use bitcoin::{psbt, Weight}; +use bitcoin::psbt; use serde::{Deserialize, Serialize}; @@ -47,103 +46,6 @@ impl AsRef<[u8]> for KeychainKind { } } -/// Fee rate -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] -// Internally stored as satoshi/vbyte -pub struct FeeRate(f32); - -impl FeeRate { - /// Create a new instance checking the value provided - /// - /// ## Panics - /// - /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. - fn new_checked(value: f32) -> Self { - assert!(value.is_normal() || value == 0.0); - assert!(value.is_sign_positive()); - - FeeRate(value) - } - - /// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu - pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self { - FeeRate::new_checked(sat_per_kwu / 250.0_f32) - } - - /// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb - pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self { - FeeRate::new_checked(sat_per_kvb / 1000.0_f32) - } - - /// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes - /// - /// ## Panics - /// - /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. - pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self { - FeeRate::new_checked(btc_per_kvb * 1e5) - } - - /// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte - /// - /// ## Panics - /// - /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. - pub fn from_sat_per_vb(sat_per_vb: f32) -> Self { - FeeRate::new_checked(sat_per_vb) - } - - /// Create a new [`FeeRate`] with the default min relay fee value - pub const fn default_min_relay_fee() -> Self { - FeeRate(1.0) - } - - /// Calculate fee rate from `fee` and weight units (`wu`). - pub fn from_wu(fee: u64, wu: Weight) -> FeeRate { - Self::from_vb(fee, wu.to_vbytes_ceil() as usize) - } - - /// Calculate fee rate from `fee` and `vbytes`. - pub fn from_vb(fee: u64, vbytes: usize) -> FeeRate { - let rate = fee as f32 / vbytes as f32; - Self::from_sat_per_vb(rate) - } - - /// Return the value as satoshi/vbyte - pub fn as_sat_per_vb(&self) -> f32 { - self.0 - } - - /// Return the value as satoshi/kwu - pub fn sat_per_kwu(&self) -> f32 { - self.0 * 250.0_f32 - } - - /// Calculate absolute fee in Satoshis using size in weight units. - pub fn fee_wu(&self, wu: Weight) -> u64 { - self.fee_vb(wu.to_vbytes_ceil() as usize) - } - - /// Calculate absolute fee in Satoshis using size in virtual bytes. - pub fn fee_vb(&self, vbytes: usize) -> u64 { - (self.as_sat_per_vb() * vbytes as f32).ceil() as u64 - } -} - -impl Default for FeeRate { - fn default() -> Self { - FeeRate::default_min_relay_fee() - } -} - -impl Sub for FeeRate { - type Output = Self; - - fn sub(self, other: FeeRate) -> Self::Output { - FeeRate(self.0 - other.0) - } -} - /// Trait implemented by types that can be used to measure weight units. pub trait Vbytes { /// Convert weight units to virtual bytes. @@ -244,73 +146,3 @@ impl Utxo { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_store_feerate_in_const() { - const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee(); - } - - #[test] - #[should_panic] - fn test_invalid_feerate_neg_zero() { - let _ = FeeRate::from_sat_per_vb(-0.0); - } - - #[test] - #[should_panic] - fn test_invalid_feerate_neg_value() { - let _ = FeeRate::from_sat_per_vb(-5.0); - } - - #[test] - #[should_panic] - fn test_invalid_feerate_nan() { - let _ = FeeRate::from_sat_per_vb(f32::NAN); - } - - #[test] - #[should_panic] - fn test_invalid_feerate_inf() { - let _ = FeeRate::from_sat_per_vb(f32::INFINITY); - } - - #[test] - fn test_valid_feerate_pos_zero() { - let _ = FeeRate::from_sat_per_vb(0.0); - } - - #[test] - fn test_fee_from_btc_per_kvb() { - let fee = FeeRate::from_btc_per_kvb(1e-5); - assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); - } - - #[test] - fn test_fee_from_sat_per_vbyte() { - let fee = FeeRate::from_sat_per_vb(1.0); - assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); - } - - #[test] - fn test_fee_default_min_relay_fee() { - let fee = FeeRate::default_min_relay_fee(); - assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); - } - - #[test] - fn test_fee_from_sat_per_kvb() { - let fee = FeeRate::from_sat_per_kvb(1000.0); - assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); - } - - #[test] - fn test_fee_from_sat_per_kwu() { - let fee = FeeRate::from_sat_per_kwu(250.0); - assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); - assert_eq!(fee.sat_per_kwu(), 250.0); - } -} diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index ac6084cfc..5383e5525 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -41,7 +41,7 @@ //! &self, //! required_utxos: Vec, //! optional_utxos: Vec, -//! fee_rate: bdk::FeeRate, +//! fee_rate: FeeRate, //! target_amount: u64, //! drain_script: &Script, //! ) -> Result { @@ -61,7 +61,7 @@ //! }, //! ) //! .collect::>(); -//! let additional_fees = fee_rate.fee_wu(additional_weight); +//! let additional_fees = (fee_rate * additional_weight).to_sat(); //! let amount_needed_with_fees = additional_fees + target_amount; //! if selected_amount < amount_needed_with_fees { //! return Err(coin_selection::Error::InsufficientFunds { @@ -101,10 +101,10 @@ //! ``` use crate::chain::collections::HashSet; -use crate::types::FeeRate; use crate::wallet::utils::IsDust; use crate::Utxo; use crate::WeightedUtxo; +use bitcoin::FeeRate; use alloc::vec::Vec; use bitcoin::consensus::encode::serialize; @@ -313,7 +313,8 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess { // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value) let drain_output_len = serialize(drain_script).len() + 8usize; - let change_fee = fee_rate.fee_vb(drain_output_len); + let change_fee = + (fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat(); let drain_val = remaining_amount.saturating_sub(change_fee); if drain_val.is_dust(drain_script) { @@ -344,9 +345,12 @@ fn select_sorted_utxos( (&mut selected_amount, &mut fee_amount), |(selected_amount, fee_amount), (must_use, weighted_utxo)| { if must_use || **selected_amount < target_amount + **fee_amount { - **fee_amount += fee_rate.fee_wu(Weight::from_wu( - (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64, - )); + **fee_amount += (fee_rate + * Weight::from_wu( + (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64, + )) + .to_sat(); + **selected_amount += weighted_utxo.utxo.txout().value; Some(weighted_utxo.utxo) } else { @@ -387,9 +391,10 @@ struct OutputGroup { impl OutputGroup { fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self { - let fee = fee_rate.fee_wu(Weight::from_wu( - (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64, - )); + let fee = (fee_rate + * Weight::from_wu((TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64)) + .to_sat(); + let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64; OutputGroup { weighted_utxo, @@ -456,7 +461,8 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { .iter() .fold(0, |acc, x| acc + x.effective_value); - let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb(); + let cost_of_change = + (Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat(); // `curr_value` and `curr_available_value` are both the sum of *effective_values* of // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with @@ -547,7 +553,7 @@ impl BranchAndBoundCoinSelection { mut curr_value: i64, mut curr_available_value: i64, target_amount: i64, - cost_of_change: f32, + cost_of_change: u64, drain_script: &Script, fee_rate: FeeRate, ) -> Result { @@ -893,7 +899,7 @@ mod test { .coin_select( utxos, vec![], - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -914,7 +920,7 @@ mod test { .coin_select( utxos, vec![], - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -935,7 +941,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -957,7 +963,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -975,7 +981,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1000.0), + FeeRate::from_sat_per_vb_unchecked(1000), target_amount, &drain_script, ) @@ -992,7 +998,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1013,7 +1019,7 @@ mod test { .coin_select( utxos, vec![], - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1034,7 +1040,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1056,7 +1062,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1075,7 +1081,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1000.0), + FeeRate::from_sat_per_vb_unchecked(1000), target_amount, &drain_script, ) @@ -1096,7 +1102,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1117,7 +1123,7 @@ mod test { .coin_select( utxos.clone(), utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1138,7 +1144,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1175,7 +1181,7 @@ mod test { .coin_select( required, optional, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1197,7 +1203,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1215,7 +1221,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1000.0), + FeeRate::from_sat_per_vb_unchecked(1000), target_amount, &drain_script, ) @@ -1232,7 +1238,7 @@ mod test { .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(1.0), + FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, ) @@ -1258,7 +1264,7 @@ mod test { .coin_select( vec![], optional_utxos, - FeeRate::from_sat_per_vb(0.0), + FeeRate::ZERO, target_amount, &drain_script, ) @@ -1270,7 +1276,7 @@ mod test { #[test] #[should_panic(expected = "BnBNoExactMatch")] fn test_bnb_function_no_exact_match() { - let fee_rate = FeeRate::from_sat_per_vb(10.0); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); let utxos: Vec = get_test_utxos() .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) @@ -1279,7 +1285,7 @@ mod test { let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); let size_of_change = 31; - let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb(); + let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; @@ -1300,7 +1306,7 @@ mod test { #[test] #[should_panic(expected = "BnBTotalTriesExceeded")] fn test_bnb_function_tries_exceeded() { - let fee_rate = FeeRate::from_sat_per_vb(10.0); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); let utxos: Vec = generate_same_value_utxos(100_000, 100_000) .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) @@ -1309,7 +1315,7 @@ mod test { let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); let size_of_change = 31; - let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb(); + let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); let target_amount = 20_000 + FEE_AMOUNT; let drain_script = ScriptBuf::default(); @@ -1331,9 +1337,9 @@ mod test { // The match won't be exact but still in the range #[test] fn test_bnb_function_almost_exact_match_with_fees() { - let fee_rate = FeeRate::from_sat_per_vb(1.0); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); let size_of_change = 31; - let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb(); + let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) .into_iter() @@ -1346,7 +1352,7 @@ mod test { // 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) - // cost_of_change + 5. - let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5; + let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5; let drain_script = ScriptBuf::default(); @@ -1371,7 +1377,7 @@ mod test { fn test_bnb_function_exact_match_more_utxos() { let seed = [0; 32]; let mut rng: StdRng = SeedableRng::from_seed(seed); - let fee_rate = FeeRate::from_sat_per_vb(0.0); + let fee_rate = FeeRate::ZERO; for _ in 0..200 { let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40) @@ -1397,7 +1403,7 @@ mod test { curr_value, curr_available_value, target_amount, - 0.0, + 0, &drain_script, fee_rate, ) @@ -1413,7 +1419,7 @@ mod test { let mut utxos = generate_random_utxos(&mut rng, 300); let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT; - let fee_rate = FeeRate::from_sat_per_vb(1.0); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); let utxos: Vec = utxos .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) @@ -1442,7 +1448,7 @@ mod test { let selection = BranchAndBoundCoinSelection::default().coin_select( vec![], utxos, - FeeRate::from_sat_per_vb(10.0), + FeeRate::from_sat_per_vb_unchecked(10), 500_000, &drain_script, ); @@ -1468,7 +1474,7 @@ mod test { let selection = BranchAndBoundCoinSelection::default().coin_select( required, optional, - FeeRate::from_sat_per_vb(10.0), + FeeRate::from_sat_per_vb_unchecked(10), 500_000, &drain_script, ); @@ -1490,7 +1496,7 @@ mod test { let selection = BranchAndBoundCoinSelection::default().coin_select( utxos, vec![], - FeeRate::from_sat_per_vb(10_000.0), + FeeRate::from_sat_per_vb_unchecked(10_000), 500_000, &drain_script, ); diff --git a/crates/bdk/src/wallet/error.rs b/crates/bdk/src/wallet/error.rs index db58fef06..548dc783f 100644 --- a/crates/bdk/src/wallet/error.rs +++ b/crates/bdk/src/wallet/error.rs @@ -14,7 +14,7 @@ use crate::descriptor::policy::PolicyError; use crate::descriptor::DescriptorError; use crate::wallet::coin_selection; -use crate::{descriptor, FeeRate, KeychainKind}; +use crate::{descriptor, KeychainKind}; use alloc::string::String; use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid}; use core::fmt; @@ -83,8 +83,8 @@ pub enum CreateTxError

{ }, /// When bumping a tx the fee rate requested is lower than required FeeRateTooLow { - /// Required fee rate (satoshi/vbyte) - required: FeeRate, + /// Required fee rate + required: bitcoin::FeeRate, }, /// `manually_selected_only` option is selected but no utxo has been passed NoUtxosSelected, @@ -168,8 +168,8 @@ where CreateTxError::FeeRateTooLow { required } => { write!( f, - "Fee rate too low: required {} sat/vbyte", - required.as_sat_per_vb() + "Fee rate too low: required {} sat/kwu", + required.to_sat_per_kwu() ) } CreateTxError::NoUtxosSelected => { diff --git a/crates/bdk/src/wallet/hardwaresigner.rs b/crates/bdk/src/wallet/hardwaresigner.rs index aec49297c..8b11e784a 100644 --- a/crates/bdk/src/wallet/hardwaresigner.rs +++ b/crates/bdk/src/wallet/hardwaresigner.rs @@ -18,7 +18,7 @@ //! # use bdk::signer::SignerOrdering; //! # use bdk::wallet::hardwaresigner::HWISigner; //! # use bdk::wallet::AddressIndex::New; -//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet}; +//! # use bdk::{KeychainKind, SignOptions, Wallet}; //! # use hwi::HWIClient; //! # use std::sync::Arc; //! # diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 4db035fb6..3b133602f 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -33,7 +33,7 @@ use bdk_chain::{ use bitcoin::secp256k1::{All, Secp256k1}; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::{ - absolute, Address, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, + absolute, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid, Weight, Witness, }; use bitcoin::{consensus::encode::serialize, BlockHash}; @@ -986,10 +986,8 @@ impl Wallet { /// ``` /// [`insert_txout`]: Self::insert_txout pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result { - self.calculate_fee(tx).map(|fee| { - let weight = tx.weight(); - FeeRate::from_wu(fee, weight) - }) + self.calculate_fee(tx) + .map(|fee| bitcoin::Amount::from_sat(fee) / tx.weight()) } /// Compute the `tx`'s sent and received amounts (in satoshis). @@ -1432,32 +1430,31 @@ impl Wallet { (Some(rbf), _) => rbf.get_value(), }; - let (fee_rate, mut fee_amount) = match params - .fee_policy - .as_ref() - .unwrap_or(&FeePolicy::FeeRate(FeeRate::default())) - { + let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() { //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256 FeePolicy::FeeAmount(fee) => { if let Some(previous_fee) = params.bumping_fee { - if *fee < previous_fee.absolute { + if fee < previous_fee.absolute { return Err(CreateTxError::FeeTooLow { required: previous_fee.absolute, }); } } - (FeeRate::from_sat_per_vb(0.0), *fee) + (FeeRate::ZERO, fee) } FeePolicy::FeeRate(rate) => { if let Some(previous_fee) = params.bumping_fee { - let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0); - if *rate < required_feerate { + let required_feerate = FeeRate::from_sat_per_kwu( + previous_fee.rate.to_sat_per_kwu() + + FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb + ); + if rate < required_feerate { return Err(CreateTxError::FeeRateTooLow { required: required_feerate, }); } } - (*rate, 0) + (rate, 0) } }; @@ -1500,7 +1497,7 @@ impl Wallet { outgoing += value; } - fee_amount += fee_rate.fee_wu(tx.weight()); + fee_amount += (fee_rate * tx.weight()).to_sat(); // Segwit transactions' header is 2WU larger than legacy txs' header, // as they contain a witness marker (1WU) and a witness flag (1WU) (see BIP144). @@ -1511,7 +1508,7 @@ impl Wallet { // end up with a transaction with a slightly higher fee rate than the requested one. // If, instead, we undershoot, we may end up with a feerate lower than the requested one // - we might come up with non broadcastable txs! - fee_amount += fee_rate.fee_wu(Weight::from_wu(2)); + fee_amount += (fee_rate * Weight::from_wu(2)).to_sat(); if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed && internal_descriptor.is_none() @@ -1652,7 +1649,7 @@ impl Wallet { /// let mut psbt = { /// let mut builder = wallet.build_fee_bump(tx.txid())?; /// builder - /// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)); + /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")); /// builder.finish()? /// }; /// @@ -1780,7 +1777,7 @@ impl Wallet { utxos: original_utxos, bumping_fee: Some(tx_builder::PreviousFee { absolute: fee, - rate: fee_rate.as_sat_per_vb(), + rate: fee_rate, }), ..Default::default() }; diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index 45d215fd8..aa81d1f35 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -31,7 +31,7 @@ //! // Create a transaction with one output to `to_address` of 50_000 satoshi //! .add_recipient(to_address.script_pubkey(), 50_000) //! // With a custom fee rate of 5.0 satoshi/vbyte -//! .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)) +//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")) //! // Only spend non-change outputs //! .do_not_spend_change() //! // Turn on RBF signaling @@ -53,10 +53,10 @@ use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transa use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use super::ChangeSet; -use crate::types::{FeeRate, KeychainKind, LocalOutput, WeightedUtxo}; +use crate::types::{KeychainKind, LocalOutput, WeightedUtxo}; use crate::wallet::CreateTxError; use crate::{Utxo, Wallet}; - +use bitcoin::FeeRate; /// Context in which the [`TxBuilder`] is valid pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} @@ -163,7 +163,7 @@ pub(crate) struct TxParams { #[derive(Clone, Copy, Debug)] pub(crate) struct PreviousFee { pub absolute: u64, - pub rate: f32, + pub rate: FeeRate, } #[derive(Debug, Clone, Copy)] @@ -174,7 +174,7 @@ pub(crate) enum FeePolicy { impl Default for FeePolicy { fn default() -> Self { - FeePolicy::FeeRate(FeeRate::default_min_relay_fee()) + FeePolicy::FeeRate(FeeRate::BROADCAST_MIN) } } @@ -191,14 +191,12 @@ impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> { // methods supported by both contexts, for any CoinSelectionAlgorithm impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> { - /// Set a custom fee rate - /// The fee_rate method sets the mining fee paid by the transaction as a rate on its size. - /// This means that the total fee paid is equal to this rate * size of the transaction in virtual Bytes (vB) or Weight Unit (wu). - /// This rate is internally expressed in satoshis-per-virtual-bytes (sats/vB) using FeeRate::from_sat_per_vb, but can also be set by: - /// * sats/kvB (1000 sats/kvB == 1 sats/vB) using FeeRate::from_sat_per_kvb - /// * btc/kvB (0.00001000 btc/kvB == 1 sats/vB) using FeeRate::from_btc_per_kvb - /// * sats/kwu (250 sats/kwu == 1 sats/vB) using FeeRate::from_sat_per_kwu - /// Default is 1 sat/vB (see min_relay_fee) + /// Set a custom fee rate. + /// + /// This method sets the mining fee paid by the transaction as a rate on its size. + /// This means that the total fee paid is equal to `fee_rate` times the size + /// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default + /// relay policy. /// /// Note that this is really a minimum feerate -- it's possible to /// overshoot it slightly since adding a change output to drain the remaining @@ -781,7 +779,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> { /// .drain_wallet() /// // Send the excess (which is all the coins minus the fee) to this address. /// .drain_to(to_address.script_pubkey()) - /// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)) + /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")) /// .enable_rbf(); /// let psbt = tx_builder.finish()?; /// # Ok::<(), anyhow::Error>(()) diff --git a/crates/bdk/tests/psbt.rs b/crates/bdk/tests/psbt.rs index 3c4968bfe..e3ace9db0 100644 --- a/crates/bdk/tests/psbt.rs +++ b/crates/bdk/tests/psbt.rs @@ -1,7 +1,7 @@ use bdk::bitcoin::TxIn; use bdk::wallet::AddressIndex; use bdk::wallet::AddressIndex::New; -use bdk::{psbt, FeeRate, SignOptions}; +use bdk::{psbt, SignOptions}; use bitcoin::psbt::PartiallySignedTransaction as Psbt; use core::str::FromStr; mod common; @@ -10,6 +10,12 @@ use common::*; // from bip 174 const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA"; +fn feerate_unchecked(sat_vb: f64) -> bitcoin::FeeRate { + // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu + let sat_kwu = (sat_vb * 250.0).ceil() as u64; + bitcoin::FeeRate::from_sat_per_kwu(sat_kwu) +} + #[test] #[should_panic(expected = "InputIndexOutOfRange")] fn test_psbt_malformed_psbt_input_legacy() { @@ -82,13 +88,13 @@ fn test_psbt_sign_with_finalized() { fn test_psbt_fee_rate_with_witness_utxo() { use psbt::PsbtUtils; - let expected_fee_rate = 1.2345; + let expected_fee_rate = feerate_unchecked(1.2345); let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); + builder.fee_rate(expected_fee_rate); let mut psbt = builder.finish().unwrap(); let fee_amount = psbt.fee_amount(); assert!(fee_amount.is_some()); @@ -99,21 +105,21 @@ fn test_psbt_fee_rate_with_witness_utxo() { assert!(finalized); let finalized_fee_rate = psbt.fee_rate().unwrap(); - assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate); - assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb()); + assert!(finalized_fee_rate >= expected_fee_rate); + assert!(finalized_fee_rate < unfinalized_fee_rate); } #[test] fn test_psbt_fee_rate_with_nonwitness_utxo() { use psbt::PsbtUtils; - let expected_fee_rate = 1.2345; + let expected_fee_rate = feerate_unchecked(1.2345); let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); + builder.fee_rate(expected_fee_rate); let mut psbt = builder.finish().unwrap(); let fee_amount = psbt.fee_amount(); assert!(fee_amount.is_some()); @@ -123,21 +129,21 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() { assert!(finalized); let finalized_fee_rate = psbt.fee_rate().unwrap(); - assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate); - assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb()); + assert!(finalized_fee_rate >= expected_fee_rate); + assert!(finalized_fee_rate < unfinalized_fee_rate); } #[test] fn test_psbt_fee_rate_with_missing_txout() { use psbt::PsbtUtils; - let expected_fee_rate = 1.2345; + let expected_fee_rate = feerate_unchecked(1.2345); let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wpkh_wallet.get_address(New); let mut builder = wpkh_wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); + builder.fee_rate(expected_fee_rate); let mut wpkh_psbt = builder.finish().unwrap(); wpkh_psbt.inputs[0].witness_utxo = None; @@ -149,7 +155,7 @@ fn test_psbt_fee_rate_with_missing_txout() { let addr = pkh_wallet.get_address(New); let mut builder = pkh_wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); + builder.fee_rate(expected_fee_rate); let mut pkh_psbt = builder.finish().unwrap(); pkh_psbt.inputs[0].non_witness_utxo = None; diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 271b87163..a8a2c9282 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -9,11 +9,13 @@ use bdk::wallet::error::CreateTxError; use bdk::wallet::tx_builder::AddForeignUtxoError; use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet}; use bdk::wallet::{AddressIndex::*, NewError}; -use bdk::{FeeRate, KeychainKind}; +use bdk::KeychainKind; use bdk_chain::COINBASE_MATURITY; use bdk_chain::{BlockId, ConfirmationTime}; use bitcoin::hashes::Hash; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; +use bitcoin::Amount; +use bitcoin::FeeRate; use bitcoin::ScriptBuf; use bitcoin::{ absolute, script::PushBytesBuf, taproot::TapNodeHash, Address, OutPoint, Sequence, Transaction, @@ -21,7 +23,6 @@ use bitcoin::{ }; use bitcoin::{psbt, Network}; use bitcoin::{BlockHash, Txid}; - mod common; use common::*; @@ -55,6 +56,12 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { receive_output(wallet, value, anchor) } +fn feerate_unchecked(sat_vb: f64) -> FeeRate { + // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu + let sat_kwu = (sat_vb * 250.0).ceil() as u64; + FeeRate::from_sat_per_kwu(sat_kwu) +} + // The satisfaction size of a P2WPKH is 112 WU = // 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len) // On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for @@ -246,9 +253,11 @@ fn test_get_funded_wallet_tx_fee_rate() { // 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 = 452 bytes, as vbytes = (452+3)/4 = 113 - // fee rate (sats per vbyte) = fee / vbytes = 1000 / 113 = 8.8495575221 rounded to 8.849558 - assert_eq!(tx_fee_rate.as_sat_per_vb(), 8.849558); + // tx weight = 452 wu, as vbytes = (452 + 3) / 4 = 113 + // fee_rate (sats per kwu) = fee / weight = 1000sat / 0.452kwu = 2212 + // fee_rate (sats per vbyte ceil) = fee / vsize = 1000sat / 113vb = 9 + assert_eq!(tx_fee_rate.to_sat_per_kwu(), 2212); + assert_eq!(tx_fee_rate.to_sat_per_vb_ceil(), 9); } #[test] @@ -302,11 +311,15 @@ macro_rules! assert_fee_rate { assert_eq!(fee_amount, $fees); - let tx_fee_rate = FeeRate::from_wu($fees, tx.weight()); - let fee_rate = $fee_rate; + let tx_fee_rate = (Amount::from_sat(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).as_sat_per_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate); + 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); } @@ -647,7 +660,7 @@ fn test_create_tx_default_fee_rate() { let psbt = builder.finish().unwrap(); let fee = check_fee!(wallet, psbt); - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::default(), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::BROADCAST_MIN, @add_signature); } #[test] @@ -657,11 +670,11 @@ fn test_create_tx_custom_fee_rate() { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), 25_000) - .fee_rate(FeeRate::from_sat_per_vb(5.0)); + .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); let psbt = builder.finish().unwrap(); let fee = check_fee!(wallet, psbt); - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); } #[test] @@ -753,7 +766,7 @@ fn test_create_tx_drain_to_dust_amount() { builder .drain_to(addr.script_pubkey()) .drain_wallet() - .fee_rate(FeeRate::from_sat_per_vb(453.0)); + .fee_rate(FeeRate::from_sat_per_vb_unchecked(454)); builder.finish().unwrap(); } @@ -1499,7 +1512,7 @@ fn test_bump_fee_low_fee_rate() { .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(1.0)); + builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(1)); builder.finish().unwrap(); } @@ -1569,7 +1582,7 @@ fn test_bump_fee_reduce_change() { .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf(); + builder.fee_rate(feerate_unchecked(2.5)).enable_rbf(); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); let fee = check_fee!(wallet, psbt); @@ -1600,7 +1613,7 @@ fn test_bump_fee_reduce_change() { sent_received.1 ); - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), feerate_unchecked(2.5), @add_signature); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_absolute(200); @@ -1665,7 +1678,7 @@ fn test_bump_fee_reduce_single_recipient() { let mut builder = wallet.build_fee_bump(txid).unwrap(); builder - .fee_rate(FeeRate::from_sat_per_vb(2.5)) + .fee_rate(feerate_unchecked(2.5)) .allow_shrinking(addr.script_pubkey()) .unwrap(); let psbt = builder.finish().unwrap(); @@ -1679,7 +1692,7 @@ fn test_bump_fee_reduce_single_recipient() { assert_eq!(tx.output.len(), 1); assert_eq!(tx.output[0].value + fee.unwrap_or(0), sent_received.0); - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), feerate_unchecked(2.5), @add_signature); } #[test] @@ -1774,7 +1787,7 @@ fn test_bump_fee_drain_wallet() { .drain_wallet() .allow_shrinking(addr.script_pubkey()) .unwrap() - .fee_rate(FeeRate::from_sat_per_vb(5.0)); + .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.extract_tx()); @@ -1837,7 +1850,7 @@ fn test_bump_fee_remove_output_manually_selected_only() { let mut builder = wallet.build_fee_bump(txid).unwrap(); builder .manually_selected_only() - .fee_rate(FeeRate::from_sat_per_vb(255.0)); + .fee_rate(FeeRate::from_sat_per_vb_unchecked(255)); builder.finish().unwrap(); } @@ -1878,7 +1891,7 @@ fn test_bump_fee_add_input() { .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); + 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()); let fee = check_fee!(wallet, psbt); @@ -1905,7 +1918,7 @@ fn test_bump_fee_add_input() { sent_received.1 ); - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); } #[test] @@ -1988,7 +2001,7 @@ fn test_bump_fee_no_change_add_input_and_change() { // now bump the fees without using `allow_shrinking`. the wallet should add an // extra input and a change output, and leave the original output untouched let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); + 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()); let fee = check_fee!(wallet, psbt); @@ -2020,7 +2033,7 @@ fn test_bump_fee_no_change_add_input_and_change() { 75_000 - original_send_all_amount - fee.unwrap_or(0) ); - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); } #[test] @@ -2065,7 +2078,7 @@ fn test_bump_fee_add_input_change_dust() { // two inputs (50k, 25k) and one output (45k) - epsilon // We use epsilon here to avoid asking for a slightly too high feerate let fee_abs = 50_000 + 25_000 - 45_000 - 10; - builder.fee_rate(FeeRate::from_wu(fee_abs, new_tx_weight)); + 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()); let fee = check_fee!(wallet, psbt); @@ -2088,7 +2101,7 @@ fn test_bump_fee_add_input_change_dust() { 45_000 ); - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature); } #[test] @@ -2119,7 +2132,7 @@ fn test_bump_fee_force_add_input() { builder .add_utxo(incoming_op) .unwrap() - .fee_rate(FeeRate::from_sat_per_vb(5.0)); + .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()); let fee = check_fee!(wallet, psbt); @@ -2147,7 +2160,7 @@ fn test_bump_fee_force_add_input() { sent_received.1 ); - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); } #[test] @@ -2243,7 +2256,7 @@ fn test_bump_fee_unconfirmed_inputs_only() { .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(25.0)); + builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25)); builder.finish().unwrap(); } @@ -2278,7 +2291,7 @@ fn test_bump_fee_unconfirmed_input() { let mut builder = wallet.build_fee_bump(txid).unwrap(); builder - .fee_rate(FeeRate::from_sat_per_vb(15.0)) + .fee_rate(FeeRate::from_sat_per_vb_unchecked(15)) .allow_shrinking(addr.script_pubkey()) .unwrap(); builder.finish().unwrap(); @@ -2298,7 +2311,7 @@ fn test_fee_amount_negative_drain_val() { let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") .unwrap() .assume_checked(); - let fee_rate = FeeRate::from_sat_per_vb(2.01); + let fee_rate = feerate_unchecked(2.01); let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); let mut builder = wallet.build_tx(); @@ -3499,7 +3512,7 @@ fn test_fee_rate_sign_no_grinding_high_r() { // alright. let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_address(New); - let fee_rate = FeeRate::from_sat_per_vb(1.0); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); let mut builder = wallet.build_tx(); let mut data = PushBytesBuf::try_from(vec![0]).unwrap(); builder @@ -3565,7 +3578,7 @@ fn test_fee_rate_sign_grinding_low_r() { // signature is 70 bytes. let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_address(New); - let fee_rate = FeeRate::from_sat_per_vb(1.0); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); let mut builder = wallet.build_tx(); builder .drain_to(addr.script_pubkey()) diff --git a/crates/hwi/src/lib.rs b/crates/hwi/src/lib.rs index 079066878..4e2cd0c0f 100644 --- a/crates/hwi/src/lib.rs +++ b/crates/hwi/src/lib.rs @@ -7,7 +7,7 @@ //! # use bdk::signer::SignerOrdering; //! # use bdk_hwi::HWISigner; //! # use bdk::wallet::AddressIndex::New; -//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet}; +//! # use bdk::{KeychainKind, SignOptions, Wallet}; //! # use hwi::HWIClient; //! # use std::sync::Arc; //! # From 09bd86e2d82020d5f4079c5d234888a9eed50489 Mon Sep 17 00:00:00 2001 From: vmammal Date: Thu, 16 Nov 2023 18:09:20 -0500 Subject: [PATCH 2/5] test(bdk): initialize all feerates from `u64` This makes the helper `feerate_unchecked` now redundant but still usable. --- crates/bdk/tests/common.rs | 15 ++++++++++++++- crates/bdk/tests/psbt.rs | 13 ++++--------- crates/bdk/tests/wallet.rs | 18 +++++++----------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs index 3e0292a29..c236ef186 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -4,7 +4,7 @@ use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet}; use bdk_chain::indexed_tx_graph::Indexer; use bdk_chain::{BlockId, ConfirmationTime}; use bitcoin::hashes::Hash; -use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid}; +use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut, Txid}; use std::str::FromStr; // Return a fake wallet that appears to be funded for testing. @@ -154,3 +154,16 @@ pub fn get_test_tr_with_taptree_xprv() -> &'static str { pub fn get_test_tr_dup_keys() -> &'static str { "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" } + +/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is +/// useful in cases where we want to create a feerate from a `f64`, as the +/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer. +/// +/// **Note** this 'quick and dirty' conversion should only be used when the input +/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow, +/// or else the resulting value will be inaccurate. +fn feerate_unchecked(sat_vb: f64) -> FeeRate { + // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu + let sat_kwu = (sat_vb * 250.0).ceil() as u64; + FeeRate::from_sat_per_kwu(sat_kwu) +} diff --git a/crates/bdk/tests/psbt.rs b/crates/bdk/tests/psbt.rs index e3ace9db0..935ddb11f 100644 --- a/crates/bdk/tests/psbt.rs +++ b/crates/bdk/tests/psbt.rs @@ -1,3 +1,4 @@ +use bdk::bitcoin::FeeRate; use bdk::bitcoin::TxIn; use bdk::wallet::AddressIndex; use bdk::wallet::AddressIndex::New; @@ -10,12 +11,6 @@ use common::*; // from bip 174 const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA"; -fn feerate_unchecked(sat_vb: f64) -> bitcoin::FeeRate { - // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu - let sat_kwu = (sat_vb * 250.0).ceil() as u64; - bitcoin::FeeRate::from_sat_per_kwu(sat_kwu) -} - #[test] #[should_panic(expected = "InputIndexOutOfRange")] fn test_psbt_malformed_psbt_input_legacy() { @@ -88,7 +83,7 @@ fn test_psbt_sign_with_finalized() { fn test_psbt_fee_rate_with_witness_utxo() { use psbt::PsbtUtils; - let expected_fee_rate = feerate_unchecked(1.2345); + let expected_fee_rate = FeeRate::from_sat_per_kwu(310); let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_address(New); @@ -113,7 +108,7 @@ fn test_psbt_fee_rate_with_witness_utxo() { fn test_psbt_fee_rate_with_nonwitness_utxo() { use psbt::PsbtUtils; - let expected_fee_rate = feerate_unchecked(1.2345); + let expected_fee_rate = FeeRate::from_sat_per_kwu(310); let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_address(New); @@ -137,7 +132,7 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() { fn test_psbt_fee_rate_with_missing_txout() { use psbt::PsbtUtils; - let expected_fee_rate = feerate_unchecked(1.2345); + let expected_fee_rate = FeeRate::from_sat_per_kwu(310); let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wpkh_wallet.get_address(New); diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index a8a2c9282..a0a916161 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -56,12 +56,6 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { receive_output(wallet, value, anchor) } -fn feerate_unchecked(sat_vb: f64) -> FeeRate { - // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu - let sat_kwu = (sat_vb * 250.0).ceil() as u64; - FeeRate::from_sat_per_kwu(sat_kwu) -} - // The satisfaction size of a P2WPKH is 112 WU = // 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len) // On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for @@ -1581,8 +1575,9 @@ fn test_bump_fee_reduce_change() { .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) .unwrap(); + 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_unchecked(2.5)).enable_rbf(); + builder.fee_rate(feerate).enable_rbf(); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); let fee = check_fee!(wallet, psbt); @@ -1613,7 +1608,7 @@ fn test_bump_fee_reduce_change() { sent_received.1 ); - assert_fee_rate!(psbt, fee.unwrap_or(0), feerate_unchecked(2.5), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_absolute(200); @@ -1676,9 +1671,10 @@ fn test_bump_fee_reduce_single_recipient() { .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) .unwrap(); + 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_unchecked(2.5)) + .fee_rate(feerate) .allow_shrinking(addr.script_pubkey()) .unwrap(); let psbt = builder.finish().unwrap(); @@ -1692,7 +1688,7 @@ fn test_bump_fee_reduce_single_recipient() { assert_eq!(tx.output.len(), 1); assert_eq!(tx.output[0].value + fee.unwrap_or(0), sent_received.0); - assert_fee_rate!(psbt, fee.unwrap_or(0), feerate_unchecked(2.5), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature); } #[test] @@ -2311,7 +2307,7 @@ fn test_fee_amount_negative_drain_val() { let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") .unwrap() .assume_checked(); - let fee_rate = feerate_unchecked(2.01); + let fee_rate = FeeRate::from_sat_per_kwu(500); let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); let mut builder = wallet.build_tx(); From 89608ddd0fd582d011f7b7ae8bbeb63dee6a3b99 Mon Sep 17 00:00:00 2001 From: vmammal Date: Thu, 16 Nov 2023 18:13:44 -0500 Subject: [PATCH 3/5] refactor(bdk): display CreateTxError::FeeRateTooLow in sat/vb Also modify a unit test `test_bump_fee_low_fee_rate` to additionally assert the expected error message --- crates/bdk/src/wallet/error.rs | 6 ++++-- crates/bdk/src/wallet/mod.rs | 11 +++++++++++ crates/bdk/tests/wallet.rs | 16 +++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/bdk/src/wallet/error.rs b/crates/bdk/src/wallet/error.rs index 548dc783f..46cf8ef3c 100644 --- a/crates/bdk/src/wallet/error.rs +++ b/crates/bdk/src/wallet/error.rs @@ -168,8 +168,10 @@ where CreateTxError::FeeRateTooLow { required } => { write!( f, - "Fee rate too low: required {} sat/kwu", - required.to_sat_per_kwu() + // Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31 + //"Fee rate too low: required {required:#}" + "Fee rate too low: required {} sat/vb", + crate::floating_rate!(required) ) } CreateTxError::NoUtxosSelected => { diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 3b133602f..8cb1bbebc 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -2563,6 +2563,17 @@ fn create_signers( Ok((signers, change_signers)) } +/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb. +#[macro_export] +#[doc(hidden)] +macro_rules! floating_rate { + ($rate:expr) => {{ + use $crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; + // sat_kwu / 250.0 -> sat_vb + $rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64) + }}; +} + #[macro_export] #[doc(hidden)] /// Macro for getting a wallet for use in a doctest diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index a0a916161..821afd757 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -1488,7 +1488,6 @@ fn test_bump_fee_confirmed_tx() { } #[test] -#[should_panic(expected = "FeeRateTooLow")] fn test_bump_fee_low_fee_rate() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_address(New); @@ -1497,6 +1496,7 @@ fn test_bump_fee_low_fee_rate() { .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf(); let psbt = builder.finish().unwrap(); + let feerate = psbt.fee_rate().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1506,8 +1506,18 @@ fn test_bump_fee_low_fee_rate() { .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(1)); - builder.finish().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] From 0d64beb040e885d653adb901dc49482f9daafddf Mon Sep 17 00:00:00 2001 From: vmammal Date: Thu, 16 Nov 2023 18:45:21 -0500 Subject: [PATCH 4/5] chore: organize some imports --- crates/bdk/src/wallet/mod.rs | 4 ++-- crates/bdk/src/wallet/tx_builder.rs | 16 +++++++--------- crates/bdk/tests/common.rs | 2 +- crates/bdk/tests/wallet.rs | 13 ++++++------- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 8cb1bbebc..3e3a2944e 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -33,8 +33,8 @@ use bdk_chain::{ use bitcoin::secp256k1::{All, Secp256k1}; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::{ - absolute, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, - Txid, Weight, Witness, + absolute, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, + TxOut, Txid, Weight, Witness, }; use bitcoin::{consensus::encode::serialize, BlockHash}; use bitcoin::{constants::genesis_block, psbt}; diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index aa81d1f35..4a7415c3b 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -40,23 +40,21 @@ //! # Ok::<(), anyhow::Error>(()) //! ``` -use crate::collections::BTreeMap; -use crate::collections::HashSet; use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; -use bdk_chain::PersistBackend; use core::cell::RefCell; use core::fmt; use core::marker::PhantomData; +use bdk_chain::PersistBackend; use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt}; -use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid}; +use bitcoin::script::PushBytes; +use bitcoin::{absolute, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; -use super::ChangeSet; -use crate::types::{KeychainKind, LocalOutput, WeightedUtxo}; -use crate::wallet::CreateTxError; -use crate::{Utxo, Wallet}; -use bitcoin::FeeRate; +use super::{ChangeSet, CreateTxError, Wallet}; +use crate::collections::{BTreeMap, HashSet}; +use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; + /// Context in which the [`TxBuilder`] is valid pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs index c236ef186..b4012286b 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -162,7 +162,7 @@ pub fn get_test_tr_dup_keys() -> &'static str { /// **Note** this 'quick and dirty' conversion should only be used when the input /// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow, /// or else the resulting value will be inaccurate. -fn feerate_unchecked(sat_vb: f64) -> FeeRate { +pub fn feerate_unchecked(sat_vb: f64) -> FeeRate { // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu let sat_kwu = (sat_vb * 250.0).ceil() as u64; FeeRate::from_sat_per_kwu(sat_kwu) diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 821afd757..938d2f90e 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -13,16 +13,15 @@ use bdk::KeychainKind; use bdk_chain::COINBASE_MATURITY; use bdk_chain::{BlockId, ConfirmationTime}; use bitcoin::hashes::Hash; +use bitcoin::psbt; +use bitcoin::script::PushBytesBuf; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; -use bitcoin::Amount; -use bitcoin::FeeRate; -use bitcoin::ScriptBuf; +use bitcoin::taproot::TapNodeHash; use bitcoin::{ - absolute, script::PushBytesBuf, taproot::TapNodeHash, Address, OutPoint, Sequence, Transaction, - TxIn, TxOut, Weight, + absolute, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Sequence, + Transaction, TxIn, TxOut, Txid, Weight, }; -use bitcoin::{psbt, Network}; -use bitcoin::{BlockHash, Txid}; + mod common; use common::*; From 475a77219a6e7649c91b7cfeb44871cee9d0f374 Mon Sep 17 00:00:00 2001 From: vmammal Date: Sat, 2 Mar 2024 09:48:16 -0500 Subject: [PATCH 5/5] refactor(bdk)!: Remove trait Vbytes The only place this is being used is a unit test that is easily refactored. For size conversions prefer methods on e.g. `Weight`. --- crates/bdk/src/types.rs | 13 ------------- crates/bdk/src/wallet/coin_selection.rs | 17 ++++++----------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/crates/bdk/src/types.rs b/crates/bdk/src/types.rs index 3bde290e4..4ce961b7e 100644 --- a/crates/bdk/src/types.rs +++ b/crates/bdk/src/types.rs @@ -46,19 +46,6 @@ impl AsRef<[u8]> for KeychainKind { } } -/// Trait implemented by types that can be used to measure weight units. -pub trait Vbytes { - /// Convert weight units to virtual bytes. - fn vbytes(self) -> usize; -} - -impl Vbytes for usize { - fn vbytes(self) -> usize { - // ref: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations - (self as f32 / 4.0).ceil() as usize - } -} - /// An unspent output owned by a [`Wallet`]. /// /// [`Wallet`]: crate::Wallet diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index 5383e5525..5122a1493 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -744,12 +744,11 @@ mod test { use core::str::FromStr; use bdk_chain::ConfirmationTime; - use bitcoin::{OutPoint, ScriptBuf, TxOut}; + use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut}; use super::*; use crate::types::*; use crate::wallet::coin_selection::filter_duplicates; - use crate::wallet::Vbytes; use rand::rngs::StdRng; use rand::seq::SliceRandom; @@ -1233,22 +1232,18 @@ mod test { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); let target_amount = 99932; // first utxo's effective value + let feerate = FeeRate::BROADCAST_MIN; let result = BranchAndBoundCoinSelection::new(0) - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) + .coin_select(vec![], utxos, feerate, target_amount, &drain_script) .unwrap(); assert_eq!(result.selected.len(), 1); assert_eq!(result.selected_amount(), 100_000); - let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes(); + let input_weight = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE) as u64; // the final fee rate should be exactly the same as the fee rate given - assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON); + let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight); + assert_eq!(result_feerate, feerate); } #[test]