Merge rust-bitcoin/rust-bitcoin#4164: Prepare to enforce MAX_MONEY invariant

5d851f1c3e Remove deprecated amount methods (Tobin C. Harding)
76a2d70b28 Make mul weight by fee return NumOpResult (Tobin C. Harding)
f9eb307953 Remove panic in dust value functions (Tobin C. Harding)
13595fbe7d Fix amount whole bitcoin constructors (Tobin C. Harding)
ac71680202 Pick one - MAX or MAX_MONEY (Tobin C. Harding)
6d70c77cf9 Enforce newtype sanity rules for amount types (Tobin C. Harding)
e6f7b26d80 Use _unchecked in amount const types (Tobin C. Harding)
ef0af8d62e Use sat/ssat constructors throughout tests (Andrew Poelstra)
8ecdc7c275 Use den_ prefix for local Denomination variable (Tobin C. Harding)
938461cc65 psbt: Use Amount::ZERO in unit test (Tobin C. Harding)

Pull request description:

  We want to start enforcing MAX_MONEY as an invariant in the amount types. There are a few more steps we can do first to make that change easier to review.

ACKs for top commit:
  jamillambert:
    ACK 5d851f1c3e
  apoelstra:
    ACK 5d851f1c3e98d7d426e5897b2d734b77a299ccfb; successfully ran local tests

Tree-SHA512: 9e28b273d41fc143656e3a84736b6abe477fae5721b02bce7436551bd489cc235dc7e9fc68ffafa98f75a61065470ac514570a42bea94e90fedbb31f3cd61031
This commit is contained in:
merge-script 2025-03-12 12:14:14 +00:00
commit 3cef539aac
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
12 changed files with 285 additions and 381 deletions

View File

@ -261,7 +261,7 @@ crate::internal_macros::define_extension_trait! {
/// Returns the minimum value an output with this script should have in order to be /// Returns the minimum value an output with this script should have in order to be
/// broadcastable on todays Bitcoin network. /// broadcastable on todays Bitcoin network.
#[deprecated(since = "0.32.0", note = "use `minimal_non_dust` etc. instead")] #[deprecated(since = "0.32.0", note = "use `minimal_non_dust` etc. instead")]
fn dust_value(&self) -> Amount { self.minimal_non_dust() } fn dust_value(&self) -> Option<Amount> { self.minimal_non_dust() }
/// Returns the minimum value an output with this script should have in order to be /// Returns the minimum value an output with this script should have in order to be
/// broadcastable on today's Bitcoin network. /// broadcastable on today's Bitcoin network.
@ -272,7 +272,7 @@ crate::internal_macros::define_extension_trait! {
/// To use a custom value, use [`minimal_non_dust_custom`]. /// To use a custom value, use [`minimal_non_dust_custom`].
/// ///
/// [`minimal_non_dust_custom`]: Script::minimal_non_dust_custom /// [`minimal_non_dust_custom`]: Script::minimal_non_dust_custom
fn minimal_non_dust(&self) -> Amount { fn minimal_non_dust(&self) -> Option<Amount> {
self.minimal_non_dust_internal(DUST_RELAY_TX_FEE.into()) self.minimal_non_dust_internal(DUST_RELAY_TX_FEE.into())
} }
@ -287,7 +287,7 @@ crate::internal_macros::define_extension_trait! {
/// To use the default Bitcoin Core value, use [`minimal_non_dust`]. /// To use the default Bitcoin Core value, use [`minimal_non_dust`].
/// ///
/// [`minimal_non_dust`]: Script::minimal_non_dust /// [`minimal_non_dust`]: Script::minimal_non_dust
fn minimal_non_dust_custom(&self, dust_relay_fee: FeeRate) -> Amount { fn minimal_non_dust_custom(&self, dust_relay_fee: FeeRate) -> Option<Amount> {
self.minimal_non_dust_internal(dust_relay_fee.to_sat_per_kwu() * 4) self.minimal_non_dust_internal(dust_relay_fee.to_sat_per_kwu() * 4)
} }
@ -394,7 +394,7 @@ mod sealed {
crate::internal_macros::define_extension_trait! { crate::internal_macros::define_extension_trait! {
pub(crate) trait ScriptExtPriv impl for Script { pub(crate) trait ScriptExtPriv impl for Script {
fn minimal_non_dust_internal(&self, dust_relay_fee: u64) -> Amount { fn minimal_non_dust_internal(&self, dust_relay_fee: u64) -> Option<Amount> {
// This must never be lower than Bitcoin Core's GetDustThreshold() (as of v0.21) as it may // This must never be lower than Bitcoin Core's GetDustThreshold() (as of v0.21) as it may
// otherwise allow users to create transactions which likely can never be broadcast/confirmed. // otherwise allow users to create transactions which likely can never be broadcast/confirmed.
let sats = dust_relay_fee let sats = dust_relay_fee
@ -408,13 +408,12 @@ crate::internal_macros::define_extension_trait! {
32 + 4 + 1 + 107 + 4 + // The spend cost copied from Core 32 + 4 + 1 + 107 + 4 + // The spend cost copied from Core
8 + // The serialized size of the TxOut's amount field 8 + // The serialized size of the TxOut's amount field
self.consensus_encode(&mut sink()).expect("sinks don't error").to_u64() // The serialized size of this script_pubkey self.consensus_encode(&mut sink()).expect("sinks don't error").to_u64() // The serialized size of this script_pubkey
}) })?
.expect("dust_relay_fee or script length should not be absurdly large")
/ 1000; // divide by 1000 like in Core to get value as it cancels out DEFAULT_MIN_RELAY_TX_FEE / 1000; // divide by 1000 like in Core to get value as it cancels out DEFAULT_MIN_RELAY_TX_FEE
// Note: We ensure the division happens at the end, since Core performs the division at the end. // Note: We ensure the division happens at the end, since Core performs the division at the end.
// This will make sure none of the implicit floor operations mess with the value. // This will make sure none of the implicit floor operations mess with the value.
Amount::from_sat(sats) Some(Amount::from_sat(sats))
} }
fn count_sigops_internal(&self, accurate: bool) -> usize { fn count_sigops_internal(&self, accurate: bool) -> usize {

View File

@ -684,10 +684,10 @@ fn default_dust_value() {
// well-known scriptPubKey types. // well-known scriptPubKey types.
let script_p2wpkh = Builder::new().push_int_unchecked(0).push_slice([42; 20]).into_script(); let script_p2wpkh = Builder::new().push_int_unchecked(0).push_slice([42; 20]).into_script();
assert!(script_p2wpkh.is_p2wpkh()); assert!(script_p2wpkh.is_p2wpkh());
assert_eq!(script_p2wpkh.minimal_non_dust(), Amount::from_sat_unchecked(294)); assert_eq!(script_p2wpkh.minimal_non_dust(), Some(Amount::from_sat_unchecked(294)));
assert_eq!( assert_eq!(
script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)), script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)),
Amount::from_sat_unchecked(588) Some(Amount::from_sat_unchecked(588))
); );
let script_p2pkh = Builder::new() let script_p2pkh = Builder::new()
@ -698,10 +698,10 @@ fn default_dust_value() {
.push_opcode(OP_CHECKSIG) .push_opcode(OP_CHECKSIG)
.into_script(); .into_script();
assert!(script_p2pkh.is_p2pkh()); assert!(script_p2pkh.is_p2pkh());
assert_eq!(script_p2pkh.minimal_non_dust(), Amount::from_sat_unchecked(546)); assert_eq!(script_p2pkh.minimal_non_dust(), Some(Amount::from_sat_unchecked(546)));
assert_eq!( assert_eq!(
script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)), script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)),
Amount::from_sat_unchecked(1092) Some(Amount::from_sat_unchecked(1092))
); );
} }

View File

@ -183,8 +183,8 @@ crate::internal_macros::define_extension_trait! {
/// To use a custom value, use [`minimal_non_dust_custom`]. /// To use a custom value, use [`minimal_non_dust_custom`].
/// ///
/// [`minimal_non_dust_custom`]: TxOut::minimal_non_dust_custom /// [`minimal_non_dust_custom`]: TxOut::minimal_non_dust_custom
fn minimal_non_dust(script_pubkey: ScriptBuf) -> Self { fn minimal_non_dust(script_pubkey: ScriptBuf) -> Option<TxOut> {
TxOut { value: script_pubkey.minimal_non_dust(), script_pubkey } Some(TxOut { value: script_pubkey.minimal_non_dust()?, script_pubkey })
} }
/// Constructs a new `TxOut` with given script and the smallest possible `value` that is **not** dust /// Constructs a new `TxOut` with given script and the smallest possible `value` that is **not** dust
@ -198,8 +198,8 @@ crate::internal_macros::define_extension_trait! {
/// To use the default Bitcoin Core value, use [`minimal_non_dust`]. /// To use the default Bitcoin Core value, use [`minimal_non_dust`].
/// ///
/// [`minimal_non_dust`]: TxOut::minimal_non_dust /// [`minimal_non_dust`]: TxOut::minimal_non_dust
fn minimal_non_dust_custom(script_pubkey: ScriptBuf, dust_relay_fee: FeeRate) -> Self { fn minimal_non_dust_custom(script_pubkey: ScriptBuf, dust_relay_fee: FeeRate) -> Option<TxOut> {
TxOut { value: script_pubkey.minimal_non_dust_custom(dust_relay_fee), script_pubkey } Some(TxOut { value: script_pubkey.minimal_non_dust_custom(dust_relay_fee)?, script_pubkey })
} }
} }
} }

View File

@ -2283,7 +2283,7 @@ mod tests {
version: transaction::Version::TWO, version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO, lock_time: absolute::LockTime::ZERO,
input: vec![TxIn::EMPTY_COINBASE, TxIn::EMPTY_COINBASE], input: vec![TxIn::EMPTY_COINBASE, TxIn::EMPTY_COINBASE],
output: vec![TxOut { value: Amount::from_sat(0), script_pubkey: ScriptBuf::new() }], output: vec![TxOut { value: Amount::ZERO, script_pubkey: ScriptBuf::new() }],
}; };
let mut psbt = Psbt::from_unsigned_tx(unsigned_tx).unwrap(); let mut psbt = Psbt::from_unsigned_tx(unsigned_tx).unwrap();

View File

@ -110,7 +110,7 @@ fn serde_regression_txin() {
#[test] #[test]
fn serde_regression_txout() { fn serde_regression_txout() {
let txout = let txout =
TxOut { value: Amount::MAX_MONEY, script_pubkey: ScriptBuf::from(vec![0u8, 1u8, 2u8]) }; TxOut { value: Amount::MAX, script_pubkey: ScriptBuf::from(vec![0u8, 1u8, 2u8]) };
let got = serialize(&txout).unwrap(); let got = serialize(&txout).unwrap();
let want = include_bytes!("data/serde/txout_bincode") as &[_]; let want = include_bytes!("data/serde/txout_bincode") as &[_];
assert_eq!(got, want) assert_eq!(got, want)

View File

@ -39,6 +39,7 @@ pub use self::{
signed::SignedAmount, signed::SignedAmount,
unsigned::Amount, unsigned::Amount,
}; };
pub(crate) use self::result::OptionExt;
/// A set of denominations in which amounts can be expressed. /// A set of denominations in which amounts can be expressed.
/// ///

View File

@ -1,7 +1,6 @@
// SPDX-License-Identifier: CC0-1.0 // SPDX-License-Identifier: CC0-1.0
//! Provides a monodic numeric result type that is used to return the result of //! Provides a monodic type used when mathematical operations (`core::ops`) return an amount type.
//! doing mathematical operations (`core::ops`) on amount types.
use core::{fmt, ops}; use core::{fmt, ops};
@ -378,7 +377,7 @@ impl<'a> core::iter::Sum<&'a NumOpResult<SignedAmount>> for NumOpResult<SignedAm
} }
} }
pub(in crate::amount) trait OptionExt<T> { pub(crate) trait OptionExt<T> {
fn valid_or_error(self) -> NumOpResult<T>; fn valid_or_error(self) -> NumOpResult<T>;
} }

View File

@ -16,6 +16,7 @@ use super::{
DisplayStyle, OutOfRangeError, ParseAmountError, ParseError, DisplayStyle, OutOfRangeError, ParseAmountError, ParseError,
}; };
mod encapsulate {
/// A signed amount. /// A signed amount.
/// ///
/// The [`SignedAmount`] type can be used to express Bitcoin amounts that support arithmetic and /// The [`SignedAmount`] type can be used to express Bitcoin amounts that support arithmetic and
@ -48,19 +49,41 @@ use super::{
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SignedAmount(i64); pub struct SignedAmount(i64);
impl SignedAmount {
/// Constructs a new [`SignedAmount`] with satoshi precision and the given number of satoshis.
///
/// Caller to guarantee that `satoshi` is within valid range.
///
/// See [`Self::MIN`] and [`Self::MAX`].
pub const fn from_sat_unchecked(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) }
/// Gets the number of satoshis in this [`SignedAmount`].
///
/// # Examples
///
/// ```
/// # use bitcoin_units::SignedAmount;
/// assert_eq!(SignedAmount::ONE_BTC.to_sat(), 100_000_000);
/// ```
pub const fn to_sat(self) -> i64 { self.0 }
}
}
#[doc(inline)]
pub use encapsulate::SignedAmount;
impl SignedAmount { impl SignedAmount {
/// The zero amount. /// The zero amount.
pub const ZERO: Self = SignedAmount(0); pub const ZERO: Self = SignedAmount::from_sat_unchecked(0);
/// Exactly one satoshi. /// Exactly one satoshi.
pub const ONE_SAT: Self = SignedAmount(1); pub const ONE_SAT: Self = SignedAmount::from_sat_unchecked(1);
/// Exactly one bitcoin. /// Exactly one bitcoin.
pub const ONE_BTC: Self = SignedAmount(100_000_000); pub const ONE_BTC: Self = SignedAmount::from_sat_unchecked(100_000_000);
/// Exactly fifty bitcoin. /// Exactly fifty bitcoin.
pub const FIFTY_BTC: Self = Self::from_sat_unchecked(50 * 100_000_000); pub const FIFTY_BTC: Self = SignedAmount::from_sat_unchecked(50 * 100_000_000);
/// The maximum value allowed as an amount. Useful for sanity checking. /// The maximum value allowed as an amount. Useful for sanity checking.
pub const MAX_MONEY: Self = SignedAmount(21_000_000 * 100_000_000); pub const MAX_MONEY: Self = SignedAmount::from_sat_unchecked(21_000_000 * 100_000_000);
/// The minimum value of an amount. /// The minimum value of an amount.
pub const MIN: Self = SignedAmount(-21_000_000 * 100_000_000); pub const MIN: Self = SignedAmount::from_sat_unchecked(-21_000_000 * 100_000_000);
/// The maximum value of an amount. /// The maximum value of an amount.
pub const MAX: Self = SignedAmount::MAX_MONEY; pub const MAX: Self = SignedAmount::MAX_MONEY;
@ -73,24 +96,9 @@ impl SignedAmount {
/// let amount = SignedAmount::from_sat(-100_000); /// let amount = SignedAmount::from_sat(-100_000);
/// assert_eq!(amount.to_sat(), -100_000); /// assert_eq!(amount.to_sat(), -100_000);
/// ``` /// ```
pub const fn from_sat(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) } pub const fn from_sat(satoshi: i64) -> SignedAmount {
SignedAmount::from_sat_unchecked(satoshi)
/// Gets the number of satoshis in this [`SignedAmount`]. }
///
/// # Examples
///
/// ```
/// # use bitcoin_units::SignedAmount;
/// assert_eq!(SignedAmount::ONE_BTC.to_sat(), 100_000_000);
/// ```
pub const fn to_sat(self) -> i64 { self.0 }
/// Constructs a new [`SignedAmount`] with satoshi precision and the given number of satoshis.
///
/// Caller to guarantee that `satoshi` is within valid range.
///
/// See [`Self::MIN`] and [`Self::MAX_MONEY`].
pub const fn from_sat_unchecked(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) }
/// Converts from a value expressing a decimal number of bitcoin to a [`SignedAmount`]. /// Converts from a value expressing a decimal number of bitcoin to a [`SignedAmount`].
/// ///
@ -113,29 +121,19 @@ impl SignedAmount {
} }
/// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`]. /// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`].
/// #[allow(clippy::missing_panics_doc)]
/// # Errors pub fn from_int_btc<T: Into<i32>>(whole_bitcoin: T) -> SignedAmount {
/// SignedAmount::from_int_btc_const(whole_bitcoin.into())
/// The function errors if the argument multiplied by the number of sats
/// per bitcoin overflows an `i64` type.
pub fn from_int_btc<T: Into<i64>>(whole_bitcoin: T) -> Result<SignedAmount, OutOfRangeError> {
match whole_bitcoin.into().checked_mul(100_000_000) {
Some(amount) => Ok(Self::from_sat(amount)),
None => Err(OutOfRangeError { is_signed: true, is_greater_than_max: true }),
}
} }
/// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`] /// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`]
/// in const context. /// in const context.
/// #[allow(clippy::missing_panics_doc)]
/// # Panics pub const fn from_int_btc_const(whole_bitcoin: i32) -> SignedAmount {
/// let btc = whole_bitcoin as i64; // Can't call `into` in const context.
/// The function panics if the argument multiplied by the number of sats match btc.checked_mul(100_000_000) {
/// per bitcoin overflows an `i64` type.
pub const fn from_int_btc_const(whole_bitcoin: i64) -> SignedAmount {
match whole_bitcoin.checked_mul(100_000_000) {
Some(amount) => SignedAmount::from_sat(amount), Some(amount) => SignedAmount::from_sat(amount),
None => panic!("checked_mul overflowed"), None => panic!("cannot overflow in i64"),
} }
} }
@ -153,11 +151,11 @@ impl SignedAmount {
(false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError( (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
ParseAmountErrorInner::OutOfRange(OutOfRangeError::too_big(true)), ParseAmountErrorInner::OutOfRange(OutOfRangeError::too_big(true)),
)), )),
(false, sat) => Ok(SignedAmount(sat as i64)), // Cast ok, value in this arm does not wrap. (false, sat) => Ok(SignedAmount::from_sat(sat as i64)), // Cast ok, value in this arm does not wrap.
(true, sat) if sat > SignedAmount::MIN.to_sat().unsigned_abs() => Err( (true, sat) if sat > SignedAmount::MIN.to_sat().unsigned_abs() => Err(
ParseAmountError(ParseAmountErrorInner::OutOfRange(OutOfRangeError::too_small())), ParseAmountError(ParseAmountErrorInner::OutOfRange(OutOfRangeError::too_small())),
), ),
(true, sat) => Ok(SignedAmount(-(sat as i64))), // Cast ok, value in this arm does not wrap. (true, sat) => Ok(SignedAmount::from_sat(-(sat as i64))), // Cast ok, value in this arm does not wrap.
} }
} }
@ -300,11 +298,11 @@ impl SignedAmount {
/// Gets the absolute value of this [`SignedAmount`]. /// Gets the absolute value of this [`SignedAmount`].
#[must_use] #[must_use]
pub fn abs(self) -> SignedAmount { SignedAmount(self.0.abs()) } pub fn abs(self) -> SignedAmount { SignedAmount::from_sat(self.to_sat().abs()) }
/// Gets the absolute value of this [`SignedAmount`] returning [`Amount`]. /// Gets the absolute value of this [`SignedAmount`] returning [`Amount`].
#[must_use] #[must_use]
pub fn unsigned_abs(self) -> Amount { Amount::from_sat(self.0.unsigned_abs()) } pub fn unsigned_abs(self) -> Amount { Amount::from_sat(self.to_sat().unsigned_abs()) }
/// Returns a number representing sign of this [`SignedAmount`]. /// Returns a number representing sign of this [`SignedAmount`].
/// ///
@ -312,19 +310,19 @@ impl SignedAmount {
/// - `1` if the amount is positive /// - `1` if the amount is positive
/// - `-1` if the amount is negative /// - `-1` if the amount is negative
#[must_use] #[must_use]
pub fn signum(self) -> i64 { self.0.signum() } pub fn signum(self) -> i64 { self.to_sat().signum() }
/// Checks if this [`SignedAmount`] is positive. /// Checks if this [`SignedAmount`] is positive.
/// ///
/// Returns `true` if this [`SignedAmount`] is positive and `false` if /// Returns `true` if this [`SignedAmount`] is positive and `false` if
/// this [`SignedAmount`] is zero or negative. /// this [`SignedAmount`] is zero or negative.
pub fn is_positive(self) -> bool { self.0.is_positive() } pub fn is_positive(self) -> bool { self.to_sat().is_positive() }
/// Checks if this [`SignedAmount`] is negative. /// Checks if this [`SignedAmount`] is negative.
/// ///
/// Returns `true` if this [`SignedAmount`] is negative and `false` if /// Returns `true` if this [`SignedAmount`] is negative and `false` if
/// this [`SignedAmount`] is zero or positive. /// this [`SignedAmount`] is zero or positive.
pub fn is_negative(self) -> bool { self.0.is_negative() } pub fn is_negative(self) -> bool { self.to_sat().is_negative() }
/// Returns the absolute value of this [`SignedAmount`]. /// Returns the absolute value of this [`SignedAmount`].
/// ///
@ -334,8 +332,8 @@ impl SignedAmount {
#[must_use] #[must_use]
pub const fn checked_abs(self) -> Option<SignedAmount> { pub const fn checked_abs(self) -> Option<SignedAmount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_abs() { match self.to_sat().checked_abs() {
Some(res) => Some(SignedAmount(res)), Some(res) => Some(SignedAmount::from_sat(res)),
None => None, None => None,
} }
} }
@ -346,8 +344,8 @@ impl SignedAmount {
#[must_use] #[must_use]
pub const fn checked_add(self, rhs: SignedAmount) -> Option<SignedAmount> { pub const fn checked_add(self, rhs: SignedAmount) -> Option<SignedAmount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_add(rhs.0) { match self.to_sat().checked_add(rhs.to_sat()) {
Some(res) => SignedAmount(res).check_min_max(), Some(res) => SignedAmount::from_sat(res).check_min_max(),
None => None, None => None,
} }
} }
@ -359,8 +357,8 @@ impl SignedAmount {
#[must_use] #[must_use]
pub const fn checked_sub(self, rhs: SignedAmount) -> Option<SignedAmount> { pub const fn checked_sub(self, rhs: SignedAmount) -> Option<SignedAmount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_sub(rhs.0) { match self.to_sat().checked_sub(rhs.to_sat()) {
Some(res) => SignedAmount(res).check_min_max(), Some(res) => SignedAmount::from_sat(res).check_min_max(),
None => None, None => None,
} }
} }
@ -372,8 +370,8 @@ impl SignedAmount {
#[must_use] #[must_use]
pub const fn checked_mul(self, rhs: i64) -> Option<SignedAmount> { pub const fn checked_mul(self, rhs: i64) -> Option<SignedAmount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_mul(rhs) { match self.to_sat().checked_mul(rhs) {
Some(res) => SignedAmount(res).check_min_max(), Some(res) => SignedAmount::from_sat(res).check_min_max(),
None => None, None => None,
} }
} }
@ -386,8 +384,8 @@ impl SignedAmount {
#[must_use] #[must_use]
pub const fn checked_div(self, rhs: i64) -> Option<SignedAmount> { pub const fn checked_div(self, rhs: i64) -> Option<SignedAmount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_div(rhs) { match self.to_sat().checked_div(rhs) {
Some(res) => Some(SignedAmount(res)), Some(res) => Some(SignedAmount::from_sat(res)),
None => None, None => None,
} }
} }
@ -398,34 +396,12 @@ impl SignedAmount {
#[must_use] #[must_use]
pub const fn checked_rem(self, rhs: i64) -> Option<SignedAmount> { pub const fn checked_rem(self, rhs: i64) -> Option<SignedAmount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_rem(rhs) { match self.to_sat().checked_rem(rhs) {
Some(res) => Some(SignedAmount(res)), Some(res) => Some(SignedAmount::from_sat(res)),
None => None, None => None,
} }
} }
/// Unchecked addition.
///
/// Computes `self + rhs`.
///
/// # Panics
///
/// On overflow, panics in debug mode, wraps in release mode.
#[must_use]
#[deprecated(since = "TBD", note = "consider converting to u64 using `to_sat`")]
pub fn unchecked_add(self, rhs: SignedAmount) -> SignedAmount { Self(self.0 + rhs.0) }
/// Unchecked subtraction.
///
/// Computes `self - rhs`.
///
/// # Panics
///
/// On overflow, panics in debug mode, wraps in release mode.
#[must_use]
#[deprecated(since = "TBD", note = "consider converting to u64 using `to_sat`")]
pub fn unchecked_sub(self, rhs: SignedAmount) -> SignedAmount { Self(self.0 - rhs.0) }
/// Subtraction that doesn't allow negative [`SignedAmount`]s. /// Subtraction that doesn't allow negative [`SignedAmount`]s.
/// ///
/// Returns [`None`] if either `self`, `rhs` or the result is strictly negative. /// Returns [`None`] if either `self`, `rhs` or the result is strictly negative.
@ -453,7 +429,7 @@ impl SignedAmount {
/// Checks the amount is within the allowed range. /// Checks the amount is within the allowed range.
const fn check_min_max(self) -> Option<SignedAmount> { const fn check_min_max(self) -> Option<SignedAmount> {
if self.0 < Self::MIN.0 || self.0 > Self::MAX.0 { if self.to_sat() < Self::MIN.to_sat() || self.to_sat() > Self::MAX.to_sat() {
None None
} else { } else {
Some(self) Some(self)
@ -514,6 +490,6 @@ impl From<Amount> for SignedAmount {
impl<'a> Arbitrary<'a> for SignedAmount { impl<'a> Arbitrary<'a> for SignedAmount {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let s = i64::arbitrary(u)?; let s = i64::arbitrary(u)?;
Ok(Self(s)) Ok(Self::from_sat(s))
} }
} }

View File

@ -16,11 +16,11 @@ use super::*;
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
use crate::{FeeRate, Weight}; use crate::{FeeRate, Weight};
fn sat(sat: u64) -> Amount { Amount::from_sat(sat) }
fn ssat(ssat: i64) -> SignedAmount { SignedAmount::from_sat(ssat) }
#[test] #[test]
fn sanity_check() { fn sanity_check() {
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
assert_eq!(ssat(-100).abs(), ssat(100)); assert_eq!(ssat(-100).abs(), ssat(100));
assert_eq!(ssat(i64::MIN + 1).checked_abs().unwrap(), ssat(i64::MAX)); assert_eq!(ssat(i64::MIN + 1).checked_abs().unwrap(), ssat(i64::MAX));
assert_eq!(ssat(-100).signum(), -1); assert_eq!(ssat(-100).signum(), -1);
@ -102,25 +102,22 @@ fn from_str_zero_without_denomination() {
#[test] #[test]
fn from_int_btc() { fn from_int_btc() {
let amt = Amount::from_int_btc_const(2); let amt = Amount::from_int_btc_const(2);
assert_eq!(Amount::from_sat_unchecked(200_000_000), amt); assert_eq!(sat(200_000_000), amt);
} }
#[test] #[test]
fn amount_try_from_signed_amount() { fn amount_try_from_signed_amount() {
let sa_positive = SignedAmount::from_sat_unchecked(123); let sa_positive = ssat(123);
let ua_positive = Amount::try_from(sa_positive).unwrap(); let ua_positive = Amount::try_from(sa_positive).unwrap();
assert_eq!(ua_positive, Amount::from_sat_unchecked(123)); assert_eq!(ua_positive, sat(123));
let sa_negative = SignedAmount::from_sat_unchecked(-123); let sa_negative = ssat(-123);
let result = Amount::try_from(sa_negative); let result = Amount::try_from(sa_negative);
assert_eq!(result, Err(OutOfRangeError { is_signed: false, is_greater_than_max: false })); assert_eq!(result, Err(OutOfRangeError { is_signed: false, is_greater_than_max: false }));
} }
#[test] #[test]
fn mul_div() { fn mul_div() {
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
let op_result_sat = |sat| NumOpResult::Valid(Amount::from_sat(sat)); let op_result_sat = |sat| NumOpResult::Valid(Amount::from_sat(sat));
let op_result_ssat = |sat| NumOpResult::Valid(SignedAmount::from_sat(sat)); let op_result_ssat = |sat| NumOpResult::Valid(SignedAmount::from_sat(sat));
@ -141,17 +138,14 @@ fn neg() {
#[cfg(feature = "std")] #[cfg(feature = "std")]
#[test] #[test]
fn overflows() { fn overflows() {
let result = Amount::MAX + Amount::from_sat_unchecked(1); let result = Amount::MAX + sat(1);
assert!(result.is_error()); assert!(result.is_error());
let result = Amount::from_sat_unchecked(8_446_744_073_709_551_615) * 3; let result = sat(8_446_744_073_709_551_615) * 3;
assert!(result.is_error()); assert!(result.is_error());
} }
#[test] #[test]
fn add() { fn add() {
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
assert!(sat(0) + sat(0) == sat(0).into()); assert!(sat(0) + sat(0) == sat(0).into());
assert!(sat(127) + sat(179) == sat(306).into()); assert!(sat(127) + sat(179) == sat(306).into());
@ -164,9 +158,6 @@ fn add() {
#[test] #[test]
fn sub() { fn sub() {
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
assert!(sat(0) - sat(0) == sat(0).into()); assert!(sat(0) - sat(0) == sat(0).into());
assert!(sat(179) - sat(127) == sat(52).into()); assert!(sat(179) - sat(127) == sat(52).into());
assert!((sat(127) - sat(179)).is_error()); assert!((sat(127) - sat(179)).is_error());
@ -180,9 +171,6 @@ fn sub() {
#[test] #[test]
fn checked_arithmetic() { fn checked_arithmetic() {
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
assert_eq!(SignedAmount::MAX.checked_add(ssat(1)), None); assert_eq!(SignedAmount::MAX.checked_add(ssat(1)), None);
assert_eq!(SignedAmount::MIN.checked_sub(ssat(1)), None); assert_eq!(SignedAmount::MIN.checked_sub(ssat(1)), None);
assert_eq!(Amount::MAX.checked_add(sat(1)), None); assert_eq!(Amount::MAX.checked_add(sat(1)), None);
@ -192,22 +180,8 @@ fn checked_arithmetic() {
assert_eq!(ssat(-6).checked_div(2), Some(ssat(-3))); assert_eq!(ssat(-6).checked_div(2), Some(ssat(-3)));
} }
#[test]
#[allow(deprecated_in_future)]
fn unchecked_arithmetic() {
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
assert_eq!(ssat(10).unchecked_add(ssat(20)), ssat(30));
assert_eq!(ssat(50).unchecked_sub(ssat(10)), ssat(40));
assert_eq!(sat(5).unchecked_add(sat(7)), sat(12));
assert_eq!(sat(10).unchecked_sub(sat(7)), sat(3));
}
#[test] #[test]
fn positive_sub() { fn positive_sub() {
let ssat = SignedAmount::from_sat;
assert_eq!(ssat(10).positive_sub(ssat(7)).unwrap(), ssat(3)); assert_eq!(ssat(10).positive_sub(ssat(7)).unwrap(), ssat(3));
assert!(ssat(-10).positive_sub(ssat(7)).is_none()); assert!(ssat(-10).positive_sub(ssat(7)).is_none());
assert!(ssat(10).positive_sub(ssat(-7)).is_none()); assert!(ssat(10).positive_sub(ssat(-7)).is_none());
@ -218,12 +192,12 @@ fn positive_sub() {
#[test] #[test]
fn amount_checked_div_by_weight_ceil() { fn amount_checked_div_by_weight_ceil() {
let weight = Weight::from_kwu(1).unwrap(); let weight = Weight::from_kwu(1).unwrap();
let fee_rate = Amount::from_sat_unchecked(1).checked_div_by_weight_ceil(weight).unwrap(); let fee_rate = sat(1).checked_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)); 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 = Amount::from_sat_unchecked(329).checked_div_by_weight_ceil(weight).unwrap(); let fee_rate = sat(329).checked_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)); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(864));
@ -236,12 +210,12 @@ fn amount_checked_div_by_weight_ceil() {
#[test] #[test]
fn amount_checked_div_by_weight_floor() { fn amount_checked_div_by_weight_floor() {
let weight = Weight::from_kwu(1).unwrap(); let weight = Weight::from_kwu(1).unwrap();
let fee_rate = Amount::from_sat_unchecked(1).checked_div_by_weight_floor(weight).unwrap(); let fee_rate = sat(1).checked_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)); 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 = Amount::from_sat_unchecked(329).checked_div_by_weight_floor(weight).unwrap(); let fee_rate = sat(329).checked_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)); assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863));
@ -253,7 +227,7 @@ fn amount_checked_div_by_weight_floor() {
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
#[test] #[test]
fn amount_checked_div_by_fee_rate() { fn amount_checked_div_by_fee_rate() {
let amount = Amount::from_sat_unchecked(1000); let amount = sat(1000);
let fee_rate = FeeRate::from_sat_per_kwu(2); let fee_rate = FeeRate::from_sat_per_kwu(2);
// Test floor division // Test floor division
@ -266,7 +240,7 @@ fn amount_checked_div_by_fee_rate() {
assert_eq!(weight, Weight::from_wu(500_000)); // Same result for exact division assert_eq!(weight, Weight::from_wu(500_000)); // Same result for exact division
// Test truncation behavior // Test truncation behavior
let amount = Amount::from_sat_unchecked(1000); let amount = sat(1000);
let fee_rate = FeeRate::from_sat_per_kwu(3); let fee_rate = FeeRate::from_sat_per_kwu(3);
let floor_weight = amount.checked_div_by_fee_rate_floor(fee_rate).unwrap(); 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(); let ceil_weight = amount.checked_div_by_fee_rate_ceil(fee_rate).unwrap();
@ -287,7 +261,7 @@ fn amount_checked_div_by_fee_rate() {
// Test overflow case // Test overflow case
let tiny_fee_rate = FeeRate::from_sat_per_kwu(1); let tiny_fee_rate = FeeRate::from_sat_per_kwu(1);
let large_amount = Amount::from_sat(u64::MAX); let large_amount = 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_floor(tiny_fee_rate).is_none());
assert!(large_amount.checked_div_by_fee_rate_ceil(tiny_fee_rate).is_none()); assert!(large_amount.checked_div_by_fee_rate_ceil(tiny_fee_rate).is_none());
} }
@ -298,8 +272,6 @@ fn floating_point() {
use super::Denomination as D; use super::Denomination as D;
let f = Amount::from_float_in; let f = Amount::from_float_in;
let sf = SignedAmount::from_float_in; let sf = SignedAmount::from_float_in;
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
assert_eq!(f(11.22, D::Bitcoin), Ok(sat(1_122_000_000))); assert_eq!(f(11.22, D::Bitcoin), Ok(sat(1_122_000_000)));
assert_eq!(sf(-11.22, D::MilliBitcoin), Ok(ssat(-1_122_000))); assert_eq!(sf(-11.22, D::MilliBitcoin), Ok(ssat(-1_122_000)));
@ -339,53 +311,63 @@ fn floating_point() {
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin. #[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
fn parsing() { fn parsing() {
use super::ParseAmountError as E; use super::ParseAmountError as E;
let btc = Denomination::Bitcoin;
let sat = Denomination::Satoshi; let den_btc = Denomination::Bitcoin;
let den_sat = Denomination::Satoshi;
let p = Amount::from_str_in; let p = Amount::from_str_in;
let sp = SignedAmount::from_str_in; let sp = SignedAmount::from_str_in;
assert_eq!(p("x", btc), Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 0 })));
assert_eq!( assert_eq!(
p("-", btc), p("x", den_btc),
Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 0 }))
);
assert_eq!(
p("-", den_btc),
Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })) Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign }))
); );
assert_eq!( assert_eq!(
sp("-", btc), sp("-", den_btc),
Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })) Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign }))
); );
assert_eq!( assert_eq!(
p("-1.0x", btc), p("-1.0x", den_btc),
Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 4 })) Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 4 }))
); );
assert_eq!( assert_eq!(
p("0.0 ", btc), p("0.0 ", den_btc),
Err(E::from(InvalidCharacterError { invalid_char: ' ', position: 3 })) Err(E::from(InvalidCharacterError { invalid_char: ' ', position: 3 }))
); );
assert_eq!( assert_eq!(
p("0.000.000", btc), p("0.000.000", den_btc),
Err(E::from(InvalidCharacterError { invalid_char: '.', position: 5 })) Err(E::from(InvalidCharacterError { invalid_char: '.', position: 5 }))
); );
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
let more_than_max = format!("{}", Amount::MAX.to_sat() + 1); let more_than_max = format!("{}", Amount::MAX.to_sat() + 1);
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
assert_eq!(p(&more_than_max, btc), Err(OutOfRangeError::too_big(false).into())); assert_eq!(p(&more_than_max, den_btc), Err(OutOfRangeError::too_big(false).into()));
assert_eq!(p("0.000000042", btc), Err(TooPreciseError { position: 10 }.into())); assert_eq!(p("0.000000042", den_btc), Err(TooPreciseError { position: 10 }.into()));
assert_eq!(p("1.0000000", sat), Ok(Amount::from_sat_unchecked(1))); assert_eq!(p("1.0000000", den_sat), Ok(sat(1)));
assert_eq!(p("1.1", sat), Err(TooPreciseError { position: 2 }.into())); assert_eq!(p("1.1", den_sat), Err(TooPreciseError { position: 2 }.into()));
assert_eq!(p("1000.1", sat), Err(TooPreciseError { position: 5 }.into())); assert_eq!(p("1000.1", den_sat), Err(TooPreciseError { position: 5 }.into()));
assert_eq!(p("1001.0000000", sat), Ok(Amount::from_sat_unchecked(1001))); assert_eq!(p("1001.0000000", den_sat), Ok(sat(1001)));
assert_eq!(p("1000.0000001", sat), Err(TooPreciseError { position: 11 }.into())); assert_eq!(p("1000.0000001", den_sat), Err(TooPreciseError { position: 11 }.into()));
assert_eq!(p("1", btc), Ok(Amount::from_sat_unchecked(1_000_000_00))); assert_eq!(p("1", den_btc), Ok(sat(1_000_000_00)));
assert_eq!(sp("-.5", btc), Ok(SignedAmount::from_sat_unchecked(-500_000_00))); assert_eq!(sp("-.5", den_btc), Ok(ssat(-500_000_00)));
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
assert_eq!(sp(&SignedAmount::MIN.to_sat().to_string(), sat), Ok(SignedAmount::MIN)); assert_eq!(sp(&SignedAmount::MIN.to_sat().to_string(), den_sat), Ok(SignedAmount::MIN));
assert_eq!(p("1.1", btc), Ok(Amount::from_sat_unchecked(1_100_000_00))); assert_eq!(p("1.1", den_btc), Ok(sat(1_100_000_00)));
assert_eq!(p("100", sat), Ok(Amount::from_sat_unchecked(100))); assert_eq!(p("100", den_sat), Ok(sat(100)));
assert_eq!(p("55", sat), Ok(Amount::from_sat_unchecked(55))); assert_eq!(p("55", den_sat), Ok(sat(55)));
assert_eq!(p("2100000000000000", sat), Ok(Amount::from_sat_unchecked(21_000_000__000_000_00))); assert_eq!(
assert_eq!(p("2100000000000000.", sat), Ok(Amount::from_sat_unchecked(21_000_000__000_000_00))); p("2100000000000000", den_sat),
assert_eq!(p("21000000", btc), Ok(Amount::from_sat_unchecked(21_000_000__000_000_00))); Ok(sat(21_000_000__000_000_00))
);
assert_eq!(
p("2100000000000000.", den_sat),
Ok(sat(21_000_000__000_000_00))
);
assert_eq!(p("21000000", den_btc), Ok(sat(21_000_000__000_000_00)));
// exactly 50 chars. // exactly 50 chars.
assert_eq!( assert_eq!(
@ -408,15 +390,12 @@ fn to_string() {
assert_eq!(format!("{:.8}", Amount::ONE_BTC.display_in(D::Bitcoin)), "1.00000000"); assert_eq!(format!("{:.8}", Amount::ONE_BTC.display_in(D::Bitcoin)), "1.00000000");
assert_eq!(Amount::ONE_BTC.to_string_in(D::Satoshi), "100000000"); assert_eq!(Amount::ONE_BTC.to_string_in(D::Satoshi), "100000000");
assert_eq!(Amount::ONE_SAT.to_string_in(D::Bitcoin), "0.00000001"); assert_eq!(Amount::ONE_SAT.to_string_in(D::Bitcoin), "0.00000001");
assert_eq!(SignedAmount::from_sat_unchecked(-42).to_string_in(D::Bitcoin), "-0.00000042"); assert_eq!(ssat(-42).to_string_in(D::Bitcoin), "-0.00000042");
assert_eq!(Amount::ONE_BTC.to_string_with_denomination(D::Bitcoin), "1 BTC"); assert_eq!(Amount::ONE_BTC.to_string_with_denomination(D::Bitcoin), "1 BTC");
assert_eq!(SignedAmount::ONE_BTC.to_string_with_denomination(D::Satoshi), "100000000 satoshi"); assert_eq!(SignedAmount::ONE_BTC.to_string_with_denomination(D::Satoshi), "100000000 satoshi");
assert_eq!(Amount::ONE_SAT.to_string_with_denomination(D::Bitcoin), "0.00000001 BTC"); assert_eq!(Amount::ONE_SAT.to_string_with_denomination(D::Bitcoin), "0.00000001 BTC");
assert_eq!( assert_eq!(ssat(-42).to_string_with_denomination(D::Bitcoin), "-0.00000042 BTC");
SignedAmount::from_sat_unchecked(-42).to_string_with_denomination(D::Bitcoin),
"-0.00000042 BTC"
);
} }
// May help identify a problem sooner // May help identify a problem sooner
@ -593,8 +572,6 @@ check_format_non_negative_show_denom! {
#[test] #[test]
fn unsigned_signed_conversion() { fn unsigned_signed_conversion() {
let ssat = SignedAmount::from_sat;
let sat = Amount::from_sat;
let max_sats: u64 = Amount::MAX.to_sat(); let max_sats: u64 = Amount::MAX.to_sat();
assert_eq!(sat(max_sats).to_signed(), ssat(max_sats as i64)); assert_eq!(sat(max_sats).to_signed(), ssat(max_sats as i64));
@ -672,12 +649,12 @@ fn from_str() {
case("21000001 BTC", Err(OutOfRangeError::too_big(false))); case("21000001 BTC", Err(OutOfRangeError::too_big(false)));
case("18446744073709551616 sat", Err(OutOfRangeError::too_big(false))); case("18446744073709551616 sat", Err(OutOfRangeError::too_big(false)));
ok_case(".5 bits", Amount::from_sat_unchecked(50)); ok_case(".5 bits", sat(50));
ok_scase("-.5 bits", SignedAmount::from_sat_unchecked(-50)); ok_scase("-.5 bits", ssat(-50));
ok_case("0.00253583 BTC", Amount::from_sat_unchecked(253_583)); ok_case("0.00253583 BTC", sat(253_583));
ok_scase("-5 satoshi", SignedAmount::from_sat_unchecked(-5)); ok_scase("-5 satoshi", ssat(-5));
ok_case("0.10000000 BTC", Amount::from_sat_unchecked(100_000_00)); ok_case("0.10000000 BTC", sat(100_000_00));
ok_scase("-100 bits", SignedAmount::from_sat_unchecked(-10_000)); ok_scase("-100 bits", ssat(-10_000));
ok_case("21000000 BTC", Amount::MAX); ok_case("21000000 BTC", Amount::MAX);
ok_scase("21000000 BTC", SignedAmount::MAX); ok_scase("21000000 BTC", SignedAmount::MAX);
ok_scase("-21000000 BTC", SignedAmount::MIN); ok_scase("-21000000 BTC", SignedAmount::MIN);
@ -739,15 +716,28 @@ fn to_from_string_in() {
assert_eq!(ua_str(&ua_sat(21_000_000).to_string_in(D::Bit), D::Bit), Ok(ua_sat(21_000_000))); assert_eq!(ua_str(&ua_sat(21_000_000).to_string_in(D::Bit), D::Bit), Ok(ua_sat(21_000_000)));
assert_eq!(ua_str(&ua_sat(0).to_string_in(D::Satoshi), D::Satoshi), Ok(ua_sat(0))); assert_eq!(ua_str(&ua_sat(0).to_string_in(D::Satoshi), D::Satoshi), Ok(ua_sat(0)));
assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::Bitcoin), D::Bitcoin).is_ok()); assert!(
assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::CentiBitcoin), D::CentiBitcoin) ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::Bitcoin), D::Bitcoin).is_ok()
);
assert!(ua_str(
&ua_sat(Amount::MAX.to_sat()).to_string_in(D::CentiBitcoin),
D::CentiBitcoin
)
.is_ok()); .is_ok());
assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::MilliBitcoin), D::MilliBitcoin) assert!(ua_str(
&ua_sat(Amount::MAX.to_sat()).to_string_in(D::MilliBitcoin),
D::MilliBitcoin
)
.is_ok()); .is_ok());
assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::MicroBitcoin), D::MicroBitcoin) assert!(ua_str(
&ua_sat(Amount::MAX.to_sat()).to_string_in(D::MicroBitcoin),
D::MicroBitcoin
)
.is_ok()); .is_ok());
assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::Bit), D::Bit).is_ok()); assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::Bit), D::Bit).is_ok());
assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::Satoshi), D::Satoshi).is_ok()); assert!(
ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::Satoshi), D::Satoshi).is_ok()
);
assert_eq!( assert_eq!(
sa_str(&SignedAmount::MAX.to_string_in(D::Satoshi), D::MicroBitcoin), sa_str(&SignedAmount::MAX.to_string_in(D::Satoshi), D::MicroBitcoin),
@ -767,7 +757,7 @@ fn to_string_with_denomination_from_str_roundtrip() {
use super::Denomination as D; use super::Denomination as D;
let amt = Amount::from_sat_unchecked(42); let amt = sat(42);
let denom = Amount::to_string_with_denomination; let denom = Amount::to_string_with_denomination;
assert_eq!(denom(amt, D::Bitcoin).parse::<Amount>(), Ok(amt)); assert_eq!(denom(amt, D::Bitcoin).parse::<Amount>(), Ok(amt));
assert_eq!(denom(amt, D::CentiBitcoin).parse::<Amount>(), Ok(amt)); assert_eq!(denom(amt, D::CentiBitcoin).parse::<Amount>(), Ok(amt));
@ -798,10 +788,7 @@ fn serde_as_sat() {
} }
serde_test::assert_tokens( serde_test::assert_tokens(
&T { &T { amt: sat(123_456_789), samt: ssat(-123_456_789) },
amt: Amount::from_sat_unchecked(123_456_789),
samt: SignedAmount::from_sat_unchecked(-123_456_789),
},
&[ &[
serde_test::Token::Struct { name: "T", len: 2 }, serde_test::Token::Struct { name: "T", len: 2 },
serde_test::Token::Str("amt"), serde_test::Token::Str("amt"),
@ -828,10 +815,7 @@ fn serde_as_btc() {
pub samt: SignedAmount, pub samt: SignedAmount,
} }
let orig = T { let orig = T { amt: sat(20_000_000__000_000_01), samt: ssat(-20_000_000__000_000_01) };
amt: Amount::from_sat_unchecked(20_000_000__000_000_01),
samt: SignedAmount::from_sat_unchecked(-20_000_000__000_000_01),
};
let json = "{\"amt\": 20000000.00000001, \ let json = "{\"amt\": 20000000.00000001, \
\"samt\": -20000000.00000001}"; \"samt\": -20000000.00000001}";
@ -865,10 +849,7 @@ fn serde_as_str() {
} }
serde_test::assert_tokens( serde_test::assert_tokens(
&T { &T { amt: sat(123_456_789), samt: ssat(-123_456_789) },
amt: Amount::from_sat_unchecked(123_456_789),
samt: SignedAmount::from_sat_unchecked(-123_456_789),
},
&[ &[
serde_test::Token::Struct { name: "T", len: 2 }, serde_test::Token::Struct { name: "T", len: 2 },
serde_test::Token::String("amt"), serde_test::Token::String("amt"),
@ -895,10 +876,7 @@ fn serde_as_btc_opt() {
pub samt: Option<SignedAmount>, pub samt: Option<SignedAmount>,
} }
let with = T { let with = T { amt: Some(sat(2_500_000_00)), samt: Some(ssat(-2_500_000_00)) };
amt: Some(Amount::from_sat_unchecked(2_500_000_00)),
samt: Some(SignedAmount::from_sat_unchecked(-2_500_000_00)),
};
let without = T { amt: None, samt: None }; let without = T { amt: None, samt: None };
// Test Roundtripping // Test Roundtripping
@ -937,10 +915,7 @@ fn serde_as_sat_opt() {
pub samt: Option<SignedAmount>, pub samt: Option<SignedAmount>,
} }
let with = T { let with = T { amt: Some(sat(2_500_000_00)), samt: Some(ssat(-2_500_000_00)) };
amt: Some(Amount::from_sat_unchecked(2_500_000_00)),
samt: Some(SignedAmount::from_sat_unchecked(-2_500_000_00)),
};
let without = T { amt: None, samt: None }; let without = T { amt: None, samt: None };
// Test Roundtripping // Test Roundtripping
@ -979,10 +954,7 @@ fn serde_as_str_opt() {
pub samt: Option<SignedAmount>, pub samt: Option<SignedAmount>,
} }
let with = T { let with = T { amt: Some(sat(123_456_789)), samt: Some(ssat(-123_456_789)) };
amt: Some(Amount::from_sat_unchecked(123_456_789)),
samt: Some(SignedAmount::from_sat_unchecked(-123_456_789)),
};
let without = T { amt: None, samt: None }; let without = T { amt: None, samt: None };
// Test Roundtripping // Test Roundtripping
@ -1009,9 +981,6 @@ fn serde_as_str_opt() {
#[test] #[test]
fn sum_amounts() { fn sum_amounts() {
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
assert_eq!([].iter().sum::<NumOpResult<Amount>>(), Amount::ZERO.into()); assert_eq!([].iter().sum::<NumOpResult<Amount>>(), Amount::ZERO.into());
assert_eq!([].iter().sum::<NumOpResult<SignedAmount>>(), SignedAmount::ZERO.into()); assert_eq!([].iter().sum::<NumOpResult<SignedAmount>>(), SignedAmount::ZERO.into());
@ -1044,9 +1013,6 @@ fn sum_amounts() {
#[test] #[test]
fn checked_sum_amounts() { fn checked_sum_amounts() {
let sat = Amount::from_sat;
let ssat = SignedAmount::from_sat;
assert_eq!([].into_iter().checked_sum(), Some(Amount::ZERO)); assert_eq!([].into_iter().checked_sum(), Some(Amount::ZERO));
assert_eq!([].into_iter().checked_sum(), Some(SignedAmount::ZERO)); assert_eq!([].into_iter().checked_sum(), Some(SignedAmount::ZERO));
@ -1112,8 +1078,6 @@ fn disallow_unknown_denomination() {
#[test] #[test]
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
fn trailing_zeros_for_amount() { fn trailing_zeros_for_amount() {
let sat = Amount::from_sat;
assert_eq!(format!("{}", sat(1_000_000)), "0.01 BTC"); assert_eq!(format!("{}", sat(1_000_000)), "0.01 BTC");
assert_eq!(format!("{}", Amount::ONE_SAT), "0.00000001 BTC"); assert_eq!(format!("{}", Amount::ONE_SAT), "0.00000001 BTC");
assert_eq!(format!("{}", Amount::ONE_BTC), "1 BTC"); assert_eq!(format!("{}", Amount::ONE_BTC), "1 BTC");
@ -1194,8 +1158,6 @@ fn add_sub_combos() {
#[test] #[test]
fn unsigned_addition() { fn unsigned_addition() {
let sat = Amount::from_sat;
assert_eq!(sat(0) + sat(0), NumOpResult::from(sat(0))); assert_eq!(sat(0) + sat(0), NumOpResult::from(sat(0)));
assert_eq!(sat(0) + sat(307), NumOpResult::from(sat(307))); assert_eq!(sat(0) + sat(307), NumOpResult::from(sat(307)));
assert_eq!(sat(307) + sat(0), NumOpResult::from(sat(307))); assert_eq!(sat(307) + sat(0), NumOpResult::from(sat(307)));
@ -1205,8 +1167,6 @@ fn unsigned_addition() {
#[test] #[test]
fn signed_addition() { fn signed_addition() {
let ssat = SignedAmount::from_sat;
assert_eq!(ssat(0) + ssat(0), NumOpResult::from(ssat(0))); assert_eq!(ssat(0) + ssat(0), NumOpResult::from(ssat(0)));
assert_eq!(ssat(0) + ssat(307), NumOpResult::from(ssat(307))); assert_eq!(ssat(0) + ssat(307), NumOpResult::from(ssat(307)));
assert_eq!(ssat(307) + ssat(0), NumOpResult::from(ssat(307))); assert_eq!(ssat(307) + ssat(0), NumOpResult::from(ssat(307)));
@ -1226,8 +1186,6 @@ fn signed_addition() {
#[test] #[test]
fn unsigned_subtraction() { fn unsigned_subtraction() {
let sat = Amount::from_sat;
assert_eq!(sat(0) - sat(0), NumOpResult::from(sat(0))); assert_eq!(sat(0) - sat(0), NumOpResult::from(sat(0)));
assert_eq!(sat(307) - sat(0), NumOpResult::from(sat(307))); assert_eq!(sat(307) - sat(0), NumOpResult::from(sat(307)));
assert_eq!(sat(461) - sat(307), NumOpResult::from(sat(154))); assert_eq!(sat(461) - sat(307), NumOpResult::from(sat(154)));
@ -1235,8 +1193,6 @@ fn unsigned_subtraction() {
#[test] #[test]
fn signed_subtraction() { fn signed_subtraction() {
let ssat = SignedAmount::from_sat;
assert_eq!(ssat(0) - ssat(0), NumOpResult::from(ssat(0))); assert_eq!(ssat(0) - ssat(0), NumOpResult::from(ssat(0)));
assert_eq!(ssat(0) - ssat(307), NumOpResult::from(ssat(-307))); assert_eq!(ssat(0) - ssat(307), NumOpResult::from(ssat(-307)));
assert_eq!(ssat(307) - ssat(0), NumOpResult::from(ssat(307))); assert_eq!(ssat(307) - ssat(0), NumOpResult::from(ssat(307)));
@ -1252,11 +1208,8 @@ fn signed_subtraction() {
#[test] #[test]
fn op_int_combos() { fn op_int_combos() {
let sat = Amount::from_sat; let res = |n_sat| NumOpResult::from(sat(n_sat));
let ssat = SignedAmount::from_sat; let sres = |n_ssat| NumOpResult::from(ssat(n_ssat));
let res = |sat| NumOpResult::from(Amount::from_sat(sat));
let sres = |ssat| NumOpResult::from(SignedAmount::from_sat(ssat));
assert_eq!(sat(23) * 31, res(713)); assert_eq!(sat(23) * 31, res(713));
assert_eq!(ssat(23) * 31, sres(713)); assert_eq!(ssat(23) * 31, sres(713));
@ -1295,8 +1248,6 @@ fn op_int_combos() {
#[test] #[test]
fn unsigned_amount_div_by_amount() { fn unsigned_amount_div_by_amount() {
let sat = Amount::from_sat;
assert_eq!(sat(0) / sat(7), 0); assert_eq!(sat(0) / sat(7), 0);
assert_eq!(sat(1897) / sat(7), 271); assert_eq!(sat(1897) / sat(7), 271);
} }
@ -1309,8 +1260,6 @@ fn unsigned_amount_div_by_amount_zero() {
#[test] #[test]
fn signed_amount_div_by_amount() { fn signed_amount_div_by_amount() {
let ssat = SignedAmount::from_sat;
assert_eq!(ssat(0) / ssat(7), 0); assert_eq!(ssat(0) / ssat(7), 0);
assert_eq!(ssat(1897) / ssat(7), 271); assert_eq!(ssat(1897) / ssat(7), 271);
@ -1331,7 +1280,7 @@ fn check_const() {
assert_eq!(Amount::ONE_BTC.to_sat(), 100_000_000); assert_eq!(Amount::ONE_BTC.to_sat(), 100_000_000);
assert_eq!(SignedAmount::FIFTY_BTC.to_sat(), SignedAmount::ONE_BTC.to_sat() * 50); assert_eq!(SignedAmount::FIFTY_BTC.to_sat(), SignedAmount::ONE_BTC.to_sat() * 50);
assert_eq!(Amount::FIFTY_BTC.to_sat(), Amount::ONE_BTC.to_sat() * 50); assert_eq!(Amount::FIFTY_BTC.to_sat(), Amount::ONE_BTC.to_sat() * 50);
assert_eq!(Amount::MAX_MONEY.to_sat() as i64, SignedAmount::MAX_MONEY.to_sat()); assert_eq!(Amount::MAX.to_sat() as i64, SignedAmount::MAX.to_sat());
} }
// Sanity check than stdlib supports the set of reference combinations for the ops we want. // Sanity check than stdlib supports the set of reference combinations for the ops we want.

View File

@ -16,12 +16,21 @@ use super::{
OutOfRangeError, ParseAmountError, ParseError, SignedAmount, OutOfRangeError, ParseAmountError, ParseError, SignedAmount,
}; };
mod encapsulate {
/// An amount. /// An amount.
/// ///
/// The [`Amount`] type can be used to express Bitcoin amounts that support arithmetic and /// The [`Amount`] type can be used to express Bitcoin amounts that support arithmetic and
/// conversion to various denominations. The [`Amount`] type does not implement [`serde`] traits /// conversion to various denominations. The [`Amount`] type does not implement [`serde`] traits
/// but we do provide modules for serializing as satoshis or bitcoin. /// but we do provide modules for serializing as satoshis or bitcoin.
/// ///
/// Warning!
///
/// This type implements several arithmetic operations from [`core::ops`].
/// To prevent errors due to an overflow when using these operations,
/// it is advised to instead use the checked arithmetic methods whose names
/// start with `checked_`. The operations from [`core::ops`] that [`Amount`]
/// implements will panic when an overflow occurs.
///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
@ -40,17 +49,37 @@ use super::{
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Amount(u64); pub struct Amount(u64);
impl Amount {
/// Constructs a new [`Amount`] with satoshi precision and the given number of satoshis.
///
/// Caller to guarantee that `satoshi` is within valid range. See [`Self::MAX`].
pub const fn from_sat_unchecked(satoshi: u64) -> Amount { Self(satoshi) }
/// Gets the number of satoshis in this [`Amount`].
///
/// # Examples
///
/// ```
/// # use bitcoin_units::Amount;
/// assert_eq!(Amount::ONE_BTC.to_sat(), 100_000_000);
/// ```
pub const fn to_sat(self) -> u64 { self.0 }
}
}
#[doc(inline)]
pub use encapsulate::Amount;
impl Amount { impl Amount {
/// The zero amount. /// The zero amount.
pub const ZERO: Self = Amount(0); pub const ZERO: Self = Amount::from_sat_unchecked(0);
/// Exactly one satoshi. /// Exactly one satoshi.
pub const ONE_SAT: Self = Amount(1); pub const ONE_SAT: Self = Amount::from_sat_unchecked(1);
/// Exactly one bitcoin. /// Exactly one bitcoin.
pub const ONE_BTC: Self = Self::from_int_btc_const(1); pub const ONE_BTC: Self = Amount::from_sat_unchecked(100_000_000);
/// Exactly fifty bitcoin. /// Exactly fifty bitcoin.
pub const FIFTY_BTC: Self = Self::from_sat_unchecked(50 * 100_000_000); pub const FIFTY_BTC: Self = Amount::from_sat_unchecked(50 * 100_000_000);
/// The maximum value allowed as an amount. Useful for sanity checking. /// The maximum value allowed as an amount. Useful for sanity checking.
pub const MAX_MONEY: Self = Self::from_int_btc_const(21_000_000); pub const MAX_MONEY: Self = Amount::from_sat_unchecked(21_000_000 * 100_000_000);
/// The minimum value of an amount. /// The minimum value of an amount.
pub const MIN: Self = Amount::ZERO; pub const MIN: Self = Amount::ZERO;
/// The maximum value of an amount. /// The maximum value of an amount.
@ -67,22 +96,7 @@ impl Amount {
/// let amount = Amount::from_sat(100_000); /// let amount = Amount::from_sat(100_000);
/// assert_eq!(amount.to_sat(), 100_000); /// assert_eq!(amount.to_sat(), 100_000);
/// ``` /// ```
pub const fn from_sat(satoshi: u64) -> Amount { Amount(satoshi) } pub const fn from_sat(satoshi: u64) -> Amount { Amount::from_sat_unchecked(satoshi) }
/// Gets the number of satoshis in this [`Amount`].
///
/// # Examples
///
/// ```
/// # use bitcoin_units::Amount;
/// assert_eq!(Amount::ONE_BTC.to_sat(), 100_000_000);
/// ```
pub const fn to_sat(self) -> u64 { self.0 }
/// Constructs a new [`Amount`] with satoshi precision and the given number of satoshis.
///
/// Caller to guarantee that `satoshi` is within valid range. See [`Self::MAX_MONEY`].
pub const fn from_sat_unchecked(satoshi: u64) -> Amount { Self(satoshi) }
/// Converts from a value expressing a decimal number of bitcoin to an [`Amount`]. /// Converts from a value expressing a decimal number of bitcoin to an [`Amount`].
/// ///
@ -105,30 +119,19 @@ impl Amount {
} }
/// Converts from a value expressing a whole number of bitcoin to an [`Amount`]. /// Converts from a value expressing a whole number of bitcoin to an [`Amount`].
/// #[allow(clippy::missing_panics_doc)]
/// # Errors pub fn from_int_btc<T: Into<u32>>(whole_bitcoin: T) -> Amount {
/// Amount::from_int_btc_const(whole_bitcoin.into())
/// The function errors if the argument multiplied by the number of sats
/// per bitcoin overflows a `u64` type.
pub fn from_int_btc<T: Into<u64>>(whole_bitcoin: T) -> Result<Amount, OutOfRangeError> {
match whole_bitcoin.into().checked_mul(100_000_000) {
Some(amount) => Ok(Self::from_sat(amount)),
None => Err(OutOfRangeError { is_signed: false, is_greater_than_max: true }),
}
} }
/// Converts from a value expressing a whole number of bitcoin to an [`Amount`] /// Converts from a value expressing a whole number of bitcoin to an [`Amount`]
/// in const context. /// in const context.
/// #[allow(clippy::missing_panics_doc)]
/// # Panics
///
/// The function panics if the argument multiplied by the number of sats
/// per bitcoin overflows a `u64` type.
pub const fn from_int_btc_const(whole_bitcoin: u32) -> Amount { pub const fn from_int_btc_const(whole_bitcoin: u32) -> Amount {
let btc = whole_bitcoin as u64; // Can't call u64::from in const context. let btc = whole_bitcoin as u64; // Can't call `into` in const context.
match btc.checked_mul(100_000_000) { match btc.checked_mul(100_000_000) {
Some(amount) => Amount::from_sat(amount), Some(amount) => Amount::from_sat(amount),
None => panic!("checked_mul overflowed"), None => panic!("cannot overflow a u64"),
} }
} }
@ -148,7 +151,7 @@ impl Amount {
OutOfRangeError::negative(), OutOfRangeError::negative(),
))); )));
} }
if sats > Self::MAX.0 { if sats > Self::MAX.to_sat() {
return Err(ParseAmountError(ParseAmountErrorInner::OutOfRange( return Err(ParseAmountError(ParseAmountErrorInner::OutOfRange(
OutOfRangeError::too_big(false), OutOfRangeError::too_big(false),
))); )));
@ -299,8 +302,8 @@ impl Amount {
#[must_use] #[must_use]
pub const fn checked_add(self, rhs: Amount) -> Option<Amount> { pub const fn checked_add(self, rhs: Amount) -> Option<Amount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_add(rhs.0) { match self.to_sat().checked_add(rhs.to_sat()) {
Some(res) => Amount(res).check_max(), Some(res) => Amount::from_sat(res).check_max(),
None => None, None => None,
} }
} }
@ -311,8 +314,8 @@ impl Amount {
#[must_use] #[must_use]
pub const fn checked_sub(self, rhs: Amount) -> Option<Amount> { pub const fn checked_sub(self, rhs: Amount) -> Option<Amount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_sub(rhs.0) { match self.to_sat().checked_sub(rhs.to_sat()) {
Some(res) => Some(Amount(res)), Some(res) => Some(Amount::from_sat(res)),
None => None, None => None,
} }
} }
@ -323,8 +326,8 @@ impl Amount {
#[must_use] #[must_use]
pub const fn checked_mul(self, rhs: u64) -> Option<Amount> { pub const fn checked_mul(self, rhs: u64) -> Option<Amount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_mul(rhs) { match self.to_sat().checked_mul(rhs) {
Some(res) => Amount(res).check_max(), Some(res) => Amount::from_sat(res).check_max(),
None => None, None => None,
} }
} }
@ -337,8 +340,8 @@ impl Amount {
#[must_use] #[must_use]
pub const fn checked_div(self, rhs: u64) -> Option<Amount> { pub const fn checked_div(self, rhs: u64) -> Option<Amount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_div(rhs) { match self.to_sat().checked_div(rhs) {
Some(res) => Some(Amount(res)), Some(res) => Some(Amount::from_sat(res)),
None => None, None => None,
} }
} }
@ -349,34 +352,12 @@ impl Amount {
#[must_use] #[must_use]
pub const fn checked_rem(self, rhs: u64) -> Option<Amount> { pub const fn checked_rem(self, rhs: u64) -> Option<Amount> {
// No `map()` in const context. // No `map()` in const context.
match self.0.checked_rem(rhs) { match self.to_sat().checked_rem(rhs) {
Some(res) => Some(Amount(res)), Some(res) => Some(Amount::from_sat(res)),
None => None, None => None,
} }
} }
/// Unchecked addition.
///
/// Computes `self + rhs`.
///
/// # Panics
///
/// On overflow, panics in debug mode, wraps in release mode.
#[must_use]
#[deprecated(since = "TBD", note = "consider converting to u64 using `to_sat`")]
pub fn unchecked_add(self, rhs: Amount) -> Amount { Self(self.0 + rhs.0) }
/// Unchecked subtraction.
///
/// Computes `self - rhs`.
///
/// # Panics
///
/// On overflow, panics in debug mode, wraps in release mode.
#[must_use]
#[deprecated(since = "TBD", note = "consider converting to u64 using `to_sat`")]
pub fn unchecked_sub(self, rhs: Amount) -> Amount { Self(self.0 - rhs.0) }
/// Converts to a signed amount. /// Converts to a signed amount.
#[rustfmt::skip] // Moves code comments to the wrong line. #[rustfmt::skip] // Moves code comments to the wrong line.
pub fn to_signed(self) -> SignedAmount { pub fn to_signed(self) -> SignedAmount {
@ -385,7 +366,7 @@ impl Amount {
/// Checks if the amount is below the maximum value. Returns `None` if it is above. /// Checks if the amount is below the maximum value. Returns `None` if it is above.
const fn check_max(self) -> Option<Amount> { const fn check_max(self) -> Option<Amount> {
if self.0 > Self::MAX.0 { if self.to_sat() > Self::MAX.to_sat() {
None None
} else { } else {
Some(self) Some(self)
@ -445,6 +426,6 @@ impl TryFrom<SignedAmount> for Amount {
impl<'a> Arbitrary<'a> for Amount { impl<'a> Arbitrary<'a> for Amount {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let a = u64::arbitrary(u)?; let a = u64::arbitrary(u)?;
Ok(Self(a)) Ok(Self::from_sat(a))
} }
} }

View File

@ -13,6 +13,7 @@
use core::ops; use core::ops;
use crate::amount::{NumOpResult, OptionExt};
use crate::{Amount, FeeRate, Weight}; use crate::{Amount, FeeRate, Weight};
impl Amount { impl Amount {
@ -160,17 +161,15 @@ impl FeeRate {
/// Computes the ceiling so that the fee computation is conservative. /// Computes the ceiling so that the fee computation is conservative.
impl ops::Mul<FeeRate> for Weight { impl ops::Mul<FeeRate> for Weight {
type Output = Amount; type Output = NumOpResult<Amount>;
fn mul(self, rhs: FeeRate) -> Self::Output { fn mul(self, rhs: FeeRate) -> Self::Output { rhs.checked_mul_by_weight(self).valid_or_error() }
Amount::from_sat((rhs.to_sat_per_kwu() * self.to_wu() + 999) / 1000)
}
} }
impl ops::Mul<Weight> for FeeRate { impl ops::Mul<Weight> for FeeRate {
type Output = Amount; type Output = NumOpResult<Amount>;
fn mul(self, rhs: Weight) -> Self::Output { rhs * self } fn mul(self, rhs: Weight) -> Self::Output { self.checked_mul_by_weight(rhs).valid_or_error() }
} }
impl ops::Div<Weight> for Amount { impl ops::Div<Weight> for Amount {
@ -265,7 +264,7 @@ mod tests {
let three = Weight::from_vb(3).unwrap(); let three = Weight::from_vb(3).unwrap();
let six = Amount::from_sat_unchecked(6); let six = Amount::from_sat_unchecked(6);
assert_eq!(two * three, six); assert_eq!(two * three, six.into());
} }
#[test] #[test]

View File

@ -22,10 +22,10 @@ macro_rules! check {
check! { check! {
amount_unsigned_one_sat, Amount, Amount::ONE_SAT, "0.00000001 BTC"; amount_unsigned_one_sat, Amount, Amount::ONE_SAT, "0.00000001 BTC";
amount_unsigned_max_money, Amount, Amount::MAX_MONEY, "21000000 BTC"; amount_unsigned_max_money, Amount, Amount::MAX, "21000000 BTC";
amount_signed_one_sat, SignedAmount, SignedAmount::ONE_SAT, "0.00000001 BTC"; amount_signed_one_sat, SignedAmount, SignedAmount::ONE_SAT, "0.00000001 BTC";
amount_signed_max_money, SignedAmount, SignedAmount::MAX_MONEY, "21000000 BTC"; amount_signed_max_money, SignedAmount, SignedAmount::MAX, "21000000 BTC";
block_height_min, BlockHeight, BlockHeight::MIN, "0"; block_height_min, BlockHeight, BlockHeight::MIN, "0";
block_height_max, BlockHeight, BlockHeight::MAX, "4294967295"; block_height_max, BlockHeight, BlockHeight::MAX, "4294967295";