x402_kit/schemes/
exact_evm.rs

1use 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
119/// Exact EVM Scheme information holder
120pub 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}