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_lib::AuthScheme;
9use miden_lib::account::faucets::BasicFungibleFaucet;
10use miden_lib::account::interface::AccountInterface;
11pub use miden_lib::utils::ScriptBuilderError;
12use miden_objects::Word;
13use miden_objects::account::Account;
14pub use miden_tx::utils::sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
15pub use miden_tx::utils::{
16    ByteReader,
17    ByteWriter,
18    Deserializable,
19    DeserializationError,
20    Serializable,
21    ToHex,
22    bytes_to_hex_string,
23    hex_to_bytes,
24};
25
26use crate::alloc::borrow::ToOwned;
27
28/// Converts an amount in the faucet base units to the token's decimals.
29///
30/// This is meant for display purposes only.
31pub fn base_units_to_tokens(units: u64, decimals: u8) -> String {
32    let units_str = units.to_string();
33    let len = units_str.len();
34
35    if decimals == 0 {
36        return units_str;
37    }
38
39    if decimals as usize >= len {
40        // Handle cases where the number of decimals is greater than the length of units
41        "0.".to_owned() + &"0".repeat(decimals as usize - len) + &units_str
42    } else {
43        // Insert the decimal point at the correct position
44        let integer_part = &units_str[..len - decimals as usize];
45        let fractional_part = &units_str[len - decimals as usize..];
46        format!("{integer_part}.{fractional_part}")
47    }
48}
49
50/// Errors that can occur when parsing a token represented as a decimal number in
51/// a string into base units.
52#[derive(thiserror::Error, Debug)]
53pub enum TokenParseError {
54    #[error("Number of decimals {0} must be less than or equal to {max_decimals}", max_decimals = BasicFungibleFaucet::MAX_DECIMALS)]
55    MaxDecimals(u8),
56    #[error("More than one decimal point")]
57    MultipleDecimalPoints,
58    #[error("Failed to parse u64")]
59    ParseU64(#[source] ParseIntError),
60    #[error("Amount has more than {0} decimal places")]
61    TooManyDecimals(u8),
62}
63
64/// Converts a decimal number, represented as a string, into an integer by shifting
65/// the decimal point to the right by a specified number of decimal places.
66pub fn tokens_to_base_units(decimal_str: &str, n_decimals: u8) -> Result<u64, TokenParseError> {
67    if n_decimals > BasicFungibleFaucet::MAX_DECIMALS {
68        return Err(TokenParseError::MaxDecimals(n_decimals));
69    }
70
71    // Split the string on the decimal point
72    let parts: Vec<&str> = decimal_str.split('.').collect();
73
74    if parts.len() > 2 {
75        return Err(TokenParseError::MultipleDecimalPoints);
76    }
77
78    // Validate that the parts are valid numbers
79    for part in &parts {
80        part.parse::<u64>().map_err(TokenParseError::ParseU64)?;
81    }
82
83    // Get the integer part
84    let integer_part = parts[0];
85
86    // Get the fractional part; remove trailing zeros
87    let mut fractional_part = if parts.len() > 1 {
88        parts[1].trim_end_matches('0').to_string()
89    } else {
90        String::new()
91    };
92
93    // Check if the fractional part has more than N decimals
94    if fractional_part.len() > n_decimals.into() {
95        return Err(TokenParseError::TooManyDecimals(n_decimals));
96    }
97
98    // Add extra zeros if the fractional part is shorter than N decimals
99    while fractional_part.len() < n_decimals.into() {
100        fractional_part.push('0');
101    }
102
103    // Combine the integer and padded fractional part
104    let combined = format!("{}{}", integer_part, &fractional_part[0..n_decimals.into()]);
105
106    // Convert the combined string to an integer
107    combined.parse::<u64>().map_err(TokenParseError::ParseU64)
108}
109
110/// Gets the public key from the storage of an account. The function is required to create an
111/// `AccountFile` for exporting accounts in the cli and the web client.
112///
113/// # Arguments
114/// - `account`: The Accounts from which to extract the public keys.
115pub fn get_public_keys_from_account(account: &Account) -> Vec<Word> {
116    let mut words = vec![];
117    let interface: AccountInterface = account.into();
118
119    for auth in interface.auth() {
120        match auth {
121            AuthScheme::NoAuth | AuthScheme::Unknown => {},
122            AuthScheme::RpoFalcon512Multisig { pub_keys, .. } => {
123                words.extend(pub_keys.iter().map(|k| Word::from(*k)));
124            },
125            AuthScheme::RpoFalcon512 { pub_key } | AuthScheme::EcdsaK256Keccak { pub_key } => {
126                words.push(Word::from(*pub_key));
127            },
128            AuthScheme::EcdsaK256KeccakMultisig { pub_keys, .. } => {
129                words.extend(pub_keys.iter().map(|k| Word::from(*k)));
130            },
131        }
132    }
133
134    words
135}
136
137// TESTS
138// ================================================================================================
139
140#[cfg(test)]
141mod tests {
142    use crate::utils::{TokenParseError, base_units_to_tokens, tokens_to_base_units};
143
144    #[test]
145    fn convert_tokens_to_base_units() {
146        assert_eq!(tokens_to_base_units("18446744.073709551615", 12).unwrap(), u64::MAX);
147        assert_eq!(tokens_to_base_units("7531.2468", 8).unwrap(), 753_124_680_000);
148        assert_eq!(tokens_to_base_units("7531.2468", 4).unwrap(), 75_312_468);
149        assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
150        assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
151        assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
152        assert_eq!(tokens_to_base_units("1234", 8).unwrap(), 123_400_000_000);
153        assert_eq!(tokens_to_base_units("1", 0).unwrap(), 1);
154        assert!(matches!(
155            tokens_to_base_units("1.1", 0),
156            Err(TokenParseError::TooManyDecimals(0))
157        ),);
158        assert!(matches!(
159            tokens_to_base_units("18446744.073709551615", 11),
160            Err(TokenParseError::TooManyDecimals(11))
161        ),);
162        assert!(matches!(tokens_to_base_units("123u3.23", 4), Err(TokenParseError::ParseU64(_))),);
163        assert!(matches!(tokens_to_base_units("2.k3", 4), Err(TokenParseError::ParseU64(_))),);
164        assert_eq!(tokens_to_base_units("12.345000", 4).unwrap(), 123_450);
165        assert!(tokens_to_base_units("0.0001.00000001", 12).is_err());
166    }
167
168    #[test]
169    fn convert_base_units_to_tokens() {
170        assert_eq!(base_units_to_tokens(u64::MAX, 12), "18446744.073709551615");
171        assert_eq!(base_units_to_tokens(753_124_680_000, 8), "7531.24680000");
172        assert_eq!(base_units_to_tokens(75_312_468, 4), "7531.2468");
173        assert_eq!(base_units_to_tokens(75_312_468, 0), "75312468");
174    }
175}