rust_x402/
types.rs

1//! Core types for the x402 protocol
2
3use 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
11/// Type alias for authentication headers function
12pub type AuthHeadersFn =
13    dyn Fn() -> crate::Result<HashMap<String, HashMap<String, String>>> + Send + Sync;
14
15/// Type alias for authentication headers function wrapped in Arc
16pub type AuthHeadersFnArc = Arc<AuthHeadersFn>;
17
18/// Type alias for authentication headers function wrapped in Box
19pub type AuthHeadersFnBox = Box<AuthHeadersFn>;
20
21/// x402 protocol version
22pub const X402_VERSION: u32 = 1;
23
24/// Network configuration for x402 payments
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Network {
27    Mainnet,
28    Testnet,
29}
30
31/// Network configuration with chain-specific details
32#[derive(Debug, Clone)]
33pub struct NetworkConfig {
34    /// Chain ID for the network
35    pub chain_id: u64,
36    /// USDC contract address
37    pub usdc_contract: String,
38    /// Network name
39    pub name: String,
40    /// Whether this is a testnet
41    pub is_testnet: bool,
42}
43
44impl NetworkConfig {
45    /// Base mainnet configuration
46    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    /// Base Sepolia testnet configuration
56    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    /// Get network config by name
66    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    /// Get the network identifier string
77    pub fn as_str(&self) -> &'static str {
78        match self {
79            Network::Mainnet => "base",
80            Network::Testnet => "base-sepolia",
81        }
82    }
83
84    /// Get the USDC contract address for this network
85    pub fn usdc_address(&self) -> &'static str {
86        match self {
87            Network::Mainnet => "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
88            Network::Testnet => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
89        }
90    }
91
92    /// Get the USDC token name for this network
93    pub fn usdc_name(&self) -> &'static str {
94        match self {
95            Network::Mainnet => "USD Coin",
96            Network::Testnet => "USDC",
97        }
98    }
99}
100
101/// Payment requirements for a resource
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct PaymentRequirements {
104    /// Payment scheme identifier (e.g., "exact")
105    pub scheme: String,
106    /// Blockchain network identifier (e.g., "base-sepolia", "ethereum-mainnet")
107    pub network: String,
108    /// Required payment amount in atomic token units
109    #[serde(rename = "maxAmountRequired")]
110    pub max_amount_required: String,
111    /// Token contract address
112    pub asset: String,
113    /// Recipient wallet address for the payment
114    #[serde(rename = "payTo")]
115    pub pay_to: String,
116    /// URL of the protected resource
117    pub resource: String,
118    /// Human-readable description of the resource
119    pub description: String,
120    /// MIME type of the expected response
121    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
122    pub mime_type: Option<String>,
123    /// JSON schema describing the response format
124    #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
125    pub output_schema: Option<Value>,
126    /// Maximum time allowed for payment completion in seconds
127    #[serde(rename = "maxTimeoutSeconds")]
128    pub max_timeout_seconds: u32,
129    /// Scheme-specific additional information
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub extra: Option<Value>,
132}
133
134impl PaymentRequirements {
135    /// Create a new payment requirements instance
136    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    /// Set USDC token information in the extra field
161    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    /// Get the amount as a decimal
171    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    /// Get the amount in decimal units (e.g., 0.01 for 1 cent)
178    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/// Payment payload for client payment authorization
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct PaymentPayload {
188    /// Protocol version identifier
189    #[serde(rename = "x402Version")]
190    pub x402_version: u32,
191    /// Payment scheme identifier
192    pub scheme: String,
193    /// Blockchain network identifier
194    pub network: String,
195    /// Payment data object
196    pub payload: ExactEvmPayload,
197}
198
199impl PaymentPayload {
200    /// Create a new payment payload
201    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    /// Decode a base64-encoded payment payload
215    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    /// Encode the payment payload to base64
223    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/// Exact EVM payment payload (EIP-3009)
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct ExactEvmPayload {
233    /// EIP-712 signature for authorization
234    pub signature: String,
235    /// EIP-3009 authorization parameters
236    pub authorization: ExactEvmPayloadAuthorization,
237}
238
239/// EIP-3009 authorization parameters
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ExactEvmPayloadAuthorization {
242    /// Payer's wallet address
243    pub from: String,
244    /// Recipient's wallet address
245    pub to: String,
246    /// Payment amount in atomic units
247    pub value: String,
248    /// Unix timestamp when authorization becomes valid
249    #[serde(rename = "validAfter")]
250    pub valid_after: String,
251    /// Unix timestamp when authorization expires
252    #[serde(rename = "validBefore")]
253    pub valid_before: String,
254    /// 32-byte random nonce to prevent replay attacks
255    pub nonce: String,
256}
257
258impl ExactEvmPayloadAuthorization {
259    /// Create a new authorization
260    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    /// Check if the authorization is currently valid
279    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    /// Get the validity duration
292    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/// Payment verification response
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct VerifyResponse {
307    /// Whether the payment is valid
308    #[serde(rename = "isValid")]
309    pub is_valid: bool,
310    /// Reason for invalidity (if applicable)
311    #[serde(rename = "invalidReason", skip_serializing_if = "Option::is_none")]
312    pub invalid_reason: Option<String>,
313    /// Payer's address
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub payer: Option<String>,
316}
317
318/// Payment settlement response
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct SettleResponse {
321    /// Whether the settlement was successful
322    pub success: bool,
323    /// Error reason if settlement failed
324    #[serde(rename = "errorReason", skip_serializing_if = "Option::is_none")]
325    pub error_reason: Option<String>,
326    /// Transaction hash or identifier
327    pub transaction: String,
328    /// Network where the transaction was executed
329    pub network: String,
330    /// Payer address if applicable
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub payer: Option<String>,
333}
334
335impl SettleResponse {
336    /// Encode the settle response to base64
337    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/// Facilitator configuration
345#[derive(Clone)]
346pub struct FacilitatorConfig {
347    /// Base URL of the facilitator service
348    pub url: String,
349    /// Request timeout
350    pub timeout: Option<Duration>,
351    /// Function to create authentication headers
352    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    /// Create a new facilitator config
367    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    /// Validate the facilitator configuration
376    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    /// Set the request timeout
391    pub fn with_timeout(mut self, timeout: Duration) -> Self {
392        self.timeout = Some(timeout);
393        self
394    }
395
396    /// Set the auth headers creator
397    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/// Payment requirements response (HTTP 402 response)
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct PaymentRequirementsResponse {
412    /// Protocol version
413    #[serde(rename = "x402Version")]
414    pub x402_version: u32,
415    /// Human-readable error message
416    pub error: String,
417    /// Array of acceptable payment methods
418    pub accepts: Vec<PaymentRequirements>,
419}
420
421impl PaymentRequirementsResponse {
422    /// Create a new payment requirements response
423    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/// Supported payment schemes and networks
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct SupportedKinds {
435    /// List of supported payment schemes and networks
436    pub kinds: Vec<SupportedKind>,
437}
438
439/// Individual supported payment scheme and network
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct SupportedKind {
442    /// Protocol version
443    #[serde(rename = "x402Version")]
444    pub x402_version: u32,
445    /// Payment scheme identifier
446    pub scheme: String,
447    /// Blockchain network identifier
448    pub network: String,
449    /// Additional metadata provided by the facilitator
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub metadata: Option<Value>,
452}
453
454/// Discovery API resource
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct DiscoveryResource {
457    /// The resource URL or identifier
458    pub resource: String,
459    /// Resource type (e.g., "http")
460    pub r#type: String,
461    /// Protocol version supported by the resource
462    #[serde(rename = "x402Version")]
463    pub x402_version: u32,
464    /// Payment requirements for this resource
465    pub accepts: Vec<PaymentRequirements>,
466    /// Unix timestamp of when the resource was last updated
467    #[serde(rename = "lastUpdated")]
468    pub last_updated: u64,
469    /// Additional metadata
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub metadata: Option<Value>,
472}
473
474/// Discovery API response
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct DiscoveryResponse {
477    /// Protocol version
478    #[serde(rename = "x402Version")]
479    pub x402_version: u32,
480    /// List of discoverable resources
481    pub items: Vec<DiscoveryResource>,
482    /// Pagination information
483    pub pagination: PaginationInfo,
484}
485
486/// Pagination information
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct PaginationInfo {
489    /// Maximum number of results
490    pub limit: u32,
491    /// Number of results skipped
492    pub offset: u32,
493    /// Total number of results
494    pub total: u32,
495}
496
497/// Common network configurations
498pub mod networks {
499    /// Base mainnet configuration
500    pub const BASE_MAINNET: &str = "base";
501    /// Base Sepolia testnet configuration
502    pub const BASE_SEPOLIA: &str = "base-sepolia";
503    /// Avalanche mainnet configuration
504    pub const AVALANCHE_MAINNET: &str = "avalanche";
505    /// Avalanche Fuji testnet configuration
506    pub const AVALANCHE_FUJI: &str = "avalanche-fuji";
507
508    /// Get USDC contract address for a network
509    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    /// Check if a network is supported
520    pub fn is_supported(network: &str) -> bool {
521        matches!(
522            network,
523            BASE_MAINNET | BASE_SEPOLIA | AVALANCHE_MAINNET | AVALANCHE_FUJI
524        )
525    }
526
527    /// Get all supported networks
528    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
538/// Common payment schemes
539pub mod schemes {
540    /// Exact payment scheme (EIP-3009)
541    pub const EXACT: &str = "exact";
542}