1use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use core::num::ParseIntError;
7
8use miden_protocol::asset::AssetAmount;
9use miden_protocol::errors::AssetError;
10use miden_standards::account::faucets::FungibleFaucet;
11pub use miden_tx::utils::serde::{
12 ByteReader,
13 ByteWriter,
14 Deserializable,
15 DeserializationError,
16 Serializable,
17};
18pub use miden_tx::utils::sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
19pub use miden_tx::utils::{ToHex, bytes_to_hex_string, hex_to_bytes};
20
21use crate::alloc::borrow::ToOwned;
22
23pub fn base_units_to_tokens(units: AssetAmount, decimals: u8) -> String {
27 let units_str = units.as_u64().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 = FungibleFaucet::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 #[error("Amount is not a valid asset amount")]
58 InvalidAmount(#[source] AssetError),
59}
60
61pub fn tokens_to_base_units(
64 decimal_str: &str,
65 n_decimals: u8,
66) -> Result<AssetAmount, TokenParseError> {
67 if n_decimals > FungibleFaucet::MAX_DECIMALS {
68 return Err(TokenParseError::MaxDecimals(n_decimals));
69 }
70
71 let parts: Vec<&str> = decimal_str.split('.').collect();
73
74 if parts.len() > 2 {
75 return Err(TokenParseError::MultipleDecimalPoints);
76 }
77
78 for part in &parts {
80 part.parse::<u64>().map_err(TokenParseError::ParseU64)?;
81 }
82
83 let integer_part = parts[0];
85
86 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 if fractional_part.len() > n_decimals.into() {
95 return Err(TokenParseError::TooManyDecimals(n_decimals));
96 }
97
98 while fractional_part.len() < n_decimals.into() {
100 fractional_part.push('0');
101 }
102
103 let combined = format!("{}{}", integer_part, &fractional_part[0..n_decimals.into()]);
105
106 let units = combined.parse::<u64>().map_err(TokenParseError::ParseU64)?;
108
109 AssetAmount::new(units).map_err(TokenParseError::InvalidAmount)
110}
111
112#[cfg(test)]
116mod tests {
117 use miden_protocol::asset::AssetAmount;
118
119 use crate::utils::{TokenParseError, base_units_to_tokens, tokens_to_base_units};
120
121 fn amount(units: u64) -> AssetAmount {
122 AssetAmount::new(units).unwrap()
123 }
124
125 #[test]
126 fn convert_tokens_to_base_units() {
127 assert_eq!(tokens_to_base_units("9223372.034707292160", 12).unwrap(), AssetAmount::MAX);
128 assert_eq!(tokens_to_base_units("7531.2468", 8).unwrap(), amount(753_124_680_000));
129 assert_eq!(tokens_to_base_units("7531.2468", 4).unwrap(), amount(75_312_468));
130 assert_eq!(tokens_to_base_units("0", 3).unwrap(), AssetAmount::ZERO);
131 assert_eq!(tokens_to_base_units("1234", 8).unwrap(), amount(123_400_000_000));
132 assert_eq!(tokens_to_base_units("1", 0).unwrap(), amount(1));
133 assert!(matches!(
134 tokens_to_base_units("1.1", 0),
135 Err(TokenParseError::TooManyDecimals(0))
136 ),);
137 assert!(matches!(
138 tokens_to_base_units("18446744.073709551615", 11),
139 Err(TokenParseError::TooManyDecimals(11))
140 ),);
141 assert!(matches!(tokens_to_base_units("123u3.23", 4), Err(TokenParseError::ParseU64(_))),);
142 assert!(matches!(tokens_to_base_units("2.k3", 4), Err(TokenParseError::ParseU64(_))),);
143 assert_eq!(tokens_to_base_units("12.345000", 4).unwrap(), amount(123_450));
144 assert!(tokens_to_base_units("0.0001.00000001", 12).is_err());
145 assert!(matches!(
147 tokens_to_base_units("18446744.073709551615", 12),
148 Err(TokenParseError::InvalidAmount(_))
149 ),);
150 }
151
152 #[test]
153 fn convert_base_units_to_tokens() {
154 assert_eq!(base_units_to_tokens(AssetAmount::MAX, 12), "9223372.034707292160");
155 assert_eq!(base_units_to_tokens(amount(753_124_680_000), 8), "7531.24680000");
156 assert_eq!(base_units_to_tokens(amount(75_312_468), 4), "7531.2468");
157 assert_eq!(base_units_to_tokens(amount(75_312_468), 0), "75312468");
158 }
159}