diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs index ecd813ffd..b8374456b 100644 --- a/units/src/amount/tests.rs +++ b/units/src/amount/tests.rs @@ -220,6 +220,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()); + } }