Skip to main content

bitcoin_units/amount/
mod.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! Bitcoin amounts.
4//!
5//! This module mainly introduces the [`Amount`] and [`SignedAmount`] types.
6//! We refer to the documentation on the types for more information.
7
8pub mod error;
9mod result;
10#[cfg(feature = "serde")]
11pub mod serde;
12
13mod signed;
14#[cfg(test)]
15mod tests;
16mod unsigned;
17#[cfg(kani)]
18mod verification;
19
20use core::cmp::Ordering;
21use core::convert::Infallible;
22use core::fmt;
23use core::str::FromStr;
24
25#[cfg(feature = "arbitrary")]
26use arbitrary::{Arbitrary, Unstructured};
27
28use self::error::{MissingDigitsKind, ParseAmountErrorInner, ParseErrorInner};
29
30#[rustfmt::skip]                // Keep public re-exports separate.
31#[doc(inline)]
32pub use self::{
33    signed::SignedAmount,
34    unsigned::Amount,
35};
36#[cfg(feature = "encoding")]
37#[doc(no_inline)]
38pub use self::error::AmountDecoderError;
39#[doc(no_inline)]
40pub use self::error::{
41    BadPositionError, InputTooLargeError, InvalidCharacterError, MissingDenominationError,
42    MissingDigitsError, OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError,
43    PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError,
44};
45#[doc(inline)]
46#[cfg(feature = "encoding")]
47pub use self::unsigned::{AmountDecoder, AmountEncoder};
48
49/// A set of denominations in which amounts can be expressed.
50///
51/// # Accepted Denominations
52///
53/// All upper or lower case, excluding SI prefixes c, m and u (or µ) which must be lower case.
54/// - Singular: BTC, cBTC, mBTC, uBTC
55/// - Plural or singular: sat, satoshi, bit
56///
57/// # Note
58///
59/// Due to ambiguity between mega and milli we prohibit usage of leading capital 'M'. It is
60/// more important to protect users from incorrectly using a capital M to mean milli than to
61/// allow Megabitcoin which is not a realistic denomination, and Megasatoshi which is
62/// equivalent to cBTC which is allowed.
63///
64/// # Examples
65///
66/// ```
67/// # use bitcoin_units::{amount, Amount};
68///
69/// let equal = [
70///     ("1 BTC", 100_000_000),
71///     ("1 cBTC", 1_000_000),
72///     ("1 mBTC", 100_000),
73///     ("1 uBTC", 100),
74///     ("1 bit", 100),
75///     ("1 sat", 1),
76/// ];
77/// for (string, sats) in equal {
78///     assert_eq!(
79///         string.parse::<Amount>().expect("valid bitcoin amount string"),
80///         Amount::from_sat(sats)?,
81///     )
82/// }
83/// # Ok::<_, amount::OutOfRangeError>(())
84/// ```
85#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
86#[non_exhaustive]
87#[allow(clippy::doc_markdown)]
88pub enum Denomination {
89    /// BTC (1 BTC = 100,000,000 satoshi).
90    Bitcoin,
91    /// cBTC (1 cBTC = 1,000,000 satoshi).
92    CentiBitcoin,
93    /// mBTC (1 mBTC = 100,000 satoshi).
94    MilliBitcoin,
95    /// µBTC (1 µBTC = 100 satoshi).
96    MicroBitcoin,
97    /// bits (bits = µBTC).
98    Bit,
99    /// satoshi (1 BTC = 100,000,000 satoshi).
100    Satoshi,
101    /// Stops users from casting this enum to an integer.
102    // May get removed if one day Rust supports disabling casts natively.
103    #[doc(hidden)]
104    _DoNotUse(Infallible),
105}
106
107impl Denomination {
108    /// Convenience alias for `Denomination::Bitcoin`.
109    pub const BTC: Self = Self::Bitcoin;
110
111    /// Convenience alias for `Denomination::Satoshi`.
112    pub const SAT: Self = Self::Satoshi;
113
114    /// The number of decimal places more than a satoshi.
115    #[inline]
116    fn precision(self) -> i8 {
117        match self {
118            Self::Bitcoin => -8,
119            Self::CentiBitcoin => -6,
120            Self::MilliBitcoin => -5,
121            Self::MicroBitcoin => -2,
122            Self::Bit => -2,
123            Self::Satoshi => 0,
124            Self::_DoNotUse(infallible) => match infallible {},
125        }
126    }
127
128    /// Returns a string representation of this denomination.
129    #[inline]
130    fn as_str(self) -> &'static str {
131        match self {
132            Self::Bitcoin => "BTC",
133            Self::CentiBitcoin => "cBTC",
134            Self::MilliBitcoin => "mBTC",
135            Self::MicroBitcoin => "uBTC",
136            Self::Bit => "bits",
137            Self::Satoshi => "satoshi",
138            Self::_DoNotUse(infallible) => match infallible {},
139        }
140    }
141
142    /// The different `str` forms of denominations that are recognized.
143    #[inline]
144    fn forms(s: &str) -> Option<Self> {
145        match s {
146            "BTC" | "btc" => Some(Self::Bitcoin),
147            "cBTC" | "cbtc" => Some(Self::CentiBitcoin),
148            "mBTC" | "mbtc" => Some(Self::MilliBitcoin),
149            "uBTC" | "ubtc" | "µBTC" | "µbtc" => Some(Self::MicroBitcoin),
150            "bit" | "bits" | "BIT" | "BITS" => Some(Self::Bit),
151            "SATOSHI" | "satoshi" | "SATOSHIS" | "satoshis" | "SAT" | "sat" | "SATS" | "sats" =>
152                Some(Self::Satoshi),
153            _ => None,
154        }
155    }
156}
157
158/// These forms are ambiguous and could have many meanings.  For example, M could denote Mega or Milli.
159/// If any of these forms are used, an error type `PossiblyConfusingDenomination` is returned.
160const CONFUSING_FORMS: [&str; 6] = ["CBTC", "Cbtc", "MBTC", "Mbtc", "UBTC", "Ubtc"];
161
162impl fmt::Display for Denomination {
163    #[inline]
164    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(self.as_str()) }
165}
166
167impl FromStr for Denomination {
168    type Err = ParseDenominationError;
169
170    /// Converts from a `str` to a `Denomination`.
171    ///
172    /// # Errors
173    ///
174    /// - [`ParseDenominationError::PossiblyConfusing`]: If the denomination begins with a capital
175    ///   letter that could be confused with centi, milli, or micro-bitcoin.
176    /// - [`ParseDenominationError::Unknown`]: If an unknown denomination is used.
177    fn from_str(s: &str) -> Result<Self, Self::Err> {
178        use self::ParseDenominationError as E;
179
180        if CONFUSING_FORMS.contains(&s) {
181            return Err(E::PossiblyConfusing(PossiblyConfusingDenominationError(s.into())));
182        };
183
184        let form = Self::forms(s);
185
186        form.ok_or_else(|| E::Unknown(UnknownDenominationError(s.into())))
187    }
188}
189
190/// Returns `Some(position)` if the precision is not supported.
191///
192/// The position indicates the first digit that is too precise.
193fn is_too_precise(s: &str, precision: usize) -> Option<usize> {
194    match s.find('.') {
195        Some(pos) if precision >= pos => Some(0),
196        Some(pos) => s[..pos]
197            .char_indices()
198            .rev()
199            .take(precision)
200            .find(|(_, d)| *d != '0')
201            .map(|(i, _)| i)
202            .or_else(|| {
203                s[(pos + 1)..].char_indices().find(|(_, d)| *d != '0').map(|(i, _)| i + pos + 1)
204            }),
205        None if precision >= s.len() => Some(0),
206        None => s.char_indices().rev().take(precision).find(|(_, d)| *d != '0').map(|(i, _)| i),
207    }
208}
209
210const INPUT_STRING_LEN_LIMIT: usize = 50;
211
212/// Parses a decimal string in the given denomination into a satoshi value and a
213/// [`bool`] indicator for a negative amount.
214///
215/// The `bool` is only needed to distinguish -0 from 0.
216#[allow(clippy::too_many_lines)]
217fn parse_signed_to_satoshi(
218    mut s: &str,
219    denom: Denomination,
220) -> Result<(bool, SignedAmount), InnerParseError> {
221    if s.is_empty() {
222        return Err(InnerParseError::MissingDigits(MissingDigitsError {
223            kind: MissingDigitsKind::Empty,
224        }));
225    }
226    if s.len() > INPUT_STRING_LEN_LIMIT {
227        return Err(InnerParseError::InputTooLarge(s.len()));
228    }
229
230    let is_negative = s.starts_with('-');
231    if is_negative {
232        if s.len() == 1 {
233            return Err(InnerParseError::MissingDigits(MissingDigitsError {
234                kind: MissingDigitsKind::OnlyMinusSign,
235            }));
236        }
237        s = &s[1..];
238    }
239
240    let max_decimals = {
241        // The difference in precision between native (satoshi)
242        // and desired denomination.
243        let precision_diff = -denom.precision();
244        if precision_diff <= 0 {
245            // If precision diff is negative, this means we are parsing
246            // into a less precise amount. That is not allowed unless
247            // there are no decimals and the last digits are zeroes as
248            // many as the difference in precision.
249            let last_n = precision_diff.unsigned_abs().into();
250            if let Some(position) = is_too_precise(s, last_n) {
251                match s.parse::<i64>() {
252                    Ok(0) => return Ok((is_negative, SignedAmount::ZERO)),
253                    _ =>
254                        return Err(InnerParseError::TooPrecise(TooPreciseError {
255                            position: position + usize::from(is_negative),
256                        })),
257                }
258            }
259            s = &s[0..s.find('.').unwrap_or(s.len()) - last_n];
260            0
261        } else {
262            precision_diff
263        }
264    };
265
266    let mut decimals = None;
267    // The number of consecutive underscores
268    let mut underscores = None;
269    let mut value: i64 = 0; // as satoshis
270    for (i, c) in s.char_indices() {
271        match c {
272            '0'..='9' => {
273                // Do `value = 10 * value + digit`, catching overflows.
274                match 10_i64.checked_mul(value) {
275                    None => return Err(InnerParseError::Overflow { is_negative }),
276                    Some(val) => match val.checked_add(i64::from(c as u8 - b'0')) {
277                        None => return Err(InnerParseError::Overflow { is_negative }),
278                        Some(val) => value = val,
279                    },
280                }
281                // Increment the decimal digit counter if past decimal.
282                decimals = match decimals {
283                    None => None,
284                    Some(d) if d < max_decimals => Some(d + 1),
285                    _ =>
286                        return Err(InnerParseError::TooPrecise(TooPreciseError {
287                            position: i + usize::from(is_negative),
288                        })),
289                };
290                underscores = None;
291            }
292            '_' if i == 0 =>
293            // Leading underscore
294                return Err(InnerParseError::BadPosition(BadPositionError {
295                    char: '_',
296                    position: i + usize::from(is_negative),
297                })),
298            '_' => match underscores {
299                None => underscores = Some(1),
300                // Consecutive underscores
301                _ =>
302                    return Err(InnerParseError::BadPosition(BadPositionError {
303                        char: '_',
304                        position: i + usize::from(is_negative),
305                    })),
306            },
307            '.' => match decimals {
308                None if max_decimals <= 0 => break,
309                None => {
310                    decimals = Some(0);
311                    underscores = None;
312                }
313                // Double decimal dot.
314                _ =>
315                    return Err(InnerParseError::InvalidCharacter(InvalidCharacterError {
316                        invalid_char: '.',
317                        position: i + usize::from(is_negative),
318                    })),
319            },
320            c =>
321                return Err(InnerParseError::InvalidCharacter(InvalidCharacterError {
322                    invalid_char: c,
323                    position: i + usize::from(is_negative),
324                })),
325        }
326    }
327
328    // Decimally shift left by `max_decimals - decimals`.
329    let scale_factor = max_decimals - decimals.unwrap_or(0);
330    for _ in 0..scale_factor {
331        value = match 10_i64.checked_mul(value) {
332            Some(v) => v,
333            None => return Err(InnerParseError::Overflow { is_negative }),
334        };
335    }
336
337    let mut ret =
338        SignedAmount::from_sat(value).map_err(|_| InnerParseError::Overflow { is_negative })?;
339    if is_negative {
340        ret = -ret;
341    }
342    Ok((is_negative, ret))
343}
344
345#[derive(Debug)]
346enum InnerParseError {
347    Overflow { is_negative: bool },
348    TooPrecise(TooPreciseError),
349    MissingDigits(MissingDigitsError),
350    InputTooLarge(usize),
351    InvalidCharacter(InvalidCharacterError),
352    BadPosition(BadPositionError),
353}
354
355impl From<Infallible> for InnerParseError {
356    fn from(never: Infallible) -> Self { match never {} }
357}
358
359impl InnerParseError {
360    #[inline]
361    fn convert(self, is_signed: bool) -> ParseAmountError {
362        match self {
363            Self::Overflow { is_negative } =>
364                ParseAmountError(ParseAmountErrorInner::OutOfRange(OutOfRangeError {
365                    is_signed,
366                    is_greater_than_max: !is_negative,
367                })),
368            Self::TooPrecise(e) => ParseAmountError(ParseAmountErrorInner::TooPrecise(e)),
369            Self::MissingDigits(e) => ParseAmountError(ParseAmountErrorInner::MissingDigits(e)),
370            Self::InputTooLarge(len) =>
371                ParseAmountError(ParseAmountErrorInner::InputTooLarge(InputTooLargeError { len })),
372            Self::InvalidCharacter(e) =>
373                ParseAmountError(ParseAmountErrorInner::InvalidCharacter(e)),
374            Self::BadPosition(e) => ParseAmountError(ParseAmountErrorInner::BadPosition(e)),
375        }
376    }
377}
378
379fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseError> {
380    let (i, j) = if let Some(i) = s.find(' ') {
381        (i, i + 1)
382    } else {
383        let i = s
384            .find(|c: char| c.is_alphabetic())
385            .ok_or(ParseError(ParseErrorInner::MissingDenomination(MissingDenominationError)))?;
386        (i, i)
387    };
388    Ok((&s[..i], s[j..].parse().map_err(|e| ParseError(ParseErrorInner::Denomination(e)))?))
389}
390
391/// Options given by `fmt::Formatter`
392#[derive(Debug, Clone, Copy, Eq, PartialEq)]
393struct FormatOptions {
394    fill: char,
395    align: Option<fmt::Alignment>,
396    width: Option<usize>,
397    precision: Option<usize>,
398    sign_plus: bool,
399    sign_aware_zero_pad: bool,
400}
401
402impl FormatOptions {
403    #[inline]
404    fn from_formatter(f: &fmt::Formatter) -> Self {
405        Self {
406            fill: f.fill(),
407            align: f.align(),
408            width: f.width(),
409            precision: f.precision(),
410            sign_plus: f.sign_plus(),
411            sign_aware_zero_pad: f.sign_aware_zero_pad(),
412        }
413    }
414}
415
416impl Default for FormatOptions {
417    #[inline]
418    fn default() -> Self {
419        Self {
420            fill: ' ',
421            align: None,
422            width: None,
423            precision: None,
424            sign_plus: false,
425            sign_aware_zero_pad: false,
426        }
427    }
428}
429
430fn dec_width(mut num: u64) -> usize {
431    let mut width = 1;
432    loop {
433        num /= 10;
434        if num == 0 {
435            break;
436        }
437        width += 1;
438    }
439    width
440}
441
442fn repeat_char(f: &mut dyn fmt::Write, c: char, count: usize) -> fmt::Result {
443    for _ in 0..count {
444        f.write_char(c)?;
445    }
446    Ok(())
447}
448
449/// Formats the given satoshi amount in the given denomination.
450fn fmt_satoshi_in(
451    mut satoshi: u64,
452    negative: bool,
453    f: &mut dyn fmt::Write,
454    denom: Denomination,
455    show_denom: bool,
456    options: FormatOptions,
457) -> fmt::Result {
458    let precision = denom.precision();
459    // First we normalize the number:
460    // {num_before_decimal_point}{:0exp}{"." if nb_decimals > 0}{:0nb_decimals}{num_after_decimal_point}{:0trailing_decimal_zeros}
461    let mut num_after_decimal_point = 0;
462    let mut norm_nb_decimals = 0;
463    let mut num_before_decimal_point = satoshi;
464    let trailing_decimal_zeros;
465    let mut exp = 0;
466    match precision.cmp(&0) {
467        // We add the number of zeroes to the end
468        Ordering::Greater => {
469            if satoshi > 0 {
470                exp = precision as usize; // Cast ok, checked not negative above.
471            }
472            trailing_decimal_zeros = options.precision.unwrap_or(0);
473        }
474        Ordering::Less => {
475            let precision = precision.unsigned_abs();
476            // round the number if needed
477            // rather than fiddling with chars, we just modify satoshi and let the simpler algorithm take over.
478            if let Some(format_precision) = options.precision {
479                if usize::from(precision) > format_precision {
480                    // precision is u8 so in this branch options.precision() < 255 which fits in u32
481                    let rounding_divisor =
482                        10u64.pow(u32::from(precision) - format_precision as u32); // Cast ok, commented above.
483                    let remainder = satoshi % rounding_divisor;
484                    satoshi -= remainder;
485                    if remainder / (rounding_divisor / 10) >= 5 {
486                        satoshi += rounding_divisor;
487                    }
488                }
489            }
490            let divisor = 10u64.pow(precision.into());
491            num_before_decimal_point = satoshi / divisor;
492            num_after_decimal_point = satoshi % divisor;
493            // normalize by stripping trailing zeros
494            if num_after_decimal_point == 0 {
495                norm_nb_decimals = 0;
496            } else {
497                norm_nb_decimals = usize::from(precision);
498                while num_after_decimal_point % 10 == 0 {
499                    norm_nb_decimals -= 1;
500                    num_after_decimal_point /= 10;
501                }
502            }
503            // compute requested precision
504            let opt_precision = options.precision.unwrap_or(0);
505            trailing_decimal_zeros = opt_precision.saturating_sub(norm_nb_decimals);
506        }
507        Ordering::Equal => trailing_decimal_zeros = options.precision.unwrap_or(0),
508    }
509    let total_decimals = norm_nb_decimals + trailing_decimal_zeros;
510    // Compute expected width of the number
511    let mut num_width = if total_decimals > 0 {
512        // 1 for decimal point
513        1 + total_decimals
514    } else {
515        0
516    };
517    num_width += dec_width(num_before_decimal_point) + exp;
518    if options.sign_plus || negative {
519        num_width += 1;
520    }
521
522    if show_denom {
523        // + 1 for space
524        num_width += denom.as_str().len() + 1;
525    }
526
527    let width = options.width.unwrap_or(0);
528    let align = options.align.unwrap_or(fmt::Alignment::Right);
529    let (left_pad, pad_right) = match (num_width < width, options.sign_aware_zero_pad, align) {
530        (false, _, _) => (0, 0),
531        // Alignment is always right (ignored) when zero-padding
532        (true, true, _) | (true, false, fmt::Alignment::Right) => (width - num_width, 0),
533        (true, false, fmt::Alignment::Left) => (0, width - num_width),
534        // If the required padding is odd it needs to be skewed to the left
535        (true, false, fmt::Alignment::Center) =>
536            ((width - num_width) / 2, (width - num_width).div_ceil(2)),
537    };
538
539    if !options.sign_aware_zero_pad {
540        repeat_char(f, options.fill, left_pad)?;
541    }
542
543    if negative {
544        write!(f, "-")?;
545    } else if options.sign_plus {
546        write!(f, "+")?;
547    }
548
549    if options.sign_aware_zero_pad {
550        repeat_char(f, '0', left_pad)?;
551    }
552
553    write!(f, "{}", num_before_decimal_point)?;
554
555    repeat_char(f, '0', exp)?;
556
557    if total_decimals > 0 {
558        write!(f, ".")?;
559    }
560    if norm_nb_decimals > 0 {
561        write!(f, "{:0width$}", num_after_decimal_point, width = norm_nb_decimals)?;
562    }
563    repeat_char(f, '0', trailing_decimal_zeros)?;
564
565    if show_denom {
566        write!(f, " {}", denom.as_str())?;
567    }
568
569    repeat_char(f, options.fill, pad_right)?;
570    Ok(())
571}
572
573/// A helper/builder that displays amount with specified settings.
574///
575/// This provides richer interface than [`fmt::Formatter`]:
576///
577/// * Ability to select denomination
578/// * Show or hide denomination
579/// * Dynamically-selected denomination - show in sats if less than 1 BTC.
580///
581/// However, this can still be combined with [`fmt::Formatter`] options to precisely control zeros,
582/// padding, alignment... The formatting works like floats from `core` but note that precision will
583/// **never** be lossy - that means no rounding.
584///
585/// Note: This implementation is currently **unstable**. The only thing that we can promise is that
586/// unless the precision is changed, this will display an accurate, human-readable number, and the
587/// default serialization (one with unmodified [`fmt::Formatter`] options) will round-trip with [`FromStr`]
588///
589/// See [`Amount::display_in`] and [`Amount::display_dynamic`] on how to construct this.
590#[derive(Debug, Clone)]
591pub struct Display {
592    /// Absolute value of satoshis to display (sign is below)
593    sats_abs: u64,
594    /// The sign
595    is_negative: bool,
596    /// How to display the value
597    style: DisplayStyle,
598}
599
600impl Display {
601    /// Makes subsequent calls to `Display::fmt` display denomination.
602    #[inline]
603    #[must_use]
604    pub fn show_denomination(mut self) -> Self {
605        match &mut self.style {
606            DisplayStyle::FixedDenomination { show_denomination, .. } => *show_denomination = true,
607            // No-op because dynamic denomination is always shown
608            DisplayStyle::DynamicDenomination => (),
609        }
610        self
611    }
612}
613
614impl fmt::Display for Display {
615    #[inline]
616    #[rustfmt::skip]
617    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
618        let format_options = FormatOptions::from_formatter(f);
619        match &self.style {
620            DisplayStyle::FixedDenomination { show_denomination, denomination } => {
621                fmt_satoshi_in(self.sats_abs, self.is_negative, f, *denomination, *show_denomination, format_options)
622            },
623            DisplayStyle::DynamicDenomination if self.sats_abs >= Amount::ONE_BTC.to_sat() => {
624                fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Bitcoin, true, format_options)
625            },
626            DisplayStyle::DynamicDenomination => {
627                fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Satoshi, true, format_options)
628            },
629        }
630    }
631}
632
633#[derive(Clone, Debug)]
634enum DisplayStyle {
635    FixedDenomination { denomination: Denomination, show_denomination: bool },
636    DynamicDenomination,
637}
638
639#[cfg(feature = "arbitrary")]
640impl<'a> Arbitrary<'a> for Denomination {
641    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
642        let choice = u.int_in_range(0..=5)?;
643        match choice {
644            0 => Ok(Self::Bitcoin),
645            1 => Ok(Self::CentiBitcoin),
646            2 => Ok(Self::MilliBitcoin),
647            3 => Ok(Self::MicroBitcoin),
648            4 => Ok(Self::Bit),
649            _ => Ok(Self::Satoshi),
650        }
651    }
652}