Merge rust-bitcoin/rust-bitcoin#2370: Improve units

7bf478373a Model `TooBig` and `Negative` as `OutOfRange` (Martin Habovstiak)
54cbbf804f Express `i64::MAX + 1` as `i64::MIN.unsigned_abs()` (Martin Habovstiak)
b562a18914 Move denomination error out of `ParseAmountError` (Martin Habovstiak)
5e6c65bc1a Clean up `unsigned_abs` (Martin Habovstiak)

Pull request description:

  Closes #2265
  Closes #2266

  Disclaimer: I did this in December and don't remember why I haven't pushed it. Maybe because it's somehow broken but I don't see how so please review a bit more carefully just in case.

ACKs for top commit:
  tcharding:
    ACK 7bf478373a
  apoelstra:
    ACK 7bf478373a

Tree-SHA512: 1f6e9adae9168bd045c9b09f06d9a69efd47ccc7709ac9ecaf48cb86e265b448b9b52a199ac5e6838d5029f5bc7514c5d7deb15a4d7c8a4606a353f390745570
This commit is contained in:
Andrew Poelstra 2024-01-25 16:00:42 +00:00
commit e2b9555070
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
1 changed files with 251 additions and 89 deletions

View File

@ -145,14 +145,54 @@ impl FromStr for Denomination {
} }
} }
/// An error during amount parsing amount with denomination.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
/// Invalid amount.
Amount(ParseAmountError),
/// Invalid denomination.
Denomination(ParseDenominationError),
}
impl From<ParseAmountError> for ParseError {
fn from(e: ParseAmountError) -> Self { Self::Amount(e) }
}
impl From<ParseDenominationError> for ParseError {
fn from(e: ParseDenominationError) -> Self { Self::Denomination(e) }
}
impl From<OutOfRangeError> for ParseError {
fn from(e: OutOfRangeError) -> 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),
}
}
}
#[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),
}
}
}
/// An error during amount parsing. /// An error during amount parsing.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]
pub enum ParseAmountError { pub enum ParseAmountError {
/// Amount is negative. /// The amount is too big or too small.
Negative, OutOfRange(OutOfRangeError),
/// Amount is too big to fit inside the type.
TooBig,
/// Amount has higher precision than supported by the type. /// Amount has higher precision than supported by the type.
TooPrecise, TooPrecise,
/// Invalid number format. /// Invalid number format.
@ -161,8 +201,6 @@ pub enum ParseAmountError {
InputTooLarge, InputTooLarge,
/// Invalid character in input. /// Invalid character in input.
InvalidCharacter(char), InvalidCharacter(char),
/// Invalid denomination.
InvalidDenomination(ParseDenominationError),
} }
impl fmt::Display for ParseAmountError { impl fmt::Display for ParseAmountError {
@ -170,13 +208,11 @@ impl fmt::Display for ParseAmountError {
use ParseAmountError::*; use ParseAmountError::*;
match *self { match *self {
Negative => f.write_str("amount is negative"), OutOfRange(error) => write_err!(f, "amount out of range"; error),
TooBig => f.write_str("amount is too big"),
TooPrecise => f.write_str("amount has a too high precision"), TooPrecise => f.write_str("amount has a too high precision"),
InvalidFormat => f.write_str("invalid number format"), InvalidFormat => f.write_str("invalid number format"),
InputTooLarge => f.write_str("input string was too large"), InputTooLarge => f.write_str("input string was too large"),
InvalidCharacter(c) => write!(f, "invalid character in input: {}", c), InvalidCharacter(c) => write!(f, "invalid character in input: {}", c),
InvalidDenomination(ref e) => write_err!(f, "invalid denomination"; e),
} }
} }
} }
@ -187,17 +223,85 @@ impl std::error::Error for ParseAmountError {
use ParseAmountError::*; use ParseAmountError::*;
match *self { match *self {
Negative | TooBig | TooPrecise | InvalidFormat | InputTooLarge TooPrecise | InvalidFormat | InputTooLarge
| InvalidCharacter(_) => None, | InvalidCharacter(_) => None,
InvalidDenomination(ref e) => Some(e), OutOfRange(ref error) => Some(error),
} }
} }
} }
impl From<ParseDenominationError> for ParseAmountError { /// Returned when a parsed amount is too big or too small.
fn from(e: ParseDenominationError) -> Self { Self::InvalidDenomination(e) } #[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<OutOfRangeError> for ParseAmountError {
fn from(value: OutOfRangeError) -> Self {
ParseAmountError::OutOfRange(value)
}
}
/// An error during amount parsing. /// An error during amount parsing.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]
@ -277,18 +381,18 @@ fn is_too_precise(s: &str, precision: usize) -> bool {
fn parse_signed_to_satoshi( fn parse_signed_to_satoshi(
mut s: &str, mut s: &str,
denom: Denomination, denom: Denomination,
) -> Result<(bool, u64), ParseAmountError> { ) -> Result<(bool, u64), InnerParseError> {
if s.is_empty() { if s.is_empty() {
return Err(ParseAmountError::InvalidFormat); return Err(InnerParseError::InvalidFormat);
} }
if s.len() > 50 { if s.len() > 50 {
return Err(ParseAmountError::InputTooLarge); return Err(InnerParseError::InputTooLarge);
} }
let is_negative = s.starts_with('-'); let is_negative = s.starts_with('-');
if is_negative { if is_negative {
if s.len() == 1 { if s.len() == 1 {
return Err(ParseAmountError::InvalidFormat); return Err(InnerParseError::InvalidFormat);
} }
s = &s[1..]; s = &s[1..];
} }
@ -302,11 +406,11 @@ fn parse_signed_to_satoshi(
// into a less precise amount. That is not allowed unless // into a less precise amount. That is not allowed unless
// there are no decimals and the last digits are zeroes as // there are no decimals and the last digits are zeroes as
// many as the difference in precision. // many as the difference in precision.
let last_n = unsigned_abs(precision_diff).into(); let last_n = precision_diff.unsigned_abs().into();
if is_too_precise(s, last_n) { if is_too_precise(s, last_n) {
match s.parse::<i64>() { match s.parse::<i64>() {
Ok(0) => return Ok((is_negative, 0)), Ok(0) => return Ok((is_negative, 0)),
_ => return Err(ParseAmountError::TooPrecise), _ => return Err(InnerParseError::TooPrecise),
} }
} }
s = &s[0..s.find('.').unwrap_or(s.len()) - last_n]; s = &s[0..s.find('.').unwrap_or(s.len()) - last_n];
@ -323,9 +427,9 @@ fn parse_signed_to_satoshi(
'0'..='9' => { '0'..='9' => {
// Do `value = 10 * value + digit`, catching overflows. // Do `value = 10 * value + digit`, catching overflows.
match 10_u64.checked_mul(value) { match 10_u64.checked_mul(value) {
None => return Err(ParseAmountError::TooBig), None => return Err(InnerParseError::Overflow { is_negative }),
Some(val) => match val.checked_add((c as u8 - b'0') as u64) { Some(val) => match val.checked_add((c as u8 - b'0') as u64) {
None => return Err(ParseAmountError::TooBig), None => return Err(InnerParseError::Overflow { is_negative }),
Some(val) => value = val, Some(val) => value = val,
}, },
} }
@ -333,16 +437,16 @@ fn parse_signed_to_satoshi(
decimals = match decimals { decimals = match decimals {
None => None, None => None,
Some(d) if d < max_decimals => Some(d + 1), Some(d) if d < max_decimals => Some(d + 1),
_ => return Err(ParseAmountError::TooPrecise), _ => return Err(InnerParseError::TooPrecise),
}; };
} }
'.' => match decimals { '.' => match decimals {
None if max_decimals <= 0 => break, None if max_decimals <= 0 => break,
None => decimals = Some(0), None => decimals = Some(0),
// Double decimal dot. // Double decimal dot.
_ => return Err(ParseAmountError::InvalidFormat), _ => return Err(InnerParseError::InvalidFormat),
}, },
c => return Err(ParseAmountError::InvalidCharacter(c)), c => return Err(InnerParseError::InvalidCharacter(c)),
} }
} }
@ -351,14 +455,34 @@ fn parse_signed_to_satoshi(
for _ in 0..scale_factor { for _ in 0..scale_factor {
value = match 10_u64.checked_mul(value) { value = match 10_u64.checked_mul(value) {
Some(v) => v, Some(v) => v,
None => return Err(ParseAmountError::TooBig), None => return Err(InnerParseError::Overflow { is_negative }),
}; };
} }
Ok((is_negative, value)) Ok((is_negative, value))
} }
fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseAmountError> { enum InnerParseError {
Overflow { is_negative: bool },
TooPrecise,
InvalidFormat,
InputTooLarge,
InvalidCharacter(char),
}
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 => ParseAmountError::TooPrecise,
Self::InvalidFormat => ParseAmountError::InvalidFormat,
Self::InputTooLarge => ParseAmountError::InputTooLarge,
Self::InvalidCharacter(c) => ParseAmountError::InvalidCharacter(c),
}
}
}
fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseError> {
let (i, j) = if let Some(i) = s.find(' ') { let (i, j) = if let Some(i) = s.find(' ') {
(i, i + 1) (i, i + 1)
} else { } else {
@ -416,9 +540,6 @@ fn dec_width(mut num: u64) -> usize {
width width
} }
// NIH due to MSRV, impl copied from `core::i8::unsigned_abs` (introduced in Rust 1.51.1).
fn unsigned_abs(x: i8) -> u8 { x.wrapping_abs() as u8 }
fn repeat_char(f: &mut dyn fmt::Write, c: char, count: usize) -> fmt::Result { fn repeat_char(f: &mut dyn fmt::Write, c: char, count: usize) -> fmt::Result {
for _ in 0..count { for _ in 0..count {
f.write_char(c)?; f.write_char(c)?;
@ -452,7 +573,7 @@ fn fmt_satoshi_in(
trailing_decimal_zeros = options.precision.unwrap_or(0); trailing_decimal_zeros = options.precision.unwrap_or(0);
} }
Ordering::Less => { Ordering::Less => {
let precision = unsigned_abs(precision); let precision = precision.unsigned_abs();
let divisor = 10u64.pow(precision.into()); let divisor = 10u64.pow(precision.into());
num_before_decimal_point = satoshi / divisor; num_before_decimal_point = satoshi / divisor;
num_after_decimal_point = satoshi % divisor; num_after_decimal_point = satoshi % divisor;
@ -613,9 +734,10 @@ impl Amount {
/// Note: This only parses the value string. If you want to parse a value /// Note: This only parses the value string. If you want to parse a value
/// with denomination, use [FromStr]. /// with denomination, use [FromStr].
pub fn from_str_in(s: &str, denom: Denomination) -> Result<Amount, ParseAmountError> { pub fn from_str_in(s: &str, denom: Denomination) -> Result<Amount, ParseAmountError> {
let (negative, satoshi) = parse_signed_to_satoshi(s, denom)?; let (negative, satoshi) = parse_signed_to_satoshi(s, denom)
.map_err(|error| error.convert(false))?;
if negative { if negative {
return Err(ParseAmountError::Negative); return Err(ParseAmountError::OutOfRange(OutOfRangeError::negative()));
} }
Ok(Amount::from_sat(satoshi)) Ok(Amount::from_sat(satoshi))
} }
@ -624,9 +746,9 @@ impl Amount {
/// [Self::to_string_with_denomination] or with [fmt::Display]. /// [Self::to_string_with_denomination] or with [fmt::Display].
/// If you want to parse only the amount without the denomination, /// If you want to parse only the amount without the denomination,
/// use [Self::from_str_in]. /// use [Self::from_str_in].
pub fn from_str_with_denomination(s: &str) -> Result<Amount, ParseAmountError> { pub fn from_str_with_denomination(s: &str) -> Result<Amount, ParseError> {
let (amt, denom) = split_amount_and_denomination(s)?; let (amt, denom) = split_amount_and_denomination(s)?;
Amount::from_str_in(amt, denom) Amount::from_str_in(amt, denom).map_err(Into::into)
} }
/// Express this [Amount] as a floating-point value in the given denomination. /// Express this [Amount] as a floating-point value in the given denomination.
@ -655,7 +777,7 @@ impl Amount {
/// Please be aware of the risk of using floating-point numbers. /// Please be aware of the risk of using floating-point numbers.
pub fn from_float_in(value: f64, denom: Denomination) -> Result<Amount, ParseAmountError> { pub fn from_float_in(value: f64, denom: Denomination) -> Result<Amount, ParseAmountError> {
if value < 0.0 { if value < 0.0 {
return Err(ParseAmountError::Negative); return Err(OutOfRangeError::negative().into());
} }
// This is inefficient, but the safest way to deal with this. The parsing logic is safe. // 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. // Any performance-critical application should not be dealing with floats.
@ -740,7 +862,7 @@ impl Amount {
/// Convert to a signed amount. /// Convert to a signed amount.
pub fn to_signed(self) -> Result<SignedAmount, ParseAmountError> { pub fn to_signed(self) -> Result<SignedAmount, ParseAmountError> {
if self.to_sat() > SignedAmount::MAX.to_sat() as u64 { if self.to_sat() > SignedAmount::MAX.to_sat() as u64 {
Err(ParseAmountError::TooBig) Err(ParseAmountError::OutOfRange(OutOfRangeError::too_big(true)))
} else { } else {
Ok(SignedAmount::from_sat(self.to_sat() as i64)) Ok(SignedAmount::from_sat(self.to_sat() as i64))
} }
@ -825,7 +947,7 @@ impl ops::DivAssign<u64> for Amount {
} }
impl FromStr for Amount { impl FromStr for Amount {
type Err = ParseAmountError; type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> { Amount::from_str_with_denomination(s) } fn from_str(s: &str) -> Result<Self, Self::Err> { Amount::from_str_with_denomination(s) }
} }
@ -943,12 +1065,12 @@ impl SignedAmount {
/// Note: This only parses the value string. If you want to parse a value /// Note: This only parses the value string. If you want to parse a value
/// with denomination, use [FromStr]. /// with denomination, use [FromStr].
pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> { pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParseAmountError> {
match parse_signed_to_satoshi(s, denom)? { match parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(true))? {
// (negative, amount) // (negative, amount)
(false, sat) if sat > i64::MAX as u64 => Err(ParseAmountError::TooBig), (false, sat) if sat > i64::MAX as u64 => Err(ParseAmountError::OutOfRange(OutOfRangeError::too_big(true))),
(false, sat) => Ok(SignedAmount(sat as i64)), (false, sat) => Ok(SignedAmount(sat as i64)),
(true, sat) if sat == i64::MAX as u64 + 1 => Ok(SignedAmount(i64::MIN)), (true, sat) if sat == i64::MIN.unsigned_abs() => Ok(SignedAmount(i64::MIN)),
(true, sat) if sat > i64::MAX as u64 + 1 => Err(ParseAmountError::TooBig), (true, sat) if sat > i64::MIN.unsigned_abs() => Err(ParseAmountError::OutOfRange(OutOfRangeError::too_small())),
(true, sat) => Ok(SignedAmount(-(sat as i64))), (true, sat) => Ok(SignedAmount(-(sat as i64))),
} }
} }
@ -957,9 +1079,9 @@ impl SignedAmount {
/// [Self::to_string_with_denomination] or with [fmt::Display]. /// [Self::to_string_with_denomination] or with [fmt::Display].
/// If you want to parse only the amount without the denomination, /// If you want to parse only the amount without the denomination,
/// use [Self::from_str_in]. /// use [Self::from_str_in].
pub fn from_str_with_denomination(s: &str) -> Result<SignedAmount, ParseAmountError> { pub fn from_str_with_denomination(s: &str) -> Result<SignedAmount, ParseError> {
let (amt, denom) = split_amount_and_denomination(s)?; let (amt, denom) = split_amount_and_denomination(s)?;
SignedAmount::from_str_in(amt, denom) SignedAmount::from_str_in(amt, denom).map_err(Into::into)
} }
/// Express this [SignedAmount] as a floating-point value in the given denomination. /// Express this [SignedAmount] as a floating-point value in the given denomination.
@ -990,15 +1112,10 @@ impl SignedAmount {
SignedAmount::from_str_in(&value.to_string(), denom) SignedAmount::from_str_in(&value.to_string(), denom)
} }
/// Returns the absolute value as satoshis.
///
/// This is the implementation of `unsigned_abs()` copied from `core` to support older MSRV.
fn to_sat_abs(self) -> u64 { self.to_sat().wrapping_abs() as u64 }
/// Create an object that implements [`fmt::Display`] using specified denomination. /// Create an object that implements [`fmt::Display`] using specified denomination.
pub fn display_in(self, denomination: Denomination) -> Display { pub fn display_in(self, denomination: Denomination) -> Display {
Display { Display {
sats_abs: self.to_sat_abs(), sats_abs: self.unsigned_abs().to_sat(),
is_negative: self.is_negative(), is_negative: self.is_negative(),
style: DisplayStyle::FixedDenomination { denomination, show_denomination: false }, style: DisplayStyle::FixedDenomination { denomination, show_denomination: false },
} }
@ -1010,7 +1127,7 @@ impl SignedAmount {
/// avoid confusion the denomination is always shown. /// avoid confusion the denomination is always shown.
pub fn display_dynamic(self) -> Display { pub fn display_dynamic(self) -> Display {
Display { Display {
sats_abs: self.to_sat_abs(), sats_abs: self.unsigned_abs().to_sat(),
is_negative: self.is_negative(), is_negative: self.is_negative(),
style: DisplayStyle::DynamicDenomination, style: DisplayStyle::DynamicDenomination,
} }
@ -1021,7 +1138,7 @@ impl SignedAmount {
/// Does not include the denomination. /// Does not include the denomination.
#[rustfmt::skip] #[rustfmt::skip]
pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result { pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result {
fmt_satoshi_in(self.to_sat_abs(), self.is_negative(), f, denom, false, FormatOptions::default()) 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. /// Get a string number of this [SignedAmount] in the given denomination.
@ -1047,6 +1164,9 @@ impl SignedAmount {
/// Get the absolute value of this [SignedAmount]. /// Get the absolute value of this [SignedAmount].
pub fn abs(self) -> SignedAmount { SignedAmount(self.0.abs()) } 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]. /// Returns a number representing sign of this [SignedAmount].
/// ///
/// - `0` if the amount is zero /// - `0` if the amount is zero
@ -1109,9 +1229,9 @@ impl SignedAmount {
} }
/// Convert to an unsigned amount. /// Convert to an unsigned amount.
pub fn to_unsigned(self) -> Result<Amount, ParseAmountError> { pub fn to_unsigned(self) -> Result<Amount, OutOfRangeError> {
if self.is_negative() { if self.is_negative() {
Err(ParseAmountError::Negative) Err(OutOfRangeError::negative())
} else { } else {
Ok(Amount::from_sat(self.to_sat() as u64)) Ok(Amount::from_sat(self.to_sat() as u64))
} }
@ -1198,7 +1318,7 @@ impl ops::DivAssign<i64> for SignedAmount {
} }
impl FromStr for SignedAmount { impl FromStr for SignedAmount {
type Err = ParseAmountError; type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> { SignedAmount::from_str_with_denomination(s) } fn from_str(s: &str) -> Result<Self, Self::Err> { SignedAmount::from_str_with_denomination(s) }
} }
@ -1270,9 +1390,10 @@ pub mod serde {
//! } //! }
//! ``` //! ```
use core::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::amount::{Amount, Denomination, SignedAmount}; use crate::amount::{Amount, Denomination, SignedAmount, ParseAmountError};
/// This trait is used only to avoid code duplication and naming collisions /// This trait is used only to avoid code duplication and naming collisions
/// of the different serde serialization crates. /// of the different serde serialization crates.
@ -1298,6 +1419,30 @@ pub mod serde {
fn ser_btc_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>; fn ser_btc_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>;
} }
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 { impl SerdeAmount for Amount {
fn ser_sat<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { fn ser_sat<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
u64::serialize(&self.to_sat(), s) u64::serialize(&self.to_sat(), s)
@ -1310,7 +1455,9 @@ pub mod serde {
} }
fn des_btc<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> { fn des_btc<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> {
use serde::de::Error; use serde::de::Error;
Amount::from_btc(f64::deserialize(d)?).map_err(D::Error::custom) Amount::from_btc(f64::deserialize(d)?)
.map_err(DisplayFullError)
.map_err(D::Error::custom)
} }
} }
@ -1336,7 +1483,9 @@ pub mod serde {
} }
fn des_btc<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> { fn des_btc<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> {
use serde::de::Error; use serde::de::Error;
SignedAmount::from_btc(f64::deserialize(d)?).map_err(D::Error::custom) SignedAmount::from_btc(f64::deserialize(d)?)
.map_err(DisplayFullError)
.map_err(D::Error::custom)
} }
} }
@ -1529,7 +1678,7 @@ mod verification {
if n1 <= i64::MAX as u64 { if n1 <= i64::MAX as u64 {
Ok(SignedAmount::from_sat(n1.try_into().unwrap())) Ok(SignedAmount::from_sat(n1.try_into().unwrap()))
} else { } else {
Err(ParseAmountError::TooBig) Err(ParseAmountError::OutOfRange(OutOfRangeError::too_big(false)))
}, },
); );
} }
@ -1632,7 +1781,7 @@ mod tests {
let s = format!("-0 {}", denom); let s = format!("-0 {}", denom);
match Amount::from_str(&s) { match Amount::from_str(&s) {
Err(e) => assert_eq!(e, ParseAmountError::Negative), Err(e) => assert_eq!(e, ParseError::Amount(ParseAmountError::OutOfRange(OutOfRangeError::negative()))),
Ok(_) => panic!("Unsigned amount from {}", s), Ok(_) => panic!("Unsigned amount from {}", s),
} }
match SignedAmount::from_str(&s) { match SignedAmount::from_str(&s) {
@ -1710,24 +1859,24 @@ mod tests {
assert_eq!(f(0.0001234, D::Bitcoin), Ok(sat(12340))); assert_eq!(f(0.0001234, D::Bitcoin), Ok(sat(12340)));
assert_eq!(sf(-0.00012345, D::Bitcoin), Ok(ssat(-12345))); assert_eq!(sf(-0.00012345, D::Bitcoin), Ok(ssat(-12345)));
assert_eq!(f(-100.0, D::MilliSatoshi), Err(ParseAmountError::Negative)); assert_eq!(f(-100.0, D::MilliSatoshi), Err(OutOfRangeError::negative().into()));
assert_eq!(f(11.22, D::Satoshi), Err(ParseAmountError::TooPrecise)); assert_eq!(f(11.22, D::Satoshi), Err(ParseAmountError::TooPrecise));
assert_eq!(sf(-100.0, D::MilliSatoshi), Err(ParseAmountError::TooPrecise)); assert_eq!(sf(-100.0, D::MilliSatoshi), Err(ParseAmountError::TooPrecise));
assert_eq!(f(42.123456781, D::Bitcoin), Err(ParseAmountError::TooPrecise)); assert_eq!(f(42.123456781, D::Bitcoin), Err(ParseAmountError::TooPrecise));
assert_eq!(sf(-184467440738.0, D::Bitcoin), Err(ParseAmountError::TooBig)); assert_eq!(sf(-184467440738.0, D::Bitcoin), Err(OutOfRangeError::too_small().into()));
assert_eq!(f(18446744073709551617.0, D::Satoshi), Err(ParseAmountError::TooBig)); assert_eq!(f(18446744073709551617.0, D::Satoshi), Err(OutOfRangeError::too_big(false).into()));
// Amount can be grater than the max SignedAmount. // Amount can be grater than the max SignedAmount.
assert!(f(SignedAmount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi).is_ok()); assert!(f(SignedAmount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi).is_ok());
assert_eq!( assert_eq!(
f(Amount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi), f(Amount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi),
Err(ParseAmountError::TooBig) Err(OutOfRangeError::too_big(false).into())
); );
assert_eq!( assert_eq!(
sf(SignedAmount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi), sf(SignedAmount::MAX.to_float_in(D::Satoshi) + 1.0, D::Satoshi),
Err(ParseAmountError::TooBig) Err(OutOfRangeError::too_big(true).into())
); );
let btc = move |f| SignedAmount::from_btc(f).unwrap(); let btc = move |f| SignedAmount::from_btc(f).unwrap();
@ -1757,7 +1906,7 @@ mod tests {
assert_eq!(p("0.0 ", btc), Err(ParseAmountError::InvalidCharacter(' '))); assert_eq!(p("0.0 ", btc), Err(ParseAmountError::InvalidCharacter(' ')));
assert_eq!(p("0.000.000", btc), Err(E::InvalidFormat)); assert_eq!(p("0.000.000", btc), Err(E::InvalidFormat));
let more_than_max = format!("1{}", Amount::MAX); let more_than_max = format!("1{}", Amount::MAX);
assert_eq!(p(&more_than_max, btc), Err(E::TooBig)); assert_eq!(p(&more_than_max, btc), Err(OutOfRangeError::too_big(false).into()));
assert_eq!(p("0.000000042", btc), Err(E::TooPrecise)); assert_eq!(p("0.000000042", btc), Err(E::TooPrecise));
assert_eq!(p("999.0000000", msat), Err(E::TooPrecise)); assert_eq!(p("999.0000000", msat), Err(E::TooPrecise));
assert_eq!(p("1.0000000", msat), Err(E::TooPrecise)); assert_eq!(p("1.0000000", msat), Err(E::TooPrecise));
@ -1795,7 +1944,7 @@ mod tests {
// exactly 50 chars. // exactly 50 chars.
assert_eq!( assert_eq!(
p("100000000000000.0000000000000000000000000000000000", Denomination::Bitcoin), p("100000000000000.0000000000000000000000000000000000", Denomination::Bitcoin),
Err(E::TooBig) Err(OutOfRangeError::too_big(false).into())
); );
// more than 50 chars. // more than 50 chars.
assert_eq!( assert_eq!(
@ -2011,13 +2160,12 @@ mod tests {
#[test] #[test]
fn test_unsigned_signed_conversion() { fn test_unsigned_signed_conversion() {
use super::ParseAmountError as E;
let sa = SignedAmount::from_sat; let sa = SignedAmount::from_sat;
let ua = Amount::from_sat; let ua = Amount::from_sat;
assert_eq!(Amount::MAX.to_signed(), Err(E::TooBig)); assert_eq!(Amount::MAX.to_signed(), Err(OutOfRangeError::too_big(true).into()));
assert_eq!(ua(i64::MAX as u64).to_signed(), Ok(sa(i64::MAX))); assert_eq!(ua(i64::MAX as u64).to_signed(), Ok(sa(i64::MAX)));
assert_eq!(ua(i64::MAX as u64 + 1).to_signed(), Err(E::TooBig)); assert_eq!(ua(i64::MAX as u64 + 1).to_signed(), Err(OutOfRangeError::too_big(true).into()));
assert_eq!(sa(i64::MAX).to_unsigned(), Ok(ua(i64::MAX as u64))); assert_eq!(sa(i64::MAX).to_unsigned(), Ok(ua(i64::MAX as u64)));
@ -2031,7 +2179,7 @@ mod tests {
use super::ParseAmountError as E; use super::ParseAmountError as E;
assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x'))); assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x').into()));
assert_eq!( assert_eq!(
Amount::from_str("xBTC"), Amount::from_str("xBTC"),
Err(Unknown(UnknownDenominationError("xBTC".into())).into()), Err(Unknown(UnknownDenominationError("xBTC".into())).into()),
@ -2040,42 +2188,56 @@ mod tests {
Amount::from_str("5 BTC BTC"), Amount::from_str("5 BTC BTC"),
Err(Unknown(UnknownDenominationError("BTC BTC".into())).into()), Err(Unknown(UnknownDenominationError("BTC BTC".into())).into()),
); );
assert_eq!(Amount::from_str("5BTC BTC"), Err(E::InvalidCharacter('B'))); assert_eq!(Amount::from_str("5BTC BTC"), Err(E::InvalidCharacter('B').into()));
assert_eq!( assert_eq!(
Amount::from_str("5 5 BTC"), Amount::from_str("5 5 BTC"),
Err(Unknown(UnknownDenominationError("5 BTC".into())).into()), Err(Unknown(UnknownDenominationError("5 BTC".into())).into()),
); );
#[track_caller] #[track_caller]
fn case(s: &str, expected: Result<Amount, ParseAmountError>) { fn ok_case(s: &str, expected: Amount) {
assert_eq!(Amount::from_str(s).unwrap(), expected);
assert_eq!(Amount::from_str(&s.replace(' ', "")).unwrap(), expected);
}
#[track_caller]
fn case(s: &str, expected: Result<Amount, impl Into<ParseError>>) {
let expected = expected.map_err(Into::into);
assert_eq!(Amount::from_str(s), expected); assert_eq!(Amount::from_str(s), expected);
assert_eq!(Amount::from_str(&s.replace(' ', "")), expected); assert_eq!(Amount::from_str(&s.replace(' ', "")), expected);
} }
#[track_caller] #[track_caller]
fn scase(s: &str, expected: Result<SignedAmount, ParseAmountError>) { fn ok_scase(s: &str, expected: SignedAmount) {
assert_eq!(SignedAmount::from_str(s).unwrap(), expected);
assert_eq!(SignedAmount::from_str(&s.replace(' ', "")).unwrap(), expected);
}
#[track_caller]
fn scase(s: &str, expected: Result<SignedAmount, impl Into<ParseError>>) {
let expected = expected.map_err(Into::into);
assert_eq!(SignedAmount::from_str(s), expected); assert_eq!(SignedAmount::from_str(s), expected);
assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected); assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected);
} }
case("5 BCH", Err(Unknown(UnknownDenominationError("BCH".into())).into())); case("5 BCH", Err(Unknown(UnknownDenominationError("BCH".into()))));
case("-1 BTC", Err(E::Negative)); case("-1 BTC", Err(OutOfRangeError::negative()));
case("-0.0 BTC", Err(E::Negative)); case("-0.0 BTC", Err(OutOfRangeError::negative()));
case("0.123456789 BTC", Err(E::TooPrecise)); case("0.123456789 BTC", Err(E::TooPrecise));
scase("-0.1 satoshi", Err(E::TooPrecise)); scase("-0.1 satoshi", Err(E::TooPrecise));
case("0.123456 mBTC", Err(E::TooPrecise)); case("0.123456 mBTC", Err(E::TooPrecise));
scase("-1.001 bits", Err(E::TooPrecise)); scase("-1.001 bits", Err(E::TooPrecise));
scase("-200000000000 BTC", Err(E::TooBig)); scase("-200000000000 BTC", Err(OutOfRangeError::too_small()));
case("18446744073709551616 sat", Err(E::TooBig)); case("18446744073709551616 sat", Err(OutOfRangeError::too_big(false)));
case(".5 bits", Ok(Amount::from_sat(50))); ok_case(".5 bits", Amount::from_sat(50));
scase("-.5 bits", Ok(SignedAmount::from_sat(-50))); ok_scase("-.5 bits", SignedAmount::from_sat(-50));
case("0.00253583 BTC", Ok(Amount::from_sat(253583))); ok_case("0.00253583 BTC", Amount::from_sat(253583));
scase("-5 satoshi", Ok(SignedAmount::from_sat(-5))); ok_scase("-5 satoshi", SignedAmount::from_sat(-5));
case("0.10000000 BTC", Ok(Amount::from_sat(100_000_00))); ok_case("0.10000000 BTC", Amount::from_sat(100_000_00));
scase("-100 bits", Ok(SignedAmount::from_sat(-10_000))); ok_scase("-100 bits", SignedAmount::from_sat(-10_000));
scase(&format!("{} SAT", i64::MIN), Ok(SignedAmount::from_sat(i64::MIN))); ok_scase(&format!("{} SAT", i64::MIN), SignedAmount::from_sat(i64::MIN));
} }
#[test] #[test]
@ -2129,12 +2291,12 @@ mod tests {
assert_eq!( assert_eq!(
sa_str(&sa_sat(i64::MAX).to_string_in(D::Satoshi), D::MicroBitcoin), sa_str(&sa_sat(i64::MAX).to_string_in(D::Satoshi), D::MicroBitcoin),
Err(ParseAmountError::TooBig) Err(OutOfRangeError::too_big(true).into())
); );
// Test an overflow bug in `abs()` // Test an overflow bug in `abs()`
assert_eq!( assert_eq!(
sa_str(&sa_sat(i64::MIN).to_string_in(D::Satoshi), D::MicroBitcoin), sa_str(&sa_sat(i64::MIN).to_string_in(D::Satoshi), D::MicroBitcoin),
Err(ParseAmountError::TooBig) Err(OutOfRangeError::too_small().into())
); );
assert_eq!( assert_eq!(
@ -2247,7 +2409,7 @@ mod tests {
serde_json::from_str("{\"amt\": 1000000.000000001, \"samt\": 1}"); serde_json::from_str("{\"amt\": 1000000.000000001, \"samt\": 1}");
assert!(t.unwrap_err().to_string().contains(&ParseAmountError::TooPrecise.to_string())); assert!(t.unwrap_err().to_string().contains(&ParseAmountError::TooPrecise.to_string()));
let t: Result<T, serde_json::Error> = serde_json::from_str("{\"amt\": -1, \"samt\": 1}"); let t: Result<T, serde_json::Error> = serde_json::from_str("{\"amt\": -1, \"samt\": 1}");
assert!(t.unwrap_err().to_string().contains(&ParseAmountError::Negative.to_string())); assert!(dbg!(t.unwrap_err().to_string()).contains(&OutOfRangeError::negative().to_string()));
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@ -2420,7 +2582,7 @@ mod tests {
for denom in unknown.iter() { for denom in unknown.iter() {
match Denomination::from_str(denom) { match Denomination::from_str(denom) {
Ok(_) => panic!("from_str should error for {}", denom), Ok(_) => panic!("from_str should error for {}", denom),
Err(ParseDenominationError::Unknown(_)) => {} Err(ParseDenominationError::Unknown(_)) => (),
Err(e) => panic!("unexpected error: {}", e), Err(e) => panic!("unexpected error: {}", e),
} }
} }