Repository: rust-bitcoin/rust-bitcoin
Pull request #4164 (2025-03-02 23:54:25)

Prepare to enforce MAX_MONEY invariant
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.

tcharding: After this goes in we can undraft #4157.
github-actions[bot]: :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.
github-actions[bot]: :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.
tcharding: 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?
Kixunil: > 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. :)
tcharding: I added a pair of constructor functions to the test module instead of local aliases. It is much cleaner, thanks for pushing for that.
github-actions[bot]: :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.
tcharding: 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.
tcharding: 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.
github-actions[bot]: :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.
tcharding: @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.
  
jamillambert: 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?
yancyribbens: > 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).
tcharding: @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.
tcharding: > 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.
tcharding: Yes!! Lets do it, merge this one and merge #4157. LFG
apoelstra: >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.
Kixunil: 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.
tcharding: 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.
apoelstra: 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.
apoelstra: 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.
github-actions[bot]: :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.
tcharding: Rebase looks good, thanks.

ACK 26d79a4646940467ded5a7fc766cae77db819d95
apoelstra: @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.
tcharding: 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.
tcharding: I could rebase force push so you can ack again?
apoelstra: @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..).
tcharding: I just did an empty ammend (changed timestamp of last patch). Now GitHub thinks there are merge conflicts. 

Patch:
diff --git a/bitcoin/src/blockdata/script/borrowed.rs b/bitcoin/src/blockdata/script/borrowed.rs
index 624feb3b..4ae5c9ce 100644
--- a/bitcoin/src/blockdata/script/borrowed.rs
+++ b/bitcoin/src/blockdata/script/borrowed.rs
@@ -261,7 +261,7 @@ fn redeem_script(&self) -> Option<&Script> {
         /// 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 @@ fn dust_value(&self) -> Amount { self.minimal_non_dust() }
         /// 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 @@ fn minimal_non_dust(&self) -> Amount {
         /// 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 @@ impl Sealed for super::Script {}
 
 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 @@ fn minimal_non_dust_internal(&self, dust_relay_fee: u64) -> Amount {
                     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/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index 4510c465..0c60d3c6 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -183,8 +183,8 @@ fn size(&self) -> usize { size_from_script_pubkey(&self.script_pubkey) }
         /// 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 @@ fn minimal_non_dust(script_pubkey: ScriptBuf) -> Self {
         /// 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 f10cbbb3..b2787d73 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -2283,7 +2283,7 @@ fn sign_psbt() {
             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/units/src/amount/mod.rs b/units/src/amount/mod.rs
index 7fb8bc53..940b9a5a 100644
--- a/units/src/amount/mod.rs
+++ b/units/src/amount/mod.rs
@@ -39,6 +39,7 @@
     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 ae1c6e12..4b78d008 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 @@ fn sum<I>(iter: I) -> Self
     }
 }
 
-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 7551ac19..a04795de 100644
--- a/units/src/amount/signed.rs
+++ b/units/src/amount/signed.rs
@@ -16,51 +16,74 @@
     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 @@ pub fn from_btc(btc: f64) -> Result<SignedAmount, ParseAmountError> {
     }
 
     /// 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 @@ pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAm
             (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 @@ pub fn to_string_with_denomination(self, denom: Denomination) -> String {
 
     /// 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 @@ pub fn unsigned_abs(self) -> Amount { Amount::from_sat(self.0.unsigned_abs()) }
     /// - `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 @@ pub fn is_negative(self) -> bool { self.0.is_negative() }
     #[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 @@ pub const fn checked_abs(self) -> Option<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 @@ pub const fn checked_add(self, rhs: SignedAmount) -> Option<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 @@ pub const fn checked_sub(self, rhs: SignedAmount) -> Option<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 @@ pub const fn checked_mul(self, rhs: i64) -> Option<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 @@ pub const fn checked_div(self, rhs: i64) -> Option<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 @@ pub fn to_unsigned(self) -> Result<Amount, OutOfRangeError> {
 
     /// 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 @@ fn from(value: Amount) -> Self {
 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/unsigned.rs b/units/src/amount/unsigned.rs
index 89d7eb35..c3488dfa 100644
--- a/units/src/amount/unsigned.rs
+++ b/units/src/amount/unsigned.rs
@@ -16,41 +16,70 @@
     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 @@ pub fn from_btc(btc: f64) -> Result<Amount, ParseAmountError> {
     }
 
     /// 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 @@ pub fn from_str_in(s: &str, denom: Denomination) -> Result<Amount, ParseAmountEr
                 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 @@ pub fn to_string_with_denomination(self, denom: Denomination) -> String {
     #[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 @@ pub const fn checked_add(self, rhs: Amount) -> Option<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 @@ pub const fn checked_sub(self, rhs: Amount) -> Option<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 @@ pub const fn checked_mul(self, rhs: u64) -> Option<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 @@ pub const fn checked_div(self, rhs: u64) -> Option<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 @@ pub fn to_signed(self) -> SignedAmount {
 
     /// 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 @@ fn try_from(value: SignedAmount) -> Result<Self, Self::Error> { value.to_unsigne
 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 1bb3b9b6..39dff63b 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 @@ pub const fn checked_mul_by_weight(self, weight: Weight) -> Option<Amount> {
 
 /// 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 @@ fn multiply() {
         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]


Test patch:
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index 9b47ef65..4443645b 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/tests/serde.rs b/bitcoin/tests/serde.rs
index 807f838a..dff93b0c 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/tests.rs b/units/src/amount/tests.rs
index d263623e..2d3b5e1c 100644
--- a/units/src/amount/tests.rs
+++ b/units/src/amount/tests.rs
@@ -16,11 +16,11 @@
 #[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 @@ fn $test_name() {
 
 #[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 scase(s: &str, expected: Result<SignedAmount, impl Into<ParseError>>) {
     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 @@ struct T {
     }
 
     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 @@ struct T {
         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 @@ struct T {
     }
 
     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 @@ struct T {
         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 @@ struct T {
         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 @@ struct T {
         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 @@ struct T {
 
 #[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 @@ macro_rules! check_op {
 
 #[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/tests/str.rs b/units/tests/str.rs
index 89068bf9..0b365af2 100644
--- a/units/tests/str.rs
+++ b/units/tests/str.rs
@@ -22,10 +22,10 @@ fn $test_name() {
 
 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";