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}