// 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 { NumOpResult::from(sat(n_sat)) } #[track_caller] fn sres(n_sat: i64) -> NumOpResult { 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::() { 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::() { Err(e) => assert_eq!( e, ParseError(ParseErrorInner::Amount(ParseAmountError( ParseAmountErrorInner::OutOfRange(OutOfRangeError::negative()) ))) ), Ok(_) => panic!("unsigned amount from {}", s), } match s.parse::() { 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::(), Err(InvalidCharacterError { invalid_char: 'x', position: 0 }.into()) ); assert_eq!( "xBTC".parse::(), Err(Unknown(UnknownDenominationError("xBTC".into())).into()), ); assert_eq!( "5 BTC BTC".parse::(), Err(Unknown(UnknownDenominationError("BTC BTC".into())).into()), ); assert_eq!( "5BTC BTC".parse::(), Err(E::from(InvalidCharacterError { invalid_char: 'B', position: 1 }).into()) ); assert_eq!( "5 5 BTC".parse::(), Err(Unknown(UnknownDenominationError("5 BTC".into())).into()), ); #[track_caller] fn ok_case(s: &str, expected: Amount) { assert_eq!(s.parse::().unwrap(), expected); assert_eq!(s.replace(' ', "").parse::().unwrap(), expected); } #[track_caller] fn case(s: &str, expected: Result>) { let expected = expected.map_err(Into::into); assert_eq!(s.parse::(), expected); assert_eq!(s.replace(' ', "").parse::(), expected); } #[track_caller] fn ok_scase(s: &str, expected: SignedAmount) { assert_eq!(s.parse::().unwrap(), expected); assert_eq!(s.replace(' ', "").parse::().unwrap(), expected); } #[track_caller] fn scase(s: &str, expected: Result>) { let expected = expected.map_err(Into::into); assert_eq!(s.parse::(), expected); assert_eq!(s.replace(' ', "").parse::(), 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::(), Ok(amt)); assert_eq!(denom(amt, D::CentiBitcoin).parse::(), Ok(amt)); assert_eq!(denom(amt, D::MilliBitcoin).parse::(), Ok(amt)); assert_eq!(denom(amt, D::MicroBitcoin).parse::(), Ok(amt)); assert_eq!(denom(amt, D::Bit).parse::(), Ok(amt)); assert_eq!(denom(amt, D::Satoshi).parse::(), Ok(amt)); assert_eq!( "42 satoshi BTC".parse::(), Err(Unknown(UnknownDenominationError("satoshi BTC".into())).into()), ); assert_eq!( "-42 satoshi BTC".parse::(), 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 = 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 = 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, #[serde(default, with = "crate::amount::serde::as_btc::opt")] pub samt: Option, } 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, #[serde(default, with = "crate::amount::serde::as_sat::opt")] pub samt: Option, } 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, #[serde(default, with = "crate::amount::serde::as_str::opt")] pub samt: Option, } 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::>(), Amount::ZERO.into()); assert_eq!([].iter().sum::>(), SignedAmount::ZERO.into()); let results = [NumOpResult::Valid(sat(42)), NumOpResult::Valid(sat(1337)), NumOpResult::Valid(sat(21))]; assert_eq!(results.iter().sum::>(), 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::Valid(ssat(1400)) ); let amounts = [sat(42), sat(1337), sat(21)]; assert_eq!( amounts.iter().map(|a| NumOpResult::Valid(*a)).sum::>(), sat(1400).into(), ); assert_eq!( amounts.into_iter().map(NumOpResult::Valid).sum::>(), sat(1400).into(), ); let amounts = [ssat(-42), ssat(1337), ssat(21)]; assert_eq!( amounts.iter().map(NumOpResult::from).sum::>(), ssat(1316).into(), ); assert_eq!( amounts.into_iter().map(NumOpResult::from).sum::>(), 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::().is_ok()); } } #[test] fn disallow_confusing_forms() { let confusing = ["CBTC", "Cbtc", "MBTC", "Mbtc", "UBTC", "Ubtc"]; for denom in confusing { match denom.parse::() { 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::() { 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 = sat + sat; let sres: NumOpResult = 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 = sat + sat; let sres: NumOpResult = 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::>(); let _ = amount_refs.iter().copied().sum::>(); let _ = amount_refs.into_iter().sum::>(); } #[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()); }