1use chrono::Utc;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Duration;
10
11pub type AuthHeadersFn =
13 dyn Fn() -> crate::Result<HashMap<String, HashMap<String, String>>> + Send + Sync;
14
15pub type AuthHeadersFnArc = Arc<AuthHeadersFn>;
17
18pub type AuthHeadersFnBox = Box<AuthHeadersFn>;
20
21pub const X402_VERSION: u32 = 1;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Network {
27 Mainnet,
28 Testnet,
29}
30
31#[derive(Debug, Clone)]
33pub struct NetworkConfig {
34 pub chain_id: u64,
36 pub usdc_contract: String,
38 pub name: String,
40 pub is_testnet: bool,
42}
43
44impl NetworkConfig {
45 pub fn base_mainnet() -> Self {
47 Self {
48 chain_id: 8453,
49 usdc_contract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913".to_string(),
50 name: "base".to_string(),
51 is_testnet: false,
52 }
53 }
54
55 pub fn base_sepolia() -> Self {
57 Self {
58 chain_id: 84532,
59 usdc_contract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e".to_string(),
60 name: "base-sepolia".to_string(),
61 is_testnet: true,
62 }
63 }
64
65 pub fn from_name(name: &str) -> Option<Self> {
67 match name {
68 "base" => Some(Self::base_mainnet()),
69 "base-sepolia" => Some(Self::base_sepolia()),
70 _ => None,
71 }
72 }
73}
74
75impl Network {
76 pub fn as_str(&self) -> &'static str {
78 match self {
79 Network::Mainnet => "base",
80 Network::Testnet => "base-sepolia",
81 }
82 }
83
84 pub fn usdc_address(&self) -> &'static str {
86 match self {
87 Network::Mainnet => "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
88 Network::Testnet => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
89 }
90 }
91
92 pub fn usdc_name(&self) -> &'static str {
94 match self {
95 Network::Mainnet => "USD Coin",
96 Network::Testnet => "USDC",
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct PaymentRequirements {
104 pub scheme: String,
106 pub network: String,
108 #[serde(rename = "maxAmountRequired")]
110 pub max_amount_required: String,
111 pub asset: String,
113 #[serde(rename = "payTo")]
115 pub pay_to: String,
116 pub resource: String,
118 pub description: String,
120 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
122 pub mime_type: Option<String>,
123 #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
125 pub output_schema: Option<Value>,
126 #[serde(rename = "maxTimeoutSeconds")]
128 pub max_timeout_seconds: u32,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub extra: Option<Value>,
132}
133
134impl PaymentRequirements {
135 pub fn new(
137 scheme: impl Into<String>,
138 network: impl Into<String>,
139 max_amount_required: impl Into<String>,
140 asset: impl Into<String>,
141 pay_to: impl Into<String>,
142 resource: impl Into<String>,
143 description: impl Into<String>,
144 ) -> Self {
145 Self {
146 scheme: scheme.into(),
147 network: network.into(),
148 max_amount_required: max_amount_required.into(),
149 asset: asset.into(),
150 pay_to: pay_to.into(),
151 resource: resource.into(),
152 description: description.into(),
153 mime_type: None,
154 output_schema: None,
155 max_timeout_seconds: 60,
156 extra: None,
157 }
158 }
159
160 pub fn set_usdc_info(&mut self, network: Network) -> crate::Result<()> {
162 let mut usdc_info = HashMap::new();
163 usdc_info.insert("name".to_string(), network.usdc_name().to_string());
164 usdc_info.insert("version".to_string(), "2".to_string());
165
166 self.extra = Some(serde_json::to_value(usdc_info)?);
167 Ok(())
168 }
169
170 pub fn amount_as_decimal(&self) -> crate::Result<Decimal> {
172 self.max_amount_required
173 .parse()
174 .map_err(|_| crate::X402Error::invalid_payment_requirements("Invalid amount format"))
175 }
176
177 pub fn amount_in_decimal_units(&self, decimals: u8) -> crate::Result<Decimal> {
179 let amount = self.amount_as_decimal()?;
180 let divisor = Decimal::from(10u64.pow(decimals as u32));
181 Ok(amount / divisor)
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct PaymentPayload {
188 #[serde(rename = "x402Version")]
190 pub x402_version: u32,
191 pub scheme: String,
193 pub network: String,
195 pub payload: ExactEvmPayload,
197}
198
199impl PaymentPayload {
200 pub fn new(
202 scheme: impl Into<String>,
203 network: impl Into<String>,
204 payload: ExactEvmPayload,
205 ) -> Self {
206 Self {
207 x402_version: X402_VERSION,
208 scheme: scheme.into(),
209 network: network.into(),
210 payload,
211 }
212 }
213
214 pub fn from_base64(encoded: &str) -> crate::Result<Self> {
216 use base64::{engine::general_purpose, Engine as _};
217 let decoded = general_purpose::STANDARD.decode(encoded)?;
218 let payload: PaymentPayload = serde_json::from_slice(&decoded)?;
219 Ok(payload)
220 }
221
222 pub fn to_base64(&self) -> crate::Result<String> {
224 use base64::{engine::general_purpose, Engine as _};
225 let json = serde_json::to_string(self)?;
226 Ok(general_purpose::STANDARD.encode(json))
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct ExactEvmPayload {
233 pub signature: String,
235 pub authorization: ExactEvmPayloadAuthorization,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ExactEvmPayloadAuthorization {
242 pub from: String,
244 pub to: String,
246 pub value: String,
248 #[serde(rename = "validAfter")]
250 pub valid_after: String,
251 #[serde(rename = "validBefore")]
253 pub valid_before: String,
254 pub nonce: String,
256}
257
258impl ExactEvmPayloadAuthorization {
259 pub fn new(
261 from: impl Into<String>,
262 to: impl Into<String>,
263 value: impl Into<String>,
264 valid_after: impl Into<String>,
265 valid_before: impl Into<String>,
266 nonce: impl Into<String>,
267 ) -> Self {
268 Self {
269 from: from.into(),
270 to: to.into(),
271 value: value.into(),
272 valid_after: valid_after.into(),
273 valid_before: valid_before.into(),
274 nonce: nonce.into(),
275 }
276 }
277
278 pub fn is_valid_now(&self) -> crate::Result<bool> {
280 let now = Utc::now().timestamp();
281 let valid_after: i64 = self.valid_after.parse().map_err(|_| {
282 crate::X402Error::invalid_authorization("Invalid valid_after timestamp")
283 })?;
284 let valid_before: i64 = self.valid_before.parse().map_err(|_| {
285 crate::X402Error::invalid_authorization("Invalid valid_before timestamp")
286 })?;
287
288 Ok(now >= valid_after && now <= valid_before)
289 }
290
291 pub fn validity_duration(&self) -> crate::Result<Duration> {
293 let valid_after: i64 = self.valid_after.parse().map_err(|_| {
294 crate::X402Error::invalid_authorization("Invalid valid_after timestamp")
295 })?;
296 let valid_before: i64 = self.valid_before.parse().map_err(|_| {
297 crate::X402Error::invalid_authorization("Invalid valid_before timestamp")
298 })?;
299
300 Ok(Duration::from_secs((valid_before - valid_after) as u64))
301 }
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct VerifyResponse {
307 #[serde(rename = "isValid")]
309 pub is_valid: bool,
310 #[serde(rename = "invalidReason", skip_serializing_if = "Option::is_none")]
312 pub invalid_reason: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub payer: Option<String>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct SettleResponse {
321 pub success: bool,
323 #[serde(rename = "errorReason", skip_serializing_if = "Option::is_none")]
325 pub error_reason: Option<String>,
326 pub transaction: String,
328 pub network: String,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub payer: Option<String>,
333}
334
335impl SettleResponse {
336 pub fn to_base64(&self) -> crate::Result<String> {
338 use base64::{engine::general_purpose, Engine as _};
339 let json = serde_json::to_string(self)?;
340 Ok(general_purpose::STANDARD.encode(json))
341 }
342}
343
344#[derive(Clone)]
346pub struct FacilitatorConfig {
347 pub url: String,
349 pub timeout: Option<Duration>,
351 pub create_auth_headers: Option<AuthHeadersFnArc>,
353}
354
355impl std::fmt::Debug for FacilitatorConfig {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 f.debug_struct("FacilitatorConfig")
358 .field("url", &self.url)
359 .field("timeout", &self.timeout)
360 .field("create_auth_headers", &"<function>")
361 .finish()
362 }
363}
364
365impl FacilitatorConfig {
366 pub fn new(url: impl Into<String>) -> Self {
368 Self {
369 url: url.into(),
370 timeout: None,
371 create_auth_headers: None,
372 }
373 }
374
375 pub fn validate(&self) -> crate::Result<()> {
377 if self.url.is_empty() {
378 return Err(crate::X402Error::config("Facilitator URL cannot be empty"));
379 }
380
381 if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
382 return Err(crate::X402Error::config(
383 "Facilitator URL must start with http:// or https://",
384 ));
385 }
386
387 Ok(())
388 }
389
390 pub fn with_timeout(mut self, timeout: Duration) -> Self {
392 self.timeout = Some(timeout);
393 self
394 }
395
396 pub fn with_auth_headers(mut self, creator: AuthHeadersFnBox) -> Self {
398 self.create_auth_headers = Some(Arc::from(creator));
399 self
400 }
401}
402
403impl Default for FacilitatorConfig {
404 fn default() -> Self {
405 Self::new("https://x402.org/facilitator")
406 }
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct PaymentRequirementsResponse {
412 #[serde(rename = "x402Version")]
414 pub x402_version: u32,
415 pub error: String,
417 pub accepts: Vec<PaymentRequirements>,
419}
420
421impl PaymentRequirementsResponse {
422 pub fn new(error: impl Into<String>, accepts: Vec<PaymentRequirements>) -> Self {
424 Self {
425 x402_version: X402_VERSION,
426 error: error.into(),
427 accepts,
428 }
429 }
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct SupportedKinds {
435 pub kinds: Vec<SupportedKind>,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct SupportedKind {
442 #[serde(rename = "x402Version")]
444 pub x402_version: u32,
445 pub scheme: String,
447 pub network: String,
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub metadata: Option<Value>,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct DiscoveryResource {
457 pub resource: String,
459 pub r#type: String,
461 #[serde(rename = "x402Version")]
463 pub x402_version: u32,
464 pub accepts: Vec<PaymentRequirements>,
466 #[serde(rename = "lastUpdated")]
468 pub last_updated: u64,
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub metadata: Option<Value>,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct DiscoveryResponse {
477 #[serde(rename = "x402Version")]
479 pub x402_version: u32,
480 pub items: Vec<DiscoveryResource>,
482 pub pagination: PaginationInfo,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct PaginationInfo {
489 pub limit: u32,
491 pub offset: u32,
493 pub total: u32,
495}
496
497pub mod networks {
499 pub const BASE_MAINNET: &str = "base";
501 pub const BASE_SEPOLIA: &str = "base-sepolia";
503 pub const AVALANCHE_MAINNET: &str = "avalanche";
505 pub const AVALANCHE_FUJI: &str = "avalanche-fuji";
507
508 pub fn get_usdc_address(network: &str) -> Option<&'static str> {
510 match network {
511 BASE_MAINNET => Some("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"),
512 BASE_SEPOLIA => Some("0x036CbD53842c5426634e7929541eC2318f3dCF7e"),
513 AVALANCHE_MAINNET => Some("0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E"),
514 AVALANCHE_FUJI => Some("0x5425890298aed601595a70AB815c96711a31Bc65"),
515 _ => None,
516 }
517 }
518
519 pub fn is_supported(network: &str) -> bool {
521 matches!(
522 network,
523 BASE_MAINNET | BASE_SEPOLIA | AVALANCHE_MAINNET | AVALANCHE_FUJI
524 )
525 }
526
527 pub fn all_supported() -> Vec<&'static str> {
529 vec![
530 BASE_MAINNET,
531 BASE_SEPOLIA,
532 AVALANCHE_MAINNET,
533 AVALANCHE_FUJI,
534 ]
535 }
536}
537
538pub mod schemes {
540 pub const EXACT: &str = "exact";
542}