Skip to main content

x402_rs/chain/eip155/
mod.rs

1//! EVM chain support for x402 payments via EIP-155.
2//!
3//! This module provides types and providers for interacting with EVM-compatible blockchains
4//! in the x402 protocol. It supports ERC-3009 `transferWithAuthorization` for gasless
5//! token transfers, which is the foundation of x402 payments on EVM chains.
6//!
7//! # Key Types
8//!
9//! - [`Eip155ChainReference`] - A numeric chain ID for EVM networks (e.g., `8453` for Base)
10//! - [`Eip155ChainProvider`] - Provider for interacting with EVM chains
11//! - [`Eip155TokenDeployment`] - Token deployment information including address and decimals
12//! - [`MetaTransaction`] - Parameters for sending meta-transactions
13//!
14//! # Submodules
15//!
16//! - [`types`] - Wire format types like [`ChecksummedAddress`](types::ChecksummedAddress) and [`TokenAmount`](types::TokenAmount)
17//! - [`pending_nonce_manager`] - Nonce management for concurrent transaction submission
18//!
19//! # ERC-3009 Support
20//!
21//! The x402 protocol uses ERC-3009 `transferWithAuthorization` for payments. This allows
22//! users to sign payment authorizations off-chain, which the facilitator then submits
23//! on-chain. The facilitator pays the gas fees and is reimbursed through the payment.
24//!
25//! # Example
26//!
27//! ```ignore
28//! use x402_rs::chain::eip155::{Eip155ChainReference, Eip155TokenDeployment};
29//! use x402_rs::networks::{KnownNetworkEip155, USDC};
30//!
31//! // Get USDC deployment on Base
32//! let usdc = USDC::base();
33//! assert_eq!(usdc.decimals, 6);
34//!
35//! // Parse a human-readable amount
36//! let amount = usdc.parse("10.50").unwrap();
37//! // amount.amount is now 10_500_000 (10.50 * 10^6)
38//! ```
39
40pub mod pending_nonce_manager;
41pub mod types;
42
43use alloy_network::{Ethereum as AlloyEthereum, EthereumWallet, NetworkWallet, TransactionBuilder};
44use alloy_primitives::{Address, B256, Bytes, U256};
45use alloy_provider::fillers::{
46    BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller,
47};
48use alloy_provider::{
49    Identity, PendingTransactionError, Provider, ProviderBuilder, RootProvider, WalletProvider,
50};
51use alloy_rpc_client::RpcClient;
52use alloy_rpc_types_eth::{BlockId, TransactionReceipt, TransactionRequest};
53use alloy_signer::Signer;
54use alloy_signer_local::PrivateKeySigner;
55use alloy_transport::TransportError;
56use alloy_transport::layers::{FallbackLayer, ThrottleLayer};
57use alloy_transport_http::Http;
58use serde::{Deserialize, Serialize};
59use std::fmt::{Display, Formatter};
60use std::num::NonZeroUsize;
61use std::ops::Mul;
62use std::sync::Arc;
63use std::sync::atomic::{AtomicUsize, Ordering};
64use tower::ServiceBuilder;
65use tracing::Instrument;
66
67use crate::chain::{ChainId, ChainProviderOps, DeployedTokenAmount, FromConfig};
68use crate::config::{Eip155ChainConfig, RpcConfig};
69use crate::util::money_amount::{MoneyAmount, MoneyAmountParseError};
70
71pub use pending_nonce_manager::*;
72pub use types::*;
73
74/// Combined filler type for gas, blob gas, nonce, and chain ID.
75pub type InnerFiller = JoinFill<
76    GasFiller,
77    JoinFill<BlobGasFiller, JoinFill<NonceFiller<PendingNonceManager>, ChainIdFiller>>,
78>;
79
80/// The fully composed Ethereum provider type used in this project.
81///
82/// Combines multiple filler layers for gas, nonce, chain ID, blob gas, and wallet signing,
83/// and wraps a [`RootProvider`] for actual JSON-RPC communication.
84pub type InnerProvider = FillProvider<
85    JoinFill<JoinFill<Identity, InnerFiller>, WalletFiller<EthereumWallet>>,
86    RootProvider,
87>;
88
89/// The CAIP-2 namespace for EVM-compatible chains.
90pub const EIP155_NAMESPACE: &str = "eip155";
91
92/// A numeric chain ID for EVM-compatible networks.
93///
94/// This type wraps the numeric chain ID used by EVM networks (e.g., `1` for Ethereum mainnet,
95/// `8453` for Base). It can be converted to/from a [`ChainId`] for use with the x402 protocol.
96///
97/// # Example
98///
99/// ```
100/// use x402_rs::chain::eip155::Eip155ChainReference;
101/// use x402_rs::chain::ChainId;
102///
103/// let base = Eip155ChainReference::new(8453);
104/// let chain_id: ChainId = base.into();
105/// assert_eq!(chain_id.to_string(), "eip155:8453");
106/// ```
107#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
108pub struct Eip155ChainReference(u64);
109
110impl Eip155ChainReference {
111    /// Converts this chain reference to a CAIP-2 [`ChainId`].
112    pub fn as_chain_id(&self) -> ChainId {
113        ChainId::new(EIP155_NAMESPACE, self.0.to_string())
114    }
115}
116
117impl From<Eip155ChainReference> for ChainId {
118    fn from(value: Eip155ChainReference) -> Self {
119        ChainId::new(EIP155_NAMESPACE, value.0.to_string())
120    }
121}
122
123impl From<&Eip155ChainReference> for ChainId {
124    fn from(value: &Eip155ChainReference) -> Self {
125        ChainId::new(EIP155_NAMESPACE, value.0.to_string())
126    }
127}
128
129impl TryFrom<ChainId> for Eip155ChainReference {
130    type Error = Eip155ChainReferenceFormatError;
131
132    fn try_from(value: ChainId) -> Result<Self, Self::Error> {
133        if value.namespace != EIP155_NAMESPACE {
134            return Err(Eip155ChainReferenceFormatError::InvalidNamespace(
135                value.namespace,
136            ));
137        }
138        let chain_id: u64 = value.reference.parse().map_err(|_| {
139            Eip155ChainReferenceFormatError::InvalidReference(value.reference.clone())
140        })?;
141        Ok(Eip155ChainReference(chain_id))
142    }
143}
144
145impl TryFrom<&ChainId> for Eip155ChainReference {
146    type Error = Eip155ChainReferenceFormatError;
147
148    fn try_from(value: &ChainId) -> Result<Self, Self::Error> {
149        if value.namespace != EIP155_NAMESPACE {
150            return Err(Eip155ChainReferenceFormatError::InvalidNamespace(
151                value.namespace.clone(),
152            ));
153        }
154        let chain_id: u64 = value.reference.parse().map_err(|_| {
155            Eip155ChainReferenceFormatError::InvalidReference(value.reference.clone())
156        })?;
157        Ok(Eip155ChainReference(chain_id))
158    }
159}
160
161/// Error returned when converting a [`ChainId`] to an [`Eip155ChainReference`].
162#[derive(Debug, thiserror::Error)]
163pub enum Eip155ChainReferenceFormatError {
164    /// The chain ID namespace is not `eip155`.
165    #[error("Invalid namespace {0}, expected eip155")]
166    InvalidNamespace(String),
167    /// The chain reference is not a valid numeric value.
168    #[error("Invalid eip155 chain reference {0}")]
169    InvalidReference(String),
170}
171
172impl Eip155ChainReference {
173    /// Creates a new chain reference from a numeric chain ID.
174    pub fn new(chain_id: u64) -> Self {
175        Self(chain_id)
176    }
177
178    /// Returns the numeric chain ID.
179    pub fn inner(&self) -> u64 {
180        self.0
181    }
182}
183
184impl Display for Eip155ChainReference {
185    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186        write!(f, "{}", self.0)
187    }
188}
189
190/// Information about a token deployment on an EVM chain.
191///
192/// This type contains all the information needed to interact with a token contract,
193/// including its address, decimal places, and optional EIP-712 domain parameters
194/// for signature verification.
195///
196/// # Example
197///
198/// ```ignore
199/// use x402_rs::networks::{KnownNetworkEip155, USDC};
200///
201/// // Get USDC deployment on Base
202/// let usdc = USDC::base();
203/// assert_eq!(usdc.decimals, 6);
204///
205/// // Parse a human-readable amount to token units
206/// let amount = usdc.parse("10.50").unwrap();
207/// assert_eq!(amount.amount, U256::from(10_500_000u64));
208/// ```
209#[derive(Debug, Clone, Eq, PartialEq, Hash)]
210#[allow(dead_code)] // Public for consumption by downstream crates.
211pub struct Eip155TokenDeployment {
212    /// The chain this token is deployed on.
213    pub chain_reference: Eip155ChainReference,
214    /// The token contract address.
215    pub address: Address,
216    /// Number of decimal places for the token (e.g., 6 for USDC, 18 for most ERC-20s).
217    pub decimals: u8,
218    /// Optional EIP-712 domain parameters for signature verification.
219    pub eip712: Option<TokenDeploymentEip712>,
220}
221
222#[allow(dead_code)] // Public for consumption by downstream crates.
223impl Eip155TokenDeployment {
224    /// Creates a token amount from a raw value.
225    ///
226    /// The value should already be in the token's smallest unit (e.g., wei).
227    pub fn amount<V: Into<TokenAmount>>(
228        &self,
229        v: V,
230    ) -> DeployedTokenAmount<U256, Eip155TokenDeployment> {
231        DeployedTokenAmount {
232            amount: v.into().0,
233            token: self.clone(),
234        }
235    }
236
237    /// Parses a human-readable amount string into token units.
238    ///
239    /// Accepts formats like `"10.50"`, `"$10.50"`, `"1,000"`, etc.
240    /// The amount is scaled by the token's decimal places.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if:
245    /// - The input cannot be parsed as a number
246    /// - The input has more decimal places than the token supports
247    /// - The value is out of range
248    ///
249    /// # Example
250    ///
251    /// ```ignore
252    /// use x402_rs::networks::{KnownNetworkEip155, USDC};
253    ///
254    /// let usdc = USDC::base();
255    /// let amount = usdc.parse("10.50").unwrap();
256    /// // 10.50 USDC = 10,500,000 units (6 decimals)
257    /// assert_eq!(amount.amount, U256::from(10_500_000u64));
258    /// ```
259    pub fn parse<V>(
260        &self,
261        v: V,
262    ) -> Result<DeployedTokenAmount<U256, Eip155TokenDeployment>, MoneyAmountParseError>
263    where
264        V: TryInto<MoneyAmount>,
265        MoneyAmountParseError: From<<V as TryInto<MoneyAmount>>::Error>,
266    {
267        let money_amount = v.try_into()?;
268        let scale = money_amount.scale();
269        let token_scale = self.decimals as u32;
270        if scale > token_scale {
271            return Err(MoneyAmountParseError::WrongPrecision {
272                money: scale,
273                token: token_scale,
274            });
275        }
276        let scale_diff = token_scale - scale;
277        let multiplier = U256::from(10).pow(U256::from(scale_diff));
278        let digits = money_amount.mantissa();
279        let value = U256::from(digits).mul(multiplier);
280        Ok(DeployedTokenAmount {
281            amount: value,
282            token: self.clone(),
283        })
284    }
285}
286
287/// EIP-712 domain parameters for a token deployment.
288///
289/// These parameters are used when verifying EIP-712 typed data signatures
290/// for ERC-3009 `transferWithAuthorization` calls.
291#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
292#[allow(dead_code)] // Public for consumption by downstream crates.
293pub struct TokenDeploymentEip712 {
294    /// The token name as specified in the EIP-712 domain.
295    pub name: String,
296    /// The token version as specified in the EIP-712 domain.
297    pub version: String,
298}
299
300/// Provider for interacting with EVM-compatible blockchains.
301///
302/// This provider handles:
303/// - Transaction signing with multiple signers (round-robin selection)
304/// - Nonce management with automatic reset on failures
305/// - Gas estimation and pricing (EIP-1559 and legacy)
306/// - Transaction receipt fetching with configurable timeouts
307///
308/// # Multiple Signers
309///
310/// The provider supports multiple signers for load distribution. When sending
311/// transactions, signers are selected in round-robin fashion to distribute
312/// the transaction load and avoid nonce conflicts.
313///
314/// # Nonce Management
315///
316/// Uses [`PendingNonceManager`] to track nonces locally and query pending
317/// transactions on initialization. If a transaction fails, the nonce is
318/// automatically reset to force a fresh query on the next transaction.
319#[derive(Debug)]
320pub struct Eip155ChainProvider {
321    chain: Eip155ChainReference,
322    eip1559: bool,
323    flashblocks: bool,
324    receipt_timeout_secs: u64,
325    inner: InnerProvider,
326    /// Available signer addresses for round-robin selection.
327    signer_addresses: Arc<Vec<Address>>,
328    /// Current position in round-robin signer rotation.
329    signer_cursor: Arc<AtomicUsize>,
330    /// Nonce manager for resetting nonces on transaction failures.
331    nonce_manager: PendingNonceManager,
332}
333
334impl Eip155ChainProvider {
335    pub fn rpc_client(chain_id: ChainId, rpc: &[RpcConfig]) -> RpcClient {
336        let transports = rpc
337            .iter()
338            .filter_map(|provider_config| {
339                let scheme = provider_config.http.scheme();
340                let is_http = scheme == "http" || scheme == "https";
341                if !is_http {
342                    return None;
343                }
344                let rpc_url = provider_config.http.clone();
345                tracing::info!(chain=%chain_id, rpc_url=%rpc_url, rate_limit=?provider_config.rate_limit, "Using HTTP transport");
346                let rate_limit = provider_config.rate_limit.unwrap_or(u32::MAX);
347                let service = ServiceBuilder::new()
348                    .layer(ThrottleLayer::new(rate_limit))
349                    .service(Http::new(rpc_url));
350                Some(service)
351            })
352            .collect::<Vec<_>>();
353        let fallback = ServiceBuilder::new()
354            .layer(
355                FallbackLayer::default().with_active_transport_count(
356                    NonZeroUsize::new(transports.len())
357                        .expect("Non-zero amount of stateless transports"),
358                ),
359            )
360            .service(transports);
361        RpcClient::new(fallback, false)
362    }
363
364    /// Round-robin selection of next signer from wallet.
365    fn next_signer_address(&self) -> Address {
366        debug_assert!(!self.signer_addresses.is_empty());
367        if self.signer_addresses.len() == 1 {
368            self.signer_addresses[0]
369        } else {
370            let next =
371                self.signer_cursor.fetch_add(1, Ordering::Relaxed) % self.signer_addresses.len();
372            self.signer_addresses[next]
373        }
374    }
375}
376
377/// Creates a new provider from configuration.
378///
379/// Initializes signers, RPC transports, and the nonce manager.
380///
381/// # Errors
382///
383/// Returns an error if:
384/// - No signers are configured
385/// - Signer private keys are invalid
386/// - RPC transport initialization fails
387#[async_trait::async_trait]
388impl FromConfig<Eip155ChainConfig> for Eip155ChainProvider {
389    async fn from_config(config: &Eip155ChainConfig) -> Result<Self, Box<dyn std::error::Error>> {
390        // 1. Signers
391        let signers = config
392            .signers()
393            .iter()
394            .map(|s| B256::from_slice(s.inner().as_bytes()))
395            .map(|b| {
396                PrivateKeySigner::from_bytes(&b)
397                    .map(|s| s.with_chain_id(Some(config.chain_reference().inner())))
398            })
399            .collect::<Result<Vec<_>, _>>()?;
400        if signers.is_empty() {
401            return Err("at least one signer should be provided".into());
402        }
403        let wallet = {
404            let mut iter = signers.into_iter();
405            let first_signer = iter
406                .next()
407                .expect("iterator contains at least one element by construction");
408            let mut wallet = EthereumWallet::from(first_signer);
409            for signer in iter {
410                wallet.register_signer(signer);
411            }
412            wallet
413        };
414        let signer_addresses =
415            NetworkWallet::<AlloyEthereum>::signer_addresses(&wallet).collect::<Vec<_>>();
416        let signer_addresses = Arc::new(signer_addresses);
417        let signer_cursor = Arc::new(AtomicUsize::new(0));
418
419        // 2. Transports
420        let client = Self::rpc_client(config.chain_id(), config.rpc());
421
422        // 3. Provider
423        // Create nonce manager explicitly so we can store a reference for error handling
424        let nonce_manager = PendingNonceManager::default();
425        // Build the filler stack: Gas -> BlobGas -> Nonce -> ChainId
426        // This mirrors the InnerFiller type but with our custom nonce manager
427        let filler = JoinFill::new(
428            GasFiller,
429            JoinFill::new(
430                BlobGasFiller::default(),
431                JoinFill::new(
432                    NonceFiller::new(nonce_manager.clone()),
433                    ChainIdFiller::default(),
434                ),
435            ),
436        );
437        let inner: InnerProvider = ProviderBuilder::default()
438            .filler(filler)
439            .wallet(wallet)
440            .connect_client(client);
441
442        tracing::info!(chain=%config.chain_id(), signers=?signer_addresses, "Using EVM provider");
443
444        Ok(Self {
445            chain: config.chain_reference(),
446            eip1559: config.eip1559(),
447            flashblocks: config.flashblocks(),
448            receipt_timeout_secs: config.receipt_timeout_secs(),
449            inner,
450            signer_addresses,
451            signer_cursor,
452            nonce_manager,
453        })
454    }
455}
456
457impl Eip155MetaTransactionProvider for Eip155ChainProvider {
458    type Error = MetaTransactionSendError;
459    type Inner = InnerProvider;
460
461    fn inner(&self) -> &Self::Inner {
462        &self.inner
463    }
464
465    fn chain(&self) -> &Eip155ChainReference {
466        &self.chain
467    }
468
469    /// Send a meta-transaction with provided `to`, `calldata`, and automatically selected signer.
470    ///
471    /// This method constructs a transaction from the provided [`MetaTransaction`], automatically
472    /// selects the next available signer using round-robin selection, and handles gas pricing
473    /// based on whether the network supports EIP-1559.
474    ///
475    /// If the transaction fails at any point (during submission or receipt fetching), the nonce
476    /// for the sending address is reset to force a fresh query on the next transaction. This
477    /// ensures correctness even when transactions partially succeed (e.g., submitted but receipt
478    /// fetch times out).
479    ///
480    /// # Gas Pricing Strategy
481    ///
482    /// - **EIP-1559 networks**: Uses automatic gas pricing via the provider's fillers.
483    /// - **Legacy networks**: Fetches the current gas price using `get_gas_price()` and sets it explicitly.
484    ///
485    /// # Timeout Configuration
486    ///
487    /// Receipt fetching is subject to a configurable timeout:
488    /// - Default: 30 seconds
489    /// - Override via `TX_RECEIPT_TIMEOUT_SECS` environment variable
490    /// - If the timeout expires, the nonce is reset and an error is returned
491    ///
492    /// # Parameters
493    ///
494    /// - `tx`: A [`MetaTransaction`] containing the target address and calldata.
495    ///
496    /// # Returns
497    ///
498    /// A [`TransactionReceipt`] once the transaction has been mined and confirmed.
499    ///
500    /// # Errors
501    ///
502    /// Returns [`FacilitatorLocalError::ContractCall`] if:
503    /// - Gas price fetching fails (on legacy networks)
504    /// - Transaction sending fails
505    /// - Receipt retrieval fails or times out
506    async fn send_transaction(
507        &self,
508        tx: MetaTransaction,
509    ) -> Result<TransactionReceipt, Self::Error> {
510        let from_address = self.next_signer_address();
511        let mut txr = TransactionRequest::default()
512            .with_to(tx.to)
513            .with_from(from_address)
514            .with_input(tx.calldata);
515
516        if !self.eip1559 {
517            let provider = &self.inner;
518            let gas: u128 = provider
519                .get_gas_price()
520                .instrument(tracing::info_span!("get_gas_price"))
521                .await?;
522            txr.set_gas_price(gas);
523        }
524
525        // Estimate gas if not provided
526        if txr.gas.is_none() {
527            let block_id = if self.flashblocks {
528                BlockId::latest()
529            } else {
530                BlockId::pending()
531            };
532            let gas_limit = self.inner.estimate_gas(txr.clone()).block(block_id).await?;
533            txr.set_gas_limit(gas_limit)
534        }
535
536        // Send transaction with error handling for nonce reset
537        let pending_tx = match self.inner.send_transaction(txr).await {
538            Ok(pending) => pending,
539            Err(e) => {
540                // Transaction submission failed - reset nonce to force requery
541                self.nonce_manager.reset_nonce(from_address).await;
542                return Err(MetaTransactionSendError::Transport(e));
543            }
544        };
545
546        // Get receipt with timeout and error handling for nonce reset
547        // Default timeout of 30 seconds is reasonable for most EVM chains
548        let timeout = std::time::Duration::from_secs(self.receipt_timeout_secs);
549
550        let watcher = pending_tx
551            .with_required_confirmations(tx.confirmations)
552            .with_timeout(Some(timeout));
553
554        match watcher.get_receipt().await {
555            Ok(receipt) => Ok(receipt),
556            Err(e) => {
557                // Receipt fetch failed (timeout or other error) - reset nonce to force requery
558                self.nonce_manager.reset_nonce(from_address).await;
559                Err(MetaTransactionSendError::PendingTransaction(e))
560            }
561        }
562    }
563}
564
565#[derive(Debug, thiserror::Error)]
566pub enum MetaTransactionSendError {
567    #[error(transparent)]
568    Transport(#[from] TransportError),
569    #[error(transparent)]
570    PendingTransaction(#[from] PendingTransactionError),
571    #[allow(dead_code)] // Public for consumption by downstream crates.
572    #[error("{0}")]
573    Custom(String),
574}
575
576impl ChainProviderOps for Eip155ChainProvider {
577    fn signer_addresses(&self) -> Vec<String> {
578        self.inner
579            .signer_addresses()
580            .map(|a| a.to_string())
581            .collect()
582    }
583
584    fn chain_id(&self) -> ChainId {
585        self.chain.into()
586    }
587}
588
589/// Meta-transaction parameters: target address, calldata, and required confirmations.
590pub struct MetaTransaction {
591    /// Target contract address.
592    pub to: Address,
593    /// Transaction calldata (encoded function call).
594    pub calldata: Bytes,
595    /// Number of block confirmations to wait for.
596    pub confirmations: u64,
597}
598
599/// Trait for sending meta-transactions with custom target and calldata.
600pub trait Eip155MetaTransactionProvider {
601    /// Error type for operations.
602    type Error;
603    /// Underlying provider type.
604    type Inner: Provider;
605
606    /// Returns reference to underlying provider.
607    fn inner(&self) -> &Self::Inner;
608    /// Returns reference to chain descriptor.
609    fn chain(&self) -> &Eip155ChainReference;
610
611    /// Sends a meta-transaction to the network.
612    fn send_transaction(
613        &self,
614        tx: MetaTransaction,
615    ) -> impl Future<Output = Result<TransactionReceipt, Self::Error>> + Send;
616}
617
618impl<T: Eip155MetaTransactionProvider> Eip155MetaTransactionProvider for Arc<T> {
619    type Error = T::Error;
620    type Inner = T::Inner;
621
622    fn inner(&self) -> &Self::Inner {
623        (**self).inner()
624    }
625
626    fn chain(&self) -> &Eip155ChainReference {
627        (**self).chain()
628    }
629
630    fn send_transaction(
631        &self,
632        tx: MetaTransaction,
633    ) -> impl Future<Output = Result<TransactionReceipt, Self::Error>> + Send {
634        (**self).send_transaction(tx)
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use crate::chain::solana::{
642        Address as SolanaAddress, SolanaChainReference, SolanaTokenDeployment,
643    };
644    use crate::networks::KnownNetworkSolana;
645    use std::str::FromStr;
646
647    fn create_test_deployment(decimals: u8) -> Eip155TokenDeployment {
648        let chain_ref = Eip155ChainReference::new(1); // Mainnet
649        Eip155TokenDeployment {
650            chain_reference: chain_ref,
651            address: alloy_primitives::Address::ZERO,
652            decimals,
653            eip712: None,
654        }
655    }
656
657    #[test]
658    fn test_parse_whole_number() {
659        let deployment = create_test_deployment(6); // 6 decimals like USDC
660        let result = deployment.parse("100");
661        assert!(result.is_ok());
662        assert_eq!(result.unwrap().amount, U256::from(100_000_000u64)); // 100 * 10^6
663    }
664
665    #[test]
666    fn test_parse_with_decimals() {
667        let deployment = create_test_deployment(6);
668        let result = deployment.parse("1.50");
669        assert!(result.is_ok());
670        assert_eq!(result.unwrap().amount, U256::from(1_500_000u64)); // 1.50 * 10^6
671    }
672
673    #[test]
674    fn test_parse_zero_decimals() {
675        let deployment = create_test_deployment(0);
676        let result = deployment.parse("42");
677        assert!(result.is_ok());
678        assert_eq!(result.unwrap().amount, U256::from(42u64));
679    }
680
681    #[test]
682    fn test_parse_precision_too_high() {
683        let deployment = create_test_deployment(2); // Only 2 decimals
684        let result = deployment.parse("1.234"); // 3 decimals - should fail
685        assert!(result.is_err());
686        let err = result.unwrap_err();
687        assert!(matches!(err, MoneyAmountParseError::WrongPrecision { .. }));
688    }
689
690    #[test]
691    fn test_parse_exact_precision() {
692        let deployment = create_test_deployment(9); // 9 decimals
693        let result = deployment.parse("0.123456789");
694        assert!(result.is_ok());
695        assert_eq!(result.unwrap().amount, U256::from(123_456_789u64));
696    }
697
698    #[test]
699    fn test_parse_smallest_amount() {
700        let deployment = create_test_deployment(6);
701        let result = deployment.parse("0.000001");
702        assert!(result.is_ok());
703        assert_eq!(result.unwrap().amount, U256::from(1u64));
704    }
705
706    #[test]
707    fn test_parse_with_currency_symbol() {
708        let deployment = create_test_deployment(6);
709        let result = deployment.parse("$10.50");
710        assert!(result.is_ok());
711        assert_eq!(result.unwrap().amount, U256::from(10_500_000u64));
712    }
713
714    #[test]
715    fn test_parse_with_commas() {
716        let deployment = create_test_deployment(6);
717        let result = deployment.parse("1,000");
718        assert!(result.is_ok());
719        assert_eq!(result.unwrap().amount, U256::from(1_000_000_000u64));
720    }
721
722    #[test]
723    fn test_parse_large_amount() {
724        let deployment = create_test_deployment(6);
725        let result = deployment.parse("999999999");
726        assert!(result.is_ok());
727        // 999999999 * 10^6 = 999999999000000
728        assert_eq!(result.unwrap().amount, U256::from(999_999_999_000_000u64));
729    }
730
731    #[test]
732    fn test_parse_very_large_amount_with_high_decimals() {
733        // EIP155 uses U256, so we can handle much larger amounts than Solana
734        let deployment = create_test_deployment(18); // 18 decimals like ETH
735        let result = deployment.parse("999999999"); // 9 digits, 0 decimals
736        assert!(result.is_ok());
737        // 999999999 * 10^18 = 999999999000000000000000000
738        let expected = U256::from(999_999_999u64) * U256::from(10).pow(U256::from(18));
739        assert_eq!(result.unwrap().amount, expected);
740    }
741
742    #[test]
743    fn test_parse_matches_solana_behavior() {
744        // Create equivalent deployments with same decimals
745        let eip155_deployment = create_test_deployment(6);
746
747        let solana_chain = SolanaChainReference::solana();
748        let solana_deployment = SolanaTokenDeployment::new(
749            solana_chain,
750            SolanaAddress::from_str("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZ5nc4pb").unwrap(),
751            6,
752        );
753
754        // Test various amounts
755        let test_cases = ["1", "1.5", "0.01", "100", "999.999"];
756
757        for amount in test_cases {
758            let eip155_result = eip155_deployment.parse(amount);
759            let solana_result = solana_deployment.parse(amount);
760
761            assert_eq!(eip155_result.is_ok(), solana_result.is_ok());
762
763            if let (Ok(eip155), Ok(solana)) = (eip155_result, solana_result) {
764                // EIP155 uses U256, Solana uses u64
765                let eip155_value: u64 = eip155.amount.try_into().unwrap();
766                assert_eq!(eip155_value, solana.amount);
767            }
768        }
769    }
770}