1557 lines
57 KiB
Rust
1557 lines
57 KiB
Rust
// SPDX-License-Identifier: CC0-1.0
|
|
|
|
//! Unit tests for the `amount` module.
|
|
|
|
#[cfg(feature = "alloc")]
|
|
use alloc::format;
|
|
#[cfg(feature = "alloc")]
|
|
use alloc::string::{String, ToString};
|
|
use core::num::{NonZeroI64, NonZeroU64};
|
|
#[cfg(feature = "std")]
|
|
use std::panic;
|
|
|
|
#[cfg(feature = "serde")]
|
|
use ::serde::{Deserialize, Serialize};
|
|
|
|
use super::*;
|
|
#[cfg(feature = "alloc")]
|
|
use crate::{FeeRate, Weight};
|
|
use crate::{MathOp, NumOpResult};
|
|
|
|
#[track_caller]
|
|
fn sat(sat: u64) -> Amount { Amount::from_sat(sat).unwrap() }
|
|
|
|
#[track_caller]
|
|
fn ssat(ssat: i64) -> SignedAmount { SignedAmount::from_sat(ssat).unwrap() }
|
|
|
|
#[track_caller]
|
|
fn res(n_sat: u64) -> NumOpResult<Amount> { NumOpResult::from(sat(n_sat)) }
|
|
|
|
#[track_caller]
|
|
fn sres(n_sat: i64) -> NumOpResult<SignedAmount> { NumOpResult::from(ssat(n_sat)) }
|
|
|
|
#[test]
|
|
fn sanity_check() {
|
|
assert_eq!(ssat(-100).abs(), ssat(100));
|
|
assert_eq!(ssat(-100).signum(), -1);
|
|
assert_eq!(ssat(0).signum(), 0);
|
|
assert_eq!(ssat(100).signum(), 1);
|
|
assert_eq!(SignedAmount::from(sat(100)), ssat(100));
|
|
assert!(!ssat(-100).is_positive());
|
|
assert!(ssat(100).is_positive());
|
|
|
|
#[cfg(feature = "alloc")]
|
|
{
|
|
assert_eq!(Amount::from_float_in(0_f64, Denomination::Bitcoin).unwrap(), sat(0));
|
|
assert_eq!(Amount::from_float_in(2_f64, Denomination::Bitcoin).unwrap(), sat(200_000_000));
|
|
assert!(Amount::from_float_in(-100_f64, Denomination::Bitcoin).is_err());
|
|
}
|
|
|
|
let result = NumOpResult::Valid(sat(123));
|
|
assert_eq!(Some(sat(123)), result.ok());
|
|
assert!(result.is_valid());
|
|
assert!(!result.is_error());
|
|
}
|
|
|
|
#[test]
|
|
fn check_if_num_is_too_precise() {
|
|
assert_eq!(is_too_precise("1234", 3).unwrap(), 3);
|
|
assert_eq!(is_too_precise("1234.1234", 3).unwrap(), 3);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "alloc")]
|
|
fn from_str_zero() {
|
|
let denoms = ["BTC", "cBTC", "mBTC", "uBTC", "bits", "sats"];
|
|
for denom in denoms {
|
|
for v in &["0", "000"] {
|
|
let s = format!("{} {}", v, denom);
|
|
match s.parse::<Amount>() {
|
|
Err(e) => panic!("failed to crate amount from {}: {:?}", s, e),
|
|
Ok(amount) => assert_eq!(amount, Amount::ZERO),
|
|
}
|
|
}
|
|
|
|
let s = format!("-0 {}", denom);
|
|
match s.parse::<Amount>() {
|
|
Err(e) => assert_eq!(
|
|
e,
|
|
ParseError(ParseErrorInner::Amount(ParseAmountError(
|
|
ParseAmountErrorInner::OutOfRange(OutOfRangeError::negative())
|
|
)))
|
|
),
|
|
Ok(_) => panic!("unsigned amount from {}", s),
|
|
}
|
|
match s.parse::<SignedAmount>() {
|
|
Err(e) => panic!("failed to crate amount from {}: {:?}", s, e),
|
|
Ok(amount) => assert_eq!(amount, SignedAmount::ZERO),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn from_str_zero_without_denomination() {
|
|
let _a = Amount::from_str("0").unwrap();
|
|
let _a = Amount::from_str("0.0").unwrap();
|
|
let _a = Amount::from_str("00.0").unwrap();
|
|
|
|
assert!(Amount::from_str("-0").is_err());
|
|
assert!(Amount::from_str("-0.0").is_err());
|
|
assert!(Amount::from_str("-00.0").is_err());
|
|
|
|
let _a = SignedAmount::from_str("-0").unwrap();
|
|
let _a = SignedAmount::from_str("-0.0").unwrap();
|
|
let _a = SignedAmount::from_str("-00.0").unwrap();
|
|
|
|
let _a = SignedAmount::from_str("0").unwrap();
|
|
let _a = SignedAmount::from_str("0.0").unwrap();
|
|
let _a = SignedAmount::from_str("00.0").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn from_int_btc() {
|
|
let amt = Amount::from_btc_u16(2);
|
|
assert_eq!(sat(200_000_000), amt);
|
|
let amt = Amount::from_int_btc(2_u16);
|
|
assert_eq!(sat(200_000_000), amt);
|
|
|
|
let amt = SignedAmount::from_btc_i16(-2);
|
|
assert_eq!(ssat(-200_000_000), amt);
|
|
let amt = SignedAmount::from_int_btc(-2_i16);
|
|
assert_eq!(ssat(-200_000_000), amt);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "alloc")]
|
|
fn display_display_struct() {
|
|
let display_fixed_btc = Display {
|
|
sats_abs: 100_000_000,
|
|
is_negative: false,
|
|
style: DisplayStyle::FixedDenomination {
|
|
denomination: Denomination::Bitcoin,
|
|
show_denomination: true,
|
|
},
|
|
};
|
|
assert_eq!(format!("{}", display_fixed_btc), "1 BTC");
|
|
|
|
let display_fixed_sat = Display {
|
|
sats_abs: 1,
|
|
is_negative: false,
|
|
style: DisplayStyle::FixedDenomination {
|
|
denomination: Denomination::Satoshi,
|
|
show_denomination: true,
|
|
},
|
|
};
|
|
assert_eq!(format!("{}", display_fixed_sat), "1 satoshi");
|
|
|
|
let display_dynamic_btc = Display {
|
|
sats_abs: 100_000_000,
|
|
is_negative: false,
|
|
style: DisplayStyle::DynamicDenomination,
|
|
};
|
|
assert_eq!(format!("{}", display_dynamic_btc), "1 BTC");
|
|
|
|
let display_dynamic_sat = Display {
|
|
sats_abs: 99_999_999,
|
|
is_negative: false,
|
|
style: DisplayStyle::DynamicDenomination,
|
|
};
|
|
assert_eq!(format!("{}", display_dynamic_sat), "99999999 satoshi");
|
|
|
|
let display_negative_btc = Display {
|
|
sats_abs: 100_000_000,
|
|
is_negative: true,
|
|
style: DisplayStyle::DynamicDenomination,
|
|
};
|
|
assert_eq!(format!("{}", display_negative_btc), "-1 BTC");
|
|
|
|
let display_negative_sat = Display {
|
|
sats_abs: 99_999_999,
|
|
is_negative: true,
|
|
style: DisplayStyle::DynamicDenomination,
|
|
};
|
|
assert_eq!(format!("{}", display_negative_sat), "-99999999 satoshi");
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "alloc")]
|
|
fn display_math_op() {
|
|
let cases = [
|
|
(MathOp::Add, "add"),
|
|
(MathOp::Sub, "sub"),
|
|
(MathOp::Mul, "mul"),
|
|
(MathOp::Div, "div"),
|
|
(MathOp::Rem, "rem"),
|
|
(MathOp::Neg, "neg"),
|
|
];
|
|
|
|
for (op, expected) in cases {
|
|
assert_eq!(format!("{}", op), expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn amount_try_from_signed_amount() {
|
|
let sa_positive = ssat(123);
|
|
let ua_positive = Amount::try_from(sa_positive).unwrap();
|
|
assert_eq!(ua_positive, sat(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 op_result_sat = |a| NumOpResult::Valid(sat(a));
|
|
let op_result_ssat = |a| NumOpResult::Valid(ssat(a));
|
|
|
|
assert_eq!(sat(14) * 3, op_result_sat(42));
|
|
assert_eq!(sat(14) / 2, op_result_sat(7));
|
|
assert_eq!(sat(14) % 3, op_result_sat(2));
|
|
assert_eq!(ssat(-14) * 3, op_result_ssat(-42));
|
|
assert_eq!(ssat(-14) / 2, op_result_ssat(-7));
|
|
assert_eq!(ssat(-14) % 3, op_result_ssat(-2));
|
|
}
|
|
|
|
#[test]
|
|
fn neg() {
|
|
let amount = -ssat(2);
|
|
assert_eq!(amount.to_sat(), -2);
|
|
}
|
|
|
|
#[test]
|
|
fn add() {
|
|
assert!(sat(0) + sat(0) == sat(0).into());
|
|
assert!(sat(127) + sat(179) == sat(306).into());
|
|
|
|
assert!(ssat(0) + ssat(0) == ssat(0).into());
|
|
assert!(ssat(127) + ssat(179) == ssat(306).into());
|
|
assert!(ssat(-127) + ssat(179) == ssat(52).into());
|
|
assert!(ssat(127) + ssat(-179) == ssat(-52).into());
|
|
assert!(ssat(-127) + ssat(-179) == ssat(-306).into());
|
|
}
|
|
|
|
#[test]
|
|
fn sub() {
|
|
assert!(sat(0) - sat(0) == sat(0).into());
|
|
assert!(sat(179) - sat(127) == sat(52).into());
|
|
assert!((sat(127) - sat(179)).is_error());
|
|
|
|
assert!(ssat(0) - ssat(0) == ssat(0).into());
|
|
assert!(ssat(127) - ssat(179) == ssat(-52).into());
|
|
assert!(ssat(-127) - ssat(179) == ssat(-306).into());
|
|
assert!(ssat(127) - ssat(-179) == ssat(306).into());
|
|
assert!(ssat(-127) - ssat(-179) == ssat(52).into());
|
|
}
|
|
|
|
#[test]
|
|
fn checked_arithmetic() {
|
|
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);
|
|
assert_eq!(Amount::MIN.checked_sub(sat(1)), None);
|
|
|
|
assert_eq!(sat(5).checked_div(2), Some(sat(2))); // integer division
|
|
assert_eq!(ssat(-6).checked_div(2), Some(ssat(-3)));
|
|
}
|
|
|
|
#[test]
|
|
fn positive_sub() {
|
|
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());
|
|
assert!(ssat(10).positive_sub(ssat(11)).is_none());
|
|
}
|
|
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
fn amount_checked_div_by_weight_ceil() {
|
|
let weight = Weight::from_kwu(1).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 = 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));
|
|
|
|
let fee_rate = Amount::ONE_SAT.checked_div_by_weight_ceil(Weight::ZERO);
|
|
assert!(fee_rate.is_none());
|
|
}
|
|
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
fn amount_checked_div_by_weight_floor() {
|
|
let weight = Weight::from_kwu(1).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 = 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));
|
|
|
|
let fee_rate = Amount::ONE_SAT.checked_div_by_weight_floor(Weight::ZERO);
|
|
assert!(fee_rate.is_none());
|
|
}
|
|
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
fn amount_checked_div_by_fee_rate() {
|
|
let amount = sat(1000);
|
|
let fee_rate = FeeRate::from_sat_per_kwu(2);
|
|
|
|
// Test floor division
|
|
let weight = amount.checked_div_by_fee_rate_floor(fee_rate).unwrap();
|
|
// 1000 sats / (2 sats/kwu) = 500,000 wu
|
|
assert_eq!(weight, Weight::from_wu(500_000));
|
|
|
|
// Test ceiling division
|
|
let weight = amount.checked_div_by_fee_rate_ceil(fee_rate).unwrap();
|
|
assert_eq!(weight, Weight::from_wu(500_000)); // Same result for exact division
|
|
|
|
// Test truncation behavior
|
|
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();
|
|
assert_eq!(floor_weight, Weight::from_wu(333_333));
|
|
assert_eq!(ceil_weight, Weight::from_wu(333_334));
|
|
|
|
// Test division by zero
|
|
let zero_fee_rate = FeeRate::from_sat_per_kwu(0);
|
|
assert!(amount.checked_div_by_fee_rate_floor(zero_fee_rate).is_none());
|
|
assert!(amount.checked_div_by_fee_rate_ceil(zero_fee_rate).is_none());
|
|
|
|
// Test with maximum amount
|
|
let max_amount = Amount::MAX;
|
|
let small_fee_rate = FeeRate::from_sat_per_kwu(1);
|
|
let weight = max_amount.checked_div_by_fee_rate_floor(small_fee_rate).unwrap();
|
|
// 21_000_000_0000_0000 sats / (1 sat/kwu) = 2_100_000_000_000_000_000 wu
|
|
assert_eq!(weight, Weight::from_wu(2_100_000_000_000_000_000));
|
|
}
|
|
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
fn floating_point() {
|
|
use super::Denomination as D;
|
|
let f = Amount::from_float_in;
|
|
let sf = SignedAmount::from_float_in;
|
|
|
|
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)));
|
|
assert_eq!(f(11.22, D::Bit), Ok(sat(1122)));
|
|
assert_eq!(f(0.000_123_4, D::Bitcoin), Ok(sat(12_340)));
|
|
assert_eq!(sf(-0.000_123_45, D::Bitcoin), Ok(ssat(-12_345)));
|
|
|
|
assert_eq!(f(11.22, D::Satoshi), Err(TooPreciseError { position: 3 }.into()));
|
|
assert_eq!(f(42.123_456_781, D::Bitcoin), Err(TooPreciseError { position: 11 }.into()));
|
|
assert_eq!(sf(-184_467_440_738.0, D::Bitcoin), Err(OutOfRangeError::too_small().into()));
|
|
assert_eq!(
|
|
f(18_446_744_073_709_551_617.0, D::Satoshi),
|
|
Err(OutOfRangeError::too_big(false).into())
|
|
);
|
|
|
|
assert_eq!(
|
|
f(Amount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi),
|
|
Err(OutOfRangeError::too_big(false).into())
|
|
);
|
|
|
|
assert_eq!(
|
|
sf(SignedAmount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi),
|
|
Err(OutOfRangeError::too_big(true).into())
|
|
);
|
|
|
|
let btc = move |f| SignedAmount::from_btc(f).unwrap();
|
|
assert_eq!(btc(2.5).to_float_in(D::Bitcoin), 2.5);
|
|
assert_eq!(btc(-2.5).to_float_in(D::CentiBitcoin), -250.0);
|
|
assert_eq!(btc(-2.5).to_float_in(D::MilliBitcoin), -2500.0);
|
|
assert_eq!(btc(2.5).to_float_in(D::Satoshi), 250_000_000.0);
|
|
|
|
let btc = move |f| Amount::from_btc(f).unwrap();
|
|
assert_eq!(&btc(0.0012).to_float_in(D::Bitcoin).to_string(), "0.0012");
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
|
|
fn parsing() {
|
|
use super::ParseAmountError as E;
|
|
|
|
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", 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("-", den_btc),
|
|
Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign }))
|
|
);
|
|
assert_eq!(
|
|
p("-1.0x", den_btc),
|
|
Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 4 }))
|
|
);
|
|
assert_eq!(
|
|
p("0.0 ", den_btc),
|
|
Err(E::from(InvalidCharacterError { invalid_char: ' ', position: 3 }))
|
|
);
|
|
assert_eq!(
|
|
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, 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(), 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!(
|
|
p("100000000000000.0000000000000000000000000000000000", Denomination::Bitcoin),
|
|
Err(OutOfRangeError::too_big(false).into())
|
|
);
|
|
// more than 50 chars.
|
|
assert_eq!(
|
|
p("100000000000000.00000000000000000000000000000000000", Denomination::Bitcoin),
|
|
Err(E(ParseAmountErrorInner::InputTooLarge(InputTooLargeError { len: 51 })))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "alloc")]
|
|
fn to_string() {
|
|
use super::Denomination as D;
|
|
|
|
assert_eq!(Amount::ONE_BTC.to_string_in(D::Bitcoin), "1");
|
|
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!(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!(ssat(-42).to_string_with_denomination(D::Bitcoin), "-0.00000042 BTC");
|
|
}
|
|
|
|
// May help identify a problem sooner
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
fn test_repeat_char() {
|
|
let mut buf = String::new();
|
|
repeat_char(&mut buf, '0', 0).unwrap();
|
|
assert_eq!(buf.len(), 0);
|
|
repeat_char(&mut buf, '0', 42).unwrap();
|
|
assert_eq!(buf.len(), 42);
|
|
assert!(buf.chars().all(|c| c == '0'));
|
|
}
|
|
|
|
// Creates individual test functions to make it easier to find which check failed.
|
|
macro_rules! check_format_non_negative {
|
|
($denom:ident; $($test_name:ident, $val:literal, $format_string:literal, $expected:literal);* $(;)?) => {
|
|
$(
|
|
#[test]
|
|
#[cfg(feature = "alloc")]
|
|
fn $test_name() {
|
|
assert_eq!(format!($format_string, sat($val).display_in(Denomination::$denom)), $expected);
|
|
assert_eq!(format!($format_string, ssat($val as i64).display_in(Denomination::$denom)), $expected);
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
|
|
macro_rules! check_format_non_negative_show_denom {
|
|
($denom:ident, $denom_suffix:literal; $($test_name:ident, $val:literal, $format_string:literal, $expected:literal);* $(;)?) => {
|
|
$(
|
|
#[test]
|
|
#[cfg(feature = "alloc")]
|
|
fn $test_name() {
|
|
assert_eq!(format!($format_string, sat($val).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix));
|
|
assert_eq!(format!($format_string, ssat($val as i64).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix));
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
|
|
check_format_non_negative! {
|
|
Satoshi;
|
|
sat_check_fmt_non_negative_0, 0, "{}", "0";
|
|
sat_check_fmt_non_negative_1, 0, "{:2}", " 0";
|
|
sat_check_fmt_non_negative_2, 0, "{:02}", "00";
|
|
sat_check_fmt_non_negative_3, 0, "{:.1}", "0.0";
|
|
sat_check_fmt_non_negative_4, 0, "{:4.1}", " 0.0";
|
|
sat_check_fmt_non_negative_5, 0, "{:04.1}", "00.0";
|
|
sat_check_fmt_non_negative_6, 1, "{}", "1";
|
|
sat_check_fmt_non_negative_7, 1, "{:2}", " 1";
|
|
sat_check_fmt_non_negative_8, 1, "{:02}", "01";
|
|
sat_check_fmt_non_negative_9, 1, "{:.1}", "1.0";
|
|
sat_check_fmt_non_negative_10, 1, "{:4.1}", " 1.0";
|
|
sat_check_fmt_non_negative_11, 1, "{:04.1}", "01.0";
|
|
sat_check_fmt_non_negative_12, 10, "{}", "10";
|
|
sat_check_fmt_non_negative_13, 10, "{:2}", "10";
|
|
sat_check_fmt_non_negative_14, 10, "{:02}", "10";
|
|
sat_check_fmt_non_negative_15, 10, "{:3}", " 10";
|
|
sat_check_fmt_non_negative_16, 10, "{:03}", "010";
|
|
sat_check_fmt_non_negative_17, 10, "{:.1}", "10.0";
|
|
sat_check_fmt_non_negative_18, 10, "{:5.1}", " 10.0";
|
|
sat_check_fmt_non_negative_19, 10, "{:05.1}", "010.0";
|
|
sat_check_fmt_non_negative_20, 1, "{:<2}", "1 ";
|
|
sat_check_fmt_non_negative_21, 1, "{:<02}", "01";
|
|
sat_check_fmt_non_negative_22, 1, "{:<3.1}", "1.0";
|
|
sat_check_fmt_non_negative_23, 1, "{:<4.1}", "1.0 ";
|
|
}
|
|
|
|
check_format_non_negative_show_denom! {
|
|
Satoshi, " satoshi";
|
|
sat_check_fmt_non_negative_show_denom_0, 0, "{}", "0";
|
|
sat_check_fmt_non_negative_show_denom_1, 0, "{:2}", "0";
|
|
sat_check_fmt_non_negative_show_denom_2, 0, "{:02}", "0";
|
|
sat_check_fmt_non_negative_show_denom_3, 0, "{:9}", "0";
|
|
sat_check_fmt_non_negative_show_denom_4, 0, "{:09}", "0";
|
|
sat_check_fmt_non_negative_show_denom_5, 0, "{:10}", " 0";
|
|
sat_check_fmt_non_negative_show_denom_6, 0, "{:010}", "00";
|
|
sat_check_fmt_non_negative_show_denom_7, 0, "{:.1}", "0.0";
|
|
sat_check_fmt_non_negative_show_denom_8, 0, "{:11.1}", "0.0";
|
|
sat_check_fmt_non_negative_show_denom_9, 0, "{:011.1}", "0.0";
|
|
sat_check_fmt_non_negative_show_denom_10, 0, "{:12.1}", " 0.0";
|
|
sat_check_fmt_non_negative_show_denom_11, 0, "{:012.1}", "00.0";
|
|
sat_check_fmt_non_negative_show_denom_12, 1, "{}", "1";
|
|
sat_check_fmt_non_negative_show_denom_13, 1, "{:10}", " 1";
|
|
sat_check_fmt_non_negative_show_denom_14, 1, "{:010}", "01";
|
|
sat_check_fmt_non_negative_show_denom_15, 1, "{:.1}", "1.0";
|
|
sat_check_fmt_non_negative_show_denom_16, 1, "{:12.1}", " 1.0";
|
|
sat_check_fmt_non_negative_show_denom_17, 1, "{:012.1}", "01.0";
|
|
sat_check_fmt_non_negative_show_denom_18, 10, "{}", "10";
|
|
sat_check_fmt_non_negative_show_denom_19, 10, "{:10}", "10";
|
|
sat_check_fmt_non_negative_show_denom_20, 10, "{:010}", "10";
|
|
sat_check_fmt_non_negative_show_denom_21, 10, "{:11}", " 10";
|
|
sat_check_fmt_non_negative_show_denom_22, 10, "{:011}", "010";
|
|
}
|
|
|
|
check_format_non_negative! {
|
|
Bitcoin;
|
|
btc_check_fmt_non_negative_0, 0, "{}", "0";
|
|
btc_check_fmt_non_negative_1, 0, "{:2}", " 0";
|
|
btc_check_fmt_non_negative_2, 0, "{:02}", "00";
|
|
btc_check_fmt_non_negative_3, 0, "{:.1}", "0.0";
|
|
btc_check_fmt_non_negative_4, 0, "{:4.1}", " 0.0";
|
|
btc_check_fmt_non_negative_5, 0, "{:04.1}", "00.0";
|
|
btc_check_fmt_non_negative_6, 1, "{}", "0.00000001";
|
|
btc_check_fmt_non_negative_7, 1, "{:2}", "0.00000001";
|
|
btc_check_fmt_non_negative_8, 1, "{:02}", "0.00000001";
|
|
btc_check_fmt_non_negative_9, 1, "{:.1}", "0.0";
|
|
btc_check_fmt_non_negative_10, 1, "{:11}", " 0.00000001";
|
|
btc_check_fmt_non_negative_11, 1, "{:11.1}", " 0.0";
|
|
btc_check_fmt_non_negative_12, 1, "{:011.1}", "000000000.0";
|
|
btc_check_fmt_non_negative_13, 1, "{:.9}", "0.000000010";
|
|
btc_check_fmt_non_negative_14, 1, "{:11.9}", "0.000000010";
|
|
btc_check_fmt_non_negative_15, 1, "{:011.9}", "0.000000010";
|
|
btc_check_fmt_non_negative_16, 1, "{:12.9}", " 0.000000010";
|
|
btc_check_fmt_non_negative_17, 1, "{:012.9}", "00.000000010";
|
|
btc_check_fmt_non_negative_18, 100_000_000, "{}", "1";
|
|
btc_check_fmt_non_negative_19, 100_000_000, "{:2}", " 1";
|
|
btc_check_fmt_non_negative_20, 100_000_000, "{:02}", "01";
|
|
btc_check_fmt_non_negative_21, 100_000_000, "{:.1}", "1.0";
|
|
btc_check_fmt_non_negative_22, 100_000_000, "{:4.1}", " 1.0";
|
|
btc_check_fmt_non_negative_23, 100_000_000, "{:04.1}", "01.0";
|
|
btc_check_fmt_non_negative_24, 110_000_000, "{}", "1.1";
|
|
btc_check_fmt_non_negative_25, 100_000_001, "{}", "1.00000001";
|
|
btc_check_fmt_non_negative_26, 100_000_001, "{:1}", "1.00000001";
|
|
btc_check_fmt_non_negative_27, 100_000_001, "{:.1}", "1.0";
|
|
btc_check_fmt_non_negative_28, 100_000_001, "{:10}", "1.00000001";
|
|
btc_check_fmt_non_negative_29, 100_000_001, "{:11}", " 1.00000001";
|
|
btc_check_fmt_non_negative_30, 100_000_001, "{:011}", "01.00000001";
|
|
btc_check_fmt_non_negative_31, 100_000_001, "{:.8}", "1.00000001";
|
|
btc_check_fmt_non_negative_32, 100_000_001, "{:.9}", "1.000000010";
|
|
btc_check_fmt_non_negative_33, 100_000_001, "{:11.9}", "1.000000010";
|
|
btc_check_fmt_non_negative_34, 100_000_001, "{:12.9}", " 1.000000010";
|
|
btc_check_fmt_non_negative_35, 100_000_001, "{:012.9}", "01.000000010";
|
|
btc_check_fmt_non_negative_36, 100_000_001, "{:+011.8}", "+1.00000001";
|
|
btc_check_fmt_non_negative_37, 100_000_001, "{:+12.8}", " +1.00000001";
|
|
btc_check_fmt_non_negative_38, 100_000_001, "{:+012.8}", "+01.00000001";
|
|
btc_check_fmt_non_negative_39, 100_000_001, "{:+12.9}", "+1.000000010";
|
|
btc_check_fmt_non_negative_40, 100_000_001, "{:+012.9}", "+1.000000010";
|
|
btc_check_fmt_non_negative_41, 100_000_001, "{:+13.9}", " +1.000000010";
|
|
btc_check_fmt_non_negative_42, 100_000_001, "{:+013.9}", "+01.000000010";
|
|
btc_check_fmt_non_negative_43, 100_000_001, "{:<10}", "1.00000001";
|
|
btc_check_fmt_non_negative_44, 100_000_001, "{:<11}", "1.00000001 ";
|
|
btc_check_fmt_non_negative_45, 100_000_001, "{:<011}", "01.00000001";
|
|
btc_check_fmt_non_negative_46, 100_000_001, "{:<11.9}", "1.000000010";
|
|
btc_check_fmt_non_negative_47, 100_000_001, "{:<12.9}", "1.000000010 ";
|
|
btc_check_fmt_non_negative_48, 100_000_001, "{:<12}", "1.00000001 ";
|
|
btc_check_fmt_non_negative_49, 100_000_001, "{:^11}", "1.00000001 ";
|
|
btc_check_fmt_non_negative_50, 100_000_001, "{:^11.9}", "1.000000010";
|
|
btc_check_fmt_non_negative_51, 100_000_001, "{:^12.9}", "1.000000010 ";
|
|
btc_check_fmt_non_negative_52, 100_000_001, "{:^12}", " 1.00000001 ";
|
|
btc_check_fmt_non_negative_53, 100_000_001, "{:^12.9}", "1.000000010 ";
|
|
btc_check_fmt_non_negative_54, 100_000_001, "{:^13.9}", " 1.000000010 ";
|
|
}
|
|
|
|
check_format_non_negative_show_denom! {
|
|
Bitcoin, " BTC";
|
|
btc_check_fmt_non_negative_show_denom_0, 1, "{:14.1}", " 0.0";
|
|
btc_check_fmt_non_negative_show_denom_1, 1, "{:14.8}", "0.00000001";
|
|
btc_check_fmt_non_negative_show_denom_2, 1, "{:15}", " 0.00000001";
|
|
btc_check_fmt_non_negative_show_denom_3, 1, "{:015}", "00.00000001";
|
|
btc_check_fmt_non_negative_show_denom_4, 1, "{:.9}", "0.000000010";
|
|
btc_check_fmt_non_negative_show_denom_5, 1, "{:15.9}", "0.000000010";
|
|
btc_check_fmt_non_negative_show_denom_6, 1, "{:16.9}", " 0.000000010";
|
|
btc_check_fmt_non_negative_show_denom_7, 1, "{:016.9}", "00.000000010";
|
|
}
|
|
|
|
check_format_non_negative_show_denom! {
|
|
Bitcoin, " BTC ";
|
|
btc_check_fmt_non_negative_show_denom_align_0, 1, "{:<15}", "0.00000001";
|
|
btc_check_fmt_non_negative_show_denom_align_1, 1, "{:^15}", "0.00000001";
|
|
btc_check_fmt_non_negative_show_denom_align_2, 1, "{:^16}", " 0.00000001";
|
|
}
|
|
|
|
#[test]
|
|
fn unsigned_signed_conversion() {
|
|
let max_sats: u64 = Amount::MAX.to_sat();
|
|
|
|
assert_eq!(sat(max_sats).to_signed(), ssat(max_sats as i64));
|
|
assert_eq!(ssat(max_sats as i64).to_unsigned(), Ok(sat(max_sats)));
|
|
assert_eq!(ssat(max_sats as i64).to_unsigned().unwrap().to_signed(), ssat(max_sats as i64));
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
|
|
#[allow(clippy::items_after_statements)] // Define functions where we use them.
|
|
fn from_str() {
|
|
use ParseDenominationError::*;
|
|
|
|
use super::ParseAmountError as E;
|
|
|
|
assert_eq!(
|
|
"x BTC".parse::<Amount>(),
|
|
Err(InvalidCharacterError { invalid_char: 'x', position: 0 }.into())
|
|
);
|
|
assert_eq!(
|
|
"xBTC".parse::<Amount>(),
|
|
Err(Unknown(UnknownDenominationError("xBTC".into())).into()),
|
|
);
|
|
assert_eq!(
|
|
"5 BTC BTC".parse::<Amount>(),
|
|
Err(Unknown(UnknownDenominationError("BTC BTC".into())).into()),
|
|
);
|
|
assert_eq!(
|
|
"5BTC BTC".parse::<Amount>(),
|
|
Err(E::from(InvalidCharacterError { invalid_char: 'B', position: 1 }).into())
|
|
);
|
|
assert_eq!(
|
|
"5 5 BTC".parse::<Amount>(),
|
|
Err(Unknown(UnknownDenominationError("5 BTC".into())).into()),
|
|
);
|
|
|
|
#[track_caller]
|
|
fn ok_case(s: &str, expected: Amount) {
|
|
assert_eq!(s.parse::<Amount>().unwrap(), expected);
|
|
assert_eq!(s.replace(' ', "").parse::<Amount>().unwrap(), expected);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn case(s: &str, expected: Result<Amount, impl Into<ParseError>>) {
|
|
let expected = expected.map_err(Into::into);
|
|
assert_eq!(s.parse::<Amount>(), expected);
|
|
assert_eq!(s.replace(' ', "").parse::<Amount>(), expected);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn ok_scase(s: &str, expected: SignedAmount) {
|
|
assert_eq!(s.parse::<SignedAmount>().unwrap(), expected);
|
|
assert_eq!(s.replace(' ', "").parse::<SignedAmount>().unwrap(), expected);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn scase(s: &str, expected: Result<SignedAmount, impl Into<ParseError>>) {
|
|
let expected = expected.map_err(Into::into);
|
|
assert_eq!(s.parse::<SignedAmount>(), expected);
|
|
assert_eq!(s.replace(' ', "").parse::<SignedAmount>(), expected);
|
|
}
|
|
|
|
case("5 BCH", Err(Unknown(UnknownDenominationError("BCH".into()))));
|
|
|
|
case("-1 BTC", Err(OutOfRangeError::negative()));
|
|
case("-0.0 BTC", Err(OutOfRangeError::negative()));
|
|
case("0.123456789 BTC", Err(TooPreciseError { position: 10 }));
|
|
scase("-0.1 satoshi", Err(TooPreciseError { position: 3 }));
|
|
case("0.123456 mBTC", Err(TooPreciseError { position: 7 }));
|
|
scase("-1.001 bits", Err(TooPreciseError { position: 5 }));
|
|
scase("-21000001 BTC", Err(OutOfRangeError::too_small()));
|
|
scase("21000001 BTC", Err(OutOfRangeError::too_big(true)));
|
|
scase("-2100000000000001 SAT", Err(OutOfRangeError::too_small()));
|
|
scase("2100000000000001 SAT", Err(OutOfRangeError::too_big(true)));
|
|
case("21000001 BTC", Err(OutOfRangeError::too_big(false)));
|
|
case("18446744073709551616 sat", Err(OutOfRangeError::too_big(false)));
|
|
|
|
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);
|
|
}
|
|
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
|
|
fn to_from_string_in() {
|
|
use super::Denomination as D;
|
|
let ua_str = Amount::from_str_in;
|
|
let ua_sat = |n| Amount::from_sat(n).unwrap();
|
|
let sa_str = SignedAmount::from_str_in;
|
|
let sa_sat = |n| SignedAmount::from_sat(n).unwrap();
|
|
|
|
assert_eq!("0.5", ua_sat(50).to_string_in(D::Bit));
|
|
assert_eq!("-0.5", sa_sat(-50).to_string_in(D::Bit));
|
|
assert_eq!("0.00253583", ua_sat(253_583).to_string_in(D::Bitcoin));
|
|
assert_eq!("-5", sa_sat(-5).to_string_in(D::Satoshi));
|
|
assert_eq!("0.1", ua_sat(100_000_00).to_string_in(D::Bitcoin));
|
|
assert_eq!("-0.1", sa_sat(-100_000_00).to_string_in(D::Bitcoin));
|
|
|
|
assert_eq!("0.253583", ua_sat(253_583).to_string_in(D::CentiBitcoin));
|
|
assert_eq!("-0.253583", sa_sat(-253_583).to_string_in(D::CentiBitcoin));
|
|
assert_eq!("10", ua_sat(100_000_00).to_string_in(D::CentiBitcoin));
|
|
assert_eq!("-10", sa_sat(-100_000_00).to_string_in(D::CentiBitcoin));
|
|
|
|
assert_eq!("2.53583", ua_sat(253_583).to_string_in(D::MilliBitcoin));
|
|
assert_eq!("-2.53583", sa_sat(-253_583).to_string_in(D::MilliBitcoin));
|
|
assert_eq!("100", ua_sat(100_000_00).to_string_in(D::MilliBitcoin));
|
|
assert_eq!("-100", sa_sat(-100_000_00).to_string_in(D::MilliBitcoin));
|
|
|
|
assert_eq!("2535.83", ua_sat(253_583).to_string_in(D::MicroBitcoin));
|
|
assert_eq!("-2535.83", sa_sat(-253_583).to_string_in(D::MicroBitcoin));
|
|
assert_eq!("100000", ua_sat(100_000_00).to_string_in(D::MicroBitcoin));
|
|
assert_eq!("-100000", sa_sat(-100_000_00).to_string_in(D::MicroBitcoin));
|
|
|
|
assert_eq!("0.5", ua_sat(50).to_string_in(D::Bit));
|
|
assert_eq!("100", ua_sat(10_000).to_string_in(D::Bit));
|
|
assert_eq!("-0.5", sa_sat(-50).to_string_in(D::Bit));
|
|
assert_eq!("-100", sa_sat(-10_000).to_string_in(D::Bit));
|
|
|
|
assert_eq!("5", ua_sat(5).to_string_in(D::Satoshi));
|
|
assert_eq!("-5", sa_sat(-5).to_string_in(D::Satoshi));
|
|
|
|
assert_eq!("0.50", format!("{:.2}", ua_sat(50).display_in(D::Bit)));
|
|
assert_eq!("-0.50", format!("{:.2}", sa_sat(-50).display_in(D::Bit)));
|
|
|
|
assert_eq!("0.10000000", format!("{:.8}", ua_sat(100_000_00).display_in(D::Bitcoin)));
|
|
assert_eq!("-100.00", format!("{:.2}", sa_sat(-10_000).display_in(D::Bit)));
|
|
|
|
assert_eq!(ua_str(&ua_sat(500).to_string_in(D::Bitcoin), D::Bitcoin), Ok(ua_sat(500)));
|
|
assert_eq!(ua_str(&ua_sat(1).to_string_in(D::CentiBitcoin), D::CentiBitcoin), Ok(ua_sat(1)));
|
|
assert_eq!(
|
|
ua_str(&ua_sat(1_000_000_000_000).to_string_in(D::MilliBitcoin), D::MilliBitcoin),
|
|
Ok(ua_sat(1_000_000_000_000))
|
|
);
|
|
assert_eq!(ua_str(&ua_sat(1).to_string_in(D::MicroBitcoin), D::MicroBitcoin), Ok(ua_sat(1)));
|
|
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::Bit), D::Bit).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),
|
|
Err(OutOfRangeError::too_big(true).into())
|
|
);
|
|
// Test an overflow bug in `abs()`
|
|
assert_eq!(
|
|
sa_str(&SignedAmount::MIN.to_string_in(D::Satoshi), D::MicroBitcoin),
|
|
Err(OutOfRangeError::too_small().into())
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
fn to_string_with_denomination_from_str_roundtrip() {
|
|
use ParseDenominationError::*;
|
|
|
|
use super::Denomination as D;
|
|
|
|
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));
|
|
assert_eq!(denom(amt, D::MilliBitcoin).parse::<Amount>(), Ok(amt));
|
|
assert_eq!(denom(amt, D::MicroBitcoin).parse::<Amount>(), Ok(amt));
|
|
assert_eq!(denom(amt, D::Bit).parse::<Amount>(), Ok(amt));
|
|
assert_eq!(denom(amt, D::Satoshi).parse::<Amount>(), Ok(amt));
|
|
|
|
assert_eq!(
|
|
"42 satoshi BTC".parse::<Amount>(),
|
|
Err(Unknown(UnknownDenominationError("satoshi BTC".into())).into()),
|
|
);
|
|
assert_eq!(
|
|
"-42 satoshi BTC".parse::<SignedAmount>(),
|
|
Err(Unknown(UnknownDenominationError("satoshi BTC".into())).into()),
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "serde")]
|
|
#[test]
|
|
fn serde_as_sat() {
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct T {
|
|
#[serde(with = "crate::amount::serde::as_sat")]
|
|
pub amt: Amount,
|
|
#[serde(with = "crate::amount::serde::as_sat")]
|
|
pub samt: SignedAmount,
|
|
}
|
|
|
|
serde_test::assert_tokens(
|
|
&T { amt: sat(123_456_789), samt: ssat(-123_456_789) },
|
|
&[
|
|
serde_test::Token::Struct { name: "T", len: 2 },
|
|
serde_test::Token::Str("amt"),
|
|
serde_test::Token::U64(123_456_789),
|
|
serde_test::Token::Str("samt"),
|
|
serde_test::Token::I64(-123_456_789),
|
|
serde_test::Token::StructEnd,
|
|
],
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "serde")]
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
|
|
fn serde_as_btc() {
|
|
use serde_json;
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct T {
|
|
#[serde(with = "crate::amount::serde::as_btc")]
|
|
pub amt: Amount,
|
|
#[serde(with = "crate::amount::serde::as_btc")]
|
|
pub samt: SignedAmount,
|
|
}
|
|
|
|
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}";
|
|
let t: T = serde_json::from_str(json).unwrap();
|
|
assert_eq!(t, orig);
|
|
|
|
let value: serde_json::Value = serde_json::from_str(json).unwrap();
|
|
assert_eq!(t, serde_json::from_value(value).unwrap());
|
|
|
|
// errors
|
|
let t: Result<T, serde_json::Error> =
|
|
serde_json::from_str("{\"amt\": 1000000.000000001, \"samt\": 1}");
|
|
assert!(t.unwrap_err().to_string().contains(
|
|
&ParseAmountError(ParseAmountErrorInner::TooPrecise(TooPreciseError { position: 16 }))
|
|
.to_string()
|
|
));
|
|
let t: Result<T, serde_json::Error> = serde_json::from_str("{\"amt\": -1, \"samt\": 1}");
|
|
assert!(t.unwrap_err().to_string().contains(&OutOfRangeError::negative().to_string()));
|
|
}
|
|
|
|
#[cfg(feature = "serde")]
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
fn serde_as_str() {
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct T {
|
|
#[serde(with = "crate::amount::serde::as_str")]
|
|
pub amt: Amount,
|
|
#[serde(with = "crate::amount::serde::as_str")]
|
|
pub samt: SignedAmount,
|
|
}
|
|
|
|
serde_test::assert_tokens(
|
|
&T { amt: sat(123_456_789), samt: ssat(-123_456_789) },
|
|
&[
|
|
serde_test::Token::Struct { name: "T", len: 2 },
|
|
serde_test::Token::String("amt"),
|
|
serde_test::Token::String("1.23456789"),
|
|
serde_test::Token::String("samt"),
|
|
serde_test::Token::String("-1.23456789"),
|
|
serde_test::Token::StructEnd,
|
|
],
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "serde")]
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
|
|
fn serde_as_btc_opt() {
|
|
use serde_json;
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
|
|
struct T {
|
|
#[serde(default, with = "crate::amount::serde::as_btc::opt")]
|
|
pub amt: Option<Amount>,
|
|
#[serde(default, with = "crate::amount::serde::as_btc::opt")]
|
|
pub samt: Option<SignedAmount>,
|
|
}
|
|
|
|
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
|
|
for s in [&with, &without] {
|
|
let v = serde_json::to_string(s).unwrap();
|
|
let w: T = serde_json::from_str(&v).unwrap();
|
|
assert_eq!(w, *s);
|
|
}
|
|
|
|
let t: T = serde_json::from_str("{\"amt\": 2.5, \"samt\": -2.5}").unwrap();
|
|
assert_eq!(t, with);
|
|
|
|
let t: T = serde_json::from_str("{}").unwrap();
|
|
assert_eq!(t, without);
|
|
|
|
let value_with: serde_json::Value =
|
|
serde_json::from_str("{\"amt\": 2.5, \"samt\": -2.5}").unwrap();
|
|
assert_eq!(with, serde_json::from_value(value_with).unwrap());
|
|
|
|
let value_without: serde_json::Value = serde_json::from_str("{}").unwrap();
|
|
assert_eq!(without, serde_json::from_value(value_without).unwrap());
|
|
}
|
|
|
|
#[cfg(feature = "serde")]
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
|
|
fn serde_as_sat_opt() {
|
|
use serde_json;
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
|
|
struct T {
|
|
#[serde(default, with = "crate::amount::serde::as_sat::opt")]
|
|
pub amt: Option<Amount>,
|
|
#[serde(default, with = "crate::amount::serde::as_sat::opt")]
|
|
pub samt: Option<SignedAmount>,
|
|
}
|
|
|
|
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
|
|
for s in [&with, &without] {
|
|
let v = serde_json::to_string(s).unwrap();
|
|
let w: T = serde_json::from_str(&v).unwrap();
|
|
assert_eq!(w, *s);
|
|
}
|
|
|
|
let t: T = serde_json::from_str("{\"amt\": 250000000, \"samt\": -250000000}").unwrap();
|
|
assert_eq!(t, with);
|
|
|
|
let t: T = serde_json::from_str("{}").unwrap();
|
|
assert_eq!(t, without);
|
|
|
|
let value_with: serde_json::Value =
|
|
serde_json::from_str("{\"amt\": 250000000, \"samt\": -250000000}").unwrap();
|
|
assert_eq!(with, serde_json::from_value(value_with).unwrap());
|
|
|
|
let value_without: serde_json::Value = serde_json::from_str("{}").unwrap();
|
|
assert_eq!(without, serde_json::from_value(value_without).unwrap());
|
|
}
|
|
|
|
#[cfg(feature = "serde")]
|
|
#[cfg(feature = "alloc")]
|
|
#[test]
|
|
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
|
|
fn serde_as_str_opt() {
|
|
use serde_json;
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
|
|
struct T {
|
|
#[serde(default, with = "crate::amount::serde::as_str::opt")]
|
|
pub amt: Option<Amount>,
|
|
#[serde(default, with = "crate::amount::serde::as_str::opt")]
|
|
pub samt: Option<SignedAmount>,
|
|
}
|
|
|
|
let with = T { amt: Some(sat(123_456_789)), samt: Some(ssat(-123_456_789)) };
|
|
let without = T { amt: None, samt: None };
|
|
|
|
// Test Roundtripping
|
|
for s in [&with, &without] {
|
|
let v = serde_json::to_string(s).unwrap();
|
|
let w: T = serde_json::from_str(&v).unwrap();
|
|
assert_eq!(w, *s);
|
|
}
|
|
|
|
let t: T =
|
|
serde_json::from_str("{\"amt\": \"1.23456789\", \"samt\": \"-1.23456789\"}").unwrap();
|
|
assert_eq!(t, with);
|
|
|
|
let t: T = serde_json::from_str("{}").unwrap();
|
|
assert_eq!(t, without);
|
|
|
|
let value_with: serde_json::Value =
|
|
serde_json::from_str("{\"amt\": \"1.23456789\", \"samt\": \"-1.23456789\"}").unwrap();
|
|
assert_eq!(with, serde_json::from_value(value_with).unwrap());
|
|
|
|
let value_without: serde_json::Value = serde_json::from_str("{}").unwrap();
|
|
assert_eq!(without, serde_json::from_value(value_without).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn sum_amounts() {
|
|
assert_eq!([].iter().sum::<NumOpResult<Amount>>(), Amount::ZERO.into());
|
|
assert_eq!([].iter().sum::<NumOpResult<SignedAmount>>(), SignedAmount::ZERO.into());
|
|
|
|
let results =
|
|
[NumOpResult::Valid(sat(42)), NumOpResult::Valid(sat(1337)), NumOpResult::Valid(sat(21))];
|
|
assert_eq!(results.iter().sum::<NumOpResult<Amount>>(), NumOpResult::Valid(sat(1400)));
|
|
|
|
let signed_results = [
|
|
NumOpResult::Valid(ssat(42)),
|
|
NumOpResult::Valid(ssat(1337)),
|
|
NumOpResult::Valid(ssat(21)),
|
|
];
|
|
assert_eq!(
|
|
signed_results.iter().sum::<NumOpResult<SignedAmount>>(),
|
|
NumOpResult::Valid(ssat(1400))
|
|
);
|
|
|
|
let amounts = [sat(42), sat(1337), sat(21)];
|
|
assert_eq!(
|
|
amounts.iter().map(|a| NumOpResult::Valid(*a)).sum::<NumOpResult<Amount>>(),
|
|
sat(1400).into(),
|
|
);
|
|
assert_eq!(
|
|
amounts.into_iter().map(NumOpResult::Valid).sum::<NumOpResult<Amount>>(),
|
|
sat(1400).into(),
|
|
);
|
|
|
|
let amounts = [ssat(-42), ssat(1337), ssat(21)];
|
|
assert_eq!(
|
|
amounts.iter().map(NumOpResult::from).sum::<NumOpResult<SignedAmount>>(),
|
|
ssat(1316).into(),
|
|
);
|
|
assert_eq!(
|
|
amounts.into_iter().map(NumOpResult::from).sum::<NumOpResult<SignedAmount>>(),
|
|
ssat(1316).into()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn checked_sum_amounts() {
|
|
assert_eq!([].into_iter().checked_sum(), Some(Amount::ZERO));
|
|
assert_eq!([].into_iter().checked_sum(), Some(SignedAmount::ZERO));
|
|
|
|
let amounts = [sat(42), sat(1337), sat(21)];
|
|
let sum = amounts.into_iter().checked_sum();
|
|
assert_eq!(sum, Some(sat(1400)));
|
|
|
|
let amounts = [Amount::MAX_MONEY, sat(1337), sat(21)];
|
|
let sum = amounts.into_iter().checked_sum();
|
|
assert_eq!(sum, None);
|
|
|
|
let amounts = [SignedAmount::MIN, ssat(-1), ssat(21)];
|
|
let sum = amounts.into_iter().checked_sum();
|
|
assert_eq!(sum, None);
|
|
|
|
let amounts = [SignedAmount::MAX, ssat(1), ssat(21)];
|
|
let sum = amounts.into_iter().checked_sum();
|
|
assert_eq!(sum, None);
|
|
|
|
let amounts = [ssat(42), ssat(3301), ssat(21)];
|
|
let sum = amounts.into_iter().checked_sum();
|
|
assert_eq!(sum, Some(ssat(3364)));
|
|
}
|
|
|
|
#[test]
|
|
fn denomination_string_acceptable_forms() {
|
|
// Exhaustive list of valid forms.
|
|
let valid = [
|
|
"BTC", "btc", "cBTC", "cbtc", "mBTC", "mbtc", "uBTC", "ubtc", "µBTC", "µbtc", "bit",
|
|
"bits", "BIT", "BITS", "SATOSHI", "satoshi", "SATOSHIS", "satoshis", "SAT", "sat", "SATS",
|
|
"sats",
|
|
];
|
|
for denom in valid {
|
|
assert!(denom.parse::<Denomination>().is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn disallow_confusing_forms() {
|
|
let confusing = ["CBTC", "Cbtc", "MBTC", "Mbtc", "UBTC", "Ubtc"];
|
|
for denom in confusing {
|
|
match denom.parse::<Denomination>() {
|
|
Ok(_) => panic!("from_str should error for {}", denom),
|
|
Err(ParseDenominationError::PossiblyConfusing(_)) => {}
|
|
Err(e) => panic!("unexpected error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn disallow_unknown_denomination() {
|
|
// Non-exhaustive list of unknown forms.
|
|
let unknown = ["NBTC", "ABC", "abc", "mSat", "msat"];
|
|
for denom in unknown {
|
|
match denom.parse::<Denomination>() {
|
|
Ok(_) => panic!("from_str should error for {}", denom),
|
|
Err(ParseDenominationError::Unknown(_)) => (),
|
|
Err(e) => panic!("unexpected error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "alloc")]
|
|
fn trailing_zeros_for_amount() {
|
|
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");
|
|
assert_eq!(format!("{}", sat(1)), "0.00000001 BTC");
|
|
assert_eq!(format!("{}", sat(10)), "0.0000001 BTC");
|
|
assert_eq!(format!("{:.2}", sat(10)), "0.00 BTC");
|
|
assert_eq!(format!("{:.2}", sat(100)), "0.00 BTC");
|
|
assert_eq!(format!("{:.2}", sat(1000)), "0.00 BTC");
|
|
assert_eq!(format!("{:.2}", sat(10_000)), "0.00 BTC");
|
|
assert_eq!(format!("{:.2}", sat(100_000)), "0.00 BTC");
|
|
assert_eq!(format!("{:.2}", sat(1_000_000)), "0.01 BTC");
|
|
assert_eq!(format!("{:.2}", sat(10_000_000)), "0.10 BTC");
|
|
assert_eq!(format!("{:.2}", sat(100_000_000)), "1.00 BTC");
|
|
assert_eq!(format!("{:.2}", sat(500_000)), "0.01 BTC");
|
|
assert_eq!(format!("{:.2}", sat(9_500_000)), "0.10 BTC");
|
|
assert_eq!(format!("{:.2}", sat(99_500_000)), "1.00 BTC");
|
|
assert_eq!(format!("{}", sat(100_000_000)), "1 BTC");
|
|
assert_eq!(format!("{}", sat(40_000_000_000)), "400 BTC");
|
|
assert_eq!(format!("{:.10}", sat(100_000_000)), "1.0000000000 BTC");
|
|
assert_eq!(format!("{}", sat(400_000_000_000_010)), "4000000.0000001 BTC");
|
|
assert_eq!(format!("{}", sat(400_000_000_000_000)), "4000000 BTC");
|
|
}
|
|
|
|
#[test]
|
|
fn add_sub_combos() {
|
|
// Checks lhs op rhs for all reference combos.
|
|
macro_rules! check_ref {
|
|
($($lhs:ident $op:tt $rhs:ident = $ans:ident);* $(;)?) => {
|
|
$(
|
|
assert_eq!($lhs $op $rhs, $ans);
|
|
assert_eq!(&$lhs $op $rhs, $ans);
|
|
assert_eq!($lhs $op &$rhs, $ans);
|
|
assert_eq!(&$lhs $op &$rhs, $ans);
|
|
)*
|
|
}
|
|
}
|
|
|
|
// Checks lhs op rhs for all amount and `NumOpResult` combos.
|
|
macro_rules! check_res {
|
|
($($amount:ident, $op:tt, $lhs:literal, $rhs:literal, $ans:literal);* $(;)?) => {
|
|
$(
|
|
let amt = |sat| $amount::from_sat(sat).unwrap();
|
|
|
|
let sat_lhs = amt($lhs);
|
|
let sat_rhs = amt($rhs);
|
|
|
|
let res_lhs = NumOpResult::from(sat_lhs);
|
|
let res_rhs = NumOpResult::from(sat_rhs);
|
|
|
|
let ans = NumOpResult::from(amt($ans));
|
|
|
|
check_ref! {
|
|
sat_lhs $op sat_rhs = ans;
|
|
sat_lhs $op res_rhs = ans;
|
|
res_lhs $op sat_rhs = ans;
|
|
res_lhs $op res_rhs = ans;
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
|
|
// Checks lhs op rhs for both amount types.
|
|
macro_rules! check_op {
|
|
($($lhs:literal $op:tt $rhs:literal = $ans:literal);* $(;)?) => {
|
|
$(
|
|
check_res!(Amount, $op, $lhs, $rhs, $ans);
|
|
check_res!(SignedAmount, $op, $lhs, $rhs, $ans);
|
|
)*
|
|
}
|
|
}
|
|
|
|
// We do not currently support division involving `NumOpResult` and an amount type.
|
|
check_op! {
|
|
307 + 461 = 768;
|
|
461 - 307 = 154;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unsigned_addition() {
|
|
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)));
|
|
assert_eq!(sat(307) + sat(461), NumOpResult::from(sat(768)));
|
|
assert_eq!(sat(0) + Amount::MAX_MONEY, NumOpResult::from(Amount::MAX_MONEY));
|
|
}
|
|
|
|
#[test]
|
|
fn signed_addition() {
|
|
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)));
|
|
assert_eq!(ssat(307) + ssat(461), NumOpResult::from(ssat(768)));
|
|
assert_eq!(ssat(0) + SignedAmount::MAX_MONEY, NumOpResult::from(SignedAmount::MAX_MONEY));
|
|
|
|
assert_eq!(ssat(0) + ssat(-307), NumOpResult::from(ssat(-307)));
|
|
assert_eq!(ssat(-307) + ssat(0), NumOpResult::from(ssat(-307)));
|
|
assert_eq!(ssat(-307) + ssat(461), NumOpResult::from(ssat(154)));
|
|
assert_eq!(ssat(307) + ssat(-461), NumOpResult::from(ssat(-154)));
|
|
assert_eq!(ssat(-307) + ssat(-461), NumOpResult::from(ssat(-768)));
|
|
assert_eq!(
|
|
SignedAmount::MAX_MONEY + -SignedAmount::MAX_MONEY,
|
|
NumOpResult::from(SignedAmount::ZERO)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unsigned_subtraction() {
|
|
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)));
|
|
}
|
|
|
|
#[test]
|
|
fn signed_subtraction() {
|
|
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)));
|
|
assert_eq!(ssat(307) - ssat(461), NumOpResult::from(ssat(-154)));
|
|
assert_eq!(ssat(0) - SignedAmount::MAX_MONEY, NumOpResult::from(-SignedAmount::MAX_MONEY));
|
|
|
|
assert_eq!(ssat(0) - ssat(-307), NumOpResult::from(ssat(307)));
|
|
assert_eq!(ssat(-307) - ssat(0), NumOpResult::from(ssat(-307)));
|
|
assert_eq!(ssat(-307) - ssat(461), NumOpResult::from(ssat(-768)));
|
|
assert_eq!(ssat(307) - ssat(-461), NumOpResult::from(ssat(768)));
|
|
assert_eq!(ssat(-307) - ssat(-461), NumOpResult::from(ssat(154)));
|
|
}
|
|
|
|
#[test]
|
|
fn op_int_combos() {
|
|
assert_eq!(sat(23) * 31, res(713));
|
|
assert_eq!(ssat(23) * 31, sres(713));
|
|
assert_eq!(res(23) * 31, res(713));
|
|
assert_eq!(sres(23) * 31, sres(713));
|
|
|
|
assert_eq!(31 * sat(23), res(713));
|
|
assert_eq!(31 * ssat(23), sres(713));
|
|
assert_eq!(31 * res(23), res(713));
|
|
assert_eq!(31 * sres(23), sres(713));
|
|
|
|
// No remainder.
|
|
assert_eq!(sat(1897) / 7, res(271));
|
|
assert_eq!(ssat(1897) / 7, sres(271));
|
|
assert_eq!(res(1897) / 7, res(271));
|
|
assert_eq!(sres(1897) / 7, sres(271));
|
|
|
|
// Truncation works as expected.
|
|
assert_eq!(sat(1901) / 7, res(271));
|
|
assert_eq!(ssat(1901) / 7, sres(271));
|
|
assert_eq!(res(1901) / 7, res(271));
|
|
assert_eq!(sres(1901) / 7, sres(271));
|
|
|
|
// No remainder.
|
|
assert_eq!(sat(1897) % 7, res(0));
|
|
assert_eq!(ssat(1897) % 7, sres(0));
|
|
assert_eq!(res(1897) % 7, res(0));
|
|
assert_eq!(sres(1897) % 7, sres(0));
|
|
|
|
// Remainder works as expected.
|
|
assert_eq!(sat(1901) % 7, res(4));
|
|
assert_eq!(ssat(1901) % 7, sres(4));
|
|
assert_eq!(res(1901) % 7, res(4));
|
|
assert_eq!(sres(1901) % 7, sres(4));
|
|
}
|
|
|
|
#[test]
|
|
fn unsigned_amount_div_by_amount() {
|
|
assert_eq!((sat(0) / sat(7)).unwrap(), 0);
|
|
assert_eq!((sat(1897) / sat(7)).unwrap(), 271);
|
|
}
|
|
|
|
#[test]
|
|
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)).unwrap(), 0);
|
|
|
|
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]
|
|
fn signed_amount_div_by_amount_zero() {
|
|
let res = ssat(1897) / SignedAmount::ZERO;
|
|
assert!(res.into_result().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn mul_assign() {
|
|
let mut result = res(113);
|
|
result *= 367;
|
|
assert_eq!(result, res(41471));
|
|
|
|
let mut max = res(Amount::MAX.to_sat());
|
|
max *= 2;
|
|
assert!(max.is_error());
|
|
|
|
let mut signed_result = sres(-211);
|
|
signed_result *= 431;
|
|
assert_eq!(signed_result, sres(-90941));
|
|
|
|
let mut min = sres(SignedAmount::MIN.to_sat());
|
|
min *= 2;
|
|
assert!(min.is_error());
|
|
}
|
|
|
|
#[test]
|
|
fn div_assign() {
|
|
let mut result = res(41471);
|
|
result /= 367;
|
|
assert_eq!(result, res(113));
|
|
result /= 0;
|
|
assert!(result.is_error());
|
|
|
|
let mut signed_result = sres(-90941);
|
|
signed_result /= 211;
|
|
assert_eq!(signed_result, sres(-431));
|
|
signed_result /= 0;
|
|
assert!(signed_result.is_error());
|
|
}
|
|
|
|
#[test]
|
|
fn check_const() {
|
|
assert_eq!(SignedAmount::ONE_BTC.to_sat(), 100_000_000);
|
|
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.to_sat() as i64, SignedAmount::MAX.to_sat());
|
|
}
|
|
|
|
// Sanity check than stdlib supports the set of reference combinations for the ops we want.
|
|
#[test]
|
|
#[allow(clippy::op_ref)] // We are explicitly testing the references work with ops.
|
|
fn sanity_all_ops() {
|
|
let x = 127;
|
|
|
|
let _ = x + x;
|
|
let _ = &x + x;
|
|
let _ = x + &x;
|
|
let _ = &x + &x;
|
|
|
|
let _ = x - x;
|
|
let _ = &x - x;
|
|
let _ = x - &x;
|
|
let _ = &x - &x;
|
|
|
|
let _ = -x;
|
|
}
|
|
|
|
// Verify we have implemented all combinations of ops for the amount types and `NumOpResult` type.
|
|
// It's easier to read this test than check the code.
|
|
#[test]
|
|
#[allow(clippy::op_ref)] // We are explicitly testing the references work with ops.
|
|
fn num_op_result_ops() {
|
|
let sat = Amount::from_sat(1).unwrap();
|
|
let ssat = SignedAmount::from_sat(1).unwrap();
|
|
|
|
// Explicit type as sanity check.
|
|
let res: NumOpResult<Amount> = sat + sat;
|
|
let sres: NumOpResult<SignedAmount> = ssat + ssat;
|
|
|
|
macro_rules! check_op {
|
|
($(let _ = $lhs:ident $op:tt $rhs:ident);* $(;)?) => {
|
|
$(
|
|
let _ = $lhs $op $rhs;
|
|
let _ = &$lhs $op $rhs;
|
|
let _ = $lhs $op &$rhs;
|
|
let _ = &$lhs $op &$rhs;
|
|
)*
|
|
}
|
|
}
|
|
|
|
// We do not currently support division involving `NumOpResult` and an amount type.
|
|
check_op! {
|
|
// Operations where RHS is the result of another operation.
|
|
let _ = sat + res;
|
|
let _ = sat - res;
|
|
// let _ = sat / res;
|
|
let _ = ssat + sres;
|
|
let _ = ssat - sres;
|
|
// let _ = ssat / sres;
|
|
|
|
// Operations where LHS is the result of another operation.
|
|
let _ = res + sat;
|
|
let _ = res - sat;
|
|
// let _ = res / sat;
|
|
let _ = sres + ssat;
|
|
let _ = sres - ssat;
|
|
// let _ = sres / ssat;
|
|
|
|
// Operations that where both sides are the result of another operation.
|
|
let _ = res + res;
|
|
let _ = res - res;
|
|
// let _ = res / res;
|
|
let _ = sres + sres;
|
|
let _ = sres - sres;
|
|
// let _ = sres / sres;
|
|
};
|
|
}
|
|
|
|
// Verify we have implemented all combinations of ops for the `NumOpResult` type and an integer.
|
|
// It's easier to read this test than check the code.
|
|
#[test]
|
|
#[allow(clippy::op_ref)] // We are explicitly testing the references work with ops.
|
|
fn num_op_result_ops_integer() {
|
|
let sat = Amount::from_sat(1).unwrap();
|
|
let ssat = SignedAmount::from_sat(1).unwrap();
|
|
|
|
// Explicit type as sanity check.
|
|
let res: NumOpResult<Amount> = sat + sat;
|
|
let sres: NumOpResult<SignedAmount> = ssat + ssat;
|
|
|
|
macro_rules! check_op {
|
|
($(let _ = $lhs:ident $op:tt $rhs:literal);* $(;)?) => {
|
|
$(
|
|
let _ = $lhs $op $rhs;
|
|
let _ = &$lhs $op $rhs;
|
|
let _ = $lhs $op &$rhs;
|
|
let _ = &$lhs $op &$rhs;
|
|
)*
|
|
}
|
|
}
|
|
check_op! {
|
|
// Operations on an amount type and an integer.
|
|
let _ = sat * 3_u64; // Explicit type for the benefit of the reader.
|
|
let _ = sat / 3;
|
|
let _ = sat % 3;
|
|
|
|
let _ = ssat * 3_i64; // Explicit type for the benefit of the reader.
|
|
let _ = ssat / 3;
|
|
let _ = ssat % 3;
|
|
|
|
// Operations on a `NumOpResult` and integer.
|
|
let _ = res * 3_u64; // Explicit type for the benefit of the reader.
|
|
let _ = res / 3;
|
|
let _ = res % 3;
|
|
|
|
let _ = sres * 3_i64; // Explicit type for the benefit of the reader.
|
|
let _ = sres / 3;
|
|
let _ = sres % 3;
|
|
};
|
|
}
|
|
|
|
// Verify we have implemented all `Neg` for the amount types.
|
|
#[test]
|
|
fn amount_op_result_neg() {
|
|
// TODO: Implement Neg all round.
|
|
|
|
// let sat = Amount::from_sat(1).unwrap();
|
|
let ssat = SignedAmount::from_sat(1).unwrap();
|
|
|
|
// let _ = -sat;
|
|
let _ = -ssat;
|
|
// let _ = -res;
|
|
// let _ = -sres;
|
|
}
|
|
|
|
// Verify we have implemented all `Sum` for the `NumOpResult` type.
|
|
#[test]
|
|
fn amount_op_result_sum() {
|
|
let res = Amount::from_sat(1).unwrap() + Amount::from_sat(1).unwrap();
|
|
let amounts = [res, res];
|
|
let amount_refs = [&res, &res];
|
|
|
|
// Sum iterators.
|
|
let _ = amounts.iter().sum::<NumOpResult<Amount>>();
|
|
let _ = amount_refs.iter().copied().sum::<NumOpResult<Amount>>();
|
|
let _ = amount_refs.into_iter().sum::<NumOpResult<Amount>>();
|
|
}
|
|
|
|
#[test]
|
|
fn math_op_errors() {
|
|
let overflow = Amount::MAX + Amount::from_sat(1).unwrap();
|
|
if let NumOpResult::Error(err) = overflow {
|
|
assert!(err.operation().is_overflow());
|
|
assert!(!err.operation().is_div_by_zero());
|
|
} else {
|
|
panic!("Expected an overflow error, but got a valid result");
|
|
}
|
|
|
|
let div_by_zero = Amount::from_sat(10).unwrap() / Amount::ZERO;
|
|
if let NumOpResult::Error(err) = div_by_zero {
|
|
assert!(!err.operation().is_overflow());
|
|
assert!(err.operation().is_div_by_zero());
|
|
} else {
|
|
panic!("Expected a division by zero error, but got a valid result");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::op_ref)]
|
|
fn amount_div_nonzero() {
|
|
let amount = Amount::from_sat(100).unwrap();
|
|
let divisor = NonZeroU64::new(4).unwrap();
|
|
let result = amount / divisor;
|
|
assert_eq!(result, Amount::from_sat(25).unwrap());
|
|
//checking also for &T/&U variant
|
|
assert_eq!(&amount / &divisor, Amount::from_sat(25).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::op_ref)]
|
|
fn signed_amount_div_nonzero() {
|
|
let signed = SignedAmount::from_sat(-100).unwrap();
|
|
let divisor = NonZeroI64::new(4).unwrap();
|
|
let result = signed / divisor;
|
|
assert_eq!(result, SignedAmount::from_sat(-25).unwrap());
|
|
//checking also for &T/U, T/&Uvariant
|
|
assert_eq!(&signed / divisor, SignedAmount::from_sat(-25).unwrap());
|
|
assert_eq!(signed / &divisor, SignedAmount::from_sat(-25).unwrap());
|
|
}
|