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