Model `TooBig` and `Negative` as `OutOfRange`
The error returned when parsing amount had a `Negative` variant which was weird/unreachable when parsing `SignedAmount`. Also weirdly, parsing would return `TooBig` when the amount was negative - too low. To resolve this we merge them into one `OutOfRange` variant that nuges the consumers to make principled decisions and print error messages as amounts being more than or less than a specific value which is easier to understand for the users. Notably, the API still allows getting information about which type was parsed and which bound was crossed but in a less obvious way. This is OK since users who have a very good reason can use this information but most won't. Closes #2266
This commit is contained in:
parent
54cbbf804f
commit
7bf478373a
|
@ -145,7 +145,6 @@ impl FromStr for Denomination {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// An error during amount parsing amount with denomination.
|
/// An error during amount parsing amount with denomination.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
|
@ -165,6 +164,10 @@ impl From<ParseDenominationError> for ParseError {
|
||||||
fn from(e: ParseDenominationError) -> Self { Self::Denomination(e) }
|
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 {
|
impl fmt::Display for ParseError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
@ -188,10 +191,8 @@ impl std::error::Error for ParseError {
|
||||||
#[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.
|
||||||
|
@ -207,8 +208,7 @@ 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"),
|
||||||
|
@ -223,12 +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,
|
||||||
|
OutOfRange(ref error) => Some(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<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]
|
||||||
|
@ -308,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..];
|
||||||
}
|
}
|
||||||
|
@ -337,7 +410,7 @@ fn parse_signed_to_satoshi(
|
||||||
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];
|
||||||
|
@ -354,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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -364,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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,13 +455,33 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
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)
|
||||||
|
@ -641,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))
|
||||||
}
|
}
|
||||||
|
@ -683,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.
|
||||||
|
@ -768,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))
|
||||||
}
|
}
|
||||||
|
@ -971,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::MIN.unsigned_abs() => Ok(SignedAmount(i64::MIN)),
|
(true, sat) if sat == i64::MIN.unsigned_abs() => Ok(SignedAmount(i64::MIN)),
|
||||||
(true, sat) if sat > i64::MIN.unsigned_abs() => 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))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1135,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))
|
||||||
}
|
}
|
||||||
|
@ -1296,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.
|
||||||
|
@ -1324,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)
|
||||||
|
@ -1336,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1362,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1555,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)))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1658,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, ParseError::Amount(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) {
|
||||||
|
@ -1736,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();
|
||||||
|
@ -1783,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));
|
||||||
|
@ -1821,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!(
|
||||||
|
@ -2037,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)));
|
||||||
|
|
||||||
|
@ -2100,14 +2222,14 @@ mod tests {
|
||||||
|
|
||||||
case("5 BCH", Err(Unknown(UnknownDenominationError("BCH".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)));
|
||||||
|
|
||||||
ok_case(".5 bits", Amount::from_sat(50));
|
ok_case(".5 bits", Amount::from_sat(50));
|
||||||
ok_scase("-.5 bits", SignedAmount::from_sat(-50));
|
ok_scase("-.5 bits", SignedAmount::from_sat(-50));
|
||||||
|
@ -2169,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!(
|
||||||
|
@ -2287,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")]
|
||||||
|
@ -2460,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue