Skip to main content

r402_core/scheme/
mod.rs

1//! Scheme identifiers, registry, and the extensible payment scheme system.
2//!
3//! An x402 **scheme** is a strategy for transforming a payment requirement
4//! into an on-chain transaction. Workspace markers cover the v2 schemes
5//! r402 implements or is landing:
6//!
7//! - [`ExactScheme`] — transfer of at least `amount` (chain binding defines exactness)
8//! - [`UptoScheme`] — buyer authorises up to a maximum; settle may charge less
9//! - [`BatchSettlementScheme`] — deferred / channel-backed settlement
10//! - [`AuthCaptureScheme`] — authorize / capture / void / refund flows
11//!
12//! Client, server, and facilitator implementations all reference schemes by
13//! their `SchemeId` (namespace + scheme name). The registry module wires
14//! chains + schemes to [`crate::facilitator::Facilitator`] handlers.
15
16mod client;
17mod registry;
18pub mod sealed;
19mod server;
20
21pub use client::*;
22pub use registry::*;
23pub use server::*;
24
25/// Identity trait for scheme markers.
26///
27/// Implemented by name-marker types such as [`ExactScheme`] / [`UptoScheme`]
28/// and by concrete scheme handlers provided by chain crates.
29pub trait SchemeId {
30    /// CAIP-2 namespace (e.g. `"eip155"`, `"solana"`).
31    fn namespace(&self) -> &str;
32    /// Scheme name (e.g. `"exact"`, `"upto"`).
33    fn scheme(&self) -> &str;
34    /// CAIP-2 family pattern — defaults to `"{namespace}:*"`.
35    fn caip_family(&self) -> String {
36        format!("{}:*", self.namespace())
37    }
38    /// Human-readable identifier — defaults to `"{namespace}-{scheme}"`.
39    fn id(&self) -> String {
40        format!("{}-{}", self.namespace(), self.scheme())
41    }
42}
43
44/// Unit marker representing the string literal `"exact"`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct ExactScheme;
47
48/// Unit marker representing the string literal `"upto"`.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct UptoScheme;
51
52/// Unit marker representing the string literal `"batch-settlement"`.
53///
54/// Capital-backed (or credit-backed) deferred settlement; see
55/// `scheme_batch_settlement.md` / EVM binding.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct BatchSettlementScheme;
58
59/// Unit marker representing the string literal `"auth-capture"`.
60///
61/// Authorize / capture / void / refund; see `scheme_auth_capture.md`.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub struct AuthCaptureScheme;
64
65macro_rules! impl_scheme_marker {
66    ($ty:ty, $value:literal) => {
67        impl $ty {
68            /// The canonical wire value.
69            pub const VALUE: &'static str = $value;
70        }
71
72        impl std::fmt::Display for $ty {
73            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74                f.write_str(Self::VALUE)
75            }
76        }
77
78        impl AsRef<str> for $ty {
79            fn as_ref(&self) -> &str {
80                Self::VALUE
81            }
82        }
83
84        impl std::str::FromStr for $ty {
85            type Err = String;
86            fn from_str(s: &str) -> Result<Self, Self::Err> {
87                if s == Self::VALUE {
88                    Ok(Self)
89                } else {
90                    Err(format!("expected '{}', got '{s}'", Self::VALUE))
91                }
92            }
93        }
94
95        impl serde::Serialize for $ty {
96            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
97                serializer.serialize_str(Self::VALUE)
98            }
99        }
100
101        impl<'de> serde::Deserialize<'de> for $ty {
102            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
103                // `Cow` rather than `&str`: scheme markers are also decoded from
104                // owned `serde_json::Value` trees (e.g. `TypedVerifyRequest::from_verify`),
105                // where no borrowed string is available.
106                let s = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
107                let s = s.as_ref();
108                if s == Self::VALUE {
109                    Ok(Self)
110                } else {
111                    Err(serde::de::Error::custom(format!(
112                        "expected '{}', got '{s}'",
113                        Self::VALUE
114                    )))
115                }
116            }
117        }
118    };
119}
120
121impl_scheme_marker!(ExactScheme, "exact");
122impl_scheme_marker!(UptoScheme, "upto");
123impl_scheme_marker!(BatchSettlementScheme, "batch-settlement");
124impl_scheme_marker!(AuthCaptureScheme, "auth-capture");
125
126#[cfg(test)]
127mod marker_tests {
128    use super::*;
129
130    #[test]
131    fn exact_serde_roundtrip() {
132        let encoded = serde_json::to_string(&ExactScheme).unwrap();
133        assert_eq!(encoded, r#""exact""#);
134        let decoded: ExactScheme = serde_json::from_str(&encoded).unwrap();
135        assert_eq!(decoded, ExactScheme);
136    }
137
138    #[test]
139    fn upto_serde_roundtrip() {
140        let encoded = serde_json::to_string(&UptoScheme).unwrap();
141        assert_eq!(encoded, r#""upto""#);
142        let decoded: UptoScheme = serde_json::from_str(&encoded).unwrap();
143        assert_eq!(decoded, UptoScheme);
144    }
145
146    #[test]
147    fn batch_settlement_serde_roundtrip() {
148        let encoded = serde_json::to_string(&BatchSettlementScheme).unwrap();
149        assert_eq!(encoded, r#""batch-settlement""#);
150        let decoded: BatchSettlementScheme = serde_json::from_str(&encoded).unwrap();
151        assert_eq!(decoded, BatchSettlementScheme);
152    }
153
154    #[test]
155    fn auth_capture_serde_roundtrip() {
156        let encoded = serde_json::to_string(&AuthCaptureScheme).unwrap();
157        assert_eq!(encoded, r#""auth-capture""#);
158        let decoded: AuthCaptureScheme = serde_json::from_str(&encoded).unwrap();
159        assert_eq!(decoded, AuthCaptureScheme);
160    }
161
162    #[test]
163    fn wrong_scheme_rejected() {
164        assert!(serde_json::from_str::<ExactScheme>(r#""upto""#).is_err());
165        assert!(serde_json::from_str::<UptoScheme>(r#""exact""#).is_err());
166        assert!(serde_json::from_str::<BatchSettlementScheme>(r#""exact""#).is_err());
167        assert!(serde_json::from_str::<AuthCaptureScheme>(r#""batch-settlement""#).is_err());
168    }
169
170    /// Scheme markers must decode from an owned `serde_json::Value` tree, not
171    /// only from a borrowing `&str` deserializer. `TypedVerifyRequest::from_verify`
172    /// goes through `serde_json::from_value`, which cannot hand out borrowed
173    /// strings.
174    #[test]
175    fn markers_decode_from_owned_value() {
176        let exact: ExactScheme = serde_json::from_value(serde_json::json!("exact")).unwrap();
177        assert_eq!(exact, ExactScheme);
178        let upto: UptoScheme = serde_json::from_value(serde_json::json!("upto")).unwrap();
179        assert_eq!(upto, UptoScheme);
180        let batch: BatchSettlementScheme =
181            serde_json::from_value(serde_json::json!("batch-settlement")).unwrap();
182        assert_eq!(batch, BatchSettlementScheme);
183        let auth: AuthCaptureScheme =
184            serde_json::from_value(serde_json::json!("auth-capture")).unwrap();
185        assert_eq!(auth, AuthCaptureScheme);
186        assert!(serde_json::from_value::<ExactScheme>(serde_json::json!("upto")).is_err());
187    }
188}