Skip to main content

miden_client/
utils.rs

1//! Provides various utilities that are commonly used throughout the Miden
2//! client library.
3
4use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use core::num::ParseIntError;
7
8use miden_standards::account::faucets::BasicFungibleFaucet;
9pub use miden_tx::utils::serde::{
10    ByteReader,
11    ByteWriter,
12    Deserializable,
13    DeserializationError,
14    Serializable,
15};
16pub use miden_tx::utils::sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
17pub use miden_tx::utils::{ToHex, bytes_to_hex_string, hex_to_bytes};
18
19use crate::alloc::borrow::ToOwned;
20
21/// Converts an amount in the faucet base units to the token's decimals.
22///
23/// This is meant for display purposes only.
24pub fn base_units_to_tokens(units: u64, decimals: u8) -> String {
25    let units_str = units.to_string();
26    let len = units_str.len();
27
28    if decimals == 0 {
29        return units_str;
30    }
31
32    if decimals as usize >= len {
33        // Handle cases where the number of decimals is greater than the length of units
34        "0.".to_owned() + &"0".repeat(decimals as usize - len) + &units_str
35    } else {
36        // Insert the decimal point at the correct position
37        let integer_part = &units_str[..len - decimals as usize];
38        let fractional_part = &units_str[len - decimals as usize..];
39        format!("{integer_part}.{fractional_part}")
40    }
41}
42
43/// Errors that can occur when parsing a token represented as a decimal number in
44/// a string into base units.
45#[derive(thiserror::Error, Debug)]
46pub enum TokenParseError {
47    #[error("Number of decimals {0} must be less than or equal to {max_decimals}", max_decimals = BasicFungibleFaucet::MAX_DECIMALS)]
48    MaxDecimals(u8),
49    #[error("More than one decimal point")]
50    MultipleDecimalPoints,
51    #[error("Failed to parse u64")]
52    ParseU64(#[source] ParseIntError),
53    #[error("Amount has more than {0} decimal places")]
54    TooManyDecimals(u8),
55}
56
57/// Converts a decimal number, represented as a string, into an integer by shifting
58/// the decimal point to the right by a specified number of decimal places.
59pub fn tokens_to_base_units(decimal_str: &str, n_decimals: u8) -> Result<u64, TokenParseError> {
60    if n_decimals > BasicFungibleFaucet::MAX_DECIMALS {
61        return Err(TokenParseError::MaxDecimals(n_decimals));
62    }
63
64    // Split the string on the decimal point
65    let parts: Vec<&str> = decimal_str.split('.').collect();
66
67    if parts.len() > 2 {
68        return Err(TokenParseError::MultipleDecimalPoints);
69    }
70
71    // Validate that the parts are valid numbers
72    for part in &parts {
73        part.parse::<u64>().map_err(TokenParseError::ParseU64)?;
74    }
75
76    // Get the integer part
77    let integer_part = parts[0];
78
79    // Get the fractional part; remove trailing zeros
80    let mut fractional_part = if parts.len() > 1 {
81        parts[1].trim_end_matches('0').to_string()
82    } else {
83        String::new()
84    };
85
86    // Check if the fractional part has more than N decimals
87    if fractional_part.len() > n_decimals.into() {
88        return Err(TokenParseError::TooManyDecimals(n_decimals));
89    }
90
91    // Add extra zeros if the fractional part is shorter than N decimals
92    while fractional_part.len() < n_decimals.into() {
93        fractional_part.push('0');
94    }
95
96    // Combine the integer and padded fractional part
97    let combined = format!("{}{}", integer_part, &fractional_part[0..n_decimals.into()]);
98
99    // Convert the combined string to an integer
100    combined.parse::<u64>().map_err(TokenParseError::ParseU64)
101}
102
103// TESTS
104// ================================================================================================
105
106#[cfg(test)]
107mod tests {
108    use crate::utils::{TokenParseError, base_units_to_tokens, tokens_to_base_units};
109
110    #[test]
111    fn convert_tokens_to_base_units() {
112        assert_eq!(tokens_to_base_units("18446744.073709551615", 12).unwrap(), u64::MAX);
113        assert_eq!(tokens_to_base_units("7531.2468", 8).unwrap(), 753_124_680_000);
114        assert_eq!(tokens_to_base_units("7531.2468", 4).unwrap(), 75_312_468);
115        assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
116        assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
117        assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
118        assert_eq!(tokens_to_base_units("1234", 8).unwrap(), 123_400_000_000);
119        assert_eq!(tokens_to_base_units("1", 0).unwrap(), 1);
120        assert!(matches!(
121            tokens_to_base_units("1.1", 0),
122            Err(TokenParseError::TooManyDecimals(0))
123        ),);
124        assert!(matches!(
125            tokens_to_base_units("18446744.073709551615", 11),
126            Err(TokenParseError::TooManyDecimals(11))
127        ),);
128        assert!(matches!(tokens_to_base_units("123u3.23", 4), Err(TokenParseError::ParseU64(_))),);
129        assert!(matches!(tokens_to_base_units("2.k3", 4), Err(TokenParseError::ParseU64(_))),);
130        assert_eq!(tokens_to_base_units("12.345000", 4).unwrap(), 123_450);
131        assert!(tokens_to_base_units("0.0001.00000001", 12).is_err());
132    }
133
134    #[test]
135    fn convert_base_units_to_tokens() {
136        assert_eq!(base_units_to_tokens(u64::MAX, 12), "18446744.073709551615");
137        assert_eq!(base_units_to_tokens(753_124_680_000, 8), "7531.24680000");
138        assert_eq!(base_units_to_tokens(75_312_468, 4), "7531.2468");
139        assert_eq!(base_units_to_tokens(75_312_468, 0), "75312468");
140    }
141}