x402_kit/schemes/
exact_evm_signer.rs1use alloy_core::{
2 sol,
3 sol_types::{Eip712Domain, SolStruct, eip712_domain},
4};
5use alloy_primitives::{FixedBytes, U256};
6use alloy_signer::{Error as AlloySignerError, Signer as AlloySigner};
7use serde::Deserialize;
8
9use crate::{
10 core::{PaymentSelection, Scheme, SchemeSigner},
11 networks::evm::{EvmAddress, EvmSignature, ExplicitEvmAsset, ExplicitEvmNetwork},
12 schemes::exact_evm::*,
13};
14
15use std::{fmt::Debug, time::SystemTime};
16
17pub trait AuthorizationSigner {
18 type Error: std::error::Error;
19
20 fn sign_authorization(
21 &self,
22 authorization: &Eip3009Authorization,
23 asset_eip712_domain: &Eip712Domain,
24 ) -> impl Future<Output = Result<EvmSignature, Self::Error>>;
25}
26
27sol!(
28 struct Eip3009Authorization {
32 address from;
33 address to;
34 uint256 value;
35 uint256 validAfter;
36 uint256 validBefore;
37 bytes32 nonce;
38 }
39);
40
41impl From<ExactEvmAuthorization> for Eip3009Authorization {
42 fn from(authorization: ExactEvmAuthorization) -> Self {
43 Eip3009Authorization {
44 from: authorization.from.0,
45 to: authorization.to.0,
46 value: U256::from(authorization.value.0),
47 validAfter: U256::from(authorization.valid_after.0),
48 validBefore: U256::from(authorization.valid_before.0),
49 nonce: FixedBytes(authorization.nonce.0),
50 }
51 }
52}
53
54impl<S: AlloySigner> AuthorizationSigner for S {
55 type Error = AlloySignerError;
56
57 async fn sign_authorization(
58 &self,
59 authorization: &Eip3009Authorization,
60 domain: &Eip712Domain,
61 ) -> Result<EvmSignature, Self::Error> {
62 let eip712_hash = authorization.eip712_signing_hash(domain);
63 let signature = self.sign_hash(&eip712_hash).await?;
64
65 Ok(EvmSignature(signature))
66 }
67}
68
69pub struct ExactEvmSigner<S: AuthorizationSigner, A: ExplicitEvmAsset> {
70 pub signer: S,
71 pub asset: A,
72}
73
74#[derive(Debug, thiserror::Error)]
75pub enum ExactEvmSignError<S: AuthorizationSigner> {
76 #[error("Signer error: {0}")]
77 SignerError(S::Error),
78 #[error("System time error: {0}")]
79 SystemTimeError(#[from] std::time::SystemTimeError),
80}
81
82impl<S, A> SchemeSigner<EvmAddress> for ExactEvmSigner<S, A>
83where
84 S: AuthorizationSigner + Debug,
85 A: ExplicitEvmAsset,
86{
87 type Scheme = ExactEvmScheme;
88 type Error = ExactEvmSignError<S>;
89
90 async fn sign(
91 &self,
92 selected: &PaymentSelection<EvmAddress>,
93 ) -> Result<<Self::Scheme as Scheme>::Payload, Self::Error> {
94 let now = SystemTime::now()
95 .duration_since(SystemTime::UNIX_EPOCH)?
96 .as_secs();
97
98 #[derive(Deserialize, Default)]
99 struct Eip712DomainExtra {
100 name: String,
101 version: String,
102 }
103
104 let eip712_domain_info = selected
105 .extra
106 .as_ref()
107 .and_then(|extra| serde_json::from_value::<Eip712DomainExtra>(extra.clone()).ok())
108 .unwrap_or_default();
110
111 let authorization = ExactEvmAuthorization {
112 from: selected.pay_to,
113 to: selected.pay_to,
114 value: selected.amount,
115 valid_after: TimestampSeconds(now.saturating_sub(300)),
117 valid_before: TimestampSeconds(now + selected.max_timeout_seconds),
118 nonce: Nonce(rand::random()),
119 };
120
121 let signer = &self.signer;
122 let auth_clone = authorization.clone();
123 let domain = eip712_domain!(
124 name: eip712_domain_info.name,
125 version: eip712_domain_info.version,
126 chain_id: A::Network::NETWORK.chain_id,
127 verifying_contract: A::ASSET.address.0,
128 );
129 let signature = signer
130 .sign_authorization(&auth_clone.into(), &domain)
131 .await
132 .map_err(Self::Error::SignerError)?;
133 Ok(ExactEvmPayload {
134 signature,
135 authorization,
136 })
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use alloy::signers::local::PrivateKeySigner;
143 use alloy_primitives::address;
144 use serde_json::json;
145 use url::Url;
146
147 use crate::{
148 core::Resource,
149 networks::evm::{assets::UsdcBaseSepolia, networks::BaseSepolia},
150 types::{AmountValue, Record},
151 };
152
153 use super::*;
154
155 #[tokio::test]
156 async fn test_signing() {
157 let signer = PrivateKeySigner::random();
158
159 let evm_signer = ExactEvmSigner {
160 signer,
161 asset: UsdcBaseSepolia,
162 };
163
164 let resource = Resource::builder()
165 .url(Url::parse("https://example.com/payment").unwrap())
166 .description("Payment for services".to_string())
167 .mime_type("application/json".to_string())
168 .build();
169
170 let payment = PaymentSelection {
171 amount: 1000u64.into(),
172 resource,
173 pay_to: EvmAddress(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")),
174 max_timeout_seconds: 60,
175 asset: UsdcBaseSepolia::ASSET.address,
176 extra: Some(json!({
177 "name": "USD Coin",
178 "version": "2"
179 })),
180 extensions: Record::new(),
181 };
182
183 let payload = evm_signer
184 .sign(&payment)
185 .await
186 .expect("Signing should succeed");
187
188 assert_eq!(payload.authorization.value, AmountValue(1000));
189
190 let domain = eip712_domain! {
192 name: "USD Coin".to_string(),
193 version: "2".to_string(),
194 chain_id: BaseSepolia::NETWORK.chain_id,
195 verifying_contract: UsdcBaseSepolia::ASSET.address.0,
196 };
197
198 let recovered_address = payload
199 .signature
200 .0
201 .recover_address_from_prehash(
202 &Eip3009Authorization::from(payload.authorization.clone())
203 .eip712_signing_hash(&domain.into()),
204 )
205 .expect("Recovery should succeed");
206
207 assert_eq!(recovered_address, evm_signer.signer.address());
208 }
209}