Skip to main content

miden_standards/interop/eth/
amount.rs

1use alloc::vec::Vec;
2
3use miden_protocol::Felt;
4use miden_protocol::asset::FungibleAsset;
5use miden_protocol::utils::bytes_to_packed_u32_elements;
6use primitive_types::U256;
7use thiserror::Error;
8
9// ================================================================================================
10// ETHEREUM AMOUNT ERROR
11// ================================================================================================
12
13/// Error type for Ethereum amount conversions.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
15pub enum EthAmountError {
16    /// The amount doesn't fit in the target type.
17    #[error("amount overflow: value doesn't fit in target type")]
18    Overflow,
19    /// The scaling factor is too large (> 18).
20    #[error("scaling factor too large: maximum is 18")]
21    ScaleTooLarge,
22    /// The scaled-down value doesn't fit in a u64.
23    #[error("scaled value doesn't fit in u64")]
24    ScaledValueDoesNotFitU64,
25    /// The scaled-down value exceeds the maximum fungible token amount.
26    #[error("scaled value exceeds the maximum fungible token amount")]
27    ScaledValueExceedsMaxFungibleAmount,
28}
29
30// ================================================================================================
31// ETHEREUM AMOUNT
32// ================================================================================================
33
34/// Represents an Ethereum uint256 amount as 8 u32 values.
35///
36/// This type provides a more typed representation of Ethereum amounts compared to raw `[u32; 8]`
37/// arrays, while maintaining compatibility with the existing MASM processing pipeline.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct EthAmount([u8; 32]);
40
41impl EthAmount {
42    /// Creates an [`EthAmount`] from a 32-byte array.
43    pub fn new(bytes: [u8; 32]) -> Self {
44        Self(bytes)
45    }
46
47    /// Creates an [`EthAmount`] from a decimal (uint) string.
48    ///
49    /// The string should contain only ASCII decimal digits (e.g. `"2000000000000000000"`).
50    /// The value is stored as a 32-byte big-endian array, matching the Solidity uint256 layout.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`EthAmountError`] if the string is empty, contains non-digit characters,
55    /// or represents a value that overflows uint256.
56    pub fn from_uint_str(s: &str) -> Result<Self, EthAmountError> {
57        let value = U256::from_dec_str(s).map_err(|_| EthAmountError::Overflow)?;
58        Ok(Self(value.to_big_endian()))
59    }
60
61    /// Converts the EthAmount to a U256 for easier arithmetic operations.
62    pub fn to_u256(&self) -> U256 {
63        U256::from_big_endian(&self.0)
64    }
65
66    /// Creates an EthAmount from a U256 value.
67    ///
68    /// This constructor is only available in test code to make test arithmetic easier.
69    #[cfg(any(test, feature = "testing"))]
70    pub fn from_u256(value: U256) -> Self {
71        Self(value.to_big_endian())
72    }
73
74    /// Converts the amount to a vector of field elements for note storage.
75    ///
76    /// Each u32 value in the amount array is converted to a [`Felt`].
77    pub fn to_elements(&self) -> Vec<Felt> {
78        bytes_to_packed_u32_elements(&self.0)
79    }
80
81    /// Returns the raw 32-byte array.
82    pub const fn as_bytes(&self) -> &[u8; 32] {
83        &self.0
84    }
85}
86
87// ================================================================================================
88// U256 SCALING DOWN HELPERS
89// ================================================================================================
90
91/// Maximum scaling factor for decimal conversions
92const MAX_SCALING_FACTOR: u32 = 18;
93
94/// Calculate 10^scale where scale is a u32 exponent.
95///
96/// # Errors
97/// Returns [`EthAmountError::ScaleTooLarge`] if scale > 18.
98fn pow10_u64(scale: u32) -> Result<u64, EthAmountError> {
99    if scale > MAX_SCALING_FACTOR {
100        return Err(EthAmountError::ScaleTooLarge);
101    }
102    Ok(10_u64.pow(scale))
103}
104
105impl EthAmount {
106    /// Converts a U256 amount to a Miden Felt by scaling down by 10^scale_exp.
107    ///
108    /// This is the deterministic reference implementation that computes:
109    /// - `y = floor(x / 10^scale_exp)` (the Miden amount as a Felt)
110    ///
111    /// # Arguments
112    /// * `scale_exp` - The scaling exponent (0-18)
113    ///
114    /// # Returns
115    /// The scaled-down Miden amount as a Felt
116    ///
117    /// # Errors
118    /// - [`EthAmountError::ScaleTooLarge`] if scale_exp > 18
119    /// - [`EthAmountError::ScaledValueDoesNotFitU64`] if the result doesn't fit in a u64
120    /// - [`EthAmountError::ScaledValueExceedsMaxFungibleAmount`] if the scaled value exceeds the
121    ///   maximum fungible token amount
122    ///
123    /// # Example
124    /// ```ignore
125    /// let eth_amount = EthAmount::from_u64(1_000_000_000_000_000_000); // 1 ETH in wei
126    /// let miden_amount = eth_amount.scale_to_asset_amount(12)?;
127    /// // Result: 1_000_000 (1e6, Miden representation)
128    /// ```
129    pub fn scale_to_asset_amount(&self, scale_exp: u32) -> Result<Felt, EthAmountError> {
130        let x = self.to_u256();
131        let scale = U256::from(pow10_u64(scale_exp)?);
132
133        let y_u256 = x / scale;
134
135        // y must fit into u64; canonical Felt is guaranteed by max amount bound
136        let y_u64: u64 = y_u256.try_into().map_err(|_| EthAmountError::ScaledValueDoesNotFitU64)?;
137
138        if y_u64 > FungibleAsset::MAX_AMOUNT.as_u64() {
139            return Err(EthAmountError::ScaledValueExceedsMaxFungibleAmount);
140        }
141
142        // Safe because FungibleAsset::MAX_AMOUNT < Felt modulus
143        let y_felt = Felt::try_from(y_u64).expect("scaled value must fit into canonical Felt");
144        Ok(y_felt)
145    }
146}