Skip to main content

x402_networks/evm/
exact.rs

1use bon::Builder;
2use serde::{Deserialize, Serialize};
3
4use x402_core::{
5    core::{Payment, Scheme},
6    transport::PaymentRequirements,
7    types::{AmountValue, AnyJson},
8};
9
10use crate::evm::{EvmAddress, EvmNetwork, EvmSignature, ExplicitEvmAsset, ExplicitEvmNetwork};
11
12use std::{
13    fmt::{Debug, Display},
14    str::FromStr,
15};
16
17#[derive(Clone, Copy, PartialEq, Eq, Hash)]
18pub struct Nonce(pub [u8; 32]);
19
20impl Debug for Nonce {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        write!(f, "Nonce(0x{})", hex::encode(self.0))
23    }
24}
25
26impl Display for Nonce {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "0x{}", hex::encode(self.0))
29    }
30}
31
32impl FromStr for Nonce {
33    type Err = hex::FromHexError;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        let s = s.strip_prefix("0x").unwrap_or(s);
37        let bytes = hex::decode(s)?;
38        if bytes.len() != 32 {
39            return Err(hex::FromHexError::InvalidStringLength);
40        }
41        let mut arr = [0u8; 32];
42        arr.copy_from_slice(&bytes);
43        Ok(Nonce(arr))
44    }
45}
46
47impl Serialize for Nonce {
48    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
49    where
50        S: serde::Serializer,
51    {
52        serializer.serialize_str(&self.to_string())
53    }
54}
55
56impl<'de> Deserialize<'de> for Nonce {
57    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
58    where
59        D: serde::Deserializer<'de>,
60    {
61        let s = String::deserialize(deserializer)?;
62        let nonce = Nonce::from_str(&s).map_err(serde::de::Error::custom)?;
63        Ok(nonce)
64    }
65}
66
67#[derive(Clone, Copy, PartialEq, Eq, Hash)]
68pub struct TimestampSeconds(pub u64);
69
70impl Display for TimestampSeconds {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "{}", self.0)
73    }
74}
75
76impl Debug for TimestampSeconds {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "TimeSeconds({})", self.0)
79    }
80}
81
82impl Serialize for TimestampSeconds {
83    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
84    where
85        S: serde::Serializer,
86    {
87        serializer.serialize_str(&self.to_string())
88    }
89}
90
91impl<'de> Deserialize<'de> for TimestampSeconds {
92    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
93    where
94        D: serde::Deserializer<'de>,
95    {
96        let s = String::deserialize(deserializer)?;
97        let seconds = s.parse::<u64>().map_err(serde::de::Error::custom)?;
98        Ok(TimestampSeconds(seconds))
99    }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct ExactEvmPayload {
105    pub signature: EvmSignature,
106    pub authorization: ExactEvmAuthorization,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct ExactEvmAuthorization {
112    pub from: EvmAddress,
113    pub to: EvmAddress,
114    pub value: AmountValue,
115    pub valid_after: TimestampSeconds,
116    pub valid_before: TimestampSeconds,
117    pub nonce: Nonce,
118}
119
120/// Exact EVM Scheme information holder
121pub struct ExactEvmScheme(pub EvmNetwork);
122
123impl Scheme for ExactEvmScheme {
124    type Network = EvmNetwork;
125    type Payload = ExactEvmPayload;
126    const SCHEME_NAME: &'static str = "exact";
127
128    fn network(&self) -> &Self::Network {
129        &self.0
130    }
131}
132
133#[derive(Builder, Debug, Clone)]
134pub struct ExactEvm<A: ExplicitEvmAsset> {
135    pub asset: A,
136    #[builder(into)]
137    pub pay_to: EvmAddress,
138    pub amount: u64,
139    pub max_timeout_seconds_override: Option<u64>,
140    pub extra_override: Option<AnyJson>,
141}
142
143impl<A: ExplicitEvmAsset> From<ExactEvm<A>> for Payment<ExactEvmScheme, EvmAddress> {
144    fn from(scheme: ExactEvm<A>) -> Self {
145        Payment {
146            scheme: ExactEvmScheme(A::Network::NETWORK),
147            pay_to: scheme.pay_to,
148            asset: A::ASSET,
149            amount: scheme.amount.into(),
150            max_timeout_seconds: scheme.max_timeout_seconds_override.unwrap_or(300),
151            extra: scheme
152                .extra_override
153                .or(A::EIP712_DOMAIN.and_then(|v| serde_json::to_value(v).ok())),
154        }
155    }
156}
157
158impl<A: ExplicitEvmAsset> From<ExactEvm<A>> for PaymentRequirements {
159    fn from(scheme: ExactEvm<A>) -> Self {
160        PaymentRequirements::from(Payment::from(scheme))
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use alloy_primitives::address;
167    use serde_json::json;
168
169    use crate::evm::assets::UsdcBaseSepolia;
170
171    use super::*;
172
173    #[test]
174    fn test_build_payment_requirements() {
175        let scheme = ExactEvm::builder()
176            .asset(UsdcBaseSepolia)
177            .amount(1000)
178            .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20"))
179            .build();
180        let payment_requirements: PaymentRequirements = scheme.into();
181
182        assert_eq!(payment_requirements.scheme, "exact");
183        assert_eq!(
184            payment_requirements.asset,
185            UsdcBaseSepolia::ASSET.address.to_string()
186        );
187        assert_eq!(payment_requirements.amount, 1000u64.into());
188    }
189
190    #[test]
191    fn test_extra_override() {
192        let pr: PaymentRequirements = ExactEvm::builder()
193            .asset(UsdcBaseSepolia)
194            .amount(1000)
195            .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20"))
196            .build()
197            .into();
198
199        assert!(pr.extra.is_some());
200        assert_eq!(
201            pr.extra,
202            serde_json::to_value(UsdcBaseSepolia::EIP712_DOMAIN).ok()
203        );
204
205        let pr: PaymentRequirements = ExactEvm::builder()
206            .asset(UsdcBaseSepolia)
207            .amount(1000)
208            .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20"))
209            .extra_override(json!({"foo": "bar"}))
210            .build()
211            .into();
212
213        assert_eq!(pr.extra, Some(json!({"foo": "bar"})));
214    }
215}