rust-bitcoin-unsafe-fast/units/src/amount/mod.rs

624 lines
21 KiB
Rust

// SPDX-License-Identifier: CC0-1.0
//! Bitcoin amounts.
//!
//! This module mainly introduces the [`Amount`] and [`SignedAmount`] types.
//! We refer to the documentation on the types for more information.
mod error;
#[cfg(feature = "serde")]
pub mod serde;
mod signed;
#[cfg(test)]
mod tests;
mod unsigned;
#[cfg(kani)]
mod verification;
use core::cmp::Ordering;
use core::convert::Infallible;
use core::fmt;
use core::str::FromStr;
#[cfg(feature = "arbitrary")]
use arbitrary::{Arbitrary, Unstructured};
use self::error::{MissingDigitsKind, ParseAmountErrorInner, ParseErrorInner};
#[rustfmt::skip] // Keep public re-exports separate.
#[doc(inline)]
pub use self::{
error::{
InputTooLargeError, InvalidCharacterError, MissingDenominationError, MissingDigitsError,
OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError,
PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError,
},
signed::SignedAmount,
unsigned::Amount,
};
/// A set of denominations in which amounts can be expressed.
///
/// # Accepted Denominations
///
/// All upper or lower case, excluding SI prefix (c, m, u) which must be lower case.
/// - Singular: BTC, cBTC, mBTC, uBTC
/// - Plural or singular: sat, satoshi, bit
///
/// # Note
///
/// Due to ambiguity between mega and milli we prohibit usage of leading capital 'M'. It is
/// more important to protect users from incorrectly using a capital M to mean milli than to
/// allow Megabitcoin which is not a realistic denomination, and Megasatoshi which is
/// equivalent to cBTC which is allowed.
///
/// # Examples
///
/// ```
/// # use bitcoin_units::Amount;
///
/// assert_eq!("1 BTC".parse::<Amount>().unwrap(), Amount::from_sat(100_000_000));
/// assert_eq!("1 cBTC".parse::<Amount>().unwrap(), Amount::from_sat(1_000_000));
/// assert_eq!("1 mBTC".parse::<Amount>().unwrap(), Amount::from_sat(100_000));
/// assert_eq!("1 uBTC".parse::<Amount>().unwrap(), Amount::from_sat(100));
/// assert_eq!("1 bit".parse::<Amount>().unwrap(), Amount::from_sat(100));
/// assert_eq!("1 sat".parse::<Amount>().unwrap(), Amount::from_sat(1));
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum Denomination {
/// BTC
Bitcoin,
/// cBTC
CentiBitcoin,
/// mBTC
MilliBitcoin,
/// uBTC
MicroBitcoin,
/// bits
Bit,
/// satoshi
Satoshi,
}
impl Denomination {
/// Convenience alias for `Denomination::Bitcoin`.
pub const BTC: Self = Denomination::Bitcoin;
/// Convenience alias for `Denomination::Satoshi`.
pub const SAT: Self = Denomination::Satoshi;
/// The number of decimal places more than a satoshi.
fn precision(self) -> i8 {
match self {
Denomination::Bitcoin => -8,
Denomination::CentiBitcoin => -6,
Denomination::MilliBitcoin => -5,
Denomination::MicroBitcoin => -2,
Denomination::Bit => -2,
Denomination::Satoshi => 0,
}
}
/// Returns a string representation of this denomination.
fn as_str(self) -> &'static str {
match self {
Denomination::Bitcoin => "BTC",
Denomination::CentiBitcoin => "cBTC",
Denomination::MilliBitcoin => "mBTC",
Denomination::MicroBitcoin => "uBTC",
Denomination::Bit => "bits",
Denomination::Satoshi => "satoshi",
}
}
/// The different `str` forms of denominations that are recognized.
fn forms(s: &str) -> Option<Self> {
match s {
"BTC" | "btc" => Some(Denomination::Bitcoin),
"cBTC" | "cbtc" => Some(Denomination::CentiBitcoin),
"mBTC" | "mbtc" => Some(Denomination::MilliBitcoin),
"uBTC" | "ubtc" => Some(Denomination::MicroBitcoin),
"bit" | "bits" | "BIT" | "BITS" => Some(Denomination::Bit),
"SATOSHI" | "satoshi" | "SATOSHIS" | "satoshis" | "SAT" | "sat" | "SATS" | "sats" =>
Some(Denomination::Satoshi),
_ => None,
}
}
}
/// These form are ambigous and could have many meanings. For example, M could denote Mega or Milli.
/// If any of these forms are used, an error type `PossiblyConfusingDenomination` is returned.
const CONFUSING_FORMS: [&str; 6] = ["CBTC", "Cbtc", "MBTC", "Mbtc", "UBTC", "Ubtc"];
impl fmt::Display for Denomination {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(self.as_str()) }
}
impl FromStr for Denomination {
type Err = ParseDenominationError;
/// Converts from a `str` to a `Denomination`.
///
/// # Errors
///
/// - [`ParseDenominationError::PossiblyConfusing`]: If the denomination begins with a capital
/// letter that could be confused with centi, milli, or micro-bitcoin.
/// - [`ParseDenominationError::Unknown`]: If an unknown denomination is used.
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::ParseDenominationError as E;
if CONFUSING_FORMS.contains(&s) {
return Err(E::PossiblyConfusing(PossiblyConfusingDenominationError(s.into())));
};
let form = self::Denomination::forms(s);
form.ok_or_else(|| E::Unknown(UnknownDenominationError(s.into())))
}
}
/// Returns `Some(position)` if the precision is not supported.
///
/// The position indicates the first digit that is too precise.
fn is_too_precise(s: &str, precision: usize) -> Option<usize> {
match s.find('.') {
Some(pos) if precision >= pos => Some(0),
Some(pos) => s[..pos]
.char_indices()
.rev()
.take(precision)
.find(|(_, d)| *d != '0')
.map(|(i, _)| i)
.or_else(|| {
s[(pos + 1)..].char_indices().find(|(_, d)| *d != '0').map(|(i, _)| i + pos + 1)
}),
None if precision >= s.len() => Some(0),
None => s.char_indices().rev().take(precision).find(|(_, d)| *d != '0').map(|(i, _)| i),
}
}
const INPUT_STRING_LEN_LIMIT: usize = 50;
/// Parses a decimal string in the given denomination into a satoshi value and a
/// [`bool`] indicator for a negative amount.
fn parse_signed_to_satoshi(
mut s: &str,
denom: Denomination,
) -> Result<(bool, u64), InnerParseError> {
if s.is_empty() {
return Err(InnerParseError::MissingDigits(MissingDigitsError {
kind: MissingDigitsKind::Empty,
}));
}
if s.len() > INPUT_STRING_LEN_LIMIT {
return Err(InnerParseError::InputTooLarge(s.len()));
}
let is_negative = s.starts_with('-');
if is_negative {
if s.len() == 1 {
return Err(InnerParseError::MissingDigits(MissingDigitsError {
kind: MissingDigitsKind::OnlyMinusSign,
}));
}
s = &s[1..];
}
let max_decimals = {
// The difference in precision between native (satoshi)
// and desired denomination.
let precision_diff = -denom.precision();
if precision_diff <= 0 {
// If precision diff is negative, this means we are parsing
// into a less precise amount. That is not allowed unless
// there are no decimals and the last digits are zeroes as
// many as the difference in precision.
let last_n = precision_diff.unsigned_abs().into();
if let Some(position) = is_too_precise(s, last_n) {
match s.parse::<i64>() {
Ok(0) => return Ok((is_negative, 0)),
_ =>
return Err(InnerParseError::TooPrecise(TooPreciseError {
position: position + usize::from(is_negative),
})),
}
}
s = &s[0..s.find('.').unwrap_or(s.len()) - last_n];
0
} else {
precision_diff
}
};
let mut decimals = None;
let mut value: u64 = 0; // as satoshis
for (i, c) in s.char_indices() {
match c {
'0'..='9' => {
// Do `value = 10 * value + digit`, catching overflows.
match 10_u64.checked_mul(value) {
None => return Err(InnerParseError::Overflow { is_negative }),
Some(val) => match val.checked_add(u64::from(c as u8 - b'0')) {
None => return Err(InnerParseError::Overflow { is_negative }),
Some(val) => value = val,
},
}
// Increment the decimal digit counter if past decimal.
decimals = match decimals {
None => None,
Some(d) if d < max_decimals => Some(d + 1),
_ =>
return Err(InnerParseError::TooPrecise(TooPreciseError {
position: i + usize::from(is_negative),
})),
};
}
'.' => match decimals {
None if max_decimals <= 0 => break,
None => decimals = Some(0),
// Double decimal dot.
_ =>
return Err(InnerParseError::InvalidCharacter(InvalidCharacterError {
invalid_char: '.',
position: i + usize::from(is_negative),
})),
},
c =>
return Err(InnerParseError::InvalidCharacter(InvalidCharacterError {
invalid_char: c,
position: i + usize::from(is_negative),
})),
}
}
// Decimally shift left by `max_decimals - decimals`.
let scale_factor = max_decimals - decimals.unwrap_or(0);
for _ in 0..scale_factor {
value = match 10_u64.checked_mul(value) {
Some(v) => v,
None => return Err(InnerParseError::Overflow { is_negative }),
};
}
Ok((is_negative, value))
}
enum InnerParseError {
Overflow { is_negative: bool },
TooPrecise(TooPreciseError),
MissingDigits(MissingDigitsError),
InputTooLarge(usize),
InvalidCharacter(InvalidCharacterError),
}
impl From<Infallible> for InnerParseError {
fn from(never: Infallible) -> Self { match never {} }
}
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(e) => ParseAmountError(ParseAmountErrorInner::TooPrecise(e)),
Self::MissingDigits(e) => ParseAmountError(ParseAmountErrorInner::MissingDigits(e)),
Self::InputTooLarge(len) =>
ParseAmountError(ParseAmountErrorInner::InputTooLarge(InputTooLargeError { len })),
Self::InvalidCharacter(e) =>
ParseAmountError(ParseAmountErrorInner::InvalidCharacter(e)),
}
}
}
fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseError> {
let (i, j) = if let Some(i) = s.find(' ') {
(i, i + 1)
} else {
let i = s
.find(|c: char| c.is_alphabetic())
.ok_or(ParseError(ParseErrorInner::MissingDenomination(MissingDenominationError)))?;
(i, i)
};
Ok((&s[..i], s[j..].parse()?))
}
/// Options given by `fmt::Formatter`
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
struct FormatOptions {
fill: char,
align: Option<fmt::Alignment>,
width: Option<usize>,
precision: Option<usize>,
sign_plus: bool,
sign_aware_zero_pad: bool,
}
impl FormatOptions {
fn from_formatter(f: &fmt::Formatter) -> Self {
FormatOptions {
fill: f.fill(),
align: f.align(),
width: f.width(),
precision: f.precision(),
sign_plus: f.sign_plus(),
sign_aware_zero_pad: f.sign_aware_zero_pad(),
}
}
}
impl Default for FormatOptions {
fn default() -> Self {
FormatOptions {
fill: ' ',
align: None,
width: None,
precision: None,
sign_plus: false,
sign_aware_zero_pad: false,
}
}
}
fn dec_width(mut num: u64) -> usize {
let mut width = 1;
loop {
num /= 10;
if num == 0 {
break;
}
width += 1;
}
width
}
fn repeat_char(f: &mut dyn fmt::Write, c: char, count: usize) -> fmt::Result {
for _ in 0..count {
f.write_char(c)?;
}
Ok(())
}
/// Format the given satoshi amount in the given denomination.
fn fmt_satoshi_in(
mut satoshi: u64,
negative: bool,
f: &mut dyn fmt::Write,
denom: Denomination,
show_denom: bool,
options: FormatOptions,
) -> fmt::Result {
let precision = denom.precision();
// First we normalize the number:
// {num_before_decimal_point}{:0exp}{"." if nb_decimals > 0}{:0nb_decimals}{num_after_decimal_point}{:0trailing_decimal_zeros}
let mut num_after_decimal_point = 0;
let mut norm_nb_decimals = 0;
let mut num_before_decimal_point = satoshi;
let trailing_decimal_zeros;
let mut exp = 0;
match precision.cmp(&0) {
// We add the number of zeroes to the end
Ordering::Greater => {
if satoshi > 0 {
exp = precision as usize; // Cast ok, checked not negative above.
}
trailing_decimal_zeros = options.precision.unwrap_or(0);
}
Ordering::Less => {
let precision = precision.unsigned_abs();
// round the number if needed
// rather than fiddling with chars, we just modify satoshi and let the simpler algorithm take over.
if let Some(format_precision) = options.precision {
if usize::from(precision) > format_precision {
// precision is u8 so in this branch options.precision() < 255 which fits in u32
let rounding_divisor =
10u64.pow(u32::from(precision) - format_precision as u32); // Cast ok, commented above.
let remainder = satoshi % rounding_divisor;
satoshi -= remainder;
if remainder / (rounding_divisor / 10) >= 5 {
satoshi += rounding_divisor;
}
}
}
let divisor = 10u64.pow(precision.into());
num_before_decimal_point = satoshi / divisor;
num_after_decimal_point = satoshi % divisor;
// normalize by stripping trailing zeros
if num_after_decimal_point == 0 {
norm_nb_decimals = 0;
} else {
norm_nb_decimals = usize::from(precision);
while num_after_decimal_point % 10 == 0 {
norm_nb_decimals -= 1;
num_after_decimal_point /= 10;
}
}
// compute requested precision
let opt_precision = options.precision.unwrap_or(0);
trailing_decimal_zeros = opt_precision.saturating_sub(norm_nb_decimals);
}
Ordering::Equal => trailing_decimal_zeros = options.precision.unwrap_or(0),
}
let total_decimals = norm_nb_decimals + trailing_decimal_zeros;
// Compute expected width of the number
let mut num_width = if total_decimals > 0 {
// 1 for decimal point
1 + total_decimals
} else {
0
};
num_width += dec_width(num_before_decimal_point) + exp;
if options.sign_plus || negative {
num_width += 1;
}
if show_denom {
// + 1 for space
num_width += denom.as_str().len() + 1;
}
let width = options.width.unwrap_or(0);
let align = options.align.unwrap_or(fmt::Alignment::Right);
let (left_pad, pad_right) = match (num_width < width, options.sign_aware_zero_pad, align) {
(false, _, _) => (0, 0),
// Alignment is always right (ignored) when zero-padding
(true, true, _) | (true, false, fmt::Alignment::Right) => (width - num_width, 0),
(true, false, fmt::Alignment::Left) => (0, width - num_width),
// If the required padding is odd it needs to be skewed to the left
(true, false, fmt::Alignment::Center) =>
((width - num_width) / 2, (width - num_width + 1) / 2),
};
if !options.sign_aware_zero_pad {
repeat_char(f, options.fill, left_pad)?;
}
if negative {
write!(f, "-")?;
} else if options.sign_plus {
write!(f, "+")?;
}
if options.sign_aware_zero_pad {
repeat_char(f, '0', left_pad)?;
}
write!(f, "{}", num_before_decimal_point)?;
repeat_char(f, '0', exp)?;
if total_decimals > 0 {
write!(f, ".")?;
}
if norm_nb_decimals > 0 {
write!(f, "{:0width$}", num_after_decimal_point, width = norm_nb_decimals)?;
}
repeat_char(f, '0', trailing_decimal_zeros)?;
if show_denom {
write!(f, " {}", denom.as_str())?;
}
repeat_char(f, options.fill, pad_right)?;
Ok(())
}
/// A helper/builder that displays amount with specified settings.
///
/// This provides richer interface than [`fmt::Formatter`]:
///
/// * Ability to select denomination
/// * Show or hide denomination
/// * Dynamically-selected denomination - show in sats if less than 1 BTC.
///
/// However, this can still be combined with [`fmt::Formatter`] options to precisely control zeros,
/// padding, alignment... The formatting works like floats from `core` but note that precision will
/// **never** be lossy - that means no rounding.
///
/// Note: This implementation is currently **unstable**. The only thing that we can promise is that
/// unless the precision is changed, this will display an accurate, human-readable number, and the
/// default serialization (one with unmodified [`fmt::Formatter`] options) will round-trip with [`FromStr`]
///
/// See [`Amount::display_in`] and [`Amount::display_dynamic`] on how to construct this.
#[derive(Debug, Clone)]
pub struct Display {
/// Absolute value of satoshis to display (sign is below)
sats_abs: u64,
/// The sign
is_negative: bool,
/// How to display the value
style: DisplayStyle,
}
impl Display {
/// Makes subsequent calls to `Display::fmt` display denomination.
#[must_use]
pub fn show_denomination(mut self) -> Self {
match &mut self.style {
DisplayStyle::FixedDenomination { show_denomination, .. } => *show_denomination = true,
// No-op because dynamic denomination is always shown
DisplayStyle::DynamicDenomination => (),
}
self
}
}
impl fmt::Display for Display {
#[rustfmt::skip]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let format_options = FormatOptions::from_formatter(f);
match &self.style {
DisplayStyle::FixedDenomination { show_denomination, denomination } => {
fmt_satoshi_in(self.sats_abs, self.is_negative, f, *denomination, *show_denomination, format_options)
},
DisplayStyle::DynamicDenomination if self.sats_abs >= Amount::ONE_BTC.to_sat() => {
fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Bitcoin, true, format_options)
},
DisplayStyle::DynamicDenomination => {
fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Satoshi, true, format_options)
},
}
}
}
#[derive(Clone, Debug)]
enum DisplayStyle {
FixedDenomination { denomination: Denomination, show_denomination: bool },
DynamicDenomination,
}
/// Calculates the sum over the iterator using checked arithmetic.
pub trait CheckedSum<R>: sealed::Sealed<R> {
/// Calculates the sum over the iterator using checked arithmetic. If an over or underflow would
/// happen it returns [`None`].
fn checked_sum(self) -> Option<R>;
}
impl<T> CheckedSum<Amount> for T
where
T: Iterator<Item = Amount>,
{
fn checked_sum(mut self) -> Option<Amount> {
let first = Some(self.next().unwrap_or_default());
self.fold(first, |acc, item| acc.and_then(|acc| acc.checked_add(item)))
}
}
impl<T> CheckedSum<SignedAmount> for T
where
T: Iterator<Item = SignedAmount>,
{
fn checked_sum(mut self) -> Option<SignedAmount> {
let first = Some(self.next().unwrap_or_default());
self.fold(first, |acc, item| acc.and_then(|acc| acc.checked_add(item)))
}
}
mod sealed {
use super::{Amount, SignedAmount};
/// Used to seal the `CheckedSum` trait
pub trait Sealed<A> {}
impl<T> Sealed<Amount> for T where T: Iterator<Item = Amount> {}
impl<T> Sealed<SignedAmount> for T where T: Iterator<Item = SignedAmount> {}
}
#[cfg(feature = "arbitrary")]
impl<'a> Arbitrary<'a> for Denomination {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let choice = u.int_in_range(0..=5)?;
match choice {
0 => Ok(Denomination::Bitcoin),
1 => Ok(Denomination::CentiBitcoin),
2 => Ok(Denomination::MilliBitcoin),
3 => Ok(Denomination::MicroBitcoin),
4 => Ok(Denomination::Bit),
_ => Ok(Denomination::Satoshi),
}
}
}