Repository: rust-bitcoin/rust-bitcoin
Pull request #4312 (2025-04-07 02:18:01)

units: Return `NumOpResult` when implementing `Div`
First 4 patches are preparatory clean up. Patch 5 and 6 make all implementations of `Div` in the crate return `NumOpResult`. Done as two separate patches to assist review and in case we need to iterate, patch 5 is likely trivial to review.

Fix: #4215


Patch:
diff --git a/bitcoin/src/blockdata/mod.rs b/bitcoin/src/blockdata/mod.rs
index ab3b6746..a6f39ba9 100644
--- a/bitcoin/src/blockdata/mod.rs
+++ b/bitcoin/src/blockdata/mod.rs
@@ -19,7 +19,7 @@
     weight::Weight
 };
 
-/// Implements `FeeRate` and associated features.
+/// Implements `FeeRate` and assoctiated features.
 pub mod fee_rate {
     #[cfg(feature = "serde")]
     pub use units::fee_rate::serde;
diff --git a/bitcoin/src/blockdata/script/builder.rs b/bitcoin/src/blockdata/script/builder.rs
index bc22f8e1..7efacd10 100644
--- a/bitcoin/src/blockdata/script/builder.rs
+++ b/bitcoin/src/blockdata/script/builder.rs
@@ -129,7 +129,7 @@ pub fn push_relative_lock_time(self, lock_time: relative::LockTime) -> Builder {
     ///
     /// # Deprecated
     /// This method is deprecated in favor of `push_relative_lock_time`.
-    ///
+    /// 
     /// In Bitcoin script semantics, when using CHECKSEQUENCEVERIFY, you typically
     /// want to push a relative locktime value to be compared against the input's
     /// sequence number, not the sequence number itself.
diff --git a/nightly-version b/nightly-version
index 4312103f..ba747383 100644
--- a/nightly-version
+++ b/nightly-version
@@ -1,1 +1,1 @@
-nightly-2025-04-04
+nightly-2025-03-21
diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs
index 8928cea9..6fb09fbc 100644
--- a/units/src/amount/mod.rs
+++ b/units/src/amount/mod.rs
@@ -35,11 +35,9 @@
         OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError,
         PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError,
     },
-    result::{NumOpError, NumOpResult},
     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 ab06f207..614e55bd 100644
--- a/units/src/amount/result.rs
+++ b/units/src/amount/result.rs
@@ -1,112 +1,13 @@
 // SPDX-License-Identifier: CC0-1.0
 
-//! Provides a monodic type used when mathematical operations (`core::ops`) return an amount type.
+//! Provides a monodic type returned by mathematical operations (`core::ops`).
 
-use core::{fmt, ops};
+use core::ops;
 
 use NumOpResult as R;
 
 use super::{Amount, SignedAmount};
-
-/// Result of an operation on [`Amount`] or [`SignedAmount`].
-///
-/// The type parameter `T` should be normally `Amount` or `SignedAmount`.
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-#[must_use]
-pub enum NumOpResult<T> {
-    /// Result of a successful mathematical operation.
-    Valid(T),
-    /// Result of an unsuccessful mathematical operation.
-    Error(NumOpError),
-}
-
-impl<T: fmt::Debug> NumOpResult<T> {
-    /// Returns the contained valid amount, consuming `self`.
-    ///
-    /// # Panics
-    ///
-    /// Panics with `msg` if the numeric result is an `Error`.
-    #[inline]
-    #[track_caller]
-    pub fn expect(self, msg: &str) -> T {
-        match self {
-            R::Valid(amount) => amount,
-            R::Error(_) => panic!("{}", msg),
-        }
-    }
-
-    /// Returns the contained valid amount, consuming `self`.
-    ///
-    /// # Panics
-    ///
-    /// Panics if the numeric result is an `Error`.
-    #[inline]
-    #[track_caller]
-    pub fn unwrap(self) -> T {
-        match self {
-            R::Valid(amount) => amount,
-            R::Error(e) => panic!("tried to unwrap an invalid numeric result: {:?}", e),
-        }
-    }
-
-    /// Returns the contained error, consuming `self`.
-    ///
-    /// # Panics
-    ///
-    /// Panics if the numeric result is a valid amount.
-    #[inline]
-    #[track_caller]
-    pub fn unwrap_err(self) -> NumOpError {
-        match self {
-            R::Error(e) => e,
-            R::Valid(a) => panic!("tried to unwrap a valid numeric result: {:?}", a),
-        }
-    }
-
-    /// Converts this `NumOpResult` to an `Option<T>`.
-    #[inline]
-    pub fn ok(self) -> Option<T> {
-        match self {
-            R::Valid(amount) => Some(amount),
-            R::Error(_) => None,
-        }
-    }
-
-    /// Converts this `NumOpResult` to a `Result<T, NumOpError>`.
-    #[inline]
-    #[allow(clippy::missing_errors_doc)]
-    pub fn into_result(self) -> Result<T, NumOpError> {
-        match self {
-            R::Valid(amount) => Ok(amount),
-            R::Error(e) => Err(e),
-        }
-    }
-
-    /// Calls `op` if the numeric result is `Valid`, otherwise returns the `Error` value of `self`.
-    #[inline]
-    pub fn and_then<F>(self, op: F) -> NumOpResult<T>
-    where
-        F: FnOnce(T) -> NumOpResult<T>,
-    {
-        match self {
-            R::Valid(amount) => op(amount),
-            R::Error(e) => R::Error(e),
-        }
-    }
-
-    /// Returns `true` if the numeric result is a valid amount.
-    #[inline]
-    pub fn is_valid(&self) -> bool {
-        match self {
-            R::Valid(_) => true,
-            R::Error(_) => false,
-        }
-    }
-
-    /// Returns `true` if the numeric result is an invalid amount.
-    #[inline]
-    pub fn is_error(&self) -> bool { !self.is_valid() }
-}
+use crate::{MathOp, NumOpError, NumOpResult, OptionExt};
 
 impl From<Amount> for NumOpResult<Amount> {
     fn from(a: Amount) -> Self { Self::Valid(a) }
@@ -126,7 +27,7 @@ fn from(a: &SignedAmount) -> Self { Self::Valid(*a) }
     impl ops::Add<Amount> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).valid_or_error() }
+        fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).valid_or_error(MathOp::Add) }
     }
     impl ops::Add<NumOpResult<Amount>> for Amount {
         type Output = NumOpResult<Amount>;
@@ -137,7 +38,7 @@ fn add(self, rhs: NumOpResult<Amount>) -> Self::Output { rhs.and_then(|a| a + se
     impl ops::Sub<Amount> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).valid_or_error() }
+        fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).valid_or_error(MathOp::Sub) }
     }
     impl ops::Sub<NumOpResult<Amount>> for Amount {
         type Output = NumOpResult<Amount>;
@@ -153,7 +54,7 @@ fn sub(self, rhs: NumOpResult<Amount>) -> Self::Output {
     impl ops::Mul<u64> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).valid_or_error() }
+        fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).valid_or_error(MathOp::Mul) }
     }
     impl ops::Mul<u64> for NumOpResult<Amount> {
         type Output = NumOpResult<Amount>;
@@ -163,7 +64,7 @@ fn mul(self, rhs: u64) -> Self::Output { self.and_then(|lhs| lhs * rhs) }
     impl ops::Mul<Amount> for u64 {
         type Output = NumOpResult<Amount>;
 
-        fn mul(self, rhs: Amount) -> Self::Output { rhs.checked_mul(self).valid_or_error() }
+        fn mul(self, rhs: Amount) -> Self::Output { rhs.checked_mul(self).valid_or_error(MathOp::Mul) }
     }
     impl ops::Mul<NumOpResult<Amount>> for u64 {
         type Output = NumOpResult<Amount>;
@@ -174,7 +75,7 @@ fn mul(self, rhs: NumOpResult<Amount>) -> Self::Output { rhs.and_then(|rhs| self
     impl ops::Div<u64> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).valid_or_error() }
+        fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).valid_or_error(MathOp::Div) }
     }
     impl ops::Div<u64> for NumOpResult<Amount> {
         type Output = NumOpResult<Amount>;
@@ -182,15 +83,17 @@ impl ops::Div<u64> for NumOpResult<Amount> {
         fn div(self, rhs: u64) -> Self::Output { self.and_then(|lhs| lhs / rhs) }
     }
     impl ops::Div<Amount> for Amount {
-        type Output = u64;
+        type Output = NumOpResult<u64>;
 
-        fn div(self, rhs: Amount) -> Self::Output { self.to_sat() / rhs.to_sat() }
+        fn div(self, rhs: Amount) -> Self::Output {
+            self.to_sat().checked_div(rhs.to_sat()).valid_or_error(MathOp::Div)
+        }
     }
 
     impl ops::Rem<u64> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).valid_or_error() }
+        fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).valid_or_error(MathOp::Rem) }
     }
     impl ops::Rem<u64> for NumOpResult<Amount> {
         type Output = NumOpResult<Amount>;
@@ -201,7 +104,7 @@ fn rem(self, modulus: u64) -> Self::Output { self.and_then(|lhs| lhs % modulus)
     impl ops::Add<SignedAmount> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn add(self, rhs: SignedAmount) -> Self::Output { self.checked_add(rhs).valid_or_error() }
+        fn add(self, rhs: SignedAmount) -> Self::Output { self.checked_add(rhs).valid_or_error(MathOp::Add) }
     }
     impl ops::Add<NumOpResult<SignedAmount>> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
@@ -212,7 +115,7 @@ fn add(self, rhs: NumOpResult<SignedAmount>) -> Self::Output { rhs.and_then(|a|
     impl ops::Sub<SignedAmount> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn sub(self, rhs: SignedAmount) -> Self::Output { self.checked_sub(rhs).valid_or_error() }
+        fn sub(self, rhs: SignedAmount) -> Self::Output { self.checked_sub(rhs).valid_or_error(MathOp::Sub) }
     }
     impl ops::Sub<NumOpResult<SignedAmount>> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
@@ -228,7 +131,7 @@ fn sub(self, rhs: NumOpResult<SignedAmount>) -> Self::Output {
     impl ops::Mul<i64> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn mul(self, rhs: i64) -> Self::Output { self.checked_mul(rhs).valid_or_error() }
+        fn mul(self, rhs: i64) -> Self::Output { self.checked_mul(rhs).valid_or_error(MathOp::Mul) }
     }
     impl ops::Mul<i64> for NumOpResult<SignedAmount> {
         type Output = NumOpResult<SignedAmount>;
@@ -238,7 +141,7 @@ fn mul(self, rhs: i64) -> Self::Output { self.and_then(|lhs| lhs * rhs) }
     impl ops::Mul<SignedAmount> for i64 {
         type Output = NumOpResult<SignedAmount>;
 
-        fn mul(self, rhs: SignedAmount) -> Self::Output { rhs.checked_mul(self).valid_or_error() }
+        fn mul(self, rhs: SignedAmount) -> Self::Output { rhs.checked_mul(self).valid_or_error(MathOp::Mul) }
     }
     impl ops::Mul<NumOpResult<SignedAmount>> for i64 {
         type Output = NumOpResult<SignedAmount>;
@@ -249,7 +152,7 @@ fn mul(self, rhs: NumOpResult<SignedAmount>) -> Self::Output { rhs.and_then(|rhs
     impl ops::Div<i64> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn div(self, rhs: i64) -> Self::Output { self.checked_div(rhs).valid_or_error() }
+        fn div(self, rhs: i64) -> Self::Output { self.checked_div(rhs).valid_or_error(MathOp::Div) }
     }
     impl ops::Div<i64> for NumOpResult<SignedAmount> {
         type Output = NumOpResult<SignedAmount>;
@@ -257,15 +160,17 @@ impl ops::Div<i64> for NumOpResult<SignedAmount> {
         fn div(self, rhs: i64) -> Self::Output { self.and_then(|lhs| lhs / rhs) }
     }
     impl ops::Div<SignedAmount> for SignedAmount {
-        type Output = i64;
+        type Output = NumOpResult<i64>;
 
-        fn div(self, rhs: SignedAmount) -> Self::Output { self.to_sat() / rhs.to_sat() }
+        fn div(self, rhs: SignedAmount) -> Self::Output {
+            self.to_sat().checked_div(rhs.to_sat()).valid_or_error(MathOp::Div)
+        }
     }
 
     impl ops::Rem<i64> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).valid_or_error() }
+        fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).valid_or_error(MathOp::Rem) }
     }
     impl ops::Rem<i64> for NumOpResult<SignedAmount> {
         type Output = NumOpResult<SignedAmount>;
@@ -282,7 +187,7 @@ impl<T> ops::Add<NumOpResult<T>> for NumOpResult<T>
         fn add(self, rhs: Self) -> Self::Output {
             match (self, rhs) {
                 (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-                (_, _) => R::Error(NumOpError {}),
+                (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
             }
         }
     }
@@ -305,7 +210,7 @@ impl<T> ops::Sub<NumOpResult<T>> for NumOpResult<T>
         fn sub(self, rhs: Self) -> Self::Output {
             match (self, rhs) {
                 (R::Valid(lhs), R::Valid(rhs)) => lhs - rhs,
-                (_, _) => R::Error(NumOpError {}),
+                (_, _) => R::Error(NumOpError::while_doing(MathOp::Sub)),
             }
         }
     }
@@ -340,7 +245,7 @@ fn sum<I>(iter: I) -> Self
     {
         iter.fold(R::Valid(Amount::ZERO), |acc, amount| match (acc, amount) {
             (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-            (_, _) => R::Error(NumOpError {}),
+            (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
         })
     }
 }
@@ -351,7 +256,7 @@ fn sum<I>(iter: I) -> Self
     {
         iter.fold(R::Valid(Amount::ZERO), |acc, amount| match (acc, amount) {
             (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-            (_, _) => R::Error(NumOpError {}),
+            (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
         })
     }
 }
@@ -363,7 +268,7 @@ fn sum<I>(iter: I) -> Self
     {
         iter.fold(R::Valid(SignedAmount::ZERO), |acc, amount| match (acc, amount) {
             (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-            (_, _) => R::Error(NumOpError {}),
+            (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
         })
     }
 }
@@ -374,45 +279,7 @@ fn sum<I>(iter: I) -> Self
     {
         iter.fold(R::Valid(SignedAmount::ZERO), |acc, amount| match (acc, amount) {
             (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-            (_, _) => R::Error(NumOpError {}),
+            (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
         })
     }
 }
-
-pub(crate) trait OptionExt<T> {
-    fn valid_or_error(self) -> NumOpResult<T>;
-}
-
-impl OptionExt<Amount> for Option<Amount> {
-    #[inline]
-    fn valid_or_error(self) -> NumOpResult<Amount> {
-        match self {
-            Some(amount) => R::Valid(amount),
-            None => R::Error(NumOpError {}),
-        }
-    }
-}
-
-impl OptionExt<SignedAmount> for Option<SignedAmount> {
-    #[inline]
-    fn valid_or_error(self) -> NumOpResult<SignedAmount> {
-        match self {
-            Some(amount) => R::Valid(amount),
-            None => R::Error(NumOpError {}),
-        }
-    }
-}
-
-/// An error occurred while doing a mathematical operation.
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct NumOpError;
-
-impl fmt::Display for NumOpError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "a math operation on amounts gave an invalid numeric result")
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for NumOpError {}
diff --git a/units/src/fee.rs b/units/src/fee.rs
index 21d223c9..45d18e8d 100644
--- a/units/src/fee.rs
+++ b/units/src/fee.rs
@@ -13,8 +13,7 @@
 
 use core::ops;
 
-use crate::amount::{NumOpResult, OptionExt};
-use crate::{Amount, FeeRate, Weight};
+use crate::{Amount, FeeRate, MathOp, NumOpResult, OptionExt, Weight};
 
 impl Amount {
     /// Checked weight ceiling division.
@@ -167,30 +166,30 @@ pub const fn checked_mul_by_weight(self, weight: Weight) -> Option<Amount> {
     impl ops::Mul<FeeRate> for Weight {
         type Output = NumOpResult<Amount>;
         fn mul(self, rhs: FeeRate) -> Self::Output {
-            rhs.checked_mul_by_weight(self).valid_or_error()
+            rhs.checked_mul_by_weight(self).valid_or_error(MathOp::Mul)
         }
     }
 
     impl ops::Mul<Weight> for FeeRate {
         type Output = NumOpResult<Amount>;
         fn mul(self, rhs: Weight) -> Self::Output {
-            self.checked_mul_by_weight(rhs).valid_or_error()
+            self.checked_mul_by_weight(rhs).valid_or_error(MathOp::Mul)
         }
     }
 
     impl ops::Div<Weight> for Amount {
-        type Output = FeeRate;
+        type Output = NumOpResult<FeeRate>;
 
         fn div(self, rhs: Weight) -> Self::Output {
-            FeeRate::from_sat_per_kwu(self.to_sat() * 1000 / rhs.to_wu())
+            self.checked_div_by_weight_floor(rhs).valid_or_error(MathOp::Div)
         }
     }
 
     impl ops::Div<FeeRate> for Amount {
-        type Output = Weight;
+        type Output = NumOpResult<Weight>;
 
         fn div(self, rhs: FeeRate) -> Self::Output {
-            self.checked_div_by_fee_rate_floor(rhs).unwrap()
+            self.checked_div_by_fee_rate_floor(rhs).valid_or_error(MathOp::Div)
         }
     }
 }
@@ -216,7 +215,7 @@ mod tests {
 
     #[test]
     fn fee_rate_div_by_weight() {
-        let fee_rate = Amount::from_sat_u32(329) / Weight::from_wu(381);
+        let fee_rate = (Amount::from_sat_u32(329) / Weight::from_wu(381)).unwrap();
         assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863));
     }
 
@@ -276,21 +275,21 @@ fn amount_div_by_fee_rate() {
         // Test exact division
         let amount = Amount::from_sat_u32(1000);
         let fee_rate = FeeRate::from_sat_per_kwu(2);
-        let weight = amount / fee_rate;
+        let weight = (amount / fee_rate).unwrap();
         assert_eq!(weight, Weight::from_wu(500_000));
 
         // Test reference division
-        let weight_ref = &amount / fee_rate;
+        let weight_ref = (&amount / fee_rate).unwrap();
         assert_eq!(weight_ref, Weight::from_wu(500_000));
-        let weight_ref2 = amount / &fee_rate;
+        let weight_ref2 = (amount / &fee_rate).unwrap();
         assert_eq!(weight_ref2, Weight::from_wu(500_000));
-        let weight_ref3 = &amount / &fee_rate;
+        let weight_ref3 = (&amount / &fee_rate).unwrap();
         assert_eq!(weight_ref3, Weight::from_wu(500_000));
 
         // Test truncation behavior
         let amount = Amount::from_sat_u32(1000);
         let fee_rate = FeeRate::from_sat_per_kwu(3);
-        let weight = amount / fee_rate;
+        let weight = (amount / fee_rate).unwrap();
         // 1000 * 1000 = 1,000,000 msats
         // 1,000,000 / 3 = 333,333.33... wu
         // Should truncate down to 333,333 wu
diff --git a/units/src/internal_macros.rs b/units/src/internal_macros.rs
index c5f0ac6f..ee6fc33d 100644
--- a/units/src/internal_macros.rs
+++ b/units/src/internal_macros.rs
@@ -4,7 +4,7 @@
 //!
 //! Macros meant to be used inside the `bitcoin-units` library.
 
-/// Implements an opcode for various reference combinations.
+/// Implements a mathematical operation for various reference combinations.
 ///
 /// Given `$ty`, assumes the `$op_trait<$other_ty>` trait is implemented on it,
 /// and implements the same trait with the full matrix of `&$ty` and `&$other_ty`:
diff --git a/units/src/lib.rs b/units/src/lib.rs
index 4a166ebb..0919efb7 100644
--- a/units/src/lib.rs
+++ b/units/src/lib.rs
@@ -20,6 +20,7 @@
 
 mod fee;
 mod internal_macros;
+mod result;
 
 #[doc(hidden)]
 pub mod _export {
@@ -43,6 +44,8 @@ pub mod _core {
     amount::{Amount, SignedAmount},
     block::{BlockHeight, BlockInterval},
     fee_rate::FeeRate,
+    result::{NumOpError, NumOpResult, MathOp},
     time::BlockTime,
     weight::Weight
 };
+pub(crate) use self::result::OptionExt;
diff --git a/units/src/result.rs b/units/src/result.rs
new file mode 100644
index 00000000..8276e6cc
--- /dev/null
+++ b/units/src/result.rs
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! Provides a monodic type returned by mathematical operations (`core::ops`).
+
+use core::fmt;
+
+use NumOpResult as R;
+
+use crate::{Amount, FeeRate, SignedAmount, Weight};
+
+/// Result of a mathematical operation on two numeric types.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[must_use]
+pub enum NumOpResult<T> {
+    /// Result of a successful mathematical operation.
+    Valid(T),
+    /// Result of an unsuccessful mathematical operation.
+    Error(NumOpError),
+}
+
+impl<T: fmt::Debug> NumOpResult<T> {
+    /// Returns the contained valid numeric type, consuming `self`.
+    ///
+    /// # Panics
+    ///
+    /// Panics with `msg` if the numeric result is an `Error`.
+    #[inline]
+    #[track_caller]
+    pub fn expect(self, msg: &str) -> T {
+        match self {
+            R::Valid(x) => x,
+            R::Error(_) => panic!("{}", msg),
+        }
+    }
+
+    /// Returns the contained valid numeric type, consuming `self`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the numeric result is an `Error`.
+    #[inline]
+    #[track_caller]
+    pub fn unwrap(self) -> T {
+        match self {
+            R::Valid(x) => x,
+            R::Error(e) => panic!("tried to unwrap an invalid numeric result: {:?}", e),
+        }
+    }
+
+    /// Returns the contained error, consuming `self`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the numeric result is valid.
+    #[inline]
+    #[track_caller]
+    pub fn unwrap_err(self) -> NumOpError {
+        match self {
+            R::Error(e) => e,
+            R::Valid(a) => panic!("tried to unwrap a valid numeric result: {:?}", a),
+        }
+    }
+
+    /// Converts this `NumOpResult` to an `Option<T>`.
+    #[inline]
+    pub fn ok(self) -> Option<T> {
+        match self {
+            R::Valid(x) => Some(x),
+            R::Error(_) => None,
+        }
+    }
+
+    /// Converts this `NumOpResult` to a `Result<T, NumOpError>`.
+    #[inline]
+    #[allow(clippy::missing_errors_doc)]
+    pub fn into_result(self) -> Result<T, NumOpError> {
+        match self {
+            R::Valid(x) => Ok(x),
+            R::Error(e) => Err(e),
+        }
+    }
+
+    /// Calls `op` if the numeric result is `Valid`, otherwise returns the `Error` value of `self`.
+    #[inline]
+    pub fn and_then<F>(self, op: F) -> NumOpResult<T>
+    where
+        F: FnOnce(T) -> NumOpResult<T>,
+    {
+        match self {
+            R::Valid(x) => op(x),
+            R::Error(e) => R::Error(e),
+        }
+    }
+
+    /// Returns `true` if the numeric result is valid.
+    #[inline]
+    pub fn is_valid(&self) -> bool {
+        match self {
+            R::Valid(_) => true,
+            R::Error(_) => false,
+        }
+    }
+
+    /// Returns `true` if the numeric result is invalid.
+    #[inline]
+    pub fn is_error(&self) -> bool { !self.is_valid() }
+}
+
+pub(crate) trait OptionExt<T> {
+    fn valid_or_error(self, op: MathOp) -> NumOpResult<T>;
+}
+
+macro_rules! impl_opt_ext {
+    ($($ty:ident),* $(,)?) => {
+        $(
+            impl OptionExt<$ty> for Option<$ty> {
+                #[inline]
+                fn valid_or_error(self, op: MathOp) -> NumOpResult<$ty> {
+                    match self {
+                        Some(amount) => R::Valid(amount),
+                        None => R::Error(NumOpError(op)),
+                    }
+                }
+            }
+        )*
+    }
+}
+impl_opt_ext!(Amount, SignedAmount, u64, i64, FeeRate, Weight);
+
+/// An error occurred while doing a mathematical operation.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct NumOpError(MathOp);
+
+impl NumOpError {
+    /// Creates a [`NumOpError`] caused by `op`.
+    pub fn while_doing(op: MathOp) -> Self { NumOpError(op) }
+
+    /// Returns the [`MathOp`] that caused this error.
+    pub fn operation(self) -> MathOp { self.0 }
+}
+
+impl fmt::Display for NumOpError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "math operation '{}' gave an invalid numeric result", self.operation())
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for NumOpError {}
+
+/// The math operation that caused the error.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum MathOp {
+    /// Addition failed ([`core::ops::Add`] resulted in an invalid value).
+    Add,
+    /// Subtraction failed ([`core::ops::Sub`] resulted in an invalid value).
+    Sub,
+    /// Multiplication failed ([`core::ops::Mul`] resulted in an invalid value).
+    Mul,
+    /// Division failed ([`core::ops::Div`] attempted div-by-zero).
+    Div,
+    /// Calculating the remainder failed ([`core::ops::Rem`] attempted div-by-zero).
+    Rem,
+    /// Negation failed ([`core::ops::Neg`] resulted in an invalid value).
+    Neg,
+}
+
+impl MathOp {
+    /// Returns `true` if this operation error'ed due to overflow.
+    pub fn is_overflow(self) -> bool {
+        matches!(self, MathOp::Add | MathOp::Sub | MathOp::Mul | MathOp::Neg)
+    }
+
+    /// Returns `true` if this operation error'ed due to division by zero.
+    pub fn is_div_by_zero(self) -> bool { !self.is_overflow() }
+}
+
+impl fmt::Display for MathOp {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            MathOp::Add => write!(f, "add"),
+            MathOp::Sub => write!(f, "sub"),
+            MathOp::Mul => write!(f, "mul"),
+            MathOp::Div => write!(f, "div"),
+            MathOp::Rem => write!(f, "rem"),
+            MathOp::Neg => write!(f, "neg"),
+        }
+    }
+}


Test patch:
diff --git a/bitcoin/tests/bip_174.rs b/bitcoin/tests/bip_174.rs
index 49565865..396fc6f3 100644
--- a/bitcoin/tests/bip_174.rs
+++ b/bitcoin/tests/bip_174.rs
@@ -263,15 +263,15 @@ fn update_psbt(mut psbt: Psbt, fingerprint: Fingerprint) -> Psbt {
     psbt
 }
 
-/// `pk_path` holds tuples of `(public_key, derivation_path)`. `indices` is used to access the
+/// `pk_path` holds tuples of `(public_key, derivation_path)`. `indecies` is used to access the
 /// `pk_path` vector. `fingerprint` is from the parent extended public key.
 fn bip32_derivation(
     fingerprint: Fingerprint,
     pk_path: &[(&str, &str)],
-    indices: Vec<usize>,
+    indecies: Vec<usize>,
 ) -> BTreeMap<secp256k1::PublicKey, KeySource> {
     let mut tree = BTreeMap::new();
-    for i in indices {
+    for i in indecies {
         let pk = pk_path[i].0;
         let path = pk_path[i].1;
 
diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs
index f8aba133..a2a3bec0 100644
--- a/units/src/amount/tests.rs
+++ b/units/src/amount/tests.rs
@@ -13,6 +13,7 @@
 use ::serde::{Deserialize, Serialize};
 
 use super::*;
+use crate::NumOpResult;
 #[cfg(feature = "alloc")]
 use crate::{FeeRate, Weight};
 
@@ -1230,27 +1231,31 @@ fn op_int_combos() {
 
 #[test]
 fn unsigned_amount_div_by_amount() {
-    assert_eq!(sat(0) / sat(7), 0);
-    assert_eq!(sat(1897) / sat(7), 271);
+    assert_eq!((sat(0) / sat(7)).unwrap(), 0);
+    assert_eq!((sat(1897) / sat(7)).unwrap(), 271);
 }
 
 #[test]
-#[should_panic(expected = "attempt to divide by zero")]
-fn unsigned_amount_div_by_amount_zero() { let _ = sat(1897) / Amount::ZERO; }
+fn unsigned_amount_div_by_amount_zero() {
+    let res = sat(1897) / Amount::ZERO;
+    assert!(res.into_result().is_err());
+}
 
 #[test]
 fn signed_amount_div_by_amount() {
-    assert_eq!(ssat(0) / ssat(7), 0);
+    assert_eq!((ssat(0) / ssat(7)).unwrap(), 0);
 
-    assert_eq!(ssat(1897) / ssat(7), 271);
-    assert_eq!(ssat(1897) / ssat(-7), -271);
-    assert_eq!(ssat(-1897) / ssat(7), -271);
-    assert_eq!(ssat(-1897) / ssat(-7), 271);
+    assert_eq!((ssat(1897) / ssat(7)).unwrap(), 271);
+    assert_eq!((ssat(1897) / ssat(-7)).unwrap(), -271);
+    assert_eq!((ssat(-1897) / ssat(7)).unwrap(), -271);
+    assert_eq!((ssat(-1897) / ssat(-7)).unwrap(), 271);
 }
 
 #[test]
-#[should_panic(expected = "attempt to divide by zero")]
-fn signed_amount_div_by_amount_zero() { let _ = ssat(1897) / SignedAmount::ZERO; }
+fn signed_amount_div_by_amount_zero() {
+    let res = ssat(1897) / SignedAmount::ZERO;
+    assert!(res.into_result().is_err());
+}
 
 #[test]
 fn check_const() {