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>;