1pub 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] #[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#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
86#[non_exhaustive]
87#[allow(clippy::doc_markdown)]
88pub enum Denomination {
89 Bitcoin,
91 CentiBitcoin,
93 MilliBitcoin,
95 MicroBitcoin,
97 Bit,
99 Satoshi,
101 #[doc(hidden)]
104 _DoNotUse(Infallible),
105}
106
107impl Denomination {
108 pub const BTC: Self = Self::Bitcoin;
110
111 pub const SAT: Self = Self::Satoshi;
113
114 #[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 #[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 #[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
158const 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 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
190fn 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#[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 let precision_diff = -denom.precision();
244 if precision_diff <= 0 {
245 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 let mut underscores = None;
269 let mut value: i64 = 0; for (i, c) in s.char_indices() {
271 match c {
272 '0'..='9' => {
273 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 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 return Err(InnerParseError::BadPosition(BadPositionError {
295 char: '_',
296 position: i + usize::from(is_negative),
297 })),
298 '_' => match underscores {
299 None => underscores = Some(1),
300 _ =>
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 _ =>
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 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#[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
449fn 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 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 Ordering::Greater => {
469 if satoshi > 0 {
470 exp = precision as usize; }
472 trailing_decimal_zeros = options.precision.unwrap_or(0);
473 }
474 Ordering::Less => {
475 let precision = precision.unsigned_abs();
476 if let Some(format_precision) = options.precision {
479 if usize::from(precision) > format_precision {
480 let rounding_divisor =
482 10u64.pow(u32::from(precision) - format_precision as u32); 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 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 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 let mut num_width = if total_decimals > 0 {
512 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 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 (true, true, _) | (true, false, fmt::Alignment::Right) => (width - num_width, 0),
533 (true, false, fmt::Alignment::Left) => (0, width - num_width),
534 (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#[derive(Debug, Clone)]
591pub struct Display {
592 sats_abs: u64,
594 is_negative: bool,
596 style: DisplayStyle,
598}
599
600impl Display {
601 #[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 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}