New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add nano and pico BTC to Denomination enum #768
add nano and pico BTC to Denomination enum #768
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 7645b26
Confirmed that the existing logic handles overflow conditions etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P
is confusing - it should mean "peta" - this needs to be handled similarly to milli/Mega
Also, maybe we want to reserve "N"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With @Kixunil's comment and the re-order, LGTM.
src/util/amount.rs
Outdated
Denomination::MicroBitcoin => -2, | ||
Denomination::Bit => -2, | ||
Denomination::Satoshi => 0, | ||
Denomination::NanoBitcoin => 1, | ||
Denomination::MilliSatoshi => 3, | ||
Denomination::PicoBitcoin => 4, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have these two lines added directly underneach MicroBitcoin
? So that all the BTC sub-units are together.
fn precision(self) -> i32 {
match self {
Denomination::Bitcoin => -8,
Denomination::MilliBitcoin => -5,
Denomination::MicroBitcoin => -2,
Denomination::NanoBitcoin => 1,
Denomination::PicoBitcoin => 4,
Denomination::Bit => -2,
Denomination::Satoshi => 0,
Denomination::MilliSatoshi => 3,
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. My initial thought is using precision order but I agree it makes more sense to group by unit
src/util/amount.rs
Outdated
Denomination::Bit => "bits", | ||
Denomination::Satoshi => "satoshi", | ||
Denomination::MilliSatoshi => "msat", | ||
Denomination::PicoBitcoin => "pBTC", | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And same ordering here.
What does 'N' conflict with? ref: https://www.nist.gov/pml/weights-and-measures/metric-si-prefixes |
I can only think of Newton 😃 |
I meant what if SI adds another thing that begins with I'm not certain about this, just saying for consideration. |
7645b26
to
248180d
Compare
I do agree we should stick to the single letter case for unit denominations even if the SI does not defines any meaning for alternative case of some letter. At the same time "btc/bitcoin" part may be of any case |
248180d
to
e80de8b
Compare
Right but then it should be a separate PR to remove the logic accepting any upper/lower case in I believed I have addressed all other change requests🙏🏽. |
@bruteforcecat it already rejects unexpected prefixes and I consider it a requirement that all future code does. Otherwise it's a huge footgun. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least the error message needs to be fixed.
src/util/amount.rs
Outdated
/// - Plural or singular: sat, satoshi, bit, msat | ||
/// | ||
/// Due to ambiguity between mega and milli we prohibit usage of leading capital 'M'. | ||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
use self::ParseAmountError::*; | ||
|
||
if s.starts_with('M') { | ||
if s.starts_with(|ch| ch == 'M' || ch == 'P') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is correct, you just need to modify the error message handling of PossiblyConfusingDenomination
let (upper, lower) = match d.chars().next() {
Some('M') => ("Mega", "milli"),
Some('P') => ("Peta", "pico"),
// This panic could be avoided by adding enum ConfusingDenomination { Mega, Peta } but is it worth it?
_ => panic!("invalid error information"),
};
write!(f, "the '{}' at the beginning of {} should technically mean '{}' but that denomination is uncommon and maybe '{}' was intended", d.chars().next().unwrap(), d, upper, lower)
@@ -100,6 +108,14 @@ fn denomination_from_str(mut s: &str) -> Option<Denomination> { | |||
return Some(Denomination::MicroBitcoin); | |||
} | |||
|
|||
if s.eq_ignore_ascii_case("nBTC") { | |||
return Some(Denomination::NanoBitcoin); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest adding inside this if:
if s.starts_with('N') {
return None;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about doing this in from_str
(https://github.com/rust-bitcoin/rust-bitcoin/pull/768/files#diff-37ff9af2290ccfd3f22d2a1e399973975fb6bd3fdae8203c223009403adb1c6eR95)?
Otherwise I feel like this private function and caller function(from_str
) is a bit interleaving each other in responsibility.
I see. So basically we should only allow |
Oh, yes, |
8b63b74
to
c9b352c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your ongoing work on this! I gave just a little style suggestion. Also perhaps we could have another test for 'U' and 'N'?
#[test]
fn disallow_unknown_denomination() {
// Non-exhaustive list of unknown forms.
let unknown = vec!["NBTC", "UBTC", "ABC", "abc"];
for denom in unknown.iter() {
match Denomination::from_str(denom) {
Ok(_) => panic!("from_str should error for {}", denom),
Err(ParseAmountError::UnknownDenomination(_)) => {},
Err(e) => panic!("unexpected error: {}", e),
}
}
}
src/util/amount.rs
Outdated
match d { | ||
D::MilliBitcoin | D::PicoBitcoin | D::MilliSatoshi => | ||
Err(PossiblyConfusingDenomination(s.to_owned())), | ||
D::NanoBitcoin | D::MicroBitcoin => | ||
Err(UnknownDenomination(s.to_owned())), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its a little odd (to me at least) to see =>
without a {
. I see there are other instances of that in our code base, I'm going to patch them :) Perhaps this is cleaner if written as:
denomination_from_str(s).map_or_else(
|| Err(UnknownDenomination(s.to_owned())),
|d| {
if s.starts_with(|ch : char| ch.is_uppercase() ) {
match d {
D::MilliBitcoin | D::PicoBitcoin | D::MilliSatoshi => {
Err(PossiblyConfusingDenomination(s.to_owned()))
},
D::NanoBitcoin | D::MicroBitcoin => {
Err(UnknownDenomination(s.to_owned()))
},
_ => Ok(d)
}
} else {
Ok(d)
}
}
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, I did the patch to remove all these codebase wide but it feels a bit too much like code churn. Feel free to ignore my suggestion :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the detail suggestion. I added the test case u mentioned above. But for this formatting, I actually ran cargo fmt
and it returns this
match d {
D::MilliBitcoin | D::PicoBitcoin | D::MilliSatoshi => {
Err(PossiblyConfusingDenomination(s.to_owned()))
}
D::NanoBitcoin | D::MicroBitcoin => Err(UnknownDenomination(s.to_owned())),
_ => Ok(d),
}
So it looks like suggesting having not bracket when it can be fit to one line and vice versa.(btw is there any reason we are using cargo fmt?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, years of using rustfmt is why this jumped out at me. We don't use rustfmt in this project because the project predates rustfmt becoming popular/stable. Some folks want to introduce it but its not trivial to do so.
c9b352c
to
f4602c1
Compare
src/util/amount.rs
Outdated
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
use self::ParseAmountError::*; | ||
use self::Denomination as D; | ||
|
||
denomination_from_str(s).map_or_else( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be changed to match
? I find map_or_else
less readable.
Note that the whole thing could be much simpler with match:
let starts_with_uppercase = s.starts_with(|ch : char| ch.is_uppercase());
match denomination_from_str(s) {
Some(D::MilliBitcoin) | Some(D::PicoBitcoin) | Some(D::MilliSatoshi) if starts_with_uppercase => {
Err(PossiblyConfusingDenomination(s.to_owned()))
},
Some(D::NanoBitcoin) | Some(D::MicroBitcoin) if starts_with_uppercase => {
Err(UnknownDenomination(s.to_owned()))
},
Some(denomination) => {
Ok(denomination)
},
None => {
Err(UnknownDenomination(s.to_owned()))
},
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this?
match denomination_from_str(s) {
None => Err(UnknownDenomination(s.to_owned())),
Some(d @ D::MilliBitcoin) | Some(d @ D::PicoBitcoin) | Some(d @ D::MilliSatoshi) => {
if s.starts_with(|ch : char| ch.is_uppercase() ) {
Err(PossiblyConfusingDenomination(s.to_owned()))
} else {
Ok(d)
}
}
Some(d @ D::NanoBitcoin) | Some(d @ D::MicroBitcoin) => {
if s.starts_with(|ch : char| ch.is_uppercase() ) {
Err(UnknownDenomination(s.to_owned()))
} else {
Ok(d)
}
}
Some(d) => Ok(d),
}
I don't like repeated if s.starts_with(
but not sure if there is other solution other than nested match?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flattened matches look better to me. Do you have some specific reason for your suggestion?
Using a variable should be fine to get rid of repeating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh yours look very nice. I didn't know we can pattern match multiple enum variant with if guard(rust newbie).
I updated code using your suggestion except doing the starts_with
check inline in if guard so we can save some unnecessary checking in other other cases(None
, Some(d)
).
Or do u think this minor optimization is not worth the code duplication?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I prefer @Kixunil's version using let starts_with_uppercase = s.starts_with(|ch : char| ch.is_uppercase());
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so we can save some unnecessary checking in other other cases
I was confident that the optimizer would figure it out but checked at godbolt.org and found it wouldn't.
Here's the fix without making the code ugly:
let starts_with_uppercase = || s.starts_with(|ch : char| ch.is_uppercase());
match denomination_from_str(s) {
Some(D::MilliBitcoin) | Some(D::PicoBitcoin) | Some(D::MilliSatoshi) if starts_with_uppercase() => {
Err(PossiblyConfusingDenomination(s.to_owned()))
},
Some(D::NanoBitcoin) | Some(D::MicroBitcoin) if starts_with_uppercase() => {
Err(UnknownDenomination(s.to_owned()))
},
Some(denomination) => {
Ok(denomination)
},
None => {
Err(UnknownDenomination(s.to_owned()))
},
}
It's a bit uncommon style though. :)
Note that anyway calling that function are just two compares and jumps. I suspect that all the time wasted on them for all people running it is less than the amount of time we spent on this but hey, this is fun! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right that it's very trivial optimization. But since this one has the best of both world(it looks like the assembly code generated shorter than my previous clumsy one 😃), we will go for this option then?
464676e
to
0ce9152
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 0ce9152
There's a bit of room for improvement but not significantly important. Could be also improved in a followup PR.
src/util/amount.rs
Outdated
// This panic could be avoided by adding enum ConfusingDenomination { Mega, Peta } but is it worth it? | ||
_ => panic!("invalid error information"), | ||
}; | ||
write!(f, "the '{}' at the beginning of {} should technically mean '{}' but that denomination is uncommon and maybe '{}' was intended", d.chars().next().unwrap(), d, upper, lower) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Damn, sorry for imperfect suggestion, this would've been nicer:
let (letter, upper, lower) = match d.chars().next() {
Some('M') => ('M', "Mega", "milli"),
Some('P') => ('P', "Peta", "pico"),
// This panic could be avoided by adding enum ConfusingDenomination { Mega, Peta } but is it worth it?
_ => panic!("invalid error information"),
};
write!(f, "the '{}' at the beginning of {} should technically mean '{}' but that denomination is uncommon and maybe '{}' was intended", letter, d, upper, lower)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea. I updated it. sorry that u have to ack again.
Err(e) => panic!("unexpected error: {}", e), | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good testing for specific error with clear panic messages!
(API break label because of adding variants to public enum; we need |
…amount denomiation. add disallow_unknown_denomination test
0ce9152
to
40f38b3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 40f38b3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 40f38b3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NACK - seems like we have a bug in the code. Pls check my comment
@@ -43,6 +47,8 @@ impl Denomination { | |||
Denomination::Bitcoin => -8, | |||
Denomination::MilliBitcoin => -5, | |||
Denomination::MicroBitcoin => -2, | |||
Denomination::NanoBitcoin => 1, | |||
Denomination::PicoBitcoin => 4, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really sorry, but I do not get it. What is "picoBitcoin"? It should be 10^-12 and 3 orders of magnitude less than nanoBitcoin - i.e. less than satoshi by two orders of magnitude.
Denomination::PicoBitcoin => 4, | |
Denomination::PicoBitcoin => -2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR is correct I believe, lets see if I can clarify. These numbers go the opposite way to intuition, see the comment
/// The number of decimal places more than a satoshi.
So BTC is 8 decimal places less than a satoshi (hence -8), PicoBitcoin is 4 decimal places more than a satoshi.
Does that help?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was confused too when I reviewed it. My thinking was assuming MilliBitcoin
and MicroBitcoin
were correct, logically the number should be increased by three for each subsequent denomination: -2 + 3 == 1, 1 + 3 == 4.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right since the current precision is using satoshi
as a base unit to do comparison but it's not the canonical smallest unit so we have both negative and positive number(that's the probably the part confuse people?)
I also found the function name precision
not too clear to me. (In decimal, precision means total number of digit).
If we can treat Bitcoin as canonical largest unit
(I doubt we need other unit larger than Bitcoin as total cap <21million BTC ?), maybe we can change this function to power
(power of 10 relative to Bitcoin unit). and then it will be a decreasing negative number like(Here I think it's more natural to order by power)
impl Denomination {
/// The power of 10 relative to Bitcoin
fn power(self) -> i32 {
match self {
Denomination::Bitcoin => 0,
Denomination::MilliBitcoin => -3,
Denomination::MicroBitcoin => -6,
Denomination::Bit => -6,
Denomination::Satoshi => -8,
Denomination::NanoBitcoin => -9,
Denomination::MilliSatoshi => -11,
Denomination::PicoBitcoin => -12,
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably we may do this as a follow-up PR. I will merge the current PR since it already have reviews and solves at least one problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing review to ACK 40f38b3 since it was my misunderstanding and not a bug
|| UnknownDenomination(s.to_owned()), | ||
|_| PossiblyConfusingDenomination(s.to_owned()) | ||
)); | ||
let starts_with_uppercase = || s.starts_with(|ch: char| ch.is_uppercase()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Post-merge nit for a follow-up PR: s.starts_with(char::is_uppercase);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Soon as edition 2018 lands we seriously have to get Clippy running on CI, this stuff is wasting precious reviewer resources :)
…tr::from_str for Deonomation 8fef869 repalce unncessary extra closure with function pointer in starts_with_uppercase closure inside Denomination from_str (KaFai Choi) Pull request description: Follow-up PR from #768 #768 (comment) ACKs for top commit: apoelstra: ACK 8fef869 dr-orlovsky: ACK 8fef869 Tree-SHA512: 3fd7d77805e047a692c53ca7677d7857baa00f529e15696790544a47cebe8a143c1c11f95401436d5322b6da24bc61cc9982a069c883b4815b6c50e8bd31553e
Close 741