1use 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
25pub trait SchemeBuilder<P> {
32 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
44pub 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#[derive(Debug, PartialEq, Eq, Hash, Clone)]
52pub struct SchemeSlug {
53 pub chain_id: ChainId,
55 pub name: CompactString,
57}
58
59impl SchemeSlug {
60 #[must_use]
62 pub const fn new(chain_id: ChainId, name: CompactString) -> Self {
63 Self { chain_id, name }
64 }
65
66 #[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 #[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#[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 #[must_use]
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 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 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 #[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 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(®istry, 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(®istry, 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(®istry, 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(®istry).await.unwrap();
357 assert!(resp.kinds.is_empty());
358 assert!(resp.signers.is_empty());
359 }
360}