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}