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, Error};
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use std::fmt;
26use std::fmt::{Debug, Display, Formatter};
27use std::sync::Arc;
28
29use crate::chain::ChainId;
30use crate::proto;
31use crate::proto::v1;
32use crate::proto::{OriginalJson, SupportedResponse};
33use crate::scheme::ExtensionKey;
34
35/// Version marker for x402 protocol version 2.
36///
37/// This type serializes as the integer `2` and is used to identify V2 protocol
38/// messages in the wire format.
39#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
40pub struct X402Version2;
41
42impl X402Version2 {
43    pub const VALUE: u8 = 2;
44}
45
46impl PartialEq<u8> for X402Version2 {
47    fn eq(&self, other: &u8) -> bool {
48        *other == Self::VALUE
49    }
50}
51
52impl From<X402Version2> for u8 {
53    fn from(_: X402Version2) -> Self {
54        X402Version2::VALUE
55    }
56}
57
58impl Serialize for X402Version2 {
59    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
60        serializer.serialize_u8(Self::VALUE)
61    }
62}
63
64impl<'de> Deserialize<'de> for X402Version2 {
65    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66    where
67        D: Deserializer<'de>,
68    {
69        let num = u8::deserialize(deserializer)?;
70        if num == Self::VALUE {
71            Ok(X402Version2)
72        } else {
73            Err(serde::de::Error::custom(format!(
74                "expected version {}, got {}",
75                Self::VALUE,
76                num
77            )))
78        }
79    }
80}
81
82impl Display for X402Version2 {
83    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
84        write!(f, "{}", Self::VALUE)
85    }
86}
87
88/// Response from a V2 payment verification request.
89///
90/// V2 uses the same response format as V1.
91pub type VerifyResponse = v1::VerifyResponse;
92
93/// Response from a V2 payment settlement request.
94///
95/// V2 uses the same response format as V1.
96pub type SettleResponse = v1::SettleResponse;
97
98/// Metadata about the resource being paid for.
99///
100/// This provides human-readable information about what the buyer is paying for.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct ResourceInfo {
104    /// URL of the resource.
105    pub url: String,
106    /// Human-readable description of the resource.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub description: Option<String>,
109    /// MIME type of the resource content.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub mime_type: Option<String>,
112}
113
114/// Request to verify a V2 payment.
115///
116/// Contains the payment payload and requirements for verification.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct VerifyRequest<TPayload, TRequirements> {
120    /// Protocol version (always 2).
121    pub x402_version: X402Version2,
122    /// The signed payment authorization.
123    pub payment_payload: TPayload,
124    /// The payment requirements to verify against.
125    pub payment_requirements: TRequirements,
126}
127
128impl<TPayload, TRequirements> TryFrom<&VerifyRequest<TPayload, TRequirements>>
129    for proto::VerifyRequest
130where
131    TPayload: Serialize,
132    TRequirements: Serialize,
133{
134    type Error = serde_json::Error;
135
136    fn try_from(value: &VerifyRequest<TPayload, TRequirements>) -> Result<Self, Self::Error> {
137        let json = serde_json::to_string(value)?;
138        let raw = serde_json::value::RawValue::from_string(json)?;
139        Ok(Self(raw))
140    }
141}
142
143impl<TPayload, TRequirements> TryFrom<&proto::VerifyRequest>
144    for VerifyRequest<TPayload, TRequirements>
145where
146    Self: DeserializeOwned,
147{
148    type Error = proto::PaymentVerificationError;
149
150    fn try_from(value: &proto::VerifyRequest) -> Result<Self, Self::Error> {
151        let value = serde_json::from_str(value.as_str())?;
152        Ok(value)
153    }
154}
155
156/// A signed payment authorization from the buyer (V2 format).
157///
158/// In V2, the payment payload includes the accepted requirements, allowing
159/// the facilitator to verify that the buyer agreed to specific terms.
160///
161/// # Type Parameters
162///
163/// - `TAccepted` - The accepted requirements type
164/// - `TPayload` - The scheme-specific payload type
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct PaymentPayload<TPaymentRequirements, TPayload> {
168    /// The payment requirements the buyer accepted.
169    pub accepted: TPaymentRequirements,
170    /// The scheme-specific signed payload.
171    pub payload: TPayload,
172    /// Information about the resource being paid for.
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub resource: Option<ResourceInfo>,
175    /// Protocol version (always 2).
176    pub x402_version: X402Version2,
177    /// Optional extension data provided by the client.
178    #[serde(default, skip_serializing_if = "ExtensionsJson::is_empty")]
179    pub extensions: ExtensionsJson,
180}
181
182/// A JSON-object map of protocol extension data attached to a payment message.
183///
184/// `ExtensionsJson` is the wire representation of optional extension fields in
185/// [`PaymentPayload`] and [`PaymentRequired`]. Each extension is keyed by the
186/// string constant exposed by [`ExtensionKey::EXTENSION_KEY`] and stored as a
187/// JSON value, so heterogeneous extension types can coexist in the same map.
188///
189/// # Serialization
190///
191/// Serializes to and from a JSON object (e.g. `{ "eip2612GasSponsoring": { … } }`).
192#[derive(Debug, Clone, Serialize, Deserialize, Default)]
193pub struct ExtensionsJson(serde_json::value::Map<String, serde_json::Value>);
194
195impl ExtensionsJson {
196    /// Creates an empty `ExtensionsJson` map.
197    ///
198    /// Equivalent to [`ExtensionsJson::default()`].
199    pub fn new() -> Self {
200        Self::default()
201    }
202
203    /// Returns `true` if the map contains no extension entries.
204    pub fn is_empty(&self) -> bool {
205        self.0.is_empty()
206    }
207
208    /// Inserts or replaces an extension entry, keyed by `T::EXTENSION_KEY`.
209    ///
210    /// `value` is serialized to a [`serde_json::Value`] before insertion.
211    /// If a value was previously stored under the same key, it is returned
212    /// as `Some(old_value)`.
213    ///
214    /// # Errors
215    ///
216    /// Returns a [`serde_json::Error`] if `value` cannot be serialized to JSON.
217    pub fn insert<T>(&mut self, value: T) -> serde_json::Result<Option<serde_json::Value>>
218    where
219        T: ExtensionKey + Serialize,
220    {
221        let key = T::EXTENSION_KEY.to_string();
222        let value = serde_json::to_value(value)?;
223        Ok(self.0.insert(key, value))
224    }
225
226    /// Retrieves and deserializes an extension value by its type.
227    ///
228    /// The lookup key is taken from `T::EXTENSION_KEY` (see [`ExtensionKey`]).
229    /// Returns `None` if the key is absent or if deserialization fails.
230    ///
231    /// # Type Parameters
232    ///
233    /// - `T` – Must implement both [`ExtensionKey`] (to provide the key) and
234    ///   [`serde::de::DeserializeOwned`] (to decode the stored JSON value).
235    pub fn get<T>(&self) -> Option<T>
236    where
237        T: ExtensionKey + DeserializeOwned,
238    {
239        self.0
240            .get(T::EXTENSION_KEY)
241            .and_then(|v| T::deserialize(v).ok())
242    }
243}
244
245impl FromIterator<(String, serde_json::Value)> for ExtensionsJson {
246    /// Collects an iterator of `(key, value)` pairs into an [`ExtensionsJson`].
247    ///
248    /// Values must already be [`serde_json::Value`]s. To build from
249    /// serializable types, serialize each value with [`serde_json::to_value`]
250    /// before collecting, handling any errors at the call site.
251    fn from_iter<I: IntoIterator<Item = (String, serde_json::Value)>>(iter: I) -> Self {
252        ExtensionsJson(iter.into_iter().collect())
253    }
254}
255
256impl From<ExtensionsJson> for serde_json::Value {
257    fn from(value: ExtensionsJson) -> Self {
258        serde_json::Value::Object(value.0)
259    }
260}
261
262impl AsRef<serde_json::Map<String, serde_json::Value>> for ExtensionsJson {
263    fn as_ref(&self) -> &serde_json::Map<String, serde_json::Value> {
264        &self.0
265    }
266}
267
268impl TryFrom<serde_json::Value> for ExtensionsJson {
269    type Error = serde_json::Error;
270
271    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
272        if let serde_json::Value::Object(map) = value {
273            Ok(ExtensionsJson(map))
274        } else {
275            Err(serde_json::Error::custom("expected object"))
276        }
277    }
278}
279
280/// Payment requirements set by the seller (V2 format).
281///
282/// Defines the terms under which a payment will be accepted. V2 uses
283/// CAIP-2 chain IDs and has a simplified structure compared to V1.
284///
285/// # Type Parameters
286///
287/// - `TScheme` - The scheme identifier type (default: `String`)
288/// - `TAmount` - The amount type (default: `String`)
289/// - `TAddress` - The address type (default: `String`)
290/// - `TExtra` - Scheme-specific extra data type (default: `Option<serde_json::Value>`)
291#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
292#[serde(rename_all = "camelCase")]
293pub struct PaymentRequirements<
294    TScheme = String,
295    TAmount = String,
296    TAddress = String,
297    TExtra = Option<serde_json::Value>,
298> {
299    /// The payment scheme (e.g., "exact").
300    pub scheme: TScheme,
301    /// The CAIP-2 chain ID (e.g., "eip155:8453").
302    pub network: ChainId,
303    /// The payment amount in token units.
304    pub amount: TAmount,
305    /// The recipient address for payment.
306    pub pay_to: TAddress,
307    /// Maximum time in seconds for payment validity.
308    pub max_timeout_seconds: u64,
309    /// The token asset address.
310    pub asset: TAddress,
311    /// Scheme-specific extra data.
312    pub extra: TExtra,
313}
314
315impl<TScheme, TAmount, TAddress, TExtra> TryFrom<&OriginalJson>
316    for PaymentRequirements<TScheme, TAmount, TAddress, TExtra>
317where
318    TScheme: for<'a> serde::Deserialize<'a>,
319    TAmount: for<'a> serde::Deserialize<'a>,
320    TAddress: for<'a> serde::Deserialize<'a>,
321    TExtra: for<'a> serde::Deserialize<'a>,
322{
323    type Error = serde_json::Error;
324
325    fn try_from(value: &OriginalJson) -> Result<Self, Self::Error> {
326        let payment_requirements = serde_json::from_str(value.0.get())?;
327        Ok(payment_requirements)
328    }
329}
330
331/// HTTP 402 Payment Required response body for V2.
332///
333/// This is returned when a resource requires payment. It contains
334/// the list of acceptable payment methods and resource metadata.
335#[derive(Debug, Clone, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct PaymentRequired<TAccepts = PaymentRequirements> {
338    /// Protocol version (always 2).
339    pub x402_version: X402Version2,
340    /// Optional error message if the request was malformed.
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub error: Option<String>,
343    /// Information about the resource being paid for.
344    pub resource: Option<ResourceInfo>,
345    /// List of acceptable payment methods.
346    #[serde(default = "Vec::default")]
347    pub accepts: Vec<TAccepts>,
348    /// Optional protocol extension declarations provided by the server.
349    #[serde(default, skip_serializing_if = "ExtensionsJson::is_empty")]
350    pub extensions: ExtensionsJson,
351}
352
353/// Builder for creating V2 payment requirements.
354///
355/// A `PriceTag` wraps [`PaymentRequirements`] and provides enrichment
356/// capabilities for adding facilitator-specific data.
357///
358/// # Example
359///
360/// ```rust
361/// use x402_types::proto::v2::{PriceTag, PaymentRequirements};
362/// use x402_types::chain::ChainId;
363///
364/// let requirements = PaymentRequirements {
365///     scheme: "exact".to_string(),
366///     network: "eip155:8453".parse().unwrap(),
367///     amount: "1000000".to_string(),
368///     pay_to: "0x1234...".to_string(),
369///     asset: "0xUSDC...".to_string(),
370///     max_timeout_seconds: 300,
371///     extra: None,
372/// };
373///
374/// let price = PriceTag {
375///     requirements,
376///     enricher: None,
377/// };
378/// ```
379#[derive(Clone)]
380#[allow(dead_code)] // Public for consumption by downstream crates.
381pub struct PriceTag {
382    /// The payment requirements.
383    pub requirements: PaymentRequirements,
384    /// Optional enrichment function for adding facilitator-specific data.
385    #[doc(hidden)]
386    pub enricher: Option<Enricher>,
387}
388
389impl fmt::Debug for PriceTag {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        f.debug_struct("PriceTag")
392            .field("requirements", &self.requirements)
393            .finish()
394    }
395}
396
397/// Enrichment function type for V2 price tags.
398///
399/// Enrichers are called with the facilitator's capabilities to add
400/// facilitator-specific data to price tags (e.g., fee payer addresses).
401pub type Enricher = Arc<dyn Fn(&mut PriceTag, &SupportedResponse) + Send + Sync>;
402
403impl PriceTag {
404    /// Applies the enrichment function if one is set.
405    ///
406    /// This is called automatically when building payment requirements
407    /// to add facilitator-specific data.
408    #[allow(dead_code)]
409    pub fn enrich(&mut self, capabilities: &SupportedResponse) {
410        if let Some(enricher) = self.enricher.clone() {
411            enricher(self, capabilities);
412        }
413    }
414
415    /// Sets the maximum timeout for this price tag.
416    #[allow(dead_code)]
417    pub fn with_timeout(mut self, seconds: u64) -> Self {
418        self.requirements.max_timeout_seconds = seconds;
419        self
420    }
421}
422
423/// Compares a [`PriceTag`] with [`PaymentRequirements`].
424///
425/// This allows checking if a price tag matches specific requirements.
426impl PartialEq<PaymentRequirements> for PriceTag {
427    fn eq(&self, b: &PaymentRequirements) -> bool {
428        let a = &self.requirements;
429        a == b
430    }
431}