Skip to main content

blockle_core/
amount.rs

1//! BLOCK amounts. The base unit is 1e-8 BLOCK (like a satoshi).
2
3use thiserror::Error;
4
5/// Base units per BLOCK.
6pub const COIN: u64 = 100_000_000;
7
8#[derive(Debug, Error, PartialEq, Eq)]
9pub enum AmountError {
10    #[error("invalid amount: {0}")]
11    Invalid(String),
12}
13
14/// Parse a decimal BLOCK amount ("1", "0.5", "12.34567890") into base units.
15pub fn parse_amount(s: &str) -> Result<u64, AmountError> {
16    let err = || AmountError::Invalid(s.to_string());
17    let s = s.trim();
18    if s.is_empty() || s.starts_with('-') || s.starts_with('+') {
19        return Err(err());
20    }
21    let (whole, frac) = match s.split_once('.') {
22        Some((w, f)) => (w, f),
23        None => (s, ""),
24    };
25    if whole.is_empty() && frac.is_empty() {
26        return Err(err());
27    }
28    if frac.len() > 8 || !whole.chars().all(|c| c.is_ascii_digit()) || !frac.chars().all(|c| c.is_ascii_digit()) {
29        return Err(err());
30    }
31    let whole: u64 = if whole.is_empty() { 0 } else { whole.parse().map_err(|_| err())? };
32    let frac_units: u64 = if frac.is_empty() {
33        0
34    } else {
35        let padded = format!("{frac:0<8}");
36        padded.parse().map_err(|_| err())?
37    };
38    whole
39        .checked_mul(COIN)
40        .and_then(|v| v.checked_add(frac_units))
41        .ok_or_else(err)
42}
43
44/// Format base units as a decimal BLOCK string.
45pub fn format_amount(units: u64) -> String {
46    let whole = units / COIN;
47    let frac = units % COIN;
48    if frac == 0 {
49        format!("{whole}")
50    } else {
51        let s = format!("{frac:08}");
52        format!("{whole}.{}", s.trim_end_matches('0'))
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn parse_and_format() {
62        assert_eq!(parse_amount("1").unwrap(), COIN);
63        assert_eq!(parse_amount("0.5").unwrap(), COIN / 2);
64        assert_eq!(parse_amount("50").unwrap(), 50 * COIN);
65        assert_eq!(parse_amount("0.00000001").unwrap(), 1);
66        assert_eq!(parse_amount("210000").unwrap(), 210_000 * COIN);
67        assert!(parse_amount("-1").is_err());
68        assert!(parse_amount("1.123456789").is_err());
69        assert!(parse_amount("abc").is_err());
70        assert!(parse_amount("").is_err());
71
72        assert_eq!(format_amount(COIN), "1");
73        assert_eq!(format_amount(COIN / 2), "0.5");
74        assert_eq!(format_amount(1), "0.00000001");
75        assert_eq!(parse_amount(&format_amount(123_456_789)).unwrap(), 123_456_789);
76    }
77}