use super::config::PaymentMiddlewareConfig;
use crate::types::{
PaymentPayload, PaymentRequirements, PaymentRequirementsResponse, SettleResponse,
};
use crate::{Result, X402Error};
use axum::{
extract::{Request, State},
http::{HeaderValue, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct PaymentMiddleware {
pub config: Arc<PaymentMiddlewareConfig>,
pub facilitator: Option<crate::facilitator::FacilitatorClient>,
pub template_config: Option<crate::template::PaywallConfig>,
}
#[derive(Debug)]
pub enum PaymentResult {
Success {
response: axum::response::Response,
settlement: SettleResponse,
},
PaymentRequired { response: axum::response::Response },
VerificationFailed { response: axum::response::Response },
SettlementFailed { response: axum::response::Response },
}
impl PaymentMiddleware {
pub fn new(amount: rust_decimal::Decimal, pay_to: impl Into<String>) -> Self {
Self {
config: Arc::new(PaymentMiddlewareConfig::new(amount, pay_to)),
facilitator: None,
template_config: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
Arc::make_mut(&mut self.config).description = Some(description.into());
self
}
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
Arc::make_mut(&mut self.config).mime_type = Some(mime_type.into());
self
}
pub fn with_max_timeout_seconds(mut self, max_timeout_seconds: u32) -> Self {
Arc::make_mut(&mut self.config).max_timeout_seconds = max_timeout_seconds;
self
}
pub fn with_output_schema(mut self, output_schema: serde_json::Value) -> Self {
Arc::make_mut(&mut self.config).output_schema = Some(output_schema);
self
}
pub fn with_facilitator_config(
mut self,
facilitator_config: crate::types::FacilitatorConfig,
) -> Self {
Arc::make_mut(&mut self.config).facilitator_config = facilitator_config;
self
}
pub fn with_testnet(mut self, testnet: bool) -> Self {
Arc::make_mut(&mut self.config).testnet = testnet;
self
}
pub fn with_custom_paywall_html(mut self, html: impl Into<String>) -> Self {
Arc::make_mut(&mut self.config).custom_paywall_html = Some(html.into());
self
}
pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
Arc::make_mut(&mut self.config).resource = Some(resource.into());
self
}
pub fn with_resource_root_url(mut self, url: impl Into<String>) -> Self {
Arc::make_mut(&mut self.config).resource_root_url = Some(url.into());
self
}
pub fn config(&self) -> &PaymentMiddlewareConfig {
&self.config
}
pub fn with_facilitator(mut self, facilitator: crate::facilitator::FacilitatorClient) -> Self {
self.facilitator = Some(facilitator);
self
}
pub fn with_template_config(mut self, template_config: crate::template::PaywallConfig) -> Self {
self.template_config = Some(template_config);
self
}
pub async fn verify(&self, payment_payload: &PaymentPayload) -> bool {
let facilitator = if let Some(facilitator) = &self.facilitator {
facilitator.clone()
} else {
match crate::facilitator::FacilitatorClient::new(self.config.facilitator_config.clone())
{
Ok(facilitator) => facilitator,
Err(_) => return false,
}
};
if let Ok(requirements) = self.config.create_payment_requirements("/") {
if let Ok(response) = facilitator.verify(payment_payload, &requirements).await {
return response.is_valid;
}
}
false
}
pub async fn settle(&self, payment_payload: &PaymentPayload) -> Result<SettleResponse> {
let facilitator = if let Some(facilitator) = &self.facilitator {
facilitator.clone()
} else {
crate::facilitator::FacilitatorClient::new(self.config.facilitator_config.clone())?
};
let requirements = self.config.create_payment_requirements("/")?;
facilitator.settle(payment_payload, &requirements).await
}
pub async fn verify_with_requirements(
&self,
payment_payload: &PaymentPayload,
requirements: &PaymentRequirements,
) -> Result<bool> {
let facilitator = if let Some(facilitator) = &self.facilitator {
facilitator.clone()
} else {
crate::facilitator::FacilitatorClient::new(self.config.facilitator_config.clone())?
};
let response = facilitator.verify(payment_payload, requirements).await?;
Ok(response.is_valid)
}
pub async fn settle_with_requirements(
&self,
payment_payload: &PaymentPayload,
requirements: &PaymentRequirements,
) -> Result<SettleResponse> {
let facilitator = if let Some(facilitator) = &self.facilitator {
facilitator.clone()
} else {
crate::facilitator::FacilitatorClient::new(self.config.facilitator_config.clone())?
};
facilitator.settle(payment_payload, requirements).await
}
pub async fn process_payment(&self, request: Request, next: Next) -> Result<PaymentResult> {
let headers = request.headers();
let uri = request.uri().to_string();
let user_agent = headers
.get("User-Agent")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let accept = headers
.get("Accept")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let is_web_browser = accept.contains("text/html") && user_agent.contains("Mozilla");
let payment_requirements = self.config.create_payment_requirements(&uri)?;
let payment_header = headers.get("X-PAYMENT").and_then(|v| v.to_str().ok());
match payment_header {
Some(payment_b64) => {
let payment_payload = PaymentPayload::from_base64(payment_b64).map_err(|e| {
X402Error::invalid_payment_payload(format!("Failed to decode payment: {}", e))
})?;
let facilitator = if let Some(facilitator) = &self.facilitator {
facilitator.clone()
} else {
crate::facilitator::FacilitatorClient::new(
self.config.facilitator_config.clone(),
)?
};
let verify_response = facilitator
.verify(&payment_payload, &payment_requirements)
.await
.map_err(|e| {
X402Error::facilitator_error(format!("Payment verification failed: {}", e))
})?;
if !verify_response.is_valid {
let error_response = self.create_payment_required_response(
"Payment verification failed",
&payment_requirements,
is_web_browser,
)?;
return Ok(PaymentResult::VerificationFailed {
response: error_response,
});
}
let mut response = next.run(request).await;
let settle_response = facilitator
.settle(&payment_payload, &payment_requirements)
.await
.map_err(|e| {
X402Error::facilitator_error(format!("Payment settlement failed: {}", e))
})?;
let settlement_header = settle_response.to_base64().map_err(|e| {
X402Error::config(format!("Failed to encode settlement response: {}", e))
})?;
if let Ok(header_value) = HeaderValue::from_str(&settlement_header) {
response
.headers_mut()
.insert("X-PAYMENT-RESPONSE", header_value);
}
Ok(PaymentResult::Success {
response,
settlement: settle_response,
})
}
None => {
let response = self.create_payment_required_response(
"X-PAYMENT header is required",
&payment_requirements,
is_web_browser,
)?;
Ok(PaymentResult::PaymentRequired { response })
}
}
}
fn create_payment_required_response(
&self,
error: &str,
payment_requirements: &PaymentRequirements,
is_web_browser: bool,
) -> Result<axum::response::Response> {
if is_web_browser {
let html = if let Some(custom_html) = &self.config.custom_paywall_html {
custom_html.clone()
} else {
let paywall_config = self.template_config.clone().unwrap_or_else(|| {
crate::template::PaywallConfig::new()
.with_app_name("x402 Service")
.with_app_logo("💰")
});
crate::template::generate_paywall_html(
error,
std::slice::from_ref(payment_requirements),
Some(&paywall_config),
)
};
let response = Response::builder()
.status(StatusCode::PAYMENT_REQUIRED)
.header("Content-Type", "text/html")
.body(html.into())
.map_err(|e| X402Error::config(format!("Failed to create HTML response: {}", e)))?;
Ok(response)
} else {
let payment_response =
PaymentRequirementsResponse::new(error, vec![payment_requirements.clone()]);
Ok(Json(payment_response).into_response())
}
}
}
pub async fn payment_middleware(
State(middleware): State<PaymentMiddleware>,
request: Request,
next: Next,
) -> Result<impl IntoResponse> {
match middleware.process_payment(request, next).await? {
PaymentResult::Success { response, .. } => Ok(response),
PaymentResult::PaymentRequired { response } => Ok(response),
PaymentResult::VerificationFailed { response } => Ok(response),
PaymentResult::SettlementFailed { response } => Ok(response),
}
}