1use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use core::num::ParseIntError;
7
8use miden_standards::account::faucets::BasicFungibleFaucet;
9pub use miden_tx::utils::sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
10pub use miden_tx::utils::{
11 ByteReader,
12 ByteWriter,
13 Deserializable,
14 DeserializationError,
15 Serializable,
16 ToHex,
17 bytes_to_hex_string,
18 hex_to_bytes,
19};
20
21use crate::alloc::borrow::ToOwned;
22
23pub fn base_units_to_tokens(units: u64, decimals: u8) -> String {
27 let units_str = units.to_string();
28 let len = units_str.len();
29
30 if decimals == 0 {
31 return units_str;
32 }
33
34 if decimals as usize >= len {
35 "0.".to_owned() + &"0".repeat(decimals as usize - len) + &units_str
37 } else {
38 let integer_part = &units_str[..len - decimals as usize];
40 let fractional_part = &units_str[len - decimals as usize..];
41 format!("{integer_part}.{fractional_part}")
42 }
43}
44
45#[derive(thiserror::Error, Debug)]
48pub enum TokenParseError {
49 #[error("Number of decimals {0} must be less than or equal to {max_decimals}", max_decimals = BasicFungibleFaucet::MAX_DECIMALS)]
50 MaxDecimals(u8),
51 #[error("More than one decimal point")]
52 MultipleDecimalPoints,
53 #[error("Failed to parse u64")]
54 ParseU64(#[source] ParseIntError),
55 #[error("Amount has more than {0} decimal places")]
56 TooManyDecimals(u8),
57}
58
59pub fn tokens_to_base_units(decimal_str: &str, n_decimals: u8) -> Result<u64, TokenParseError> {
62 if n_decimals > BasicFungibleFaucet::MAX_DECIMALS {
63 return Err(TokenParseError::MaxDecimals(n_decimals));
64 }
65
66 let parts: Vec<&str> = decimal_str.split('.').collect();
68
69 if parts.len() > 2 {
70 return Err(TokenParseError::MultipleDecimalPoints);
71 }
72
73 for part in &parts {
75 part.parse::<u64>().map_err(TokenParseError::ParseU64)?;
76 }
77
78 let integer_part = parts[0];
80
81 let mut fractional_part = if parts.len() > 1 {
83 parts[1].trim_end_matches('0').to_string()
84 } else {
85 String::new()
86 };
87
88 if fractional_part.len() > n_decimals.into() {
90 return Err(TokenParseError::TooManyDecimals(n_decimals));
91 }
92
93 while fractional_part.len() < n_decimals.into() {
95 fractional_part.push('0');
96 }
97
98 let combined = format!("{}{}", integer_part, &fractional_part[0..n_decimals.into()]);
100
101 combined.parse::<u64>().map_err(TokenParseError::ParseU64)
103}
104
105#[cfg(test)]
109mod tests {
110 use crate::utils::{TokenParseError, base_units_to_tokens, tokens_to_base_units};
111
112 #[test]
113 fn convert_tokens_to_base_units() {
114 assert_eq!(tokens_to_base_units("18446744.073709551615", 12).unwrap(), u64::MAX);
115 assert_eq!(tokens_to_base_units("7531.2468", 8).unwrap(), 753_124_680_000);
116 assert_eq!(tokens_to_base_units("7531.2468", 4).unwrap(), 75_312_468);
117 assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
118 assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
119 assert_eq!(tokens_to_base_units("0", 3).unwrap(), 0);
120 assert_eq!(tokens_to_base_units("1234", 8).unwrap(), 123_400_000_000);
121 assert_eq!(tokens_to_base_units("1", 0).unwrap(), 1);
122 assert!(matches!(
123 tokens_to_base_units("1.1", 0),
124 Err(TokenParseError::TooManyDecimals(0))
125 ),);
126 assert!(matches!(
127 tokens_to_base_units("18446744.073709551615", 11),
128 Err(TokenParseError::TooManyDecimals(11))
129 ),);
130 assert!(matches!(tokens_to_base_units("123u3.23", 4), Err(TokenParseError::ParseU64(_))),);
131 assert!(matches!(tokens_to_base_units("2.k3", 4), Err(TokenParseError::ParseU64(_))),);
132 assert_eq!(tokens_to_base_units("12.345000", 4).unwrap(), 123_450);
133 assert!(tokens_to_base_units("0.0001.00000001", 12).is_err());
134 }
135
136 #[test]
137 fn convert_base_units_to_tokens() {
138 assert_eq!(base_units_to_tokens(u64::MAX, 12), "18446744.073709551615");
139 assert_eq!(base_units_to_tokens(753_124_680_000, 8), "7531.24680000");
140 assert_eq!(base_units_to_tokens(75_312_468, 4), "7531.2468");
141 assert_eq!(base_units_to_tokens(75_312_468, 0), "75312468");
142 }
143}