rust_x402/types/
payment.rs

1//! Payment-related types
2
3use super::network::Network;
4use chrono::Utc;
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9use std::time::Duration;
10
11/// x402 protocol version
12pub const X402_VERSION: u32 = 1;
13
14/// Payment requirements for a resource
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PaymentRequirements {
17    /// Payment scheme identifier (e.g., "exact")
18    pub scheme: String,
19    /// Blockchain network identifier (e.g., "base-sepolia", "ethereum-mainnet")
20    pub network: String,
21    /// Required payment amount in atomic token units
22    #[serde(rename = "maxAmountRequired")]
23    pub max_amount_required: String,
24    /// Token contract address
25    pub asset: String,
26    /// Recipient wallet address for the payment
27    #[serde(rename = "payTo")]
28    pub pay_to: String,
29    /// URL of the protected resource
30    pub resource: String,
31    /// Human-readable description of the resource
32    pub description: String,
33    /// MIME type of the expected response
34    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
35    pub mime_type: Option<String>,
36    /// JSON schema describing the response format
37    #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
38    pub output_schema: Option<Value>,
39    /// Maximum time allowed for payment completion in seconds
40    #[serde(rename = "maxTimeoutSeconds")]
41    pub max_timeout_seconds: u32,
42    /// Scheme-specific additional information
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub extra: Option<Value>,
45}
46
47impl PaymentRequirements {
48    /// Create a new payment requirements instance
49    pub fn new(
50        scheme: impl Into<String>,
51        network: impl Into<String>,
52        max_amount_required: impl Into<String>,
53        asset: impl Into<String>,
54        pay_to: impl Into<String>,
55        resource: impl Into<String>,
56        description: impl Into<String>,
57    ) -> Self {
58        Self {
59            scheme: scheme.into(),
60            network: network.into(),
61            max_amount_required: max_amount_required.into(),
62            asset: asset.into(),
63            pay_to: pay_to.into(),
64            resource: resource.into(),
65            description: description.into(),
66            mime_type: None,
67            output_schema: None,
68            max_timeout_seconds: 60,
69            extra: None,
70        }
71    }
72
73    /// Set USDC token information in the extra field
74    pub fn set_usdc_info(&mut self, network: Network) -> crate::Result<()> {
75        let mut usdc_info = HashMap::new();
76        usdc_info.insert("name".to_string(), network.usdc_name().to_string());
77        usdc_info.insert("version".to_string(), "2".to_string());
78
79        self.extra = Some(serde_json::to_value(usdc_info)?);
80        Ok(())
81    }
82
83    /// Get the amount as a decimal
84    pub fn amount_as_decimal(&self) -> crate::Result<Decimal> {
85        self.max_amount_required
86            .parse()
87            .map_err(|_| crate::X402Error::invalid_payment_requirements("Invalid amount format"))
88    }
89
90    /// Get the amount in decimal units (e.g., 0.01 for 1 cent)
91    pub fn amount_in_decimal_units(&self, decimals: u8) -> crate::Result<Decimal> {
92        let amount = self.amount_as_decimal()?;
93        let divisor = Decimal::from(10u64.pow(decimals as u32));
94        Ok(amount / divisor)
95    }
96}
97
98/// Payment payload for client payment authorization
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct PaymentPayload {
101    /// Protocol version identifier
102    #[serde(rename = "x402Version")]
103    pub x402_version: u32,
104    /// Payment scheme identifier
105    pub scheme: String,
106    /// Blockchain network identifier
107    pub network: String,
108    /// Payment data object
109    pub payload: ExactEvmPayload,
110}
111
112impl PaymentPayload {
113    /// Create a new payment payload
114    pub fn new(
115        scheme: impl Into<String>,
116        network: impl Into<String>,
117        payload: ExactEvmPayload,
118    ) -> Self {
119        Self {
120            x402_version: X402_VERSION,
121            scheme: scheme.into(),
122            network: network.into(),
123            payload,
124        }
125    }
126
127    /// Decode a base64-encoded payment payload
128    pub fn from_base64(encoded: &str) -> crate::Result<Self> {
129        use base64::{engine::general_purpose, Engine as _};
130        let decoded = general_purpose::STANDARD.decode(encoded)?;
131        let payload: PaymentPayload = serde_json::from_slice(&decoded)?;
132        Ok(payload)
133    }
134
135    /// Encode the payment payload to base64
136    pub fn to_base64(&self) -> crate::Result<String> {
137        use base64::{engine::general_purpose, Engine as _};
138        let json = serde_json::to_string(self)?;
139        Ok(general_purpose::STANDARD.encode(json))
140    }
141}
142
143/// Exact EVM payment payload (EIP-3009)
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ExactEvmPayload {
146    /// EIP-712 signature for authorization
147    pub signature: String,
148    /// EIP-3009 authorization parameters
149    pub authorization: ExactEvmPayloadAuthorization,
150}
151
152/// EIP-3009 authorization parameters
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ExactEvmPayloadAuthorization {
155    /// Payer's wallet address
156    pub from: String,
157    /// Recipient's wallet address
158    pub to: String,
159    /// Payment amount in atomic units
160    pub value: String,
161    /// Unix timestamp when authorization becomes valid
162    #[serde(rename = "validAfter")]
163    pub valid_after: String,
164    /// Unix timestamp when authorization expires
165    #[serde(rename = "validBefore")]
166    pub valid_before: String,
167    /// 32-byte random nonce to prevent replay attacks
168    pub nonce: String,
169}
170
171impl ExactEvmPayloadAuthorization {
172    /// Create a new authorization
173    pub fn new(
174        from: impl Into<String>,
175        to: impl Into<String>,
176        value: impl Into<String>,
177        valid_after: impl Into<String>,
178        valid_before: impl Into<String>,
179        nonce: impl Into<String>,
180    ) -> Self {
181        Self {
182            from: from.into(),
183            to: to.into(),
184            value: value.into(),
185            valid_after: valid_after.into(),
186            valid_before: valid_before.into(),
187            nonce: nonce.into(),
188        }
189    }
190
191    /// Check if the authorization is currently valid
192    pub fn is_valid_now(&self) -> crate::Result<bool> {
193        let now = Utc::now().timestamp();
194        let valid_after: i64 = self.valid_after.parse().map_err(|_| {
195            crate::X402Error::invalid_authorization("Invalid valid_after timestamp")
196        })?;
197        let valid_before: i64 = self.valid_before.parse().map_err(|_| {
198            crate::X402Error::invalid_authorization("Invalid valid_before timestamp")
199        })?;
200
201        Ok(now >= valid_after && now <= valid_before)
202    }
203
204    /// Get the validity duration
205    pub fn validity_duration(&self) -> crate::Result<Duration> {
206        let valid_after: i64 = self.valid_after.parse().map_err(|_| {
207            crate::X402Error::invalid_authorization("Invalid valid_after timestamp")
208        })?;
209        let valid_before: i64 = self.valid_before.parse().map_err(|_| {
210            crate::X402Error::invalid_authorization("Invalid valid_before timestamp")
211        })?;
212
213        Ok(Duration::from_secs((valid_before - valid_after) as u64))
214    }
215}
216
217/// Payment requirements response (HTTP 402 response)
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PaymentRequirementsResponse {
220    /// Protocol version
221    #[serde(rename = "x402Version")]
222    pub x402_version: u32,
223    /// Human-readable error message
224    pub error: String,
225    /// Array of acceptable payment methods
226    pub accepts: Vec<PaymentRequirements>,
227}
228
229impl PaymentRequirementsResponse {
230    /// Create a new payment requirements response
231    pub fn new(error: impl Into<String>, accepts: Vec<PaymentRequirements>) -> Self {
232        Self {
233            x402_version: X402_VERSION,
234            error: error.into(),
235            accepts,
236        }
237    }
238}