Skip to main content

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    /// URL of the resource.
104    pub url: String,
105    /// Human-readable description of the resource.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub description: Option<String>,
108    /// MIME type of the resource content.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub mime_type: Option<String>,
111}
112
113/// Request to verify a V2 payment.
114///
115/// Contains the payment payload and requirements for verification.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct VerifyRequest<TPayload, TRequirements> {
119    /// Protocol version (always 2).
120    pub x402_version: X402Version2,
121    /// The signed payment authorization.
122    pub payment_payload: TPayload,
123    /// The payment requirements to verify against.
124    pub payment_requirements: TRequirements,
125}
126
127impl<TPayload, TRequirements> TryFrom<&VerifyRequest<TPayload, TRequirements>>
128    for proto::VerifyRequest
129where
130    TPayload: Serialize,
131    TRequirements: Serialize,
132{
133    type Error = serde_json::Error;
134
135    fn try_from(value: &VerifyRequest<TPayload, TRequirements>) -> Result<Self, Self::Error> {
136        let json = serde_json::to_string(value)?;
137        let raw = serde_json::value::RawValue::from_string(json)?;
138        Ok(Self(raw))
139    }
140}
141
142impl<TPayload, TRequirements> TryFrom<&proto::VerifyRequest>
143    for VerifyRequest<TPayload, TRequirements>
144where
145    TPayload: DeserializeOwned,
146    TRequirements: DeserializeOwned,
147{
148    type Error = proto::PaymentVerificationError;
149
150    fn try_from(value: &proto::VerifyRequest) -> Result<Self, Self::Error> {
151        let deserialized = serde_json::from_str(value.as_str())?;
152        Ok(deserialized)
153    }
154}
155
156impl<TPayload, TRequirements> VerifyRequest<TPayload, TRequirements>
157where
158    Self: DeserializeOwned,
159{
160    pub fn from_proto(
161        // FIXME REMOVE THIS
162        request: proto::VerifyRequest,
163    ) -> Result<Self, proto::PaymentVerificationError> {
164        let value = serde_json::from_str(request.as_str())?;
165        Ok(value)
166    }
167}
168
169/// A signed payment authorization from the buyer (V2 format).
170///
171/// In V2, the payment payload includes the accepted requirements, allowing
172/// the facilitator to verify that the buyer agreed to specific terms.
173///
174/// # Type Parameters
175///
176/// - `TAccepted` - The accepted requirements type
177/// - `TPayload` - The scheme-specific payload type
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct PaymentPayload<TPaymentRequirements, TPayload> {
181    /// The payment requirements the buyer accepted.
182    pub accepted: TPaymentRequirements,
183    /// The scheme-specific signed payload.
184    pub payload: TPayload,
185    /// Information about the resource being paid for.
186    pub resource: Option<ResourceInfo>,
187    /// Protocol version (always 2).
188    pub x402_version: X402Version2,
189}
190
191/// Payment requirements set by the seller (V2 format).
192///
193/// Defines the terms under which a payment will be accepted. V2 uses
194/// CAIP-2 chain IDs and has a simplified structure compared to V1.
195///
196/// # Type Parameters
197///
198/// - `TScheme` - The scheme identifier type (default: `String`)
199/// - `TAmount` - The amount type (default: `String`)
200/// - `TAddress` - The address type (default: `String`)
201/// - `TExtra` - Scheme-specific extra data type (default: `Option<serde_json::Value>`)
202#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
203#[serde(rename_all = "camelCase")]
204pub struct PaymentRequirements<
205    TScheme = String,
206    TAmount = String,
207    TAddress = String,
208    TExtra = Option<serde_json::Value>,
209> {
210    /// The payment scheme (e.g., "exact").
211    pub scheme: TScheme,
212    /// The CAIP-2 chain ID (e.g., "eip155:8453").
213    pub network: ChainId,
214    /// The payment amount in token units.
215    pub amount: TAmount,
216    /// The recipient address for payment.
217    pub pay_to: TAddress,
218    /// Maximum time in seconds for payment validity.
219    pub max_timeout_seconds: u64,
220    /// The token asset address.
221    pub asset: TAddress,
222    /// Scheme-specific extra data.
223    pub extra: TExtra,
224}
225
226impl<TScheme, TAmount, TAddress, TExtra> TryFrom<&OriginalJson>
227    for PaymentRequirements<TScheme, TAmount, TAddress, TExtra>
228where
229    TScheme: for<'a> serde::Deserialize<'a>,
230    TAmount: for<'a> serde::Deserialize<'a>,
231    TAddress: for<'a> serde::Deserialize<'a>,
232    TExtra: for<'a> serde::Deserialize<'a>,
233{
234    type Error = serde_json::Error;
235
236    fn try_from(value: &OriginalJson) -> Result<Self, Self::Error> {
237        let payment_requirements = serde_json::from_str(value.0.get())?;
238        Ok(payment_requirements)
239    }
240}
241
242/// HTTP 402 Payment Required response body for V2.
243///
244/// This is returned when a resource requires payment. It contains
245/// the list of acceptable payment methods and resource metadata.
246#[derive(Debug, Clone, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct PaymentRequired<TAccepts = PaymentRequirements> {
249    /// Protocol version (always 2).
250    pub x402_version: X402Version2,
251    /// Optional error message if the request was malformed.
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub error: Option<String>,
254    /// Information about the resource being paid for.
255    pub resource: ResourceInfo,
256    /// List of acceptable payment methods.
257    #[serde(default = "Vec::default")]
258    pub accepts: Vec<TAccepts>,
259}
260
261/// Builder for creating V2 payment requirements.
262///
263/// A `PriceTag` wraps [`PaymentRequirements`] and provides enrichment
264/// capabilities for adding facilitator-specific data.
265///
266/// # Example
267///
268/// ```rust
269/// use x402_types::proto::v2::{PriceTag, PaymentRequirements};
270/// use x402_types::chain::ChainId;
271///
272/// let requirements = PaymentRequirements {
273///     scheme: "exact".to_string(),
274///     network: "eip155:8453".parse().unwrap(),
275///     amount: "1000000".to_string(),
276///     pay_to: "0x1234...".to_string(),
277///     asset: "0xUSDC...".to_string(),
278///     max_timeout_seconds: 300,
279///     extra: None,
280/// };
281///
282/// let price = PriceTag {
283///     requirements,
284///     enricher: None,
285/// };
286/// ```
287#[derive(Clone)]
288#[allow(dead_code)] // Public for consumption by downstream crates.
289pub struct PriceTag {
290    /// The payment requirements.
291    pub requirements: PaymentRequirements,
292    /// Optional enrichment function for adding facilitator-specific data.
293    #[doc(hidden)]
294    pub enricher: Option<Enricher>,
295}
296
297impl fmt::Debug for PriceTag {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        f.debug_struct("PriceTag")
300            .field("requirements", &self.requirements)
301            .finish()
302    }
303}
304
305/// Enrichment function type for V2 price tags.
306///
307/// Enrichers are called with the facilitator's capabilities to add
308/// facilitator-specific data to price tags (e.g., fee payer addresses).
309pub type Enricher = Arc<dyn Fn(&mut PriceTag, &SupportedResponse) + Send + Sync>;
310
311impl PriceTag {
312    /// Applies the enrichment function if one is set.
313    ///
314    /// This is called automatically when building payment requirements
315    /// to add facilitator-specific data.
316    #[allow(dead_code)]
317    pub fn enrich(&mut self, capabilities: &SupportedResponse) {
318        if let Some(enricher) = self.enricher.clone() {
319            enricher(self, capabilities);
320        }
321    }
322
323    /// Sets the maximum timeout for this price tag.
324    #[allow(dead_code)]
325    pub fn with_timeout(mut self, seconds: u64) -> Self {
326        self.requirements.max_timeout_seconds = seconds;
327        self
328    }
329}
330
331/// Compares a [`PriceTag`] with [`PaymentRequirements`].
332///
333/// This allows checking if a price tag matches specific requirements.
334impl PartialEq<PaymentRequirements> for PriceTag {
335    fn eq(&self, b: &PaymentRequirements) -> bool {
336        let a = &self.requirements;
337        a == b
338    }
339}