Skip to main content

r402_http/
types.rs

1//! HTTP-specific types for the x402 payment protocol server middleware.
2//!
3//! Provides route configuration, payment options, request context, and
4//! processing result types used by [`super::server::PaymentGate`].
5//!
6//! Corresponds to Python SDK's `http/types.py`.
7
8use r402::proto::{PaymentPayload, PaymentRequirements};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12/// A payment option accepted by a protected route.
13///
14/// Defines a (scheme, network) pair along with price and recipient for a
15/// single payment method accepted at an endpoint.
16///
17/// Corresponds to Python SDK's `PaymentOption`.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct PaymentOption {
21    /// Payment scheme identifier (e.g., `"exact"`).
22    pub scheme: String,
23
24    /// Recipient address (e.g., `"0x..."`).
25    pub pay_to: String,
26
27    /// Price — a money string (e.g., `"1.50"`) or structured amount.
28    pub price: Value,
29
30    /// CAIP-2 network identifier (e.g., `"eip155:8453"`).
31    pub network: String,
32
33    /// Maximum payment validity in seconds (defaults to 300).
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub max_timeout_seconds: Option<u64>,
36
37    /// Scheme-specific extra data.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub extra: Option<Value>,
40}
41
42/// Configuration for a payment-protected route.
43///
44/// Specifies which payment options a route accepts, along with optional
45/// metadata for resource description and paywall customisation.
46///
47/// Corresponds to Python SDK's `RouteConfig`.
48#[derive(Debug, Clone)]
49pub struct RouteConfig {
50    /// Accepted payment options for this route.
51    pub accepts: Vec<PaymentOption>,
52
53    /// Override resource URL (defaults to request URL).
54    pub resource: Option<String>,
55
56    /// Human-readable description of the resource.
57    pub description: Option<String>,
58
59    /// MIME type of the resource.
60    pub mime_type: Option<String>,
61}
62
63impl RouteConfig {
64    /// Creates a new route config with a single payment option.
65    #[must_use]
66    pub fn single(option: PaymentOption) -> Self {
67        Self {
68            accepts: vec![option],
69            resource: None,
70            description: None,
71            mime_type: None,
72        }
73    }
74
75    /// Creates a new route config with multiple payment options.
76    #[must_use]
77    pub fn multi(options: Vec<PaymentOption>) -> Self {
78        Self {
79            accepts: options,
80            resource: None,
81            description: None,
82            mime_type: None,
83        }
84    }
85
86    /// Sets the resource URL override.
87    #[must_use]
88    pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
89        self.resource = Some(resource.into());
90        self
91    }
92
93    /// Sets the resource description.
94    #[must_use]
95    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
96        self.description = Some(desc.into());
97        self
98    }
99
100    /// Sets the MIME type.
101    #[must_use]
102    pub fn with_mime_type(mut self, mime: impl Into<String>) -> Self {
103        self.mime_type = Some(mime.into());
104        self
105    }
106}
107
108/// Result of processing an HTTP request through the payment gate.
109///
110/// Corresponds to Python SDK's `HTTPProcessResult`.
111#[derive(Debug)]
112pub enum ProcessResult {
113    /// Route does not require payment — pass through to inner service.
114    NoPaymentRequired,
115
116    /// Payment verified successfully.
117    PaymentVerified {
118        /// The verified payment payload.
119        payload: PaymentPayload,
120        /// The matching payment requirements.
121        requirements: PaymentRequirements,
122    },
123
124    /// Payment error — return 402 or error response.
125    PaymentError {
126        /// HTTP status code (typically 402 or 500).
127        status: u16,
128        /// Response headers to include.
129        headers: Vec<(String, String)>,
130        /// JSON response body.
131        body: Value,
132    },
133}
134
135/// Result of settlement processing after a successful response.
136///
137/// Corresponds to Python SDK's `ProcessSettleResult`.
138#[derive(Debug)]
139pub struct SettleResult {
140    /// Whether settlement succeeded.
141    pub success: bool,
142    /// Error reason if settlement failed.
143    pub error_reason: Option<String>,
144    /// Headers to add to the response (e.g., `PAYMENT-RESPONSE`).
145    pub headers: Vec<(String, String)>,
146    /// Transaction hash/ID.
147    pub transaction: Option<String>,
148    /// Network identifier.
149    pub network: Option<String>,
150    /// Payer address.
151    pub payer: Option<String>,
152}
153
154/// Paywall UI configuration for browser-based 402 responses.
155///
156/// Corresponds to Python SDK's `PaywallConfig` in `http/types.py`.
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct PaywallConfig {
160    /// Application name to display.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub app_name: Option<String>,
163
164    /// URL to application logo.
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub app_logo: Option<String>,
167
168    /// Whether this is a testnet deployment.
169    #[serde(default)]
170    pub testnet: bool,
171}
172
173/// A validation error for a route configuration.
174///
175/// Returned by [`super::server::PaymentGateLayer::validate_routes`] when a
176/// payment option references an unregistered scheme or unsupported
177/// facilitator combination.
178///
179/// Corresponds to Python SDK's `RouteValidationError` in `http/types.py`.
180#[derive(Debug, Clone)]
181pub struct RouteValidationError {
182    /// The route pattern (e.g., `"GET /weather"`).
183    pub route_pattern: String,
184    /// Scheme identifier (e.g., `"exact"`).
185    pub scheme: String,
186    /// CAIP-2 network identifier.
187    pub network: String,
188    /// Reason code (`"missing_scheme"` or `"missing_facilitator"`).
189    pub reason: String,
190    /// Human-readable error message.
191    pub message: String,
192}
193
194impl std::fmt::Display for RouteValidationError {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        write!(f, "{}", self.message)
197    }
198}
199
200/// A compiled route entry mapping a method + path pattern to its config.
201#[derive(Debug, Clone)]
202pub(crate) struct CompiledRoute {
203    /// HTTP method (uppercase) or `"*"` for any method.
204    pub method: String,
205    /// Path pattern (e.g., `/weather`, `/api/*`).
206    pub path_pattern: String,
207    /// Payment configuration for this route.
208    pub config: RouteConfig,
209}
210
211impl CompiledRoute {
212    /// Checks whether this route matches the given method and path.
213    pub fn matches(&self, method: &str, path: &str) -> bool {
214        // Method match
215        if self.method != "*" && !self.method.eq_ignore_ascii_case(method) {
216            return false;
217        }
218
219        // Path match with simple glob support
220        match_path_pattern(&self.path_pattern, path)
221    }
222}
223
224/// Simple glob-style path matching.
225///
226/// Supports:
227/// - Exact match: `/weather` matches `/weather`
228/// - Trailing wildcard: `/api/*` matches `/api/foo` and `/api/foo/bar`
229/// - Full wildcard: `*` matches everything
230fn match_path_pattern(pattern: &str, path: &str) -> bool {
231    if pattern == "*" {
232        return true;
233    }
234
235    let normalized_path = path.split('?').next().unwrap_or(path);
236    let normalized_path = normalized_path.trim_end_matches('/');
237    let normalized_pattern = pattern.trim_end_matches('/');
238
239    if normalized_pattern.ends_with("/*") {
240        let prefix = &normalized_pattern[..normalized_pattern.len() - 2];
241        normalized_path == prefix || normalized_path.starts_with(&format!("{prefix}/"))
242    } else {
243        normalized_path.eq_ignore_ascii_case(normalized_pattern)
244    }
245}
246
247/// Parses a route pattern string into method + path.
248///
249/// Supports formats:
250/// - `"GET /weather"` → method=`GET`, path=`/weather`
251/// - `"/weather"` → method=`*`, path=`/weather`
252/// - `"*"` → method=`*`, path=`*`
253pub(crate) fn parse_route_pattern(pattern: &str) -> (String, String) {
254    let trimmed = pattern.trim();
255    if let Some((method, path)) = trimmed.split_once(char::is_whitespace) {
256        (method.to_uppercase(), path.trim().to_owned())
257    } else {
258        ("*".to_owned(), trimmed.to_owned())
259    }
260}