From 851e4111ec10d0982bfa7a56aec2b48d9c5a578a Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 09:52:22 +1100 Subject: [PATCH 01/14] Run the formatter --- bitcoin/src/address/mod.rs | 4 ++-- bitcoin/src/blockdata/mod.rs | 5 ++++- hashes/src/siphash24.rs | 1 - primitives/src/lib.rs | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index 9eac89094..5875422f3 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -803,8 +803,8 @@ impl Address { /// Parse a bech32 Address string pub fn from_bech32_str(s: &str) -> Result, Bech32Error> { - let (hrp, witness_version, data) = bech32::segwit::decode(s) - .map_err(|e| Bech32Error::ParseBech32(ParseBech32Error(e)))?; + let (hrp, witness_version, data) = + bech32::segwit::decode(s).map_err(|e| Bech32Error::ParseBech32(ParseBech32Error(e)))?; let version = WitnessVersion::try_from(witness_version.to_u8())?; let program = WitnessProgram::new(version, &data) .expect("bech32 guarantees valid program length for witness"); diff --git a/bitcoin/src/blockdata/mod.rs b/bitcoin/src/blockdata/mod.rs index e48e4870d..a23230ab6 100644 --- a/bitcoin/src/blockdata/mod.rs +++ b/bitcoin/src/blockdata/mod.rs @@ -87,7 +87,10 @@ pub mod locktime { //! whether bit 22 of the `u32` consensus value is set. /// Re-export everything from the `primitives::locktime::relative` module. - pub use primitives::locktime::relative::{Height, LockTime, Time, TimeOverflowError, DisabledLockTimeError, IncompatibleHeightError, IncompatibleTimeError}; + pub use primitives::locktime::relative::{ + DisabledLockTimeError, Height, IncompatibleHeightError, IncompatibleTimeError, + LockTime, Time, TimeOverflowError, + }; } } diff --git a/hashes/src/siphash24.rs b/hashes/src/siphash24.rs index c9ebda442..973b9089d 100644 --- a/hashes/src/siphash24.rs +++ b/hashes/src/siphash24.rs @@ -1,4 +1,3 @@ - // SPDX-License-Identifier: CC0-1.0 //! SipHash 2-4 implementation. diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index e6f819732..592b9c70a 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -51,7 +51,7 @@ pub use units::amount::{Amount, SignedAmount}; pub use units::{ block::{BlockHeight, BlockInterval}, fee_rate::FeeRate, - weight::Weight + weight::Weight, }; #[doc(inline)] From abc54d034389fd2be75420648ac3e3b0b64a626f Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 09:09:33 +1100 Subject: [PATCH 02/14] Make amount module a directory In preparation for splitting the error types out of `amount.rs` into their own file move the `amount.rs` file to `amount/mod.rs`. File move only, no other changes. --- units/src/{amount.rs => amount/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename units/src/{amount.rs => amount/mod.rs} (100%) diff --git a/units/src/amount.rs b/units/src/amount/mod.rs similarity index 100% rename from units/src/amount.rs rename to units/src/amount/mod.rs From 5ce827c5e02483761be0bc8e17ea1c3c97ee7118 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 09:23:56 +1100 Subject: [PATCH 03/14] amount: Move error code to submodule There is _a lot_ of error types in the `amount` module. Move them to a separate `error` module. Add a bunch of `pub(super)` to keep things private to the `amount` module. Eventually we will want to close all these errors. --- units/src/amount/error.rs | 373 +++++++++++++++++++++++++++++++++++++ units/src/amount/mod.rs | 376 ++------------------------------------ 2 files changed, 384 insertions(+), 365 deletions(-) create mode 100644 units/src/amount/error.rs diff --git a/units/src/amount/error.rs b/units/src/amount/error.rs new file mode 100644 index 000000000..8c0e79fad --- /dev/null +++ b/units/src/amount/error.rs @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Error types for bitcoin amounts. + +use core::fmt; + +use internals::error::InputString; +use internals::write_err; + +use super::INPUT_STRING_LEN_LIMIT; + +/// An error during amount parsing amount with denomination. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum ParseError { + /// Invalid amount. + Amount(ParseAmountError), + + /// Invalid denomination. + Denomination(ParseDenominationError), + + /// The denomination was not identified. + MissingDenomination(MissingDenominationError), +} + +internals::impl_from_infallible!(ParseError); + +impl From for ParseError { + fn from(e: ParseAmountError) -> Self { Self::Amount(e) } +} + +impl From for ParseError { + fn from(e: ParseDenominationError) -> Self { Self::Denomination(e) } +} + +impl From for ParseError { + fn from(e: OutOfRangeError) -> Self { Self::Amount(e.into()) } +} + +impl From for ParseError { + fn from(e: TooPreciseError) -> Self { Self::Amount(e.into()) } +} + +impl From for ParseError { + fn from(e: MissingDigitsError) -> Self { Self::Amount(e.into()) } +} + +impl From for ParseError { + fn from(e: InputTooLargeError) -> Self { Self::Amount(e.into()) } +} + +impl From for ParseError { + fn from(e: InvalidCharacterError) -> Self { Self::Amount(e.into()) } +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseError::Amount(error) => write_err!(f, "invalid amount"; error), + ParseError::Denomination(error) => write_err!(f, "invalid denomination"; error), + // We consider this to not be a source because it currently doesn't contain useful + // information + ParseError::MissingDenomination(_) => + f.write_str("the input doesn't contain a denomination"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ParseError::Amount(error) => Some(error), + ParseError::Denomination(error) => Some(error), + // We consider this to not be a source because it currently doesn't contain useful + // information + ParseError::MissingDenomination(_) => None, + } + } +} + +/// An error during amount parsing. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum ParseAmountError { + /// The amount is too big or too small. + OutOfRange(OutOfRangeError), + /// Amount has higher precision than supported by the type. + TooPrecise(TooPreciseError), + /// A digit was expected but not found. + MissingDigits(MissingDigitsError), + /// Input string was too large. + InputTooLarge(InputTooLargeError), + /// Invalid character in input. + InvalidCharacter(InvalidCharacterError), +} + +impl From for ParseAmountError { + fn from(value: TooPreciseError) -> Self { Self::TooPrecise(value) } +} + +impl From for ParseAmountError { + fn from(value: MissingDigitsError) -> Self { Self::MissingDigits(value) } +} + +impl From for ParseAmountError { + fn from(value: InputTooLargeError) -> Self { Self::InputTooLarge(value) } +} + +impl From for ParseAmountError { + fn from(value: InvalidCharacterError) -> Self { Self::InvalidCharacter(value) } +} + +internals::impl_from_infallible!(ParseAmountError); + +impl fmt::Display for ParseAmountError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ParseAmountError::*; + + match *self { + OutOfRange(ref error) => write_err!(f, "amount out of range"; error), + TooPrecise(ref error) => write_err!(f, "amount has a too high precision"; error), + MissingDigits(ref error) => write_err!(f, "the input has too few digits"; error), + InputTooLarge(ref error) => write_err!(f, "the input is too large"; error), + InvalidCharacter(ref error) => write_err!(f, "invalid character in the input"; error), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseAmountError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use ParseAmountError::*; + + match *self { + TooPrecise(ref error) => Some(error), + InputTooLarge(ref error) => Some(error), + OutOfRange(ref error) => Some(error), + MissingDigits(ref error) => Some(error), + InvalidCharacter(ref error) => Some(error), + } + } +} + +/// Error returned when a parsed amount is too big or too small. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct OutOfRangeError { + pub(super) is_signed: bool, + pub(super) is_greater_than_max: bool, +} + +impl OutOfRangeError { + /// Returns the minimum and maximum allowed values for the type that was parsed. + /// + /// This can be used to give a hint to the user which values are allowed. + pub fn valid_range(&self) -> (i64, u64) { + match self.is_signed { + true => (i64::MIN, i64::MAX as u64), + false => (0, u64::MAX), + } + } + + /// Returns true if the input value was large than the maximum allowed value. + pub fn is_above_max(&self) -> bool { self.is_greater_than_max } + + /// Returns true if the input value was smaller than the minimum allowed value. + pub fn is_below_min(&self) -> bool { !self.is_greater_than_max } + + pub(crate) fn too_big(is_signed: bool) -> Self { Self { is_signed, is_greater_than_max: true } } + + pub(crate) fn too_small() -> Self { + Self { + // implied - negative() is used for the other + is_signed: true, + is_greater_than_max: false, + } + } + + pub(crate) fn negative() -> Self { + Self { + // implied - too_small() is used for the other + is_signed: false, + is_greater_than_max: false, + } + } +} + +impl fmt::Display for OutOfRangeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.is_greater_than_max { + write!(f, "the amount is greater than {}", self.valid_range().1) + } else { + write!(f, "the amount is less than {}", self.valid_range().0) + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for OutOfRangeError {} + +impl From for ParseAmountError { + fn from(value: OutOfRangeError) -> Self { ParseAmountError::OutOfRange(value) } +} + +/// Error returned when the input string has higher precision than satoshis. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct TooPreciseError { + pub(super) position: usize, +} + +impl fmt::Display for TooPreciseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.position { + 0 => f.write_str("the amount is less than 1 satoshi but it's not zero"), + pos => write!( + f, + "the digits starting from position {} represent a sub-satoshi amount", + pos + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TooPreciseError {} + +/// Error returned when the input string is too large. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct InputTooLargeError { + pub(super) len: usize, +} + +impl fmt::Display for InputTooLargeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.len - INPUT_STRING_LEN_LIMIT { + 1 => write!( + f, + "the input is one character longer than the maximum allowed length ({})", + INPUT_STRING_LEN_LIMIT + ), + n => write!( + f, + "the input is {} characters longer than the maximum allowed length ({})", + n, INPUT_STRING_LEN_LIMIT + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InputTooLargeError {} + +/// Error returned when digits were expected in the input but there were none. +/// +/// In particular, this is currently returned when the string is empty or only contains the minus sign. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MissingDigitsError { + pub(super) kind: MissingDigitsKind, +} + +impl fmt::Display for MissingDigitsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.kind { + MissingDigitsKind::Empty => f.write_str("the input is empty"), + MissingDigitsKind::OnlyMinusSign => + f.write_str("there are no digits following the minus (-) sign"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for MissingDigitsError {} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(super) enum MissingDigitsKind { + Empty, + OnlyMinusSign, +} + +/// Error returned when the input contains an invalid character. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InvalidCharacterError { + pub(super) invalid_char: char, + pub(super) position: usize, +} + +impl fmt::Display for InvalidCharacterError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.invalid_char { + '.' => f.write_str("there is more than one decimal separator (dot) in the input"), + '-' => f.write_str("there is more than one minus sign (-) in the input"), + c => write!( + f, + "the character '{}' at position {} is not a valid digit", + c, self.position + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InvalidCharacterError {} + +/// An error during amount parsing. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum ParseDenominationError { + /// The denomination was unknown. + Unknown(UnknownDenominationError), + /// The denomination has multiple possible interpretations. + PossiblyConfusing(PossiblyConfusingDenominationError), +} + +internals::impl_from_infallible!(ParseDenominationError); + +impl fmt::Display for ParseDenominationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ParseDenominationError::*; + + match *self { + Unknown(ref e) => write_err!(f, "denomination parse error"; e), + PossiblyConfusing(ref e) => write_err!(f, "denomination parse error"; e), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseDenominationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use ParseDenominationError::*; + + match *self { + Unknown(_) | PossiblyConfusing(_) => None, + } + } +} + +/// Error returned when the denomination is empty. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct MissingDenominationError; + +/// Error returned when parsing an unknown denomination. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct UnknownDenominationError(pub(super) InputString); + +impl fmt::Display for UnknownDenominationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.unknown_variant("bitcoin denomination", f) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for UnknownDenominationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} + +/// Error returned when parsing a possibly confusing denomination. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct PossiblyConfusingDenominationError(pub(super) InputString); + +impl fmt::Display for PossiblyConfusingDenominationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}: possibly confusing denomination - we intentionally do not support 'M' and 'P' so as to not confuse mega/milli and peta/pico", self.0.display_cannot_parse("bitcoin denomination")) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PossiblyConfusingDenominationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index aad698b89..02f393a28 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -5,6 +5,8 @@ //! This module mainly introduces the [`Amount`] and [`SignedAmount`] types. //! We refer to the documentation on the types for more information. +pub mod error; + #[cfg(feature = "alloc")] use alloc::string::{String, ToString}; use core::cmp::Ordering; @@ -15,12 +17,19 @@ use core::{default, fmt, ops}; use ::serde::{Deserialize, Serialize}; #[cfg(feature = "arbitrary")] use arbitrary::{Arbitrary, Unstructured}; -use internals::error::InputString; -use internals::write_err; +use self::error::MissingDigitsKind; #[cfg(feature = "alloc")] use crate::{FeeRate, Weight}; +#[rustfmt::skip] // Keep public re-exports separate. +#[doc(inline)] +pub use self::error::{ + InputTooLargeError, InvalidCharacterError, MissingDenominationError, MissingDigitsError, + OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError, + PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError, +}; + /// A set of denominations in which amounts can be expressed. /// /// # Accepted Denominations @@ -143,369 +152,6 @@ impl FromStr for Denomination { } } -/// An error during amount parsing amount with denomination. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub enum ParseError { - /// Invalid amount. - Amount(ParseAmountError), - - /// Invalid denomination. - Denomination(ParseDenominationError), - - /// The denomination was not identified. - MissingDenomination(MissingDenominationError), -} - -internals::impl_from_infallible!(ParseError); - -impl From for ParseError { - fn from(e: ParseAmountError) -> Self { Self::Amount(e) } -} - -impl From for ParseError { - fn from(e: ParseDenominationError) -> Self { Self::Denomination(e) } -} - -impl From for ParseError { - fn from(e: OutOfRangeError) -> Self { Self::Amount(e.into()) } -} - -impl From for ParseError { - fn from(e: TooPreciseError) -> Self { Self::Amount(e.into()) } -} - -impl From for ParseError { - fn from(e: MissingDigitsError) -> Self { Self::Amount(e.into()) } -} - -impl From for ParseError { - fn from(e: InputTooLargeError) -> Self { Self::Amount(e.into()) } -} - -impl From for ParseError { - fn from(e: InvalidCharacterError) -> Self { Self::Amount(e.into()) } -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ParseError::Amount(error) => write_err!(f, "invalid amount"; error), - ParseError::Denomination(error) => write_err!(f, "invalid denomination"; error), - // We consider this to not be a source because it currently doesn't contain useful - // information - ParseError::MissingDenomination(_) => - f.write_str("the input doesn't contain a denomination"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - ParseError::Amount(error) => Some(error), - ParseError::Denomination(error) => Some(error), - // We consider this to not be a source because it currently doesn't contain useful - // information - ParseError::MissingDenomination(_) => None, - } - } -} - -/// An error during amount parsing. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub enum ParseAmountError { - /// The amount is too big or too small. - OutOfRange(OutOfRangeError), - /// Amount has higher precision than supported by the type. - TooPrecise(TooPreciseError), - /// A digit was expected but not found. - MissingDigits(MissingDigitsError), - /// Input string was too large. - InputTooLarge(InputTooLargeError), - /// Invalid character in input. - InvalidCharacter(InvalidCharacterError), -} - -impl From for ParseAmountError { - fn from(value: TooPreciseError) -> Self { Self::TooPrecise(value) } -} - -impl From for ParseAmountError { - fn from(value: MissingDigitsError) -> Self { Self::MissingDigits(value) } -} - -impl From for ParseAmountError { - fn from(value: InputTooLargeError) -> Self { Self::InputTooLarge(value) } -} - -impl From for ParseAmountError { - fn from(value: InvalidCharacterError) -> Self { Self::InvalidCharacter(value) } -} - -internals::impl_from_infallible!(ParseAmountError); - -impl fmt::Display for ParseAmountError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ParseAmountError::*; - - match *self { - OutOfRange(ref error) => write_err!(f, "amount out of range"; error), - TooPrecise(ref error) => write_err!(f, "amount has a too high precision"; error), - MissingDigits(ref error) => write_err!(f, "the input has too few digits"; error), - InputTooLarge(ref error) => write_err!(f, "the input is too large"; error), - InvalidCharacter(ref error) => write_err!(f, "invalid character in the input"; error), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ParseAmountError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use ParseAmountError::*; - - match *self { - TooPrecise(ref error) => Some(error), - InputTooLarge(ref error) => Some(error), - OutOfRange(ref error) => Some(error), - MissingDigits(ref error) => Some(error), - InvalidCharacter(ref error) => Some(error), - } - } -} - -/// Error returned when a parsed amount is too big or too small. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct OutOfRangeError { - is_signed: bool, - is_greater_than_max: bool, -} - -impl OutOfRangeError { - /// Returns the minimum and maximum allowed values for the type that was parsed. - /// - /// This can be used to give a hint to the user which values are allowed. - pub fn valid_range(&self) -> (i64, u64) { - match self.is_signed { - true => (i64::MIN, i64::MAX as u64), - false => (0, u64::MAX), - } - } - - /// Returns true if the input value was large than the maximum allowed value. - pub fn is_above_max(&self) -> bool { self.is_greater_than_max } - - /// Returns true if the input value was smaller than the minimum allowed value. - pub fn is_below_min(&self) -> bool { !self.is_greater_than_max } - - pub(crate) fn too_big(is_signed: bool) -> Self { Self { is_signed, is_greater_than_max: true } } - - pub(crate) fn too_small() -> Self { - Self { - // implied - negative() is used for the other - is_signed: true, - is_greater_than_max: false, - } - } - - pub(crate) fn negative() -> Self { - Self { - // implied - too_small() is used for the other - is_signed: false, - is_greater_than_max: false, - } - } -} - -impl fmt::Display for OutOfRangeError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.is_greater_than_max { - write!(f, "the amount is greater than {}", self.valid_range().1) - } else { - write!(f, "the amount is less than {}", self.valid_range().0) - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for OutOfRangeError {} - -impl From for ParseAmountError { - fn from(value: OutOfRangeError) -> Self { ParseAmountError::OutOfRange(value) } -} - -/// Error returned when the input string has higher precision than satoshis. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct TooPreciseError { - position: usize, -} - -impl fmt::Display for TooPreciseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.position { - 0 => f.write_str("the amount is less than 1 satoshi but it's not zero"), - pos => write!( - f, - "the digits starting from position {} represent a sub-satoshi amount", - pos - ), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for TooPreciseError {} - -/// Error returned when the input string is too large. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct InputTooLargeError { - len: usize, -} - -impl fmt::Display for InputTooLargeError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.len - INPUT_STRING_LEN_LIMIT { - 1 => write!( - f, - "the input is one character longer than the maximum allowed length ({})", - INPUT_STRING_LEN_LIMIT - ), - n => write!( - f, - "the input is {} characters longer than the maximum allowed length ({})", - n, INPUT_STRING_LEN_LIMIT - ), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for InputTooLargeError {} - -/// Error returned when digits were expected in the input but there were none. -/// -/// In particular, this is currently returned when the string is empty or only contains the minus sign. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct MissingDigitsError { - kind: MissingDigitsKind, -} - -impl fmt::Display for MissingDigitsError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.kind { - MissingDigitsKind::Empty => f.write_str("the input is empty"), - MissingDigitsKind::OnlyMinusSign => - f.write_str("there are no digits following the minus (-) sign"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for MissingDigitsError {} - -#[derive(Debug, Clone, Eq, PartialEq)] -enum MissingDigitsKind { - Empty, - OnlyMinusSign, -} - -/// Error returned when the input contains an invalid character. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InvalidCharacterError { - invalid_char: char, - position: usize, -} - -impl fmt::Display for InvalidCharacterError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.invalid_char { - '.' => f.write_str("there is more than one decimal separator (dot) in the input"), - '-' => f.write_str("there is more than one minus sign (-) in the input"), - c => write!( - f, - "the character '{}' at position {} is not a valid digit", - c, self.position - ), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for InvalidCharacterError {} - -/// An error during amount parsing. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub enum ParseDenominationError { - /// The denomination was unknown. - Unknown(UnknownDenominationError), - /// The denomination has multiple possible interpretations. - PossiblyConfusing(PossiblyConfusingDenominationError), -} - -internals::impl_from_infallible!(ParseDenominationError); - -impl fmt::Display for ParseDenominationError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ParseDenominationError::*; - - match *self { - Unknown(ref e) => write_err!(f, "denomination parse error"; e), - PossiblyConfusing(ref e) => write_err!(f, "denomination parse error"; e), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ParseDenominationError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use ParseDenominationError::*; - - match *self { - Unknown(_) | PossiblyConfusing(_) => None, - } - } -} - -/// Error returned when the denomination is empty. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct MissingDenominationError; - -/// Error returned when parsing an unknown denomination. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct UnknownDenominationError(InputString); - -impl fmt::Display for UnknownDenominationError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.unknown_variant("bitcoin denomination", f) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for UnknownDenominationError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } -} - -/// Error returned when parsing a possibly confusing denomination. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct PossiblyConfusingDenominationError(InputString); - -impl fmt::Display for PossiblyConfusingDenominationError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}: possibly confusing denomination - we intentionally do not support 'M' and 'P' so as to not confuse mega/milli and peta/pico", self.0.display_cannot_parse("bitcoin denomination")) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for PossiblyConfusingDenominationError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } -} - /// Returns `Some(position)` if the precision is not supported. /// /// The position indicates the first digit that is too precise. From 01f907b7a68162f040c616619f44f4c2e177aad9 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 09:46:54 +1100 Subject: [PATCH 04/14] amount: Move verification code to submodule Code move only, no other changes. --- units/src/amount/mod.rs | 124 +------------------------------ units/src/amount/verification.rs | 121 ++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 121 deletions(-) create mode 100644 units/src/amount/verification.rs diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index 02f393a28..1923da2cc 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -7,6 +7,9 @@ pub mod error; +#[cfg(kani)] +mod verification; + #[cfg(feature = "alloc")] use alloc::string::{String, ToString}; use core::cmp::Ordering; @@ -1632,127 +1635,6 @@ pub mod serde { } } -#[cfg(kani)] -mod verification { - use std::cmp; - - use super::*; - - // Note regarding the `unwind` parameter: this defines how many iterations - // of loops kani will unwind before handing off to the SMT solver. Basically - // it should be set as low as possible such that Kani still succeeds (doesn't - // return "undecidable"). - // - // There is more info here: https://model-checking.github.io/kani/tutorial-loop-unwinding.html - // - // Unfortunately what it means to "loop" is pretty opaque ... in this case - // there appear to be loops in memcmp, which I guess comes from assert_eq!, - // though I didn't see any failures until I added the to_signed() test. - // Further confusing the issue, a value of 2 works fine on my system, but on - // CI it fails, so we need to set it higher. - #[kani::unwind(4)] - #[kani::proof] - fn u_amount_homomorphic() { - let n1 = kani::any::(); - let n2 = kani::any::(); - kani::assume(n1.checked_add(n2).is_some()); // assume we don't overflow in the actual test - assert_eq!(Amount::from_sat(n1) + Amount::from_sat(n2), Amount::from_sat(n1 + n2)); - - let mut amt = Amount::from_sat(n1); - amt += Amount::from_sat(n2); - assert_eq!(amt, Amount::from_sat(n1 + n2)); - - let max = cmp::max(n1, n2); - let min = cmp::min(n1, n2); - assert_eq!(Amount::from_sat(max) - Amount::from_sat(min), Amount::from_sat(max - min)); - - let mut amt = Amount::from_sat(max); - amt -= Amount::from_sat(min); - assert_eq!(amt, Amount::from_sat(max - min)); - - assert_eq!( - Amount::from_sat(n1).to_signed(), - if n1 <= i64::MAX as u64 { - Ok(SignedAmount::from_sat(n1.try_into().unwrap())) - } else { - Err(OutOfRangeError::too_big(true)) - }, - ); - } - - #[kani::unwind(4)] - #[kani::proof] - fn u_amount_homomorphic_checked() { - let n1 = kani::any::(); - let n2 = kani::any::(); - assert_eq!( - Amount::from_sat(n1).checked_add(Amount::from_sat(n2)), - n1.checked_add(n2).map(Amount::from_sat), - ); - assert_eq!( - Amount::from_sat(n1).checked_sub(Amount::from_sat(n2)), - n1.checked_sub(n2).map(Amount::from_sat), - ); - } - - #[kani::unwind(4)] - #[kani::proof] - fn s_amount_homomorphic() { - let n1 = kani::any::(); - let n2 = kani::any::(); - kani::assume(n1.checked_add(n2).is_some()); // assume we don't overflow in the actual test - kani::assume(n1.checked_sub(n2).is_some()); // assume we don't overflow in the actual test - assert_eq!( - SignedAmount::from_sat(n1) + SignedAmount::from_sat(n2), - SignedAmount::from_sat(n1 + n2) - ); - assert_eq!( - SignedAmount::from_sat(n1) - SignedAmount::from_sat(n2), - SignedAmount::from_sat(n1 - n2) - ); - - let mut amt = SignedAmount::from_sat(n1); - amt += SignedAmount::from_sat(n2); - assert_eq!(amt, SignedAmount::from_sat(n1 + n2)); - let mut amt = SignedAmount::from_sat(n1); - amt -= SignedAmount::from_sat(n2); - assert_eq!(amt, SignedAmount::from_sat(n1 - n2)); - - assert_eq!( - SignedAmount::from_sat(n1).to_unsigned(), - if n1 >= 0 { - Ok(Amount::from_sat(n1.try_into().unwrap())) - } else { - Err(OutOfRangeError { is_signed: false, is_greater_than_max: false }) - }, - ); - } - - #[kani::unwind(4)] - #[kani::proof] - fn s_amount_homomorphic_checked() { - let n1 = kani::any::(); - let n2 = kani::any::(); - assert_eq!( - SignedAmount::from_sat(n1).checked_add(SignedAmount::from_sat(n2)), - n1.checked_add(n2).map(SignedAmount::from_sat), - ); - assert_eq!( - SignedAmount::from_sat(n1).checked_sub(SignedAmount::from_sat(n2)), - n1.checked_sub(n2).map(SignedAmount::from_sat), - ); - - assert_eq!( - SignedAmount::from_sat(n1).positive_sub(SignedAmount::from_sat(n2)), - if n1 >= 0 && n2 >= 0 && n1 >= n2 { - Some(SignedAmount::from_sat(n1 - n2)) - } else { - None - }, - ); - } -} - #[cfg(test)] mod tests { #[cfg(feature = "alloc")] diff --git a/units/src/amount/verification.rs b/units/src/amount/verification.rs new file mode 100644 index 000000000..7e8648254 --- /dev/null +++ b/units/src/amount/verification.rs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Verification tests for the `amount` module. + +use std::cmp; + +use super::*; + +// Note regarding the `unwind` parameter: this defines how many iterations +// of loops kani will unwind before handing off to the SMT solver. Basically +// it should be set as low as possible such that Kani still succeeds (doesn't +// return "undecidable"). +// +// There is more info here: https://model-checking.github.io/kani/tutorial-loop-unwinding.html +// +// Unfortunately what it means to "loop" is pretty opaque ... in this case +// there appear to be loops in memcmp, which I guess comes from assert_eq!, +// though I didn't see any failures until I added the to_signed() test. +// Further confusing the issue, a value of 2 works fine on my system, but on +// CI it fails, so we need to set it higher. +#[kani::unwind(4)] +#[kani::proof] +fn u_amount_homomorphic() { + let n1 = kani::any::(); + let n2 = kani::any::(); + kani::assume(n1.checked_add(n2).is_some()); // assume we don't overflow in the actual test + assert_eq!(Amount::from_sat(n1) + Amount::from_sat(n2), Amount::from_sat(n1 + n2)); + + let mut amt = Amount::from_sat(n1); + amt += Amount::from_sat(n2); + assert_eq!(amt, Amount::from_sat(n1 + n2)); + + let max = cmp::max(n1, n2); + let min = cmp::min(n1, n2); + assert_eq!(Amount::from_sat(max) - Amount::from_sat(min), Amount::from_sat(max - min)); + + let mut amt = Amount::from_sat(max); + amt -= Amount::from_sat(min); + assert_eq!(amt, Amount::from_sat(max - min)); + + assert_eq!( + Amount::from_sat(n1).to_signed(), + if n1 <= i64::MAX as u64 { + Ok(SignedAmount::from_sat(n1.try_into().unwrap())) + } else { + Err(OutOfRangeError::too_big(true)) + }, + ); +} + +#[kani::unwind(4)] +#[kani::proof] +fn u_amount_homomorphic_checked() { + let n1 = kani::any::(); + let n2 = kani::any::(); + assert_eq!( + Amount::from_sat(n1).checked_add(Amount::from_sat(n2)), + n1.checked_add(n2).map(Amount::from_sat), + ); + assert_eq!( + Amount::from_sat(n1).checked_sub(Amount::from_sat(n2)), + n1.checked_sub(n2).map(Amount::from_sat), + ); +} + +#[kani::unwind(4)] +#[kani::proof] +fn s_amount_homomorphic() { + let n1 = kani::any::(); + let n2 = kani::any::(); + kani::assume(n1.checked_add(n2).is_some()); // assume we don't overflow in the actual test + kani::assume(n1.checked_sub(n2).is_some()); // assume we don't overflow in the actual test + assert_eq!( + SignedAmount::from_sat(n1) + SignedAmount::from_sat(n2), + SignedAmount::from_sat(n1 + n2) + ); + assert_eq!( + SignedAmount::from_sat(n1) - SignedAmount::from_sat(n2), + SignedAmount::from_sat(n1 - n2) + ); + + let mut amt = SignedAmount::from_sat(n1); + amt += SignedAmount::from_sat(n2); + assert_eq!(amt, SignedAmount::from_sat(n1 + n2)); + let mut amt = SignedAmount::from_sat(n1); + amt -= SignedAmount::from_sat(n2); + assert_eq!(amt, SignedAmount::from_sat(n1 - n2)); + + assert_eq!( + SignedAmount::from_sat(n1).to_unsigned(), + if n1 >= 0 { + Ok(Amount::from_sat(n1.try_into().unwrap())) + } else { + Err(OutOfRangeError { is_signed: false, is_greater_than_max: false }) + }, + ); +} + +#[kani::unwind(4)] +#[kani::proof] +fn s_amount_homomorphic_checked() { + let n1 = kani::any::(); + let n2 = kani::any::(); + assert_eq!( + SignedAmount::from_sat(n1).checked_add(SignedAmount::from_sat(n2)), + n1.checked_add(n2).map(SignedAmount::from_sat), + ); + assert_eq!( + SignedAmount::from_sat(n1).checked_sub(SignedAmount::from_sat(n2)), + n1.checked_sub(n2).map(SignedAmount::from_sat), + ); + + assert_eq!( + SignedAmount::from_sat(n1).positive_sub(SignedAmount::from_sat(n2)), + if n1 >= 0 && n2 >= 0 && n1 >= n2 { + Some(SignedAmount::from_sat(n1 - n2)) + } else { + None + }, + ); +} From cd5d1aba2f6b2939a30f0bbd3d2c38f0dbc2528d Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 09:53:30 +1100 Subject: [PATCH 05/14] amount: Format verification file Done as a separate patch so that the diff of the verification code move was less noisy. --- units/src/amount/verification.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/units/src/amount/verification.rs b/units/src/amount/verification.rs index 7e8648254..0310e19e9 100644 --- a/units/src/amount/verification.rs +++ b/units/src/amount/verification.rs @@ -112,10 +112,6 @@ fn s_amount_homomorphic_checked() { assert_eq!( SignedAmount::from_sat(n1).positive_sub(SignedAmount::from_sat(n2)), - if n1 >= 0 && n2 >= 0 && n1 >= n2 { - Some(SignedAmount::from_sat(n1 - n2)) - } else { - None - }, + if n1 >= 0 && n2 >= 0 && n1 >= n2 { Some(SignedAmount::from_sat(n1 - n2)) } else { None }, ); } From 7e1269704d9251692d2282e0f52a517187ee18b7 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 09:49:17 +1100 Subject: [PATCH 06/14] tests: Use from_sat Stop using private constructor in unit tests, use `from_sat` instead. --- units/src/amount/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index 1923da2cc..61bffe3f7 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -1705,7 +1705,7 @@ mod tests { 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(123)); + assert_eq!(sa_positive, SignedAmount::from_sat(123)); let ua_max = Amount::MAX; let result = SignedAmount::try_from(ua_max); @@ -1714,11 +1714,11 @@ mod tests { #[test] fn test_amount_try_from_signed_amount() { - let sa_positive = SignedAmount(123); + 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(-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 })); } @@ -1925,9 +1925,9 @@ mod tests { 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(1)).to_string_in(sat), sat).is_err() + SignedAmount::from_str_in(&(amount + Amount::from_sat(1)).to_string_in(sat), sat).is_err() ); - assert!(Amount::from_str_in(&(amount + Amount(1)).to_string_in(sat), sat).is_ok()); + assert!(Amount::from_str_in(&(amount + Amount::from_sat(1)).to_string_in(sat), sat).is_ok()); } // exactly 50 chars. From e0bc68042d9840a82b693ffe0f70e5523049b1a9 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 09:54:29 +1100 Subject: [PATCH 07/14] 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"); +} From 87c9a3fd11f8e913915be8e4ddfb65124bbf6212 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 11:37:40 +1100 Subject: [PATCH 08/14] amount: Format tests file Done as a separate patch so that the diff of the verification code move was less noisy. --- units/src/amount/tests.rs | 63 ++++++++++----------------------------- 1 file changed, 16 insertions(+), 47 deletions(-) diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs index 29ece202a..a695bb101 100644 --- a/units/src/amount/tests.rs +++ b/units/src/amount/tests.rs @@ -205,10 +205,7 @@ fn floating_point() { 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()) - ); + 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()); @@ -241,10 +238,7 @@ fn parsing() { 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("x", btc), Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 0 }))); assert_eq!( p("-", btc), Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })) @@ -286,19 +280,15 @@ fn parsing() { 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)) - ); + 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!(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()); } @@ -326,10 +316,7 @@ fn to_string() { 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!(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), @@ -617,32 +604,20 @@ fn to_from_string_in() { 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!("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(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(-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), @@ -855,19 +830,13 @@ fn checked_sum_amounts() { 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 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 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); @@ -881,8 +850,8 @@ fn checked_sum_amounts() { 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", + "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()); From df962673421280ee38492638fa3daf9e75e035c3 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 10:06:39 +1100 Subject: [PATCH 09/14] amount: Move serde code to submodule No changes other than moving the module code. --- units/src/amount/mod.rs | 280 +------------------------------------- units/src/amount/serde.rs | 276 +++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 278 deletions(-) create mode 100644 units/src/amount/serde.rs diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index 7e67f454c..d1ec3c58f 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -6,6 +6,8 @@ //! We refer to the documentation on the types for more information. pub mod error; +#[cfg(feature = "serde")] +pub mod serde; #[cfg(test)] mod tests; @@ -1358,281 +1360,3 @@ mod private { impl SumSeal for T where T: Iterator {} impl SumSeal for T where T: Iterator {} } - -#[cfg(feature = "serde")] -pub mod serde { - // methods are implementation of a standardized serde-specific signature - #![allow(missing_docs)] - - //! This module adds serde serialization and deserialization support for Amounts. - //! - //! Since there is not a default way to serialize and deserialize Amounts, multiple - //! ways are supported and it's up to the user to decide which serialiation to use. - //! The provided modules can be used as follows: - //! - //! ```rust,ignore - //! use serde::{Serialize, Deserialize}; - //! use bitcoin_units::Amount; - //! - //! #[derive(Serialize, Deserialize)] - //! pub struct HasAmount { - //! #[serde(with = "bitcoin_units::amount::serde::as_btc")] - //! pub amount: Amount, - //! } - //! ``` - - use core::fmt; - - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - - #[cfg(feature = "alloc")] - use super::Denomination; - use super::{Amount, ParseAmountError, SignedAmount}; - - /// This trait is used only to avoid code duplication and naming collisions - /// of the different serde serialization crates. - pub trait SerdeAmount: Copy + Sized { - fn ser_sat(self, s: S, _: private::Token) -> Result; - fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result; - #[cfg(feature = "alloc")] - fn ser_btc(self, s: S, _: private::Token) -> Result; - #[cfg(feature = "alloc")] - fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result; - } - - mod private { - /// Controls access to the trait methods. - pub struct Token; - } - - /// This trait is only for internal Amount type serialization/deserialization - pub trait SerdeAmountForOpt: Copy + Sized + SerdeAmount { - fn type_prefix(_: private::Token) -> &'static str; - fn ser_sat_opt(self, s: S, _: private::Token) -> Result; - #[cfg(feature = "alloc")] - fn ser_btc_opt(self, s: S, _: private::Token) -> Result; - } - - struct DisplayFullError(ParseAmountError); - - #[cfg(feature = "std")] - impl fmt::Display for DisplayFullError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use std::error::Error; - - fmt::Display::fmt(&self.0, f)?; - let mut source_opt = self.0.source(); - while let Some(source) = source_opt { - write!(f, ": {}", source)?; - source_opt = source.source(); - } - Ok(()) - } - } - - #[cfg(not(feature = "std"))] - impl fmt::Display for DisplayFullError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } - } - - impl SerdeAmount for Amount { - fn ser_sat(self, s: S, _: private::Token) -> Result { - u64::serialize(&self.to_sat(), s) - } - fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { - Ok(Amount::from_sat(u64::deserialize(d)?)) - } - #[cfg(feature = "alloc")] - fn ser_btc(self, s: S, _: private::Token) -> Result { - f64::serialize(&self.to_float_in(Denomination::Bitcoin), s) - } - #[cfg(feature = "alloc")] - fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { - use serde::de::Error; - Amount::from_btc(f64::deserialize(d)?) - .map_err(DisplayFullError) - .map_err(D::Error::custom) - } - } - - impl SerdeAmountForOpt for Amount { - fn type_prefix(_: private::Token) -> &'static str { "u" } - fn ser_sat_opt(self, s: S, _: private::Token) -> Result { - s.serialize_some(&self.to_sat()) - } - #[cfg(feature = "alloc")] - fn ser_btc_opt(self, s: S, _: private::Token) -> Result { - s.serialize_some(&self.to_btc()) - } - } - - impl SerdeAmount for SignedAmount { - fn ser_sat(self, s: S, _: private::Token) -> Result { - i64::serialize(&self.to_sat(), s) - } - fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { - Ok(SignedAmount::from_sat(i64::deserialize(d)?)) - } - #[cfg(feature = "alloc")] - fn ser_btc(self, s: S, _: private::Token) -> Result { - f64::serialize(&self.to_float_in(Denomination::Bitcoin), s) - } - #[cfg(feature = "alloc")] - fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { - use serde::de::Error; - SignedAmount::from_btc(f64::deserialize(d)?) - .map_err(DisplayFullError) - .map_err(D::Error::custom) - } - } - - impl SerdeAmountForOpt for SignedAmount { - fn type_prefix(_: private::Token) -> &'static str { "i" } - fn ser_sat_opt(self, s: S, _: private::Token) -> Result { - s.serialize_some(&self.to_sat()) - } - #[cfg(feature = "alloc")] - fn ser_btc_opt(self, s: S, _: private::Token) -> Result { - s.serialize_some(&self.to_btc()) - } - } - - pub mod as_sat { - //! Serialize and deserialize [`Amount`](crate::Amount) as real numbers denominated in satoshi. - //! Use with `#[serde(with = "amount::serde::as_sat")]`. - - use serde::{Deserializer, Serializer}; - - use super::private; - use crate::amount::serde::SerdeAmount; - - pub fn serialize(a: &A, s: S) -> Result { - a.ser_sat(s, private::Token) - } - - pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result { - A::des_sat(d, private::Token) - } - - pub mod opt { - //! Serialize and deserialize [`Option`](crate::Amount) as real numbers denominated in satoshi. - //! Use with `#[serde(default, with = "amount::serde::as_sat::opt")]`. - - use core::fmt; - use core::marker::PhantomData; - - use serde::{de, Deserializer, Serializer}; - - use super::private; - use crate::amount::serde::SerdeAmountForOpt; - - pub fn serialize( - a: &Option, - s: S, - ) -> Result { - match *a { - Some(a) => a.ser_sat_opt(s, private::Token), - None => s.serialize_none(), - } - } - - pub fn deserialize<'d, A: SerdeAmountForOpt, D: Deserializer<'d>>( - d: D, - ) -> Result, D::Error> { - struct VisitOptAmt(PhantomData); - - impl<'de, X: SerdeAmountForOpt> de::Visitor<'de> for VisitOptAmt { - type Value = Option; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "An Option<{}64>", X::type_prefix(private::Token)) - } - - fn visit_none(self) -> Result - where - E: de::Error, - { - Ok(None) - } - fn visit_some(self, d: D) -> Result - where - D: Deserializer<'de>, - { - Ok(Some(X::des_sat(d, private::Token)?)) - } - } - d.deserialize_option(VisitOptAmt::(PhantomData)) - } - } - } - - #[cfg(feature = "alloc")] - pub mod as_btc { - //! Serialize and deserialize [`Amount`](crate::Amount) as JSON numbers denominated in BTC. - //! Use with `#[serde(with = "amount::serde::as_btc")]`. - - use serde::{Deserializer, Serializer}; - - use super::private; - use crate::amount::serde::SerdeAmount; - - pub fn serialize(a: &A, s: S) -> Result { - a.ser_btc(s, private::Token) - } - - pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result { - A::des_btc(d, private::Token) - } - - pub mod opt { - //! Serialize and deserialize `Option` as JSON numbers denominated in BTC. - //! Use with `#[serde(default, with = "amount::serde::as_btc::opt")]`. - - use core::fmt; - use core::marker::PhantomData; - - use serde::{de, Deserializer, Serializer}; - - use super::private; - use crate::amount::serde::SerdeAmountForOpt; - - pub fn serialize( - a: &Option, - s: S, - ) -> Result { - match *a { - Some(a) => a.ser_btc_opt(s, private::Token), - None => s.serialize_none(), - } - } - - pub fn deserialize<'d, A: SerdeAmountForOpt, D: Deserializer<'d>>( - d: D, - ) -> Result, D::Error> { - struct VisitOptAmt(PhantomData); - - impl<'de, X: SerdeAmountForOpt> de::Visitor<'de> for VisitOptAmt { - type Value = Option; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "An Option") - } - - fn visit_none(self) -> Result - where - E: de::Error, - { - Ok(None) - } - fn visit_some(self, d: D) -> Result - where - D: Deserializer<'de>, - { - Ok(Some(X::des_btc(d, private::Token)?)) - } - } - d.deserialize_option(VisitOptAmt::(PhantomData)) - } - } - } -} diff --git a/units/src/amount/serde.rs b/units/src/amount/serde.rs new file mode 100644 index 000000000..6a1008cae --- /dev/null +++ b/units/src/amount/serde.rs @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: CC0-1.0 + +// methods are implementation of a standardized serde-specific signature +#![allow(missing_docs)] + +//! This module adds serde serialization and deserialization support for Amounts. +//! +//! Since there is not a default way to serialize and deserialize Amounts, multiple +//! ways are supported and it's up to the user to decide which serialiation to use. +//! The provided modules can be used as follows: +//! +//! ```rust,ignore +//! use serde::{Serialize, Deserialize}; +//! use bitcoin_units::Amount; +//! +//! #[derive(Serialize, Deserialize)] +//! pub struct HasAmount { +//! #[serde(with = "bitcoin_units::amount::serde::as_btc")] +//! pub amount: Amount, +//! } +//! ``` + +use core::fmt; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[cfg(feature = "alloc")] +use super::Denomination; +use super::{Amount, ParseAmountError, SignedAmount}; + +/// This trait is used only to avoid code duplication and naming collisions +/// of the different serde serialization crates. +pub trait SerdeAmount: Copy + Sized { + fn ser_sat(self, s: S, _: private::Token) -> Result; + fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result; + #[cfg(feature = "alloc")] + fn ser_btc(self, s: S, _: private::Token) -> Result; + #[cfg(feature = "alloc")] + fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result; +} + +mod private { + /// Controls access to the trait methods. + pub struct Token; +} + +/// This trait is only for internal Amount type serialization/deserialization +pub trait SerdeAmountForOpt: Copy + Sized + SerdeAmount { + fn type_prefix(_: private::Token) -> &'static str; + fn ser_sat_opt(self, s: S, _: private::Token) -> Result; + #[cfg(feature = "alloc")] + fn ser_btc_opt(self, s: S, _: private::Token) -> Result; +} + +struct DisplayFullError(ParseAmountError); + +#[cfg(feature = "std")] +impl fmt::Display for DisplayFullError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use std::error::Error; + + fmt::Display::fmt(&self.0, f)?; + let mut source_opt = self.0.source(); + while let Some(source) = source_opt { + write!(f, ": {}", source)?; + source_opt = source.source(); + } + Ok(()) + } +} + +#[cfg(not(feature = "std"))] +impl fmt::Display for DisplayFullError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } +} + +impl SerdeAmount for Amount { + fn ser_sat(self, s: S, _: private::Token) -> Result { + u64::serialize(&self.to_sat(), s) + } + fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { + Ok(Amount::from_sat(u64::deserialize(d)?)) + } + #[cfg(feature = "alloc")] + fn ser_btc(self, s: S, _: private::Token) -> Result { + f64::serialize(&self.to_float_in(Denomination::Bitcoin), s) + } + #[cfg(feature = "alloc")] + fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { + use serde::de::Error; + Amount::from_btc(f64::deserialize(d)?) + .map_err(DisplayFullError) + .map_err(D::Error::custom) + } +} + +impl SerdeAmountForOpt for Amount { + fn type_prefix(_: private::Token) -> &'static str { "u" } + fn ser_sat_opt(self, s: S, _: private::Token) -> Result { + s.serialize_some(&self.to_sat()) + } + #[cfg(feature = "alloc")] + fn ser_btc_opt(self, s: S, _: private::Token) -> Result { + s.serialize_some(&self.to_btc()) + } +} + +impl SerdeAmount for SignedAmount { + fn ser_sat(self, s: S, _: private::Token) -> Result { + i64::serialize(&self.to_sat(), s) + } + fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { + Ok(SignedAmount::from_sat(i64::deserialize(d)?)) + } + #[cfg(feature = "alloc")] + fn ser_btc(self, s: S, _: private::Token) -> Result { + f64::serialize(&self.to_float_in(Denomination::Bitcoin), s) + } + #[cfg(feature = "alloc")] + fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { + use serde::de::Error; + SignedAmount::from_btc(f64::deserialize(d)?) + .map_err(DisplayFullError) + .map_err(D::Error::custom) + } +} + +impl SerdeAmountForOpt for SignedAmount { + fn type_prefix(_: private::Token) -> &'static str { "i" } + fn ser_sat_opt(self, s: S, _: private::Token) -> Result { + s.serialize_some(&self.to_sat()) + } + #[cfg(feature = "alloc")] + fn ser_btc_opt(self, s: S, _: private::Token) -> Result { + s.serialize_some(&self.to_btc()) + } +} + +pub mod as_sat { + //! Serialize and deserialize [`Amount`](crate::Amount) as real numbers denominated in satoshi. + //! Use with `#[serde(with = "amount::serde::as_sat")]`. + + use serde::{Deserializer, Serializer}; + + use super::private; + use crate::amount::serde::SerdeAmount; + + pub fn serialize(a: &A, s: S) -> Result { + a.ser_sat(s, private::Token) + } + + pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result { + A::des_sat(d, private::Token) + } + + pub mod opt { + //! Serialize and deserialize [`Option`](crate::Amount) as real numbers denominated in satoshi. + //! Use with `#[serde(default, with = "amount::serde::as_sat::opt")]`. + + use core::fmt; + use core::marker::PhantomData; + + use serde::{de, Deserializer, Serializer}; + + use super::private; + use crate::amount::serde::SerdeAmountForOpt; + + pub fn serialize( + a: &Option, + s: S, + ) -> Result { + match *a { + Some(a) => a.ser_sat_opt(s, private::Token), + None => s.serialize_none(), + } + } + + pub fn deserialize<'d, A: SerdeAmountForOpt, D: Deserializer<'d>>( + d: D, + ) -> Result, D::Error> { + struct VisitOptAmt(PhantomData); + + impl<'de, X: SerdeAmountForOpt> de::Visitor<'de> for VisitOptAmt { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "An Option<{}64>", X::type_prefix(private::Token)) + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + fn visit_some(self, d: D) -> Result + where + D: Deserializer<'de>, + { + Ok(Some(X::des_sat(d, private::Token)?)) + } + } + d.deserialize_option(VisitOptAmt::(PhantomData)) + } + } +} + +#[cfg(feature = "alloc")] +pub mod as_btc { + //! Serialize and deserialize [`Amount`](crate::Amount) as JSON numbers denominated in BTC. + //! Use with `#[serde(with = "amount::serde::as_btc")]`. + + use serde::{Deserializer, Serializer}; + + use super::private; + use crate::amount::serde::SerdeAmount; + + pub fn serialize(a: &A, s: S) -> Result { + a.ser_btc(s, private::Token) + } + + pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result { + A::des_btc(d, private::Token) + } + + pub mod opt { + //! Serialize and deserialize `Option` as JSON numbers denominated in BTC. + //! Use with `#[serde(default, with = "amount::serde::as_btc::opt")]`. + + use core::fmt; + use core::marker::PhantomData; + + use serde::{de, Deserializer, Serializer}; + + use super::private; + use crate::amount::serde::SerdeAmountForOpt; + + pub fn serialize( + a: &Option, + s: S, + ) -> Result { + match *a { + Some(a) => a.ser_btc_opt(s, private::Token), + None => s.serialize_none(), + } + } + + pub fn deserialize<'d, A: SerdeAmountForOpt, D: Deserializer<'d>>( + d: D, + ) -> Result, D::Error> { + struct VisitOptAmt(PhantomData); + + impl<'de, X: SerdeAmountForOpt> de::Visitor<'de> for VisitOptAmt { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "An Option") + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + fn visit_some(self, d: D) -> Result + where + D: Deserializer<'de>, + { + Ok(Some(X::des_btc(d, private::Token)?)) + } + } + d.deserialize_option(VisitOptAmt::(PhantomData)) + } + } +} From 2d4c0fa6c11fc002f2f17fee58a53da79d93ee38 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 11:38:01 +1100 Subject: [PATCH 10/14] amount: Format serde file Done as a separate patch so that the diff of the verification code move was less noisy. --- units/src/amount/serde.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/units/src/amount/serde.rs b/units/src/amount/serde.rs index 6a1008cae..038b530ef 100644 --- a/units/src/amount/serde.rs +++ b/units/src/amount/serde.rs @@ -88,9 +88,7 @@ impl SerdeAmount for Amount { #[cfg(feature = "alloc")] fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result { use serde::de::Error; - Amount::from_btc(f64::deserialize(d)?) - .map_err(DisplayFullError) - .map_err(D::Error::custom) + Amount::from_btc(f64::deserialize(d)?).map_err(DisplayFullError).map_err(D::Error::custom) } } From 13f9fd1b77fb4a58c4d1b36a4ebc445f047c2659 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 10:08:25 +1100 Subject: [PATCH 11/14] amount: Move Amount to private unsigned module In an effort to make the `amount` module more readable move the `Amount` type to a private submodule. Re-export everything so this is not a breaking change. Code move and re-exports only. --- units/src/amount/mod.rs | 387 +--------------------------------- units/src/amount/unsigned.rs | 389 +++++++++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+), 378 deletions(-) create mode 100644 units/src/amount/unsigned.rs diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index d1ec3c58f..eaa909142 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -11,6 +11,7 @@ pub mod serde; #[cfg(test)] mod tests; +mod unsigned; #[cfg(kani)] mod verification; @@ -20,21 +21,20 @@ use core::cmp::Ordering; use core::str::FromStr; use core::{default, fmt, ops}; -#[cfg(feature = "serde")] -use ::serde::{Deserialize, Serialize}; #[cfg(feature = "arbitrary")] use arbitrary::{Arbitrary, Unstructured}; use self::error::MissingDigitsKind; -#[cfg(feature = "alloc")] -use crate::{FeeRate, Weight}; #[rustfmt::skip] // Keep public re-exports separate. #[doc(inline)] -pub use self::error::{ - InputTooLargeError, InvalidCharacterError, MissingDenominationError, MissingDigitsError, - OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError, - PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError, +pub use self::{ + error::{ + InputTooLargeError, InvalidCharacterError, MissingDenominationError, MissingDigitsError, + OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError, + PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError, + }, + unsigned::Amount, }; /// A set of denominations in which amounts can be expressed. @@ -499,375 +499,6 @@ fn fmt_satoshi_in( Ok(()) } -/// An amount. -/// -/// The [`Amount`] type can be used to express Bitcoin amounts that support -/// arithmetic and conversion to various denominations. -/// -/// -/// Warning! -/// -/// This type implements several arithmetic operations from [`core::ops`]. -/// To prevent errors due to overflow or underflow when using these operations, -/// it is advised to instead use the checked arithmetic methods whose names -/// start with `checked_`. The operations from [`core::ops`] that [`Amount`] -/// implements will panic when overflow or underflow occurs. Also note that -/// since the internal representation of amounts is unsigned, subtracting below -/// zero is considered an underflow and will cause a panic if you're not using -/// the checked arithmetic methods. -/// -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Amount(u64); - -impl Amount { - /// The zero amount. - pub const ZERO: Amount = Amount(0); - /// Exactly one satoshi. - pub const ONE_SAT: Amount = Amount(1); - /// Exactly one bitcoin. - pub const ONE_BTC: Amount = Self::from_int_btc(1); - /// The maximum value allowed as an amount. Useful for sanity checking. - pub const MAX_MONEY: Amount = Self::from_int_btc(21_000_000); - /// The minimum value of an amount. - pub const MIN: Amount = Amount::ZERO; - /// The maximum value of an amount. - pub const MAX: Amount = Amount(u64::MAX); - /// The number of bytes that an amount contributes to the size of a transaction. - pub const SIZE: usize = 8; // Serialized length of a u64. - - /// Creates an [`Amount`] with satoshi precision and the given number of satoshis. - pub const fn from_sat(satoshi: u64) -> Amount { Amount(satoshi) } - - /// Gets the number of satoshis in this [`Amount`]. - pub const fn to_sat(self) -> u64 { self.0 } - - /// Converts from a value expressing bitcoins to an [`Amount`]. - #[cfg(feature = "alloc")] - pub fn from_btc(btc: f64) -> Result { - Amount::from_float_in(btc, Denomination::Bitcoin) - } - - /// Converts from a value expressing integer values of bitcoins to an [`Amount`] - /// in const context. - /// - /// # Panics - /// - /// The function panics if the argument multiplied by the number of sats - /// per bitcoin overflows a u64 type. - pub const fn from_int_btc(btc: u64) -> Amount { - match btc.checked_mul(100_000_000) { - Some(amount) => Amount::from_sat(amount), - None => panic!("checked_mul overflowed"), - } - } - - /// Parses a decimal string as a value in the given denomination. - /// - /// Note: This only parses the value string. If you want to parse a value - /// with denomination, use [`FromStr`]. - pub fn from_str_in(s: &str, denom: Denomination) -> Result { - let (negative, satoshi) = - parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(false))?; - if negative { - return Err(ParseAmountError::OutOfRange(OutOfRangeError::negative())); - } - Ok(Amount::from_sat(satoshi)) - } - - /// Parses amounts with denomination suffix like they are produced with - /// [`Self::to_string_with_denomination`] or with [`fmt::Display`]. - /// If you want to parse only the amount without the denomination, - /// use [`Self::from_str_in`]. - pub fn from_str_with_denomination(s: &str) -> Result { - let (amt, denom) = split_amount_and_denomination(s)?; - Amount::from_str_in(amt, denom).map_err(Into::into) - } - - /// Expresses this [`Amount`] as a floating-point value in the given denomination. - /// - /// Please be aware of the risk of using floating-point numbers. - #[cfg(feature = "alloc")] - pub fn to_float_in(self, denom: Denomination) -> f64 { - self.to_string_in(denom).parse::().unwrap() - } - - /// Expresses this [`Amount`] as a floating-point value in Bitcoin. - /// - /// Please be aware of the risk of using floating-point numbers. - /// - /// # Examples - /// - /// ``` - /// # use bitcoin_units::amount::{Amount, Denomination}; - /// let amount = Amount::from_sat(100_000); - /// assert_eq!(amount.to_btc(), amount.to_float_in(Denomination::Bitcoin)) - /// ``` - #[cfg(feature = "alloc")] - pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) } - - /// Converts this [`Amount`] in floating-point notation with a given - /// denomination. - /// - /// # Errors - /// - /// If the amount is too big, too precise or negative. - /// - /// Please be aware of the risk of using floating-point numbers. - #[cfg(feature = "alloc")] - pub fn from_float_in(value: f64, denom: Denomination) -> Result { - if value < 0.0 { - return Err(OutOfRangeError::negative().into()); - } - // This is inefficient, but the safest way to deal with this. The parsing logic is safe. - // Any performance-critical application should not be dealing with floats. - Amount::from_str_in(&value.to_string(), denom) - } - - /// Creates an object that implements [`fmt::Display`] using specified denomination. - pub fn display_in(self, denomination: Denomination) -> Display { - Display { - sats_abs: self.to_sat(), - is_negative: false, - style: DisplayStyle::FixedDenomination { denomination, show_denomination: false }, - } - } - - /// Creates an object that implements [`fmt::Display`] dynamically selecting denomination. - /// - /// This will use BTC for values greater than or equal to 1 BTC and satoshis otherwise. To - /// avoid confusion the denomination is always shown. - pub fn display_dynamic(self) -> Display { - Display { - sats_abs: self.to_sat(), - is_negative: false, - style: DisplayStyle::DynamicDenomination, - } - } - - /// Formats the value of this [`Amount`] in the given denomination. - /// - /// Does not include the denomination. - #[rustfmt::skip] - #[deprecated(since = "TBD", note = "use `display_in()` instead")] - pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result { - fmt_satoshi_in(self.to_sat(), false, f, denom, false, FormatOptions::default()) - } - - /// Returns a formatted string of this [`Amount`] in the given denomination. - /// - /// Does not include the denomination. - #[cfg(feature = "alloc")] - pub fn to_string_in(self, denom: Denomination) -> String { self.display_in(denom).to_string() } - - /// Returns a formatted string of this [`Amount`] in the given denomination, - /// suffixed with the abbreviation for the denomination. - #[cfg(feature = "alloc")] - pub fn to_string_with_denomination(self, denom: Denomination) -> String { - self.display_in(denom).show_denomination().to_string() - } - - // Some arithmetic that doesn't fit in [`core::ops`] traits. - - /// Checked addition. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_add(self, rhs: Amount) -> Option { - self.0.checked_add(rhs.0).map(Amount) - } - - /// Checked subtraction. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_sub(self, rhs: Amount) -> Option { - self.0.checked_sub(rhs.0).map(Amount) - } - - /// Checked multiplication. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_mul(self, rhs: u64) -> Option { self.0.checked_mul(rhs).map(Amount) } - - /// Checked integer division. - /// - /// Be aware that integer division loses the remainder if no exact division - /// can be made. - /// Returns [`None`] if overflow occurred. - pub fn checked_div(self, rhs: u64) -> Option { self.0.checked_div(rhs).map(Amount) } - - /// Checked weight division. - /// - /// Be aware that integer division loses the remainder if no exact division - /// can be made. This method rounds up ensuring the transaction fee-rate is - /// sufficient. If you wish to round-down, use the unchecked version instead. - /// - /// [`None`] is returned if an overflow occurred. - #[cfg(feature = "alloc")] - pub fn checked_div_by_weight(self, rhs: Weight) -> Option { - let sats = self.0.checked_mul(1000)?; - let wu = rhs.to_wu(); - - let fee_rate = sats.checked_add(wu.checked_sub(1)?)?.checked_div(wu)?; - Some(FeeRate::from_sat_per_kwu(fee_rate)) - } - - /// Checked remainder. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_rem(self, rhs: u64) -> Option { self.0.checked_rem(rhs).map(Amount) } - - /// Unchecked addition. - /// - /// Computes `self + rhs`. - /// - /// # Panics - /// - /// On overflow, panics in debug mode, wraps in release mode. - pub fn unchecked_add(self, rhs: Amount) -> Amount { Self(self.0 + rhs.0) } - - /// Unchecked subtraction. - /// - /// Computes `self - rhs`. - /// - /// # Panics - /// - /// On overflow, panics in debug mode, wraps in release mode. - pub fn unchecked_sub(self, rhs: Amount) -> Amount { Self(self.0 - rhs.0) } - - /// Converts to a signed amount. - pub fn to_signed(self) -> Result { - if self.to_sat() > SignedAmount::MAX.to_sat() as u64 { - Err(OutOfRangeError::too_big(true)) - } else { - Ok(SignedAmount::from_sat(self.to_sat() as i64)) - } - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> Arbitrary<'a> for Amount { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - let a = u64::arbitrary(u)?; - Ok(Amount(a)) - } -} - -impl default::Default for Amount { - fn default() -> Self { Amount::ZERO } -} - -impl fmt::Debug for Amount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} SAT", self.to_sat()) } -} - -// No one should depend on a binding contract for Display for this type. -// Just using Bitcoin denominated string. -impl fmt::Display for Amount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.display_in(Denomination::Bitcoin).show_denomination(), f) - } -} - -impl ops::Add for Amount { - type Output = Amount; - - fn add(self, rhs: Amount) -> Self::Output { - self.checked_add(rhs).expect("Amount addition error") - } -} - -impl ops::AddAssign for Amount { - fn add_assign(&mut self, other: Amount) { *self = *self + other } -} - -impl ops::Sub for Amount { - type Output = Amount; - - fn sub(self, rhs: Amount) -> Self::Output { - self.checked_sub(rhs).expect("Amount subtraction error") - } -} - -impl ops::SubAssign for Amount { - fn sub_assign(&mut self, other: Amount) { *self = *self - other } -} - -impl ops::Rem for Amount { - type Output = Amount; - - fn rem(self, modulus: u64) -> Self { - self.checked_rem(modulus).expect("Amount remainder error") - } -} - -impl ops::RemAssign for Amount { - fn rem_assign(&mut self, modulus: u64) { *self = *self % modulus } -} - -impl ops::Mul for Amount { - type Output = Amount; - - fn mul(self, rhs: u64) -> Self::Output { - self.checked_mul(rhs).expect("Amount multiplication error") - } -} - -impl ops::MulAssign for Amount { - fn mul_assign(&mut self, rhs: u64) { *self = *self * rhs } -} - -impl ops::Div for Amount { - type Output = Amount; - - fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).expect("Amount division error") } -} - -impl ops::DivAssign for Amount { - fn div_assign(&mut self, rhs: u64) { *self = *self / rhs } -} - -impl FromStr for Amount { - type Err = ParseError; - - /// Parses a string slice where the slice includes a denomination. - /// - /// If the string slice is zero, then no denomination is required. - /// - /// # Returns - /// - /// `Ok(Amount)` if the string amount and denomination parse successfully, - /// otherwise, return `Err(ParseError)`. - fn from_str(s: &str) -> Result { - let result = Amount::from_str_with_denomination(s); - - match result { - Err(ParseError::MissingDenomination(_)) => { - let d = Amount::from_str_in(s, Denomination::Satoshi); - - if d == Ok(Amount::ZERO) { - Ok(Amount::ZERO) - } else { - result - } - } - _ => result, - } - } -} - -impl TryFrom for Amount { - type Error = OutOfRangeError; - - fn try_from(value: SignedAmount) -> Result { value.to_unsigned() } -} - -impl core::iter::Sum for Amount { - fn sum>(iter: I) -> Self { - let sats: u64 = iter.map(|amt| amt.0).sum(); - Amount::from_sat(sats) - } -} - /// A helper/builder that displays amount with specified settings. /// /// This provides richer interface than [`fmt::Formatter`]: @@ -1083,7 +714,7 @@ impl SignedAmount { pub fn abs(self) -> SignedAmount { SignedAmount(self.0.abs()) } /// Get the absolute value of this [`SignedAmount`] returning [`Amount`]. - pub fn unsigned_abs(self) -> Amount { Amount(self.0.unsigned_abs()) } + pub fn unsigned_abs(self) -> Amount { Amount::from_sat(self.0.unsigned_abs()) } /// Returns a number representing sign of this [`SignedAmount`]. /// diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs new file mode 100644 index 000000000..f638a1e13 --- /dev/null +++ b/units/src/amount/unsigned.rs @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An unsigned bitcoin amount. + +#[cfg(feature = "alloc")] +use alloc::string::{String, ToString}; +use core::str::FromStr; +use core::{default, fmt, ops}; + +#[cfg(feature = "serde")] +use ::serde::{Deserialize, Serialize}; +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Unstructured}; + +use super::{ + fmt_satoshi_in, parse_signed_to_satoshi, split_amount_and_denomination, Denomination, Display, + DisplayStyle, FormatOptions, OutOfRangeError, ParseAmountError, ParseError, SignedAmount, +}; +#[cfg(feature = "alloc")] +use crate::{FeeRate, Weight}; + +/// An amount. +/// +/// The [`Amount`] type can be used to express Bitcoin amounts that support +/// arithmetic and conversion to various denominations. +/// +/// +/// Warning! +/// +/// This type implements several arithmetic operations from [`core::ops`]. +/// To prevent errors due to overflow or underflow when using these operations, +/// it is advised to instead use the checked arithmetic methods whose names +/// start with `checked_`. The operations from [`core::ops`] that [`Amount`] +/// implements will panic when overflow or underflow occurs. Also note that +/// since the internal representation of amounts is unsigned, subtracting below +/// zero is considered an underflow and will cause a panic if you're not using +/// the checked arithmetic methods. +/// +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Amount(u64); + +impl Amount { + /// The zero amount. + pub const ZERO: Amount = Amount(0); + /// Exactly one satoshi. + pub const ONE_SAT: Amount = Amount(1); + /// Exactly one bitcoin. + pub const ONE_BTC: Amount = Self::from_int_btc(1); + /// The maximum value allowed as an amount. Useful for sanity checking. + pub const MAX_MONEY: Amount = Self::from_int_btc(21_000_000); + /// The minimum value of an amount. + pub const MIN: Amount = Amount::ZERO; + /// The maximum value of an amount. + pub const MAX: Amount = Amount(u64::MAX); + /// The number of bytes that an amount contributes to the size of a transaction. + pub const SIZE: usize = 8; // Serialized length of a u64. + + /// Creates an [`Amount`] with satoshi precision and the given number of satoshis. + pub const fn from_sat(satoshi: u64) -> Amount { Amount(satoshi) } + + /// Gets the number of satoshis in this [`Amount`]. + pub const fn to_sat(self) -> u64 { self.0 } + + /// Converts from a value expressing bitcoins to an [`Amount`]. + #[cfg(feature = "alloc")] + pub fn from_btc(btc: f64) -> Result { + Amount::from_float_in(btc, Denomination::Bitcoin) + } + + /// Converts from a value expressing integer values of bitcoins to an [`Amount`] + /// in const context. + /// + /// # Panics + /// + /// The function panics if the argument multiplied by the number of sats + /// per bitcoin overflows a u64 type. + pub const fn from_int_btc(btc: u64) -> Amount { + match btc.checked_mul(100_000_000) { + Some(amount) => Amount::from_sat(amount), + None => panic!("checked_mul overflowed"), + } + } + + /// Parses a decimal string as a value in the given denomination. + /// + /// Note: This only parses the value string. If you want to parse a value + /// with denomination, use [`FromStr`]. + pub fn from_str_in(s: &str, denom: Denomination) -> Result { + let (negative, satoshi) = + parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(false))?; + if negative { + return Err(ParseAmountError::OutOfRange(OutOfRangeError::negative())); + } + Ok(Amount::from_sat(satoshi)) + } + + /// Parses amounts with denomination suffix like they are produced with + /// [`Self::to_string_with_denomination`] or with [`fmt::Display`]. + /// If you want to parse only the amount without the denomination, + /// use [`Self::from_str_in`]. + pub fn from_str_with_denomination(s: &str) -> Result { + let (amt, denom) = split_amount_and_denomination(s)?; + Amount::from_str_in(amt, denom).map_err(Into::into) + } + + /// Expresses this [`Amount`] as a floating-point value in the given denomination. + /// + /// Please be aware of the risk of using floating-point numbers. + #[cfg(feature = "alloc")] + pub fn to_float_in(self, denom: Denomination) -> f64 { + self.to_string_in(denom).parse::().unwrap() + } + + /// Expresses this [`Amount`] as a floating-point value in Bitcoin. + /// + /// Please be aware of the risk of using floating-point numbers. + /// + /// # Examples + /// + /// ``` + /// # use bitcoin_units::amount::{Amount, Denomination}; + /// let amount = Amount::from_sat(100_000); + /// assert_eq!(amount.to_btc(), amount.to_float_in(Denomination::Bitcoin)) + /// ``` + #[cfg(feature = "alloc")] + pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) } + + /// Converts this [`Amount`] in floating-point notation with a given + /// denomination. + /// + /// # Errors + /// + /// If the amount is too big, too precise or negative. + /// + /// Please be aware of the risk of using floating-point numbers. + #[cfg(feature = "alloc")] + pub fn from_float_in(value: f64, denom: Denomination) -> Result { + if value < 0.0 { + return Err(OutOfRangeError::negative().into()); + } + // This is inefficient, but the safest way to deal with this. The parsing logic is safe. + // Any performance-critical application should not be dealing with floats. + Amount::from_str_in(&value.to_string(), denom) + } + + /// Creates an object that implements [`fmt::Display`] using specified denomination. + pub fn display_in(self, denomination: Denomination) -> Display { + Display { + sats_abs: self.to_sat(), + is_negative: false, + style: DisplayStyle::FixedDenomination { denomination, show_denomination: false }, + } + } + + /// Creates an object that implements [`fmt::Display`] dynamically selecting denomination. + /// + /// This will use BTC for values greater than or equal to 1 BTC and satoshis otherwise. To + /// avoid confusion the denomination is always shown. + pub fn display_dynamic(self) -> Display { + Display { + sats_abs: self.to_sat(), + is_negative: false, + style: DisplayStyle::DynamicDenomination, + } + } + + /// Formats the value of this [`Amount`] in the given denomination. + /// + /// Does not include the denomination. + #[rustfmt::skip] + #[deprecated(since = "TBD", note = "use `display_in()` instead")] + pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result { + fmt_satoshi_in(self.to_sat(), false, f, denom, false, FormatOptions::default()) + } + + /// Returns a formatted string of this [`Amount`] in the given denomination. + /// + /// Does not include the denomination. + #[cfg(feature = "alloc")] + pub fn to_string_in(self, denom: Denomination) -> String { self.display_in(denom).to_string() } + + /// Returns a formatted string of this [`Amount`] in the given denomination, + /// suffixed with the abbreviation for the denomination. + #[cfg(feature = "alloc")] + pub fn to_string_with_denomination(self, denom: Denomination) -> String { + self.display_in(denom).show_denomination().to_string() + } + + // Some arithmetic that doesn't fit in [`core::ops`] traits. + + /// Checked addition. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_add(self, rhs: Amount) -> Option { + self.0.checked_add(rhs.0).map(Amount) + } + + /// Checked subtraction. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_sub(self, rhs: Amount) -> Option { + self.0.checked_sub(rhs.0).map(Amount) + } + + /// Checked multiplication. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_mul(self, rhs: u64) -> Option { self.0.checked_mul(rhs).map(Amount) } + + /// Checked integer division. + /// + /// Be aware that integer division loses the remainder if no exact division + /// can be made. + /// Returns [`None`] if overflow occurred. + pub fn checked_div(self, rhs: u64) -> Option { self.0.checked_div(rhs).map(Amount) } + + /// Checked weight division. + /// + /// Be aware that integer division loses the remainder if no exact division + /// can be made. This method rounds up ensuring the transaction fee-rate is + /// sufficient. If you wish to round-down, use the unchecked version instead. + /// + /// [`None`] is returned if an overflow occurred. + #[cfg(feature = "alloc")] + pub fn checked_div_by_weight(self, rhs: Weight) -> Option { + let sats = self.0.checked_mul(1000)?; + let wu = rhs.to_wu(); + + let fee_rate = sats.checked_add(wu.checked_sub(1)?)?.checked_div(wu)?; + Some(FeeRate::from_sat_per_kwu(fee_rate)) + } + + /// Checked remainder. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_rem(self, rhs: u64) -> Option { self.0.checked_rem(rhs).map(Amount) } + + /// Unchecked addition. + /// + /// Computes `self + rhs`. + /// + /// # Panics + /// + /// On overflow, panics in debug mode, wraps in release mode. + pub fn unchecked_add(self, rhs: Amount) -> Amount { Self(self.0 + rhs.0) } + + /// Unchecked subtraction. + /// + /// Computes `self - rhs`. + /// + /// # Panics + /// + /// On overflow, panics in debug mode, wraps in release mode. + pub fn unchecked_sub(self, rhs: Amount) -> Amount { Self(self.0 - rhs.0) } + + /// Converts to a signed amount. + pub fn to_signed(self) -> Result { + if self.to_sat() > SignedAmount::MAX.to_sat() as u64 { + Err(OutOfRangeError::too_big(true)) + } else { + Ok(SignedAmount::from_sat(self.to_sat() as i64)) + } + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for Amount { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let a = u64::arbitrary(u)?; + Ok(Amount(a)) + } +} + +impl default::Default for Amount { + fn default() -> Self { Amount::ZERO } +} + +impl fmt::Debug for Amount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} SAT", self.to_sat()) } +} + +// No one should depend on a binding contract for Display for this type. +// Just using Bitcoin denominated string. +impl fmt::Display for Amount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.display_in(Denomination::Bitcoin).show_denomination(), f) + } +} + +impl ops::Add for Amount { + type Output = Amount; + + fn add(self, rhs: Amount) -> Self::Output { + self.checked_add(rhs).expect("Amount addition error") + } +} + +impl ops::AddAssign for Amount { + fn add_assign(&mut self, other: Amount) { *self = *self + other } +} + +impl ops::Sub for Amount { + type Output = Amount; + + fn sub(self, rhs: Amount) -> Self::Output { + self.checked_sub(rhs).expect("Amount subtraction error") + } +} + +impl ops::SubAssign for Amount { + fn sub_assign(&mut self, other: Amount) { *self = *self - other } +} + +impl ops::Rem for Amount { + type Output = Amount; + + fn rem(self, modulus: u64) -> Self { + self.checked_rem(modulus).expect("Amount remainder error") + } +} + +impl ops::RemAssign for Amount { + fn rem_assign(&mut self, modulus: u64) { *self = *self % modulus } +} + +impl ops::Mul for Amount { + type Output = Amount; + + fn mul(self, rhs: u64) -> Self::Output { + self.checked_mul(rhs).expect("Amount multiplication error") + } +} + +impl ops::MulAssign for Amount { + fn mul_assign(&mut self, rhs: u64) { *self = *self * rhs } +} + +impl ops::Div for Amount { + type Output = Amount; + + fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).expect("Amount division error") } +} + +impl ops::DivAssign for Amount { + fn div_assign(&mut self, rhs: u64) { *self = *self / rhs } +} + +impl FromStr for Amount { + type Err = ParseError; + + /// Parses a string slice where the slice includes a denomination. + /// + /// If the string slice is zero, then no denomination is required. + /// + /// # Returns + /// + /// `Ok(Amount)` if the string amount and denomination parse successfully, + /// otherwise, return `Err(ParseError)`. + fn from_str(s: &str) -> Result { + let result = Amount::from_str_with_denomination(s); + + match result { + Err(ParseError::MissingDenomination(_)) => { + let d = Amount::from_str_in(s, Denomination::Satoshi); + + if d == Ok(Amount::ZERO) { + Ok(Amount::ZERO) + } else { + result + } + } + _ => result, + } + } +} + +impl TryFrom for Amount { + type Error = OutOfRangeError; + + fn try_from(value: SignedAmount) -> Result { value.to_unsigned() } +} + +impl core::iter::Sum for Amount { + fn sum>(iter: I) -> Self { + let sats: u64 = iter.map(|amt| amt.0).sum(); + Amount::from_sat(sats) + } +} From 0fc0e8760b87f02627c0524a3af1638844d9021e Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 12:54:39 +1100 Subject: [PATCH 12/14] docs: Remove link from self When we move `SignedAmount` to a submodule linking to `self` introduces a clippy warning, I'm not exactly sure why but lets remove the link in preparation for the move. --- units/src/amount/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index eaa909142..b496c73c2 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -800,7 +800,7 @@ impl SignedAmount { /// Subtraction that doesn't allow negative [`SignedAmount`]s. /// - /// Returns [`None`] if either [`self`], `rhs` or the result is strictly negative. + /// Returns [`None`] if either `self`, `rhs` or the result is strictly negative. pub fn positive_sub(self, rhs: SignedAmount) -> Option { if self.is_negative() || rhs.is_negative() || rhs > self { None From d4d931160397830ca15fe2ca58a26a5bacf7f435 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 11:36:06 +1100 Subject: [PATCH 13/14] amount: Move SignedAmount to private signed module In an effort to make the `amount` module more readable move the `SignedAmount` type to a private submodule. Re-export everything so this is not a breaking change. Code move and re-exports only. --- units/src/amount/mod.rs | 400 +----------------------------------- units/src/amount/signed.rs | 407 +++++++++++++++++++++++++++++++++++++ 2 files changed, 410 insertions(+), 397 deletions(-) create mode 100644 units/src/amount/signed.rs diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs index b496c73c2..f78b5c378 100644 --- a/units/src/amount/mod.rs +++ b/units/src/amount/mod.rs @@ -9,20 +9,16 @@ pub mod error; #[cfg(feature = "serde")] pub mod serde; +mod signed; #[cfg(test)] mod tests; mod unsigned; #[cfg(kani)] mod verification; -#[cfg(feature = "alloc")] -use alloc::string::{String, ToString}; use core::cmp::Ordering; +use core::fmt; use core::str::FromStr; -use core::{default, fmt, ops}; - -#[cfg(feature = "arbitrary")] -use arbitrary::{Arbitrary, Unstructured}; use self::error::MissingDigitsKind; @@ -34,6 +30,7 @@ pub use self::{ OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError, PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError, }, + signed::SignedAmount, unsigned::Amount, }; @@ -562,397 +559,6 @@ enum DisplayStyle { DynamicDenomination, } -/// A signed amount. -/// -/// The [`SignedAmount`] type can be used to express Bitcoin amounts that support -/// arithmetic and conversion to various denominations. -/// -/// -/// Warning! -/// -/// This type implements several arithmetic operations from [`core::ops`]. -/// To prevent errors due to overflow or underflow when using these operations, -/// it is advised to instead use the checked arithmetic methods whose names -/// start with `checked_`. The operations from [`core::ops`] that [`Amount`] -/// implements will panic when overflow or underflow occurs. -/// -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SignedAmount(i64); - -impl SignedAmount { - /// The zero amount. - pub const ZERO: SignedAmount = SignedAmount(0); - /// Exactly one satoshi. - pub const ONE_SAT: SignedAmount = SignedAmount(1); - /// Exactly one bitcoin. - pub const ONE_BTC: SignedAmount = SignedAmount(100_000_000); - /// The maximum value allowed as an amount. Useful for sanity checking. - pub const MAX_MONEY: SignedAmount = SignedAmount(21_000_000 * 100_000_000); - /// The minimum value of an amount. - pub const MIN: SignedAmount = SignedAmount(i64::MIN); - /// The maximum value of an amount. - pub const MAX: SignedAmount = SignedAmount(i64::MAX); - - /// Create an [`SignedAmount`] with satoshi precision and the given number of satoshis. - pub const fn from_sat(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) } - - /// Gets the number of satoshis in this [`SignedAmount`]. - pub const fn to_sat(self) -> i64 { self.0 } - - /// Convert from a value expressing bitcoins to an [`SignedAmount`]. - #[cfg(feature = "alloc")] - pub fn from_btc(btc: f64) -> Result { - SignedAmount::from_float_in(btc, Denomination::Bitcoin) - } - - /// Parse a decimal string as a value in the given denomination. - /// - /// Note: This only parses the value string. If you want to parse a value - /// with denomination, use [`FromStr`]. - pub fn from_str_in(s: &str, denom: Denomination) -> Result { - match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? { - // (negative, amount) - (false, sat) if sat > i64::MAX as u64 => - Err(ParseAmountError::OutOfRange(OutOfRangeError::too_big(true))), - (false, sat) => Ok(SignedAmount(sat as i64)), - (true, sat) if sat == i64::MIN.unsigned_abs() => Ok(SignedAmount(i64::MIN)), - (true, sat) if sat > i64::MIN.unsigned_abs() => - Err(ParseAmountError::OutOfRange(OutOfRangeError::too_small())), - (true, sat) => Ok(SignedAmount(-(sat as i64))), - } - } - - /// Parses amounts with denomination suffix like they are produced with - /// [`Self::to_string_with_denomination`] or with [`fmt::Display`]. - /// If you want to parse only the amount without the denomination, - /// use [`Self::from_str_in`]. - pub fn from_str_with_denomination(s: &str) -> Result { - let (amt, denom) = split_amount_and_denomination(s)?; - SignedAmount::from_str_in(amt, denom).map_err(Into::into) - } - - /// Express this [`SignedAmount`] as a floating-point value in the given denomination. - /// - /// Please be aware of the risk of using floating-point numbers. - #[cfg(feature = "alloc")] - pub fn to_float_in(self, denom: Denomination) -> f64 { - self.to_string_in(denom).parse::().unwrap() - } - - /// Express this [`SignedAmount`] as a floating-point value in Bitcoin. - /// - /// Equivalent to `to_float_in(Denomination::Bitcoin)`. - /// - /// Please be aware of the risk of using floating-point numbers. - #[cfg(feature = "alloc")] - pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) } - - /// Convert this [`SignedAmount`] in floating-point notation with a given - /// denomination. - /// - /// # Errors - /// - /// If the amount is too big, too precise or negative. - /// - /// Please be aware of the risk of using floating-point numbers. - #[cfg(feature = "alloc")] - pub fn from_float_in( - value: f64, - denom: Denomination, - ) -> Result { - // This is inefficient, but the safest way to deal with this. The parsing logic is safe. - // Any performance-critical application should not be dealing with floats. - SignedAmount::from_str_in(&value.to_string(), denom) - } - - /// Create an object that implements [`fmt::Display`] using specified denomination. - pub fn display_in(self, denomination: Denomination) -> Display { - Display { - sats_abs: self.unsigned_abs().to_sat(), - is_negative: self.is_negative(), - style: DisplayStyle::FixedDenomination { denomination, show_denomination: false }, - } - } - - /// Create an object that implements [`fmt::Display`] dynamically selecting denomination. - /// - /// This will use BTC for values greater than or equal to 1 BTC and satoshis otherwise. To - /// avoid confusion the denomination is always shown. - pub fn display_dynamic(self) -> Display { - Display { - sats_abs: self.unsigned_abs().to_sat(), - is_negative: self.is_negative(), - style: DisplayStyle::DynamicDenomination, - } - } - - /// Format the value of this [`SignedAmount`] in the given denomination. - /// - /// Does not include the denomination. - #[rustfmt::skip] - #[deprecated(since = "TBD", note = "use `display_in()` instead")] - pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result { - fmt_satoshi_in(self.unsigned_abs().to_sat(), self.is_negative(), f, denom, false, FormatOptions::default()) - } - - /// Get a string number of this [`SignedAmount`] in the given denomination. - /// - /// Does not include the denomination. - #[cfg(feature = "alloc")] - pub fn to_string_in(self, denom: Denomination) -> String { self.display_in(denom).to_string() } - - /// Get a formatted string of this [`SignedAmount`] in the given denomination, - /// suffixed with the abbreviation for the denomination. - #[cfg(feature = "alloc")] - pub fn to_string_with_denomination(self, denom: Denomination) -> String { - self.display_in(denom).show_denomination().to_string() - } - - // Some arithmetic that doesn't fit in [`core::ops`] traits. - - /// Get the absolute value of this [`SignedAmount`]. - pub fn abs(self) -> SignedAmount { SignedAmount(self.0.abs()) } - - /// Get the absolute value of this [`SignedAmount`] returning [`Amount`]. - pub fn unsigned_abs(self) -> Amount { Amount::from_sat(self.0.unsigned_abs()) } - - /// Returns a number representing sign of this [`SignedAmount`]. - /// - /// - `0` if the amount is zero - /// - `1` if the amount is positive - /// - `-1` if the amount is negative - pub fn signum(self) -> i64 { self.0.signum() } - - /// Checks if this [`SignedAmount`] is positive. - /// - /// Returns `true` if this [`SignedAmount`] is positive and `false` if - /// this [`SignedAmount`] is zero or negative. - pub fn is_positive(self) -> bool { self.0.is_positive() } - - /// Checks if this [`SignedAmount`] is negative. - /// - /// Returns `true` if this [`SignedAmount`] is negative and `false` if - /// this [`SignedAmount`] is zero or positive. - pub fn is_negative(self) -> bool { self.0.is_negative() } - - /// Returns the absolute value of this [`SignedAmount`]. - /// - /// Consider using `unsigned_abs` which is often more practical. - /// - /// Returns [`None`] if overflow occurred. (`self == MIN`) - pub fn checked_abs(self) -> Option { self.0.checked_abs().map(SignedAmount) } - - /// Checked addition. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_add(self, rhs: SignedAmount) -> Option { - self.0.checked_add(rhs.0).map(SignedAmount) - } - - /// Checked subtraction. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_sub(self, rhs: SignedAmount) -> Option { - self.0.checked_sub(rhs.0).map(SignedAmount) - } - - /// Checked multiplication. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_mul(self, rhs: i64) -> Option { - self.0.checked_mul(rhs).map(SignedAmount) - } - - /// Checked integer division. - /// - /// Be aware that integer division loses the remainder if no exact division - /// can be made. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_div(self, rhs: i64) -> Option { - self.0.checked_div(rhs).map(SignedAmount) - } - - /// Checked remainder. - /// - /// Returns [`None`] if overflow occurred. - pub fn checked_rem(self, rhs: i64) -> Option { - self.0.checked_rem(rhs).map(SignedAmount) - } - - /// Unchecked addition. - /// - /// Computes `self + rhs`. - /// - /// # Panics - /// - /// On overflow, panics in debug mode, wraps in release mode. - pub fn unchecked_add(self, rhs: SignedAmount) -> SignedAmount { Self(self.0 + rhs.0) } - - /// Unchecked subtraction. - /// - /// Computes `self - rhs`. - /// - /// # Panics - /// - /// On overflow, panics in debug mode, wraps in release mode. - pub fn unchecked_sub(self, rhs: SignedAmount) -> SignedAmount { Self(self.0 - rhs.0) } - - /// Subtraction that doesn't allow negative [`SignedAmount`]s. - /// - /// Returns [`None`] if either `self`, `rhs` or the result is strictly negative. - pub fn positive_sub(self, rhs: SignedAmount) -> Option { - if self.is_negative() || rhs.is_negative() || rhs > self { - None - } else { - self.checked_sub(rhs) - } - } - - /// Converts to an unsigned amount. - pub fn to_unsigned(self) -> Result { - if self.is_negative() { - Err(OutOfRangeError::negative()) - } else { - Ok(Amount::from_sat(self.to_sat() as u64)) - } - } -} - -impl default::Default for SignedAmount { - fn default() -> Self { SignedAmount::ZERO } -} - -impl fmt::Debug for SignedAmount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "SignedAmount({} SAT)", self.to_sat()) - } -} - -// No one should depend on a binding contract for Display for this type. -// Just using Bitcoin denominated string. -impl fmt::Display for SignedAmount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.display_in(Denomination::Bitcoin).show_denomination(), f) - } -} - -impl ops::Add for SignedAmount { - type Output = SignedAmount; - - fn add(self, rhs: SignedAmount) -> Self::Output { - self.checked_add(rhs).expect("SignedAmount addition error") - } -} - -impl ops::AddAssign for SignedAmount { - fn add_assign(&mut self, other: SignedAmount) { *self = *self + other } -} - -impl ops::Sub for SignedAmount { - type Output = SignedAmount; - - fn sub(self, rhs: SignedAmount) -> Self::Output { - self.checked_sub(rhs).expect("SignedAmount subtraction error") - } -} - -impl ops::SubAssign for SignedAmount { - fn sub_assign(&mut self, other: SignedAmount) { *self = *self - other } -} - -impl ops::Rem for SignedAmount { - type Output = SignedAmount; - - fn rem(self, modulus: i64) -> Self { - self.checked_rem(modulus).expect("SignedAmount remainder error") - } -} - -impl ops::RemAssign for SignedAmount { - fn rem_assign(&mut self, modulus: i64) { *self = *self % modulus } -} - -impl ops::Mul for SignedAmount { - type Output = SignedAmount; - - fn mul(self, rhs: i64) -> Self::Output { - self.checked_mul(rhs).expect("SignedAmount multiplication error") - } -} - -impl ops::MulAssign for SignedAmount { - fn mul_assign(&mut self, rhs: i64) { *self = *self * rhs } -} - -impl ops::Div for SignedAmount { - type Output = SignedAmount; - - fn div(self, rhs: i64) -> Self::Output { - self.checked_div(rhs).expect("SignedAmount division error") - } -} - -impl ops::DivAssign for SignedAmount { - fn div_assign(&mut self, rhs: i64) { *self = *self / rhs } -} - -impl ops::Neg for SignedAmount { - type Output = Self; - - fn neg(self) -> Self::Output { Self(self.0.neg()) } -} - -impl FromStr for SignedAmount { - type Err = ParseError; - - /// Parses a string slice where the slice includes a denomination. - /// - /// If the string slice is zero or negative zero, then no denomination is required. - /// - /// # Returns - /// - /// `Ok(Amount)` if the string amount and denomination parse successfully, - /// otherwise, return `Err(ParseError)`. - fn from_str(s: &str) -> Result { - let result = SignedAmount::from_str_with_denomination(s); - - match result { - Err(ParseError::MissingDenomination(_)) => { - let d = SignedAmount::from_str_in(s, Denomination::Satoshi); - - if d == Ok(SignedAmount::ZERO) { - Ok(SignedAmount::ZERO) - } else { - result - } - } - _ => result, - } - } -} - -impl TryFrom for SignedAmount { - type Error = OutOfRangeError; - - fn try_from(value: Amount) -> Result { value.to_signed() } -} - -impl core::iter::Sum for SignedAmount { - fn sum>(iter: I) -> Self { - let sats: i64 = iter.map(|amt| amt.0).sum(); - SignedAmount::from_sat(sats) - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> Arbitrary<'a> for SignedAmount { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - let s = i64::arbitrary(u)?; - Ok(SignedAmount(s)) - } -} - /// Calculates the sum over the iterator using checked arithmetic. pub trait CheckedSum: private::SumSeal { /// Calculates the sum over the iterator using checked arithmetic. If an over or underflow would diff --git a/units/src/amount/signed.rs b/units/src/amount/signed.rs new file mode 100644 index 000000000..ed365f6ac --- /dev/null +++ b/units/src/amount/signed.rs @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! A signed bitcoin amount. + +#[cfg(feature = "alloc")] +use alloc::string::{String, ToString}; +use core::str::FromStr; +use core::{default, fmt, ops}; + +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Unstructured}; + +use super::{ + fmt_satoshi_in, parse_signed_to_satoshi, split_amount_and_denomination, Amount, Denomination, + Display, DisplayStyle, FormatOptions, OutOfRangeError, ParseAmountError, ParseError, +}; + +/// A signed amount. +/// +/// The [`SignedAmount`] type can be used to express Bitcoin amounts that support +/// arithmetic and conversion to various denominations. +/// +/// +/// Warning! +/// +/// This type implements several arithmetic operations from [`core::ops`]. +/// To prevent errors due to overflow or underflow when using these operations, +/// it is advised to instead use the checked arithmetic methods whose names +/// start with `checked_`. The operations from [`core::ops`] that [`Amount`] +/// implements will panic when overflow or underflow occurs. +/// +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SignedAmount(i64); + +impl SignedAmount { + /// The zero amount. + pub const ZERO: SignedAmount = SignedAmount(0); + /// Exactly one satoshi. + pub const ONE_SAT: SignedAmount = SignedAmount(1); + /// Exactly one bitcoin. + pub const ONE_BTC: SignedAmount = SignedAmount(100_000_000); + /// The maximum value allowed as an amount. Useful for sanity checking. + pub const MAX_MONEY: SignedAmount = SignedAmount(21_000_000 * 100_000_000); + /// The minimum value of an amount. + pub const MIN: SignedAmount = SignedAmount(i64::MIN); + /// The maximum value of an amount. + pub const MAX: SignedAmount = SignedAmount(i64::MAX); + + /// Create an [`SignedAmount`] with satoshi precision and the given number of satoshis. + pub const fn from_sat(satoshi: i64) -> SignedAmount { SignedAmount(satoshi) } + + /// Gets the number of satoshis in this [`SignedAmount`]. + pub const fn to_sat(self) -> i64 { self.0 } + + /// Convert from a value expressing bitcoins to an [`SignedAmount`]. + #[cfg(feature = "alloc")] + pub fn from_btc(btc: f64) -> Result { + SignedAmount::from_float_in(btc, Denomination::Bitcoin) + } + + /// Parse a decimal string as a value in the given denomination. + /// + /// Note: This only parses the value string. If you want to parse a value + /// with denomination, use [`FromStr`]. + pub fn from_str_in(s: &str, denom: Denomination) -> Result { + match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? { + // (negative, amount) + (false, sat) if sat > i64::MAX as u64 => + Err(ParseAmountError::OutOfRange(OutOfRangeError::too_big(true))), + (false, sat) => Ok(SignedAmount(sat as i64)), + (true, sat) if sat == i64::MIN.unsigned_abs() => Ok(SignedAmount(i64::MIN)), + (true, sat) if sat > i64::MIN.unsigned_abs() => + Err(ParseAmountError::OutOfRange(OutOfRangeError::too_small())), + (true, sat) => Ok(SignedAmount(-(sat as i64))), + } + } + + /// Parses amounts with denomination suffix like they are produced with + /// [`Self::to_string_with_denomination`] or with [`fmt::Display`]. + /// If you want to parse only the amount without the denomination, + /// use [`Self::from_str_in`]. + pub fn from_str_with_denomination(s: &str) -> Result { + let (amt, denom) = split_amount_and_denomination(s)?; + SignedAmount::from_str_in(amt, denom).map_err(Into::into) + } + + /// Express this [`SignedAmount`] as a floating-point value in the given denomination. + /// + /// Please be aware of the risk of using floating-point numbers. + #[cfg(feature = "alloc")] + pub fn to_float_in(self, denom: Denomination) -> f64 { + self.to_string_in(denom).parse::().unwrap() + } + + /// Express this [`SignedAmount`] as a floating-point value in Bitcoin. + /// + /// Equivalent to `to_float_in(Denomination::Bitcoin)`. + /// + /// Please be aware of the risk of using floating-point numbers. + #[cfg(feature = "alloc")] + pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) } + + /// Convert this [`SignedAmount`] in floating-point notation with a given + /// denomination. + /// + /// # Errors + /// + /// If the amount is too big, too precise or negative. + /// + /// Please be aware of the risk of using floating-point numbers. + #[cfg(feature = "alloc")] + pub fn from_float_in( + value: f64, + denom: Denomination, + ) -> Result { + // This is inefficient, but the safest way to deal with this. The parsing logic is safe. + // Any performance-critical application should not be dealing with floats. + SignedAmount::from_str_in(&value.to_string(), denom) + } + + /// Create an object that implements [`fmt::Display`] using specified denomination. + pub fn display_in(self, denomination: Denomination) -> Display { + Display { + sats_abs: self.unsigned_abs().to_sat(), + is_negative: self.is_negative(), + style: DisplayStyle::FixedDenomination { denomination, show_denomination: false }, + } + } + + /// Create an object that implements [`fmt::Display`] dynamically selecting denomination. + /// + /// This will use BTC for values greater than or equal to 1 BTC and satoshis otherwise. To + /// avoid confusion the denomination is always shown. + pub fn display_dynamic(self) -> Display { + Display { + sats_abs: self.unsigned_abs().to_sat(), + is_negative: self.is_negative(), + style: DisplayStyle::DynamicDenomination, + } + } + + /// Format the value of this [`SignedAmount`] in the given denomination. + /// + /// Does not include the denomination. + #[rustfmt::skip] + #[deprecated(since = "TBD", note = "use `display_in()` instead")] + pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result { + fmt_satoshi_in(self.unsigned_abs().to_sat(), self.is_negative(), f, denom, false, FormatOptions::default()) + } + + /// Get a string number of this [`SignedAmount`] in the given denomination. + /// + /// Does not include the denomination. + #[cfg(feature = "alloc")] + pub fn to_string_in(self, denom: Denomination) -> String { self.display_in(denom).to_string() } + + /// Get a formatted string of this [`SignedAmount`] in the given denomination, + /// suffixed with the abbreviation for the denomination. + #[cfg(feature = "alloc")] + pub fn to_string_with_denomination(self, denom: Denomination) -> String { + self.display_in(denom).show_denomination().to_string() + } + + // Some arithmetic that doesn't fit in [`core::ops`] traits. + + /// Get the absolute value of this [`SignedAmount`]. + pub fn abs(self) -> SignedAmount { SignedAmount(self.0.abs()) } + + /// Get the absolute value of this [`SignedAmount`] returning [`Amount`]. + pub fn unsigned_abs(self) -> Amount { Amount::from_sat(self.0.unsigned_abs()) } + + /// Returns a number representing sign of this [`SignedAmount`]. + /// + /// - `0` if the amount is zero + /// - `1` if the amount is positive + /// - `-1` if the amount is negative + pub fn signum(self) -> i64 { self.0.signum() } + + /// Checks if this [`SignedAmount`] is positive. + /// + /// Returns `true` if this [`SignedAmount`] is positive and `false` if + /// this [`SignedAmount`] is zero or negative. + pub fn is_positive(self) -> bool { self.0.is_positive() } + + /// Checks if this [`SignedAmount`] is negative. + /// + /// Returns `true` if this [`SignedAmount`] is negative and `false` if + /// this [`SignedAmount`] is zero or positive. + pub fn is_negative(self) -> bool { self.0.is_negative() } + + /// Returns the absolute value of this [`SignedAmount`]. + /// + /// Consider using `unsigned_abs` which is often more practical. + /// + /// Returns [`None`] if overflow occurred. (`self == MIN`) + pub fn checked_abs(self) -> Option { self.0.checked_abs().map(SignedAmount) } + + /// Checked addition. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_add(self, rhs: SignedAmount) -> Option { + self.0.checked_add(rhs.0).map(SignedAmount) + } + + /// Checked subtraction. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_sub(self, rhs: SignedAmount) -> Option { + self.0.checked_sub(rhs.0).map(SignedAmount) + } + + /// Checked multiplication. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_mul(self, rhs: i64) -> Option { + self.0.checked_mul(rhs).map(SignedAmount) + } + + /// Checked integer division. + /// + /// Be aware that integer division loses the remainder if no exact division + /// can be made. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_div(self, rhs: i64) -> Option { + self.0.checked_div(rhs).map(SignedAmount) + } + + /// Checked remainder. + /// + /// Returns [`None`] if overflow occurred. + pub fn checked_rem(self, rhs: i64) -> Option { + self.0.checked_rem(rhs).map(SignedAmount) + } + + /// Unchecked addition. + /// + /// Computes `self + rhs`. + /// + /// # Panics + /// + /// On overflow, panics in debug mode, wraps in release mode. + pub fn unchecked_add(self, rhs: SignedAmount) -> SignedAmount { Self(self.0 + rhs.0) } + + /// Unchecked subtraction. + /// + /// Computes `self - rhs`. + /// + /// # Panics + /// + /// On overflow, panics in debug mode, wraps in release mode. + pub fn unchecked_sub(self, rhs: SignedAmount) -> SignedAmount { Self(self.0 - rhs.0) } + + /// Subtraction that doesn't allow negative [`SignedAmount`]s. + /// + /// Returns [`None`] if either `self`, `rhs` or the result is strictly negative. + pub fn positive_sub(self, rhs: SignedAmount) -> Option { + if self.is_negative() || rhs.is_negative() || rhs > self { + None + } else { + self.checked_sub(rhs) + } + } + + /// Converts to an unsigned amount. + pub fn to_unsigned(self) -> Result { + if self.is_negative() { + Err(OutOfRangeError::negative()) + } else { + Ok(Amount::from_sat(self.to_sat() as u64)) + } + } +} + +impl default::Default for SignedAmount { + fn default() -> Self { SignedAmount::ZERO } +} + +impl fmt::Debug for SignedAmount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SignedAmount({} SAT)", self.to_sat()) + } +} + +// No one should depend on a binding contract for Display for this type. +// Just using Bitcoin denominated string. +impl fmt::Display for SignedAmount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.display_in(Denomination::Bitcoin).show_denomination(), f) + } +} + +impl ops::Add for SignedAmount { + type Output = SignedAmount; + + fn add(self, rhs: SignedAmount) -> Self::Output { + self.checked_add(rhs).expect("SignedAmount addition error") + } +} + +impl ops::AddAssign for SignedAmount { + fn add_assign(&mut self, other: SignedAmount) { *self = *self + other } +} + +impl ops::Sub for SignedAmount { + type Output = SignedAmount; + + fn sub(self, rhs: SignedAmount) -> Self::Output { + self.checked_sub(rhs).expect("SignedAmount subtraction error") + } +} + +impl ops::SubAssign for SignedAmount { + fn sub_assign(&mut self, other: SignedAmount) { *self = *self - other } +} + +impl ops::Rem for SignedAmount { + type Output = SignedAmount; + + fn rem(self, modulus: i64) -> Self { + self.checked_rem(modulus).expect("SignedAmount remainder error") + } +} + +impl ops::RemAssign for SignedAmount { + fn rem_assign(&mut self, modulus: i64) { *self = *self % modulus } +} + +impl ops::Mul for SignedAmount { + type Output = SignedAmount; + + fn mul(self, rhs: i64) -> Self::Output { + self.checked_mul(rhs).expect("SignedAmount multiplication error") + } +} + +impl ops::MulAssign for SignedAmount { + fn mul_assign(&mut self, rhs: i64) { *self = *self * rhs } +} + +impl ops::Div for SignedAmount { + type Output = SignedAmount; + + fn div(self, rhs: i64) -> Self::Output { + self.checked_div(rhs).expect("SignedAmount division error") + } +} + +impl ops::DivAssign for SignedAmount { + fn div_assign(&mut self, rhs: i64) { *self = *self / rhs } +} + +impl ops::Neg for SignedAmount { + type Output = Self; + + fn neg(self) -> Self::Output { Self(self.0.neg()) } +} + +impl FromStr for SignedAmount { + type Err = ParseError; + + /// Parses a string slice where the slice includes a denomination. + /// + /// If the string slice is zero or negative zero, then no denomination is required. + /// + /// # Returns + /// + /// `Ok(Amount)` if the string amount and denomination parse successfully, + /// otherwise, return `Err(ParseError)`. + fn from_str(s: &str) -> Result { + let result = SignedAmount::from_str_with_denomination(s); + + match result { + Err(ParseError::MissingDenomination(_)) => { + let d = SignedAmount::from_str_in(s, Denomination::Satoshi); + + if d == Ok(SignedAmount::ZERO) { + Ok(SignedAmount::ZERO) + } else { + result + } + } + _ => result, + } + } +} + +impl TryFrom for SignedAmount { + type Error = OutOfRangeError; + + fn try_from(value: Amount) -> Result { value.to_signed() } +} + +impl core::iter::Sum for SignedAmount { + fn sum>(iter: I) -> Self { + let sats: i64 = iter.map(|amt| amt.0).sum(); + SignedAmount::from_sat(sats) + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for SignedAmount { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let s = i64::arbitrary(u)?; + Ok(SignedAmount(s)) + } +} From 10ff979fbd76232ab28127600b7515bcf86467b9 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 31 Oct 2024 11:45:39 +1100 Subject: [PATCH 14/14] amount: Move arbitrary impl In an effort to make the `unsigned` and `signed` files be diff'able move the `arbitrary` code to be in the same place. Code move only. --- units/src/amount/unsigned.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs index f638a1e13..46738bbd7 100644 --- a/units/src/amount/unsigned.rs +++ b/units/src/amount/unsigned.rs @@ -264,14 +264,6 @@ impl Amount { } } -#[cfg(feature = "arbitrary")] -impl<'a> Arbitrary<'a> for Amount { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - let a = u64::arbitrary(u)?; - Ok(Amount(a)) - } -} - impl default::Default for Amount { fn default() -> Self { Amount::ZERO } } @@ -387,3 +379,11 @@ impl core::iter::Sum for Amount { Amount::from_sat(sats) } } + +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for Amount { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let a = u64::arbitrary(u)?; + Ok(Amount(a)) + } +}