Skip to main content

x402_types/proto/
v1.rs

1//! Protocol version 1 (V1) types for x402.
2//!
3//! This module defines the wire format types for the original x402 protocol version.
4//! V1 uses network names (e.g., "base-sepolia") instead of CAIP-2 chain IDs.
5//!
6//! # Key Types
7//!
8//! - [`X402Version1`] - Version marker that serializes as `1`
9//! - [`PaymentPayload`] - Signed payment authorization from the buyer
10//! - [`PaymentRequirements`] - Payment terms set by the seller
11//! - [`PaymentRequired`] - HTTP 402 response body
12//! - [`VerifyRequest`] / [`VerifyResponse`] - Verification messages
13//! - [`SettleResponse`] - Settlement result
14//! - [`PriceTag`] - Builder for creating payment requirements
15
16use serde::de::DeserializeOwned;
17use serde::{Deserialize, Deserializer, Serialize, Serializer};
18use std::fmt;
19use std::fmt::Display;
20use std::sync::Arc;
21
22use crate::proto;
23use crate::proto::{OriginalJson, SupportedResponse};
24
25/// Version marker for x402 protocol version 1.
26///
27/// This type serializes as the integer `1` and is used to identify V1 protocol
28/// messages in the wire format.
29#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
30pub struct X402Version1;
31
32impl X402Version1 {
33    pub const VALUE: u8 = 1;
34}
35
36impl PartialEq<u8> for X402Version1 {
37    fn eq(&self, other: &u8) -> bool {
38        *other == Self::VALUE
39    }
40}
41
42impl From<X402Version1> for u8 {
43    fn from(_: X402Version1) -> Self {
44        X402Version1::VALUE
45    }
46}
47
48impl Serialize for X402Version1 {
49    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
50        serializer.serialize_u8(Self::VALUE)
51    }
52}
53
54impl<'de> Deserialize<'de> for X402Version1 {
55    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56    where
57        D: Deserializer<'de>,
58    {
59        let num = u8::deserialize(deserializer)?;
60        if num == Self::VALUE {
61            Ok(X402Version1)
62        } else {
63            Err(serde::de::Error::custom(format!(
64                "expected version {}, got {}",
65                Self::VALUE,
66                num
67            )))
68        }
69    }
70}
71
72impl Display for X402Version1 {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(f, "{}", Self::VALUE)
75    }
76}
77
78/// Response from a payment settlement request.
79///
80/// Indicates whether the payment was successfully settled on-chain.
81pub enum SettleResponse {
82    /// Settlement succeeded.
83    Success {
84        /// The address that paid.
85        payer: String,
86        /// The transaction hash.
87        transaction: String,
88        /// The network where settlement occurred.
89        network: String,
90    },
91    /// Settlement failed.
92    Error {
93        /// The reason for failure.
94        reason: String,
95        /// The network where settlement was attempted.
96        network: String,
97    },
98}
99
100impl From<SettleResponse> for proto::SettleResponse {
101    fn from(val: SettleResponse) -> Self {
102        proto::SettleResponse(
103            serde_json::to_value(val).expect("SettleResponse serialization failed"),
104        )
105    }
106}
107
108#[derive(Serialize, Deserialize)]
109struct SettleResponseWire {
110    pub success: bool,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub error_reason: Option<String>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub payer: Option<String>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub transaction: Option<String>,
117    pub network: String,
118}
119
120impl Serialize for SettleResponse {
121    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122    where
123        S: Serializer,
124    {
125        let wire = match self {
126            SettleResponse::Success {
127                payer,
128                transaction,
129                network,
130            } => SettleResponseWire {
131                success: true,
132                error_reason: None,
133                payer: Some(payer.clone()),
134                transaction: Some(transaction.clone()),
135                network: network.clone(),
136            },
137            SettleResponse::Error { reason, network } => SettleResponseWire {
138                success: false,
139                error_reason: Some(reason.clone()),
140                payer: None,
141                transaction: None,
142                network: network.clone(),
143            },
144        };
145        wire.serialize(serializer)
146    }
147}
148
149impl<'de> Deserialize<'de> for SettleResponse {
150    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
151    where
152        D: Deserializer<'de>,
153    {
154        let wire = SettleResponseWire::deserialize(deserializer)?;
155        match wire.success {
156            true => {
157                let payer = wire
158                    .payer
159                    .ok_or_else(|| serde::de::Error::missing_field("payer"))?;
160                let transaction = wire
161                    .transaction
162                    .ok_or_else(|| serde::de::Error::missing_field("transaction"))?;
163                Ok(SettleResponse::Success {
164                    payer,
165                    transaction,
166                    network: wire.network,
167                })
168            }
169            false => {
170                let reason = wire
171                    .error_reason
172                    .ok_or_else(|| serde::de::Error::missing_field("error_reason"))?;
173                Ok(SettleResponse::Error {
174                    reason,
175                    network: wire.network,
176                })
177            }
178        }
179    }
180}
181
182/// Result returned by a facilitator after verifying a [`PaymentPayload`] against the provided [`PaymentRequirements`].
183///
184/// This response indicates whether the payment authorization is valid and identifies the payer. If invalid,
185/// it includes a reason describing why verification failed (e.g., wrong network, an invalid scheme, insufficient funds).
186#[derive(Debug)]
187pub enum VerifyResponse {
188    /// The payload matches the requirements and passes all checks.
189    Valid { payer: String },
190    /// The payload was well-formed but failed verification due to the specified [`FacilitatorErrorReason`]
191    Invalid {
192        reason: String,
193        payer: Option<String>,
194    },
195}
196
197impl From<VerifyResponse> for proto::VerifyResponse {
198    fn from(val: VerifyResponse) -> Self {
199        proto::VerifyResponse(
200            serde_json::to_value(val).expect("VerifyResponse serialization failed"),
201        )
202    }
203}
204
205impl TryFrom<proto::VerifyResponse> for VerifyResponse {
206    type Error = serde_json::Error;
207    fn try_from(value: proto::VerifyResponse) -> Result<Self, Self::Error> {
208        let json = value.0;
209        serde_json::from_value(json)
210    }
211}
212
213impl VerifyResponse {
214    /// Constructs a successful verification response with the given `payer` address.
215    ///
216    /// Indicates that the provided payment payload has been validated against the payment requirements.
217    pub fn valid(payer: String) -> Self {
218        VerifyResponse::Valid { payer }
219    }
220
221    /// Constructs a failed verification response with the given `payer` address and error `reason`.
222    ///
223    /// Indicates that the payment was recognized but rejected due to reasons such as
224    /// insufficient funds, invalid network, or scheme mismatch.
225    #[allow(dead_code)]
226    pub fn invalid(payer: Option<String>, reason: String) -> Self {
227        VerifyResponse::Invalid { reason, payer }
228    }
229}
230
231#[derive(Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233struct VerifyResponseWire {
234    is_valid: bool,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    payer: Option<String>,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    invalid_reason: Option<String>,
239}
240
241impl Serialize for VerifyResponse {
242    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
243    where
244        S: Serializer,
245    {
246        let wire = match self {
247            VerifyResponse::Valid { payer } => VerifyResponseWire {
248                is_valid: true,
249                payer: Some(payer.clone()),
250                invalid_reason: None,
251            },
252            VerifyResponse::Invalid { reason, payer } => VerifyResponseWire {
253                is_valid: false,
254                payer: payer.clone(),
255                invalid_reason: Some(reason.clone()),
256            },
257        };
258        wire.serialize(serializer)
259    }
260}
261
262impl<'de> Deserialize<'de> for VerifyResponse {
263    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
264    where
265        D: Deserializer<'de>,
266    {
267        let wire = VerifyResponseWire::deserialize(deserializer)?;
268        match wire.is_valid {
269            true => {
270                let payer = wire
271                    .payer
272                    .ok_or_else(|| serde::de::Error::missing_field("payer"))?;
273                Ok(VerifyResponse::Valid { payer })
274            }
275            false => {
276                let reason = wire
277                    .invalid_reason
278                    .ok_or_else(|| serde::de::Error::missing_field("invalid_reason"))?;
279                let payer = wire.payer;
280                Ok(VerifyResponse::Invalid { reason, payer })
281            }
282        }
283    }
284}
285
286/// Request to verify a V1 payment.
287///
288/// Contains the payment payload and requirements for verification.
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct VerifyRequest<TPayload, TRequirements> {
292    /// Protocol version (always 1).
293    pub x402_version: X402Version1,
294    /// The signed payment authorization.
295    pub payment_payload: TPayload,
296    /// The payment requirements to verify against.
297    pub payment_requirements: TRequirements,
298}
299
300impl<TPayload, TRequirements> VerifyRequest<TPayload, TRequirements>
301where
302    Self: DeserializeOwned,
303{
304    pub fn from_proto(
305        // FIXME REMOVE THIS
306        request: proto::VerifyRequest,
307    ) -> Result<Self, proto::PaymentVerificationError> {
308        let value = serde_json::from_str(request.as_str())?;
309        Ok(value)
310    }
311}
312
313impl<TPayload, TRequirements> TryInto<proto::VerifyRequest>
314    for VerifyRequest<TPayload, TRequirements>
315where
316    TPayload: Serialize,
317    TRequirements: Serialize,
318{
319    type Error = serde_json::Error;
320    fn try_into(self) -> Result<proto::VerifyRequest, Self::Error> {
321        let json = serde_json::to_value(self)?;
322        let raw = serde_json::value::to_raw_value(&json)?;
323        Ok(proto::VerifyRequest(raw))
324    }
325}
326
327/// A signed payment authorization from the buyer.
328///
329/// This contains the cryptographic proof that the buyer has authorized
330/// a payment, along with metadata about the payment scheme and network.
331///
332/// # Type Parameters
333///
334/// - `TScheme` - The scheme identifier type (default: `String`)
335/// - `TPayload` - The scheme-specific payload type (default: raw JSON)
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct PaymentPayload<TScheme = String, TPayload = Box<serde_json::value::RawValue>> {
339    /// Protocol version (always 1).
340    pub x402_version: X402Version1,
341    /// The payment scheme (e.g., "exact").
342    pub scheme: TScheme,
343    /// The network name (e.g., "base-sepolia").
344    pub network: String,
345    /// The scheme-specific signed payload.
346    pub payload: TPayload,
347}
348
349/// Payment requirements set by the seller.
350///
351/// Defines the terms under which a payment will be accepted, including
352/// the amount, recipient, asset, and timing constraints.
353///
354/// # Type Parameters
355///
356/// - `TScheme` - The scheme identifier type (default: `String`)
357/// - `TAmount` - The amount type (default: `String`)
358/// - `TAddress` - The address type (default: `String`)
359/// - `TExtra` - Scheme-specific extra data type (default: `serde_json::Value`)
360#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
361#[serde(rename_all = "camelCase")]
362pub struct PaymentRequirements<
363    TScheme = String,
364    TAmount = String,
365    TAddress = String,
366    TExtra = serde_json::Value,
367> {
368    /// The payment scheme (e.g., "exact").
369    pub scheme: TScheme,
370    /// The network name (e.g., "base-sepolia").
371    pub network: String,
372    /// The maximum amount required for payment.
373    pub max_amount_required: TAmount,
374    /// The resource URL being paid for.
375    pub resource: String,
376    /// Human-readable description of the resource.
377    pub description: String,
378    /// MIME type of the resource.
379    pub mime_type: String,
380    /// Optional JSON schema for the resource output.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub output_schema: Option<serde_json::Value>,
383    /// The recipient address for payment.
384    pub pay_to: TAddress,
385    /// Maximum time in seconds for payment validity.
386    pub max_timeout_seconds: u64,
387    /// The token asset address.
388    pub asset: TAddress,
389    /// Scheme-specific extra data.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub extra: Option<TExtra>,
392}
393
394impl<TScheme, TAmount, TAddress, TExtra> TryFrom<&OriginalJson>
395    for PaymentRequirements<TScheme, TAmount, TAddress, TExtra>
396where
397    TScheme: for<'a> serde::Deserialize<'a>,
398    TAmount: for<'a> serde::Deserialize<'a>,
399    TAddress: for<'a> serde::Deserialize<'a>,
400    TExtra: for<'a> serde::Deserialize<'a>,
401{
402    type Error = serde_json::Error;
403
404    fn try_from(value: &OriginalJson) -> Result<Self, Self::Error> {
405        let payment_requirements = serde_json::from_str(value.0.get())?;
406        Ok(payment_requirements)
407    }
408}
409
410/// HTTP 402 Payment Required response body for V1.
411///
412/// This is returned when a resource requires payment. It contains
413/// the list of acceptable payment methods.
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct PaymentRequired<TAccepts = PaymentRequirements> {
417    /// Protocol version (always 1).
418    pub x402_version: X402Version1,
419    /// List of acceptable payment methods.
420    #[serde(default = "Vec::default")]
421    pub accepts: Vec<TAccepts>,
422    /// Optional error message if the request was malformed.
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub error: Option<String>,
425}
426
427/// Builder for creating payment requirements.
428///
429/// A `PriceTag` is a convenient way to specify payment terms that can
430/// be converted into [`PaymentRequirements`] for inclusion in a 402 response.
431///
432/// # Example
433///
434/// ```rust
435/// use x402_types::proto::v1::PriceTag;
436///
437/// let price = PriceTag {
438///     scheme: "exact".to_string(),
439///     pay_to: "0x1234...".to_string(),
440///     asset: "0xUSDC...".to_string(),
441///     network: "base".to_string(),
442///     amount: "1000000".to_string(), // 1 USDC
443///     max_timeout_seconds: 300,
444///     extra: None,
445///     enricher: None,
446/// };
447/// ```
448#[derive(Clone)]
449#[allow(dead_code)] // Public for consumption by downstream crates.
450pub struct PriceTag {
451    /// The payment scheme (e.g., "exact").
452    pub scheme: String,
453    /// The recipient address.
454    pub pay_to: String,
455    /// The token asset address.
456    pub asset: String,
457    /// The network name.
458    pub network: String,
459    /// The payment amount in token units.
460    pub amount: String,
461    /// Maximum time in seconds for payment validity.
462    pub max_timeout_seconds: u64,
463    /// Scheme-specific extra data.
464    pub extra: Option<serde_json::Value>,
465    /// Optional enrichment function for adding facilitator-specific data.
466    #[doc(hidden)]
467    pub enricher: Option<Enricher>,
468}
469
470impl fmt::Debug for PriceTag {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        f.debug_struct("PriceTag")
473            .field("scheme", &self.scheme)
474            .field("pay_to", &self.pay_to)
475            .field("asset", &self.asset)
476            .field("network", &self.network)
477            .field("amount", &self.amount)
478            .field("max_timeout_seconds", &self.max_timeout_seconds)
479            .field("extra", &self.extra)
480            .finish()
481    }
482}
483
484/// Enrichment function type for price tags.
485///
486/// Enrichers are called with the facilitator's capabilities to add
487/// facilitator-specific data to price tags (e.g., fee payer addresses).
488pub type Enricher = Arc<dyn Fn(&mut PriceTag, &SupportedResponse) + Send + Sync>;
489
490impl PriceTag {
491    /// Applies the enrichment function if one is set.
492    ///
493    /// This is called automatically when building payment requirements
494    /// to add facilitator-specific data.
495    #[allow(dead_code)]
496    pub fn enrich(&mut self, capabilities: &SupportedResponse) {
497        if let Some(enricher) = self.enricher.clone() {
498            enricher(self, capabilities);
499        }
500    }
501
502    /// Sets the maximum timeout for this price tag.
503    #[allow(dead_code)]
504    pub fn with_timeout(mut self, seconds: u64) -> Self {
505        self.max_timeout_seconds = seconds;
506        self
507    }
508}