Make FeeRate from sat constructors infallible

We now have constructors that take an arbitrary size fee
rate (`Amount`). The `from_sat_per_foo` constructors can be made
infallible by taking a `u32` instead of `u64`. This makes the API more
ergonomic but limits the fee rate to just under 42 BTC which is plenty.

Note we just delete the `from_sat_per_vb_u32` function because it is
unreleased, in the past we had `from_sat_per_vb_unchecked` so we could
put that back in if we wanted to be a bit more kind to downstream. Can
be done later, we likely want to go over the public API before release
and add a few things back in that we forgot to deprecate or could not
for some reason during dev.

Fuzz with a new function that consumes a `u32`.
This commit is contained in:
Tobin C. Harding 2025-06-16 09:14:47 +10:00
parent 3b0286bd56
commit 0ff8d82193
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
11 changed files with 120 additions and 128 deletions

View File

@ -751,7 +751,7 @@ fn default_dust_value() {
assert!(script_p2wpkh.is_p2wpkh()); assert!(script_p2wpkh.is_p2wpkh());
assert_eq!(script_p2wpkh.minimal_non_dust(), Amount::from_sat_u32(294)); assert_eq!(script_p2wpkh.minimal_non_dust(), Amount::from_sat_u32(294));
assert_eq!( assert_eq!(
script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_u32(6)), script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb(6)),
Some(Amount::from_sat_u32(588)) Some(Amount::from_sat_u32(588))
); );
@ -765,7 +765,7 @@ fn default_dust_value() {
assert!(script_p2pkh.is_p2pkh()); assert!(script_p2pkh.is_p2pkh());
assert_eq!(script_p2pkh.minimal_non_dust(), Amount::from_sat_u32(546)); assert_eq!(script_p2pkh.minimal_non_dust(), Amount::from_sat_u32(546));
assert_eq!( assert_eq!(
script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_u32(6)), script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb(6)),
Some(Amount::from_sat_u32(1092)) Some(Amount::from_sat_u32(1092))
); );
} }

View File

@ -1689,7 +1689,7 @@ mod tests {
#[test] #[test]
fn effective_value_happy_path() { fn effective_value_happy_path() {
let value = "1 cBTC".parse::<Amount>().unwrap(); let value = "1 cBTC".parse::<Amount>().unwrap();
let fee_rate = FeeRate::from_sat_per_kwu(10).unwrap(); let fee_rate = FeeRate::from_sat_per_kwu(10);
let effective_value = effective_value(fee_rate, InputWeightPrediction::P2WPKH_MAX, value); let effective_value = effective_value(fee_rate, InputWeightPrediction::P2WPKH_MAX, value);
// 10 sat/kwu * 272 wu = 3 sats (rounding up) // 10 sat/kwu * 272 wu = 3 sats (rounding up)

View File

@ -127,7 +127,7 @@ impl Psbt {
/// 1000 sats/vByte. 25k sats/vByte is obviously a mistake at this point. /// 1000 sats/vByte. 25k sats/vByte is obviously a mistake at this point.
/// ///
/// [`extract_tx`]: Psbt::extract_tx /// [`extract_tx`]: Psbt::extract_tx
pub const DEFAULT_MAX_FEE_RATE: FeeRate = FeeRate::from_sat_per_vb_u32(25_000); pub const DEFAULT_MAX_FEE_RATE: FeeRate = FeeRate::from_sat_per_vb(25_000);
/// An alias for [`extract_tx_fee_rate_limit`]. /// An alias for [`extract_tx_fee_rate_limit`].
/// ///
@ -1437,7 +1437,7 @@ mod tests {
// Large fee rate errors if we pass in 1 sat/vb so just use this to get the error fee rate returned. // Large fee rate errors if we pass in 1 sat/vb so just use this to get the error fee rate returned.
let error_fee_rate = psbt let error_fee_rate = psbt
.clone() .clone()
.extract_tx_with_fee_rate_limit(FeeRate::from_sat_per_vb_u32(1)) .extract_tx_with_fee_rate_limit(FeeRate::from_sat_per_vb(1))
.map_err(|e| match e { .map_err(|e| match e {
ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate, ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate,
_ => panic!(""), _ => panic!(""),
@ -1475,7 +1475,7 @@ mod tests {
ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate, ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate,
_ => panic!(""), _ => panic!(""),
}), }),
Err(FeeRate::from_sat_per_kwu(6250003).unwrap()) // 6250000 is 25k sat/vbyte Err(FeeRate::from_sat_per_kwu(6250003)) // 6250000 is 25k sat/vbyte
); );
// Lowering the input satoshis by 1 lowers the sat/kwu by 3 // Lowering the input satoshis by 1 lowers the sat/kwu by 3

View File

@ -2,7 +2,7 @@ use bitcoin::address::Address;
use bitcoin::consensus::encode; use bitcoin::consensus::encode;
use bitcoin::script::{self, ScriptExt as _}; use bitcoin::script::{self, ScriptExt as _};
use bitcoin::{FeeRate, Network}; use bitcoin::{FeeRate, Network};
use bitcoin_fuzz::fuzz_utils::{consume_random_bytes, consume_u64}; use bitcoin_fuzz::fuzz_utils::{consume_random_bytes, consume_u32};
use honggfuzz::fuzz; use honggfuzz::fuzz;
fn do_test(data: &[u8]) { fn do_test(data: &[u8]) {
@ -17,7 +17,7 @@ fn do_test(data: &[u8]) {
let _ = script.count_sigops_legacy(); let _ = script.count_sigops_legacy();
let _ = script.minimal_non_dust(); let _ = script.minimal_non_dust();
let fee_rate = FeeRate::from_sat_per_kwu(consume_u64(&mut new_data)).unwrap(); let fee_rate = FeeRate::from_sat_per_kwu(consume_u32(&mut new_data));
let _ = script.minimal_non_dust_custom(fee_rate); let _ = script.minimal_non_dust_custom(fee_rate);
let mut b = script::Builder::new(); let mut b = script::Builder::new();

View File

@ -35,3 +35,16 @@ pub fn consume_u64(data: &mut &[u8]) -> u64 {
u64_bytes[7], u64_bytes[7],
]) ])
} }
#[allow(dead_code)]
pub fn consume_u32(data: &mut &[u8]) -> u32 {
// We need at least 4 bytes to read a u32
if data.len() < 4 {
return 0;
}
let (u32_bytes, rest) = data.split_at(4);
*data = rest;
u32::from_le_bytes([u32_bytes[0], u32_bytes[1], u32_bytes[2], u32_bytes[3]])
}

View File

@ -270,13 +270,13 @@ fn amount_checked_div_by_weight_ceil() {
let weight = Weight::from_kwu(1).unwrap(); let weight = Weight::from_kwu(1).unwrap();
let fee_rate = sat(1).div_by_weight_ceil(weight).unwrap(); let fee_rate = sat(1).div_by_weight_ceil(weight).unwrap();
// 1 sats / 1,000 wu = 1 sats/kwu // 1 sats / 1,000 wu = 1 sats/kwu
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1).unwrap()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1));
let weight = Weight::from_wu(381); let weight = Weight::from_wu(381);
let fee_rate = sat(329).div_by_weight_ceil(weight).unwrap(); let fee_rate = sat(329).div_by_weight_ceil(weight).unwrap();
// 329 sats / 381 wu = 863.5 sats/kwu // 329 sats / 381 wu = 863.5 sats/kwu
// round up to 864 // round up to 864
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(864).unwrap()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(864));
let fee_rate = Amount::ONE_SAT.div_by_weight_ceil(Weight::ZERO); let fee_rate = Amount::ONE_SAT.div_by_weight_ceil(Weight::ZERO);
assert!(fee_rate.is_error()); assert!(fee_rate.is_error());
@ -288,13 +288,13 @@ fn amount_checked_div_by_weight_floor() {
let weight = Weight::from_kwu(1).unwrap(); let weight = Weight::from_kwu(1).unwrap();
let fee_rate = sat(1).div_by_weight_floor(weight).unwrap(); let fee_rate = sat(1).div_by_weight_floor(weight).unwrap();
// 1 sats / 1,000 wu = 1 sats/kwu // 1 sats / 1,000 wu = 1 sats/kwu
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1).unwrap()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1));
let weight = Weight::from_wu(381); let weight = Weight::from_wu(381);
let fee_rate = sat(329).div_by_weight_floor(weight).unwrap(); let fee_rate = sat(329).div_by_weight_floor(weight).unwrap();
// 329 sats / 381 wu = 863.5 sats/kwu // 329 sats / 381 wu = 863.5 sats/kwu
// round down to 863 // round down to 863
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863).unwrap()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863));
let fee_rate = Amount::ONE_SAT.div_by_weight_floor(Weight::ZERO); let fee_rate = Amount::ONE_SAT.div_by_weight_floor(Weight::ZERO);
assert!(fee_rate.is_error()); assert!(fee_rate.is_error());
@ -304,7 +304,7 @@ fn amount_checked_div_by_weight_floor() {
#[test] #[test]
fn amount_checked_div_by_fee_rate() { fn amount_checked_div_by_fee_rate() {
let amount = sat(1000); let amount = sat(1000);
let fee_rate = FeeRate::from_sat_per_kwu(2).unwrap(); let fee_rate = FeeRate::from_sat_per_kwu(2);
// Test floor division // Test floor division
let weight = amount.div_by_fee_rate_floor(fee_rate).unwrap(); let weight = amount.div_by_fee_rate_floor(fee_rate).unwrap();
@ -317,20 +317,20 @@ fn amount_checked_div_by_fee_rate() {
// Test truncation behavior // Test truncation behavior
let amount = sat(1000); let amount = sat(1000);
let fee_rate = FeeRate::from_sat_per_kwu(3).unwrap(); let fee_rate = FeeRate::from_sat_per_kwu(3);
let floor_weight = amount.div_by_fee_rate_floor(fee_rate).unwrap(); let floor_weight = amount.div_by_fee_rate_floor(fee_rate).unwrap();
let ceil_weight = amount.div_by_fee_rate_ceil(fee_rate).unwrap(); let ceil_weight = amount.div_by_fee_rate_ceil(fee_rate).unwrap();
assert_eq!(floor_weight, Weight::from_wu(333_333)); assert_eq!(floor_weight, Weight::from_wu(333_333));
assert_eq!(ceil_weight, Weight::from_wu(333_334)); assert_eq!(ceil_weight, Weight::from_wu(333_334));
// Test division by zero // Test division by zero
let zero_fee_rate = FeeRate::from_sat_per_kwu(0).unwrap(); let zero_fee_rate = FeeRate::from_sat_per_kwu(0);
assert!(amount.div_by_fee_rate_floor(zero_fee_rate).is_error()); assert!(amount.div_by_fee_rate_floor(zero_fee_rate).is_error());
assert!(amount.div_by_fee_rate_ceil(zero_fee_rate).is_error()); assert!(amount.div_by_fee_rate_ceil(zero_fee_rate).is_error());
// Test with maximum amount // Test with maximum amount
let max_amount = Amount::MAX; let max_amount = Amount::MAX;
let small_fee_rate = FeeRate::from_sat_per_kwu(1).unwrap(); let small_fee_rate = FeeRate::from_sat_per_kwu(1);
let weight = max_amount.div_by_fee_rate_floor(small_fee_rate).unwrap(); let weight = max_amount.div_by_fee_rate_floor(small_fee_rate).unwrap();
// 21_000_000_0000_0000 sats / (1 sat/kwu) = 2_100_000_000_000_000_000 wu // 21_000_000_0000_0000 sats / (1 sat/kwu) = 2_100_000_000_000_000_000 wu
assert_eq!(weight, Weight::from_wu(2_100_000_000_000_000_000)); assert_eq!(weight, Weight::from_wu(2_100_000_000_000_000_000));

View File

@ -439,7 +439,7 @@ impl Amount {
/// let amount = Amount::from_sat(10)?; /// let amount = Amount::from_sat(10)?;
/// let weight = Weight::from_wu(300); /// let weight = Weight::from_wu(300);
/// let fee_rate = amount.div_by_weight_ceil(weight).expect("valid fee rate"); /// let fee_rate = amount.div_by_weight_ceil(weight).expect("valid fee rate");
/// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(34).expect("valid fee rate")); /// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(34));
/// # Ok::<_, amount::OutOfRangeError>(()) /// # Ok::<_, amount::OutOfRangeError>(())
/// ``` /// ```
pub const fn div_by_weight_ceil(self, weight: Weight) -> NumOpResult<FeeRate> { pub const fn div_by_weight_ceil(self, weight: Weight) -> NumOpResult<FeeRate> {

View File

@ -190,12 +190,12 @@ mod tests {
#[test] #[test]
fn fee_rate_div_by_weight() { fn fee_rate_div_by_weight() {
let fee_rate = (Amount::from_sat_u32(329) / Weight::from_wu(381)).unwrap(); let fee_rate = (Amount::from_sat_u32(329) / Weight::from_wu(381)).unwrap();
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863).unwrap()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863));
} }
#[test] #[test]
fn fee_wu() { fn fee_wu() {
let fee_rate = FeeRate::from_sat_per_vb(2).unwrap(); let fee_rate = FeeRate::from_sat_per_vb(2);
let weight = Weight::from_vb(3).unwrap(); let weight = Weight::from_vb(3).unwrap();
assert_eq!(fee_rate.to_fee(weight), Amount::from_sat_u32(6)); assert_eq!(fee_rate.to_fee(weight), Amount::from_sat_u32(6));
} }
@ -204,19 +204,19 @@ mod tests {
fn weight_mul() { fn weight_mul() {
let weight = Weight::from_vb(10).unwrap(); let weight = Weight::from_vb(10).unwrap();
let fee: Amount = let fee: Amount =
FeeRate::from_sat_per_vb(10).unwrap().mul_by_weight(weight).expect("expected Amount"); FeeRate::from_sat_per_vb(10).mul_by_weight(weight).expect("expected Amount");
assert_eq!(Amount::from_sat_u32(100), fee); assert_eq!(Amount::from_sat_u32(100), fee);
let fee = FeeRate::from_sat_per_kwu(10).unwrap().mul_by_weight(Weight::MAX); let fee = FeeRate::from_sat_per_kwu(10).mul_by_weight(Weight::MAX);
assert!(fee.is_error()); assert!(fee.is_error());
let weight = Weight::from_vb(3).unwrap(); let weight = Weight::from_vb(3).unwrap();
let fee_rate = FeeRate::from_sat_per_vb(3).unwrap(); let fee_rate = FeeRate::from_sat_per_vb(3);
let fee = fee_rate.mul_by_weight(weight).unwrap(); let fee = fee_rate.mul_by_weight(weight).unwrap();
assert_eq!(Amount::from_sat_u32(9), fee); assert_eq!(Amount::from_sat_u32(9), fee);
let weight = Weight::from_wu(381); let weight = Weight::from_wu(381);
let fee_rate = FeeRate::from_sat_per_kwu(864).unwrap(); let fee_rate = FeeRate::from_sat_per_kwu(864);
let fee = weight.mul_by_fee_rate(fee_rate).unwrap(); let fee = weight.mul_by_fee_rate(fee_rate).unwrap();
// 381 * 0.864 yields 329.18. // 381 * 0.864 yields 329.18.
// The result is then rounded up to 330. // The result is then rounded up to 330.
@ -226,7 +226,7 @@ mod tests {
#[test] #[test]
#[allow(clippy::op_ref)] #[allow(clippy::op_ref)]
fn multiply() { fn multiply() {
let two = FeeRate::from_sat_per_vb(2).unwrap(); let two = FeeRate::from_sat_per_vb(2);
let three = Weight::from_vb(3).unwrap(); let three = Weight::from_vb(3).unwrap();
let six = Amount::from_sat_u32(6); let six = Amount::from_sat_u32(6);
@ -243,9 +243,9 @@ mod tests {
fn amount_div_by_fee_rate() { fn amount_div_by_fee_rate() {
// Test exact division // Test exact division
let amount = Amount::from_sat_u32(1000); let amount = Amount::from_sat_u32(1000);
let fee_rate = FeeRate::from_sat_per_kwu(2).unwrap(); let fee_rate = FeeRate::from_sat_per_kwu(2);
let weight = (amount / fee_rate).unwrap(); let weight = amount / fee_rate;
assert_eq!(weight, Weight::from_wu(500_000)); assert_eq!(weight.unwrap(), Weight::from_wu(500_000));
// Test reference division // Test reference division
let weight_ref = (&amount / fee_rate).unwrap(); let weight_ref = (&amount / fee_rate).unwrap();
@ -257,19 +257,19 @@ mod tests {
// Test truncation behavior // Test truncation behavior
let amount = Amount::from_sat_u32(1000); let amount = Amount::from_sat_u32(1000);
let fee_rate = FeeRate::from_sat_per_kwu(3).unwrap(); let fee_rate = FeeRate::from_sat_per_kwu(3);
let weight = (amount / fee_rate).unwrap(); let weight = amount / fee_rate;
// 1000 * 1000 = 1,000,000 msats // 1000 * 1000 = 1,000,000 msats
// 1,000,000 / 3 = 333,333.33... wu // 1,000,000 / 3 = 333,333.33... wu
// Should truncate down to 333,333 wu // Should truncate down to 333,333 wu
assert_eq!(weight, Weight::from_wu(333_333)); assert_eq!(weight.unwrap(), Weight::from_wu(333_333));
// Verify that ceiling division gives different result // Verify that ceiling division gives different result
let ceil_weight = amount.div_by_fee_rate_ceil(fee_rate).unwrap(); let ceil_weight = amount.div_by_fee_rate_ceil(fee_rate).unwrap();
assert_eq!(ceil_weight, Weight::from_wu(333_334)); assert_eq!(ceil_weight, Weight::from_wu(333_334));
// Test that division by zero returns None // Test that division by zero returns None
let zero_rate = FeeRate::from_sat_per_kwu(0).unwrap(); let zero_rate = FeeRate::from_sat_per_kwu(0);
assert!(amount.div_by_fee_rate_floor(zero_rate).is_error()); assert!(amount.div_by_fee_rate_floor(zero_rate).is_error());
assert!(amount.div_by_fee_rate_ceil(zero_rate).is_error()); assert!(amount.div_by_fee_rate_ceil(zero_rate).is_error());
} }

View File

@ -50,19 +50,15 @@ impl FeeRate {
/// The minimum fee rate required to broadcast a transaction. /// The minimum fee rate required to broadcast a transaction.
/// ///
/// The value matches the default Bitcoin Core policy at the time of library release. /// The value matches the default Bitcoin Core policy at the time of library release.
pub const BROADCAST_MIN: FeeRate = FeeRate::from_sat_per_vb_u32(1); pub const BROADCAST_MIN: FeeRate = FeeRate::from_sat_per_vb(1);
/// The fee rate used to compute dust amount. /// The fee rate used to compute dust amount.
pub const DUST: FeeRate = FeeRate::from_sat_per_vb_u32(3); pub const DUST: FeeRate = FeeRate::from_sat_per_vb(3);
/// Constructs a new [`FeeRate`] from satoshis per 1000 weight units, /// Constructs a new [`FeeRate`] from satoshis per 1000 weight units.
/// returning `None` if overflow occurred. pub const fn from_sat_per_kwu(sat_kwu: u32) -> Self {
pub const fn from_sat_per_kwu(sat_kwu: u64) -> Option<Self> { let fee_rate = (sat_kwu as u64) * 4_000; // No `Into` in const context.
// No `map()` in const context. FeeRate::from_sat_per_mvb(fee_rate)
match sat_kwu.checked_mul(4_000) {
Some(fee_rate) => Some(FeeRate::from_sat_per_mvb(fee_rate)),
None => None,
}
} }
/// Constructs a new [`FeeRate`] from amount per 1000 weight units. /// Constructs a new [`FeeRate`] from amount per 1000 weight units.
@ -74,14 +70,10 @@ impl FeeRate {
} }
} }
/// Constructs a new [`FeeRate`] from satoshis per virtual byte, /// Constructs a new [`FeeRate`] from satoshis per virtual byte.
/// returning `None` if overflow occurred. pub const fn from_sat_per_vb(sat_vb: u32) -> Self {
pub const fn from_sat_per_vb(sat_vb: u64) -> Option<Self> { let fee_rate = (sat_vb as u64) * 1_000_000; // No `Into` in const context.
// No `map()` in const context. FeeRate::from_sat_per_mvb(fee_rate)
match sat_vb.checked_mul(1_000_000) {
Some(fee_rate) => Some(FeeRate::from_sat_per_mvb(fee_rate)),
None => None,
}
} }
/// Constructs a new [`FeeRate`] from amount per virtual byte. /// Constructs a new [`FeeRate`] from amount per virtual byte.
@ -93,20 +85,10 @@ impl FeeRate {
} }
} }
/// Constructs a new [`FeeRate`] from satoshis per virtual bytes. /// Constructs a new [`FeeRate`] from satoshis per kilo virtual bytes (1,000 vbytes).
pub const fn from_sat_per_vb_u32(sat_vb: u32) -> Self { pub const fn from_sat_per_kvb(sat_kvb: u32) -> Self {
let sat_vb = sat_vb as u64; // No `Into` in const context. let fee_rate = (sat_kvb as u64) * 1_000; // No `Into` in const context.
FeeRate::from_sat_per_mvb(sat_vb * 1_000_000) FeeRate::from_sat_per_mvb(fee_rate)
}
/// Constructs a new [`FeeRate`] from satoshis per kilo virtual bytes (1,000 vbytes),
/// returning `None` if overflow occurred.
pub const fn from_sat_per_kvb(sat_kvb: u64) -> Option<Self> {
// No `map()` in const context.
match sat_kvb.checked_mul(1_000) {
Some(fee_rate) => Some(FeeRate::from_sat_per_mvb(fee_rate)),
None => None,
}
} }
/// Constructs a new [`FeeRate`] from satoshis per kilo virtual bytes (1,000 vbytes). /// Constructs a new [`FeeRate`] from satoshis per kilo virtual bytes (1,000 vbytes).
@ -301,18 +283,18 @@ mod tests {
#[test] #[test]
#[allow(clippy::op_ref)] #[allow(clippy::op_ref)]
fn feerate_div_nonzero() { fn feerate_div_nonzero() {
let rate = FeeRate::from_sat_per_kwu(200).unwrap(); let rate = FeeRate::from_sat_per_kwu(200);
let divisor = NonZeroU64::new(2).unwrap(); let divisor = NonZeroU64::new(2).unwrap();
assert_eq!(rate / divisor, FeeRate::from_sat_per_kwu(100).unwrap()); assert_eq!(rate / divisor, FeeRate::from_sat_per_kwu(100));
assert_eq!(&rate / &divisor, FeeRate::from_sat_per_kwu(100).unwrap()); assert_eq!(&rate / &divisor, FeeRate::from_sat_per_kwu(100));
} }
#[test] #[test]
#[allow(clippy::op_ref)] #[allow(clippy::op_ref)]
fn addition() { fn addition() {
let one = FeeRate::from_sat_per_kwu(1).unwrap(); let one = FeeRate::from_sat_per_kwu(1);
let two = FeeRate::from_sat_per_kwu(2).unwrap(); let two = FeeRate::from_sat_per_kwu(2);
let three = FeeRate::from_sat_per_kwu(3).unwrap(); let three = FeeRate::from_sat_per_kwu(3);
assert!(one + two == three); assert!(one + two == three);
assert!(&one + two == three); assert!(&one + two == three);
@ -323,9 +305,9 @@ mod tests {
#[test] #[test]
#[allow(clippy::op_ref)] #[allow(clippy::op_ref)]
fn subtract() { fn subtract() {
let three = FeeRate::from_sat_per_kwu(3).unwrap(); let three = FeeRate::from_sat_per_kwu(3);
let seven = FeeRate::from_sat_per_kwu(7).unwrap(); let seven = FeeRate::from_sat_per_kwu(7);
let ten = FeeRate::from_sat_per_kwu(10).unwrap(); let ten = FeeRate::from_sat_per_kwu(10);
assert_eq!(ten - seven, three); assert_eq!(ten - seven, three);
assert_eq!(&ten - seven, three); assert_eq!(&ten - seven, three);
@ -335,44 +317,45 @@ mod tests {
#[test] #[test]
fn add_assign() { fn add_assign() {
let mut f = FeeRate::from_sat_per_kwu(1).unwrap(); let mut f = FeeRate::from_sat_per_kwu(1);
f += FeeRate::from_sat_per_kwu(2).unwrap(); f += FeeRate::from_sat_per_kwu(2);
assert_eq!(f, FeeRate::from_sat_per_kwu(3).unwrap()); assert_eq!(f, FeeRate::from_sat_per_kwu(3));
let mut f = FeeRate::from_sat_per_kwu(1).unwrap(); let mut f = FeeRate::from_sat_per_kwu(1);
f += &FeeRate::from_sat_per_kwu(2).unwrap(); f += &FeeRate::from_sat_per_kwu(2);
assert_eq!(f, FeeRate::from_sat_per_kwu(3).unwrap()); assert_eq!(f, FeeRate::from_sat_per_kwu(3));
} }
#[test] #[test]
fn sub_assign() { fn sub_assign() {
let mut f = FeeRate::from_sat_per_kwu(3).unwrap(); let mut f = FeeRate::from_sat_per_kwu(3);
f -= FeeRate::from_sat_per_kwu(2).unwrap(); f -= FeeRate::from_sat_per_kwu(2);
assert_eq!(f, FeeRate::from_sat_per_kwu(1).unwrap()); assert_eq!(f, FeeRate::from_sat_per_kwu(1));
let mut f = FeeRate::from_sat_per_kwu(3).unwrap(); let mut f = FeeRate::from_sat_per_kwu(3);
f -= &FeeRate::from_sat_per_kwu(2).unwrap(); f -= &FeeRate::from_sat_per_kwu(2);
assert_eq!(f, FeeRate::from_sat_per_kwu(1).unwrap()); assert_eq!(f, FeeRate::from_sat_per_kwu(1));
} }
#[test] #[test]
fn checked_add() { fn checked_add() {
let one = FeeRate::from_sat_per_kwu(1).unwrap(); let one = FeeRate::from_sat_per_kwu(1);
let two = FeeRate::from_sat_per_kwu(2).unwrap(); let two = FeeRate::from_sat_per_kwu(2);
let three = FeeRate::from_sat_per_kwu(3).unwrap(); let three = FeeRate::from_sat_per_kwu(3);
assert_eq!(one.checked_add(two).unwrap(), three); assert_eq!(one.checked_add(two).unwrap(), three);
assert!(FeeRate::from_sat_per_kvb(u64::MAX).is_none()); // sanity check. // Sanity check - no overflow adding one to per kvb max.
let _ = FeeRate::from_sat_per_kvb(u32::MAX).checked_add(one).unwrap();
let fee_rate = FeeRate::from_sat_per_mvb(u64::MAX).checked_add(one); let fee_rate = FeeRate::from_sat_per_mvb(u64::MAX).checked_add(one);
assert!(fee_rate.is_none()); assert!(fee_rate.is_none());
} }
#[test] #[test]
fn checked_sub() { fn checked_sub() {
let one = FeeRate::from_sat_per_kwu(1).unwrap(); let one = FeeRate::from_sat_per_kwu(1);
let two = FeeRate::from_sat_per_kwu(2).unwrap(); let two = FeeRate::from_sat_per_kwu(2);
let three = FeeRate::from_sat_per_kwu(3).unwrap(); let three = FeeRate::from_sat_per_kwu(3);
assert_eq!(three.checked_sub(two).unwrap(), one); assert_eq!(three.checked_sub(two).unwrap(), one);
let fee_rate = FeeRate::ZERO.checked_sub(one); let fee_rate = FeeRate::ZERO.checked_sub(one);
@ -390,35 +373,25 @@ mod tests {
#[test] #[test]
fn fee_rate_from_sat_per_vb() { fn fee_rate_from_sat_per_vb() {
let fee_rate = FeeRate::from_sat_per_vb(10).expect("expected feerate in sat/kwu"); let fee_rate = FeeRate::from_sat_per_vb(10);
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500).unwrap()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500));
} }
#[test] #[test]
fn fee_rate_from_sat_per_kvb() { fn fee_rate_from_sat_per_kvb() {
let fee_rate = FeeRate::from_sat_per_kvb(11).unwrap(); let fee_rate = FeeRate::from_sat_per_kvb(11);
assert_eq!(fee_rate, FeeRate::from_sat_per_mvb(11_000)); assert_eq!(fee_rate, FeeRate::from_sat_per_mvb(11_000));
} }
#[test] #[test]
fn fee_rate_from_sat_per_vb_overflow() { fn from_sat_per_vb() {
let fee_rate = FeeRate::from_sat_per_vb(u64::MAX); let fee_rate = FeeRate::from_sat_per_vb(10);
assert!(fee_rate.is_none()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500));
} }
#[test]
fn from_sat_per_vb_u32() {
let fee_rate = FeeRate::from_sat_per_vb_u32(10);
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500).unwrap());
}
#[test]
#[cfg(debug_assertions)]
fn from_sat_per_vb_u32_cannot_panic() { FeeRate::from_sat_per_vb_u32(u32::MAX); }
#[test] #[test]
fn raw_feerate() { fn raw_feerate() {
let fee_rate = FeeRate::from_sat_per_kwu(749).unwrap(); let fee_rate = FeeRate::from_sat_per_kwu(749);
assert_eq!(fee_rate.to_sat_per_kwu_floor(), 749); assert_eq!(fee_rate.to_sat_per_kwu_floor(), 749);
assert_eq!(fee_rate.to_sat_per_vb_floor(), 2); assert_eq!(fee_rate.to_sat_per_vb_floor(), 2);
assert_eq!(fee_rate.to_sat_per_vb_ceil(), 3); assert_eq!(fee_rate.to_sat_per_vb_ceil(), 3);
@ -427,24 +400,22 @@ mod tests {
#[test] #[test]
fn checked_mul() { fn checked_mul() {
let fee_rate = FeeRate::from_sat_per_kwu(10) let fee_rate = FeeRate::from_sat_per_kwu(10)
.unwrap()
.checked_mul(10) .checked_mul(10)
.expect("expected feerate in sat/kwu"); .expect("expected feerate in sat/kwu");
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(100).unwrap()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(100));
let fee_rate = FeeRate::from_sat_per_kwu(10).unwrap().checked_mul(u64::MAX); let fee_rate = FeeRate::from_sat_per_kwu(10).checked_mul(u64::MAX);
assert!(fee_rate.is_none()); assert!(fee_rate.is_none());
} }
#[test] #[test]
fn checked_div() { fn checked_div() {
let fee_rate = FeeRate::from_sat_per_kwu(10) let fee_rate = FeeRate::from_sat_per_kwu(10)
.unwrap()
.checked_div(10) .checked_div(10)
.expect("expected feerate in sat/kwu"); .expect("expected feerate in sat/kwu");
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1).unwrap()); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1));
let fee_rate = FeeRate::from_sat_per_kwu(10).unwrap().checked_div(0); let fee_rate = FeeRate::from_sat_per_kwu(10).checked_div(0);
assert!(fee_rate.is_none()); assert!(fee_rate.is_none());
} }

View File

@ -33,15 +33,19 @@ pub mod as_sat_per_kwu_floor {
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::FeeRate; use crate::{Amount, FeeRate};
pub fn serialize<S: Serializer>(f: &FeeRate, s: S) -> Result<S::Ok, S::Error> { pub fn serialize<S: Serializer>(f: &FeeRate, s: S) -> Result<S::Ok, S::Error> {
u64::serialize(&f.to_sat_per_kwu_floor(), s) u64::serialize(&f.to_sat_per_kwu_floor(), s)
} }
pub fn deserialize<'d, D: Deserializer<'d>>(d: D) -> Result<FeeRate, D::Error> { pub fn deserialize<'d, D: Deserializer<'d>>(d: D) -> Result<FeeRate, D::Error> {
FeeRate::from_sat_per_kwu(u64::deserialize(d)?) let sat = u64::deserialize(d)?;
.ok_or_else(|| serde::de::Error::custom("overflowed sats/kwu")) FeeRate::from_per_kwu(
Amount::from_sat(sat).map_err(|_| serde::de::Error::custom("amount out of range"))?,
)
.into_result()
.map_err(|_| serde::de::Error::custom("fee rate too big for sats/kwu"))
} }
pub mod opt { pub mod opt {
@ -100,8 +104,7 @@ pub mod as_sat_per_vb_floor {
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::fee_rate::serde::OverflowError; use crate::{Amount, FeeRate};
use crate::fee_rate::FeeRate;
pub fn serialize<S: Serializer>(f: &FeeRate, s: S) -> Result<S::Ok, S::Error> { pub fn serialize<S: Serializer>(f: &FeeRate, s: S) -> Result<S::Ok, S::Error> {
u64::serialize(&f.to_sat_per_vb_floor(), s) u64::serialize(&f.to_sat_per_vb_floor(), s)
@ -109,9 +112,12 @@ pub mod as_sat_per_vb_floor {
// Errors on overflow. // Errors on overflow.
pub fn deserialize<'d, D: Deserializer<'d>>(d: D) -> Result<FeeRate, D::Error> { pub fn deserialize<'d, D: Deserializer<'d>>(d: D) -> Result<FeeRate, D::Error> {
FeeRate::from_sat_per_vb(u64::deserialize(d)?) let sat = u64::deserialize(d)?;
.ok_or(OverflowError) FeeRate::from_per_vb(
.map_err(serde::de::Error::custom) Amount::from_sat(sat).map_err(|_| serde::de::Error::custom("amount out of range"))?,
)
.into_result()
.map_err(|_| serde::de::Error::custom("fee rate too big for sats/vb"))
} }
pub mod opt { pub mod opt {
@ -171,8 +177,7 @@ pub mod as_sat_per_vb_ceil {
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::fee_rate::serde::OverflowError; use crate::{Amount, FeeRate};
use crate::fee_rate::FeeRate;
pub fn serialize<S: Serializer>(f: &FeeRate, s: S) -> Result<S::Ok, S::Error> { pub fn serialize<S: Serializer>(f: &FeeRate, s: S) -> Result<S::Ok, S::Error> {
u64::serialize(&f.to_sat_per_vb_ceil(), s) u64::serialize(&f.to_sat_per_vb_ceil(), s)
@ -180,9 +185,12 @@ pub mod as_sat_per_vb_ceil {
// Errors on overflow. // Errors on overflow.
pub fn deserialize<'d, D: Deserializer<'d>>(d: D) -> Result<FeeRate, D::Error> { pub fn deserialize<'d, D: Deserializer<'d>>(d: D) -> Result<FeeRate, D::Error> {
FeeRate::from_sat_per_vb(u64::deserialize(d)?) let sat = u64::deserialize(d)?;
.ok_or(OverflowError) FeeRate::from_per_vb(
.map_err(serde::de::Error::custom) Amount::from_sat(sat).map_err(|_| serde::de::Error::custom("amount out of range"))?,
)
.into_result()
.map_err(|_| serde::de::Error::custom("fee rate too big for sats/vb"))
} }
pub mod opt { pub mod opt {

View File

@ -58,7 +58,7 @@ use crate::{Amount, FeeRate, SignedAmount, Weight};
/// let a = Amount::from_sat(123).expect("valid amount"); /// let a = Amount::from_sat(123).expect("valid amount");
/// let b = Amount::from_sat(467).expect("valid amount"); /// let b = Amount::from_sat(467).expect("valid amount");
/// // Fee rate for transaction. /// // Fee rate for transaction.
/// let fee_rate = FeeRate::from_sat_per_vb(1).unwrap(); /// let fee_rate = FeeRate::from_sat_per_vb(1);
/// ///
/// // Somewhat contrived example to show addition operator chained with division. /// // Somewhat contrived example to show addition operator chained with division.
/// let max_fee = a + b; /// let max_fee = a + b;