Skip to main content

x402_rs/scheme/v1_eip155_exact/
mod.rs

1//! V1 EIP-155 "exact" payment scheme implementation.
2//!
3//! This module implements the "exact" payment scheme for EVM chains using
4//! the V1 x402 protocol. It uses ERC-3009 `transferWithAuthorization` for
5//! gasless token transfers.
6//!
7//! # Features
8//!
9//! - EIP-712 typed data signing for payment authorization
10//! - EIP-6492 support for counterfactual smart wallet signatures
11//! - EIP-1271 support for deployed smart wallet signatures
12//! - EOA signature support with split (v, r, s) components
13//! - On-chain balance verification before settlement
14//!
15//! # Signature Handling
16//!
17//! The facilitator intelligently dispatches to different `transferWithAuthorization`
18//! contract functions based on the signature format provided:
19//!
20//! - **EOA signatures (64-65 bytes)**: Parsed as (r, s, v) components and dispatched to
21//!   `transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)`
22//!   (the standard EIP-3009 function signature).
23//!
24//! - **EIP-1271 signatures (any other length)**: Passed as full signature bytes to
25//!   `transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,bytes)`
26//!   (a non-standard variant that accepts arbitrary signature bytes for contract wallets).
27//!
28//! - **EIP-6492 signatures**: Detected by the 32-byte magic suffix and validated via
29//!   the universal EIP-6492 validator contract before settlement.
30//!
31//! # Usage
32//!
33//! ```ignore
34//! use x402::scheme::v1_eip155_exact::V1Eip155Exact;
35//! use x402::networks::{KnownNetworkEip155, USDC};
36//!
37//! // Create a price tag for 1 USDC on Base
38//! let usdc = USDC::base();
39//! let price = V1Eip155Exact::price_tag(
40//!     "0x1234...",  // pay_to address
41//!     usdc.amount(1_000_000u64.into()),  // 1 USDC
42//! );
43//! ```
44
45use alloy_contract::SolCallBuilder;
46use alloy_primitives::{Address, B256, Bytes, Signature, TxHash, U256, address, hex};
47use alloy_provider::bindings::IMulticall3;
48use alloy_provider::{
49    MULTICALL3_ADDRESS, MulticallError, MulticallItem, PendingTransactionError, Provider,
50};
51use alloy_sol_types::{Eip712Domain, SolCall, SolStruct, SolType, eip712_domain, sol};
52use alloy_transport::TransportError;
53use serde::{Deserialize, Serialize};
54use std::collections::HashMap;
55use std::sync::Arc;
56use tracing::Instrument;
57use tracing::instrument;
58use tracing_core::Level;
59
60pub mod client;
61pub mod types;
62
63use crate::chain::ChainProvider;
64use crate::chain::eip155::{
65    ChecksummedAddress, Eip155ChainReference, Eip155MetaTransactionProvider, Eip155TokenDeployment,
66    MetaTransaction, MetaTransactionSendError,
67};
68use crate::chain::{ChainId, ChainProviderOps, DeployedTokenAmount};
69use crate::proto;
70use crate::proto::{PaymentVerificationError, v1};
71use crate::scheme::{
72    X402SchemeFacilitator, X402SchemeFacilitatorBuilder, X402SchemeFacilitatorError, X402SchemeId,
73};
74use crate::timestamp::UnixTimestamp;
75
76pub use types::*;
77
78/// Signature verifier for EIP-6492, EIP-1271, EOA, universally deployed on the supported EVM chains
79/// If absent on a target chain, verification will fail; you should deploy the validator there.
80pub const VALIDATOR_ADDRESS: Address = address!("0xdAcD51A54883eb67D95FAEb2BBfdC4a9a6BD2a3B");
81
82pub struct V1Eip155Exact;
83
84impl V1Eip155Exact {
85    #[allow(dead_code)] // Public for consumption by downstream crates.
86    pub fn price_tag<A: Into<ChecksummedAddress>>(
87        pay_to: A,
88        asset: DeployedTokenAmount<U256, Eip155TokenDeployment>,
89    ) -> v1::PriceTag {
90        let chain_id: ChainId = asset.token.chain_reference.into();
91        let network = chain_id
92            .as_network_name()
93            .unwrap_or_else(|| panic!("Can not get network name for chain id {}", chain_id));
94        let extra = asset
95            .token
96            .eip712
97            .and_then(|eip712| serde_json::to_value(&eip712).ok());
98        v1::PriceTag {
99            scheme: ExactScheme.to_string(),
100            pay_to: pay_to.into().to_string(),
101            asset: asset.token.address.to_string(),
102            network: network.to_string(),
103            amount: asset.amount.to_string(),
104            max_timeout_seconds: 300,
105            extra,
106            enricher: None,
107        }
108    }
109}
110
111impl X402SchemeId for V1Eip155Exact {
112    fn x402_version(&self) -> u8 {
113        1
114    }
115    fn namespace(&self) -> &str {
116        "eip155"
117    }
118    fn scheme(&self) -> &str {
119        ExactScheme.as_ref()
120    }
121}
122
123impl X402SchemeFacilitatorBuilder<&ChainProvider> for V1Eip155Exact {
124    fn build(
125        &self,
126        provider: &ChainProvider,
127        config: Option<serde_json::Value>,
128    ) -> Result<Box<dyn X402SchemeFacilitator>, Box<dyn std::error::Error>> {
129        let eip155_provider = if let ChainProvider::Eip155(provider) = provider {
130            Arc::clone(provider)
131        } else {
132            return Err("V1Eip155Exact::build: provider must be an Eip155ChainProvider".into());
133        };
134        self.build(eip155_provider, config)
135    }
136}
137
138impl<P> X402SchemeFacilitatorBuilder<P> for V1Eip155Exact
139where
140    P: Eip155MetaTransactionProvider + ChainProviderOps + Send + Sync + 'static,
141    Eip155ExactError: From<P::Error>,
142{
143    fn build(
144        &self,
145        provider: P,
146        _config: Option<serde_json::Value>,
147    ) -> Result<Box<dyn X402SchemeFacilitator>, Box<dyn std::error::Error>> {
148        Ok(Box::new(V1Eip155ExactFacilitator::new(provider)))
149    }
150}
151
152pub struct V1Eip155ExactFacilitator<P> {
153    provider: P,
154}
155
156impl<P> V1Eip155ExactFacilitator<P> {
157    /// Creates a new facilitator with the given provider.
158    pub fn new(provider: P) -> Self {
159        Self { provider }
160    }
161}
162
163#[async_trait::async_trait]
164impl<P> X402SchemeFacilitator for V1Eip155ExactFacilitator<P>
165where
166    P: Eip155MetaTransactionProvider + ChainProviderOps + Send + Sync,
167    P::Inner: Provider,
168    Eip155ExactError: From<P::Error>,
169{
170    async fn verify(
171        &self,
172        request: &proto::VerifyRequest,
173    ) -> Result<proto::VerifyResponse, X402SchemeFacilitatorError> {
174        let request = types::VerifyRequest::from_proto(request.clone())?;
175        let payload = &request.payment_payload;
176        let requirements = &request.payment_requirements;
177        let (contract, payment, eip712_domain) = assert_valid_payment(
178            self.provider.inner(),
179            self.provider.chain(),
180            payload,
181            requirements,
182        )
183        .await?;
184
185        let payer =
186            verify_payment(self.provider.inner(), &contract, &payment, &eip712_domain).await?;
187
188        Ok(v1::VerifyResponse::valid(payer.to_string()).into())
189    }
190
191    async fn settle(
192        &self,
193        request: &proto::SettleRequest,
194    ) -> Result<proto::SettleResponse, X402SchemeFacilitatorError> {
195        let request = types::SettleRequest::from_proto(request.clone())?;
196        let payload = &request.payment_payload;
197        let requirements = &request.payment_requirements;
198        let (contract, payment, eip712_domain) = assert_valid_payment(
199            self.provider.inner(),
200            self.provider.chain(),
201            payload,
202            requirements,
203        )
204        .await?;
205
206        let tx_hash = settle_payment(&self.provider, &contract, &payment, &eip712_domain).await?;
207        Ok(v1::SettleResponse::Success {
208            payer: payment.from.to_string(),
209            transaction: tx_hash.to_string(),
210            network: payload.network.clone(),
211        }
212        .into())
213    }
214
215    async fn supported(&self) -> Result<proto::SupportedResponse, X402SchemeFacilitatorError> {
216        let chain_id = self.provider.chain_id();
217        let kinds = {
218            let mut kinds = Vec::with_capacity(1);
219            let network = chain_id.as_network_name();
220            if let Some(network) = network {
221                kinds.push(proto::SupportedPaymentKind {
222                    x402_version: v1::X402Version1.into(),
223                    scheme: ExactScheme.to_string(),
224                    network: network.to_string(),
225                    extra: None,
226                });
227            }
228            kinds
229        };
230        let signers = {
231            let mut signers = HashMap::with_capacity(1);
232            signers.insert(chain_id, self.provider.signer_addresses());
233            signers
234        };
235        Ok(proto::SupportedResponse {
236            kinds,
237            extensions: Vec::new(),
238            signers,
239        })
240    }
241}
242
243/// A fully specified ERC-3009 authorization payload for EVM settlement.
244#[derive(Debug)]
245pub struct ExactEvmPayment {
246    /// Authorized sender (`from`) — EOA or smart wallet.
247    pub from: Address,
248    /// Authorized recipient (`to`).
249    pub to: Address,
250    /// Transfer amount (token units).
251    pub value: U256,
252    /// Not valid before this timestamp (inclusive).
253    pub valid_after: UnixTimestamp,
254    /// Not valid at/after this timestamp (exclusive).
255    pub valid_before: UnixTimestamp,
256    /// Unique 32-byte nonce (prevents replay).
257    pub nonce: B256,
258    /// Raw signature bytes (EIP-1271 or EIP-6492-wrapped).
259    pub signature: Bytes,
260}
261
262sol!(
263    #[allow(missing_docs)]
264    #[allow(clippy::too_many_arguments)]
265    #[derive(Debug)]
266    #[sol(rpc)]
267    IEIP3009,
268    "abi/IEIP3009.json"
269);
270
271sol! {
272    #[allow(missing_docs)]
273    #[allow(clippy::too_many_arguments)]
274    #[derive(Debug)]
275    #[sol(rpc)]
276    Validator6492,
277    "abi/Validator6492.json"
278}
279
280/// Runs all preconditions needed for a successful payment:
281/// - Valid scheme, network, and receiver.
282/// - Valid time window (validAfter/validBefore).
283/// - Correct EIP-712 domain construction.
284/// - Sufficient on-chain balance.
285/// - Sufficient value in payload.
286#[instrument(skip_all, err)]
287async fn assert_valid_payment<'a, P: Provider>(
288    provider: &'a P,
289    chain: &Eip155ChainReference,
290    payload: &types::PaymentPayload,
291    requirements: &types::PaymentRequirements,
292) -> Result<
293    (
294        IEIP3009::IEIP3009Instance<&'a P>,
295        ExactEvmPayment,
296        Eip712Domain,
297    ),
298    Eip155ExactError,
299> {
300    let chain_id: ChainId = chain.into();
301    let payload_chain_id = ChainId::from_network_name(&payload.network)
302        .ok_or(PaymentVerificationError::UnsupportedChain)?;
303    if payload_chain_id != chain_id {
304        return Err(PaymentVerificationError::ChainIdMismatch.into());
305    }
306    let requirements_chain_id = ChainId::from_network_name(&requirements.network)
307        .ok_or(PaymentVerificationError::UnsupportedChain)?;
308    if requirements_chain_id != chain_id {
309        return Err(PaymentVerificationError::ChainIdMismatch.into());
310    }
311    let authorization = &payload.payload.authorization;
312    if authorization.to != requirements.pay_to {
313        return Err(PaymentVerificationError::RecipientMismatch.into());
314    }
315    let valid_after = authorization.valid_after;
316    let valid_before = authorization.valid_before;
317    assert_time(valid_after, valid_before)?;
318    let asset_address = requirements.asset;
319    let contract = IEIP3009::new(asset_address, provider);
320
321    let domain = assert_domain(chain, &contract, &asset_address, &requirements.extra).await?;
322
323    let amount_required = requirements.max_amount_required;
324    assert_enough_balance(&contract, &authorization.from, amount_required).await?;
325    assert_enough_value(&authorization.value, &amount_required)?;
326
327    let payment = ExactEvmPayment {
328        from: authorization.from,
329        to: authorization.to,
330        value: authorization.value,
331        valid_after: authorization.valid_after,
332        valid_before: authorization.valid_before,
333        nonce: authorization.nonce,
334        signature: payload.payload.signature.clone(),
335    };
336
337    Ok((contract, payment, domain))
338}
339
340/// Validates that the current time is within the `validAfter` and `validBefore` bounds.
341///
342/// Adds a 6-second grace buffer when checking expiration to account for latency.
343#[instrument(skip_all, err)]
344pub fn assert_time(
345    valid_after: UnixTimestamp,
346    valid_before: UnixTimestamp,
347) -> Result<(), PaymentVerificationError> {
348    let now = UnixTimestamp::now();
349    if valid_before < now + 6 {
350        return Err(PaymentVerificationError::Expired);
351    }
352    if valid_after > now {
353        return Err(PaymentVerificationError::Early);
354    }
355    Ok(())
356}
357
358/// Constructs the correct EIP-712 domain for signature verification.
359#[instrument(skip_all, err, fields(
360    network = %chain.as_chain_id(),
361    asset = %asset_address
362))]
363pub async fn assert_domain<P: Provider>(
364    chain: &Eip155ChainReference,
365    token_contract: &IEIP3009::IEIP3009Instance<P>,
366    asset_address: &Address,
367    extra: &Option<PaymentRequirementsExtra>,
368) -> Result<Eip712Domain, Eip155ExactError> {
369    let name = extra.as_ref().map(|extra| extra.name.clone());
370    let name = if let Some(name) = name {
371        name
372    } else {
373        token_contract
374            .name()
375            .call()
376            .into_future()
377            .instrument(tracing::info_span!(
378                "fetch_eip712_name",
379                otel.kind = "client",
380            ))
381            .await?
382    };
383    let version = extra.as_ref().map(|extra| extra.version.clone());
384    let version = if let Some(version) = version {
385        version
386    } else {
387        token_contract
388            .version()
389            .call()
390            .into_future()
391            .instrument(tracing::info_span!(
392                "fetch_eip712_version",
393                otel.kind = "client",
394            ))
395            .await?
396    };
397    let domain = eip712_domain! {
398        name: name,
399        version: version,
400        chain_id: chain.inner(),
401        verifying_contract: *asset_address,
402    };
403    Ok(domain)
404}
405
406/// Checks if the payer has enough on-chain token balance to meet the `maxAmountRequired`.
407///
408/// Performs an `ERC20.balanceOf()` call using the token contract instance.
409#[instrument(skip_all, err, fields(
410    sender = %sender,
411    max_required = %max_amount_required,
412    token_contract = %ieip3009_token_contract.address()
413))]
414pub async fn assert_enough_balance<P: Provider>(
415    ieip3009_token_contract: &IEIP3009::IEIP3009Instance<P>,
416    sender: &Address,
417    max_amount_required: U256,
418) -> Result<(), Eip155ExactError> {
419    let balance = ieip3009_token_contract
420        .balanceOf(*sender)
421        .call()
422        .into_future()
423        .instrument(tracing::info_span!(
424            "fetch_token_balance",
425            token_contract = %ieip3009_token_contract.address(),
426            sender = %sender,
427            otel.kind = "client"
428        ))
429        .await?;
430
431    if balance < max_amount_required {
432        Err(PaymentVerificationError::InsufficientFunds.into())
433    } else {
434        Ok(())
435    }
436}
437
438/// Verifies that the declared `value` in the payload is sufficient for the required amount.
439///
440/// This is a static check (not on-chain) that compares two numbers.
441#[instrument(skip_all, err, fields(
442    sent = %sent,
443    max_amount_required = %max_amount_required
444))]
445pub fn assert_enough_value(
446    sent: &U256,
447    max_amount_required: &U256,
448) -> Result<(), PaymentVerificationError> {
449    if sent < max_amount_required {
450        Err(PaymentVerificationError::InvalidPaymentAmount)
451    } else {
452        Ok(())
453    }
454}
455
456/// Canonical data required to verify a signature.
457#[derive(Debug, Clone)]
458struct SignedMessage {
459    /// Expected signer (an EOA or contract wallet).
460    address: Address,
461    /// 32-byte digest that was signed (typically an EIP-712 hash).
462    hash: B256,
463    /// Structured signature, either EIP-6492 or EIP-1271.
464    signature: StructuredSignature,
465}
466
467sol!(
468    /// Solidity-compatible struct definition for ERC-3009 `transferWithAuthorization`.
469    ///
470    /// This matches the EIP-3009 format used in EIP-712 typed data:
471    /// it defines the authorization to transfer tokens from `from` to `to`
472    /// for a specific `value`, valid only between `validAfter` and `validBefore`
473    /// and identified by a unique `nonce`.
474    ///
475    /// This struct is primarily used to reconstruct the typed data domain/message
476    /// when verifying a client's signature.
477    #[derive(Serialize, Deserialize)]
478    struct TransferWithAuthorization {
479        address from;
480        address to;
481        uint256 value;
482        uint256 validAfter;
483        uint256 validBefore;
484        bytes32 nonce;
485    }
486);
487
488impl SignedMessage {
489    /// Construct a [`SignedMessage`] from an [`ExactEvmPayment`] and its
490    /// corresponding [`Eip712Domain`].
491    ///
492    /// This helper ties together:
493    /// - The **payment intent** (an ERC-3009 `TransferWithAuthorization` struct),
494    /// - The **EIP-712 domain** used for signing,
495    /// - And the raw signature bytes attached to the payment.
496    ///
497    /// Steps performed:
498    /// 1. Build an in-memory [`TransferWithAuthorization`] struct from the
499    ///    `ExactEvmPayment` fields (`from`, `to`, `value`, validity window, `nonce`).
500    /// 2. Compute the **EIP-712 struct hash** for that transfer under the given
501    ///    `domain`. This becomes the `hash` field of the signed message.
502    /// 3. Parse the raw signature bytes into a [`StructuredSignature`], which
503    ///    distinguishes between:
504    ///    - EIP-1271 (plain signature), and
505    ///    - EIP-6492 (counterfactual signature wrapper).
506    /// 4. Assemble all parts into a [`SignedMessage`] and return it.
507    pub fn extract(
508        payment: &ExactEvmPayment,
509        domain: &Eip712Domain,
510    ) -> Result<Self, StructuredSignatureFormatError> {
511        let transfer_with_authorization = TransferWithAuthorization {
512            from: payment.from,
513            to: payment.to,
514            value: payment.value,
515            validAfter: U256::from(payment.valid_after.as_secs()),
516            validBefore: U256::from(payment.valid_before.as_secs()),
517            nonce: payment.nonce,
518        };
519        let eip712_hash = transfer_with_authorization.eip712_signing_hash(domain);
520        let structured_signature: StructuredSignature = StructuredSignature::try_from_bytes(
521            payment.signature.clone(),
522            payment.from,
523            &eip712_hash,
524        )?;
525        let signed_message = Self {
526            address: payment.from,
527            hash: eip712_hash,
528            signature: structured_signature,
529        };
530        Ok(signed_message)
531    }
532}
533
534/// A structured representation of an Ethereum signature.
535///
536/// This enum normalizes two supported cases:
537///
538/// - **EIP-6492 wrapped signatures**: used for counterfactual contract wallets.
539///   They include deployment metadata (factory + calldata) plus the inner
540///   signature that the wallet contract will validate after deployment.
541/// - **EIP-1271 signatures**: plain contract (or EOA-style) signatures.
542#[derive(Debug, Clone)]
543enum StructuredSignature {
544    /// An EIP-6492 wrapped signature.
545    EIP6492 {
546        /// Factory contract that can deploy the wallet deterministically
547        factory: Address,
548        /// Calldata to invoke on the factory (often a CREATE2 deployment).
549        factory_calldata: Bytes,
550        /// Inner signature for the wallet itself, probably EIP-1271.
551        inner: Bytes,
552        /// Full original bytes including the 6492 wrapper and magic bytes suffix.
553        original: Bytes,
554    },
555    /// Normalized EOA signature.
556    #[allow(clippy::upper_case_acronyms)]
557    EOA(Signature),
558    /// A plain EIP-1271 or EOA signature (no 6492 wrappers).
559    EIP1271(Bytes),
560}
561
562/// The fixed 32-byte magic suffix defined by [EIP-6492](https://eips.ethereum.org/EIPS/eip-6492).
563///
564/// Any signature ending with this constant is treated as a 6492-wrapped
565/// signature; the preceding bytes are ABI-decoded as `(address factory, bytes factoryCalldata, bytes innerSig)`.
566const EIP6492_MAGIC_SUFFIX: [u8; 32] =
567    hex!("6492649264926492649264926492649264926492649264926492649264926492");
568
569sol! {
570    /// Solidity-compatible struct for decoding the prefix of an EIP-6492 signature.
571    ///
572    /// Matches the tuple `(address factory, bytes factoryCalldata, bytes innerSig)`.
573    #[derive(Debug)]
574    struct Sig6492 {
575        address factory;
576        bytes   factoryCalldata;
577        bytes   innerSig;
578    }
579}
580
581#[derive(Debug, thiserror::Error)]
582pub enum StructuredSignatureFormatError {
583    #[error(transparent)]
584    InvalidEIP6492Format(alloy_sol_types::Error),
585}
586
587impl StructuredSignature {
588    pub fn try_from_bytes(
589        bytes: Bytes,
590        expected_signer: Address,
591        prehash: &B256,
592    ) -> Result<Self, StructuredSignatureFormatError> {
593        let is_eip6492 = bytes.len() >= 32 && bytes[bytes.len() - 32..] == EIP6492_MAGIC_SUFFIX;
594        let signature = if is_eip6492 {
595            let body = &bytes[..bytes.len() - 32];
596            let sig6492 = Sig6492::abi_decode_params(body)
597                .map_err(StructuredSignatureFormatError::InvalidEIP6492Format)?;
598            StructuredSignature::EIP6492 {
599                factory: sig6492.factory,
600                factory_calldata: sig6492.factoryCalldata,
601                inner: sig6492.innerSig,
602                original: bytes,
603            }
604        } else {
605            // Let's see if it is a EOA signature
606            let eoa_signature = if bytes.len() == 65 {
607                Signature::from_raw(&bytes).ok().map(|s| s.normalized_s())
608            } else if bytes.len() == 64 {
609                Some(Signature::from_erc2098(&bytes).normalized_s())
610            } else {
611                None
612            };
613            match eoa_signature {
614                None => StructuredSignature::EIP1271(bytes),
615                Some(s) => {
616                    let is_expected_signer = s
617                        .recover_address_from_prehash(prehash)
618                        .ok()
619                        .map(|r| r == expected_signer)
620                        .unwrap_or(false);
621                    if is_expected_signer {
622                        StructuredSignature::EOA(s)
623                    } else {
624                        StructuredSignature::EIP1271(bytes)
625                    }
626                }
627            }
628        };
629        Ok(signature)
630    }
631}
632
633impl TryFrom<Bytes> for StructuredSignature {
634    type Error = StructuredSignatureFormatError;
635
636    /// Parse raw signature bytes into a `StructuredSignature`.
637    ///
638    /// Rules:
639    /// - If the last 32 bytes equal [`EIP6492_MAGIC_SUFFIX`], the prefix is
640    ///   decoded as a [`Sig6492`] struct and returned as
641    ///   [`StructuredSignature::EIP6492`].
642    /// - Otherwise, the bytes are returned as [`StructuredSignature::EIP1271`].
643    fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
644        let is_eip6492 = bytes.len() >= 32 && bytes[bytes.len() - 32..] == EIP6492_MAGIC_SUFFIX;
645        let signature = if is_eip6492 {
646            let body = &bytes[..bytes.len() - 32];
647            let sig6492 = Sig6492::abi_decode_params(body)
648                .map_err(StructuredSignatureFormatError::InvalidEIP6492Format)?;
649            StructuredSignature::EIP6492 {
650                factory: sig6492.factory,
651                factory_calldata: sig6492.factoryCalldata,
652                inner: sig6492.innerSig,
653                original: bytes,
654            }
655        } else {
656            StructuredSignature::EIP1271(bytes)
657        };
658        Ok(signature)
659    }
660}
661
662pub struct TransferWithAuthorization0Call<P>(
663    pub TransferWithAuthorizationCall<P, IEIP3009::transferWithAuthorization_0Call, Bytes>,
664);
665
666impl<'a, P: Provider> TransferWithAuthorization0Call<&'a P> {
667    /// Constructs a full `transferWithAuthorization` call for a verified payment payload.
668    ///
669    /// This function prepares the transaction builder with gas pricing adapted to the network's
670    /// capabilities (EIP-1559 or legacy) and packages it together with signature metadata
671    /// into a [`TransferWithAuthorization0Call`] structure.
672    ///
673    /// This function does not perform any validation — it assumes inputs are already checked.
674    pub fn new(
675        contract: &'a IEIP3009::IEIP3009Instance<P>,
676        payment: &ExactEvmPayment,
677        signature: Bytes,
678    ) -> Self {
679        let from = payment.from;
680        let to = payment.to;
681        let value = payment.value;
682        let valid_after = U256::from(payment.valid_after.as_secs());
683        let valid_before = U256::from(payment.valid_before.as_secs());
684        let nonce = payment.nonce;
685        let tx = contract.transferWithAuthorization_0(
686            from,
687            to,
688            value,
689            valid_after,
690            valid_before,
691            nonce,
692            signature.clone(),
693        );
694        TransferWithAuthorization0Call(TransferWithAuthorizationCall {
695            tx,
696            from,
697            to,
698            value,
699            valid_after,
700            valid_before,
701            nonce,
702            signature,
703            contract_address: *contract.address(),
704        })
705    }
706}
707
708pub struct TransferWithAuthorization1Call<P>(
709    pub TransferWithAuthorizationCall<P, IEIP3009::transferWithAuthorization_1Call, Signature>,
710);
711
712impl<'a, P: Provider> TransferWithAuthorization1Call<&'a P> {
713    /// Constructs a full `transferWithAuthorization` call for a verified payment payload
714    /// using split signature components (v, r, s).
715    ///
716    /// This function prepares the transaction builder with gas pricing adapted to the network's
717    /// capabilities (EIP-1559 or legacy) and packages it together with signature metadata
718    /// into a [`TransferWithAuthorization1Call`] structure.
719    ///
720    /// This function does not perform any validation — it assumes inputs are already checked.
721    pub fn new(
722        contract: &'a IEIP3009::IEIP3009Instance<P>,
723        payment: &ExactEvmPayment,
724        signature: Signature,
725    ) -> Self {
726        let from = payment.from;
727        let to = payment.to;
728        let value = payment.value;
729        let valid_after = U256::from(payment.valid_after.as_secs());
730        let valid_before = U256::from(payment.valid_before.as_secs());
731        let nonce = payment.nonce;
732        let v = 27 + (signature.v() as u8);
733        let r = B256::from(signature.r());
734        let s = B256::from(signature.s());
735        let tx = contract.transferWithAuthorization_1(
736            from,
737            to,
738            value,
739            valid_after,
740            valid_before,
741            nonce,
742            v,
743            r,
744            s,
745        );
746        TransferWithAuthorization1Call(TransferWithAuthorizationCall {
747            tx,
748            from,
749            to,
750            value,
751            valid_after,
752            valid_before,
753            nonce,
754            signature,
755            contract_address: *contract.address(),
756        })
757    }
758}
759
760/// A prepared call to `transferWithAuthorization` (ERC-3009) including all derived fields.
761///
762/// This struct wraps the assembled call builder, making it reusable across verification
763/// (`.call()`) and settlement (`.send()`) flows, along with context useful for tracing/logging.
764pub struct TransferWithAuthorizationCall<P, TCall, TSignature> {
765    /// The prepared call builder that can be `.call()`ed or `.send()`ed.
766    pub tx: SolCallBuilder<P, TCall>,
767    /// The sender (`from`) address for the authorization.
768    pub from: Address,
769    /// The recipient (`to`) address for the authorization.
770    pub to: Address,
771    /// The amount to transfer (value).
772    pub value: U256,
773    /// Start of the validity window (inclusive).
774    pub valid_after: U256,
775    /// End of the validity window (exclusive).
776    pub valid_before: U256,
777    /// 32-byte authorization nonce (prevents replay).
778    pub nonce: B256,
779    /// EIP-712 signature for the transfer authorization.
780    pub signature: TSignature,
781    /// Address of the token contract used for this transfer.
782    pub contract_address: Address,
783}
784
785/// Check whether contract code is present at `address`.
786///
787/// Uses `eth_getCode` against this provider. This is useful after a counterfactual
788/// deployment to confirm visibility on the sending RPC before submitting a
789/// follow-up transaction.
790async fn is_contract_deployed<P: Provider>(
791    provider: &P,
792    address: &Address,
793) -> Result<bool, TransportError> {
794    let bytes = provider
795        .get_code_at(*address)
796        .into_future()
797        .instrument(tracing::info_span!("get_code_at",
798            address = %address,
799            otel.kind = "client",
800        ))
801        .await?;
802    Ok(!bytes.is_empty())
803}
804
805pub async fn verify_payment<P: Provider>(
806    provider: &P,
807    contract: &IEIP3009::IEIP3009Instance<&P>,
808    payment: &ExactEvmPayment,
809    eip712_domain: &Eip712Domain,
810) -> Result<Address, Eip155ExactError> {
811    let signed_message = SignedMessage::extract(payment, eip712_domain)?;
812
813    let payer = signed_message.address;
814    let hash = signed_message.hash;
815    match signed_message.signature {
816        StructuredSignature::EIP6492 {
817            factory: _,
818            factory_calldata: _,
819            inner,
820            original,
821        } => {
822            // Prepare the call to validate EIP-6492 signature
823            let validator6492 = Validator6492::new(VALIDATOR_ADDRESS, &provider);
824            let is_valid_signature_call =
825                validator6492.isValidSigWithSideEffects(payer, hash, original);
826            // Prepare the call to simulate transfer the funds
827            let transfer_call = TransferWithAuthorization0Call::new(contract, payment, inner);
828            let transfer_call = transfer_call.0;
829            // Execute both calls in a single transaction simulation to accommodate for possible smart wallet creation
830            let (is_valid_signature_result, transfer_result) = provider
831                .multicall()
832                .add(is_valid_signature_call)
833                .add(transfer_call.tx)
834                .aggregate3()
835                .instrument(tracing::info_span!("call_transferWithAuthorization_0",
836                        from = %transfer_call.from,
837                        to = %transfer_call.to,
838                        value = %transfer_call.value,
839                        valid_after = %transfer_call.valid_after,
840                        valid_before = %transfer_call.valid_before,
841                        nonce = %transfer_call.nonce,
842                        signature = %transfer_call.signature,
843                        token_contract = %transfer_call.contract_address,
844                        otel.kind = "client",
845                ))
846                .await?;
847            let is_valid_signature_result = is_valid_signature_result
848                .map_err(|e| PaymentVerificationError::InvalidSignature(e.to_string()))?;
849            if !is_valid_signature_result {
850                return Err(PaymentVerificationError::InvalidSignature(
851                    "Chain reported signature to be invalid".to_string(),
852                )
853                .into());
854            }
855            transfer_result
856                .map_err(|e| PaymentVerificationError::TransactionSimulation(e.to_string()))?;
857        }
858        StructuredSignature::EIP1271(signature) => {
859            // It is EIP-1271 signature, which we can pass to the transfer simulation
860            let transfer_call = TransferWithAuthorization0Call::new(contract, payment, signature);
861            let transfer_call = transfer_call.0;
862            transfer_call
863                .tx
864                .call()
865                .into_future()
866                .instrument(tracing::info_span!("call_transferWithAuthorization_0",
867                        from = %transfer_call.from,
868                        to = %transfer_call.to,
869                        value = %transfer_call.value,
870                        valid_after = %transfer_call.valid_after,
871                        valid_before = %transfer_call.valid_before,
872                        nonce = %transfer_call.nonce,
873                        signature = %transfer_call.signature,
874                        token_contract = %transfer_call.contract_address,
875                        otel.kind = "client",
876                ))
877                .await?;
878        }
879        StructuredSignature::EOA(signature) => {
880            // It is EOA signature, which we can pass to the transfer simulation of (r,s,v)-based transferWithAuthorization function
881            let transfer_call = TransferWithAuthorization1Call::new(contract, payment, signature);
882            let transfer_call = transfer_call.0;
883            transfer_call
884                .tx
885                .call()
886                .into_future()
887                .instrument(tracing::info_span!("call_transferWithAuthorization_1",
888                        from = %transfer_call.from,
889                        to = %transfer_call.to,
890                        value = %transfer_call.value,
891                        valid_after = %transfer_call.valid_after,
892                        valid_before = %transfer_call.valid_before,
893                        nonce = %transfer_call.nonce,
894                        signature = %transfer_call.signature,
895                        token_contract = %transfer_call.contract_address,
896                        otel.kind = "client",
897                ))
898                .await?;
899        }
900    }
901
902    Ok(payer)
903}
904
905pub async fn settle_payment<P, E>(
906    provider: &P,
907    contract: &IEIP3009::IEIP3009Instance<&P::Inner>,
908    payment: &ExactEvmPayment,
909    eip712_domain: &Eip712Domain,
910) -> Result<TxHash, Eip155ExactError>
911where
912    P: Eip155MetaTransactionProvider<Error = E>,
913    Eip155ExactError: From<E>,
914{
915    let signed_message = SignedMessage::extract(payment, eip712_domain)?;
916    let payer = payment.from;
917    let transaction_receipt_fut = match signed_message.signature {
918        StructuredSignature::EIP6492 {
919            factory,
920            factory_calldata,
921            inner,
922            original: _,
923        } => {
924            let is_contract_deployed = is_contract_deployed(provider.inner(), &payer).await?;
925            let transfer_call = TransferWithAuthorization0Call::new(contract, payment, inner);
926            let transfer_call = transfer_call.0;
927            if is_contract_deployed {
928                // transferWithAuthorization with inner signature
929                Eip155MetaTransactionProvider::send_transaction(
930                    provider,
931                    MetaTransaction {
932                        to: transfer_call.tx.target(),
933                        calldata: transfer_call.tx.calldata().clone(),
934                        confirmations: 1,
935                    },
936                )
937                .instrument(
938                    tracing::info_span!("call_transferWithAuthorization_0",
939                        from = %transfer_call.from,
940                        to = %transfer_call.to,
941                        value = %transfer_call.value,
942                        valid_after = %transfer_call.valid_after,
943                        valid_before = %transfer_call.valid_before,
944                        nonce = %transfer_call.nonce,
945                        signature = %transfer_call.signature,
946                        token_contract = %transfer_call.contract_address,
947                        sig_kind="EIP6492.deployed",
948                        otel.kind = "client",
949                    ),
950                )
951            } else {
952                // deploy the smart wallet, and transferWithAuthorization with inner signature
953                let deployment_call = IMulticall3::Call3 {
954                    allowFailure: true,
955                    target: factory,
956                    callData: factory_calldata,
957                };
958                let transfer_with_authorization_call = IMulticall3::Call3 {
959                    allowFailure: false,
960                    target: transfer_call.tx.target(),
961                    callData: transfer_call.tx.calldata().clone(),
962                };
963                let aggregate_call = IMulticall3::aggregate3Call {
964                    calls: vec![deployment_call, transfer_with_authorization_call],
965                };
966                Eip155MetaTransactionProvider::send_transaction(
967                    provider,
968                    MetaTransaction {
969                        to: MULTICALL3_ADDRESS,
970                        calldata: aggregate_call.abi_encode().into(),
971                        confirmations: 1,
972                    },
973                )
974                .instrument(
975                    tracing::info_span!("call_transferWithAuthorization_0",
976                        from = %transfer_call.from,
977                        to = %transfer_call.to,
978                        value = %transfer_call.value,
979                        valid_after = %transfer_call.valid_after,
980                        valid_before = %transfer_call.valid_before,
981                        nonce = %transfer_call.nonce,
982                        signature = %transfer_call.signature,
983                        token_contract = %transfer_call.contract_address,
984                        sig_kind="EIP6492.counterfactual",
985                        otel.kind = "client",
986                    ),
987                )
988            }
989        }
990        StructuredSignature::EIP1271(eip1271_signature) => {
991            let transfer_call =
992                TransferWithAuthorization0Call::new(contract, payment, eip1271_signature);
993            let transfer_call = transfer_call.0;
994            // transferWithAuthorization with eip1271 signature
995            Eip155MetaTransactionProvider::send_transaction(
996                provider,
997                MetaTransaction {
998                    to: transfer_call.tx.target(),
999                    calldata: transfer_call.tx.calldata().clone(),
1000                    confirmations: 1,
1001                },
1002            )
1003            .instrument(tracing::info_span!("call_transferWithAuthorization_0",
1004                from = %transfer_call.from,
1005                to = %transfer_call.to,
1006                value = %transfer_call.value,
1007                valid_after = %transfer_call.valid_after,
1008                valid_before = %transfer_call.valid_before,
1009                nonce = %transfer_call.nonce,
1010                signature = %transfer_call.signature,
1011                token_contract = %transfer_call.contract_address,
1012                sig_kind="EIP1271",
1013                otel.kind = "client",
1014            ))
1015        }
1016        StructuredSignature::EOA(signature) => {
1017            let transfer_call = TransferWithAuthorization1Call::new(contract, payment, signature);
1018            let transfer_call = transfer_call.0;
1019            // transferWithAuthorization with EOA signature
1020            Eip155MetaTransactionProvider::send_transaction(
1021                provider,
1022                MetaTransaction {
1023                    to: transfer_call.tx.target(),
1024                    calldata: transfer_call.tx.calldata().clone(),
1025                    confirmations: 1,
1026                },
1027            )
1028            .instrument(tracing::info_span!("call_transferWithAuthorization_1",
1029                from = %transfer_call.from,
1030                to = %transfer_call.to,
1031                value = %transfer_call.value,
1032                valid_after = %transfer_call.valid_after,
1033                valid_before = %transfer_call.valid_before,
1034                nonce = %transfer_call.nonce,
1035                signature = %transfer_call.signature,
1036                token_contract = %transfer_call.contract_address,
1037                sig_kind="EOA",
1038                otel.kind = "client",
1039            ))
1040        }
1041    };
1042    let receipt = transaction_receipt_fut.await?;
1043    let success = receipt.status();
1044    if success {
1045        tracing::event!(Level::INFO,
1046            status = "ok",
1047            tx = %receipt.transaction_hash,
1048            "transferWithAuthorization_0 succeeded"
1049        );
1050        Ok(receipt.transaction_hash)
1051    } else {
1052        tracing::event!(
1053            Level::WARN,
1054            status = "failed",
1055            tx = %receipt.transaction_hash,
1056            "transferWithAuthorization_0 failed"
1057        );
1058        Err(Eip155ExactError::TransactionReverted(
1059            receipt.transaction_hash,
1060        ))
1061    }
1062}
1063
1064#[derive(Debug, thiserror::Error)]
1065pub enum Eip155ExactError {
1066    #[error(transparent)]
1067    Transport(#[from] TransportError),
1068    #[error(transparent)]
1069    PendingTransaction(#[from] PendingTransactionError),
1070    #[error("Transaction {0} reverted")]
1071    TransactionReverted(TxHash),
1072    #[error("Contract call failed: {0}")]
1073    ContractCall(String),
1074    #[error(transparent)]
1075    PaymentVerification(#[from] PaymentVerificationError),
1076}
1077
1078impl From<Eip155ExactError> for X402SchemeFacilitatorError {
1079    fn from(value: Eip155ExactError) -> Self {
1080        match value {
1081            Eip155ExactError::Transport(_) => Self::OnchainFailure(value.to_string()),
1082            Eip155ExactError::PendingTransaction(_) => Self::OnchainFailure(value.to_string()),
1083            Eip155ExactError::TransactionReverted(_) => Self::OnchainFailure(value.to_string()),
1084            Eip155ExactError::ContractCall(_) => Self::OnchainFailure(value.to_string()),
1085            Eip155ExactError::PaymentVerification(e) => Self::PaymentVerification(e),
1086        }
1087    }
1088}
1089
1090impl From<StructuredSignatureFormatError> for Eip155ExactError {
1091    fn from(e: StructuredSignatureFormatError) -> Self {
1092        Self::PaymentVerification(PaymentVerificationError::InvalidSignature(e.to_string()))
1093    }
1094}
1095
1096impl From<MetaTransactionSendError> for Eip155ExactError {
1097    fn from(e: MetaTransactionSendError) -> Self {
1098        match e {
1099            MetaTransactionSendError::Transport(e) => Self::Transport(e),
1100            MetaTransactionSendError::PendingTransaction(e) => Self::PendingTransaction(e),
1101            MetaTransactionSendError::Custom(e) => Self::ContractCall(e),
1102        }
1103    }
1104}
1105
1106impl From<MulticallError> for Eip155ExactError {
1107    fn from(e: MulticallError) -> Self {
1108        match e {
1109            MulticallError::ValueTx => Self::PaymentVerification(
1110                PaymentVerificationError::TransactionSimulation(e.to_string()),
1111            ),
1112            MulticallError::DecodeError(_) => Self::PaymentVerification(
1113                PaymentVerificationError::TransactionSimulation(e.to_string()),
1114            ),
1115            MulticallError::NoReturnData => Self::PaymentVerification(
1116                PaymentVerificationError::TransactionSimulation(e.to_string()),
1117            ),
1118            MulticallError::CallFailed(_) => Self::PaymentVerification(
1119                PaymentVerificationError::TransactionSimulation(e.to_string()),
1120            ),
1121            MulticallError::TransportError(transport_error) => Self::Transport(transport_error),
1122        }
1123    }
1124}
1125
1126impl From<alloy_contract::Error> for Eip155ExactError {
1127    fn from(e: alloy_contract::Error) -> Self {
1128        match e {
1129            alloy_contract::Error::UnknownFunction(_) => Self::ContractCall(e.to_string()),
1130            alloy_contract::Error::UnknownSelector(_) => Self::ContractCall(e.to_string()),
1131            alloy_contract::Error::NotADeploymentTransaction => Self::ContractCall(e.to_string()),
1132            alloy_contract::Error::ContractNotDeployed => Self::ContractCall(e.to_string()),
1133            alloy_contract::Error::ZeroData(_, _) => Self::ContractCall(e.to_string()),
1134            alloy_contract::Error::AbiError(_) => Self::ContractCall(e.to_string()),
1135            alloy_contract::Error::TransportError(e) => Self::Transport(e),
1136            alloy_contract::Error::PendingTransactionError(e) => Self::PendingTransaction(e),
1137        }
1138    }
1139}