Skip to content

Commit 6a85997

Browse files
committed
Merge rust-bitcoin/rust-bitcoin#768: add nano and pico BTC to Denomination enum
40f38b3 enforce strict SI(treat capital of m, u, n, p as invalid) in parsing amount denomiation. add disallow_unknown_denomination test (KaFai Choi) e80de8b add nano and pico BTC to Donomination enum (KaFai Choi) Pull request description: Close [741](rust-bitcoin/rust-bitcoin#741) ACKs for top commit: Kixunil: ACK 40f38b3 apoelstra: ACK 40f38b3 dr-orlovsky: Changing review to ACK 40f38b3 since it was my misunderstanding and not a bug Tree-SHA512: 4cc380b8e7403e37e7993e25848b25d74c610d4e9fe274526c613d4b3e2a9f6677c7df52310fc1cab6f1d629d9529ff9f5a2efa41d9e07eab62d0989780ae3a4
2 parents 00895c5 + 0400af7 commit 6a85997

File tree

1 file changed

+69
-13
lines changed

1 file changed

+69
-13
lines changed

src/util/amount.rs

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ pub enum Denomination {
2828
MilliBitcoin,
2929
/// uBTC
3030
MicroBitcoin,
31+
/// nBTC
32+
NanoBitcoin,
33+
/// pBTC
34+
PicoBitcoin,
3135
/// bits
3236
Bit,
3337
/// satoshi
@@ -43,6 +47,8 @@ impl Denomination {
4347
Denomination::Bitcoin => -8,
4448
Denomination::MilliBitcoin => -5,
4549
Denomination::MicroBitcoin => -2,
50+
Denomination::NanoBitcoin => 1,
51+
Denomination::PicoBitcoin => 4,
4652
Denomination::Bit => -2,
4753
Denomination::Satoshi => 0,
4854
Denomination::MilliSatoshi => 3,
@@ -56,6 +62,8 @@ impl fmt::Display for Denomination {
5662
Denomination::Bitcoin => "BTC",
5763
Denomination::MilliBitcoin => "mBTC",
5864
Denomination::MicroBitcoin => "uBTC",
65+
Denomination::NanoBitcoin => "nBTC",
66+
Denomination::PicoBitcoin => "pBTC",
5967
Denomination::Bit => "bits",
6068
Denomination::Satoshi => "satoshi",
6169
Denomination::MilliSatoshi => "msat",
@@ -68,22 +76,26 @@ impl FromStr for Denomination {
6876

6977
/// Convert from a str to Denomination.
7078
///
71-
/// Any combination of upper and/or lower case, excluding uppercase 'M' is considered valid.
72-
/// - Singular: BTC, mBTC, uBTC
79+
/// Any combination of upper and/or lower case, excluding uppercase of SI(m, u, n, p) is considered valid.
80+
/// - Singular: BTC, mBTC, uBTC, nBTC, pBTC
7381
/// - Plural or singular: sat, satoshi, bit, msat
7482
///
75-
/// Due to ambiguity between mega and milli we prohibit usage of leading capital 'M'.
83+
/// Due to ambiguity between mega and milli, pico and peta we prohibit usage of leading capital 'M', 'P'.
7684
fn from_str(s: &str) -> Result<Self, Self::Err> {
7785
use self::ParseAmountError::*;
86+
use self::Denomination as D;
7887

79-
if s.starts_with('M') {
80-
return Err(denomination_from_str(s).map_or_else(
81-
|| UnknownDenomination(s.to_owned()),
82-
|_| PossiblyConfusingDenomination(s.to_owned())
83-
));
88+
let starts_with_uppercase = || s.starts_with(|ch: char| ch.is_uppercase());
89+
match denomination_from_str(s) {
90+
None => Err(UnknownDenomination(s.to_owned())),
91+
Some(D::MilliBitcoin) | Some(D::PicoBitcoin) | Some(D::MilliSatoshi) if starts_with_uppercase() => {
92+
Err(PossiblyConfusingDenomination(s.to_owned()))
93+
}
94+
Some(D::NanoBitcoin) | Some(D::MicroBitcoin) if starts_with_uppercase() => {
95+
Err(UnknownDenomination(s.to_owned()))
96+
}
97+
Some(d) => Ok(d),
8498
}
85-
86-
denomination_from_str(s).ok_or_else(|| UnknownDenomination(s.to_owned()))
8799
}
88100
}
89101

@@ -100,6 +112,14 @@ fn denomination_from_str(mut s: &str) -> Option<Denomination> {
100112
return Some(Denomination::MicroBitcoin);
101113
}
102114

115+
if s.eq_ignore_ascii_case("nBTC") {
116+
return Some(Denomination::NanoBitcoin);
117+
}
118+
119+
if s.eq_ignore_ascii_case("pBTC") {
120+
return Some(Denomination::PicoBitcoin);
121+
}
122+
103123
if s.ends_with('s') || s.ends_with('S') {
104124
s = &s[..(s.len() - 1)];
105125
}
@@ -153,7 +173,13 @@ impl fmt::Display for ParseAmountError {
153173
ParseAmountError::InvalidCharacter(c) => write!(f, "invalid character in input: {}", c),
154174
ParseAmountError::UnknownDenomination(ref d) => write!(f, "unknown denomination: {}", d),
155175
ParseAmountError::PossiblyConfusingDenomination(ref d) => {
156-
write!(f, "the 'M' at the beginning of {} should technically mean 'Mega' but that denomination is uncommon and maybe 'milli' was intended", d)
176+
let (letter, upper, lower) = match d.chars().next() {
177+
Some('M') => ('M', "Mega", "milli"),
178+
Some('P') => ('P',"Peta", "pico"),
179+
// This panic could be avoided by adding enum ConfusingDenomination { Mega, Peta } but is it worth it?
180+
_ => panic!("invalid error information"),
181+
};
182+
write!(f, "the '{}' at the beginning of {} should technically mean '{}' but that denomination is uncommon and maybe '{}' was intended", letter, d, upper, lower)
157183
}
158184
}
159185
}
@@ -1439,6 +1465,12 @@ mod tests {
14391465
assert_eq!("-5", SignedAmount::from_sat(-5).to_string_in(D::Satoshi));
14401466
assert_eq!("0.10000000", Amount::from_sat(100_000_00).to_string_in(D::Bitcoin));
14411467
assert_eq!("-100.00", SignedAmount::from_sat(-10_000).to_string_in(D::Bit));
1468+
assert_eq!("2535830", Amount::from_sat(253583).to_string_in(D::NanoBitcoin));
1469+
assert_eq!("-100000", SignedAmount::from_sat(-10_000).to_string_in(D::NanoBitcoin));
1470+
assert_eq!("2535830000", Amount::from_sat(253583).to_string_in(D::PicoBitcoin));
1471+
assert_eq!("-100000000", SignedAmount::from_sat(-10_000).to_string_in(D::PicoBitcoin));
1472+
1473+
14421474

14431475
assert_eq!(ua_str(&ua_sat(0).to_string_in(D::Satoshi), D::Satoshi), Ok(ua_sat(0)));
14441476
assert_eq!(ua_str(&ua_sat(500).to_string_in(D::Bitcoin), D::Bitcoin), Ok(ua_sat(500)));
@@ -1453,6 +1485,15 @@ mod tests {
14531485
// Test an overflow bug in `abs()`
14541486
assert_eq!(sa_str(&sa_sat(i64::min_value()).to_string_in(D::Satoshi), D::MicroBitcoin), Err(ParseAmountError::TooBig));
14551487

1488+
assert_eq!(sa_str(&sa_sat(-1).to_string_in(D::NanoBitcoin), D::NanoBitcoin), Ok(sa_sat(-1)));
1489+
assert_eq!(sa_str(&sa_sat(i64::max_value()).to_string_in(D::Satoshi), D::NanoBitcoin), Err(ParseAmountError::TooPrecise));
1490+
assert_eq!(sa_str(&sa_sat(i64::min_value()).to_string_in(D::Satoshi), D::NanoBitcoin), Err(ParseAmountError::TooPrecise));
1491+
1492+
assert_eq!(sa_str(&sa_sat(-1).to_string_in(D::PicoBitcoin), D::PicoBitcoin), Ok(sa_sat(-1)));
1493+
assert_eq!(sa_str(&sa_sat(i64::max_value()).to_string_in(D::Satoshi), D::PicoBitcoin), Err(ParseAmountError::TooPrecise));
1494+
assert_eq!(sa_str(&sa_sat(i64::min_value()).to_string_in(D::Satoshi), D::PicoBitcoin), Err(ParseAmountError::TooPrecise));
1495+
1496+
14561497
}
14571498

14581499
#[test]
@@ -1465,7 +1506,9 @@ mod tests {
14651506
assert_eq!(Amount::from_str(&denom(amt, D::MicroBitcoin)), Ok(amt));
14661507
assert_eq!(Amount::from_str(&denom(amt, D::Bit)), Ok(amt));
14671508
assert_eq!(Amount::from_str(&denom(amt, D::Satoshi)), Ok(amt));
1509+
assert_eq!(Amount::from_str(&denom(amt, D::NanoBitcoin)), Ok(amt));
14681510
assert_eq!(Amount::from_str(&denom(amt, D::MilliSatoshi)), Ok(amt));
1511+
assert_eq!(Amount::from_str(&denom(amt, D::PicoBitcoin)), Ok(amt));
14691512

14701513
assert_eq!(Amount::from_str("42 satoshi BTC"), Err(ParseAmountError::InvalidFormat));
14711514
assert_eq!(SignedAmount::from_str("-42 satoshi BTC"), Err(ParseAmountError::InvalidFormat));
@@ -1693,7 +1736,7 @@ mod tests {
16931736
#[test]
16941737
fn denomination_string_acceptable_forms() {
16951738
// Non-exhaustive list of valid forms.
1696-
let valid = vec!["BTC", "btc", "mBTC", "mbtc", "uBTC", "ubtc", "SATOSHI","Satoshi", "Satoshis", "satoshis", "SAT", "Sat", "sats", "bit", "bits"];
1739+
let valid = vec!["BTC", "btc", "mBTC", "mbtc", "uBTC", "ubtc", "SATOSHI","Satoshi", "Satoshis", "satoshis", "SAT", "Sat", "sats", "bit", "bits", "nBTC", "pBTC"];
16971740
for denom in valid.iter() {
16981741
assert!(Denomination::from_str(denom).is_ok());
16991742
}
@@ -1702,7 +1745,7 @@ mod tests {
17021745
#[test]
17031746
fn disallow_confusing_forms() {
17041747
// Non-exhaustive list of confusing forms.
1705-
let confusing = vec!["Msat", "Msats", "MSAT", "MSATS", "MSat", "MSats", "MBTC", "Mbtc"];
1748+
let confusing = vec!["Msat", "Msats", "MSAT", "MSATS", "MSat", "MSats", "MBTC", "Mbtc", "PBTC"];
17061749
for denom in confusing.iter() {
17071750
match Denomination::from_str(denom) {
17081751
Ok(_) => panic!("from_str should error for {}", denom),
@@ -1711,5 +1754,18 @@ mod tests {
17111754
}
17121755
}
17131756
}
1757+
1758+
#[test]
1759+
fn disallow_unknown_denomination() {
1760+
// Non-exhaustive list of unknown forms.
1761+
let unknown = vec!["NBTC", "UBTC", "ABC", "abc"];
1762+
for denom in unknown.iter() {
1763+
match Denomination::from_str(denom) {
1764+
Ok(_) => panic!("from_str should error for {}", denom),
1765+
Err(ParseAmountError::UnknownDenomination(_)) => {},
1766+
Err(e) => panic!("unexpected error: {}", e),
1767+
}
1768+
}
1769+
}
17141770
}
17151771

0 commit comments

Comments
 (0)