diff --git a/bitcoin/src/blockdata/script/borrowed.rs b/bitcoin/src/blockdata/script/borrowed.rs index 624feb3b5..4ae5c9ce1 100644 --- a/bitcoin/src/blockdata/script/borrowed.rs +++ b/bitcoin/src/blockdata/script/borrowed.rs @@ -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 /// broadcastable on today’s Bitcoin network. #[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 { self.minimal_non_dust() } /// Returns the minimum value an output with this script should have in order to be /// 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`]. /// /// [`minimal_non_dust_custom`]: Script::minimal_non_dust_custom - fn minimal_non_dust(&self) -> Amount { + fn minimal_non_dust(&self) -> Option { 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`]. /// /// [`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 { 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! { 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 { // 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. 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 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 - }) - .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 // 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. - Amount::from_sat(sats) + Some(Amount::from_sat(sats)) } fn count_sigops_internal(&self, accurate: bool) -> usize { diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs index 9b47ef652..4443645b1 100644 --- a/bitcoin/src/blockdata/script/tests.rs +++ b/bitcoin/src/blockdata/script/tests.rs @@ -684,10 +684,10 @@ fn default_dust_value() { // well-known scriptPubKey types. let script_p2wpkh = Builder::new().push_int_unchecked(0).push_slice([42; 20]).into_script(); 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!( 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() @@ -698,10 +698,10 @@ fn default_dust_value() { .push_opcode(OP_CHECKSIG) .into_script(); 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!( script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)), - Amount::from_sat_unchecked(1092) + Some(Amount::from_sat_unchecked(1092)) ); } diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs index 4510c465d..0c60d3c65 100644 --- a/bitcoin/src/blockdata/transaction.rs +++ b/bitcoin/src/blockdata/transaction.rs @@ -183,8 +183,8 @@ crate::internal_macros::define_extension_trait! { /// To use a custom value, use [`minimal_non_dust_custom`]. /// /// [`minimal_non_dust_custom`]: TxOut::minimal_non_dust_custom - fn minimal_non_dust(script_pubkey: ScriptBuf) -> Self { - TxOut { value: script_pubkey.minimal_non_dust(), script_pubkey } + fn minimal_non_dust(script_pubkey: ScriptBuf) -> Option { + 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 @@ -198,8 +198,8 @@ crate::internal_macros::define_extension_trait! { /// To use the default Bitcoin Core value, use [`minimal_non_dust`]. /// /// [`minimal_non_dust`]: TxOut::minimal_non_dust - fn minimal_non_dust_custom(script_pubkey: ScriptBuf, dust_relay_fee: FeeRate) -> Self { - TxOut { value: script_pubkey.minimal_non_dust_custom(dust_relay_fee), script_pubkey } + fn minimal_non_dust_custom(script_pubkey: ScriptBuf, dust_relay_fee: FeeRate) -> Option { + Some(TxOut { value: script_pubkey.minimal_non_dust_custom(dust_relay_fee)?, script_pubkey }) } } } diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs index f10cbbb32..b2787d73e 100644 --- a/bitcoin/src/psbt/mod.rs +++ b/bitcoin/src/psbt/mod.rs @@ -2283,7 +2283,7 @@ mod tests { version: transaction::Version::TWO, lock_time: absolute::LockTime::ZERO, 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(); diff --git a/bitcoin/tests/serde.rs b/bitcoin/tests/serde.rs index 807f838a6..dff93b0c5 100644 --- a/bitcoin/tests/serde.rs +++ b/bitcoin/tests/serde.rs @@ -110,7 +110,7 @@ fn serde_regression_txin() { #[test] fn serde_regression_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 want = include_bytes!("data/serde/txout_bincode") as &[_]; assert_eq!(got, want) diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index 7fb8bc535..940b9a5a8 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -39,6 +39,7 @@ pub use self::{ signed::SignedAmount, unsigned::Amount, }; +pub(crate) use self::result::OptionExt; /// A set of denominations in which amounts can be expressed. /// diff --git a/units/src/amount/result.rs b/units/src/amount/result.rs index ae1c6e12d..4b78d0080 100644 --- a/units/src/amount/result.rs +++ b/units/src/amount/result.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -//! Provides a monodic numeric result type that is used to return the result of -//! doing mathematical operations (`core::ops`) on amount types. +//! Provides a monodic type used when mathematical operations (`core::ops`) return an amount type. use core::{fmt, ops}; @@ -378,7 +377,7 @@ impl<'a> core::iter::Sum<&'a NumOpResult> for NumOpResult { +pub(crate) trait OptionExt { fn valid_or_error(self) -> NumOpResult; } diff --git a/units/src/amount/signed.rs b/units/src/amount/signed.rs index 7551ac197..a04795de6 100644 --- a/units/src/amount/signed.rs +++ b/units/src/amount/signed.rs @@ -16,51 +16,74 @@ use super::{ DisplayStyle, OutOfRangeError, ParseAmountError, ParseError, }; -/// A signed amount. -/// -/// The [`SignedAmount`] type can be used to express Bitcoin amounts that support arithmetic and -/// conversion to various denominations. The [`SignedAmount`] type does not implement [`serde`] -/// traits 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 [`SignedAmount`] -/// implements will panic when an overflow occurs. -/// -/// # Examples -/// -/// ``` -/// # #[cfg(feature = "serde")] { -/// use serde::{Serialize, Deserialize}; -/// use bitcoin_units::SignedAmount; -/// -/// #[derive(Serialize, Deserialize)] -/// struct Foo { -/// // If you are using `rust-bitcoin` then `bitcoin::amount::serde::as_sat` also works. -/// #[serde(with = "bitcoin_units::amount::serde::as_sat")] // Also `serde::as_btc`. -/// amount: SignedAmount, -/// } -/// # } -/// ``` -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SignedAmount(i64); +mod encapsulate { + /// A signed amount. + /// + /// The [`SignedAmount`] type can be used to express Bitcoin amounts that support arithmetic and + /// conversion to various denominations. The [`SignedAmount`] type does not implement [`serde`] + /// traits 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 [`SignedAmount`] + /// implements will panic when an overflow occurs. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "serde")] { + /// use serde::{Serialize, Deserialize}; + /// use bitcoin_units::SignedAmount; + /// + /// #[derive(Serialize, Deserialize)] + /// struct Foo { + /// // If you are using `rust-bitcoin` then `bitcoin::amount::serde::as_sat` also works. + /// #[serde(with = "bitcoin_units::amount::serde::as_sat")] // Also `serde::as_btc`. + /// amount: SignedAmount, + /// } + /// # } + /// ``` + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + 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 { /// The zero amount. - pub const ZERO: Self = SignedAmount(0); + pub const ZERO: Self = SignedAmount::from_sat_unchecked(0); /// Exactly one satoshi. - pub const ONE_SAT: Self = SignedAmount(1); + pub const ONE_SAT: Self = SignedAmount::from_sat_unchecked(1); /// 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. - 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. - 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. - 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. pub const MAX: Self = SignedAmount::MAX_MONEY; @@ -73,24 +96,9 @@ impl SignedAmount { /// let amount = SignedAmount::from_sat(-100_000); /// assert_eq!(amount.to_sat(), -100_000); /// ``` - pub const fn from_sat(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 } - - /// 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) } + pub const fn from_sat(satoshi: i64) -> SignedAmount { + SignedAmount::from_sat_unchecked(satoshi) + } /// 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`]. - /// - /// # Errors - /// - /// The function errors if the argument multiplied by the number of sats - /// per bitcoin overflows an `i64` type. - pub fn from_int_btc>(whole_bitcoin: T) -> Result { - 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 }), - } + #[allow(clippy::missing_panics_doc)] + pub fn from_int_btc>(whole_bitcoin: T) -> SignedAmount { + SignedAmount::from_int_btc_const(whole_bitcoin.into()) } /// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`] /// in const context. - /// - /// # Panics - /// - /// The function panics if the argument multiplied by the number of sats - /// 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) { + #[allow(clippy::missing_panics_doc)] + pub const fn from_int_btc_const(whole_bitcoin: i32) -> SignedAmount { + let btc = whole_bitcoin as i64; // Can't call `into` in const context. + match btc.checked_mul(100_000_000) { 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( 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( 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`]. #[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`]. #[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`]. /// @@ -312,19 +310,19 @@ impl SignedAmount { /// - `1` if the amount is positive /// - `-1` if the amount is negative #[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. /// /// Returns `true` if this [`SignedAmount`] is positive and `false` if /// 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. /// /// Returns `true` if this [`SignedAmount`] is negative and `false` if /// 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`]. /// @@ -334,8 +332,8 @@ impl SignedAmount { #[must_use] pub const fn checked_abs(self) -> Option { // No `map()` in const context. - match self.0.checked_abs() { - Some(res) => Some(SignedAmount(res)), + match self.to_sat().checked_abs() { + Some(res) => Some(SignedAmount::from_sat(res)), None => None, } } @@ -346,8 +344,8 @@ impl SignedAmount { #[must_use] pub const fn checked_add(self, rhs: SignedAmount) -> Option { // No `map()` in const context. - match self.0.checked_add(rhs.0) { - Some(res) => SignedAmount(res).check_min_max(), + match self.to_sat().checked_add(rhs.to_sat()) { + Some(res) => SignedAmount::from_sat(res).check_min_max(), None => None, } } @@ -359,8 +357,8 @@ impl SignedAmount { #[must_use] pub const fn checked_sub(self, rhs: SignedAmount) -> Option { // No `map()` in const context. - match self.0.checked_sub(rhs.0) { - Some(res) => SignedAmount(res).check_min_max(), + match self.to_sat().checked_sub(rhs.to_sat()) { + Some(res) => SignedAmount::from_sat(res).check_min_max(), None => None, } } @@ -372,8 +370,8 @@ impl SignedAmount { #[must_use] pub const fn checked_mul(self, rhs: i64) -> Option { // No `map()` in const context. - match self.0.checked_mul(rhs) { - Some(res) => SignedAmount(res).check_min_max(), + match self.to_sat().checked_mul(rhs) { + Some(res) => SignedAmount::from_sat(res).check_min_max(), None => None, } } @@ -386,8 +384,8 @@ impl SignedAmount { #[must_use] pub const fn checked_div(self, rhs: i64) -> Option { // No `map()` in const context. - match self.0.checked_div(rhs) { - Some(res) => Some(SignedAmount(res)), + match self.to_sat().checked_div(rhs) { + Some(res) => Some(SignedAmount::from_sat(res)), None => None, } } @@ -398,34 +396,12 @@ impl SignedAmount { #[must_use] pub const fn checked_rem(self, rhs: i64) -> Option { // No `map()` in const context. - match self.0.checked_rem(rhs) { - Some(res) => Some(SignedAmount(res)), + match self.to_sat().checked_rem(rhs) { + Some(res) => Some(SignedAmount::from_sat(res)), 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. /// /// 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. const fn check_min_max(self) -> Option { - 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 } else { Some(self) @@ -514,6 +490,6 @@ impl From for SignedAmount { impl<'a> Arbitrary<'a> for SignedAmount { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { let s = i64::arbitrary(u)?; - Ok(Self(s)) + Ok(Self::from_sat(s)) } } diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs index d263623eb..2d3b5e1c6 100644 --- a/units/src/amount/tests.rs +++ b/units/src/amount/tests.rs @@ -16,11 +16,11 @@ use super::*; #[cfg(feature = "alloc")] use crate::{FeeRate, Weight}; +fn sat(sat: u64) -> Amount { Amount::from_sat(sat) } +fn ssat(ssat: i64) -> SignedAmount { SignedAmount::from_sat(ssat) } + #[test] fn sanity_check() { - let sat = Amount::from_sat; - let ssat = SignedAmount::from_sat; - assert_eq!(ssat(-100).abs(), ssat(100)); assert_eq!(ssat(i64::MIN + 1).checked_abs().unwrap(), ssat(i64::MAX)); assert_eq!(ssat(-100).signum(), -1); @@ -102,25 +102,22 @@ fn from_str_zero_without_denomination() { #[test] fn from_int_btc() { 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] 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(); - 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); assert_eq!(result, Err(OutOfRangeError { is_signed: false, is_greater_than_max: false })); } #[test] 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_ssat = |sat| NumOpResult::Valid(SignedAmount::from_sat(sat)); @@ -141,17 +138,14 @@ fn neg() { #[cfg(feature = "std")] #[test] fn overflows() { - let result = Amount::MAX + Amount::from_sat_unchecked(1); + let result = Amount::MAX + sat(1); 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()); } #[test] fn add() { - let sat = Amount::from_sat; - let ssat = SignedAmount::from_sat; - assert!(sat(0) + sat(0) == sat(0).into()); assert!(sat(127) + sat(179) == sat(306).into()); @@ -164,9 +158,6 @@ fn add() { #[test] fn sub() { - let sat = Amount::from_sat; - let ssat = SignedAmount::from_sat; - assert!(sat(0) - sat(0) == sat(0).into()); assert!(sat(179) - sat(127) == sat(52).into()); assert!((sat(127) - sat(179)).is_error()); @@ -180,9 +171,6 @@ fn sub() { #[test] 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::MIN.checked_sub(ssat(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))); } -#[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] fn positive_sub() { - let ssat = SignedAmount::from_sat; - 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()); @@ -218,12 +192,12 @@ fn positive_sub() { #[test] fn amount_checked_div_by_weight_ceil() { 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 assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1)); 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 // round up to 864 assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(864)); @@ -236,12 +210,12 @@ fn amount_checked_div_by_weight_ceil() { #[test] fn amount_checked_div_by_weight_floor() { 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 assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1)); 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 // round down to 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")] #[test] 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); // 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 // Test truncation behavior - let amount = Amount::from_sat_unchecked(1000); + let amount = 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(); @@ -287,7 +261,7 @@ fn amount_checked_div_by_fee_rate() { // Test overflow case 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_ceil(tiny_fee_rate).is_none()); } @@ -298,8 +272,6 @@ fn floating_point() { use super::Denomination as D; let f = Amount::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!(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. fn parsing() { 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 sp = SignedAmount::from_str_in; - assert_eq!(p("x", btc), Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 0 }))); 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 })) ); assert_eq!( - sp("-", btc), + sp("-", den_btc), Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })) ); assert_eq!( - p("-1.0x", btc), + p("-1.0x", den_btc), Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 4 })) ); assert_eq!( - p("0.0 ", btc), + p("0.0 ", den_btc), Err(E::from(InvalidCharacterError { invalid_char: ' ', position: 3 })) ); assert_eq!( - p("0.000.000", btc), + p("0.000.000", den_btc), Err(E::from(InvalidCharacterError { invalid_char: '.', position: 5 })) ); #[cfg(feature = "alloc")] let more_than_max = format!("{}", Amount::MAX.to_sat() + 1); #[cfg(feature = "alloc")] - assert_eq!(p(&more_than_max, btc), Err(OutOfRangeError::too_big(false).into())); - assert_eq!(p("0.000000042", btc), Err(TooPreciseError { position: 10 }.into())); - assert_eq!(p("1.0000000", sat), Ok(Amount::from_sat_unchecked(1))); - assert_eq!(p("1.1", sat), Err(TooPreciseError { position: 2 }.into())); - assert_eq!(p("1000.1", sat), Err(TooPreciseError { position: 5 }.into())); - assert_eq!(p("1001.0000000", sat), Ok(Amount::from_sat_unchecked(1001))); - assert_eq!(p("1000.0000001", sat), Err(TooPreciseError { position: 11 }.into())); + assert_eq!(p(&more_than_max, den_btc), Err(OutOfRangeError::too_big(false).into())); + assert_eq!(p("0.000000042", den_btc), Err(TooPreciseError { position: 10 }.into())); + assert_eq!(p("1.0000000", den_sat), Ok(sat(1))); + assert_eq!(p("1.1", den_sat), Err(TooPreciseError { position: 2 }.into())); + assert_eq!(p("1000.1", den_sat), Err(TooPreciseError { position: 5 }.into())); + assert_eq!(p("1001.0000000", den_sat), Ok(sat(1001))); + 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!(sp("-.5", btc), Ok(SignedAmount::from_sat_unchecked(-500_000_00))); + assert_eq!(p("1", den_btc), Ok(sat(1_000_000_00))); + assert_eq!(sp("-.5", den_btc), Ok(ssat(-500_000_00))); #[cfg(feature = "alloc")] - assert_eq!(sp(&SignedAmount::MIN.to_sat().to_string(), sat), Ok(SignedAmount::MIN)); - assert_eq!(p("1.1", btc), Ok(Amount::from_sat_unchecked(1_100_000_00))); - assert_eq!(p("100", sat), Ok(Amount::from_sat_unchecked(100))); - assert_eq!(p("55", sat), Ok(Amount::from_sat_unchecked(55))); - assert_eq!(p("2100000000000000", sat), Ok(Amount::from_sat_unchecked(21_000_000__000_000_00))); - assert_eq!(p("2100000000000000.", sat), Ok(Amount::from_sat_unchecked(21_000_000__000_000_00))); - assert_eq!(p("21000000", btc), Ok(Amount::from_sat_unchecked(21_000_000__000_000_00))); + assert_eq!(sp(&SignedAmount::MIN.to_sat().to_string(), den_sat), Ok(SignedAmount::MIN)); + assert_eq!(p("1.1", den_btc), Ok(sat(1_100_000_00))); + assert_eq!(p("100", den_sat), Ok(sat(100))); + assert_eq!(p("55", den_sat), Ok(sat(55))); + assert_eq!( + p("2100000000000000", den_sat), + 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. assert_eq!( @@ -408,15 +390,12 @@ fn to_string() { 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_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!(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!( - SignedAmount::from_sat_unchecked(-42).to_string_with_denomination(D::Bitcoin), - "-0.00000042 BTC" - ); + assert_eq!(ssat(-42).to_string_with_denomination(D::Bitcoin), "-0.00000042 BTC"); } // May help identify a problem sooner @@ -593,8 +572,6 @@ check_format_non_negative_show_denom! { #[test] fn unsigned_signed_conversion() { - let ssat = SignedAmount::from_sat; - let sat = Amount::from_sat; let max_sats: u64 = Amount::MAX.to_sat(); 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("18446744073709551616 sat", Err(OutOfRangeError::too_big(false))); - ok_case(".5 bits", Amount::from_sat_unchecked(50)); - ok_scase("-.5 bits", SignedAmount::from_sat_unchecked(-50)); - ok_case("0.00253583 BTC", Amount::from_sat_unchecked(253_583)); - ok_scase("-5 satoshi", SignedAmount::from_sat_unchecked(-5)); - ok_case("0.10000000 BTC", Amount::from_sat_unchecked(100_000_00)); - ok_scase("-100 bits", SignedAmount::from_sat_unchecked(-10_000)); + ok_case(".5 bits", sat(50)); + ok_scase("-.5 bits", ssat(-50)); + ok_case("0.00253583 BTC", sat(253_583)); + ok_scase("-5 satoshi", ssat(-5)); + ok_case("0.10000000 BTC", sat(100_000_00)); + ok_scase("-100 bits", ssat(-10_000)); ok_case("21000000 BTC", Amount::MAX); ok_scase("21000000 BTC", SignedAmount::MAX); 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(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!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::CentiBitcoin), D::CentiBitcoin) - .is_ok()); - assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::MilliBitcoin), D::MilliBitcoin) - .is_ok()); - assert!(ua_str(&ua_sat(Amount::MAX.to_sat()).to_string_in(D::MicroBitcoin), D::MicroBitcoin) - .is_ok()); + assert!( + 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()); + assert!(ua_str( + &ua_sat(Amount::MAX.to_sat()).to_string_in(D::MilliBitcoin), + D::MilliBitcoin + ) + .is_ok()); + assert!(ua_str( + &ua_sat(Amount::MAX.to_sat()).to_string_in(D::MicroBitcoin), + D::MicroBitcoin + ) + .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!( 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; - let amt = Amount::from_sat_unchecked(42); + let amt = sat(42); let denom = Amount::to_string_with_denomination; assert_eq!(denom(amt, D::Bitcoin).parse::(), Ok(amt)); assert_eq!(denom(amt, D::CentiBitcoin).parse::(), Ok(amt)); @@ -798,10 +788,7 @@ fn serde_as_sat() { } serde_test::assert_tokens( - &T { - amt: Amount::from_sat_unchecked(123_456_789), - samt: SignedAmount::from_sat_unchecked(-123_456_789), - }, + &T { amt: sat(123_456_789), samt: ssat(-123_456_789) }, &[ serde_test::Token::Struct { name: "T", len: 2 }, serde_test::Token::Str("amt"), @@ -828,10 +815,7 @@ fn serde_as_btc() { pub samt: SignedAmount, } - let orig = T { - amt: Amount::from_sat_unchecked(20_000_000__000_000_01), - samt: SignedAmount::from_sat_unchecked(-20_000_000__000_000_01), - }; + let orig = T { amt: sat(20_000_000__000_000_01), samt: ssat(-20_000_000__000_000_01) }; let json = "{\"amt\": 20000000.00000001, \ \"samt\": -20000000.00000001}"; @@ -865,10 +849,7 @@ fn serde_as_str() { } serde_test::assert_tokens( - &T { - amt: Amount::from_sat_unchecked(123_456_789), - samt: SignedAmount::from_sat_unchecked(-123_456_789), - }, + &T { amt: sat(123_456_789), samt: ssat(-123_456_789) }, &[ serde_test::Token::Struct { name: "T", len: 2 }, serde_test::Token::String("amt"), @@ -895,10 +876,7 @@ fn serde_as_btc_opt() { pub samt: Option, } - let with = T { - amt: Some(Amount::from_sat_unchecked(2_500_000_00)), - samt: Some(SignedAmount::from_sat_unchecked(-2_500_000_00)), - }; + let with = T { amt: Some(sat(2_500_000_00)), samt: Some(ssat(-2_500_000_00)) }; let without = T { amt: None, samt: None }; // Test Roundtripping @@ -937,10 +915,7 @@ fn serde_as_sat_opt() { pub samt: Option, } - let with = T { - amt: Some(Amount::from_sat_unchecked(2_500_000_00)), - samt: Some(SignedAmount::from_sat_unchecked(-2_500_000_00)), - }; + let with = T { amt: Some(sat(2_500_000_00)), samt: Some(ssat(-2_500_000_00)) }; let without = T { amt: None, samt: None }; // Test Roundtripping @@ -979,10 +954,7 @@ fn serde_as_str_opt() { pub samt: Option, } - let with = T { - amt: Some(Amount::from_sat_unchecked(123_456_789)), - samt: Some(SignedAmount::from_sat_unchecked(-123_456_789)), - }; + let with = T { amt: Some(sat(123_456_789)), samt: Some(ssat(-123_456_789)) }; let without = T { amt: None, samt: None }; // Test Roundtripping @@ -1009,9 +981,6 @@ fn serde_as_str_opt() { #[test] fn sum_amounts() { - let sat = Amount::from_sat; - let ssat = SignedAmount::from_sat; - assert_eq!([].iter().sum::>(), Amount::ZERO.into()); assert_eq!([].iter().sum::>(), SignedAmount::ZERO.into()); @@ -1044,9 +1013,6 @@ fn sum_amounts() { #[test] 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(SignedAmount::ZERO)); @@ -1112,8 +1078,6 @@ fn disallow_unknown_denomination() { #[test] #[cfg(feature = "alloc")] fn trailing_zeros_for_amount() { - let sat = Amount::from_sat; - assert_eq!(format!("{}", sat(1_000_000)), "0.01 BTC"); assert_eq!(format!("{}", Amount::ONE_SAT), "0.00000001 BTC"); assert_eq!(format!("{}", Amount::ONE_BTC), "1 BTC"); @@ -1194,8 +1158,6 @@ fn add_sub_combos() { #[test] fn unsigned_addition() { - let sat = Amount::from_sat; - assert_eq!(sat(0) + sat(0), NumOpResult::from(sat(0))); assert_eq!(sat(0) + sat(307), NumOpResult::from(sat(307))); assert_eq!(sat(307) + sat(0), NumOpResult::from(sat(307))); @@ -1205,8 +1167,6 @@ fn unsigned_addition() { #[test] fn signed_addition() { - let ssat = SignedAmount::from_sat; - assert_eq!(ssat(0) + ssat(0), NumOpResult::from(ssat(0))); assert_eq!(ssat(0) + ssat(307), NumOpResult::from(ssat(307))); assert_eq!(ssat(307) + ssat(0), NumOpResult::from(ssat(307))); @@ -1226,8 +1186,6 @@ fn signed_addition() { #[test] fn unsigned_subtraction() { - let sat = Amount::from_sat; - assert_eq!(sat(0) - sat(0), NumOpResult::from(sat(0))); assert_eq!(sat(307) - sat(0), NumOpResult::from(sat(307))); assert_eq!(sat(461) - sat(307), NumOpResult::from(sat(154))); @@ -1235,8 +1193,6 @@ fn unsigned_subtraction() { #[test] fn signed_subtraction() { - let ssat = SignedAmount::from_sat; - assert_eq!(ssat(0) - ssat(0), NumOpResult::from(ssat(0))); assert_eq!(ssat(0) - ssat(307), NumOpResult::from(ssat(-307))); assert_eq!(ssat(307) - ssat(0), NumOpResult::from(ssat(307))); @@ -1252,11 +1208,8 @@ fn signed_subtraction() { #[test] fn op_int_combos() { - let sat = Amount::from_sat; - let ssat = SignedAmount::from_sat; - - let res = |sat| NumOpResult::from(Amount::from_sat(sat)); - let sres = |ssat| NumOpResult::from(SignedAmount::from_sat(ssat)); + let res = |n_sat| NumOpResult::from(sat(n_sat)); + let sres = |n_ssat| NumOpResult::from(ssat(n_ssat)); assert_eq!(sat(23) * 31, res(713)); assert_eq!(ssat(23) * 31, sres(713)); @@ -1295,8 +1248,6 @@ fn op_int_combos() { #[test] fn unsigned_amount_div_by_amount() { - let sat = Amount::from_sat; - assert_eq!(sat(0) / sat(7), 0); assert_eq!(sat(1897) / sat(7), 271); } @@ -1309,8 +1260,6 @@ fn unsigned_amount_div_by_amount_zero() { #[test] fn signed_amount_div_by_amount() { - let ssat = SignedAmount::from_sat; - assert_eq!(ssat(0) / ssat(7), 0); 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!(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::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. diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs index 89d7eb35f..c3488dfaf 100644 --- a/units/src/amount/unsigned.rs +++ b/units/src/amount/unsigned.rs @@ -16,41 +16,70 @@ use super::{ OutOfRangeError, ParseAmountError, ParseError, SignedAmount, }; -/// An amount. -/// -/// 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 -/// but we do provide modules for serializing as satoshis or bitcoin. -/// -/// # Examples -/// -/// ``` -/// # #[cfg(feature = "serde")] { -/// use serde::{Serialize, Deserialize}; -/// use bitcoin_units::Amount; -/// -/// #[derive(Serialize, Deserialize)] -/// struct Foo { -/// // If you are using `rust-bitcoin` then `bitcoin::amount::serde::as_sat` also works. -/// #[serde(with = "bitcoin_units::amount::serde::as_sat")] // Also `serde::as_btc`. -/// amount: Amount, -/// } -/// # } -/// ``` -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Amount(u64); +mod encapsulate { + /// An amount. + /// + /// 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 + /// 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 + /// + /// ``` + /// # #[cfg(feature = "serde")] { + /// use serde::{Serialize, Deserialize}; + /// use bitcoin_units::Amount; + /// + /// #[derive(Serialize, Deserialize)] + /// struct Foo { + /// // If you are using `rust-bitcoin` then `bitcoin::amount::serde::as_sat` also works. + /// #[serde(with = "bitcoin_units::amount::serde::as_sat")] // Also `serde::as_btc`. + /// amount: Amount, + /// } + /// # } + /// ``` + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + 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 { /// The zero amount. - pub const ZERO: Self = Amount(0); + pub const ZERO: Self = Amount::from_sat_unchecked(0); /// Exactly one satoshi. - pub const ONE_SAT: Self = Amount(1); + pub const ONE_SAT: Self = Amount::from_sat_unchecked(1); /// 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. - 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. - 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. pub const MIN: Self = Amount::ZERO; /// The maximum value of an amount. @@ -67,22 +96,7 @@ impl Amount { /// let amount = Amount::from_sat(100_000); /// assert_eq!(amount.to_sat(), 100_000); /// ``` - pub const fn from_sat(satoshi: u64) -> Amount { Amount(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) } + pub const fn from_sat(satoshi: u64) -> Amount { Amount::from_sat_unchecked(satoshi) } /// 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`]. - /// - /// # Errors - /// - /// The function errors if the argument multiplied by the number of sats - /// per bitcoin overflows a `u64` type. - pub fn from_int_btc>(whole_bitcoin: T) -> Result { - 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 }), - } + #[allow(clippy::missing_panics_doc)] + pub fn from_int_btc>(whole_bitcoin: T) -> Amount { + Amount::from_int_btc_const(whole_bitcoin.into()) } /// Converts from a value expressing a whole number of bitcoin to an [`Amount`] /// in const context. - /// - /// # Panics - /// - /// The function panics if the argument multiplied by the number of sats - /// per bitcoin overflows a `u64` type. + #[allow(clippy::missing_panics_doc)] 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) { 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(), ))); } - if sats > Self::MAX.0 { + if sats > Self::MAX.to_sat() { return Err(ParseAmountError(ParseAmountErrorInner::OutOfRange( OutOfRangeError::too_big(false), ))); @@ -299,8 +302,8 @@ impl Amount { #[must_use] pub const fn checked_add(self, rhs: Amount) -> Option { // No `map()` in const context. - match self.0.checked_add(rhs.0) { - Some(res) => Amount(res).check_max(), + match self.to_sat().checked_add(rhs.to_sat()) { + Some(res) => Amount::from_sat(res).check_max(), None => None, } } @@ -311,8 +314,8 @@ impl Amount { #[must_use] pub const fn checked_sub(self, rhs: Amount) -> Option { // No `map()` in const context. - match self.0.checked_sub(rhs.0) { - Some(res) => Some(Amount(res)), + match self.to_sat().checked_sub(rhs.to_sat()) { + Some(res) => Some(Amount::from_sat(res)), None => None, } } @@ -323,8 +326,8 @@ impl Amount { #[must_use] pub const fn checked_mul(self, rhs: u64) -> Option { // No `map()` in const context. - match self.0.checked_mul(rhs) { - Some(res) => Amount(res).check_max(), + match self.to_sat().checked_mul(rhs) { + Some(res) => Amount::from_sat(res).check_max(), None => None, } } @@ -337,8 +340,8 @@ impl Amount { #[must_use] pub const fn checked_div(self, rhs: u64) -> Option { // No `map()` in const context. - match self.0.checked_div(rhs) { - Some(res) => Some(Amount(res)), + match self.to_sat().checked_div(rhs) { + Some(res) => Some(Amount::from_sat(res)), None => None, } } @@ -349,34 +352,12 @@ impl Amount { #[must_use] pub const fn checked_rem(self, rhs: u64) -> Option { // No `map()` in const context. - match self.0.checked_rem(rhs) { - Some(res) => Some(Amount(res)), + match self.to_sat().checked_rem(rhs) { + Some(res) => Some(Amount::from_sat(res)), 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. #[rustfmt::skip] // Moves code comments to the wrong line. 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. const fn check_max(self) -> Option { - if self.0 > Self::MAX.0 { + if self.to_sat() > Self::MAX.to_sat() { None } else { Some(self) @@ -445,6 +426,6 @@ impl TryFrom for Amount { impl<'a> Arbitrary<'a> for Amount { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { let a = u64::arbitrary(u)?; - Ok(Self(a)) + Ok(Self::from_sat(a)) } } diff --git a/units/src/fee.rs b/units/src/fee.rs index 1bb3b9b66..39dff63b0 100644 --- a/units/src/fee.rs +++ b/units/src/fee.rs @@ -13,6 +13,7 @@ use core::ops; +use crate::amount::{NumOpResult, OptionExt}; use crate::{Amount, FeeRate, Weight}; impl Amount { @@ -160,17 +161,15 @@ impl FeeRate { /// Computes the ceiling so that the fee computation is conservative. impl ops::Mul for Weight { - type Output = Amount; + type Output = NumOpResult; - fn mul(self, rhs: FeeRate) -> Self::Output { - Amount::from_sat((rhs.to_sat_per_kwu() * self.to_wu() + 999) / 1000) - } + fn mul(self, rhs: FeeRate) -> Self::Output { rhs.checked_mul_by_weight(self).valid_or_error() } } impl ops::Mul for FeeRate { - type Output = Amount; + type Output = NumOpResult; - 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 for Amount { @@ -265,7 +264,7 @@ mod tests { let three = Weight::from_vb(3).unwrap(); let six = Amount::from_sat_unchecked(6); - assert_eq!(two * three, six); + assert_eq!(two * three, six.into()); } #[test] diff --git a/units/tests/str.rs b/units/tests/str.rs index 89068bf9b..0b365af20 100644 --- a/units/tests/str.rs +++ b/units/tests/str.rs @@ -22,10 +22,10 @@ macro_rules! check { check! { 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_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_max, BlockHeight, BlockHeight::MAX, "4294967295";