1use base64::{engine::general_purpose, Engine as _};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::errors::{X402Error, X402Result};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct PaymentRequest {
10 pub max_amount_required: String,
12
13 pub asset_type: String,
15
16 pub asset_address: String,
18
19 pub payment_address: String,
21
22 pub network: String,
24
25 pub expires_at: DateTime<Utc>,
27
28 pub nonce: String,
30
31 pub payment_id: String,
33
34 pub resource: String,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub description: Option<String>,
40}
41
42impl PaymentRequest {
43 pub fn new(
45 max_amount_required: String,
46 asset_address: String,
47 payment_address: String,
48 network: String,
49 expires_at: DateTime<Utc>,
50 nonce: String,
51 payment_id: String,
52 resource: String,
53 ) -> Self {
54 Self {
55 max_amount_required,
56 asset_type: "SPL".to_string(),
57 asset_address,
58 payment_address,
59 network,
60 expires_at,
61 nonce,
62 payment_id,
63 resource,
64 description: None,
65 }
66 }
67
68 pub fn with_description(mut self, description: String) -> Self {
70 self.description = Some(description);
71 self
72 }
73
74 pub fn is_expired(&self) -> bool {
76 Utc::now() > self.expires_at
77 }
78
79 pub fn from_json(json: &str) -> X402Result<Self> {
81 serde_json::from_str(json).map_err(|e| {
82 X402Error::InvalidPaymentRequest(format!("Failed to parse payment request: {}", e))
83 })
84 }
85
86 pub fn to_json(&self) -> X402Result<String> {
88 serde_json::to_string(self).map_err(|e| {
89 X402Error::Serialization(format!("Failed to serialize payment request: {}", e))
90 })
91 }
92
93 pub fn to_base64(&self) -> X402Result<String> {
95 let json = self.to_json()?;
96 Ok(general_purpose::STANDARD.encode(json.as_bytes()))
97 }
98
99 pub fn from_base64(encoded: &str) -> X402Result<Self> {
101 let decoded = general_purpose::STANDARD.decode(encoded)?;
102 let json = String::from_utf8(decoded).map_err(|e| {
103 X402Error::InvalidPaymentRequest(format!("Invalid UTF-8 in base64 data: {}", e))
104 })?;
105 Self::from_json(&json)
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111pub struct PaymentAuthorization {
112 pub payment_id: String,
114
115 pub actual_amount: String,
117
118 pub payment_address: String,
120
121 pub asset_address: String,
123
124 pub network: String,
126
127 pub timestamp: DateTime<Utc>,
129
130 pub signature: String,
132
133 pub public_key: String,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub transaction_hash: Option<String>,
139}
140
141impl PaymentAuthorization {
142 pub fn new(
144 payment_id: String,
145 actual_amount: String,
146 payment_address: String,
147 asset_address: String,
148 network: String,
149 signature: String,
150 public_key: String,
151 ) -> Self {
152 Self {
153 payment_id,
154 actual_amount,
155 payment_address,
156 asset_address,
157 network,
158 timestamp: Utc::now(),
159 signature: signature.clone(),
160 public_key,
161 transaction_hash: Some(signature),
162 }
163 }
164
165 pub fn from_json(json: &str) -> X402Result<Self> {
167 serde_json::from_str(json).map_err(|e| {
168 X402Error::InvalidPaymentAuthorization(format!(
169 "Failed to parse payment authorization: {}",
170 e
171 ))
172 })
173 }
174
175 pub fn to_json(&self) -> X402Result<String> {
177 serde_json::to_string(self).map_err(|e| {
178 X402Error::Serialization(format!("Failed to serialize payment authorization: {}", e))
179 })
180 }
181
182 pub fn to_header_value(&self) -> X402Result<String> {
184 let json = self.to_json()?;
185 Ok(general_purpose::STANDARD.encode(json.as_bytes()))
186 }
187
188 pub fn from_header_value(encoded: &str) -> X402Result<Self> {
190 let decoded = general_purpose::STANDARD.decode(encoded)?;
191 let json = String::from_utf8(decoded).map_err(|e| {
192 X402Error::InvalidPaymentAuthorization(format!("Invalid UTF-8 in header: {}", e))
193 })?;
194 Self::from_json(&json)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use chrono::Duration;
202
203 #[test]
204 fn test_payment_request_serialization() {
205 let expires_at = Utc::now() + Duration::seconds(300);
206 let request = PaymentRequest::new(
207 "0.10".to_string(),
208 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
209 "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
210 "solana-devnet".to_string(),
211 expires_at,
212 "nonce123".to_string(),
213 "payment123".to_string(),
214 "/api/premium-data".to_string(),
215 )
216 .with_description("Access premium data".to_string());
217
218 let json = request.to_json().unwrap();
219 let deserialized = PaymentRequest::from_json(&json).unwrap();
220 assert_eq!(request, deserialized);
221 }
222
223 #[test]
224 fn test_payment_request_base64() {
225 let expires_at = Utc::now() + Duration::seconds(300);
226 let request = PaymentRequest::new(
227 "0.10".to_string(),
228 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
229 "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
230 "solana-devnet".to_string(),
231 expires_at,
232 "nonce123".to_string(),
233 "payment123".to_string(),
234 "/api/premium-data".to_string(),
235 );
236
237 let encoded = request.to_base64().unwrap();
238 let decoded = PaymentRequest::from_base64(&encoded).unwrap();
239 assert_eq!(request, decoded);
240 }
241
242 #[test]
243 fn test_payment_request_expiration() {
244 let past = Utc::now() - Duration::seconds(10);
245 let request = PaymentRequest::new(
246 "0.10".to_string(),
247 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
248 "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
249 "solana-devnet".to_string(),
250 past,
251 "nonce123".to_string(),
252 "payment123".to_string(),
253 "/api/premium-data".to_string(),
254 );
255
256 assert!(request.is_expired());
257
258 let future = Utc::now() + Duration::seconds(300);
259 let request2 = PaymentRequest::new(
260 "0.10".to_string(),
261 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
262 "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
263 "solana-devnet".to_string(),
264 future,
265 "nonce123".to_string(),
266 "payment123".to_string(),
267 "/api/premium-data".to_string(),
268 );
269
270 assert!(!request2.is_expired());
271 }
272
273 #[test]
274 fn test_payment_authorization_header() {
275 let auth = PaymentAuthorization::new(
276 "payment123".to_string(),
277 "0.10".to_string(),
278 "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
279 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
280 "solana-devnet".to_string(),
281 "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW".to_string(),
282 "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
283 );
284
285 let header = auth.to_header_value().unwrap();
286 let decoded = PaymentAuthorization::from_header_value(&header).unwrap();
287
288 assert_eq!(auth.payment_id, decoded.payment_id);
289 assert_eq!(auth.signature, decoded.signature);
290 assert_eq!(auth.public_key, decoded.public_key);
291 }
292}