Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Bug Fixes

* [fix][rust] `tokens_to_base_units` now returns `TokenParseError::AmountTooLarge` instead of `TokenParseError::ParseU64` when the combined integer+fractional string overflows `u64::MAX` ([#2506](https://github.com/0xMiden/rust-sdk/pull/2506)).

### Breaking Changes

* [BREAKING][removal][rust] Removed `Client::try_get_account`. Use `Client::get_account` and handle the `None` case, or `Client::account_reader` for existence checks and single-field reads that don't need the full materialized account ([#2362](https://github.com/0xMiden/rust-sdk/pull/2362)).
Expand Down
2 changes: 2 additions & 0 deletions bin/miden-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ testing = ["miden-client/testing"]
# Workspace dependencies
miden-client = { features = ["tonic"], workspace = true }
miden-client-sqlite-store = { workspace = true }
miden-protocol = { workspace = true }
miden-standards = { workspace = true }
miden-debug = { optional = true, workspace = true }

# External dependencies
Expand Down
2 changes: 1 addition & 1 deletion bin/miden-cli/src/commands/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use miden_client::asset::{Asset, TokenSymbol};
use miden_client::rpc::domain::account::GetAccountRequest;
use miden_client::rpc::{GrpcClient, NodeRpcClient, VerifyingRpcClient};
use miden_client::transaction::{AccountComponentInterface, AccountInterface};
use miden_client::utils::base_units_to_tokens;
use crate::utils::base_units_to_tokens;
use miden_client::vm::{Package, PackageExport};
use miden_client::{Client, PrettyPrint, Word, ZERO};

Expand Down
73 changes: 72 additions & 1 deletion bin/miden-cli/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use miden_client::account::{AccountId, FaucetMetadata};
use miden_client::address::{Address, AddressId};
use miden_client::asset::{Asset, FungibleAsset};
use miden_client::transaction::{ExecutedTransaction, InputNote};
use miden_client::utils::{base_units_to_tokens, tokens_to_base_units};
use miden_client::vm::MIN_STACK_DEPTH;
use miden_client::{Client, Felt, WORD_SIZE, Word};
use serde::Deserialize;
Expand All @@ -15,6 +14,78 @@ use crate::commands::account::DEFAULT_ACCOUNT_ID_KEY;
use crate::config::{CliConfig, get_global_miden_dir, get_local_miden_dir};
use crate::errors::CliError;

use core::num::{IntErrorKind, ParseIntError};

use miden_standards::account::faucets::FungibleFaucet;

/// Errors that can occur when parsing a token represented as a decimal number in
/// a string into base units.
#[derive(thiserror::Error, Debug)]
pub(crate) enum TokenParseError {
#[error("Number of decimals {0} must be less than or equal to {max_decimals}", max_decimals = FungibleFaucet::MAX_DECIMALS)]
MaxDecimals(u8),
#[error("More than one decimal point")]
MultipleDecimalPoints,
#[error("Failed to parse u64")]
ParseU64(#[source] ParseIntError),
#[error("Amount has more than {0} decimal places")]
TooManyDecimals(u8),
#[error("Amount is too large")]
AmountTooLarge,
#[error("Amount is not a valid asset amount")]
InvalidAmount(#[source] miden_protocol::errors::AssetError),
}


/// Converts an amount in the faucet base units to the token's decimals.
pub(crate) fn base_units_to_tokens(units: miden_client::asset::AssetAmount, decimals: u8) -> String {
let units_str = units.as_u64().to_string();
let len = units_str.len();
if decimals == 0 { return units_str; }
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}")
}
}

/// Converts a decimal number string into base units.
pub(crate) fn tokens_to_base_units(
decimal_str: &str,
n_decimals: u8,
) -> Result<miden_client::asset::AssetAmount, TokenParseError> {
if n_decimals > FungibleFaucet::MAX_DECIMALS {
return Err(TokenParseError::MaxDecimals(n_decimals));
}
let parts: Vec<&str> = decimal_str.split('.').collect();
if parts.len() > 2 {
return Err(TokenParseError::MultipleDecimalPoints);
}
for part in &parts {
part.parse::<u64>().map_err(|e| match e.kind() {
IntErrorKind::PosOverflow => TokenParseError::AmountTooLarge,
_ => TokenParseError::ParseU64(e),
})?;
}
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(TokenParseError::TooManyDecimals(n_decimals));
}
while fractional_part.len() < n_decimals.into() {
fractional_part.push('0');
}
let combined = format!("{}{}", integer_part, &fractional_part[0..n_decimals.into()]);
let units = combined.parse::<u64>().map_err(|_| TokenParseError::AmountTooLarge)?;
miden_client::asset::AssetAmount::new(units).map_err(TokenParseError::InvalidAmount)
}

pub(crate) const SHARED_TOKEN_DOCUMENTATION: &str = "There are two accepted formats for the asset:
- `<AMOUNT>::<FAUCET_ID>` where `<AMOUNT>` is in the faucet base units.
- `<AMOUNT>::<TOKEN_SYMBOL>` where `<AMOUNT>` is a decimal number representing the quantity of
Expand Down
146 changes: 0 additions & 146 deletions crates/rust-client/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
//! Provides various utilities that are commonly used throughout the Miden
//! client library.

use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::num::ParseIntError;

use miden_protocol::asset::AssetAmount;
use miden_protocol::errors::AssetError;
use miden_standards::account::faucets::FungibleFaucet;
pub use miden_tx::utils::serde::{
ByteReader,
ByteWriter,
Expand All @@ -17,143 +11,3 @@ pub use miden_tx::utils::serde::{
};
pub use miden_tx::utils::sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
pub use miden_tx::utils::{ToHex, bytes_to_hex_string, hex_to_bytes};

use crate::alloc::borrow::ToOwned;

/// Converts an amount in the faucet base units to the token's decimals.
///
/// This is meant for display purposes only.
pub fn base_units_to_tokens(units: AssetAmount, decimals: u8) -> String {
let units_str = units.as_u64().to_string();
let len = units_str.len();

if decimals == 0 {
return units_str;
}

if decimals as usize >= len {
// Handle cases where the number of decimals is greater than the length of units
"0.".to_owned() + &"0".repeat(decimals as usize - len) + &units_str
} else {
// Insert the decimal point at the correct position
let integer_part = &units_str[..len - decimals as usize];
let fractional_part = &units_str[len - decimals as usize..];
format!("{integer_part}.{fractional_part}")
}
}

/// Errors that can occur when parsing a token represented as a decimal number in
/// a string into base units.
#[derive(thiserror::Error, Debug)]
pub enum TokenParseError {
#[error("Number of decimals {0} must be less than or equal to {max_decimals}", max_decimals = FungibleFaucet::MAX_DECIMALS)]
MaxDecimals(u8),
#[error("More than one decimal point")]
MultipleDecimalPoints,
#[error("Failed to parse u64")]
ParseU64(#[source] ParseIntError),
#[error("Amount has more than {0} decimal places")]
TooManyDecimals(u8),
#[error("Amount is not a valid asset amount")]
InvalidAmount(#[source] AssetError),
}

/// Converts a decimal number, represented as a string, into an integer by shifting
/// the decimal point to the right by a specified number of decimal places.
pub fn tokens_to_base_units(
decimal_str: &str,
n_decimals: u8,
) -> Result<AssetAmount, TokenParseError> {
if n_decimals > FungibleFaucet::MAX_DECIMALS {
return Err(TokenParseError::MaxDecimals(n_decimals));
}

// Split the string on the decimal point
let parts: Vec<&str> = decimal_str.split('.').collect();

if parts.len() > 2 {
return Err(TokenParseError::MultipleDecimalPoints);
}

// Validate that the parts are valid numbers
for part in &parts {
part.parse::<u64>().map_err(TokenParseError::ParseU64)?;
}

// Get the integer part
let integer_part = parts[0];

// Get the fractional part; remove trailing zeros
let mut fractional_part = if parts.len() > 1 {
parts[1].trim_end_matches('0').to_string()
} else {
String::new()
};

// Check if the fractional part has more than N decimals
if fractional_part.len() > n_decimals.into() {
return Err(TokenParseError::TooManyDecimals(n_decimals));
}

// Add extra zeros if the fractional part is shorter than N decimals
while fractional_part.len() < n_decimals.into() {
fractional_part.push('0');
}

// Combine the integer and padded fractional part
let combined = format!("{}{}", integer_part, &fractional_part[0..n_decimals.into()]);

// Convert the combined string to an integer
let units = combined.parse::<u64>().map_err(TokenParseError::ParseU64)?;

AssetAmount::new(units).map_err(TokenParseError::InvalidAmount)
}

// TESTS
// ================================================================================================

#[cfg(test)]
mod tests {
use miden_protocol::asset::AssetAmount;

use crate::utils::{TokenParseError, base_units_to_tokens, tokens_to_base_units};

fn amount(units: u64) -> AssetAmount {
AssetAmount::new(units).unwrap()
}

#[test]
fn convert_tokens_to_base_units() {
assert_eq!(tokens_to_base_units("9223372.034707292160", 12).unwrap(), AssetAmount::MAX);
assert_eq!(tokens_to_base_units("7531.2468", 8).unwrap(), amount(753_124_680_000));
assert_eq!(tokens_to_base_units("7531.2468", 4).unwrap(), amount(75_312_468));
assert_eq!(tokens_to_base_units("0", 3).unwrap(), AssetAmount::ZERO);
assert_eq!(tokens_to_base_units("1234", 8).unwrap(), amount(123_400_000_000));
assert_eq!(tokens_to_base_units("1", 0).unwrap(), amount(1));
assert!(matches!(
tokens_to_base_units("1.1", 0),
Err(TokenParseError::TooManyDecimals(0))
),);
assert!(matches!(
tokens_to_base_units("18446744.073709551615", 11),
Err(TokenParseError::TooManyDecimals(11))
),);
assert!(matches!(tokens_to_base_units("123u3.23", 4), Err(TokenParseError::ParseU64(_))),);
assert!(matches!(tokens_to_base_units("2.k3", 4), Err(TokenParseError::ParseU64(_))),);
assert_eq!(tokens_to_base_units("12.345000", 4).unwrap(), amount(123_450));
assert!(tokens_to_base_units("0.0001.00000001", 12).is_err());
// Parses as a u64 but exceeds the maximum representable asset amount.
assert!(matches!(
tokens_to_base_units("18446744.073709551615", 12),
Err(TokenParseError::InvalidAmount(_))
),);
}

#[test]
fn convert_base_units_to_tokens() {
assert_eq!(base_units_to_tokens(AssetAmount::MAX, 12), "9223372.034707292160");
assert_eq!(base_units_to_tokens(amount(753_124_680_000), 8), "7531.24680000");
assert_eq!(base_units_to_tokens(amount(75_312_468), 4), "7531.2468");
assert_eq!(base_units_to_tokens(amount(75_312_468), 0), "75312468");
}
}