use base64::{engine::general_purpose, Engine as _};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::errors::{X402Error, X402Result};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PaymentRequest {
pub max_amount_required: String,
pub asset_type: String,
pub asset_address: String,
pub payment_address: String,
pub network: String,
pub expires_at: DateTime<Utc>,
pub nonce: String,
pub payment_id: String,
pub resource: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl PaymentRequest {
pub fn new(
max_amount_required: String,
asset_address: String,
payment_address: String,
network: String,
expires_at: DateTime<Utc>,
nonce: String,
payment_id: String,
resource: String,
) -> Self {
Self {
max_amount_required,
asset_type: "SPL".to_string(),
asset_address,
payment_address,
network,
expires_at,
nonce,
payment_id,
resource,
description: None,
}
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub fn from_json(json: &str) -> X402Result<Self> {
serde_json::from_str(json).map_err(|e| {
X402Error::InvalidPaymentRequest(format!("Failed to parse payment request: {}", e))
})
}
pub fn to_json(&self) -> X402Result<String> {
serde_json::to_string(self).map_err(|e| {
X402Error::Serialization(format!("Failed to serialize payment request: {}", e))
})
}
pub fn to_base64(&self) -> X402Result<String> {
let json = self.to_json()?;
Ok(general_purpose::STANDARD.encode(json.as_bytes()))
}
pub fn from_base64(encoded: &str) -> X402Result<Self> {
let decoded = general_purpose::STANDARD.decode(encoded)?;
let json = String::from_utf8(decoded).map_err(|e| {
X402Error::InvalidPaymentRequest(format!("Invalid UTF-8 in base64 data: {}", e))
})?;
Self::from_json(&json)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PaymentAuthorization {
pub payment_id: String,
pub actual_amount: String,
pub payment_address: String,
pub asset_address: String,
pub network: String,
pub timestamp: DateTime<Utc>,
pub signature: String,
pub public_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub transaction_hash: Option<String>,
}
impl PaymentAuthorization {
pub fn new(
payment_id: String,
actual_amount: String,
payment_address: String,
asset_address: String,
network: String,
signature: String,
public_key: String,
) -> Self {
Self {
payment_id,
actual_amount,
payment_address,
asset_address,
network,
timestamp: Utc::now(),
signature: signature.clone(),
public_key,
transaction_hash: Some(signature),
}
}
pub fn from_json(json: &str) -> X402Result<Self> {
serde_json::from_str(json).map_err(|e| {
X402Error::InvalidPaymentAuthorization(format!(
"Failed to parse payment authorization: {}",
e
))
})
}
pub fn to_json(&self) -> X402Result<String> {
serde_json::to_string(self).map_err(|e| {
X402Error::Serialization(format!("Failed to serialize payment authorization: {}", e))
})
}
pub fn to_header_value(&self) -> X402Result<String> {
let json = self.to_json()?;
Ok(general_purpose::STANDARD.encode(json.as_bytes()))
}
pub fn from_header_value(encoded: &str) -> X402Result<Self> {
let decoded = general_purpose::STANDARD.decode(encoded)?;
let json = String::from_utf8(decoded).map_err(|e| {
X402Error::InvalidPaymentAuthorization(format!("Invalid UTF-8 in header: {}", e))
})?;
Self::from_json(&json)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_payment_request_serialization() {
let expires_at = Utc::now() + Duration::seconds(300);
let request = PaymentRequest::new(
"0.10".to_string(),
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
"solana-devnet".to_string(),
expires_at,
"nonce123".to_string(),
"payment123".to_string(),
"/api/premium-data".to_string(),
)
.with_description("Access premium data".to_string());
let json = request.to_json().unwrap();
let deserialized = PaymentRequest::from_json(&json).unwrap();
assert_eq!(request, deserialized);
}
#[test]
fn test_payment_request_base64() {
let expires_at = Utc::now() + Duration::seconds(300);
let request = PaymentRequest::new(
"0.10".to_string(),
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
"solana-devnet".to_string(),
expires_at,
"nonce123".to_string(),
"payment123".to_string(),
"/api/premium-data".to_string(),
);
let encoded = request.to_base64().unwrap();
let decoded = PaymentRequest::from_base64(&encoded).unwrap();
assert_eq!(request, decoded);
}
#[test]
fn test_payment_request_expiration() {
let past = Utc::now() - Duration::seconds(10);
let request = PaymentRequest::new(
"0.10".to_string(),
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
"solana-devnet".to_string(),
past,
"nonce123".to_string(),
"payment123".to_string(),
"/api/premium-data".to_string(),
);
assert!(request.is_expired());
let future = Utc::now() + Duration::seconds(300);
let request2 = PaymentRequest::new(
"0.10".to_string(),
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
"solana-devnet".to_string(),
future,
"nonce123".to_string(),
"payment123".to_string(),
"/api/premium-data".to_string(),
);
assert!(!request2.is_expired());
}
#[test]
fn test_payment_authorization_header() {
let auth = PaymentAuthorization::new(
"payment123".to_string(),
"0.10".to_string(),
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
"solana-devnet".to_string(),
"5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW".to_string(),
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
);
let header = auth.to_header_value().unwrap();
let decoded = PaymentAuthorization::from_header_value(&header).unwrap();
assert_eq!(auth.payment_id, decoded.payment_id);
assert_eq!(auth.signature, decoded.signature);
assert_eq!(auth.public_key, decoded.public_key);
}
}