x402_networks/evm/
exact.rs1use 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
120pub 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}