Skip to main content

miden_standards/interop/eth/
address.rs

1use alloc::format;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use core::fmt;
5
6use miden_protocol::Felt;
7use miden_protocol::utils::{
8    HexParseError,
9    bytes_to_hex_string,
10    bytes_to_packed_u32_elements,
11    hex_to_bytes,
12};
13use thiserror::Error;
14
15// ================================================================================================
16// ETHEREUM ADDRESS
17// ================================================================================================
18
19/// Represents a plain Ethereum address (20 bytes).
20///
21/// This is the base type for any 20-byte Ethereum address. It is used for:
22/// - Origin token addresses (EVM token contract addresses)
23/// - Destination addresses in the bridge-out flow (real Ethereum addresses)
24/// - Any other context where a plain 20-byte Ethereum address is needed
25///
26/// # Representations used in this module
27///
28/// - Raw bytes: `[u8; 20]` in the conventional Ethereum big-endian byte order (`bytes[0]` is the
29///   most-significant byte).
30/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *big-endian limb order* (each limb encodes its 4
31///   bytes in little-endian order so felts map to keccak bytes directly):
32///   - `address[0]` = bytes[0..4]   (most-significant 4 bytes)
33///   - `address[1]` = bytes[4..8]
34///   - `address[2]` = bytes[8..12]
35///   - `address[3]` = bytes[12..16]
36///   - `address[4]` = bytes[16..20] (least-significant 4 bytes)
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct EthAddress([u8; 20]);
39
40impl EthAddress {
41    // CONSTRUCTORS
42    // --------------------------------------------------------------------------------------------
43
44    /// Creates a new [`EthAddress`] from a 20-byte array.
45    pub const fn new(bytes: [u8; 20]) -> Self {
46        Self(bytes)
47    }
48
49    /// Creates an [`EthAddress`] from a hex string (with or without "0x" prefix).
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the hex string is invalid or the hex part is not exactly 40 characters.
54    pub fn from_hex(hex_str: &str) -> Result<Self, AddressConversionError> {
55        let hex_part = hex_str.strip_prefix("0x").unwrap_or(hex_str);
56        if hex_part.len() != 40 {
57            return Err(AddressConversionError::InvalidHexLength);
58        }
59
60        let prefixed_hex = if hex_str.starts_with("0x") {
61            hex_str.to_string()
62        } else {
63            format!("0x{}", hex_str)
64        };
65
66        let bytes: [u8; 20] = hex_to_bytes(&prefixed_hex)?;
67        Ok(Self(bytes))
68    }
69
70    // PUBLIC ACCESSORS
71    // --------------------------------------------------------------------------------------------
72
73    /// Returns a reference to the underlying 20-byte array.
74    pub const fn as_bytes(&self) -> &[u8; 20] {
75        &self.0
76    }
77
78    /// Converts the address into a 20-byte array.
79    pub const fn into_bytes(self) -> [u8; 20] {
80        self.0
81    }
82
83    /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed).
84    pub fn to_hex(&self) -> String {
85        bytes_to_hex_string(self.0)
86    }
87
88    /// Converts the Ethereum address into an array of 5 [`Felt`] values for Miden VM.
89    ///
90    /// The returned order matches the Solidity ABI encoding convention (*big-endian limb order*):
91    /// - `address[0]` = bytes[0..4]   (most-significant 4 bytes)
92    /// - `address[1]` = bytes[4..8]
93    /// - `address[2]` = bytes[8..12]
94    /// - `address[3]` = bytes[12..16]
95    /// - `address[4]` = bytes[16..20] (least-significant 4 bytes)
96    ///
97    /// Each limb is interpreted as a little-endian `u32` and stored in a [`Felt`].
98    pub fn to_elements(&self) -> Vec<Felt> {
99        bytes_to_packed_u32_elements(&self.0)
100    }
101}
102
103impl fmt::Display for EthAddress {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        write!(f, "{}", self.to_hex())
106    }
107}
108
109impl From<[u8; 20]> for EthAddress {
110    fn from(bytes: [u8; 20]) -> Self {
111        Self(bytes)
112    }
113}
114
115/// Creates an [`EthAddress`] from a 32-byte (bytes32) value embedding a left-padded address.
116///
117/// In EVM ABI encoding, a bytes32-embedded address has bytes 0..12 zero and the 20-byte
118/// address in bytes 12..32. This is the encoding used by bridges that carry an address in a
119/// full bytes32 field.
120///
121/// # Errors
122///
123/// Returns an error if any of the leading 12 padding bytes are non-zero.
124impl TryFrom<[u8; 32]> for EthAddress {
125    type Error = AddressConversionError;
126
127    fn try_from(bytes: [u8; 32]) -> Result<Self, Self::Error> {
128        if bytes[0..12] != [0; 12] {
129            return Err(AddressConversionError::NonZeroBytes32Padding);
130        }
131
132        let addr: [u8; 20] = bytes[12..32].try_into().expect("slice is exactly 20 bytes");
133        Ok(Self(addr))
134    }
135}
136
137impl From<EthAddress> for [u8; 20] {
138    fn from(addr: EthAddress) -> Self {
139        addr.0
140    }
141}
142
143// ================================================================================================
144// ADDRESS CONVERSION ERROR
145// ================================================================================================
146
147/// Error type for Ethereum address conversions.
148#[derive(Debug, Clone, PartialEq, Eq, Error)]
149pub enum AddressConversionError {
150    /// The address word has non-zero padding.
151    #[error("non-zero word padding")]
152    NonZeroWordPadding,
153    /// The address has a non-zero 4-byte prefix.
154    #[error("address has non-zero 4-byte prefix")]
155    NonZeroBytePrefix,
156    /// A bytes32-embedded address has non-zero leading padding bytes.
157    #[error("leading 12 bytes must be zero for a bytes32-embedded address")]
158    NonZeroBytes32Padding,
159    /// The hex string has an unexpected length.
160    #[error("invalid hex length (expected 40 hex chars)")]
161    InvalidHexLength,
162    /// The hex string contains an invalid character.
163    #[error("invalid hex character: {0}")]
164    InvalidHexChar(char),
165    /// The hex string could not be parsed.
166    #[error("hex parse error")]
167    HexParseError,
168    /// A packed 8-byte value does not fit in the field.
169    #[error("packed 8-byte value does not fit in the field")]
170    FeltOutOfField,
171    /// The decoded value is not a valid AccountId.
172    #[error("invalid AccountId")]
173    InvalidAccountId,
174}
175
176impl From<HexParseError> for AddressConversionError {
177    fn from(_err: HexParseError) -> Self {
178        AddressConversionError::HexParseError
179    }
180}