Skip to main content

x402_rs/proto/
mod.rs

1//! Protocol types for x402 payment messages.
2//!
3//! This module defines the wire format types used in the x402 protocol for
4//! communication between buyers, sellers, and facilitators. It supports both
5//! protocol version 1 (V1) and version 2 (V2).
6//!
7//! # Protocol Versions
8//!
9//! - **V1** ([`v1`]): Original protocol with network names and simpler structure
10//! - **V2** ([`v2`]): Enhanced protocol with CAIP-2 chain IDs and richer metadata
11//!
12//! # Key Types
13//!
14//! - [`SupportedPaymentKind`] - Describes a payment method supported by a facilitator
15//! - [`SupportedResponse`] - Response from facilitator's `/supported` endpoint
16//! - [`VerifyRequest`] / [`VerifyResponse`] - Payment verification messages
17//! - [`SettleRequest`] / [`SettleResponse`] - Payment settlement messages
18//! - [`PaymentVerificationError`] - Errors that can occur during verification
19//! - [`PaymentProblem`] - Structured error response for payment failures
20//!
21//! # Wire Format
22//!
23//! All types serialize to JSON using camelCase field names. The protocol version
24//! is indicated by the `x402Version` field in payment payloads.
25
26use serde::{Deserialize, Serialize};
27use serde_with::{VecSkipError, serde_as};
28use std::collections::HashMap;
29use std::str::FromStr;
30
31use crate::chain::ChainId;
32use crate::scheme::SchemeHandlerSlug;
33
34pub mod util;
35pub mod v1;
36pub mod v2;
37
38/// Trait for types that have both V1 and V2 protocol variants.
39///
40/// This trait enables generic handling of protocol-versioned types through
41/// the [`ProtocolVersioned`] enum.
42pub trait ProtocolV {
43    /// The V1 protocol variant of this type.
44    type V1;
45    /// The V2 protocol variant of this type.
46    type V2;
47}
48
49/// A versioned protocol type that can be either V1 or V2.
50///
51/// This enum wraps protocol-specific types to allow handling both versions
52/// in a unified way.
53pub enum ProtocolVersioned<T>
54where
55    T: ProtocolV,
56{
57    /// Protocol version 1 variant.
58    #[allow(dead_code)]
59    V1(T::V1),
60    /// Protocol version 2 variant.
61    #[allow(dead_code)]
62    V2(T::V2),
63}
64
65/// Describes a payment method supported by a facilitator.
66///
67/// This type is returned in the [`SupportedResponse`] to indicate what
68/// payment schemes, networks, and protocol versions a facilitator can handle.
69///
70/// # Example
71///
72/// ```json
73/// {
74///   "x402Version": 2,
75///   "scheme": "exact",
76///   "network": "eip155:8453"
77/// }
78/// ```
79#[derive(Clone, Debug, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct SupportedPaymentKind {
82    /// The x402 protocol version (1 or 2).
83    pub x402_version: u8,
84    /// The payment scheme identifier (e.g., "exact").
85    pub scheme: String,
86    /// The network identifier (CAIP-2 chain ID for V2, network name for V1).
87    pub network: String,
88    /// Optional scheme-specific extra data.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub extra: Option<serde_json::Value>,
91}
92
93/// Response from a facilitator's `/supported` endpoint.
94///
95/// This response tells clients what payment methods the facilitator supports,
96/// including protocol versions, schemes, networks, and signer addresses.
97///
98/// # Example
99///
100/// ```json
101/// {
102///   "kinds": [
103///     { "x402Version": 2, "scheme": "exact", "network": "eip155:8453" }
104///   ],
105///   "extensions": [],
106///   "signers": {
107///     "eip155:8453": ["0x1234..."]
108///   }
109/// }
110/// ```
111#[serde_as]
112#[derive(Clone, Default, Debug, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114#[allow(dead_code)] // Public for consumption by downstream crates.
115pub struct SupportedResponse {
116    /// List of supported payment kinds.
117    #[serde_as(as = "VecSkipError<_>")]
118    pub kinds: Vec<SupportedPaymentKind>,
119    /// List of supported protocol extensions.
120    #[serde(default)]
121    pub extensions: Vec<String>,
122    /// Map of chain IDs to signer addresses for that chain.
123    #[serde(default)]
124    pub signers: HashMap<ChainId, Vec<String>>,
125}
126
127/// Request to verify a payment before settlement.
128///
129/// This wrapper contains the payment payload and requirements sent by a client
130/// to a facilitator for verification. The facilitator checks that the payment
131/// authorization is valid, properly signed, and matches the requirements.
132///
133/// The inner JSON structure varies by protocol version and scheme.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct VerifyRequest(serde_json::Value);
136
137/// Request to settle a verified payment on-chain.
138///
139/// This is the same structure as [`VerifyRequest`], containing the payment
140/// payload that was previously verified.
141pub type SettleRequest = VerifyRequest;
142
143impl From<serde_json::Value> for VerifyRequest {
144    fn from(value: serde_json::Value) -> Self {
145        Self(value)
146    }
147}
148
149impl VerifyRequest {
150    /// Consumes the request and returns the inner JSON value.
151    pub fn into_json(self) -> serde_json::Value {
152        self.0
153    }
154
155    /// Extracts the scheme handler slug from the request.
156    ///
157    /// This determines which scheme handler should process this payment
158    /// based on the protocol version, chain ID, and scheme name.
159    ///
160    /// Returns `None` if the request format is invalid or the scheme is unknown.
161    pub fn scheme_handler_slug(&self) -> Option<SchemeHandlerSlug> {
162        let x402_version: u8 = self.0.get("x402Version")?.as_u64()?.try_into().ok()?;
163        match x402_version {
164            v1::X402Version1::VALUE => {
165                let network_name = self.0.get("paymentPayload")?.get("network")?.as_str()?;
166                let chain_id = ChainId::from_network_name(network_name)?;
167                let scheme = self.0.get("paymentPayload")?.get("scheme")?.as_str()?;
168                let slug = SchemeHandlerSlug::new(chain_id, 1, scheme.into());
169                Some(slug)
170            }
171            v2::X402Version2::VALUE => {
172                let chain_id_string = self
173                    .0
174                    .get("paymentPayload")?
175                    .get("accepted")?
176                    .get("network")?
177                    .as_str()?;
178                let chain_id = ChainId::from_str(chain_id_string).ok()?;
179                let scheme = self
180                    .0
181                    .get("paymentPayload")?
182                    .get("accepted")?
183                    .get("scheme")?
184                    .as_str()?;
185                let slug = SchemeHandlerSlug::new(chain_id, 2, scheme.into());
186                Some(slug)
187            }
188            _ => None,
189        }
190    }
191}
192
193/// Response from a payment verification request.
194///
195/// Contains the verification result as JSON. The structure varies by
196/// protocol version and scheme.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct VerifyResponse(pub serde_json::Value);
199
200/// Response from a payment settlement request.
201///
202/// Contains the settlement result as JSON, typically including the
203/// transaction hash if settlement was successful.
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct SettleResponse(pub serde_json::Value);
206
207/// Errors that can occur during payment verification.
208///
209/// These errors are returned when a payment fails validation checks
210/// performed by the facilitator before settlement.
211#[derive(Debug, thiserror::Error)]
212pub enum PaymentVerificationError {
213    /// The payment payload format is invalid or malformed.
214    #[error("Invalid format: {0}")]
215    InvalidFormat(String),
216    /// The payment amount doesn't match the requirements.
217    #[error("Payment amount is invalid with respect to the payment requirements")]
218    InvalidPaymentAmount,
219    /// The payment authorization's `validAfter` timestamp is in the future.
220    #[error("Payment authorization is not yet valid")]
221    Early,
222    /// The payment authorization's `validBefore` timestamp has passed.
223    #[error("Payment authorization is expired")]
224    Expired,
225    /// The payment's chain ID doesn't match the requirements.
226    #[error("Payment chain id is invalid with respect to the payment requirements")]
227    ChainIdMismatch,
228    /// The payment recipient doesn't match the requirements.
229    #[error("Payment recipient is invalid with respect to the payment requirements")]
230    RecipientMismatch,
231    /// The payment asset (token) doesn't match the requirements.
232    #[error("Payment asset is invalid with respect to the payment requirements")]
233    AssetMismatch,
234    /// The payer's on-chain balance is insufficient.
235    #[error("Onchain balance is not enough to cover the payment amount")]
236    InsufficientFunds,
237    /// The payment signature is invalid.
238    #[error("{0}")]
239    InvalidSignature(String),
240    /// Transaction simulation failed.
241    #[error("{0}")]
242    TransactionSimulation(String),
243    /// The chain is not supported by this facilitator.
244    #[error("Unsupported chain")]
245    UnsupportedChain,
246    /// The payment scheme is not supported by this facilitator.
247    #[error("Unsupported scheme")]
248    UnsupportedScheme,
249    /// The accepted payment details don't match the requirements.
250    #[error("Accepted does not match payment requirements")]
251    AcceptedRequirementsMismatch,
252}
253
254impl AsPaymentProblem for PaymentVerificationError {
255    fn as_payment_problem(&self) -> PaymentProblem {
256        let error_reason = match self {
257            PaymentVerificationError::InvalidFormat(_) => ErrorReason::InvalidFormat,
258            PaymentVerificationError::InvalidPaymentAmount => ErrorReason::InvalidPaymentAmount,
259            PaymentVerificationError::InsufficientFunds => ErrorReason::InsufficientFunds,
260            PaymentVerificationError::Early => ErrorReason::InvalidPaymentEarly,
261            PaymentVerificationError::Expired => ErrorReason::InvalidPaymentExpired,
262            PaymentVerificationError::ChainIdMismatch => ErrorReason::ChainIdMismatch,
263            PaymentVerificationError::RecipientMismatch => ErrorReason::RecipientMismatch,
264            PaymentVerificationError::AssetMismatch => ErrorReason::AssetMismatch,
265            PaymentVerificationError::InvalidSignature(_) => ErrorReason::InvalidSignature,
266            PaymentVerificationError::TransactionSimulation(_) => {
267                ErrorReason::TransactionSimulation
268            }
269            PaymentVerificationError::UnsupportedChain => ErrorReason::UnsupportedChain,
270            PaymentVerificationError::UnsupportedScheme => ErrorReason::UnsupportedScheme,
271            PaymentVerificationError::AcceptedRequirementsMismatch => {
272                ErrorReason::AcceptedRequirementsMismatch
273            }
274        };
275        PaymentProblem::new(error_reason, self.to_string())
276    }
277}
278
279impl From<serde_json::Error> for PaymentVerificationError {
280    fn from(value: serde_json::Error) -> Self {
281        Self::InvalidFormat(value.to_string())
282    }
283}
284
285/// Machine-readable error reason codes for payment failures.
286///
287/// These codes are used in error responses to allow clients to
288/// programmatically handle different failure scenarios.
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291pub enum ErrorReason {
292    /// The payment payload format is invalid.
293    InvalidFormat,
294    /// The payment amount is incorrect.
295    InvalidPaymentAmount,
296    /// The payment authorization is not yet valid.
297    InvalidPaymentEarly,
298    /// The payment authorization has expired.
299    InvalidPaymentExpired,
300    /// The chain ID doesn't match.
301    ChainIdMismatch,
302    /// The recipient address doesn't match.
303    RecipientMismatch,
304    /// The token asset doesn't match.
305    AssetMismatch,
306    /// The accepted details don't match requirements.
307    AcceptedRequirementsMismatch,
308    /// The signature is invalid.
309    InvalidSignature,
310    /// Transaction simulation failed.
311    TransactionSimulation,
312    /// Insufficient on-chain balance.
313    InsufficientFunds,
314    /// The chain is not supported.
315    UnsupportedChain,
316    /// The scheme is not supported.
317    UnsupportedScheme,
318    /// An unexpected error occurred.
319    UnexpectedError,
320}
321
322/// Trait for converting errors into structured payment problems.
323pub trait AsPaymentProblem {
324    /// Converts this error into a [`PaymentProblem`].
325    fn as_payment_problem(&self) -> PaymentProblem;
326}
327
328/// A structured payment error with reason code and details.
329///
330/// This type is used to return detailed error information to clients
331/// when a payment fails verification or settlement.
332pub struct PaymentProblem {
333    /// The machine-readable error reason.
334    reason: ErrorReason,
335    /// Human-readable error details.
336    details: String,
337}
338
339impl PaymentProblem {
340    /// Creates a new payment problem with the given reason and details.
341    pub fn new(reason: ErrorReason, details: String) -> Self {
342        Self { reason, details }
343    }
344
345    /// Returns the error reason code.
346    pub fn reason(&self) -> ErrorReason {
347        self.reason
348    }
349
350    /// Returns the human-readable error details.
351    pub fn details(&self) -> &str {
352        &self.details
353    }
354}
355
356/// Protocol version marker for [`PaymentRequired`] responses.
357pub struct PaymentRequiredV;
358
359impl ProtocolV for PaymentRequiredV {
360    type V1 = v1::PaymentRequired;
361    type V2 = v2::PaymentRequired;
362}
363
364/// A payment required response that can be either V1 or V2.
365///
366/// This is returned with HTTP 402 status to indicate that payment is required.
367pub type PaymentRequired = ProtocolVersioned<PaymentRequiredV>;