units: Document the NumOpResult type

The `NumOpResult` type is a bit unusual. Add thorough documentation
and example usage to it.
This commit is contained in:
Tobin C. Harding 2025-04-08 13:31:53 +10:00
parent 7d05078b6a
commit c30a504ea6
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
1 changed files with 74 additions and 0 deletions

View File

@ -9,6 +9,74 @@ use NumOpResult as R;
use crate::{Amount, FeeRate, SignedAmount, Weight};
/// Result of a mathematical operation on two numeric types.
///
/// In order to prevent overflow we provide a custom result type that is similar to the normal
/// [`core::result::Result`] but implements mathematical operatons (e.g. [`core::ops::Add`]) so that
/// math operations can be chained ergonomically. This is very similar to how `NaN` works.
///
/// `NumOpResult` is a monadic type that contains `Valid` and `Error` (similar to `Ok` and `Err`).
/// It supports a subset of functions similar to `Result` (e.g. `unwrap`).
///
/// # Examples
///
/// The `NumOpResult` type provides protection against overflow and div-by-zero.
///
/// ## Overflow protection
///
/// ```
/// # use bitcoin_units::{amount, Amount};
/// // Example UTXO value.
/// let a1 = Amount::from_sat(1_000_000)?;
/// // And another value from some other UTXO.
/// let a2 = Amount::from_sat(765_432)?;
/// // Just an example (typically one would calculate fee using weight and fee rate).
/// let fee = Amount::from_sat(1_00)?;
/// // The amount we want to send.
/// let spend = Amount::from_sat(1_200_000)?;
///
/// // We can error if the change calculation overflows.
/// //
/// // For example if the `spend` value comes from the user and the `change` value is later
/// // used then overflow here could be an attack vector.
/// let change = (a1 + a2 - spend - fee).into_result().expect("handle this error");
///
/// // Or if we control all the values and know they are sane we can just `unwrap`.
/// let change = (a1 + a2 - spend - fee).unwrap();
/// // `NumOpResult` also implements `expect`.
/// let change = (a1 + a2 - spend - fee).expect("we know values don't overflow");
/// # Ok::<_, amount::OutOfRangeError>(())
/// ```
///
/// ## Divide-by-zero (overflow in `Div` or `Rem`)
///
/// In some instances one may wish to differentiate div-by-zero from overflow.
///
/// ```
/// # use bitcoin_units::{amount, Amount, FeeRate, NumOpResult, NumOpError};
/// // Two amounts that will be added to calculate the max fee.
/// let a = Amount::from_sat(123).expect("valid amount");
/// let b = Amount::from_sat(467).expect("valid amount");
/// // Fee rate for transaction.
/// let fee_rate = FeeRate::from_sat_per_vb(1).unwrap();
///
/// // Somewhat contrived example to show addition operator chained with division.
/// let max_fee = a + b;
/// let _fee = match max_fee / fee_rate {
/// NumOpResult::Valid(fee) => fee,
/// NumOpResult::Error(e) if e.is_div_by_zero() => {
/// // Do something when div by zero.
/// return Err(e);
/// },
/// NumOpResult::Error(e) => {
/// // We separate div-by-zero from overflow in case it needs to be handled separately.
/// //
/// // This branch could be hit since `max_fee` came from some previous calculation. And if
/// // an input to that calculation was from the user then overflow could be an attack vector.
/// return Err(e);
/// }
/// };
/// # Ok::<_, NumOpError>(())
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[must_use]
pub enum NumOpResult<T> {
@ -136,6 +204,12 @@ impl NumOpError {
/// Creates a [`NumOpError`] caused by `op`.
pub fn while_doing(op: MathOp) -> Self { NumOpError(op) }
/// Returns `true` if this operation error'ed due to overflow.
pub fn is_overflow(self) -> bool { self.0.is_overflow() }
/// Returns `true` if this operation error'ed due to division by zero.
pub fn is_div_by_zero(self) -> bool { self.0.is_div_by_zero() }
/// Returns the [`MathOp`] that caused this error.
pub fn operation(self) -> MathOp { self.0 }
}