//! 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`?
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
})
}
}
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};
}
}
-/// 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.
}
}
}
-
-#[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);
- }
-}
//! &self,
//! required_utxos: Vec<WeightedUtxo>,
//! optional_utxos: Vec<WeightedUtxo>,
-//! fee_rate: bdk::FeeRate,
+//! fee_rate: FeeRate,
//! target_amount: u64,
//! drain_script: &Script,
//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
//! },
//! )
//! .collect::<Vec<_>>();
-//! 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 {
//! ```
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;
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) {
(&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 {
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,
.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
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<CoinSelectionResult, Error> {
.coin_select(
utxos,
vec![],
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
utxos,
vec![],
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1000.0),
+ FeeRate::from_sat_per_vb_unchecked(1000),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
utxos,
vec![],
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1000.0),
+ FeeRate::from_sat_per_vb_unchecked(1000),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
utxos.clone(),
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
required,
optional,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1000.0),
+ FeeRate::from_sat_per_vb_unchecked(1000),
target_amount,
&drain_script,
)
.coin_select(
vec![],
utxos,
- FeeRate::from_sat_per_vb(1.0),
+ FeeRate::from_sat_per_vb_unchecked(1),
target_amount,
&drain_script,
)
.coin_select(
vec![],
optional_utxos,
- FeeRate::from_sat_per_vb(0.0),
+ FeeRate::ZERO,
target_amount,
&drain_script,
)
#[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<OutputGroup> = get_test_utxos()
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
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;
#[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<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
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();
// 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()
// 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();
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)
curr_value,
curr_available_value,
target_amount,
- 0.0,
+ 0,
&drain_script,
fee_rate,
)
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<OutputGroup> = utxos
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
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,
);
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,
);
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,
);
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;
},
/// 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,
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 => {
//! # 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;
//! #
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};
/// ```
/// [`insert_txout`]: Self::insert_txout
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
- 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).
(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)
}
};
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).
// 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()
/// 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()?
/// };
///
utxos: original_utxos,
bumping_fee: Some(tx_builder::PreviousFee {
absolute: fee,
- rate: fee_rate.as_sat_per_vb(),
+ rate: fee_rate,
}),
..Default::default()
};
//! // 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
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 {}
#[derive(Clone, Copy, Debug)]
pub(crate) struct PreviousFee {
pub absolute: u64,
- pub rate: f32,
+ pub rate: FeeRate,
}
#[derive(Debug, Clone, Copy)]
impl Default for FeePolicy {
fn default() -> Self {
- FeePolicy::FeeRate(FeeRate::default_min_relay_fee())
+ FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
}
}
// 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
/// .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>(())
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;
// 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() {
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());
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());
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;
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;
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,
};
use bitcoin::{psbt, Network};
use bitcoin::{BlockHash, Txid};
-
mod common;
use common::*;
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
// 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]
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);
}
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]
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]
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();
}
.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();
}
.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);
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);
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();
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]
.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());
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();
}
.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);
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]
// 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);
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]
// 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);
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]
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);
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]
.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();
}
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();
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();
// 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
// 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())
//! # 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;
//! #