From e0bc68042d9840a82b693ffe0f70e5523049b1a9 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 09:54:29 +1100 Subject: [PATCH] amount: Move test code to submodule In preparation for splitting the two amounts into separate files; move the `amount` module code to a submodule. Internal change only. --- units/src/amount/mod.rs | 936 +------------------------------------ units/src/amount/tests.rs | 941 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 943 insertions(+), 934 deletions(-) create mode 100644 units/src/amount/tests.rs diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index 61bffe3f7..7e67f454c 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -7,6 +7,8 @@ pub mod error; +#[cfg(test)] +mod tests; #[cfg(kani)] mod verification; @@ -1634,937 +1636,3 @@ pub mod serde { } } } - -#[cfg(test)] -mod tests { - #[cfg(feature = "alloc")] - use alloc::format; - #[cfg(feature = "std")] - use std::panic; - - use super::*; - - #[test] - #[cfg(feature = "alloc")] - fn from_str_zero() { - let denoms = ["BTC", "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::from_sat(0)), - } - } - - let s = format!("-0 {}", denom); - match s.parse::() { - Err(e) => assert_eq!( - e, - ParseError::Amount(ParseAmountError::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::from_sat(0)), - } - } - } - - #[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_int_btc(2); - assert_eq!(Amount::from_sat(200_000_000), amt); - } - - #[should_panic] - #[test] - fn from_int_btc_panic() { Amount::from_int_btc(u64::MAX); } - - #[test] - fn test_signed_amount_try_from_amount() { - let ua_positive = Amount::from_sat(123); - let sa_positive = SignedAmount::try_from(ua_positive).unwrap(); - assert_eq!(sa_positive, SignedAmount::from_sat(123)); - - let ua_max = Amount::MAX; - let result = SignedAmount::try_from(ua_max); - assert_eq!(result, Err(OutOfRangeError { is_signed: true, is_greater_than_max: true })); - } - - #[test] - fn test_amount_try_from_signed_amount() { - let sa_positive = SignedAmount::from_sat(123); - let ua_positive = Amount::try_from(sa_positive).unwrap(); - assert_eq!(ua_positive, Amount::from_sat(123)); - - let sa_negative = SignedAmount::from_sat(-123); - let result = Amount::try_from(sa_negative); - assert_eq!(result, Err(OutOfRangeError { is_signed: false, is_greater_than_max: false })); - } - - #[test] - fn mul_div() { - let sat = Amount::from_sat; - let ssat = SignedAmount::from_sat; - - assert_eq!(sat(14) * 3, sat(42)); - assert_eq!(sat(14) / 2, sat(7)); - assert_eq!(sat(14) % 3, sat(2)); - assert_eq!(ssat(-14) * 3, ssat(-42)); - assert_eq!(ssat(-14) / 2, ssat(-7)); - assert_eq!(ssat(-14) % 3, ssat(-2)); - - let mut b = ssat(30); - b /= 3; - assert_eq!(b, ssat(10)); - b %= 3; - assert_eq!(b, ssat(1)); - } - - #[cfg(feature = "std")] - #[test] - fn test_overflows() { - // panic on overflow - let result = panic::catch_unwind(|| Amount::MAX + Amount::from_sat(1)); - assert!(result.is_err()); - let result = panic::catch_unwind(|| Amount::from_sat(8446744073709551615) * 3); - assert!(result.is_err()); - } - - #[test] - fn checked_arithmetic() { - let sat = Amount::from_sat; - let ssat = SignedAmount::from_sat; - - assert_eq!(SignedAmount::MAX.checked_add(ssat(1)), None); - assert_eq!(SignedAmount::MIN.checked_sub(ssat(1)), None); - assert_eq!(Amount::MAX.checked_add(sat(1)), None); - 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))); - } - - #[cfg(feature = "alloc")] - #[test] - fn amount_checked_div_by_weight() { - let weight = Weight::from_kwu(1).unwrap(); - let fee_rate = Amount::from_sat(1).checked_div_by_weight(weight).unwrap(); - // 1 sats / 1,000 wu = 1 sats/kwu - assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1)); - - let weight = Weight::from_wu(381); - let fee_rate = Amount::from_sat(329).checked_div_by_weight(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::MAX.checked_div_by_weight(weight); - assert!(fee_rate.is_none()); - - let fee_rate = Amount::ONE_SAT.checked_div_by_weight(Weight::ZERO); - assert!(fee_rate.is_none()); - } - - #[test] - #[cfg(not(debug_assertions))] - fn unchecked_amount_add() { - let amt = Amount::MAX.unchecked_add(Amount::ONE_SAT); - assert_eq!(amt, Amount::ZERO); - } - - #[test] - #[cfg(not(debug_assertions))] - fn unchecked_signed_amount_add() { - let signed_amt = SignedAmount::MAX.unchecked_add(SignedAmount::ONE_SAT); - assert_eq!(signed_amt, SignedAmount::MIN); - } - - #[test] - #[cfg(not(debug_assertions))] - fn unchecked_amount_subtract() { - let amt = Amount::ZERO.unchecked_sub(Amount::ONE_SAT); - assert_eq!(amt, Amount::MAX); - } - - #[test] - #[cfg(not(debug_assertions))] - fn unchecked_signed_amount_subtract() { - let signed_amt = SignedAmount::MIN.unchecked_sub(SignedAmount::ONE_SAT); - assert_eq!(signed_amt, SignedAmount::MAX); - } - - #[cfg(feature = "alloc")] - #[test] - fn floating_point() { - use super::Denomination as D; - let f = Amount::from_float_in; - let sf = SignedAmount::from_float_in; - let sat = Amount::from_sat; - let ssat = SignedAmount::from_sat; - - assert_eq!(f(11.22, D::Bitcoin), Ok(sat(1122000000))); - assert_eq!(sf(-11.22, D::MilliBitcoin), Ok(ssat(-1122000))); - assert_eq!(f(11.22, D::Bit), Ok(sat(1122))); - assert_eq!(f(0.0001234, D::Bitcoin), Ok(sat(12340))); - assert_eq!(sf(-0.00012345, D::Bitcoin), Ok(ssat(-12345))); - - assert_eq!(f(11.22, D::Satoshi), Err(TooPreciseError { position: 3 }.into())); - assert_eq!(f(42.123456781, D::Bitcoin), Err(TooPreciseError { position: 11 }.into())); - assert_eq!(sf(-184467440738.0, D::Bitcoin), Err(OutOfRangeError::too_small().into())); - assert_eq!( - f(18446744073709551617.0, D::Satoshi), - Err(OutOfRangeError::too_big(false).into()) - ); - - // Amount can be grater than the max SignedAmount. - assert!(f(SignedAmount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi).is_ok()); - - 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::MilliBitcoin), -2500.0); - assert_eq!(btc(2.5).to_float_in(D::Satoshi), 250000000.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 btc = Denomination::Bitcoin; - let sat = Denomination::Satoshi; - let p = Amount::from_str_in; - let sp = SignedAmount::from_str_in; - - assert_eq!( - p("x", btc), - Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 0 })) - ); - assert_eq!( - p("-", btc), - Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })) - ); - assert_eq!( - sp("-", btc), - Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })) - ); - assert_eq!( - p("-1.0x", btc), - Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 4 })) - ); - assert_eq!( - p("0.0 ", btc), - Err(E::from(InvalidCharacterError { invalid_char: ' ', position: 3 })) - ); - assert_eq!( - p("0.000.000", btc), - Err(E::from(InvalidCharacterError { invalid_char: '.', position: 5 })) - ); - #[cfg(feature = "alloc")] - let more_than_max = format!("1{}", Amount::MAX); - #[cfg(feature = "alloc")] - assert_eq!(p(&more_than_max, btc), Err(OutOfRangeError::too_big(false).into())); - assert_eq!(p("0.000000042", btc), Err(TooPreciseError { position: 10 }.into())); - assert_eq!(p("1.0000000", sat), Ok(Amount::from_sat(1))); - assert_eq!(p("1.1", sat), Err(TooPreciseError { position: 2 }.into())); - assert_eq!(p("1000.1", sat), Err(TooPreciseError { position: 5 }.into())); - assert_eq!(p("1001.0000000", sat), Ok(Amount::from_sat(1001))); - assert_eq!(p("1000.0000001", sat), Err(TooPreciseError { position: 11 }.into())); - - assert_eq!(p("1", btc), Ok(Amount::from_sat(1_000_000_00))); - assert_eq!(sp("-.5", btc), Ok(SignedAmount::from_sat(-500_000_00))); - #[cfg(feature = "alloc")] - assert_eq!(sp(&i64::MIN.to_string(), sat), Ok(SignedAmount::from_sat(i64::MIN))); - assert_eq!(p("1.1", btc), Ok(Amount::from_sat(1_100_000_00))); - assert_eq!(p("100", sat), Ok(Amount::from_sat(100))); - assert_eq!(p("55", sat), Ok(Amount::from_sat(55))); - assert_eq!(p("5500000000000000000", sat), Ok(Amount::from_sat(55_000_000_000_000_000_00))); - // Should this even pass? - assert_eq!(p("5500000000000000000.", sat), Ok(Amount::from_sat(55_000_000_000_000_000_00))); - assert_eq!( - p("12345678901.12345678", btc), - Ok(Amount::from_sat(12_345_678_901__123_456_78)) - ); - - // make sure satoshi > i64::MAX is checked. - #[cfg(feature = "alloc")] - { - let amount = Amount::from_sat(i64::MAX as u64); - assert_eq!(Amount::from_str_in(&amount.to_string_in(sat), sat), Ok(amount)); - assert!( - SignedAmount::from_str_in(&(amount + Amount::from_sat(1)).to_string_in(sat), sat).is_err() - ); - assert!(Amount::from_str_in(&(amount + Amount::from_sat(1)).to_string_in(sat), sat).is_ok()); - } - - // 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::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!(SignedAmount::from_sat(-42).to_string_in(D::Bitcoin), "-0.00000042"); - - assert_eq!(Amount::ONE_BTC.to_string_with_denomination(D::Bitcoin), "1 BTC"); - assert_eq!( - SignedAmount::ONE_BTC.to_string_with_denomination(D::Satoshi), - "100000000 satoshi" - ); - assert_eq!(Amount::ONE_SAT.to_string_with_denomination(D::Bitcoin), "0.00000001 BTC"); - assert_eq!( - SignedAmount::from_sat(-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, Amount::from_sat($val).display_in(Denomination::$denom)), $expected); - assert_eq!(format!($format_string, SignedAmount::from_sat($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, Amount::from_sat($val).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix)); - assert_eq!(format!($format_string, SignedAmount::from_sat($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 test_unsigned_signed_conversion() { - let sa = SignedAmount::from_sat; - let ua = Amount::from_sat; - - assert_eq!(Amount::MAX.to_signed(), Err(OutOfRangeError::too_big(true))); - assert_eq!(ua(i64::MAX as u64).to_signed(), Ok(sa(i64::MAX))); - assert_eq!(ua(i64::MAX as u64 + 1).to_signed(), Err(OutOfRangeError::too_big(true))); - - assert_eq!(sa(i64::MAX).to_unsigned(), Ok(ua(i64::MAX as u64))); - - assert_eq!(sa(i64::MAX).to_unsigned().unwrap().to_signed(), Ok(sa(i64::MAX))); - } - - #[test] - #[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin. - 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("-200000000000 BTC", Err(OutOfRangeError::too_small())); - case("18446744073709551616 sat", Err(OutOfRangeError::too_big(false))); - - ok_case(".5 bits", Amount::from_sat(50)); - ok_scase("-.5 bits", SignedAmount::from_sat(-50)); - ok_case("0.00253583 BTC", Amount::from_sat(253583)); - ok_scase("-5 satoshi", SignedAmount::from_sat(-5)); - ok_case("0.10000000 BTC", Amount::from_sat(100_000_00)); - ok_scase("-100 bits", SignedAmount::from_sat(-10_000)); - #[cfg(feature = "alloc")] - ok_scase(&format!("{} SAT", i64::MIN), SignedAmount::from_sat(i64::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 = Amount::from_sat; - let sa_str = SignedAmount::from_str_in; - let sa_sat = SignedAmount::from_sat; - - assert_eq!("0.5", Amount::from_sat(50).to_string_in(D::Bit)); - assert_eq!("-0.5", SignedAmount::from_sat(-50).to_string_in(D::Bit)); - assert_eq!("0.00253583", Amount::from_sat(253583).to_string_in(D::Bitcoin)); - assert_eq!("-5", SignedAmount::from_sat(-5).to_string_in(D::Satoshi)); - assert_eq!("0.1", Amount::from_sat(100_000_00).to_string_in(D::Bitcoin)); - assert_eq!("-100", SignedAmount::from_sat(-10_000).to_string_in(D::Bit)); - - assert_eq!("0.50", format!("{:.2}", Amount::from_sat(50).display_in(D::Bit))); - assert_eq!("-0.50", format!("{:.2}", SignedAmount::from_sat(-50).display_in(D::Bit))); - assert_eq!( - "0.10000000", - format!("{:.8}", Amount::from_sat(100_000_00).display_in(D::Bitcoin)) - ); - assert_eq!("-100.00", format!("{:.2}", SignedAmount::from_sat(-10_000).display_in(D::Bit))); - - assert_eq!(ua_str(&ua_sat(0).to_string_in(D::Satoshi), D::Satoshi), Ok(ua_sat(0))); - assert_eq!(ua_str(&ua_sat(500).to_string_in(D::Bitcoin), D::Bitcoin), Ok(ua_sat(500))); - 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(1).to_string_in(D::MicroBitcoin), D::MicroBitcoin), - 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!(ua_str(&ua_sat(u64::MAX).to_string_in(D::MilliBitcoin), D::MilliBitcoin).is_ok()); - - assert_eq!( - sa_str(&sa_sat(-1).to_string_in(D::MicroBitcoin), D::MicroBitcoin), - Ok(sa_sat(-1)) - ); - - assert_eq!( - sa_str(&sa_sat(i64::MAX).to_string_in(D::Satoshi), D::MicroBitcoin), - Err(OutOfRangeError::too_big(true).into()) - ); - // Test an overflow bug in `abs()` - assert_eq!( - sa_str(&sa_sat(i64::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 = Amount::from_sat(42); - let denom = Amount::to_string_with_denomination; - assert_eq!(denom(amt, D::Bitcoin).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: Amount::from_sat(123456789), samt: SignedAmount::from_sat(-123456789) }, - &[ - serde_test::Token::Struct { name: "T", len: 2 }, - serde_test::Token::Str("amt"), - serde_test::Token::U64(123456789), - serde_test::Token::Str("samt"), - serde_test::Token::I64(-123456789), - 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: Amount::from_sat(21_000_000__000_000_01), - samt: SignedAmount::from_sat(-21_000_000__000_000_01), - }; - - let json = "{\"amt\": 21000000.00000001, \ - \"samt\": -21000000.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::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] - #[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(Amount::from_sat(2_500_000_00)), - samt: Some(SignedAmount::from_sat(-2_500_000_00)), - }; - let without = T { amt: None, samt: None }; - - // Test Roundtripping - for s in [&with, &without].iter() { - 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(Amount::from_sat(2_500_000_00)), - samt: Some(SignedAmount::from_sat(-2_500_000_00)), - }; - let without = T { amt: None, samt: None }; - - // Test Roundtripping - for s in [&with, &without].iter() { - 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()); - } - - #[test] - fn sum_amounts() { - assert_eq!(Amount::from_sat(0), [].into_iter().sum::()); - assert_eq!(SignedAmount::from_sat(0), [].into_iter().sum::()); - - let amounts = [Amount::from_sat(42), Amount::from_sat(1337), Amount::from_sat(21)]; - let sum = amounts.into_iter().sum::(); - assert_eq!(Amount::from_sat(1400), sum); - - let amounts = - [SignedAmount::from_sat(-42), SignedAmount::from_sat(1337), SignedAmount::from_sat(21)]; - let sum = amounts.into_iter().sum::(); - assert_eq!(SignedAmount::from_sat(1316), sum); - } - - #[test] - fn checked_sum_amounts() { - assert_eq!(Some(Amount::from_sat(0)), [].into_iter().checked_sum()); - assert_eq!(Some(SignedAmount::from_sat(0)), [].into_iter().checked_sum()); - - let amounts = [Amount::from_sat(42), Amount::from_sat(1337), Amount::from_sat(21)]; - let sum = amounts.into_iter().checked_sum(); - assert_eq!(Some(Amount::from_sat(1400)), sum); - - let amounts = [Amount::from_sat(u64::MAX), Amount::from_sat(1337), Amount::from_sat(21)]; - let sum = amounts.into_iter().checked_sum(); - assert_eq!(None, sum); - - let amounts = [ - SignedAmount::from_sat(i64::MIN), - SignedAmount::from_sat(-1), - SignedAmount::from_sat(21), - ]; - let sum = amounts.into_iter().checked_sum(); - assert_eq!(None, sum); - - let amounts = [ - SignedAmount::from_sat(i64::MAX), - SignedAmount::from_sat(1), - SignedAmount::from_sat(21), - ]; - let sum = amounts.into_iter().checked_sum(); - assert_eq!(None, sum); - - let amounts = - [SignedAmount::from_sat(42), SignedAmount::from_sat(3301), SignedAmount::from_sat(21)]; - let sum = amounts.into_iter().checked_sum(); - assert_eq!(Some(SignedAmount::from_sat(3364)), sum); - } - - #[test] - fn denomination_string_acceptable_forms() { - // Non-exhaustive list of valid forms. - let valid = [ - "BTC", "btc", "mBTC", "mbtc", "uBTC", "ubtc", "SATOSHI", "satoshi", "SATOSHIS", - "satoshis", "SAT", "sat", "SATS", "sats", "bit", "bits", - ]; - for denom in valid.iter() { - assert!(denom.parse::().is_ok()); - } - } - - #[test] - fn disallow_confusing_forms() { - let confusing = ["CBTC", "Cbtc", "MBTC", "Mbtc", "UBTC", "Ubtc"]; - for denom in confusing.iter() { - 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.iter() { - 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!("{}", Amount::from_sat(1000000)), "0.01 BTC"); - assert_eq!(format!("{}", Amount::ONE_SAT), "0.00000001 BTC"); - assert_eq!(format!("{}", Amount::ONE_BTC), "1 BTC"); - assert_eq!(format!("{}", Amount::from_sat(1)), "0.00000001 BTC"); - assert_eq!(format!("{}", Amount::from_sat(10)), "0.0000001 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(10)), "0.00 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(100)), "0.00 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(1000)), "0.00 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(10_000)), "0.00 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(100_000)), "0.00 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(1_000_000)), "0.01 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(10_000_000)), "0.10 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(100_000_000)), "1.00 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(500_000)), "0.01 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(9_500_000)), "0.10 BTC"); - assert_eq!(format!("{:.2}", Amount::from_sat(99_500_000)), "1.00 BTC"); - assert_eq!(format!("{}", Amount::from_sat(100_000_000)), "1 BTC"); - assert_eq!(format!("{}", Amount::from_sat(40_000_000_000)), "400 BTC"); - assert_eq!(format!("{:.10}", Amount::from_sat(100_000_000)), "1.0000000000 BTC"); - assert_eq!(format!("{}", Amount::from_sat(400_000_000_000_010)), "4000000.0000001 BTC"); - assert_eq!(format!("{}", Amount::from_sat(400_000_000_000_000)), "4000000 BTC"); - } -} diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs new file mode 100644 index 000000000..29ece202a --- /dev/null +++ b/units/src/amount/tests.rs @@ -0,0 +1,941 @@ +// 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}; +#[cfg(feature = "std")] +use std::panic; + +#[cfg(feature = "serde")] +use ::serde::{Deserialize, Serialize}; + +use super::*; +#[cfg(feature = "alloc")] +use crate::{FeeRate, Weight}; + +#[test] +#[cfg(feature = "alloc")] +fn from_str_zero() { + let denoms = ["BTC", "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::from_sat(0)), + } + } + + let s = format!("-0 {}", denom); + match s.parse::() { + Err(e) => assert_eq!( + e, + ParseError::Amount(ParseAmountError::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::from_sat(0)), + } + } +} + +#[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_int_btc(2); + assert_eq!(Amount::from_sat(200_000_000), amt); +} + +#[should_panic] +#[test] +fn from_int_btc_panic() { Amount::from_int_btc(u64::MAX); } + +#[test] +fn test_signed_amount_try_from_amount() { + let ua_positive = Amount::from_sat(123); + let sa_positive = SignedAmount::try_from(ua_positive).unwrap(); + assert_eq!(sa_positive, SignedAmount::from_sat(123)); + + let ua_max = Amount::MAX; + let result = SignedAmount::try_from(ua_max); + assert_eq!(result, Err(OutOfRangeError { is_signed: true, is_greater_than_max: true })); +} + +#[test] +fn test_amount_try_from_signed_amount() { + let sa_positive = SignedAmount::from_sat(123); + let ua_positive = Amount::try_from(sa_positive).unwrap(); + assert_eq!(ua_positive, Amount::from_sat(123)); + + let sa_negative = SignedAmount::from_sat(-123); + let result = Amount::try_from(sa_negative); + assert_eq!(result, Err(OutOfRangeError { is_signed: false, is_greater_than_max: false })); +} + +#[test] +fn mul_div() { + let sat = Amount::from_sat; + let ssat = SignedAmount::from_sat; + + assert_eq!(sat(14) * 3, sat(42)); + assert_eq!(sat(14) / 2, sat(7)); + assert_eq!(sat(14) % 3, sat(2)); + assert_eq!(ssat(-14) * 3, ssat(-42)); + assert_eq!(ssat(-14) / 2, ssat(-7)); + assert_eq!(ssat(-14) % 3, ssat(-2)); + + let mut b = ssat(30); + b /= 3; + assert_eq!(b, ssat(10)); + b %= 3; + assert_eq!(b, ssat(1)); +} + +#[cfg(feature = "std")] +#[test] +fn test_overflows() { + // panic on overflow + let result = panic::catch_unwind(|| Amount::MAX + Amount::from_sat(1)); + assert!(result.is_err()); + let result = panic::catch_unwind(|| Amount::from_sat(8446744073709551615) * 3); + assert!(result.is_err()); +} + +#[test] +fn checked_arithmetic() { + let sat = Amount::from_sat; + let ssat = SignedAmount::from_sat; + + assert_eq!(SignedAmount::MAX.checked_add(ssat(1)), None); + assert_eq!(SignedAmount::MIN.checked_sub(ssat(1)), None); + assert_eq!(Amount::MAX.checked_add(sat(1)), None); + 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))); +} + +#[cfg(feature = "alloc")] +#[test] +fn amount_checked_div_by_weight() { + let weight = Weight::from_kwu(1).unwrap(); + let fee_rate = Amount::from_sat(1).checked_div_by_weight(weight).unwrap(); + // 1 sats / 1,000 wu = 1 sats/kwu + assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1)); + + let weight = Weight::from_wu(381); + let fee_rate = Amount::from_sat(329).checked_div_by_weight(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::MAX.checked_div_by_weight(weight); + assert!(fee_rate.is_none()); + + let fee_rate = Amount::ONE_SAT.checked_div_by_weight(Weight::ZERO); + assert!(fee_rate.is_none()); +} + +#[test] +#[cfg(not(debug_assertions))] +fn unchecked_amount_add() { + let amt = Amount::MAX.unchecked_add(Amount::ONE_SAT); + assert_eq!(amt, Amount::ZERO); +} + +#[test] +#[cfg(not(debug_assertions))] +fn unchecked_signed_amount_add() { + let signed_amt = SignedAmount::MAX.unchecked_add(SignedAmount::ONE_SAT); + assert_eq!(signed_amt, SignedAmount::MIN); +} + +#[test] +#[cfg(not(debug_assertions))] +fn unchecked_amount_subtract() { + let amt = Amount::ZERO.unchecked_sub(Amount::ONE_SAT); + assert_eq!(amt, Amount::MAX); +} + +#[test] +#[cfg(not(debug_assertions))] +fn unchecked_signed_amount_subtract() { + let signed_amt = SignedAmount::MIN.unchecked_sub(SignedAmount::ONE_SAT); + assert_eq!(signed_amt, SignedAmount::MAX); +} + +#[cfg(feature = "alloc")] +#[test] +fn floating_point() { + use super::Denomination as D; + let f = Amount::from_float_in; + let sf = SignedAmount::from_float_in; + let sat = Amount::from_sat; + let ssat = SignedAmount::from_sat; + + assert_eq!(f(11.22, D::Bitcoin), Ok(sat(1122000000))); + assert_eq!(sf(-11.22, D::MilliBitcoin), Ok(ssat(-1122000))); + assert_eq!(f(11.22, D::Bit), Ok(sat(1122))); + assert_eq!(f(0.0001234, D::Bitcoin), Ok(sat(12340))); + assert_eq!(sf(-0.00012345, D::Bitcoin), Ok(ssat(-12345))); + + assert_eq!(f(11.22, D::Satoshi), Err(TooPreciseError { position: 3 }.into())); + assert_eq!(f(42.123456781, D::Bitcoin), Err(TooPreciseError { position: 11 }.into())); + assert_eq!(sf(-184467440738.0, D::Bitcoin), Err(OutOfRangeError::too_small().into())); + assert_eq!( + f(18446744073709551617.0, D::Satoshi), + Err(OutOfRangeError::too_big(false).into()) + ); + + // Amount can be grater than the max SignedAmount. + assert!(f(SignedAmount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi).is_ok()); + + 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::MilliBitcoin), -2500.0); + assert_eq!(btc(2.5).to_float_in(D::Satoshi), 250000000.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 btc = Denomination::Bitcoin; + let sat = Denomination::Satoshi; + let p = Amount::from_str_in; + let sp = SignedAmount::from_str_in; + + assert_eq!( + p("x", btc), + Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 0 })) + ); + assert_eq!( + p("-", btc), + Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })) + ); + assert_eq!( + sp("-", btc), + Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })) + ); + assert_eq!( + p("-1.0x", btc), + Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 4 })) + ); + assert_eq!( + p("0.0 ", btc), + Err(E::from(InvalidCharacterError { invalid_char: ' ', position: 3 })) + ); + assert_eq!( + p("0.000.000", btc), + Err(E::from(InvalidCharacterError { invalid_char: '.', position: 5 })) + ); + #[cfg(feature = "alloc")] + let more_than_max = format!("1{}", Amount::MAX); + #[cfg(feature = "alloc")] + assert_eq!(p(&more_than_max, btc), Err(OutOfRangeError::too_big(false).into())); + assert_eq!(p("0.000000042", btc), Err(TooPreciseError { position: 10 }.into())); + assert_eq!(p("1.0000000", sat), Ok(Amount::from_sat(1))); + assert_eq!(p("1.1", sat), Err(TooPreciseError { position: 2 }.into())); + assert_eq!(p("1000.1", sat), Err(TooPreciseError { position: 5 }.into())); + assert_eq!(p("1001.0000000", sat), Ok(Amount::from_sat(1001))); + assert_eq!(p("1000.0000001", sat), Err(TooPreciseError { position: 11 }.into())); + + assert_eq!(p("1", btc), Ok(Amount::from_sat(1_000_000_00))); + assert_eq!(sp("-.5", btc), Ok(SignedAmount::from_sat(-500_000_00))); + #[cfg(feature = "alloc")] + assert_eq!(sp(&i64::MIN.to_string(), sat), Ok(SignedAmount::from_sat(i64::MIN))); + assert_eq!(p("1.1", btc), Ok(Amount::from_sat(1_100_000_00))); + assert_eq!(p("100", sat), Ok(Amount::from_sat(100))); + assert_eq!(p("55", sat), Ok(Amount::from_sat(55))); + assert_eq!(p("5500000000000000000", sat), Ok(Amount::from_sat(55_000_000_000_000_000_00))); + // Should this even pass? + assert_eq!(p("5500000000000000000.", sat), Ok(Amount::from_sat(55_000_000_000_000_000_00))); + assert_eq!( + p("12345678901.12345678", btc), + Ok(Amount::from_sat(12_345_678_901__123_456_78)) + ); + + // make sure satoshi > i64::MAX is checked. + #[cfg(feature = "alloc")] + { + let amount = Amount::from_sat(i64::MAX as u64); + assert_eq!(Amount::from_str_in(&amount.to_string_in(sat), sat), Ok(amount)); + assert!( + SignedAmount::from_str_in(&(amount + Amount::from_sat(1)).to_string_in(sat), sat).is_err() + ); + assert!(Amount::from_str_in(&(amount + Amount::from_sat(1)).to_string_in(sat), sat).is_ok()); + } + + // 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::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!(SignedAmount::from_sat(-42).to_string_in(D::Bitcoin), "-0.00000042"); + + assert_eq!(Amount::ONE_BTC.to_string_with_denomination(D::Bitcoin), "1 BTC"); + assert_eq!( + SignedAmount::ONE_BTC.to_string_with_denomination(D::Satoshi), + "100000000 satoshi" + ); + assert_eq!(Amount::ONE_SAT.to_string_with_denomination(D::Bitcoin), "0.00000001 BTC"); + assert_eq!( + SignedAmount::from_sat(-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, Amount::from_sat($val).display_in(Denomination::$denom)), $expected); + assert_eq!(format!($format_string, SignedAmount::from_sat($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, Amount::from_sat($val).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix)); + assert_eq!(format!($format_string, SignedAmount::from_sat($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 test_unsigned_signed_conversion() { + let sa = SignedAmount::from_sat; + let ua = Amount::from_sat; + + assert_eq!(Amount::MAX.to_signed(), Err(OutOfRangeError::too_big(true))); + assert_eq!(ua(i64::MAX as u64).to_signed(), Ok(sa(i64::MAX))); + assert_eq!(ua(i64::MAX as u64 + 1).to_signed(), Err(OutOfRangeError::too_big(true))); + + assert_eq!(sa(i64::MAX).to_unsigned(), Ok(ua(i64::MAX as u64))); + + assert_eq!(sa(i64::MAX).to_unsigned().unwrap().to_signed(), Ok(sa(i64::MAX))); +} + +#[test] +#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin. +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("-200000000000 BTC", Err(OutOfRangeError::too_small())); + case("18446744073709551616 sat", Err(OutOfRangeError::too_big(false))); + + ok_case(".5 bits", Amount::from_sat(50)); + ok_scase("-.5 bits", SignedAmount::from_sat(-50)); + ok_case("0.00253583 BTC", Amount::from_sat(253583)); + ok_scase("-5 satoshi", SignedAmount::from_sat(-5)); + ok_case("0.10000000 BTC", Amount::from_sat(100_000_00)); + ok_scase("-100 bits", SignedAmount::from_sat(-10_000)); + #[cfg(feature = "alloc")] + ok_scase(&format!("{} SAT", i64::MIN), SignedAmount::from_sat(i64::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 = Amount::from_sat; + let sa_str = SignedAmount::from_str_in; + let sa_sat = SignedAmount::from_sat; + + assert_eq!("0.5", Amount::from_sat(50).to_string_in(D::Bit)); + assert_eq!("-0.5", SignedAmount::from_sat(-50).to_string_in(D::Bit)); + assert_eq!("0.00253583", Amount::from_sat(253583).to_string_in(D::Bitcoin)); + assert_eq!("-5", SignedAmount::from_sat(-5).to_string_in(D::Satoshi)); + assert_eq!("0.1", Amount::from_sat(100_000_00).to_string_in(D::Bitcoin)); + assert_eq!("-100", SignedAmount::from_sat(-10_000).to_string_in(D::Bit)); + + assert_eq!("0.50", format!("{:.2}", Amount::from_sat(50).display_in(D::Bit))); + assert_eq!("-0.50", format!("{:.2}", SignedAmount::from_sat(-50).display_in(D::Bit))); + assert_eq!( + "0.10000000", + format!("{:.8}", Amount::from_sat(100_000_00).display_in(D::Bitcoin)) + ); + assert_eq!("-100.00", format!("{:.2}", SignedAmount::from_sat(-10_000).display_in(D::Bit))); + + assert_eq!(ua_str(&ua_sat(0).to_string_in(D::Satoshi), D::Satoshi), Ok(ua_sat(0))); + assert_eq!(ua_str(&ua_sat(500).to_string_in(D::Bitcoin), D::Bitcoin), Ok(ua_sat(500))); + 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(1).to_string_in(D::MicroBitcoin), D::MicroBitcoin), + 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!(ua_str(&ua_sat(u64::MAX).to_string_in(D::MilliBitcoin), D::MilliBitcoin).is_ok()); + + assert_eq!( + sa_str(&sa_sat(-1).to_string_in(D::MicroBitcoin), D::MicroBitcoin), + Ok(sa_sat(-1)) + ); + + assert_eq!( + sa_str(&sa_sat(i64::MAX).to_string_in(D::Satoshi), D::MicroBitcoin), + Err(OutOfRangeError::too_big(true).into()) + ); + // Test an overflow bug in `abs()` + assert_eq!( + sa_str(&sa_sat(i64::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 = Amount::from_sat(42); + let denom = Amount::to_string_with_denomination; + assert_eq!(denom(amt, D::Bitcoin).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: Amount::from_sat(123456789), samt: SignedAmount::from_sat(-123456789) }, + &[ + serde_test::Token::Struct { name: "T", len: 2 }, + serde_test::Token::Str("amt"), + serde_test::Token::U64(123456789), + serde_test::Token::Str("samt"), + serde_test::Token::I64(-123456789), + 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: Amount::from_sat(21_000_000__000_000_01), + samt: SignedAmount::from_sat(-21_000_000__000_000_01), + }; + + let json = "{\"amt\": 21000000.00000001, \ + \"samt\": -21000000.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::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] +#[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(Amount::from_sat(2_500_000_00)), + samt: Some(SignedAmount::from_sat(-2_500_000_00)), + }; + let without = T { amt: None, samt: None }; + + // Test Roundtripping + for s in [&with, &without].iter() { + 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(Amount::from_sat(2_500_000_00)), + samt: Some(SignedAmount::from_sat(-2_500_000_00)), + }; + let without = T { amt: None, samt: None }; + + // Test Roundtripping + for s in [&with, &without].iter() { + 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()); +} + +#[test] +fn sum_amounts() { + assert_eq!(Amount::from_sat(0), [].into_iter().sum::()); + assert_eq!(SignedAmount::from_sat(0), [].into_iter().sum::()); + + let amounts = [Amount::from_sat(42), Amount::from_sat(1337), Amount::from_sat(21)]; + let sum = amounts.into_iter().sum::(); + assert_eq!(Amount::from_sat(1400), sum); + + let amounts = + [SignedAmount::from_sat(-42), SignedAmount::from_sat(1337), SignedAmount::from_sat(21)]; + let sum = amounts.into_iter().sum::(); + assert_eq!(SignedAmount::from_sat(1316), sum); +} + +#[test] +fn checked_sum_amounts() { + assert_eq!(Some(Amount::from_sat(0)), [].into_iter().checked_sum()); + assert_eq!(Some(SignedAmount::from_sat(0)), [].into_iter().checked_sum()); + + let amounts = [Amount::from_sat(42), Amount::from_sat(1337), Amount::from_sat(21)]; + let sum = amounts.into_iter().checked_sum(); + assert_eq!(Some(Amount::from_sat(1400)), sum); + + let amounts = [Amount::from_sat(u64::MAX), Amount::from_sat(1337), Amount::from_sat(21)]; + let sum = amounts.into_iter().checked_sum(); + assert_eq!(None, sum); + + let amounts = [ + SignedAmount::from_sat(i64::MIN), + SignedAmount::from_sat(-1), + SignedAmount::from_sat(21), + ]; + let sum = amounts.into_iter().checked_sum(); + assert_eq!(None, sum); + + let amounts = [ + SignedAmount::from_sat(i64::MAX), + SignedAmount::from_sat(1), + SignedAmount::from_sat(21), + ]; + let sum = amounts.into_iter().checked_sum(); + assert_eq!(None, sum); + + let amounts = + [SignedAmount::from_sat(42), SignedAmount::from_sat(3301), SignedAmount::from_sat(21)]; + let sum = amounts.into_iter().checked_sum(); + assert_eq!(Some(SignedAmount::from_sat(3364)), sum); +} + +#[test] +fn denomination_string_acceptable_forms() { + // Non-exhaustive list of valid forms. + let valid = [ + "BTC", "btc", "mBTC", "mbtc", "uBTC", "ubtc", "SATOSHI", "satoshi", "SATOSHIS", + "satoshis", "SAT", "sat", "SATS", "sats", "bit", "bits", + ]; + for denom in valid.iter() { + assert!(denom.parse::().is_ok()); + } +} + +#[test] +fn disallow_confusing_forms() { + let confusing = ["CBTC", "Cbtc", "MBTC", "Mbtc", "UBTC", "Ubtc"]; + for denom in confusing.iter() { + 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.iter() { + 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!("{}", Amount::from_sat(1000000)), "0.01 BTC"); + assert_eq!(format!("{}", Amount::ONE_SAT), "0.00000001 BTC"); + assert_eq!(format!("{}", Amount::ONE_BTC), "1 BTC"); + assert_eq!(format!("{}", Amount::from_sat(1)), "0.00000001 BTC"); + assert_eq!(format!("{}", Amount::from_sat(10)), "0.0000001 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(10)), "0.00 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(100)), "0.00 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(1000)), "0.00 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(10_000)), "0.00 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(100_000)), "0.00 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(1_000_000)), "0.01 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(10_000_000)), "0.10 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(100_000_000)), "1.00 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(500_000)), "0.01 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(9_500_000)), "0.10 BTC"); + assert_eq!(format!("{:.2}", Amount::from_sat(99_500_000)), "1.00 BTC"); + assert_eq!(format!("{}", Amount::from_sat(100_000_000)), "1 BTC"); + assert_eq!(format!("{}", Amount::from_sat(40_000_000_000)), "400 BTC"); + assert_eq!(format!("{:.10}", Amount::from_sat(100_000_000)), "1.0000000000 BTC"); + assert_eq!(format!("{}", Amount::from_sat(400_000_000_000_010)), "4000000.0000001 BTC"); + assert_eq!(format!("{}", Amount::from_sat(400_000_000_000_000)), "4000000 BTC"); +}