diff --git a/api/units/all-features.txt b/api/units/all-features.txt index 0f5a1c0f2..b5ed8d27d 100644 --- a/api/units/all-features.txt +++ b/api/units/all-features.txt @@ -505,6 +505,7 @@ impl core::ops::arith::AddAssign<&bitcoin_units::SignedAmount> for bitcoin_units impl core::ops::arith::AddAssign<&bitcoin_units::fee_rate::FeeRate> for bitcoin_units::fee_rate::FeeRate impl core::ops::arith::AddAssign<&bitcoin_units::weight::Weight> for bitcoin_units::weight::Weight impl core::ops::arith::Div for bitcoin_units::weight::Weight +impl core::ops::arith::Div for bitcoin_units::Amount impl core::ops::arith::Div for bitcoin_units::Amount impl core::ops::arith::Div for bitcoin_units::SignedAmount impl core::ops::arith::Div for bitcoin_units::Amount @@ -722,6 +723,8 @@ pub const bitcoin_units::weight::Weight::WITNESS_SCALE_FACTOR: u64 pub const bitcoin_units::weight::Weight::ZERO: bitcoin_units::weight::Weight pub const fn bitcoin_units::Amount::checked_add(self, rhs: bitcoin_units::Amount) -> core::option::Option pub const fn bitcoin_units::Amount::checked_div(self, rhs: u64) -> core::option::Option +pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_ceil(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option +pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_floor(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option pub const fn bitcoin_units::Amount::checked_div_by_weight_ceil(self, weight: bitcoin_units::weight::Weight) -> core::option::Option pub const fn bitcoin_units::Amount::checked_div_by_weight_floor(self, weight: bitcoin_units::weight::Weight) -> core::option::Option pub const fn bitcoin_units::Amount::checked_mul(self, rhs: u64) -> core::option::Option @@ -816,6 +819,7 @@ pub fn bitcoin_units::Amount::des_sat<'d, D: serde::de::Deserializer<'d>>(d: D, pub fn bitcoin_units::Amount::des_str<'d, D: serde::de::Deserializer<'d>>(d: D, _: private::Token) -> core::result::Result::Error> pub fn bitcoin_units::Amount::display_dynamic(self) -> bitcoin_units::amount::Display pub fn bitcoin_units::Amount::display_in(self, denomination: bitcoin_units::amount::Denomination) -> bitcoin_units::amount::Display +pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::fee_rate::FeeRate) -> Self::Output pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::weight::Weight) -> Self::Output pub fn bitcoin_units::Amount::div(self, rhs: u64) -> Self::Output pub fn bitcoin_units::Amount::div_assign(&mut self, rhs: u64) @@ -1289,6 +1293,7 @@ pub type bitcoin_units::Amount::Err = bitcoin_units::amount::ParseError pub type bitcoin_units::Amount::Error = bitcoin_units::amount::OutOfRangeError pub type bitcoin_units::Amount::Output = bitcoin_units::Amount pub type bitcoin_units::Amount::Output = bitcoin_units::fee_rate::FeeRate +pub type bitcoin_units::Amount::Output = bitcoin_units::weight::Weight pub type bitcoin_units::SignedAmount::Err = bitcoin_units::amount::ParseError pub type bitcoin_units::SignedAmount::Output = bitcoin_units::SignedAmount pub type bitcoin_units::amount::Denomination::Err = bitcoin_units::amount::ParseDenominationError diff --git a/api/units/alloc-only.txt b/api/units/alloc-only.txt index 499372621..dc428fb57 100644 --- a/api/units/alloc-only.txt +++ b/api/units/alloc-only.txt @@ -470,6 +470,7 @@ impl core::ops::arith::AddAssign<&bitcoin_units::SignedAmount> for bitcoin_units impl core::ops::arith::AddAssign<&bitcoin_units::fee_rate::FeeRate> for bitcoin_units::fee_rate::FeeRate impl core::ops::arith::AddAssign<&bitcoin_units::weight::Weight> for bitcoin_units::weight::Weight impl core::ops::arith::Div for bitcoin_units::weight::Weight +impl core::ops::arith::Div for bitcoin_units::Amount impl core::ops::arith::Div for bitcoin_units::Amount impl core::ops::arith::Div for bitcoin_units::SignedAmount impl core::ops::arith::Div for bitcoin_units::Amount @@ -660,6 +661,8 @@ pub const bitcoin_units::weight::Weight::WITNESS_SCALE_FACTOR: u64 pub const bitcoin_units::weight::Weight::ZERO: bitcoin_units::weight::Weight pub const fn bitcoin_units::Amount::checked_add(self, rhs: bitcoin_units::Amount) -> core::option::Option pub const fn bitcoin_units::Amount::checked_div(self, rhs: u64) -> core::option::Option +pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_ceil(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option +pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_floor(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option pub const fn bitcoin_units::Amount::checked_div_by_weight_ceil(self, weight: bitcoin_units::weight::Weight) -> core::option::Option pub const fn bitcoin_units::Amount::checked_div_by_weight_floor(self, weight: bitcoin_units::weight::Weight) -> core::option::Option pub const fn bitcoin_units::Amount::checked_mul(self, rhs: u64) -> core::option::Option @@ -750,6 +753,7 @@ pub fn bitcoin_units::Amount::cmp(&self, other: &bitcoin_units::Amount) -> core: pub fn bitcoin_units::Amount::default() -> Self pub fn bitcoin_units::Amount::display_dynamic(self) -> bitcoin_units::amount::Display pub fn bitcoin_units::Amount::display_in(self, denomination: bitcoin_units::amount::Denomination) -> bitcoin_units::amount::Display +pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::fee_rate::FeeRate) -> Self::Output pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::weight::Weight) -> Self::Output pub fn bitcoin_units::Amount::div(self, rhs: u64) -> Self::Output pub fn bitcoin_units::Amount::div_assign(&mut self, rhs: u64) @@ -1117,6 +1121,7 @@ pub type bitcoin_units::Amount::Err = bitcoin_units::amount::ParseError pub type bitcoin_units::Amount::Error = bitcoin_units::amount::OutOfRangeError pub type bitcoin_units::Amount::Output = bitcoin_units::Amount pub type bitcoin_units::Amount::Output = bitcoin_units::fee_rate::FeeRate +pub type bitcoin_units::Amount::Output = bitcoin_units::weight::Weight pub type bitcoin_units::SignedAmount::Err = bitcoin_units::amount::ParseError pub type bitcoin_units::SignedAmount::Output = bitcoin_units::SignedAmount pub type bitcoin_units::amount::Denomination::Err = bitcoin_units::amount::ParseDenominationError diff --git a/api/units/no-features.txt b/api/units/no-features.txt index 4532ae555..63e36fcd5 100644 --- a/api/units/no-features.txt +++ b/api/units/no-features.txt @@ -454,6 +454,7 @@ impl core::ops::arith::AddAssign<&bitcoin_units::SignedAmount> for bitcoin_units impl core::ops::arith::AddAssign<&bitcoin_units::fee_rate::FeeRate> for bitcoin_units::fee_rate::FeeRate impl core::ops::arith::AddAssign<&bitcoin_units::weight::Weight> for bitcoin_units::weight::Weight impl core::ops::arith::Div for bitcoin_units::weight::Weight +impl core::ops::arith::Div for bitcoin_units::Amount impl core::ops::arith::Div for bitcoin_units::Amount impl core::ops::arith::Div for bitcoin_units::SignedAmount impl core::ops::arith::Div for bitcoin_units::Amount @@ -644,6 +645,8 @@ pub const bitcoin_units::weight::Weight::WITNESS_SCALE_FACTOR: u64 pub const bitcoin_units::weight::Weight::ZERO: bitcoin_units::weight::Weight pub const fn bitcoin_units::Amount::checked_add(self, rhs: bitcoin_units::Amount) -> core::option::Option pub const fn bitcoin_units::Amount::checked_div(self, rhs: u64) -> core::option::Option +pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_ceil(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option +pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_floor(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option pub const fn bitcoin_units::Amount::checked_mul(self, rhs: u64) -> core::option::Option pub const fn bitcoin_units::Amount::checked_rem(self, rhs: u64) -> core::option::Option pub const fn bitcoin_units::Amount::checked_sub(self, rhs: bitcoin_units::Amount) -> core::option::Option @@ -732,6 +735,7 @@ pub fn bitcoin_units::Amount::cmp(&self, other: &bitcoin_units::Amount) -> core: pub fn bitcoin_units::Amount::default() -> Self pub fn bitcoin_units::Amount::display_dynamic(self) -> bitcoin_units::amount::Display pub fn bitcoin_units::Amount::display_in(self, denomination: bitcoin_units::amount::Denomination) -> bitcoin_units::amount::Display +pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::fee_rate::FeeRate) -> Self::Output pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::weight::Weight) -> Self::Output pub fn bitcoin_units::Amount::div(self, rhs: u64) -> Self::Output pub fn bitcoin_units::Amount::div_assign(&mut self, rhs: u64) @@ -1069,6 +1073,7 @@ pub type bitcoin_units::Amount::Err = bitcoin_units::amount::ParseError pub type bitcoin_units::Amount::Error = bitcoin_units::amount::OutOfRangeError pub type bitcoin_units::Amount::Output = bitcoin_units::Amount pub type bitcoin_units::Amount::Output = bitcoin_units::fee_rate::FeeRate +pub type bitcoin_units::Amount::Output = bitcoin_units::weight::Weight pub type bitcoin_units::SignedAmount::Err = bitcoin_units::amount::ParseError pub type bitcoin_units::SignedAmount::Output = bitcoin_units::SignedAmount pub type bitcoin_units::amount::Denomination::Err = bitcoin_units::amount::ParseDenominationError diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs index 42a9e3d3c..e298c6875 100644 --- a/units/src/amount/tests.rs +++ b/units/src/amount/tests.rs @@ -246,6 +246,48 @@ fn amount_checked_div_by_weight_floor() { assert!(fee_rate.is_none()); } +#[cfg(feature = "alloc")] +#[test] +fn amount_checked_div_by_fee_rate() { + let amount = Amount::from_sat(1000); + let fee_rate = FeeRate::from_sat_per_kwu(2); + + // Test floor division + let weight = amount.checked_div_by_fee_rate_floor(fee_rate).unwrap(); + // 1000 sats / (2 sats/kwu) = 500,000 wu + assert_eq!(weight, Weight::from_wu(500_000)); + + // Test ceiling division + let weight = amount.checked_div_by_fee_rate_ceil(fee_rate).unwrap(); + assert_eq!(weight, Weight::from_wu(500_000)); // Same result for exact division + + // Test truncation behavior + let amount = Amount::from_sat(1000); + let fee_rate = FeeRate::from_sat_per_kwu(3); + let floor_weight = amount.checked_div_by_fee_rate_floor(fee_rate).unwrap(); + let ceil_weight = amount.checked_div_by_fee_rate_ceil(fee_rate).unwrap(); + assert_eq!(floor_weight, Weight::from_wu(333_333)); + assert_eq!(ceil_weight, Weight::from_wu(333_334)); + + // Test division by zero + let zero_fee_rate = FeeRate::from_sat_per_kwu(0); + assert!(amount.checked_div_by_fee_rate_floor(zero_fee_rate).is_none()); + assert!(amount.checked_div_by_fee_rate_ceil(zero_fee_rate).is_none()); + + // Test with maximum amount + let max_amount = Amount::MAX; + let small_fee_rate = FeeRate::from_sat_per_kwu(1); + let weight = max_amount.checked_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 + assert_eq!(weight, Weight::from_wu(2_100_000_000_000_000_000)); + + // Test overflow case + let tiny_fee_rate = FeeRate::from_sat_per_kwu(1); + let large_amount = Amount::from_sat(u64::MAX); + assert!(large_amount.checked_div_by_fee_rate_floor(tiny_fee_rate).is_none()); + assert!(large_amount.checked_div_by_fee_rate_ceil(tiny_fee_rate).is_none()); +} + #[cfg(feature = "alloc")] #[test] fn floating_point() { diff --git a/units/src/fee.rs b/units/src/fee.rs index fba121055..a1475f9bd 100644 --- a/units/src/fee.rs +++ b/units/src/fee.rs @@ -68,6 +68,48 @@ impl Amount { None => None, } } + + /// Checked fee rate floor division. + /// + /// Computes the maximum weight that would result in a fee less than or equal to this amount + /// at the given `fee_rate`. Uses floor division to ensure the resulting weight doesn't cause + /// the fee to exceed the amount. + /// + /// Returns [`None`] if overflow occurred or if `fee_rate` is zero. + #[must_use] + pub const fn checked_div_by_fee_rate_floor(self, fee_rate: FeeRate) -> Option { + match self.to_sat().checked_mul(1000) { + Some(amount_msats) => match amount_msats.checked_div(fee_rate.to_sat_per_kwu()) { + Some(wu) => Some(Weight::from_wu(wu)), + None => None, + }, + None => None, + } + } + + /// Checked fee rate ceiling division. + /// + /// Computes the minimum weight that would result in a fee greater than or equal to this amount + /// at the given `fee_rate`. Uses ceiling division to ensure the resulting weight is sufficient. + /// + /// Returns [`None`] if overflow occurred or if `fee_rate` is zero. + #[must_use] + pub const fn checked_div_by_fee_rate_ceil(self, fee_rate: FeeRate) -> Option { + let rate = fee_rate.to_sat_per_kwu(); + match self.to_sat().checked_mul(1000) { + Some(amount_msats) => match rate.checked_sub(1) { + Some(rate_minus_one) => match amount_msats.checked_add(rate_minus_one) { + Some(rounded_msats) => match rounded_msats.checked_div(rate) { + Some(wu) => Some(Weight::from_wu(wu)), + None => None, + }, + None => None, + }, + None => None, + }, + None => None, + } + } } impl FeeRate { @@ -136,6 +178,21 @@ impl ops::Div for Amount { } } +impl ops::Div for Amount { + type Output = Weight; + + /// Truncating integer division. + /// + /// # Panics + /// + /// This operation will panic if `fee_rate` is zero or the division results in overflow. + /// + /// Note: This uses floor division. For ceiling division use [`Amount::checked_div_by_fee_rate_ceil`]. + fn div(self, rhs: FeeRate) -> Self::Output { + self.checked_div_by_fee_rate_floor(rhs).unwrap() + } +} + #[cfg(test)] mod tests { use super::*; @@ -199,4 +256,31 @@ mod tests { assert_eq!(two * three, six); } + + #[test] + fn amount_div_by_fee_rate() { + // Test exact division + let amount = Amount::from_sat(1000); + let fee_rate = FeeRate::from_sat_per_kwu(2); + let weight = amount / fee_rate; + assert_eq!(weight, Weight::from_wu(500_000)); + + // Test truncation behavior + let amount = Amount::from_sat(1000); + let fee_rate = FeeRate::from_sat_per_kwu(3); + let weight = amount / fee_rate; + // 1000 * 1000 = 1,000,000 msats + // 1,000,000 / 3 = 333,333.33... wu + // Should truncate down to 333,333 wu + assert_eq!(weight, Weight::from_wu(333_333)); + + // Verify that ceiling division gives different result + let ceil_weight = amount.checked_div_by_fee_rate_ceil(fee_rate).unwrap(); + assert_eq!(ceil_weight, Weight::from_wu(333_334)); + + // Test that division by zero returns None + let zero_rate = FeeRate::from_sat_per_kwu(0); + assert!(amount.checked_div_by_fee_rate_floor(zero_rate).is_none()); + assert!(amount.checked_div_by_fee_rate_ceil(zero_rate).is_none()); + } }