use actix_web::{
dev::Payload, error::ResponseError, http::StatusCode, Error, FromRequest, HttpRequest,
HttpResponse,
};
use chrono::{Duration, Utc};
use fly402_core::{PaymentAuthorization, PaymentRequest};
use std::future::{ready, Ready};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct X402Config {
pub payment_address: String,
pub token_mint: String,
pub network: String,
pub rpc_url: Option<String>,
pub auto_verify: bool,
}
#[derive(Debug, Clone)]
pub struct X402State {
pub config: X402Config,
}
#[derive(Debug, Clone)]
pub struct PaymentRequirement {
pub amount: String,
pub description: Option<String>,
pub expires_in: i64,
}
impl PaymentRequirement {
pub fn new(amount: &str) -> Self {
Self {
amount: amount.to_string(),
description: None,
expires_in: 300,
}
}
pub fn with_description(mut self, description: &str) -> Self {
self.description = Some(description.to_string());
self
}
pub fn with_expires_in(mut self, seconds: i64) -> Self {
self.expires_in = seconds;
self
}
}
pub struct PaymentExtractor {
pub authorization: PaymentAuthorization,
}
impl FromRequest for PaymentExtractor {
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let auth_header = match req.headers().get("X-Payment-Authorization") {
Some(h) => match h.to_str() {
Ok(s) => s,
Err(_) => {
return ready(Err(PaymentError::InvalidHeader.into()));
}
},
None => {
return ready(Err(PaymentError::Required.into()));
}
};
match PaymentAuthorization::from_header_value(auth_header) {
Ok(auth) => {
ready(Ok(PaymentExtractor { authorization: auth }))
}
Err(e) => ready(Err(PaymentError::InvalidAuthorization(e.to_string()).into())),
}
}
}
#[derive(Debug)]
pub enum PaymentError {
Required,
InvalidHeader,
InvalidAuthorization(String),
}
impl std::fmt::Display for PaymentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PaymentError::Required => write!(f, "Payment required"),
PaymentError::InvalidHeader => write!(f, "Invalid authorization header"),
PaymentError::InvalidAuthorization(msg) => {
write!(f, "Invalid payment authorization: {}", msg)
}
}
}
}
impl ResponseError for PaymentError {
fn status_code(&self) -> StatusCode {
match self {
PaymentError::Required => StatusCode::PAYMENT_REQUIRED,
PaymentError::InvalidHeader | PaymentError::InvalidAuthorization(_) => {
StatusCode::BAD_REQUEST
}
}
}
fn error_response(&self) -> HttpResponse {
match self {
PaymentError::Required => {
HttpResponse::PaymentRequired().json(serde_json::json!({
"error": "Payment required",
"message": "This endpoint requires payment"
}))
}
_ => HttpResponse::BadRequest().json(serde_json::json!({
"error": self.to_string()
})),
}
}
}
pub fn create_payment_request(
config: &X402Config,
requirement: &PaymentRequirement,
resource: &str,
) -> PaymentRequest {
let expires_at = Utc::now() + Duration::seconds(requirement.expires_in);
let payment_id = Uuid::new_v4().to_string();
let nonce = Uuid::new_v4().to_string();
let mut request = PaymentRequest::new(
requirement.amount.clone(),
config.token_mint.clone(),
config.payment_address.clone(),
config.network.clone(),
expires_at,
nonce,
payment_id,
resource.to_string(),
);
if let Some(desc) = &requirement.description {
request = request.with_description(desc.clone());
}
request
}
pub fn payment_required_response(payment_request: PaymentRequest) -> HttpResponse {
HttpResponse::PaymentRequired().json(payment_request)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_payment_requirement() {
let req = PaymentRequirement::new("0.10")
.with_description("Premium data access")
.with_expires_in(600);
assert_eq!(req.amount, "0.10");
assert_eq!(req.description, Some("Premium data access".to_string()));
assert_eq!(req.expires_in, 600);
}
#[test]
fn test_create_payment_request() {
let config = X402Config {
payment_address: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
token_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
network: "solana-devnet".to_string(),
rpc_url: None,
auto_verify: false,
};
let requirement = PaymentRequirement::new("0.10");
let request = create_payment_request(&config, &requirement, "/api/premium");
assert_eq!(request.max_amount_required, "0.10");
assert_eq!(request.payment_address, config.payment_address);
assert_eq!(request.asset_address, config.token_mint);
}
}