Repository: rust-bitcoin/rust-bitcoin
Pull request #4164: Prepare to enforce MAX_MONEY invariant
Author: login: tcharding
Labels: name: API break
description: This PR requires a version bump for the next release
name: P-high
description: High priority
name: C-bitcoin
description: PRs modifying the bitcoin crate
name: C-units
description: PRs modifying the units crate
name: test
Created: 2025-03-02T23:54:25Z | Merged: 2025-03-12T12:14:22Z

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.


Diff:
diff --git a/bitcoin/src/blockdata/script/borrowed.rs b/bitcoin/src/blockdata/script/borrowed.rs
index 624feb3b54..4ae5c9ce1d 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<Amount> { 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<Amount> {
             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<Amount> {
             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<Amount> {
             // 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 9b47ef6528..4443645b1b 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 4510c465d3..0c60d3c654 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<TxOut> {
+            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<TxOut> {
+            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 f10cbbb329..b2787d73e9 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 807f838a6e..dff93b0c50 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 7fb8bc5356..940b9a5a80 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 ae1c6e12d1..4b78d00800 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<SignedAmount>> for NumOpResult<SignedAm
     }
 }
 
-pub(in crate::amount) trait OptionExt<T> {
+pub(crate) trait OptionExt<T> {
     fn valid_or_error(self) -> NumOpResult<T>;
 }
 
diff --git a/units/src/amount/signed.rs b/units/src/amount/signed.rs
index 7551ac197a..a04795de6e 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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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<SignedAmount> {
         // 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<SignedAmount> {
         // 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<SignedAmount> {
         // 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<SignedAmount> {
         // 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<SignedAmount> {
         // 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<SignedAmount> {
         // 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<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
         } else {
             Some(self)
@@ -514,6 +490,6 @@ impl From<Amount> for SignedAmount {
 impl<'a> Arbitrary<'a> for SignedAmount {
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
         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 d263623eb9..2d3b5e1c62 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("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(&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", 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::<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(
-        &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<SignedAmount>,
     }
 
-    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<SignedAmount>,
     }
 
-    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<SignedAmount>,
     }
 
-    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::<NumOpResult<Amount>>(), Amount::ZERO.into());
     assert_eq!([].iter().sum::<NumOpResult<SignedAmount>>(), 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 89d7eb35ff..c3488dfafa 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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<u32>>(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<Amount> {
         // 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<Amount> {
         // 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<Amount> {
         // 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<Amount> {
         // 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<Amount> {
         // 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<Amount> {
-        if self.0 > Self::MAX.0 {
+        if self.to_sat() > Self::MAX.to_sat() {
             None
         } else {
             Some(self)
@@ -445,6 +426,6 @@ impl TryFrom<SignedAmount> for Amount {
 impl<'a> Arbitrary<'a> for Amount {
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
         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 1bb3b9b665..39dff63b07 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<FeeRate> for Weight {
-    type Output = Amount;
+    type Output = NumOpResult<Amount>;
 
-    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<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 {
@@ -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 89068bf9b3..0b365af201 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";


Reviews:
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T16:40:49Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T20:19:47Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T23:10:00Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T13:15:36Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T13:40:20Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:04:57Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:07:03Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:09:19Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:16:01Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:17:43Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:17:50Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:21:16Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:22:07Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T18:49:02Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T20:08:21Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T20:10:21Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T20:12:20Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T20:13:11Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T20:14:33Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T20:16:23Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T20:16:57Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T21:39:54Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T21:45:33Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T21:46:07Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T21:47:27Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T21:54:28Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T21:59:34Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T20:38:13Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T21:46:37Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T21:59:07Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T23:15:05Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T23:20:15Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T23:52:24Z
user: login: apoelstra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T23:55:40Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T00:19:09Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T01:05:39Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T09:19:31Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T09:43:48Z
user: login: jamillambert
body: I've had a look through everything and didn't see any problems.  I noted a couple of nits.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:55:06Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T10:02:42Z
user: login: jamillambert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:25:30Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T20:18:15Z
user: login: tcharding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T20:20:02Z
user: login: apoelstra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T14:43:38Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T15:27:28Z
user: login: jamillambert
body: ACK 26d79a4646940467ded5a7fc766cae77db819d95
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T22:50:32Z
user: login: apoelstra
body: ACK 26d79a4646940467ded5a7fc766cae77db819d95; successfully ran local tests
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-09T06:17:36Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:47:28Z
user: login: jamillambert
body: ACK 5d851f1c3e98d7d426e5897b2d734b77a299ccfb
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T19:07:24Z
user: login: apoelstra
body: ACK 26d79a4646940467ded5a7fc766cae77db819d95; successfully ran local tests
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-11T09:50:58Z
user: login: apoelstra
body: ACK 5d851f1c3e98d7d426e5897b2d734b77a299ccfb; successfully ran local tests
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-12T05:09:04Z

Review comments:
diff_hunk: @@ -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 overflow or underflow 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 overflow or underflow 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 overflow or underflow 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 overflow or underflow 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_MONEY`].
+        pub const fn from_sat_unchecked(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) }
path: units/src/amount/signed.rs
user: login: yancyribbens
body: I'm starting to think `from_sats_unchecked` should just be removed once `unwrap()` can be used in const context.
created_at: 2025-03-03T16:40:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,17 +161,15 @@ impl FeeRate {
 
 /// Computes the ceiling so that the fee computation is conservative.
 impl ops::Mul<FeeRate> for Weight {
-    type Output = Amount;
+    type Output = NumOpResult<Amount>;
path: units/src/fee.rs
user: login: tcharding
body: Note to self: this change makes the docs in `result` stale. After #4179 goes in include a change that updates the docs since `NumOpResult` is now used in ops impls that return an `Amount` not just in ops _on_ amounts. 
created_at: 2025-03-03T20:19:43Z
author_association: MEMBER
diff_hunk: @@ -160,17 +161,15 @@ impl FeeRate {
 
 /// Computes the ceiling so that the fee computation is conservative.
 impl ops::Mul<FeeRate> for Weight {
-    type Output = Amount;
+    type Output = NumOpResult<Amount>;
path: units/src/fee.rs
user: login: tcharding
body: Since I had to rebase to fix the review suggestions I just grabbed the new `result` rustdocs from https://github.com/rust-bitcoin/rust-bitcoin/pull/4179#issuecomment-2695745410 and rolled it into the `fee` change in 89751fc12
created_at: 2025-03-03T23:09:59Z
author_association: MEMBER
diff_hunk: @@ -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 overflow or underflow 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 overflow or underflow 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 overflow or underflow 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 overflow or underflow 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_MONEY`].
+        pub const fn from_sat_unchecked(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) }
path: units/src/amount/signed.rs
user: login: Kixunil
body: Yes, we've agreed elsewhere on removing it. If anyone wants to keep it I want it to be `unsafe`.
created_at: 2025-03-04T13:15:36Z
author_association: COLLABORATOR
diff_hunk: @@ -96,17 +96,22 @@ fn from_str_zero_without_denomination() {
 
 #[test]
 fn from_int_btc() {
+    let sat = Amount::from_sat;
path: units/src/amount/tests.rs
user: login: Kixunil
body: Good idea, we can later just change this to `|x| Amount::from_sat(x).unwrap`.
But it's repeated in multiple tests so why not just go all the way and define the function only once for the whole module? We can then just change one definition and it'll work fine.
created_at: 2025-03-04T13:19:04Z
author_association: COLLABORATOR
diff_hunk: @@ -50,17 +50,17 @@ pub struct SignedAmount(i64);
 
 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);
path: units/src/amount/signed.rs
user: login: Kixunil
body: Since we've agreed to remove the `unchecked` constructor we shouldn't use it here.
created_at: 2025-03-04T13:22:33Z
author_association: COLLABORATOR
diff_hunk: @@ -16,37 +16,60 @@ 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 overflow or underflow 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 overflow or underflow 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 {
path: units/src/amount/signed.rs
user: login: Kixunil
body: Good idea.
created_at: 2025-03-04T13:23:48Z
author_association: COLLABORATOR
diff_hunk: @@ -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 overflow or underflow 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 overflow or underflow 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 overflow or underflow 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 overflow or underflow 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_MONEY`].
+        pub const fn from_sat_unchecked(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) }
path: units/src/amount/signed.rs
user: login: Kixunil
body: Or at least make the constructor private. I'm fine with having the checked function in `encapsulated` module.
created_at: 2025-03-04T13:24:26Z
author_association: COLLABORATOR
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: Kixunil
body: `::MAX` looks a lot like `u64::MAX` and for Bitcoin devs an amount is intrinsically a 64 byte unsigned integer.

Maybe, but not for Rust devs, where `MAX` represents maximum value a *type* can hold. I find it an argument **for** `MAX`.

The compiler will prevent any mixups. But we should deprecate anything in older versions that unconditionally produces amounts > 21M BTC.
created_at: 2025-03-04T13:32:53Z
author_association: COLLABORATOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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 {
path: units/src/amount/signed.rs
user: login: Kixunil
body: FTR, this bad name got in and IIRC I was complaining about it.
created_at: 2025-03-04T13:34:58Z
author_association: COLLABORATOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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"),
         }
path: units/src/amount/signed.rs
user: login: Kixunil
body: Since it never overflows, might as well use `*`. But it'll still panic later.
created_at: 2025-03-04T13:36:45Z
author_association: COLLABORATOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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)]
path: units/src/amount/signed.rs
user: login: Kixunil
body: You can probably avoid this by using `*`.
created_at: 2025-03-04T13:37:24Z
author_association: COLLABORATOR
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: Kixunil
body: Didn't we agree that these are ridiculously unlikely and not worth it? I can't remember.
created_at: 2025-03-04T13:38:49Z
author_association: COLLABORATOR
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: yancyribbens
body: I accidentally commented on the followup PR, although I agree that `MAX` is better than `MAX_MONEY` here.

> The compiler will prevent any mixups. But we should deprecate anything in older versions that unconditionally produces amounts > 21M BTC.

Are you suggesting deprecating `parse_signed_to_satoshi`?  It would be a good idea to look into modifying the function so it doesn't produce values greater than 21 BTC.   Although, it's private so no need to deprecate and also that would slow down this PR so better to look into it later prob.
created_at: 2025-03-04T15:04:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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 {
path: units/src/amount/signed.rs
user: login: yancyribbens
body: not to pile on with critiques, although I don't know why `whole_bitcoin`.  But that doesn't pertain to this PR.
created_at: 2025-03-04T15:07:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: Kixunil
body: That one doesn't produce high amounts **unconditionally**.
created_at: 2025-03-04T15:09:18Z
author_association: COLLABORATOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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"),
         }
path: units/src/amount/signed.rs
user: login: yancyribbens
body: > Since it never overflows,

It can over flow.  21 million * 100000000 is greater than i32::MAX.  It would _not_overflow had this been left as `i64`.
created_at: 2025-03-04T15:16:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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"),
         }
path: units/src/amount/signed.rs
user: login: Kixunil
body: ? It's literally converted at the beginning.
created_at: 2025-03-04T15:17:43Z
author_association: COLLABORATOR
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: yancyribbens
body: > Didn't we agree that these are ridiculously unlikely and not worth it

Since it's already here, I don't see a downside to being cautious here.
created_at: 2025-03-04T15:17:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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"),
         }
path: units/src/amount/signed.rs
user: login: yancyribbens
body: Oh I see.  Sorry I was looking at the function as it exists on master before this change.
created_at: 2025-03-04T15:21:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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"),
         }
path: units/src/amount/signed.rs
user: login: yancyribbens
body: Yeah, good call that this won't overflow so the `checked_mul` isn't needed.
created_at: 2025-03-04T15:22:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: Kixunil
body: It can only happen if something is attempting to produce a script that is 175000000 times longer than the block size. To my knowledge, everyone just uses one of the standard block templates, so it's valid by construction.
created_at: 2025-03-04T18:49:02Z
author_association: COLLABORATOR
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: tcharding
body: Doing it like this makes the next change (making `from_sat` fallible) way easier. Last time I did not do that and it turned out too messy. So this is way cleaner. We can always re-visit if we don't want the `Option` and prefer to panic.
created_at: 2025-03-04T20:08:21Z
author_association: MEMBER
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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 {
path: units/src/amount/signed.rs
user: login: tcharding
body: We likely all need to go over the `amount` module again with a fine tooth comb and a bunch of issues will fall out. Can we leave rename till then?
created_at: 2025-03-04T20:10:21Z
author_association: MEMBER
diff_hunk: @@ -50,17 +50,17 @@ pub struct SignedAmount(i64);
 
 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);
path: units/src/amount/signed.rs
user: login: tcharding
body: I was trying to leave the `_unchecked` discussion out of scope for this PR because I didn't follow the conversation closely. Also since we cannot use a fallible constructor in const AFAIK then it seems reasonable ATM to use the `_unchecked` constructor. Is this too much code churn?
created_at: 2025-03-04T20:12:20Z
author_association: MEMBER
diff_hunk: @@ -96,17 +96,22 @@ fn from_str_zero_without_denomination() {
 
 #[test]
 fn from_int_btc() {
+    let sat = Amount::from_sat;
path: units/src/amount/tests.rs
user: login: tcharding
body: No worries, I can do that.
created_at: 2025-03-04T20:13:11Z
author_association: MEMBER
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: tcharding
body: So 2 votes for using `MAX` and not `MAX_MONEY`. @apoelstra do you want to weigh in, if not I'll change it to that.
created_at: 2025-03-04T20:14:32Z
author_association: MEMBER
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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"),
         }
path: units/src/amount/signed.rs
user: login: tcharding
body: > Since it never overflows, might as well use *. But it'll still panic later.

I swear last time you said "might as well use checked arithmetic to make it explicit that it can't overflow" - either way its super easy to later change in a smaller PR - can we leave it as is?
created_at: 2025-03-04T20:16:23Z
author_association: MEMBER
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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"),
         }
path: units/src/amount/signed.rs
user: login: tcharding
body: I myself have done backflips like that in subsequent reviews so no hard feelings!
created_at: 2025-03-04T20:16:56Z
author_association: MEMBER
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: Kixunil
body: How does it make it *way* easier if you can simply use `from_sat(x).unwrap()`?

Anyway, don't take this as blocking, just curiosity.
created_at: 2025-03-04T21:39:54Z
author_association: COLLABORATOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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"),
         }
path: units/src/amount/signed.rs
user: login: Kixunil
body: > I swear last time you said "might as well use checked arithmetic to make it explicit that it can't overflow"

I think we're talking apples and oranges. I'm certain I definitely didn't say such thing about arithmetic operators. I think what you have in mind is my suggestion to use `Amount::from_sat(42).unwrap()` in tests rather than `unwrap_unchecked` but in this case we're not handling the value returned from `from_sat` but the value that goes into it.

But yes, we can keep it.
created_at: 2025-03-04T21:45:33Z
author_association: COLLABORATOR
diff_hunk: @@ -121,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<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 }),
-        }
+    #[allow(clippy::missing_panics_doc)]
+    pub fn from_int_btc<T: Into<i32>>(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 {
path: units/src/amount/signed.rs
user: login: Kixunil
body: Sure, this was just a side note.
created_at: 2025-03-04T21:46:07Z
author_association: COLLABORATOR
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: Kixunil
body: BTW in any case I think we'll need to deprecate `Amount::MAX` in the old version saying it's creating absurdly large amounts and its semantics will change.
created_at: 2025-03-04T21:47:27Z
author_association: COLLABORATOR
diff_hunk: @@ -50,17 +50,17 @@ pub struct SignedAmount(i64);
 
 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);
path: units/src/amount/signed.rs
user: login: Kixunil
body: The result of that discussion was to remove the function. You don't have to make the change right now but adding more calls to it if we know we will change them for sure just adds noise for no reason. We're better off just dropping the commit that adds them, because those lines will change in any case.

> we cannot use a fallible constructor in const AFAIK

We can for a while, our MSRV allowed that like year ago if not more. But also we don't have any fallible constructor in this commit so this is non-issue.

> Is this too much code churn?

Yes, because that churn has literally zero value. So churn-to-value ration approaches infinity.
created_at: 2025-03-04T21:54:27Z
author_association: COLLABORATOR
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: tcharding
body: Its easier because one doesn't have to reason about what values cause a panic - which I did last time and still got wrong.

> Anyway, don't take this as blocking, just curiosity.

Cool, thanks.
created_at: 2025-03-04T21:59:33Z
author_association: MEMBER
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: Kixunil
body: Oh, I see. OK.
created_at: 2025-03-05T20:38:12Z
author_association: COLLABORATOR
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: tcharding
body: Only use `MAX` and at the same time deprecate `MAX` - you've lost me?
created_at: 2025-03-05T21:46:37Z
author_association: MEMBER
diff_hunk: @@ -50,17 +50,17 @@ pub struct SignedAmount(i64);
 
 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);
path: units/src/amount/signed.rs
user: login: tcharding
body: > But also we don't have any fallible constructor in this commit so this is non-issue.

Come on now bro, this whole PR is preparation for enforcing the MAX_MONEY invariant - you are having a lend of me.

Just to re-cap. The `amount` module is currently borked. Normal patching procedure does not apply because we are not trying to maintain the non-existent broken-ness, nor are we trying to maintain a releaseable state.

The new sanity policy is to have one public constructor that accesses the inner field. So if we are leaving the `_unchecked` issue out of scope for this PR it makes logical sense to use `from_sat_unchecked` as the one public function.

Furthermore I don't know what you mean about using a fallible constructor in const for associated consts, there is no `unwrap` in const and we can't touch the inner field directly?

It is extremely difficult to go from the current broken state to the state we want to release without doing many many changes. I've done my best to make the changes at least discreet and kind of sane. I expected there to be a shit fight to get this in but 'don't change that line if we are going to change it again later' is just a pain in the arse. And I've been breaking it up and re putting it together for three months. Can we just merge something and iterate please.  
created_at: 2025-03-05T21:59:07Z
author_association: MEMBER
diff_hunk: @@ -50,17 +50,17 @@ pub struct SignedAmount(i64);
 
 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);
path: units/src/amount/signed.rs
user: login: yancyribbens
body: > Furthermore I don't know what you mean about using a fallible constructor in const for associated consts, there is no unwrap in const and we can't touch the inner field directly?

Well, you can still workaround the missing `unwrap`, right?   For example, consts can be written as:
```
    pub const ZERO: Self = match SignedAmount::from_sat(0) {                                                                                                               │   |
        Ok(s) => SignedAmount::ZERO,                                                                                                                                       │75 |     pub const ZERO: Self = match SignedAmount::from_sat(0) {
        Err(e) => panic!("oops")                                                                                                                         │   |     ^^^^^^^^^^^^^^^^^^^^
    };
```

Would be nice if we could just bump to 1.83 and avoid that since it's an ugly hack.  I don't think we should add it though and then pull it later because that's kind of annoying to deprecate for downstream imo.



created_at: 2025-03-05T23:15:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: yancyribbens
body: > Only use MAX and at the same time deprecate MAX - you've lost me?

I think he's pointing out that if you're using Amount::MAX for values greater than MAX_MONEY now, and the the API changes to limit Amount::MAX to MAX_MONEY that could be a surprise (since this const is public).
created_at: 2025-03-05T23:20:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,17 +50,17 @@ pub struct SignedAmount(i64);
 
 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);
path: units/src/amount/signed.rs
user: login: tcharding
body: Correct but that is way too ugly for associated consts IMO for very little additional value.
created_at: 2025-03-05T23:52:24Z
author_association: MEMBER
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: apoelstra
body: Yeah `MAX` is fine.
created_at: 2025-03-05T23:55:39Z
author_association: MEMBER
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: tcharding
body: Now uses MAX everywhere and references this thread in the commit log to justify the change.
created_at: 2025-03-06T00:19:09Z
author_association: MEMBER
diff_hunk: @@ -50,17 +50,17 @@ pub struct SignedAmount(i64);
 
 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);
path: units/src/amount/signed.rs
user: login: yancyribbens
body: > Correct but that is way too ugly for associated consts IMO for very little additional value.

We already have a lot of this going on with the understanding that it's temporary until things are available in MSRV
created_at: 2025-03-06T01:05:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,52 +16,73 @@ 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.
-///
-/// Warning!
-///
-/// This type implements several arithmetic operations from [`core::ops`].
-/// To prevent errors due to overflow or underflow 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 overflow or underflow occurs. Also note that
-/// since the internal representation of amounts is unsigned, subtracting below
-/// zero is considered an underflow and will cause a panic if you're not using
-/// the checked arithmetic methods.
-///
-/// # 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 overflow or underflow 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 overflow or underflow occurs. Also note that
+    /// since the internal representation of amounts is unsigned, subtracting below
+    /// zero is considered an underflow and will cause a panic if you're not using
+    /// the checked arithmetic methods.
+    ///
+    /// # 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) }
path: units/src/amount/unsigned.rs
user: login: jamillambert
body: ```suggestion
        pub const fn from_sat_unchecked(satoshi: u64) -> Amount { Amount(satoshi) }
```

There is a mixture of the use of `Self::` and `Amount::` or `SignedAmount::` in signed, for no obvious reason.
created_at: 2025-03-06T09:13:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -334,53 +306,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("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(&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(Amount::from_sat_unchecked(1)));
path: units/src/amount/tests.rs
user: login: jamillambert
body: ```suggestion
    assert_eq!(p("1.0000000", den_sat), Ok(sat(1)));
```

There are a few cases of this that were missed.
created_at: 2025-03-06T09:15:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: Kixunil
body: @yancyribbens exactly!
created_at: 2025-03-06T09:19:31Z
author_association: COLLABORATOR
diff_hunk: @@ -50,17 +50,17 @@ pub struct SignedAmount(i64);
 
 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);
path: units/src/amount/signed.rs
user: login: Kixunil
body: I object to uselessly modifying the same bunch of lines twice. It's not helpful in any way to change these to use the `_unchecked` version if we know for sure we will be changing them back. It just wastes review time.

And regarding unwrapping in consts, just define a private `const fn unwrap<T>(opt: Option<T>) -> T` and then type `unwrap(Amount::from_sat(whatever))`. Or maybe consider the macro with compile-time check I suggested.
created_at: 2025-03-06T09:43:48Z
author_association: COLLABORATOR
diff_hunk: @@ -16,52 +16,73 @@ 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.
-///
-/// Warning!
-///
-/// This type implements several arithmetic operations from [`core::ops`].
-/// To prevent errors due to overflow or underflow 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 overflow or underflow occurs. Also note that
-/// since the internal representation of amounts is unsigned, subtracting below
-/// zero is considered an underflow and will cause a panic if you're not using
-/// the checked arithmetic methods.
-///
-/// # 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 overflow or underflow 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 overflow or underflow occurs. Also note that
+    /// since the internal representation of amounts is unsigned, subtracting below
+    /// zero is considered an underflow and will cause a panic if you're not using
+    /// the checked arithmetic methods.
+    ///
+    /// # 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) }
path: units/src/amount/unsigned.rs
user: login: Kixunil
body: `Self` is generally better because it protects against renames of types or added generics and such. But I don't think it'll ever be done here. I don't think this code is worth changing.
created_at: 2025-03-06T10:02:42Z
author_association: COLLABORATOR
diff_hunk: @@ -16,52 +16,73 @@ 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.
-///
-/// Warning!
-///
-/// This type implements several arithmetic operations from [`core::ops`].
-/// To prevent errors due to overflow or underflow 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 overflow or underflow occurs. Also note that
-/// since the internal representation of amounts is unsigned, subtracting below
-/// zero is considered an underflow and will cause a panic if you're not using
-/// the checked arithmetic methods.
-///
-/// # 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 overflow or underflow 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 overflow or underflow occurs. Also note that
+    /// since the internal representation of amounts is unsigned, subtracting below
+    /// zero is considered an underflow and will cause a panic if you're not using
+    /// the checked arithmetic methods.
+    ///
+    /// # 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) }
path: units/src/amount/unsigned.rs
user: login: jamillambert
body: It was more of a general comment than pulling out that one occurrence. It's just one example where the same function in unsigned and signed are different `pub const fn from_sat_unchecked(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) }`.  The functions throughout seem to vary in which they use.

But it can be changed later, or not at all.
created_at: 2025-03-06T10:25:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,52 +16,73 @@ 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.
-///
-/// Warning!
-///
-/// This type implements several arithmetic operations from [`core::ops`].
-/// To prevent errors due to overflow or underflow 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 overflow or underflow occurs. Also note that
-/// since the internal representation of amounts is unsigned, subtracting below
-/// zero is considered an underflow and will cause a panic if you're not using
-/// the checked arithmetic methods.
-///
-/// # 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 overflow or underflow 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 overflow or underflow occurs. Also note that
+    /// since the internal representation of amounts is unsigned, subtracting below
+    /// zero is considered an underflow and will cause a panic if you're not using
+    /// the checked arithmetic methods.
+    ///
+    /// # 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) }
path: units/src/amount/unsigned.rs
user: login: tcharding
body: Good, thanks. Promoted to #4210
created_at: 2025-03-06T20:18:15Z
author_association: MEMBER
diff_hunk: @@ -334,53 +306,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("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(&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(Amount::from_sat_unchecked(1)));
path: units/src/amount/tests.rs
user: login: tcharding
body: Thanks man: #4211
created_at: 2025-03-06T20:20:01Z
author_association: MEMBER
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: apoelstra
body: Opened https://github.com/rust-bitcoin/rust-bitcoin/issues/4221

We should address this before release, but fine to leave it open during refactors.
created_at: 2025-03-08T14:43:38Z
author_association: MEMBER
diff_hunk: @@ -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<Amount> { self.minimal_non_dust() }
path: bitcoin/src/blockdata/script/borrowed.rs
user: login: yancyribbens
body: Yeah a downside to this change is it is disruptive.
created_at: 2025-03-08T15:27:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,7 +158,7 @@ impl SignedAmount {
     pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
         match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
             // (negative, amount)
-            (false, sat) if sat > SignedAmount::MAX.to_sat() as u64 => Err(ParseAmountError(
+            (false, sat) if sat > SignedAmount::MAX_MONEY.to_sat() as u64 => Err(ParseAmountError(
path: units/src/amount/signed.rs
user: login: yancyribbens
body: I think there is the possibility for catastrophe changing MAX from u64::MAX to MAX_MONEY without deprecation.  Someone could be using this constant thinking that what they are getting is u64::MAX doing some computations etc.  After upgrading they are going to get a completely different result without any warning.  However, that change to `MAX` was done a while ago: https://github.com/rust-bitcoin/rust-bitcoin/pull/3719
created_at: 2025-03-10T14:47:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: tcharding
created_at: 2025-03-02T23:55:15Z
body: After this goes in we can undraft #4157.
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-02T23:55:33Z
body: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T23:10:24Z
body: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
author_association: NONE
user: login: tcharding
created_at: 2025-03-04T20:18:45Z
body: The only change required after review IMO is using a function in the tests. Since this is a blocking PR that has been in the pipeline for nearly 3 moths can we just merge it like this and do a bunch of little follow ups please?
author_association: MEMBER
user: login: Kixunil
created_at: 2025-03-04T21:58:12Z
body: > The only change required after review IMO is using a function in the tests.

I really, really want to drop the unchecked commit and my point wrt single function was to make it single right away so that the later change will be just one-line and thus easier to review, so it makes more sense to do it now rather than later but it's also not a big deal.

I'm going to sleep anyway, so you have around 8 hours to drop the commit. I believe you an do it in that time. :)
author_association: COLLABORATOR
user: login: tcharding
created_at: 2025-03-04T22:00:37Z
body: Sure thing, I'll take a look. I'm going to Sydney to collaborate on PSBT stuff today but this is top of my TODO list.
author_association: MEMBER
user: login: tcharding
created_at: 2025-03-05T09:30:03Z
body: I didn't get to this. Will do it tomorrow.
author_association: MEMBER
user: login: tcharding
created_at: 2025-03-05T22:01:22Z
body: I added a pair of constructor functions to the test module instead of local aliases. It is much cleaner, thanks for pushing for that.
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-05T22:02:49Z
body: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
author_association: NONE
user: login: tcharding
created_at: 2025-03-05T22:03:00Z
body: ~MAX vs MAX_MONEY is left as is. I would like to make it out of scope for this PR. As mentioned in 8c31236c7, by using MAX_MONEY in this PR we set the stage for a simple one liner to change it to using MAX. This PR is blocking and MAX vs MAX_MONEY usage internally is a trivial thing.~

Now uses MAX everywhere.
author_association: MEMBER
user: login: tcharding
created_at: 2025-03-05T22:04:00Z
body: Come on crew, lets make some progress. `units` should have been done months ago. `rust-bitcoin` devs need to ship or else why are we here.
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-06T00:19:54Z
body: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
author_association: NONE
user: login: tcharding
created_at: 2025-03-06T08:09:31Z
body: @apoelstra please consider merging this PR after 48 hours from the last push unless there is any review comments that indicate it is buggy. Everything else can be done as a follow up.
  
author_association: MEMBER
user: login: jamillambert
created_at: 2025-03-06T13:26:41Z
body: Sorry if this is a stupid question but:

Why do we even have an unsigned amount?

If it was to allow for infallible conversion between a `u64` and an `Amount` this is now moot. A `SignedAmount` can be used in all cases an `Amount` can.  Wouldn't it make sense to just remove unsigned and only have one amount that can be negative?
author_association: CONTRIBUTOR
user: login: yancyribbens
created_at: 2025-03-06T14:21:22Z
body: > Why do we even have an unsigned amount?

My thought is we should have both, since sometimes we want the type guarantee that a value isn't negative (which almost always is the case).
author_association: CONTRIBUTOR
user: login: apoelstra
created_at: 2025-03-06T15:22:17Z
body: > @apoelstra please consider merging this PR after 48 hours from the last push unless there is any review comments that indicate it is buggy. Everything else can be done as a follow up.

Sure. FWIW I still haven't reviewed this PR. My local CI box has been continually running for 150+ hours and I've been getting behind on review.

> [discussion about unsigned amount]

Negative numbers are hard to reason about and don't appear on-chain anywhere.

author_association: MEMBER
user: login: tcharding
created_at: 2025-03-06T20:22:51Z
body: @jamillambert your observation is correct if this were just an arbitrary abstract data type but the `Amount` type is _supposed_ to be the thing that is on chain - so never negative. But then we have all the float and string parsing complexity because applications want to interact with humans. In Bitcoin Core its an unsigned 64 bit integer.
author_association: MEMBER
user: login: tcharding
created_at: 2025-03-06T20:25:35Z
body: > My local CI box has been continually running for 150+ hours and I've been getting behind on review.

No stress, thanks for the update.
author_association: MEMBER
user: login: Kixunil
created_at: 2025-03-06T20:39:12Z
body: I have an idea. I will have to review the entire crate anyway before acking 1.0, so you can just save me looking at the diffs and merge whatever you want. Once the dust settles I'll review the crate, not the diffs and issue/PR what I find problematic. The history is shit already anyway, it won't get much worse.
author_association: COLLABORATOR
user: login: tcharding
created_at: 2025-03-06T20:58:14Z
body: Yes!! Lets do it, merge this one and merge #4157. LFG
author_association: MEMBER
user: login: apoelstra
created_at: 2025-03-06T23:34:04Z
body: >I have an idea. I will have to review the entire crate anyway before acking 1.0, so you can just save me looking at the diffs and merge whatever you want. 

This sounds great! Appreciate your willingness to still pre-review the 1.0 API despite us running you over on the development process.
author_association: MEMBER
user: login: Kixunil
created_at: 2025-03-07T09:52:21Z
body: Alright. Yeah, I figured if git history is already screwed and we will do a separate QA step before release anyway and we won't do an emergency release (because we would do such thing on top of old `bitcion`, not here) then the entire PR process is not really useful and I can just ignore it.
author_association: COLLABORATOR
user: login: tcharding
created_at: 2025-03-07T20:53:29Z
body: I don't know your queuing strategy @apoelstra but in the chance it helps you filter I added P-high and I'll do so for any `units` PRs as long as they aren't invasive of other crates now we have this agreement. Thanks @Kixunil. I have been trying to say that ever since you came back, apologies if my communication skills are lacking.
author_association: MEMBER
user: login: apoelstra
created_at: 2025-03-08T14:46:09Z
body: utACK 3b9d0c12c914d94827b29c4d0782307921276a8f

Have merge of #4161 queued up, then test of this one, then merge of #4178 (which won't affect this at all), then merge of this PR. I am a little worried that #4161 will cause a merge conflict but they're both big PRs that affect `amount` and we need to get them both in.
author_association: MEMBER
user: login: apoelstra
created_at: 2025-03-08T15:23:26Z
body: Did the rebase myself. There were no conflicts except for the "Use sat/ssat constructors throughout tests" commit where the tests had changed. I tried to redo your "replace Amount::from_sat_unchecked" with "sat" transformation. Please double-check that I succeeded.
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-08T15:23:34Z
body: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
author_association: NONE
user: login: tcharding
created_at: 2025-03-08T18:26:34Z
body: Rebase looks good, thanks.

ACK 26d79a4646940467ded5a7fc766cae77db819d95
author_association: MEMBER
user: login: apoelstra
created_at: 2025-03-09T14:33:47Z
body: @tcharding can you do a github "approve". Apparently GH won't let me push with only my approval since I was the last pusher to the PR. Which I guess makes some sense.
author_association: MEMBER
user: login: tcharding
created_at: 2025-03-09T20:46:30Z
body: Its greyed out. We have as second ack from @jamillambert that should satisfy the merge script, right? I don't remember the script checking for acks from a whitelist of maintainers.

EDIT: We need a green tick from a maintainer that is not you I suppose.
author_association: MEMBER
user: login: tcharding
created_at: 2025-03-09T20:47:16Z
body: I could rebase force push so you can ack again?
author_association: MEMBER
user: login: apoelstra
created_at: 2025-03-10T15:52:09Z
body: @tcharding yeah, if you can't give me a green checkmark then that's the best option I think. Can you tweak the final commit in some trivial way and force-push? I should be able to run local-CI on the tweaked commit pretty quickly (possibly it won't redo any tests at all, if the source tree doesn't change..).
author_association: MEMBER
user: login: tcharding
created_at: 2025-03-10T18:31:18Z
body: I just did an empty ammend (changed timestamp of last patch). Now GitHub thinks there are merge conflicts. 
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-10T18:35:38Z
body: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
author_association: NONE
user: login: tcharding
created_at: 2025-03-10T18:35:48Z
body: Conflicted with the docs PR that merged. I rebase and fixed the conflict. Its 5:30 am here but git range-diff tells me I did it correctly.
author_association: MEMBER
user: login: tcharding
created_at: 2025-03-10T18:39:54Z
body: Oh no I didn't - try now.
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-10T18:41:16Z
body: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
author_association: NONE