Merge rust-bitcoin/rust-bitcoin#1768: Allow parsing sub-sat denominations with decimal points

6c6a89b1d1 Add sub-sat fractions parsing regression test (Martin Habovstiak)
f1a3dc6719 Allow parsing sub-sat denoms with decimal points (Martin Habovstiak)
b3d9a267ea Add a few more amount parsing tests (Martin Habovstiak)

Pull request description:

  Numbers with only zeros after decimal points are valid if they are also
  multiples of `10^precision` (e.g. 1000 for msats). These were
  artificially disallowed as "too precise" which was at least misleading.

  This change allows parsing such numbers.

  And yes, I know this is not perfectly efficient (unless the compiler figures out some magic opts) but so isn't the rest of the code. TBH this parsing code drives me crazy and I'd love to rewrite it to be more efficient and readable.

ACKs for top commit:
  apoelstra:
    ACK 6c6a89b1d1

Tree-SHA512: 03cf4b416f2eac25e0aac57ef964ed06fa36c7fe8244bdcf97852cc58e1613b1ec6132379b834da58ad3240fdd61508a384202f63aa9ffa335c18cd7b2b724d3
This commit is contained in:
Andrew Poelstra 2023-05-10 19:56:15 +00:00
commit 684e14caee
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
1 changed files with 22 additions and 3 deletions

View File

@ -201,7 +201,13 @@ impl std::error::Error for ParseAmountError {
} }
fn is_too_precise(s: &str, precision: usize) -> bool { fn is_too_precise(s: &str, precision: usize) -> bool {
s.contains('.') || precision >= s.len() || s.chars().rev().take(precision).any(|d| d != '0') match s.find('.') {
Some(pos) =>
s[(pos + 1)..].chars().any(|d| d != '0')
|| precision >= pos
|| s[..pos].chars().rev().take(precision).any(|d| d != '0'),
None => precision >= s.len() || s.chars().rev().take(precision).any(|d| d != '0'),
}
} }
/// Parse decimal string in the given denomination into a satoshi value and a /// Parse decimal string in the given denomination into a satoshi value and a
@ -229,7 +235,7 @@ fn parse_signed_to_satoshi(
// The difference in precision between native (satoshi) // The difference in precision between native (satoshi)
// and desired denomination. // and desired denomination.
let precision_diff = -denom.precision(); let precision_diff = -denom.precision();
if precision_diff < 0 { if precision_diff <= 0 {
// If precision diff is negative, this means we are parsing // If precision diff is negative, this means we are parsing
// 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
@ -241,7 +247,7 @@ fn parse_signed_to_satoshi(
_ => return Err(ParseAmountError::TooPrecise), _ => return Err(ParseAmountError::TooPrecise),
} }
} }
s = &s[0..s.len() - last_n]; s = &s[0..s.find('.').unwrap_or(s.len()) - last_n];
0 0
} else { } else {
precision_diff precision_diff
@ -269,6 +275,7 @@ fn parse_signed_to_satoshi(
}; };
} }
'.' => match decimals { '.' => match decimals {
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(ParseAmountError::InvalidFormat),
@ -1672,6 +1679,7 @@ mod tests {
use super::ParseAmountError as E; use super::ParseAmountError as E;
let btc = Denomination::Bitcoin; let btc = Denomination::Bitcoin;
let sat = Denomination::Satoshi; let sat = Denomination::Satoshi;
let msat = Denomination::MilliSatoshi;
let p = Amount::from_str_in; let p = Amount::from_str_in;
let sp = SignedAmount::from_str_in; let sp = SignedAmount::from_str_in;
@ -1684,6 +1692,15 @@ mod tests {
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(E::TooBig));
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("1.0000000", msat), Err(E::TooPrecise));
assert_eq!(p("1.1", msat), Err(E::TooPrecise));
assert_eq!(p("1000.1", msat), Err(E::TooPrecise));
assert_eq!(p("1001.0000000", msat), Err(E::TooPrecise));
assert_eq!(p("1000.0000001", msat), Err(E::TooPrecise));
assert_eq!(p("1000.1000000", msat), Err(E::TooPrecise));
assert_eq!(p("1100.0000000", msat), Err(E::TooPrecise));
assert_eq!(p("10001.0000000", msat), Err(E::TooPrecise));
assert_eq!(p("1", btc), Ok(Amount::from_sat(1_000_000_00))); 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))); assert_eq!(sp("-.5", btc), Ok(SignedAmount::from_sat(-500_000_00)));
@ -1697,6 +1714,8 @@ mod tests {
p("12345678901.12345678", btc), p("12345678901.12345678", btc),
Ok(Amount::from_sat(12_345_678_901__123_456_78)) Ok(Amount::from_sat(12_345_678_901__123_456_78))
); );
assert_eq!(p("1000.0", msat), Ok(Amount::from_sat(1)));
assert_eq!(p("1000.000000000000000000000000000", msat), Ok(Amount::from_sat(1)));
// make sure satoshi > i64::MAX is checked. // make sure satoshi > i64::MAX is checked.
let amount = Amount::from_sat(i64::MAX as u64); let amount = Amount::from_sat(i64::MAX as u64);