x402_types/proto/v2.rs
1//! Protocol version 2 (V2) types for x402.
2//!
3//! This module defines the wire format types for the enhanced x402 protocol version.
4//! V2 uses CAIP-2 chain IDs (e.g., "eip155:8453") instead of network names, and
5//! includes richer resource metadata.
6//!
7//! # Key Differences from V1
8//!
9//! - Uses CAIP-2 chain IDs instead of network names
10//! - Includes [`ResourceInfo`] with URL, description, and MIME type
11//! - Simplified [`PaymentRequirements`] structure
12//! - Payment payload includes accepted requirements for verification
13//!
14//! # Key Types
15//!
16//! - [`X402Version2`] - Version marker that serializes as `2`
17//! - [`PaymentPayload`] - Signed payment with accepted requirements
18//! - [`PaymentRequirements`] - Payment terms set by the seller
19//! - [`PaymentRequired`] - HTTP 402 response body
20//! - [`ResourceInfo`] - Metadata about the paid resource
21//! - [`PriceTag`] - Builder for creating payment requirements
22
23use serde::de::DeserializeOwned;
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use std::fmt;
26use std::fmt::{Display, Formatter};
27use std::sync::Arc;
28
29use crate::chain::ChainId;
30use crate::proto;
31use crate::proto::v1;
32use crate::proto::{OriginalJson, SupportedResponse};
33
34/// Version marker for x402 protocol version 2.
35///
36/// This type serializes as the integer `2` and is used to identify V2 protocol
37/// messages in the wire format.
38#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
39pub struct X402Version2;
40
41impl X402Version2 {
42 pub const VALUE: u8 = 2;
43}
44
45impl PartialEq<u8> for X402Version2 {
46 fn eq(&self, other: &u8) -> bool {
47 *other == Self::VALUE
48 }
49}
50
51impl From<X402Version2> for u8 {
52 fn from(_: X402Version2) -> Self {
53 X402Version2::VALUE
54 }
55}
56
57impl Serialize for X402Version2 {
58 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
59 serializer.serialize_u8(Self::VALUE)
60 }
61}
62
63impl<'de> Deserialize<'de> for X402Version2 {
64 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65 where
66 D: Deserializer<'de>,
67 {
68 let num = u8::deserialize(deserializer)?;
69 if num == Self::VALUE {
70 Ok(X402Version2)
71 } else {
72 Err(serde::de::Error::custom(format!(
73 "expected version {}, got {}",
74 Self::VALUE,
75 num
76 )))
77 }
78 }
79}
80
81impl Display for X402Version2 {
82 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
83 write!(f, "{}", Self::VALUE)
84 }
85}
86
87/// Response from a V2 payment verification request.
88///
89/// V2 uses the same response format as V1.
90pub type VerifyResponse = v1::VerifyResponse;
91
92/// Response from a V2 payment settlement request.
93///
94/// V2 uses the same response format as V1.
95pub type SettleResponse = v1::SettleResponse;
96
97/// Metadata about the resource being paid for.
98///
99/// This provides human-readable information about what the buyer is paying for.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct ResourceInfo {
103 /// Human-readable description of the resource.
104 pub description: String,
105 /// MIME type of the resource content.
106 pub mime_type: String,
107 /// URL of the resource.
108 pub url: String,
109}
110
111/// Request to verify a V2 payment.
112///
113/// Contains the payment payload and requirements for verification.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct VerifyRequest<TPayload, TRequirements> {
117 /// Protocol version (always 2).
118 pub x402_version: X402Version2,
119 /// The signed payment authorization.
120 pub payment_payload: TPayload,
121 /// The payment requirements to verify against.
122 pub payment_requirements: TRequirements,
123}
124
125impl<TPayload, TRequirements> TryFrom<&VerifyRequest<TPayload, TRequirements>>
126 for proto::VerifyRequest
127where
128 TPayload: Serialize,
129 TRequirements: Serialize,
130{
131 type Error = serde_json::Error;
132
133 fn try_from(value: &VerifyRequest<TPayload, TRequirements>) -> Result<Self, Self::Error> {
134 let json = serde_json::to_string(value)?;
135 let raw = serde_json::value::RawValue::from_string(json)?;
136 Ok(Self(raw))
137 }
138}
139
140impl<TPayload, TRequirements> TryFrom<&proto::VerifyRequest>
141 for VerifyRequest<TPayload, TRequirements>
142where
143 TPayload: DeserializeOwned,
144 TRequirements: DeserializeOwned,
145{
146 type Error = proto::PaymentVerificationError;
147
148 fn try_from(value: &proto::VerifyRequest) -> Result<Self, Self::Error> {
149 let deserialized = serde_json::from_str(value.as_str())?;
150 Ok(deserialized)
151 }
152}
153
154impl<TPayload, TRequirements> VerifyRequest<TPayload, TRequirements>
155where
156 Self: DeserializeOwned,
157{
158 pub fn from_proto(
159 // FIXME REMOVE THIS
160 request: proto::VerifyRequest,
161 ) -> Result<Self, proto::PaymentVerificationError> {
162 let value = serde_json::from_str(request.as_str())?;
163 Ok(value)
164 }
165}
166
167/// A signed payment authorization from the buyer (V2 format).
168///
169/// In V2, the payment payload includes the accepted requirements, allowing
170/// the facilitator to verify that the buyer agreed to specific terms.
171///
172/// # Type Parameters
173///
174/// - `TAccepted` - The accepted requirements type
175/// - `TPayload` - The scheme-specific payload type
176#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct PaymentPayload<TPaymentRequirements, TPayload> {
179 /// The payment requirements the buyer accepted.
180 pub accepted: TPaymentRequirements,
181 /// The scheme-specific signed payload.
182 pub payload: TPayload,
183 /// Information about the resource being paid for.
184 pub resource: Option<ResourceInfo>,
185 /// Protocol version (always 2).
186 pub x402_version: X402Version2,
187}
188
189/// Payment requirements set by the seller (V2 format).
190///
191/// Defines the terms under which a payment will be accepted. V2 uses
192/// CAIP-2 chain IDs and has a simplified structure compared to V1.
193///
194/// # Type Parameters
195///
196/// - `TScheme` - The scheme identifier type (default: `String`)
197/// - `TAmount` - The amount type (default: `String`)
198/// - `TAddress` - The address type (default: `String`)
199/// - `TExtra` - Scheme-specific extra data type (default: `Option<serde_json::Value>`)
200#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
201#[serde(rename_all = "camelCase")]
202pub struct PaymentRequirements<
203 TScheme = String,
204 TAmount = String,
205 TAddress = String,
206 TExtra = Option<serde_json::Value>,
207> {
208 /// The payment scheme (e.g., "exact").
209 pub scheme: TScheme,
210 /// The CAIP-2 chain ID (e.g., "eip155:8453").
211 pub network: ChainId,
212 /// The payment amount in token units.
213 pub amount: TAmount,
214 /// The recipient address for payment.
215 pub pay_to: TAddress,
216 /// Maximum time in seconds for payment validity.
217 pub max_timeout_seconds: u64,
218 /// The token asset address.
219 pub asset: TAddress,
220 /// Scheme-specific extra data.
221 pub extra: TExtra,
222}
223
224impl<TScheme, TAmount, TAddress, TExtra> TryFrom<&OriginalJson>
225 for PaymentRequirements<TScheme, TAmount, TAddress, TExtra>
226where
227 TScheme: for<'a> serde::Deserialize<'a>,
228 TAmount: for<'a> serde::Deserialize<'a>,
229 TAddress: for<'a> serde::Deserialize<'a>,
230 TExtra: for<'a> serde::Deserialize<'a>,
231{
232 type Error = serde_json::Error;
233
234 fn try_from(value: &OriginalJson) -> Result<Self, Self::Error> {
235 let payment_requirements = serde_json::from_str(value.0.get())?;
236 Ok(payment_requirements)
237 }
238}
239
240/// HTTP 402 Payment Required response body for V2.
241///
242/// This is returned when a resource requires payment. It contains
243/// the list of acceptable payment methods and resource metadata.
244#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct PaymentRequired<TAccepts = PaymentRequirements> {
247 /// Protocol version (always 2).
248 pub x402_version: X402Version2,
249 /// Optional error message if the request was malformed.
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub error: Option<String>,
252 /// Information about the resource being paid for.
253 pub resource: ResourceInfo,
254 /// List of acceptable payment methods.
255 #[serde(default = "Vec::default")]
256 pub accepts: Vec<TAccepts>,
257}
258
259/// Builder for creating V2 payment requirements.
260///
261/// A `PriceTag` wraps [`PaymentRequirements`] and provides enrichment
262/// capabilities for adding facilitator-specific data.
263///
264/// # Example
265///
266/// ```rust
267/// use x402_types::proto::v2::{PriceTag, PaymentRequirements};
268/// use x402_types::chain::ChainId;
269///
270/// let requirements = PaymentRequirements {
271/// scheme: "exact".to_string(),
272/// network: "eip155:8453".parse().unwrap(),
273/// amount: "1000000".to_string(),
274/// pay_to: "0x1234...".to_string(),
275/// asset: "0xUSDC...".to_string(),
276/// max_timeout_seconds: 300,
277/// extra: None,
278/// };
279///
280/// let price = PriceTag {
281/// requirements,
282/// enricher: None,
283/// };
284/// ```
285#[derive(Clone)]
286#[allow(dead_code)] // Public for consumption by downstream crates.
287pub struct PriceTag {
288 /// The payment requirements.
289 pub requirements: PaymentRequirements,
290 /// Optional enrichment function for adding facilitator-specific data.
291 #[doc(hidden)]
292 pub enricher: Option<Enricher>,
293}
294
295impl fmt::Debug for PriceTag {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 f.debug_struct("PriceTag")
298 .field("requirements", &self.requirements)
299 .finish()
300 }
301}
302
303/// Enrichment function type for V2 price tags.
304///
305/// Enrichers are called with the facilitator's capabilities to add
306/// facilitator-specific data to price tags (e.g., fee payer addresses).
307pub type Enricher = Arc<dyn Fn(&mut PriceTag, &SupportedResponse) + Send + Sync>;
308
309impl PriceTag {
310 /// Applies the enrichment function if one is set.
311 ///
312 /// This is called automatically when building payment requirements
313 /// to add facilitator-specific data.
314 #[allow(dead_code)]
315 pub fn enrich(&mut self, capabilities: &SupportedResponse) {
316 if let Some(enricher) = self.enricher.clone() {
317 enricher(self, capabilities);
318 }
319 }
320
321 /// Sets the maximum timeout for this price tag.
322 #[allow(dead_code)]
323 pub fn with_timeout(mut self, seconds: u64) -> Self {
324 self.requirements.max_timeout_seconds = seconds;
325 self
326 }
327}
328
329/// Compares a [`PriceTag`] with [`PaymentRequirements`].
330///
331/// This allows checking if a price tag matches specific requirements.
332impl PartialEq<PaymentRequirements> for PriceTag {
333 fn eq(&self, b: &PaymentRequirements) -> bool {
334 let a = &self.requirements;
335 a == b
336 }
337}