o402 0.1.3

OpenAI-compatible gateway, paid with x402.
//! Resource server factory: facilitator client and scheme adapters.

use std::time::Duration;

use r402_http::server::{FacilitatorClient, X402Middleware};
use r402_protocol::network::ChainIdPattern;
use r402_server::ResourceServer;
use url::Url;

use crate::config::{ConfigError, FacilitatorAuth, PaymentConfig};

/// Builds a [`ResourceServer`] from payment config. No seller keys.
///
/// # Errors
///
/// Returns [`ConfigError::Validation`] when the facilitator URL cannot be
/// turned into a client or a CAIP-2 pattern cannot be parsed.
pub(crate) fn resource_server(
    payment: &PaymentConfig,
    base_url: &Url,
) -> Result<ResourceServer, ConfigError> {
    let fac_cfg = payment.facilitator.as_ref().ok_or_else(|| {
        ConfigError::Validation(
            "payment.facilitator.url is required when payment.enabled = true".to_owned(),
        )
    })?;
    let url = fac_cfg.url.as_ref().ok_or_else(|| {
        ConfigError::Validation(
            "payment.facilitator.url is required when payment.enabled = true".to_owned(),
        )
    })?;
    let mut fac = FacilitatorClient::try_from(url.as_str())
        .map_err(|error| ConfigError::Validation(format!("facilitator client: {error}")))?;
    fac = fac.with_timeout(Duration::from_secs(fac_cfg.timeout_secs));
    fac = if fac_cfg.supported_cache_ttl_secs == 0 {
        fac.without_supported_cache()
    } else {
        fac.with_supported_cache_ttl(Duration::from_secs(fac_cfg.supported_cache_ttl_secs))
    };
    if let Some(auth) = fac_cfg.auth.clone() {
        fac = fac.with_auth(move || {
            let auth = auth.clone();
            async move { Ok(auth.to_path_keyed_json()) }
        });
    }

    let mut mw = X402Middleware::from_facilitator(fac).with_base_url(base_url.clone());
    mw = register_schemes(mw)?;
    Ok(mw.resource_server().clone())
}

fn register_schemes(mut mw: X402Middleware) -> Result<X402Middleware, ConfigError> {
    #[cfg(feature = "evm")]
    {
        mw = mw.with_scheme(parse_pattern("eip155:*")?, r402_evm::Eip155Exact);
        mw = mw.with_scheme(parse_pattern("eip155:*")?, r402_evm::Eip155Upto);
    }
    #[cfg(feature = "svm")]
    {
        mw = mw.with_scheme(parse_pattern("solana:*")?, r402_svm::SolanaExact);
    }
    #[cfg(feature = "near")]
    {
        mw = mw.with_scheme(parse_pattern("near:*")?, r402_near::NearExact);
    }
    #[cfg(feature = "xrpl")]
    {
        mw = mw.with_scheme(parse_pattern("xrpl:*")?, r402_xrpl::XrplExact);
    }
    #[cfg(feature = "hedera")]
    {
        mw = mw.with_scheme(parse_pattern("hedera:*")?, r402_hedera::HederaExact);
    }
    #[cfg(feature = "avm")]
    {
        mw = mw.with_scheme(parse_pattern("algorand:*")?, r402_avm::AlgorandExact);
    }
    #[cfg(feature = "aptos")]
    {
        mw = mw.with_scheme(parse_pattern("aptos:*")?, r402_aptos::AptosExact);
    }
    #[cfg(feature = "keeta")]
    {
        mw = mw.with_scheme(parse_pattern("keeta:*")?, r402_keeta::KeetaExact);
    }
    #[cfg(feature = "tvm")]
    {
        mw = mw.with_scheme(parse_pattern("tvm:*")?, r402_tvm::TvmExact);
    }
    #[cfg(feature = "stellar")]
    {
        mw = mw.with_scheme(parse_pattern("stellar:*")?, r402_stellar::StellarExact);
    }
    #[cfg(feature = "concordium")]
    {
        mw = mw.with_scheme(
            parse_pattern("ccd:*")?,
            r402_concordium::ConcordiumExact::new(),
        );
    }
    #[cfg(feature = "experimental-tron")]
    {
        mw = mw.with_scheme(parse_pattern("tron:*")?, r402_tron::TronExact);
    }
    let _ = &mut mw;
    Ok(mw)
}

fn parse_pattern(raw: &str) -> Result<ChainIdPattern, ConfigError> {
    raw.parse()
        .map_err(|error| ConfigError::Validation(format!("invalid chain pattern '{raw}': {error}")))
}

impl FacilitatorAuth {
    /// Path-keyed JSON for [`FacilitatorClient::with_auth`]. Empty maps omitted.
    pub(crate) fn to_path_keyed_json(&self) -> serde_json::Value {
        let mut root = serde_json::Map::new();
        insert_path(&mut root, "verify", &self.verify);
        insert_path(&mut root, "settle", &self.settle);
        insert_path(&mut root, "supported", &self.supported);
        serde_json::Value::Object(root)
    }
}

fn insert_path(
    root: &mut serde_json::Map<String, serde_json::Value>,
    path: &str,
    headers: &indexmap::IndexMap<String, crate::config::Secret<String>>,
) {
    if headers.is_empty() {
        return;
    }
    let mut object = serde_json::Map::new();
    for (name, value) in headers {
        object.insert(
            name.clone(),
            serde_json::Value::String(value.expose().clone()),
        );
    }
    root.insert(path.to_owned(), serde_json::Value::Object(object));
}