use std::{
collections::{BTreeMap, BTreeSet},
path::PathBuf,
};
use miden_client::{Client, account::AccountId, asset::FungibleAsset};
use miden_lib::account::faucets::BasicFungibleFaucet;
use serde::{Deserialize, Serialize};
use crate::{errors::CliError, load_config_file, utils::parse_account_id};
#[derive(Debug, Serialize, Deserialize)]
pub struct FaucetDetails {
pub id: String,
pub decimals: u8,
}
pub struct FaucetDetailsMap(BTreeMap<String, FaucetDetails>);
impl FaucetDetailsMap {
pub fn new(token_symbol_map_filepath: PathBuf) -> Result<Self, CliError> {
let token_symbol_map: BTreeMap<String, FaucetDetails> =
match std::fs::read_to_string(token_symbol_map_filepath) {
Ok(content) => match toml::from_str(&content) {
Ok(token_symbol_map) => token_symbol_map,
Err(err) => {
return Err(CliError::Config(
Box::new(err),
"Failed to parse token_symbol_map file".to_string(),
));
},
},
Err(err) => {
if err.kind() != std::io::ErrorKind::NotFound {
return Err(CliError::Config(
Box::new(err),
"Failed to read token_symbol_map file".to_string(),
));
}
BTreeMap::new()
},
};
let mut faucet_ids = BTreeSet::new();
for faucet in token_symbol_map.values() {
if !faucet_ids.insert(faucet.id.clone()) {
return Err(CliError::Config(
format!(
"Faucet ID {} appears more than once in the token symbol map",
faucet.id.clone()
)
.into(),
"Failed to parse token_symbol_map file".to_string(),
));
}
}
Ok(Self(token_symbol_map))
}
pub fn get_token_symbol(&self, faucet_id: &AccountId) -> Option<String> {
self.0
.iter()
.find(|(_, faucet)| faucet.id == faucet_id.to_hex())
.map(|(symbol, _)| symbol.clone())
}
pub fn get_token_symbol_or_default(&self, faucet_id: &AccountId) -> String {
self.get_token_symbol(faucet_id).unwrap_or("Unknown".to_string())
}
pub async fn parse_fungible_asset(
&self,
client: &Client,
arg: &str,
) -> Result<FungibleAsset, CliError> {
let (amount, asset) = arg.split_once("::").ok_or(CliError::Parse(
"separator `::` not found".into(),
"Failed to parse amount and asset".to_string(),
))?;
let (faucet_id, amount) = if let Ok(id) = parse_account_id(client, asset).await {
let amount = amount
.parse::<u64>()
.map_err(|err| CliError::Parse(err.into(), "Failed to parse u64".to_string()))?;
(id, amount)
} else {
let FaucetDetails { id, decimals: faucet_decimals } =
self.0.get(asset).ok_or(CliError::Config(
"Token symbol not found in the map file".to_string().into(),
asset.to_string(),
))?;
let amount = parse_number_as_base_units(amount, *faucet_decimals)?;
(parse_account_id(client, id).await?, amount)
};
FungibleAsset::new(faucet_id, amount).map_err(CliError::Asset)
}
pub fn format_fungible_asset(
&self,
asset: &FungibleAsset,
) -> Result<(String, String), CliError> {
if let Some(token_symbol) = self.get_token_symbol(&asset.faucet_id()) {
let decimals = self
.0
.get(&token_symbol)
.ok_or(CliError::Config(
"Token symbol not found in the map file".to_string().into(),
token_symbol.clone(),
))?
.decimals;
let amount = format_amount_from_faucet_units(asset.amount(), decimals);
Ok((token_symbol, amount))
} else {
let (cli_config, _) = load_config_file()?;
Ok((
asset.faucet_id().to_bech32(cli_config.rpc.endpoint.0.to_network_id()?),
asset.amount().to_string(),
))
}
}
}
fn format_amount_from_faucet_units(units: u64, decimals: u8) -> String {
let units_str = units.to_string();
let len = units_str.len();
if decimals as usize >= len {
"0.".to_owned() + &"0".repeat(decimals as usize - len) + &units_str
} else {
let integer_part = &units_str[..len - decimals as usize];
let fractional_part = &units_str[len - decimals as usize..];
format!("{integer_part}.{fractional_part}")
}
}
fn parse_number_as_base_units(decimal_str: &str, n_decimals: u8) -> Result<u64, CliError> {
if n_decimals > BasicFungibleFaucet::MAX_DECIMALS {
return Err(CliError::Parse(
format!(
"Number of decimals must be less than or equal to {}",
BasicFungibleFaucet::MAX_DECIMALS
)
.into(),
"Faucet maximum decimals".to_string(),
));
}
let parts: Vec<&str> = decimal_str.split('.').collect();
if parts.len() > 2 {
return Err(CliError::Parse(
"More than one decimal point".into(),
"Decimals format".to_string(),
));
}
for part in &parts {
part.parse::<u64>()
.map_err(|err| CliError::Parse(err.into(), "Failed to parse u64".to_string()))?;
}
let integer_part = parts[0];
let mut fractional_part = if parts.len() > 1 {
parts[1].trim_end_matches('0').to_string()
} else {
String::new()
};
if fractional_part.len() > n_decimals.into() {
return Err(CliError::Parse(
format!("Amount has more than {n_decimals} decimal places").into(),
"Failed to parse fractional part".to_string(),
));
}
while fractional_part.len() < n_decimals.into() {
fractional_part.push('0');
}
let combined = format!("{}{}", integer_part, &fractional_part[0..n_decimals.into()]);
combined
.parse::<u64>()
.map_err(|err| CliError::Parse(err.into(), "Failed to parse u64".to_string()))
}
#[test]
fn test_parse_number_as_base_units() {
assert_eq!(parse_number_as_base_units("18446744.073709551615", 12).unwrap(), u64::MAX);
assert_eq!(parse_number_as_base_units("7531.2468", 8).unwrap(), 753_124_680_000);
assert_eq!(parse_number_as_base_units("7531.2468", 4).unwrap(), 75_312_468);
assert_eq!(parse_number_as_base_units("0", 3).unwrap(), 0);
assert_eq!(parse_number_as_base_units("0", 3).unwrap(), 0);
assert_eq!(parse_number_as_base_units("0", 3).unwrap(), 0);
assert_eq!(parse_number_as_base_units("1234", 8).unwrap(), 123_400_000_000);
assert_eq!(parse_number_as_base_units("1", 0).unwrap(), 1);
assert!(matches!(parse_number_as_base_units("1.1", 0), Err(CliError::Parse(_, _))),);
assert!(matches!(
parse_number_as_base_units("18446744.073709551615", 11),
Err(CliError::Parse(_, _))
),);
assert!(matches!(parse_number_as_base_units("123u3.23", 4), Err(CliError::Parse(_, _))),);
assert!(matches!(parse_number_as_base_units("2.k3", 4), Err(CliError::Parse(_, _))),);
assert_eq!(parse_number_as_base_units("12.345000", 4).unwrap(), 123_450);
assert!(parse_number_as_base_units("0.0001.00000001", 12).is_err());
}
#[test]
fn test_format_amount_from_faucet_units() {
assert_eq!(format_amount_from_faucet_units(u64::MAX, 12), "18446744.073709551615");
assert_eq!(format_amount_from_faucet_units(753_124_680_000, 8), "7531.24680000");
assert_eq!(format_amount_from_faucet_units(75_312_468, 4), "7531.2468");
}