x402-types 2.0.0

Core types for x402 payments: chain identifiers, protocol messages, and facilitator traits
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
//! Protocol version 2 (V2) types for x402.
//!
//! This module defines the wire format types for the enhanced x402 protocol version.
//! V2 uses CAIP-2 chain IDs (e.g., "eip155:8453") instead of network names, and
//! includes richer resource metadata.
//!
//! # Key Differences from V1
//!
//! - Uses CAIP-2 chain IDs instead of network names
//! - Includes [`ResourceInfo`] with URL, description, and MIME type
//! - Simplified [`PaymentRequirements`] structure
//! - Payment payload includes accepted requirements for verification
//!
//! # Key Types
//!
//! - [`X402Version2`] - Version marker that serializes as `2`
//! - [`PaymentPayload`] - Signed payment with accepted requirements
//! - [`PaymentRequirements`] - Payment terms set by the seller
//! - [`PaymentRequired`] - HTTP 402 response body
//! - [`ResourceInfo`] - Metadata about the paid resource
//! - [`PriceTag`] - Builder for creating payment requirements

use serde::de::{DeserializeOwned, Error};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::fmt::{Debug, Display, Formatter};
use std::sync::Arc;

use crate::chain::ChainId;
use crate::proto;
use crate::proto::v1;
use crate::proto::{OriginalJson, SupportedResponse};
use crate::scheme::ExtensionKey;

/// Version marker for x402 protocol version 2.
///
/// This type serializes as the integer `2` and is used to identify V2 protocol
/// messages in the wire format.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
pub struct X402Version2;

impl X402Version2 {
    pub const VALUE: u8 = 2;
}

impl PartialEq<u8> for X402Version2 {
    fn eq(&self, other: &u8) -> bool {
        *other == Self::VALUE
    }
}

impl From<X402Version2> for u8 {
    fn from(_: X402Version2) -> Self {
        X402Version2::VALUE
    }
}

impl Serialize for X402Version2 {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_u8(Self::VALUE)
    }
}

impl<'de> Deserialize<'de> for X402Version2 {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let num = u8::deserialize(deserializer)?;
        if num == Self::VALUE {
            Ok(X402Version2)
        } else {
            Err(serde::de::Error::custom(format!(
                "expected version {}, got {}",
                Self::VALUE,
                num
            )))
        }
    }
}

impl Display for X402Version2 {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", Self::VALUE)
    }
}

/// Response from a V2 payment verification request.
///
/// V2 uses the same response format as V1.
pub type VerifyResponse = v1::VerifyResponse;

/// Response from a V2 payment settlement request.
///
/// V2 uses the same response format as V1.
pub type SettleResponse = v1::SettleResponse;

/// Metadata about the resource being paid for.
///
/// This provides human-readable information about what the buyer is paying for.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceInfo {
    /// URL of the resource.
    pub url: String,
    /// Human-readable description of the resource.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// MIME type of the resource content.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
}

/// Request to verify a V2 payment.
///
/// Contains the payment payload and requirements for verification.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyRequest<TPayload, TRequirements> {
    /// Protocol version (always 2).
    pub x402_version: X402Version2,
    /// The signed payment authorization.
    pub payment_payload: TPayload,
    /// The payment requirements to verify against.
    pub payment_requirements: TRequirements,
}

impl<TPayload, TRequirements> TryFrom<&VerifyRequest<TPayload, TRequirements>>
    for proto::VerifyRequest
where
    TPayload: Serialize,
    TRequirements: Serialize,
{
    type Error = serde_json::Error;

    fn try_from(value: &VerifyRequest<TPayload, TRequirements>) -> Result<Self, Self::Error> {
        let json = serde_json::to_string(value)?;
        let raw = serde_json::value::RawValue::from_string(json)?;
        Ok(Self(raw))
    }
}

impl<TPayload, TRequirements> TryFrom<&proto::VerifyRequest>
    for VerifyRequest<TPayload, TRequirements>
where
    Self: DeserializeOwned,
{
    type Error = proto::PaymentVerificationError;

    fn try_from(value: &proto::VerifyRequest) -> Result<Self, Self::Error> {
        let value = serde_json::from_str(value.as_str())?;
        Ok(value)
    }
}

/// A signed payment authorization from the buyer (V2 format).
///
/// In V2, the payment payload includes the accepted requirements, allowing
/// the facilitator to verify that the buyer agreed to specific terms.
///
/// # Type Parameters
///
/// - `TAccepted` - The accepted requirements type
/// - `TPayload` - The scheme-specific payload type
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentPayload<TPaymentRequirements, TPayload> {
    /// The payment requirements the buyer accepted.
    pub accepted: TPaymentRequirements,
    /// The scheme-specific signed payload.
    pub payload: TPayload,
    /// Information about the resource being paid for.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resource: Option<ResourceInfo>,
    /// Protocol version (always 2).
    pub x402_version: X402Version2,
    /// Optional extension data provided by the client.
    #[serde(default, skip_serializing_if = "ExtensionsJson::is_empty")]
    pub extensions: ExtensionsJson,
}

/// A JSON-object map of protocol extension data attached to a payment message.
///
/// `ExtensionsJson` is the wire representation of optional extension fields in
/// [`PaymentPayload`] and [`PaymentRequired`]. Each extension is keyed by the
/// string constant exposed by [`ExtensionKey::EXTENSION_KEY`] and stored as a
/// JSON value, so heterogeneous extension types can coexist in the same map.
///
/// # Serialization
///
/// Serializes to and from a JSON object (e.g. `{ "eip2612GasSponsoring": { … } }`).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExtensionsJson(serde_json::value::Map<String, serde_json::Value>);

impl ExtensionsJson {
    /// Creates an empty `ExtensionsJson` map.
    ///
    /// Equivalent to [`ExtensionsJson::default()`].
    pub fn new() -> Self {
        Self::default()
    }

    /// Returns `true` if the map contains no extension entries.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Inserts or replaces an extension entry, keyed by `T::EXTENSION_KEY`.
    ///
    /// `value` is serialized to a [`serde_json::Value`] before insertion.
    /// If a value was previously stored under the same key, it is returned
    /// as `Some(old_value)`.
    ///
    /// # Errors
    ///
    /// Returns a [`serde_json::Error`] if `value` cannot be serialized to JSON.
    pub fn insert<T>(&mut self, value: T) -> serde_json::Result<Option<serde_json::Value>>
    where
        T: ExtensionKey + Serialize,
    {
        let key = T::EXTENSION_KEY.to_string();
        let value = serde_json::to_value(value)?;
        Ok(self.0.insert(key, value))
    }

    /// Retrieves and deserializes an extension value by its type.
    ///
    /// The lookup key is taken from `T::EXTENSION_KEY` (see [`ExtensionKey`]).
    /// Returns `None` if the key is absent or if deserialization fails.
    ///
    /// # Type Parameters
    ///
    /// - `T` – Must implement both [`ExtensionKey`] (to provide the key) and
    ///   [`serde::de::DeserializeOwned`] (to decode the stored JSON value).
    pub fn get<T>(&self) -> Option<T>
    where
        T: ExtensionKey + DeserializeOwned,
    {
        self.0
            .get(T::EXTENSION_KEY)
            .and_then(|v| T::deserialize(v).ok())
    }
}

impl FromIterator<(String, serde_json::Value)> for ExtensionsJson {
    /// Collects an iterator of `(key, value)` pairs into an [`ExtensionsJson`].
    ///
    /// Values must already be [`serde_json::Value`]s. To build from
    /// serializable types, serialize each value with [`serde_json::to_value`]
    /// before collecting, handling any errors at the call site.
    fn from_iter<I: IntoIterator<Item = (String, serde_json::Value)>>(iter: I) -> Self {
        ExtensionsJson(iter.into_iter().collect())
    }
}

impl From<ExtensionsJson> for serde_json::Value {
    fn from(value: ExtensionsJson) -> Self {
        serde_json::Value::Object(value.0)
    }
}

impl AsRef<serde_json::Map<String, serde_json::Value>> for ExtensionsJson {
    fn as_ref(&self) -> &serde_json::Map<String, serde_json::Value> {
        &self.0
    }
}

impl TryFrom<serde_json::Value> for ExtensionsJson {
    type Error = serde_json::Error;

    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
        if let serde_json::Value::Object(map) = value {
            Ok(ExtensionsJson(map))
        } else {
            Err(serde_json::Error::custom("expected object"))
        }
    }
}

/// Payment requirements set by the seller (V2 format).
///
/// Defines the terms under which a payment will be accepted. V2 uses
/// CAIP-2 chain IDs and has a simplified structure compared to V1.
///
/// # Type Parameters
///
/// - `TScheme` - The scheme identifier type (default: `String`)
/// - `TAmount` - The amount type (default: `String`)
/// - `TAddress` - The address type (default: `String`)
/// - `TExtra` - Scheme-specific extra data type (default: `Option<serde_json::Value>`)
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequirements<
    TScheme = String,
    TAmount = String,
    TAddress = String,
    TExtra = Option<serde_json::Value>,
> {
    /// The payment scheme (e.g., "exact").
    pub scheme: TScheme,
    /// The CAIP-2 chain ID (e.g., "eip155:8453").
    pub network: ChainId,
    /// The payment amount in token units.
    pub amount: TAmount,
    /// The recipient address for payment.
    pub pay_to: TAddress,
    /// Maximum time in seconds for payment validity.
    pub max_timeout_seconds: u64,
    /// The token asset address.
    pub asset: TAddress,
    /// Scheme-specific extra data.
    pub extra: TExtra,
}

impl<TScheme, TAmount, TAddress, TExtra> TryFrom<&OriginalJson>
    for PaymentRequirements<TScheme, TAmount, TAddress, TExtra>
where
    TScheme: for<'a> serde::Deserialize<'a>,
    TAmount: for<'a> serde::Deserialize<'a>,
    TAddress: for<'a> serde::Deserialize<'a>,
    TExtra: for<'a> serde::Deserialize<'a>,
{
    type Error = serde_json::Error;

    fn try_from(value: &OriginalJson) -> Result<Self, Self::Error> {
        let payment_requirements = serde_json::from_str(value.0.get())?;
        Ok(payment_requirements)
    }
}

/// HTTP 402 Payment Required response body for V2.
///
/// This is returned when a resource requires payment. It contains
/// the list of acceptable payment methods and resource metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequired<TAccepts = PaymentRequirements> {
    /// Protocol version (always 2).
    pub x402_version: X402Version2,
    /// Optional error message if the request was malformed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// Information about the resource being paid for.
    pub resource: Option<ResourceInfo>,
    /// List of acceptable payment methods.
    #[serde(default = "Vec::default")]
    pub accepts: Vec<TAccepts>,
    /// Optional protocol extension declarations provided by the server.
    #[serde(default, skip_serializing_if = "ExtensionsJson::is_empty")]
    pub extensions: ExtensionsJson,
}

/// Builder for creating V2 payment requirements.
///
/// A `PriceTag` wraps [`PaymentRequirements`] and provides enrichment
/// capabilities for adding facilitator-specific data.
///
/// # Example
///
/// ```rust
/// use x402_types::proto::v2::{PriceTag, PaymentRequirements};
/// use x402_types::chain::ChainId;
///
/// let requirements = PaymentRequirements {
///     scheme: "exact".to_string(),
///     network: "eip155:8453".parse().unwrap(),
///     amount: "1000000".to_string(),
///     pay_to: "0x1234...".to_string(),
///     asset: "0xUSDC...".to_string(),
///     max_timeout_seconds: 300,
///     extra: None,
/// };
///
/// let price = PriceTag {
///     requirements,
///     enricher: None,
/// };
/// ```
#[derive(Clone)]
#[allow(dead_code)] // Public for consumption by downstream crates.
pub struct PriceTag {
    /// The payment requirements.
    pub requirements: PaymentRequirements,
    /// Optional enrichment function for adding facilitator-specific data.
    #[doc(hidden)]
    pub enricher: Option<Enricher>,
}

impl fmt::Debug for PriceTag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PriceTag")
            .field("requirements", &self.requirements)
            .finish()
    }
}

/// Enrichment function type for V2 price tags.
///
/// Enrichers are called with the facilitator's capabilities to add
/// facilitator-specific data to price tags (e.g., fee payer addresses).
pub type Enricher = Arc<dyn Fn(&mut PriceTag, &SupportedResponse) + Send + Sync>;

impl PriceTag {
    /// Applies the enrichment function if one is set.
    ///
    /// This is called automatically when building payment requirements
    /// to add facilitator-specific data.
    #[allow(dead_code)]
    pub fn enrich(&mut self, capabilities: &SupportedResponse) {
        if let Some(enricher) = self.enricher.clone() {
            enricher(self, capabilities);
        }
    }

    /// Sets the maximum timeout for this price tag.
    #[allow(dead_code)]
    pub fn with_timeout(mut self, seconds: u64) -> Self {
        self.requirements.max_timeout_seconds = seconds;
        self
    }
}

/// Compares a [`PriceTag`] with [`PaymentRequirements`].
///
/// This allows checking if a price tag matches specific requirements.
impl PartialEq<PaymentRequirements> for PriceTag {
    fn eq(&self, b: &PaymentRequirements) -> bool {
        let a = &self.requirements;
        a == b
    }
}