Skip to main content

r402_core/scheme/
registry.rs

1//! Scheme handler builder, blueprint trait, and handler registry.
2//!
3//! [`SchemeBuilder`] defines how to construct a [`Facilitator`] from a
4//! chain provider. [`SchemeBlueprint`] combines identity
5//! ([`super::SchemeId`]) with building capability so the registry can
6//! create handlers in a single call.
7//!
8//! [`SchemeRegistry`] stores the active handlers keyed by
9//! [`SchemeSlug`] (chain + scheme name) and itself implements
10//! [`Facilitator`], dispatching to the appropriate handler per request.
11
12use std::collections::HashMap;
13use std::fmt::{self, Debug, Display, Formatter};
14
15use compact_str::CompactString;
16
17use super::SchemeId;
18use crate::chain::{ChainId, ChainProvider};
19use crate::error::FacilitatorError;
20use crate::facilitator::{DynFacilitator, Facilitator};
21use crate::wire::{
22    SettleRequest, SettleResponse, SupportedResponse, VerifyRequest, VerifyResponse,
23};
24
25/// Builds facilitator instances from a chain provider.
26///
27/// Each chain crate ships a blueprint per scheme (e.g.
28/// `ExactEvmBlueprint`, `UptoEvmBlueprint`) that knows how to construct a
29/// `Box<dyn DynFacilitator>` given a chain provider reference and optional
30/// JSON configuration.
31pub trait SchemeBuilder<P> {
32    /// Builds a facilitator for the given chain provider.
33    ///
34    /// # Errors
35    ///
36    /// Returns an error if the facilitator cannot be built.
37    fn build(
38        &self,
39        provider: P,
40        config: Option<serde_json::Value>,
41    ) -> Result<Box<dyn DynFacilitator>, Box<dyn std::error::Error + Send + Sync>>;
42}
43
44/// Marker trait combining identity ([`SchemeId`]) with build capability.
45///
46/// Blanket-implemented for any type that is both.
47pub trait SchemeBlueprint<P>: SchemeId + for<'a> SchemeBuilder<&'a P> {}
48impl<T, P> SchemeBlueprint<P> for T where T: SchemeId + for<'a> SchemeBuilder<&'a P> {}
49
50/// Unique identifier for a scheme handler instance.
51#[derive(Debug, PartialEq, Eq, Hash, Clone)]
52pub struct SchemeSlug {
53    /// Chain this handler operates on.
54    pub chain_id: ChainId,
55    /// Scheme name (e.g. `"exact"`, `"upto"`).
56    pub name: CompactString,
57}
58
59impl SchemeSlug {
60    /// Constructs a new slug.
61    #[must_use]
62    pub const fn new(chain_id: ChainId, name: CompactString) -> Self {
63        Self { chain_id, name }
64    }
65
66    /// Returns a wildcard version with the reference replaced by `*`.
67    #[must_use]
68    pub fn as_wildcard(&self) -> Self {
69        Self {
70            chain_id: ChainId::new(self.chain_id.namespace(), "*"),
71            name: self.name.clone(),
72        }
73    }
74
75    /// Returns `true` when the reference is the wildcard `*`.
76    #[must_use]
77    pub fn is_wildcard(&self) -> bool {
78        self.chain_id.reference() == "*"
79    }
80}
81
82impl Display for SchemeSlug {
83    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
84        write!(
85            f,
86            "{}:{}:{}",
87            self.chain_id.namespace(),
88            self.chain_id.reference(),
89            self.name,
90        )
91    }
92}
93
94/// Registry of active scheme handlers.
95#[derive(Default)]
96pub struct SchemeRegistry {
97    handlers: HashMap<SchemeSlug, Box<dyn DynFacilitator>>,
98}
99
100impl Debug for SchemeRegistry {
101    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
102        let slugs: Vec<String> = self.handlers.keys().map(ToString::to_string).collect();
103        f.debug_tuple("SchemeRegistry").field(&slugs).finish()
104    }
105}
106
107impl SchemeRegistry {
108    /// Constructs an empty registry.
109    #[must_use]
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Registers a handler for the given blueprint and provider.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the blueprint fails to build.
119    pub fn register<P: ChainProvider>(
120        &mut self,
121        blueprint: &dyn SchemeBlueprint<P>,
122        provider: &P,
123        config: Option<serde_json::Value>,
124    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
125        let chain_id = provider.chain_id();
126        let handler = blueprint.build(provider, config)?;
127        let slug = SchemeSlug::new(chain_id, CompactString::from(blueprint.scheme()));
128        let _ = self.handlers.insert(slug, handler);
129        Ok(())
130    }
131
132    /// Registers a single handler for an entire CAIP-2 namespace.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if the blueprint fails to build.
137    pub fn register_for_namespace<P: ChainProvider>(
138        &mut self,
139        blueprint: &dyn SchemeBlueprint<P>,
140        provider: &P,
141        config: Option<serde_json::Value>,
142    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
143        let handler = blueprint.build(provider, config)?;
144        let namespace = provider.chain_id().namespace().to_owned();
145        let slug = SchemeSlug::new(
146            ChainId::new(namespace, "*"),
147            CompactString::from(blueprint.scheme()),
148        );
149        let _ = self.handlers.insert(slug, handler);
150        Ok(())
151    }
152
153    /// Looks up a handler with wildcard fallback.
154    #[must_use]
155    pub fn by_slug(&self, slug: &SchemeSlug) -> Option<&dyn DynFacilitator> {
156        self.handlers
157            .get(slug)
158            .or_else(|| self.handlers.get(&slug.as_wildcard()))
159            .map(|h| &**h)
160    }
161
162    /// Iterates over registered handlers.
163    pub fn values(&self) -> impl Iterator<Item = &dyn DynFacilitator> {
164        self.handlers.values().map(|v| &**v)
165    }
166
167    async fn collect_supported(&self) -> SupportedResponse {
168        let mut kinds = Vec::new();
169        let mut signers: HashMap<CompactString, Vec<CompactString>> = HashMap::new();
170        for handler in self.values() {
171            let Ok(mut resp) = handler.supported().await else {
172                continue;
173            };
174            kinds.append(&mut resp.kinds);
175            for (family, addrs) in resp.signers {
176                signers.entry(family).or_default().extend(addrs);
177            }
178        }
179        for addrs in signers.values_mut() {
180            addrs.sort_unstable();
181            addrs.dedup();
182        }
183        SupportedResponse {
184            kinds,
185            extensions: Vec::new(),
186            signers,
187        }
188    }
189
190    fn require_handler(
191        &self,
192        slug: Option<SchemeSlug>,
193    ) -> Result<&dyn DynFacilitator, FacilitatorError> {
194        slug.and_then(|s| self.by_slug(&s)).ok_or_else(|| {
195            FacilitatorError::aborted(
196                "no_facilitator_for_network",
197                "no handler registered for this payment scheme",
198            )
199        })
200    }
201}
202
203impl Facilitator for SchemeRegistry {
204    async fn verify(&self, request: VerifyRequest) -> Result<VerifyResponse, FacilitatorError> {
205        let handler = self.require_handler(request.scheme_slug())?;
206        handler.verify(request).await
207    }
208
209    async fn settle(&self, request: SettleRequest) -> Result<SettleResponse, FacilitatorError> {
210        let handler = self.require_handler(request.scheme_slug())?;
211        handler.settle(request).await
212    }
213
214    async fn supported(&self) -> Result<SupportedResponse, FacilitatorError> {
215        Ok(self.collect_supported().await)
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use std::future::Future;
222
223    use super::*;
224    use crate::wire::Extensions;
225
226    struct StubFacilitator(CompactString);
227
228    impl Facilitator for StubFacilitator {
229        fn verify(
230            &self,
231            _request: VerifyRequest,
232        ) -> impl Future<Output = Result<VerifyResponse, FacilitatorError>> + Send {
233            std::future::ready(Ok(VerifyResponse::valid(self.0.clone())))
234        }
235
236        fn settle(
237            &self,
238            _request: SettleRequest,
239        ) -> impl Future<Output = Result<SettleResponse, FacilitatorError>> + Send {
240            std::future::ready(Ok(SettleResponse::Success {
241                payer: "0x".into(),
242                transaction: "0x".into(),
243                network: "eip155:1".into(),
244                amount: None,
245                extensions: Extensions::new(),
246            }))
247        }
248
249        fn supported(
250            &self,
251        ) -> impl Future<Output = Result<SupportedResponse, FacilitatorError>> + Send {
252            std::future::ready(Ok(SupportedResponse::default()))
253        }
254    }
255
256    fn make_verify(network: &str, scheme: &str) -> VerifyRequest {
257        serde_json::json!({
258            "x402Version": 2,
259            "paymentPayload": {
260                "accepted": { "network": network, "scheme": scheme }
261            },
262            "paymentRequirements": { "network": network }
263        })
264        .into()
265    }
266
267    #[test]
268    fn slug_display_format() {
269        let slug = SchemeSlug::new(ChainId::new("eip155", "8453"), "exact".into());
270        assert_eq!(slug.to_string(), "eip155:8453:exact");
271    }
272
273    #[test]
274    fn slug_wildcard_conversion() {
275        let slug = SchemeSlug::new(ChainId::new("eip155", "8453"), "exact".into());
276        assert!(!slug.is_wildcard());
277        let wild = slug.as_wildcard();
278        assert!(wild.is_wildcard());
279    }
280
281    #[test]
282    fn by_slug_exact_hit_and_miss() {
283        let mut registry = SchemeRegistry::new();
284        let slug = SchemeSlug::new(ChainId::new("eip155", "1"), "exact".into());
285        let _ = registry
286            .handlers
287            .insert(slug.clone(), Box::new(StubFacilitator("eth".into())));
288        assert!(registry.by_slug(&slug).is_some());
289
290        let miss = SchemeSlug::new(ChainId::new("eip155", "999"), "exact".into());
291        assert!(registry.by_slug(&miss).is_none());
292    }
293
294    #[test]
295    fn by_slug_wildcard_fallback() {
296        let mut registry = SchemeRegistry::new();
297        let wild = SchemeSlug::new(ChainId::new("eip155", "*"), "exact".into());
298        let _ = registry
299            .handlers
300            .insert(wild, Box::new(StubFacilitator("evm".into())));
301
302        let query = SchemeSlug::new(ChainId::new("eip155", "42161"), "exact".into());
303        assert!(registry.by_slug(&query).is_some());
304    }
305
306    #[test]
307    fn by_slug_exact_takes_priority() {
308        let mut registry = SchemeRegistry::new();
309        let wild = SchemeSlug::new(ChainId::new("eip155", "*"), "exact".into());
310        let exact = SchemeSlug::new(ChainId::new("eip155", "1"), "exact".into());
311        let _ = registry
312            .handlers
313            .insert(wild, Box::new(StubFacilitator("wild".into())));
314        let _ = registry
315            .handlers
316            .insert(exact.clone(), Box::new(StubFacilitator("exact".into())));
317        assert!(registry.by_slug(&exact).is_some());
318    }
319
320    fn stub(tag: &str) -> Box<dyn DynFacilitator> {
321        Box::new(StubFacilitator(tag.into()))
322    }
323
324    #[tokio::test]
325    async fn dispatch_verify_routes_to_correct_handler() {
326        let mut registry = SchemeRegistry::new();
327        let slug = SchemeSlug::new(ChainId::new("eip155", "8453"), "exact".into());
328        let _ = registry.handlers.insert(slug, stub("base_handler"));
329        let resp = Facilitator::verify(&registry, make_verify("eip155:8453", "exact"))
330            .await
331            .unwrap();
332        assert!(resp.is_valid());
333    }
334
335    #[tokio::test]
336    async fn dispatch_no_handler_returns_aborted() {
337        let registry = SchemeRegistry::new();
338        let err = Facilitator::verify(&registry, make_verify("eip155:999", "exact"))
339            .await
340            .unwrap_err();
341        assert!(matches!(err, FacilitatorError::Aborted { reason, .. }
342            if reason == "no_facilitator_for_network"));
343    }
344
345    #[tokio::test]
346    async fn dispatch_malformed_request_returns_aborted() {
347        let registry = SchemeRegistry::new();
348        let req: VerifyRequest = serde_json::json!({}).into();
349        let err = Facilitator::verify(&registry, req).await.unwrap_err();
350        assert!(matches!(err, FacilitatorError::Aborted { .. }));
351    }
352
353    #[tokio::test]
354    async fn collect_supported_empty_registry() {
355        let registry = SchemeRegistry::new();
356        let resp = Facilitator::supported(&registry).await.unwrap();
357        assert!(resp.kinds.is_empty());
358        assert!(resp.signers.is_empty());
359    }
360}