pub mod error;
mod result;
#[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::{
BadPositionError, InputTooLargeError, InvalidCharacterError, MissingDenominationError,
MissingDigitsError, MissingDigitsKind, ParseAmountErrorInner, ParseErrorInner,
PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError,
};
#[rustfmt::skip] #[doc(inline)]
pub use self::{
signed::SignedAmount,
unsigned::Amount,
};
#[cfg(feature = "encoding")]
#[doc(no_inline)]
pub use self::error::AmountDecoderError;
#[doc(no_inline)]
pub use self::error::{OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError};
#[doc(inline)]
#[cfg(feature = "encoding")]
pub use self::unsigned::{AmountDecoder, AmountEncoder};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[non_exhaustive]
#[allow(clippy::doc_markdown)]
pub enum Denomination {
Bitcoin,
CentiBitcoin,
MilliBitcoin,
MicroBitcoin,
Bit,
Satoshi,
#[doc(hidden)]
_DoNotUse(Infallible),
}
impl Denomination {
pub const BTC: Self = Self::Bitcoin;
pub const SAT: Self = Self::Satoshi;
fn precision(self) -> i8 {
match self {
Self::Bitcoin => -8,
Self::CentiBitcoin => -6,
Self::MilliBitcoin => -5,
Self::MicroBitcoin => -2,
Self::Bit => -2,
Self::Satoshi => 0,
Self::_DoNotUse(infallible) => match infallible {},
}
}
fn as_str(self) -> &'static str {
match self {
Self::Bitcoin => "BTC",
Self::CentiBitcoin => "cBTC",
Self::MilliBitcoin => "mBTC",
Self::MicroBitcoin => "uBTC",
Self::Bit => "bits",
Self::Satoshi => "satoshi",
Self::_DoNotUse(infallible) => match infallible {},
}
}
fn forms(s: &str) -> Option<Self> {
match s {
"BTC" | "btc" => Some(Self::Bitcoin),
"cBTC" | "cbtc" => Some(Self::CentiBitcoin),
"mBTC" | "mbtc" => Some(Self::MilliBitcoin),
"uBTC" | "ubtc" | "ยตBTC" | "ยตbtc" => Some(Self::MicroBitcoin),
"bit" | "bits" | "BIT" | "BITS" => Some(Self::Bit),
"SATOSHI" | "satoshi" | "SATOSHIS" | "satoshis" | "SAT" | "sat" | "SATS" | "sats" =>
Some(Self::Satoshi),
_ => None,
}
}
}
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;
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::forms(s);
form.ok_or_else(|| E::Unknown(UnknownDenominationError(s.into())))
}
}
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;
#[allow(clippy::too_many_lines)]
fn parse_signed_to_satoshi(
mut s: &str,
denom: Denomination,
) -> Result<(bool, SignedAmount), 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 = {
let precision_diff = -denom.precision();
if precision_diff <= 0 {
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, SignedAmount::ZERO)),
_ =>
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 underscores = None;
let mut value: i64 = 0; for (i, c) in s.char_indices() {
match c {
'0'..='9' => {
match 10_i64.checked_mul(value) {
None => return Err(InnerParseError::Overflow { is_negative }),
Some(val) => match val.checked_add(i64::from(c as u8 - b'0')) {
None => return Err(InnerParseError::Overflow { is_negative }),
Some(val) => value = val,
},
}
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),
})),
};
underscores = None;
}
'_' if i == 0 =>
return Err(InnerParseError::BadPosition(BadPositionError {
char: '_',
position: i + usize::from(is_negative),
})),
'_' => match underscores {
None => underscores = Some(1),
_ =>
return Err(InnerParseError::BadPosition(BadPositionError {
char: '_',
position: i + usize::from(is_negative),
})),
},
'.' => match decimals {
None if max_decimals <= 0 => break,
None => {
decimals = Some(0);
underscores = None;
}
_ =>
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),
})),
}
}
let scale_factor = max_decimals - decimals.unwrap_or(0);
for _ in 0..scale_factor {
value = match 10_i64.checked_mul(value) {
Some(v) => v,
None => return Err(InnerParseError::Overflow { is_negative }),
};
}
let mut ret =
SignedAmount::from_sat(value).map_err(|_| InnerParseError::Overflow { is_negative })?;
if is_negative {
ret = -ret;
}
Ok((is_negative, ret))
}
#[derive(Debug)]
enum InnerParseError {
Overflow { is_negative: bool },
TooPrecise(TooPreciseError),
MissingDigits(MissingDigitsError),
InputTooLarge(usize),
InvalidCharacter(InvalidCharacterError),
BadPosition(BadPositionError),
}
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)),
Self::BadPosition(e) => ParseAmountError(ParseAmountErrorInner::BadPosition(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()?))
}
#[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 {
Self {
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 {
Self {
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(())
}
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();
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) {
Ordering::Greater => {
if satoshi > 0 {
exp = precision as usize; }
trailing_decimal_zeros = options.precision.unwrap_or(0);
}
Ordering::Less => {
let precision = precision.unsigned_abs();
if let Some(format_precision) = options.precision {
if usize::from(precision) > format_precision {
let rounding_divisor =
10u64.pow(u32::from(precision) - format_precision as u32); 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;
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;
}
}
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;
let mut num_width = if total_decimals > 0 {
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 {
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),
(true, true, _) | (true, false, fmt::Alignment::Right) => (width - num_width, 0),
(true, false, fmt::Alignment::Left) => (0, width - num_width),
(true, false, fmt::Alignment::Center) =>
((width - num_width) / 2, (width - num_width).div_ceil(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(())
}
#[derive(Debug, Clone)]
pub struct Display {
sats_abs: u64,
is_negative: bool,
style: DisplayStyle,
}
impl Display {
#[must_use]
pub fn show_denomination(mut self) -> Self {
match &mut self.style {
DisplayStyle::FixedDenomination { show_denomination, .. } => *show_denomination = true,
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,
}
#[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(Self::Bitcoin),
1 => Ok(Self::CentiBitcoin),
2 => Ok(Self::MilliBitcoin),
3 => Ok(Self::MicroBitcoin),
4 => Ok(Self::Bit),
_ => Ok(Self::Satoshi),
}
}
}