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