diff --git a/units/src/amount.rs b/units/src/amount.rs deleted file mode 100644 index aad698b89..000000000 --- a/units/src/amount.rs +++ /dev/null @@ -1,3042 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Bitcoin amounts. -//! -//! This module mainly introduces the [`Amount`] and [`SignedAmount`] types. -//! We refer to the documentation on the types for more information. - -#[cfg(feature = "alloc")] -use alloc::string::{String, ToString}; -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 internals::error::InputString; -use internals::write_err; - -#[cfg(feature = "alloc")] -use crate::{FeeRate, Weight}; - -/// A set of denominations in which amounts can be expressed. -/// -/// # Accepted Denominations -/// -/// All upper or lower case, excluding SI prefix (c, m, u) which must be lower case. -/// - Singular: BTC, cBTC, mBTC, uBTC -/// - Plural or singular: sat, satoshi, bit -/// -/// # Note -/// -/// Due to ambiguity between mega and milli we prohibit usage of leading capital 'M'. It is -/// more important to protect users from incorrectly using a capital M to mean milli than to -/// allow Megabitcoin which is not a realistic denomination, and Megasatoshi which is -/// equivalent to cBTC which is allowed. -/// -/// # Examples -/// -/// ``` -/// # use bitcoin_units::Amount; -/// -/// assert_eq!("1 BTC".parse::().unwrap(), Amount::from_sat(100_000_000)); -/// assert_eq!("1 cBTC".parse::().unwrap(), Amount::from_sat(1_000_000)); -/// assert_eq!("1 mBTC".parse::().unwrap(), Amount::from_sat(100_000)); -/// assert_eq!("1 uBTC".parse::().unwrap(), Amount::from_sat(100)); -/// assert_eq!("1 bit".parse::().unwrap(), Amount::from_sat(100)); -/// assert_eq!("1 sat".parse::().unwrap(), Amount::from_sat(1)); -/// ``` -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -#[non_exhaustive] -pub enum Denomination { - /// BTC - Bitcoin, - /// cBTC - CentiBitcoin, - /// mBTC - MilliBitcoin, - /// uBTC - MicroBitcoin, - /// bits - Bit, - /// satoshi - Satoshi, -} - -impl Denomination { - /// Convenience alias for `Denomination::Bitcoin`. - pub const BTC: Self = Denomination::Bitcoin; - - /// Convenience alias for `Denomination::Satoshi`. - pub const SAT: Self = Denomination::Satoshi; - - /// The number of decimal places more than a satoshi. - fn precision(self) -> i8 { - match self { - Denomination::Bitcoin => -8, - Denomination::CentiBitcoin => -6, - Denomination::MilliBitcoin => -5, - Denomination::MicroBitcoin => -2, - Denomination::Bit => -2, - Denomination::Satoshi => 0, - } - } - - /// Returns a string representation of this denomination. - fn as_str(self) -> &'static str { - match self { - Denomination::Bitcoin => "BTC", - Denomination::CentiBitcoin => "cBTC", - Denomination::MilliBitcoin => "mBTC", - Denomination::MicroBitcoin => "uBTC", - Denomination::Bit => "bits", - Denomination::Satoshi => "satoshi", - } - } - - /// The different `str` forms of denominations that are recognized. - fn forms(s: &str) -> Option { - match s { - "BTC" | "btc" => Some(Denomination::Bitcoin), - "cBTC" | "cbtc" => Some(Denomination::CentiBitcoin), - "mBTC" | "mbtc" => Some(Denomination::MilliBitcoin), - "uBTC" | "ubtc" => Some(Denomination::MicroBitcoin), - "bit" | "bits" | "BIT" | "BITS" => Some(Denomination::Bit), - "SATOSHI" | "satoshi" | "SATOSHIS" | "satoshis" | "SAT" | "sat" | "SATS" | "sats" => - Some(Denomination::Satoshi), - _ => None, - } - } -} - -/// These form are ambigous and could have many meanings. For example, M could denote Mega or Milli. -/// If any of these forms are used, an error type PossiblyConfusingDenomination is returned. -const CONFUSING_FORMS: [&str; 6] = ["MBTC", "Mbtc", "CBTC", "Cbtc", "UBTC", "Ubtc"]; - -impl fmt::Display for Denomination { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(self.as_str()) } -} - -impl FromStr for Denomination { - type Err = ParseDenominationError; - - /// Converts from a `str` to a `Denomination`. - /// - /// # Errors - /// - /// - If the denomination begins with a capital `M` a [`PossiblyConfusingDenominationError`] is - /// returned. - /// - /// - If an unknown denomination is used, an [`UnknownDenominationError`] is returned. - fn from_str(s: &str) -> Result { - use self::ParseDenominationError::*; - - if CONFUSING_FORMS.contains(&s) { - return Err(PossiblyConfusing(PossiblyConfusingDenominationError(s.into()))); - }; - - let form = self::Denomination::forms(s); - - form.ok_or_else(|| Unknown(UnknownDenominationError(s.into()))) - } -} - -/// 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. -fn is_too_precise(s: &str, precision: usize) -> Option { - match s.find('.') { - Some(pos) if precision >= pos => Some(0), - Some(pos) => s[..pos] - .char_indices() - .rev() - .take(precision) - .find(|(_, d)| *d != '0') - .map(|(i, _)| i) - .or_else(|| { - s[(pos + 1)..].char_indices().find(|(_, d)| *d != '0').map(|(i, _)| i + pos + 1) - }), - None if precision >= s.len() => Some(0), - None => s.char_indices().rev().take(precision).find(|(_, d)| *d != '0').map(|(i, _)| i), - } -} - -const INPUT_STRING_LEN_LIMIT: usize = 50; - -/// Parses a decimal string in the given denomination into a satoshi value and a -/// [`bool`] indicator for a negative amount. -fn parse_signed_to_satoshi( - mut s: &str, - denom: Denomination, -) -> Result<(bool, u64), InnerParseError> { - if s.is_empty() { - return Err(InnerParseError::MissingDigits(MissingDigitsError { - kind: MissingDigitsKind::Empty, - })); - } - if s.len() > INPUT_STRING_LEN_LIMIT { - return Err(InnerParseError::InputTooLarge(s.len())); - } - - let is_negative = s.starts_with('-'); - if is_negative { - if s.len() == 1 { - return Err(InnerParseError::MissingDigits(MissingDigitsError { - kind: MissingDigitsKind::OnlyMinusSign, - })); - } - s = &s[1..]; - } - - let max_decimals = { - // The difference in precision between native (satoshi) - // and desired denomination. - let precision_diff = -denom.precision(); - if precision_diff <= 0 { - // If precision diff is negative, this means we are parsing - // into a less precise amount. That is not allowed unless - // there are no decimals and the last digits are zeroes as - // many as the difference in precision. - let last_n = precision_diff.unsigned_abs().into(); - if let Some(position) = is_too_precise(s, last_n) { - match s.parse::() { - Ok(0) => return Ok((is_negative, 0)), - _ => - return Err(InnerParseError::TooPrecise(TooPreciseError { - position: position + is_negative as usize, - })), - } - } - s = &s[0..s.find('.').unwrap_or(s.len()) - last_n]; - 0 - } else { - precision_diff - } - }; - - let mut decimals = None; - let mut value: u64 = 0; // as satoshis - for (i, c) in s.char_indices() { - match c { - '0'..='9' => { - // Do `value = 10 * value + digit`, catching overflows. - match 10_u64.checked_mul(value) { - None => return Err(InnerParseError::Overflow { is_negative }), - Some(val) => match val.checked_add((c as u8 - b'0') as u64) { - None => return Err(InnerParseError::Overflow { is_negative }), - Some(val) => value = val, - }, - } - // Increment the decimal digit counter if past decimal. - decimals = match decimals { - None => None, - Some(d) if d < max_decimals => Some(d + 1), - _ => - return Err(InnerParseError::TooPrecise(TooPreciseError { - position: i + is_negative as usize, - })), - }; - } - '.' => match decimals { - None if max_decimals <= 0 => break, - None => decimals = Some(0), - // Double decimal dot. - _ => - return Err(InnerParseError::InvalidCharacter(InvalidCharacterError { - invalid_char: '.', - position: i + is_negative as usize, - })), - }, - c => - return Err(InnerParseError::InvalidCharacter(InvalidCharacterError { - invalid_char: c, - position: i + is_negative as usize, - })), - } - } - - // Decimally shift left by `max_decimals - decimals`. - let scale_factor = max_decimals - decimals.unwrap_or(0); - for _ in 0..scale_factor { - value = match 10_u64.checked_mul(value) { - Some(v) => v, - None => return Err(InnerParseError::Overflow { is_negative }), - }; - } - - Ok((is_negative, value)) -} - -enum InnerParseError { - Overflow { is_negative: bool }, - TooPrecise(TooPreciseError), - MissingDigits(MissingDigitsError), - InputTooLarge(usize), - InvalidCharacter(InvalidCharacterError), -} - -internals::impl_from_infallible!(InnerParseError); - -impl InnerParseError { - fn convert(self, is_signed: bool) -> ParseAmountError { - match self { - Self::Overflow { is_negative } => - OutOfRangeError { is_signed, is_greater_than_max: !is_negative }.into(), - Self::TooPrecise(error) => ParseAmountError::TooPrecise(error), - Self::MissingDigits(error) => ParseAmountError::MissingDigits(error), - Self::InputTooLarge(len) => ParseAmountError::InputTooLarge(InputTooLargeError { len }), - Self::InvalidCharacter(error) => ParseAmountError::InvalidCharacter(error), - } - } -} - -fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseError> { - let (i, j) = if let Some(i) = s.find(' ') { - (i, i + 1) - } else { - let i = s - .find(|c: char| c.is_alphabetic()) - .ok_or(ParseError::MissingDenomination(MissingDenominationError))?; - (i, i) - }; - Ok((&s[..i], s[j..].parse()?)) -} - -/// Options given by `fmt::Formatter` -struct FormatOptions { - fill: char, - align: Option, - width: Option, - precision: Option, - sign_plus: bool, - sign_aware_zero_pad: bool, -} - -impl FormatOptions { - fn from_formatter(f: &fmt::Formatter) -> Self { - FormatOptions { - fill: f.fill(), - align: f.align(), - width: f.width(), - precision: f.precision(), - sign_plus: f.sign_plus(), - sign_aware_zero_pad: f.sign_aware_zero_pad(), - } - } -} - -impl Default for FormatOptions { - fn default() -> Self { - FormatOptions { - fill: ' ', - align: None, - width: None, - precision: None, - sign_plus: false, - sign_aware_zero_pad: false, - } - } -} - -fn dec_width(mut num: u64) -> usize { - let mut width = 1; - loop { - num /= 10; - if num == 0 { - break; - } - width += 1; - } - width -} - -fn repeat_char(f: &mut dyn fmt::Write, c: char, count: usize) -> fmt::Result { - for _ in 0..count { - f.write_char(c)?; - } - Ok(()) -} - -/// Format the given satoshi amount in the given denomination. -fn fmt_satoshi_in( - mut satoshi: u64, - negative: bool, - f: &mut dyn fmt::Write, - denom: Denomination, - show_denom: bool, - options: FormatOptions, -) -> fmt::Result { - let precision = denom.precision(); - // First we normalize the number: - // {num_before_decimal_point}{:0exp}{"." if nb_decimals > 0}{:0nb_decimals}{num_after_decimal_point}{:0trailing_decimal_zeros} - let mut num_after_decimal_point = 0; - let mut norm_nb_decimals = 0; - let mut num_before_decimal_point = satoshi; - let trailing_decimal_zeros; - let mut exp = 0; - match precision.cmp(&0) { - // We add the number of zeroes to the end - Ordering::Greater => { - if satoshi > 0 { - exp = precision as usize; - } - trailing_decimal_zeros = options.precision.unwrap_or(0); - } - Ordering::Less => { - let precision = precision.unsigned_abs(); - // round the number if needed - // rather than fiddling with chars, we just modify satoshi and let the simpler algorithm take over. - if let Some(format_precision) = options.precision { - if usize::from(precision) > format_precision { - // precision is u8 so in this branch options.precision() < 255 which fits in u32 - let rounding_divisor = - 10u64.pow(u32::from(precision) - format_precision as u32); - let remainder = satoshi % rounding_divisor; - satoshi -= remainder; - if remainder / (rounding_divisor / 10) >= 5 { - satoshi += rounding_divisor; - } - } - } - let divisor = 10u64.pow(precision.into()); - num_before_decimal_point = satoshi / divisor; - num_after_decimal_point = satoshi % divisor; - // normalize by stripping trailing zeros - if num_after_decimal_point == 0 { - norm_nb_decimals = 0; - } else { - norm_nb_decimals = usize::from(precision); - while num_after_decimal_point % 10 == 0 { - norm_nb_decimals -= 1; - num_after_decimal_point /= 10 - } - } - // compute requested precision - let opt_precision = options.precision.unwrap_or(0); - trailing_decimal_zeros = opt_precision.saturating_sub(norm_nb_decimals); - } - Ordering::Equal => trailing_decimal_zeros = options.precision.unwrap_or(0), - } - let total_decimals = norm_nb_decimals + trailing_decimal_zeros; - // Compute expected width of the number - let mut num_width = if total_decimals > 0 { - // 1 for decimal point - 1 + total_decimals - } else { - 0 - }; - num_width += dec_width(num_before_decimal_point) + exp; - if options.sign_plus || negative { - num_width += 1; - } - - if show_denom { - // + 1 for space - num_width += denom.as_str().len() + 1; - } - - let width = options.width.unwrap_or(0); - let align = options.align.unwrap_or(fmt::Alignment::Right); - let (left_pad, pad_right) = match (num_width < width, options.sign_aware_zero_pad, align) { - (false, _, _) => (0, 0), - // Alignment is always right (ignored) when zero-padding - (true, true, _) | (true, false, fmt::Alignment::Right) => (width - num_width, 0), - (true, false, fmt::Alignment::Left) => (0, width - num_width), - // If the required padding is odd it needs to be skewed to the left - (true, false, fmt::Alignment::Center) => - ((width - num_width) / 2, (width - num_width + 1) / 2), - }; - - if !options.sign_aware_zero_pad { - repeat_char(f, options.fill, left_pad)?; - } - - if negative { - write!(f, "-")?; - } else if options.sign_plus { - write!(f, "+")?; - } - - if options.sign_aware_zero_pad { - repeat_char(f, '0', left_pad)?; - } - - write!(f, "{}", num_before_decimal_point)?; - - repeat_char(f, '0', exp)?; - - if total_decimals > 0 { - write!(f, ".")?; - } - if norm_nb_decimals > 0 { - write!(f, "{:0width$}", num_after_decimal_point, width = norm_nb_decimals)?; - } - repeat_char(f, '0', trailing_decimal_zeros)?; - - if show_denom { - write!(f, " {}", denom.as_str())?; - } - - repeat_char(f, options.fill, pad_right)?; - 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`]: -/// -/// * Ability to select denomination -/// * Show or hide denomination -/// * Dynamically-selected denomination - show in sats if less than 1 BTC. -/// -/// However, this can still be combined with [`fmt::Formatter`] options to precisely control zeros, -/// padding, alignment... The formatting works like floats from `core` but note that precision will -/// **never** be lossy - that means no rounding. -/// -/// Note: This implementation is currently **unstable**. The only thing that we can promise is that -/// unless the precision is changed, this will display an accurate, human-readable number, and the -/// default serialization (one with unmodified [`fmt::Formatter`] options) will round-trip with [`FromStr`] -/// -/// See [`Amount::display_in`] and [`Amount::display_dynamic`] on how to construct this. -#[derive(Debug, Clone)] -pub struct Display { - /// Absolute value of satoshis to display (sign is below) - sats_abs: u64, - /// The sign - is_negative: bool, - /// How to display the value - style: DisplayStyle, -} - -impl Display { - /// Makes subsequent calls to `Display::fmt` display denomination. - pub fn show_denomination(mut self) -> Self { - match &mut self.style { - DisplayStyle::FixedDenomination { show_denomination, .. } => *show_denomination = true, - // No-op because dynamic denomination is always shown - DisplayStyle::DynamicDenomination => (), - } - self - } -} - -impl fmt::Display for Display { - #[rustfmt::skip] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let format_options = FormatOptions::from_formatter(f); - match &self.style { - DisplayStyle::FixedDenomination { show_denomination, denomination } => { - fmt_satoshi_in(self.sats_abs, self.is_negative, f, *denomination, *show_denomination, format_options) - }, - DisplayStyle::DynamicDenomination if self.sats_abs >= Amount::ONE_BTC.to_sat() => { - fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Bitcoin, true, format_options) - }, - DisplayStyle::DynamicDenomination => { - fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Satoshi, true, format_options) - }, - } - } -} - -#[derive(Clone, Debug)] -enum DisplayStyle { - FixedDenomination { denomination: Denomination, show_denomination: bool }, - 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(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 - /// happen it returns [`None`]. - fn checked_sum(self) -> Option; -} - -impl CheckedSum for T -where - T: Iterator, -{ - fn checked_sum(mut self) -> Option { - let first = Some(self.next().unwrap_or_default()); - - self.fold(first, |acc, item| acc.and_then(|acc| acc.checked_add(item))) - } -} - -impl CheckedSum for T -where - T: Iterator, -{ - fn checked_sum(mut self) -> Option { - let first = Some(self.next().unwrap_or_default()); - - self.fold(first, |acc, item| acc.and_then(|acc| acc.checked_add(item))) - } -} - -mod private { - use super::{Amount, SignedAmount}; - - /// Used to seal the `CheckedSum` trait - pub trait SumSeal {} - - 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)) - } - } - } -} - -#[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")] - 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(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(123); - let ua_positive = Amount::try_from(sa_positive).unwrap(); - assert_eq!(ua_positive, Amount::from_sat(123)); - - let sa_negative = SignedAmount(-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(1)).to_string_in(sat), sat).is_err() - ); - assert!(Amount::from_str_in(&(amount + Amount(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/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 new file mode 100644 index 000000000..f78b5c378 --- /dev/null +++ b/units/src/amount/mod.rs @@ -0,0 +1,599 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Bitcoin amounts. +//! +//! 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 = "serde")] +pub mod serde; + +mod signed; +#[cfg(test)] +mod tests; +mod unsigned; +#[cfg(kani)] +mod verification; + +use core::cmp::Ordering; +use core::fmt; +use core::str::FromStr; + +use self::error::MissingDigitsKind; + +#[rustfmt::skip] // Keep public re-exports separate. +#[doc(inline)] +pub use self::{ + error::{ + InputTooLargeError, InvalidCharacterError, MissingDenominationError, MissingDigitsError, + OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError, + PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError, + }, + signed::SignedAmount, + unsigned::Amount, +}; + +/// A set of denominations in which amounts can be expressed. +/// +/// # Accepted Denominations +/// +/// All upper or lower case, excluding SI prefix (c, m, u) which must be lower case. +/// - Singular: BTC, cBTC, mBTC, uBTC +/// - Plural or singular: sat, satoshi, bit +/// +/// # Note +/// +/// Due to ambiguity between mega and milli we prohibit usage of leading capital 'M'. It is +/// more important to protect users from incorrectly using a capital M to mean milli than to +/// allow Megabitcoin which is not a realistic denomination, and Megasatoshi which is +/// equivalent to cBTC which is allowed. +/// +/// # Examples +/// +/// ``` +/// # use bitcoin_units::Amount; +/// +/// assert_eq!("1 BTC".parse::().unwrap(), Amount::from_sat(100_000_000)); +/// assert_eq!("1 cBTC".parse::().unwrap(), Amount::from_sat(1_000_000)); +/// assert_eq!("1 mBTC".parse::().unwrap(), Amount::from_sat(100_000)); +/// assert_eq!("1 uBTC".parse::().unwrap(), Amount::from_sat(100)); +/// assert_eq!("1 bit".parse::().unwrap(), Amount::from_sat(100)); +/// assert_eq!("1 sat".parse::().unwrap(), Amount::from_sat(1)); +/// ``` +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +#[non_exhaustive] +pub enum Denomination { + /// BTC + Bitcoin, + /// cBTC + CentiBitcoin, + /// mBTC + MilliBitcoin, + /// uBTC + MicroBitcoin, + /// bits + Bit, + /// satoshi + Satoshi, +} + +impl Denomination { + /// Convenience alias for `Denomination::Bitcoin`. + pub const BTC: Self = Denomination::Bitcoin; + + /// Convenience alias for `Denomination::Satoshi`. + pub const SAT: Self = Denomination::Satoshi; + + /// The number of decimal places more than a satoshi. + fn precision(self) -> i8 { + match self { + Denomination::Bitcoin => -8, + Denomination::CentiBitcoin => -6, + Denomination::MilliBitcoin => -5, + Denomination::MicroBitcoin => -2, + Denomination::Bit => -2, + Denomination::Satoshi => 0, + } + } + + /// Returns a string representation of this denomination. + fn as_str(self) -> &'static str { + match self { + Denomination::Bitcoin => "BTC", + Denomination::CentiBitcoin => "cBTC", + Denomination::MilliBitcoin => "mBTC", + Denomination::MicroBitcoin => "uBTC", + Denomination::Bit => "bits", + Denomination::Satoshi => "satoshi", + } + } + + /// The different `str` forms of denominations that are recognized. + fn forms(s: &str) -> Option { + match s { + "BTC" | "btc" => Some(Denomination::Bitcoin), + "cBTC" | "cbtc" => Some(Denomination::CentiBitcoin), + "mBTC" | "mbtc" => Some(Denomination::MilliBitcoin), + "uBTC" | "ubtc" => Some(Denomination::MicroBitcoin), + "bit" | "bits" | "BIT" | "BITS" => Some(Denomination::Bit), + "SATOSHI" | "satoshi" | "SATOSHIS" | "satoshis" | "SAT" | "sat" | "SATS" | "sats" => + Some(Denomination::Satoshi), + _ => None, + } + } +} + +/// These form are ambigous and could have many meanings. For example, M could denote Mega or Milli. +/// If any of these forms are used, an error type PossiblyConfusingDenomination is returned. +const CONFUSING_FORMS: [&str; 6] = ["MBTC", "Mbtc", "CBTC", "Cbtc", "UBTC", "Ubtc"]; + +impl fmt::Display for Denomination { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(self.as_str()) } +} + +impl FromStr for Denomination { + type Err = ParseDenominationError; + + /// Converts from a `str` to a `Denomination`. + /// + /// # Errors + /// + /// - If the denomination begins with a capital `M` a [`PossiblyConfusingDenominationError`] is + /// returned. + /// + /// - If an unknown denomination is used, an [`UnknownDenominationError`] is returned. + fn from_str(s: &str) -> Result { + use self::ParseDenominationError::*; + + if CONFUSING_FORMS.contains(&s) { + return Err(PossiblyConfusing(PossiblyConfusingDenominationError(s.into()))); + }; + + let form = self::Denomination::forms(s); + + form.ok_or_else(|| Unknown(UnknownDenominationError(s.into()))) + } +} + +/// Returns `Some(position)` if the precision is not supported. +/// +/// The position indicates the first digit that is too precise. +fn is_too_precise(s: &str, precision: usize) -> Option { + match s.find('.') { + Some(pos) if precision >= pos => Some(0), + Some(pos) => s[..pos] + .char_indices() + .rev() + .take(precision) + .find(|(_, d)| *d != '0') + .map(|(i, _)| i) + .or_else(|| { + s[(pos + 1)..].char_indices().find(|(_, d)| *d != '0').map(|(i, _)| i + pos + 1) + }), + None if precision >= s.len() => Some(0), + None => s.char_indices().rev().take(precision).find(|(_, d)| *d != '0').map(|(i, _)| i), + } +} + +const INPUT_STRING_LEN_LIMIT: usize = 50; + +/// Parses a decimal string in the given denomination into a satoshi value and a +/// [`bool`] indicator for a negative amount. +fn parse_signed_to_satoshi( + mut s: &str, + denom: Denomination, +) -> Result<(bool, u64), InnerParseError> { + if s.is_empty() { + return Err(InnerParseError::MissingDigits(MissingDigitsError { + kind: MissingDigitsKind::Empty, + })); + } + if s.len() > INPUT_STRING_LEN_LIMIT { + return Err(InnerParseError::InputTooLarge(s.len())); + } + + let is_negative = s.starts_with('-'); + if is_negative { + if s.len() == 1 { + return Err(InnerParseError::MissingDigits(MissingDigitsError { + kind: MissingDigitsKind::OnlyMinusSign, + })); + } + s = &s[1..]; + } + + let max_decimals = { + // The difference in precision between native (satoshi) + // and desired denomination. + let precision_diff = -denom.precision(); + if precision_diff <= 0 { + // If precision diff is negative, this means we are parsing + // into a less precise amount. That is not allowed unless + // there are no decimals and the last digits are zeroes as + // many as the difference in precision. + let last_n = precision_diff.unsigned_abs().into(); + if let Some(position) = is_too_precise(s, last_n) { + match s.parse::() { + Ok(0) => return Ok((is_negative, 0)), + _ => + return Err(InnerParseError::TooPrecise(TooPreciseError { + position: position + is_negative as usize, + })), + } + } + s = &s[0..s.find('.').unwrap_or(s.len()) - last_n]; + 0 + } else { + precision_diff + } + }; + + let mut decimals = None; + let mut value: u64 = 0; // as satoshis + for (i, c) in s.char_indices() { + match c { + '0'..='9' => { + // Do `value = 10 * value + digit`, catching overflows. + match 10_u64.checked_mul(value) { + None => return Err(InnerParseError::Overflow { is_negative }), + Some(val) => match val.checked_add((c as u8 - b'0') as u64) { + None => return Err(InnerParseError::Overflow { is_negative }), + Some(val) => value = val, + }, + } + // Increment the decimal digit counter if past decimal. + decimals = match decimals { + None => None, + Some(d) if d < max_decimals => Some(d + 1), + _ => + return Err(InnerParseError::TooPrecise(TooPreciseError { + position: i + is_negative as usize, + })), + }; + } + '.' => match decimals { + None if max_decimals <= 0 => break, + None => decimals = Some(0), + // Double decimal dot. + _ => + return Err(InnerParseError::InvalidCharacter(InvalidCharacterError { + invalid_char: '.', + position: i + is_negative as usize, + })), + }, + c => + return Err(InnerParseError::InvalidCharacter(InvalidCharacterError { + invalid_char: c, + position: i + is_negative as usize, + })), + } + } + + // Decimally shift left by `max_decimals - decimals`. + let scale_factor = max_decimals - decimals.unwrap_or(0); + for _ in 0..scale_factor { + value = match 10_u64.checked_mul(value) { + Some(v) => v, + None => return Err(InnerParseError::Overflow { is_negative }), + }; + } + + Ok((is_negative, value)) +} + +enum InnerParseError { + Overflow { is_negative: bool }, + TooPrecise(TooPreciseError), + MissingDigits(MissingDigitsError), + InputTooLarge(usize), + InvalidCharacter(InvalidCharacterError), +} + +internals::impl_from_infallible!(InnerParseError); + +impl InnerParseError { + fn convert(self, is_signed: bool) -> ParseAmountError { + match self { + Self::Overflow { is_negative } => + OutOfRangeError { is_signed, is_greater_than_max: !is_negative }.into(), + Self::TooPrecise(error) => ParseAmountError::TooPrecise(error), + Self::MissingDigits(error) => ParseAmountError::MissingDigits(error), + Self::InputTooLarge(len) => ParseAmountError::InputTooLarge(InputTooLargeError { len }), + Self::InvalidCharacter(error) => ParseAmountError::InvalidCharacter(error), + } + } +} + +fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseError> { + let (i, j) = if let Some(i) = s.find(' ') { + (i, i + 1) + } else { + let i = s + .find(|c: char| c.is_alphabetic()) + .ok_or(ParseError::MissingDenomination(MissingDenominationError))?; + (i, i) + }; + Ok((&s[..i], s[j..].parse()?)) +} + +/// Options given by `fmt::Formatter` +struct FormatOptions { + fill: char, + align: Option, + width: Option, + precision: Option, + sign_plus: bool, + sign_aware_zero_pad: bool, +} + +impl FormatOptions { + fn from_formatter(f: &fmt::Formatter) -> Self { + FormatOptions { + fill: f.fill(), + align: f.align(), + width: f.width(), + precision: f.precision(), + sign_plus: f.sign_plus(), + sign_aware_zero_pad: f.sign_aware_zero_pad(), + } + } +} + +impl Default for FormatOptions { + fn default() -> Self { + FormatOptions { + fill: ' ', + align: None, + width: None, + precision: None, + sign_plus: false, + sign_aware_zero_pad: false, + } + } +} + +fn dec_width(mut num: u64) -> usize { + let mut width = 1; + loop { + num /= 10; + if num == 0 { + break; + } + width += 1; + } + width +} + +fn repeat_char(f: &mut dyn fmt::Write, c: char, count: usize) -> fmt::Result { + for _ in 0..count { + f.write_char(c)?; + } + Ok(()) +} + +/// Format the given satoshi amount in the given denomination. +fn fmt_satoshi_in( + mut satoshi: u64, + negative: bool, + f: &mut dyn fmt::Write, + denom: Denomination, + show_denom: bool, + options: FormatOptions, +) -> fmt::Result { + let precision = denom.precision(); + // First we normalize the number: + // {num_before_decimal_point}{:0exp}{"." if nb_decimals > 0}{:0nb_decimals}{num_after_decimal_point}{:0trailing_decimal_zeros} + let mut num_after_decimal_point = 0; + let mut norm_nb_decimals = 0; + let mut num_before_decimal_point = satoshi; + let trailing_decimal_zeros; + let mut exp = 0; + match precision.cmp(&0) { + // We add the number of zeroes to the end + Ordering::Greater => { + if satoshi > 0 { + exp = precision as usize; + } + trailing_decimal_zeros = options.precision.unwrap_or(0); + } + Ordering::Less => { + let precision = precision.unsigned_abs(); + // round the number if needed + // rather than fiddling with chars, we just modify satoshi and let the simpler algorithm take over. + if let Some(format_precision) = options.precision { + if usize::from(precision) > format_precision { + // precision is u8 so in this branch options.precision() < 255 which fits in u32 + let rounding_divisor = + 10u64.pow(u32::from(precision) - format_precision as u32); + let remainder = satoshi % rounding_divisor; + satoshi -= remainder; + if remainder / (rounding_divisor / 10) >= 5 { + satoshi += rounding_divisor; + } + } + } + let divisor = 10u64.pow(precision.into()); + num_before_decimal_point = satoshi / divisor; + num_after_decimal_point = satoshi % divisor; + // normalize by stripping trailing zeros + if num_after_decimal_point == 0 { + norm_nb_decimals = 0; + } else { + norm_nb_decimals = usize::from(precision); + while num_after_decimal_point % 10 == 0 { + norm_nb_decimals -= 1; + num_after_decimal_point /= 10 + } + } + // compute requested precision + let opt_precision = options.precision.unwrap_or(0); + trailing_decimal_zeros = opt_precision.saturating_sub(norm_nb_decimals); + } + Ordering::Equal => trailing_decimal_zeros = options.precision.unwrap_or(0), + } + let total_decimals = norm_nb_decimals + trailing_decimal_zeros; + // Compute expected width of the number + let mut num_width = if total_decimals > 0 { + // 1 for decimal point + 1 + total_decimals + } else { + 0 + }; + num_width += dec_width(num_before_decimal_point) + exp; + if options.sign_plus || negative { + num_width += 1; + } + + if show_denom { + // + 1 for space + num_width += denom.as_str().len() + 1; + } + + let width = options.width.unwrap_or(0); + let align = options.align.unwrap_or(fmt::Alignment::Right); + let (left_pad, pad_right) = match (num_width < width, options.sign_aware_zero_pad, align) { + (false, _, _) => (0, 0), + // Alignment is always right (ignored) when zero-padding + (true, true, _) | (true, false, fmt::Alignment::Right) => (width - num_width, 0), + (true, false, fmt::Alignment::Left) => (0, width - num_width), + // If the required padding is odd it needs to be skewed to the left + (true, false, fmt::Alignment::Center) => + ((width - num_width) / 2, (width - num_width + 1) / 2), + }; + + if !options.sign_aware_zero_pad { + repeat_char(f, options.fill, left_pad)?; + } + + if negative { + write!(f, "-")?; + } else if options.sign_plus { + write!(f, "+")?; + } + + if options.sign_aware_zero_pad { + repeat_char(f, '0', left_pad)?; + } + + write!(f, "{}", num_before_decimal_point)?; + + repeat_char(f, '0', exp)?; + + if total_decimals > 0 { + write!(f, ".")?; + } + if norm_nb_decimals > 0 { + write!(f, "{:0width$}", num_after_decimal_point, width = norm_nb_decimals)?; + } + repeat_char(f, '0', trailing_decimal_zeros)?; + + if show_denom { + write!(f, " {}", denom.as_str())?; + } + + repeat_char(f, options.fill, pad_right)?; + Ok(()) +} + +/// A helper/builder that displays amount with specified settings. +/// +/// This provides richer interface than [`fmt::Formatter`]: +/// +/// * Ability to select denomination +/// * Show or hide denomination +/// * Dynamically-selected denomination - show in sats if less than 1 BTC. +/// +/// However, this can still be combined with [`fmt::Formatter`] options to precisely control zeros, +/// padding, alignment... The formatting works like floats from `core` but note that precision will +/// **never** be lossy - that means no rounding. +/// +/// Note: This implementation is currently **unstable**. The only thing that we can promise is that +/// unless the precision is changed, this will display an accurate, human-readable number, and the +/// default serialization (one with unmodified [`fmt::Formatter`] options) will round-trip with [`FromStr`] +/// +/// See [`Amount::display_in`] and [`Amount::display_dynamic`] on how to construct this. +#[derive(Debug, Clone)] +pub struct Display { + /// Absolute value of satoshis to display (sign is below) + sats_abs: u64, + /// The sign + is_negative: bool, + /// How to display the value + style: DisplayStyle, +} + +impl Display { + /// Makes subsequent calls to `Display::fmt` display denomination. + pub fn show_denomination(mut self) -> Self { + match &mut self.style { + DisplayStyle::FixedDenomination { show_denomination, .. } => *show_denomination = true, + // No-op because dynamic denomination is always shown + DisplayStyle::DynamicDenomination => (), + } + self + } +} + +impl fmt::Display for Display { + #[rustfmt::skip] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let format_options = FormatOptions::from_formatter(f); + match &self.style { + DisplayStyle::FixedDenomination { show_denomination, denomination } => { + fmt_satoshi_in(self.sats_abs, self.is_negative, f, *denomination, *show_denomination, format_options) + }, + DisplayStyle::DynamicDenomination if self.sats_abs >= Amount::ONE_BTC.to_sat() => { + fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Bitcoin, true, format_options) + }, + DisplayStyle::DynamicDenomination => { + fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Satoshi, true, format_options) + }, + } + } +} + +#[derive(Clone, Debug)] +enum DisplayStyle { + FixedDenomination { denomination: Denomination, show_denomination: bool }, + DynamicDenomination, +} + +/// 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 + /// happen it returns [`None`]. + fn checked_sum(self) -> Option; +} + +impl CheckedSum for T +where + T: Iterator, +{ + fn checked_sum(mut self) -> Option { + let first = Some(self.next().unwrap_or_default()); + + self.fold(first, |acc, item| acc.and_then(|acc| acc.checked_add(item))) + } +} + +impl CheckedSum for T +where + T: Iterator, +{ + fn checked_sum(mut self) -> Option { + let first = Some(self.next().unwrap_or_default()); + + self.fold(first, |acc, item| acc.and_then(|acc| acc.checked_add(item))) + } +} + +mod private { + use super::{Amount, SignedAmount}; + + /// Used to seal the `CheckedSum` trait + pub trait SumSeal {} + + impl SumSeal for T where T: Iterator {} + impl SumSeal for T where T: Iterator {} +} diff --git a/units/src/amount/serde.rs b/units/src/amount/serde.rs new file mode 100644 index 000000000..038b530ef --- /dev/null +++ b/units/src/amount/serde.rs @@ -0,0 +1,274 @@ +// 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)) + } + } +} 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)) + } +} diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs new file mode 100644 index 000000000..a695bb101 --- /dev/null +++ b/units/src/amount/tests.rs @@ -0,0 +1,910 @@ +// 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"); +} diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs new file mode 100644 index 000000000..46738bbd7 --- /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)) + } + } +} + +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) + } +} + +#[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)) + } +} diff --git a/units/src/amount/verification.rs b/units/src/amount/verification.rs new file mode 100644 index 000000000..0310e19e9 --- /dev/null +++ b/units/src/amount/verification.rs @@ -0,0 +1,117 @@ +// 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 }, + ); +}