use std::collections::HashMap;
use std::fmt::{self, Debug, Display, Formatter};
use compact_str::CompactString;
use super::SchemeId;
use crate::chain::{ChainId, ChainProvider};
use crate::error::FacilitatorError;
use crate::facilitator::{DynFacilitator, Facilitator};
use crate::wire::{
SettleRequest, SettleResponse, SupportedResponse, VerifyRequest, VerifyResponse,
};
pub trait SchemeBuilder<P> {
fn build(
&self,
provider: P,
config: Option<serde_json::Value>,
) -> Result<Box<dyn DynFacilitator>, Box<dyn std::error::Error + Send + Sync>>;
}
pub trait SchemeBlueprint<P>: SchemeId + for<'a> SchemeBuilder<&'a P> {}
impl<T, P> SchemeBlueprint<P> for T where T: SchemeId + for<'a> SchemeBuilder<&'a P> {}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct SchemeSlug {
pub chain_id: ChainId,
pub name: CompactString,
}
impl SchemeSlug {
#[must_use]
pub const fn new(chain_id: ChainId, name: CompactString) -> Self {
Self { chain_id, name }
}
#[must_use]
pub fn as_wildcard(&self) -> Self {
Self {
chain_id: ChainId::new(self.chain_id.namespace(), "*"),
name: self.name.clone(),
}
}
#[must_use]
pub fn is_wildcard(&self) -> bool {
self.chain_id.reference() == "*"
}
}
impl Display for SchemeSlug {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}:{}",
self.chain_id.namespace(),
self.chain_id.reference(),
self.name,
)
}
}
#[derive(Default)]
pub struct SchemeRegistry {
handlers: HashMap<SchemeSlug, Box<dyn DynFacilitator>>,
}
impl Debug for SchemeRegistry {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let slugs: Vec<String> = self.handlers.keys().map(ToString::to_string).collect();
f.debug_tuple("SchemeRegistry").field(&slugs).finish()
}
}
impl SchemeRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register<P: ChainProvider>(
&mut self,
blueprint: &dyn SchemeBlueprint<P>,
provider: &P,
config: Option<serde_json::Value>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let chain_id = provider.chain_id();
let handler = blueprint.build(provider, config)?;
let slug = SchemeSlug::new(chain_id, CompactString::from(blueprint.scheme()));
let _ = self.handlers.insert(slug, handler);
Ok(())
}
pub fn register_for_namespace<P: ChainProvider>(
&mut self,
blueprint: &dyn SchemeBlueprint<P>,
provider: &P,
config: Option<serde_json::Value>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let handler = blueprint.build(provider, config)?;
let namespace = provider.chain_id().namespace().to_owned();
let slug = SchemeSlug::new(
ChainId::new(namespace, "*"),
CompactString::from(blueprint.scheme()),
);
let _ = self.handlers.insert(slug, handler);
Ok(())
}
#[must_use]
pub fn by_slug(&self, slug: &SchemeSlug) -> Option<&dyn DynFacilitator> {
self.handlers
.get(slug)
.or_else(|| self.handlers.get(&slug.as_wildcard()))
.map(|h| &**h)
}
pub fn values(&self) -> impl Iterator<Item = &dyn DynFacilitator> {
self.handlers.values().map(|v| &**v)
}
async fn collect_supported(&self) -> SupportedResponse {
let mut kinds = Vec::new();
let mut signers: HashMap<CompactString, Vec<CompactString>> = HashMap::new();
for handler in self.values() {
let Ok(mut resp) = handler.supported().await else {
continue;
};
kinds.append(&mut resp.kinds);
for (family, addrs) in resp.signers {
signers.entry(family).or_default().extend(addrs);
}
}
for addrs in signers.values_mut() {
addrs.sort_unstable();
addrs.dedup();
}
SupportedResponse {
kinds,
extensions: Vec::new(),
signers,
}
}
fn require_handler(
&self,
slug: Option<SchemeSlug>,
) -> Result<&dyn DynFacilitator, FacilitatorError> {
slug.and_then(|s| self.by_slug(&s)).ok_or_else(|| {
FacilitatorError::aborted(
"no_facilitator_for_network",
"no handler registered for this payment scheme",
)
})
}
}
impl Facilitator for SchemeRegistry {
async fn verify(&self, request: VerifyRequest) -> Result<VerifyResponse, FacilitatorError> {
let handler = self.require_handler(request.scheme_slug())?;
handler.verify(request).await
}
async fn settle(&self, request: SettleRequest) -> Result<SettleResponse, FacilitatorError> {
let handler = self.require_handler(request.scheme_slug())?;
handler.settle(request).await
}
async fn supported(&self) -> Result<SupportedResponse, FacilitatorError> {
Ok(self.collect_supported().await)
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use super::*;
use crate::wire::Extensions;
struct StubFacilitator(CompactString);
impl Facilitator for StubFacilitator {
fn verify(
&self,
_request: VerifyRequest,
) -> impl Future<Output = Result<VerifyResponse, FacilitatorError>> + Send {
std::future::ready(Ok(VerifyResponse::valid(self.0.clone())))
}
fn settle(
&self,
_request: SettleRequest,
) -> impl Future<Output = Result<SettleResponse, FacilitatorError>> + Send {
std::future::ready(Ok(SettleResponse::Success {
payer: "0x".into(),
transaction: "0x".into(),
network: "eip155:1".into(),
amount: None,
extensions: Extensions::new(),
}))
}
fn supported(
&self,
) -> impl Future<Output = Result<SupportedResponse, FacilitatorError>> + Send {
std::future::ready(Ok(SupportedResponse::default()))
}
}
fn make_verify(network: &str, scheme: &str) -> VerifyRequest {
serde_json::json!({
"x402Version": 2,
"paymentPayload": {
"accepted": { "network": network, "scheme": scheme }
},
"paymentRequirements": { "network": network }
})
.into()
}
#[test]
fn slug_display_format() {
let slug = SchemeSlug::new(ChainId::new("eip155", "8453"), "exact".into());
assert_eq!(slug.to_string(), "eip155:8453:exact");
}
#[test]
fn slug_wildcard_conversion() {
let slug = SchemeSlug::new(ChainId::new("eip155", "8453"), "exact".into());
assert!(!slug.is_wildcard());
let wild = slug.as_wildcard();
assert!(wild.is_wildcard());
}
#[test]
fn by_slug_exact_hit_and_miss() {
let mut registry = SchemeRegistry::new();
let slug = SchemeSlug::new(ChainId::new("eip155", "1"), "exact".into());
let _ = registry
.handlers
.insert(slug.clone(), Box::new(StubFacilitator("eth".into())));
assert!(registry.by_slug(&slug).is_some());
let miss = SchemeSlug::new(ChainId::new("eip155", "999"), "exact".into());
assert!(registry.by_slug(&miss).is_none());
}
#[test]
fn by_slug_wildcard_fallback() {
let mut registry = SchemeRegistry::new();
let wild = SchemeSlug::new(ChainId::new("eip155", "*"), "exact".into());
let _ = registry
.handlers
.insert(wild, Box::new(StubFacilitator("evm".into())));
let query = SchemeSlug::new(ChainId::new("eip155", "42161"), "exact".into());
assert!(registry.by_slug(&query).is_some());
}
#[test]
fn by_slug_exact_takes_priority() {
let mut registry = SchemeRegistry::new();
let wild = SchemeSlug::new(ChainId::new("eip155", "*"), "exact".into());
let exact = SchemeSlug::new(ChainId::new("eip155", "1"), "exact".into());
let _ = registry
.handlers
.insert(wild, Box::new(StubFacilitator("wild".into())));
let _ = registry
.handlers
.insert(exact.clone(), Box::new(StubFacilitator("exact".into())));
assert!(registry.by_slug(&exact).is_some());
}
fn stub(tag: &str) -> Box<dyn DynFacilitator> {
Box::new(StubFacilitator(tag.into()))
}
#[tokio::test]
async fn dispatch_verify_routes_to_correct_handler() {
let mut registry = SchemeRegistry::new();
let slug = SchemeSlug::new(ChainId::new("eip155", "8453"), "exact".into());
let _ = registry.handlers.insert(slug, stub("base_handler"));
let resp = Facilitator::verify(®istry, make_verify("eip155:8453", "exact"))
.await
.unwrap();
assert!(resp.is_valid());
}
#[tokio::test]
async fn dispatch_no_handler_returns_aborted() {
let registry = SchemeRegistry::new();
let err = Facilitator::verify(®istry, make_verify("eip155:999", "exact"))
.await
.unwrap_err();
assert!(matches!(err, FacilitatorError::Aborted { reason, .. }
if reason == "no_facilitator_for_network"));
}
#[tokio::test]
async fn dispatch_malformed_request_returns_aborted() {
let registry = SchemeRegistry::new();
let req: VerifyRequest = serde_json::json!({}).into();
let err = Facilitator::verify(®istry, req).await.unwrap_err();
assert!(matches!(err, FacilitatorError::Aborted { .. }));
}
#[tokio::test]
async fn collect_supported_empty_registry() {
let registry = SchemeRegistry::new();
let resp = Facilitator::supported(®istry).await.unwrap();
assert!(resp.kinds.is_empty());
assert!(resp.signers.is_empty());
}
}