pub mod client;
use crate::chain::{ChainId, ChainIdPattern, ChainProviderOps, ChainRegistry};
use crate::proto;
use crate::proto::{AsPaymentProblem, ErrorReason, PaymentProblem, PaymentVerificationError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::marker::PhantomData;
use std::ops::Deref;
#[async_trait::async_trait]
pub trait X402SchemeFacilitator: Send + Sync {
async fn verify(
&self,
request: &proto::VerifyRequest,
) -> Result<proto::VerifyResponse, X402SchemeFacilitatorError>;
async fn settle(
&self,
request: &proto::SettleRequest,
) -> Result<proto::SettleResponse, X402SchemeFacilitatorError>;
async fn supported(&self) -> Result<proto::SupportedResponse, X402SchemeFacilitatorError>;
}
pub trait X402SchemeBlueprint<P>:
X402SchemeId + for<'a> X402SchemeFacilitatorBuilder<&'a P>
{
}
impl<T, P> X402SchemeBlueprint<P> for T where
T: X402SchemeId + for<'a> X402SchemeFacilitatorBuilder<&'a P>
{
}
pub trait X402SchemeId {
fn x402_version(&self) -> u8 {
2
}
fn namespace(&self) -> &str;
fn scheme(&self) -> &str;
fn id(&self) -> String {
format!(
"v{}-{}-{}",
self.x402_version(),
self.namespace(),
self.scheme(),
)
}
}
pub trait X402SchemeFacilitatorBuilder<P> {
fn build(
&self,
provider: P,
config: Option<serde_json::Value>,
) -> Result<Box<dyn X402SchemeFacilitator>, Box<dyn std::error::Error>>;
}
#[derive(Debug, thiserror::Error)]
pub enum X402SchemeFacilitatorError {
#[error(transparent)]
PaymentVerification(#[from] PaymentVerificationError),
#[error("Onchain error: {0}")]
OnchainFailure(String),
}
impl AsPaymentProblem for X402SchemeFacilitatorError {
fn as_payment_problem(&self) -> PaymentProblem {
match self {
X402SchemeFacilitatorError::PaymentVerification(e) => e.as_payment_problem(),
X402SchemeFacilitatorError::OnchainFailure(e) => {
PaymentProblem::new(ErrorReason::UnexpectedError, e.to_string())
}
}
}
}
#[derive(Default)]
pub struct SchemeBlueprints<P>(
HashMap<String, Box<dyn X402SchemeBlueprint<P>>>,
PhantomData<P>,
);
impl<P> Debug for SchemeBlueprints<P> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let slugs: Vec<String> = self.0.keys().map(|s| s.to_string()).collect();
f.debug_tuple("SchemeBlueprints").field(&slugs).finish()
}
}
impl<P> SchemeBlueprints<P> {
pub fn new() -> Self {
Self(HashMap::new(), PhantomData)
}
pub fn and_register<B: X402SchemeBlueprint<P> + 'static>(mut self, blueprint: B) -> Self {
self.register(blueprint);
self
}
pub fn register<B: X402SchemeBlueprint<P> + 'static>(&mut self, blueprint: B) {
self.0.insert(blueprint.id(), Box::new(blueprint));
}
pub fn get(&self, id: &str) -> Option<&dyn X402SchemeBlueprint<P>> {
self.0.get(id).map(|v| v.deref())
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct SchemeHandlerSlug {
pub chain_id: ChainId,
pub x402_version: u8,
pub name: String,
}
impl SchemeHandlerSlug {
pub fn new(chain_id: ChainId, x402_version: u8, name: String) -> Self {
Self {
chain_id,
x402_version,
name,
}
}
}
impl Display for SchemeHandlerSlug {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}:v{}:{}",
self.chain_id.namespace, self.chain_id.reference, self.x402_version, self.name
)
}
}
#[derive(Default)]
pub struct SchemeRegistry(HashMap<SchemeHandlerSlug, Box<dyn X402SchemeFacilitator>>);
impl Debug for SchemeRegistry {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let slugs: Vec<String> = self.0.keys().map(|s| s.to_string()).collect();
f.debug_tuple("SchemeRegistry").field(&slugs).finish()
}
}
impl SchemeRegistry {
pub fn build<P: ChainProviderOps>(
chains: ChainRegistry<P>,
blueprints: SchemeBlueprints<P>,
config: &Vec<SchemeConfig>,
) -> Self {
let mut handlers = HashMap::with_capacity(config.len());
for config in config {
if !config.enabled {
#[cfg(feature = "telemetry")]
tracing::info!(
"Skipping disabled scheme {} for chains {}",
config.id,
config.chains
);
continue;
}
let blueprint = match blueprints.get(&config.id) {
Some(blueprint) => blueprint,
None => {
#[cfg(feature = "telemetry")]
tracing::warn!("No scheme registered: {}", config.id);
continue;
}
};
let chain_providers = chains.by_chain_id_pattern(&config.chains);
if chain_providers.is_empty() {
#[cfg(feature = "telemetry")]
tracing::warn!("No chain provider found for {}", config.chains);
continue;
}
for chain_provider in chain_providers {
let chain_id = chain_provider.chain_id();
let handler = match blueprint.build(chain_provider, config.config.clone()) {
Ok(handler) => handler,
Err(_err) => {
#[cfg(feature = "telemetry")]
tracing::error!(
"Error building scheme handler for {}: {}",
config.id,
_err
);
continue;
}
};
let slug = SchemeHandlerSlug::new(
chain_id.clone(),
blueprint.x402_version(),
blueprint.scheme().to_string(),
);
#[cfg(feature = "telemetry")]
tracing::info!(chain_id = %chain_id, scheme = %blueprint.scheme(), id=blueprint.id(), "Registered scheme handler");
handlers.insert(slug, handler);
}
}
Self(handlers)
}
pub fn by_slug(&self, slug: &SchemeHandlerSlug) -> Option<&dyn X402SchemeFacilitator> {
let handler = self.0.get(slug)?.deref();
Some(handler)
}
pub fn values(&self) -> impl Iterator<Item = &dyn X402SchemeFacilitator> {
self.0.values().map(|v| v.deref())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemeConfig {
#[serde(default = "scheme_config_defaults::default_enabled")]
pub enabled: bool,
pub id: String,
pub chains: ChainIdPattern,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<serde_json::Value>,
}
mod scheme_config_defaults {
pub fn default_enabled() -> bool {
true
}
}