use std::future::Future;
use std::sync::Arc;
use compact_str::CompactString;
use http::{HeaderMap, Uri};
use r402_facilitator::{Facilitator, FacilitatorClient, FacilitatorClientError};
use r402_protocol::extension::Extension;
use r402_protocol::network::{ChainId, ChainIdPattern};
use r402_protocol::payment::PriceTag;
use r402_server::{
PaymentFlowError, PaymentFlowName, ResourceServer, SchemeNetworkServer, schedule,
};
use url::Url;
use super::SettlementMode;
use super::layer::{ResourceTemplate, X402Layer};
use super::pricing::{DynamicPriceTags, StaticPriceTags};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BuildError {
#[error("base_url is required; call with_base_url before with_price_tag")]
MissingBaseUrl,
#[error("missing scheme {scheme} on {network}")]
MissingScheme {
scheme: CompactString,
network: ChainId,
},
#[error(transparent)]
PaymentFlow(#[from] PaymentFlowError),
#[error(transparent)]
Mode(#[from] r402_server::IncompatibleSettlementMode),
#[error(
"with_price_tags([]) requires with_auth_only first; empty static tags are not a layer bypass"
)]
EmptyPriceTags,
#[error("escrow scheme {scheme} is missing settle_on_cancel")]
MissingSettleOnCancel {
scheme: CompactString,
},
}
pub(crate) fn validate_static_layer(
server: &ResourceServer,
tags: &[PriceTag],
mode: SettlementMode,
auth_only: bool,
) -> Result<(), BuildError> {
if tags.is_empty() {
return if auth_only {
Ok(())
} else {
Err(BuildError::EmptyPriceTags)
};
}
for tag in tags {
let requirements = &tag.requirements;
let Some(scheme) =
server.registered_scheme(requirements.scheme.as_str(), &requirements.network)
else {
return Err(BuildError::MissingScheme {
scheme: requirements.scheme.clone(),
network: requirements.network.clone(),
});
};
let flow = match server.get_payment_flow(requirements) {
Ok(flow) => flow,
Err(PaymentFlowError::UnregisteredScheme { .. }) => {
return Err(BuildError::MissingScheme {
scheme: requirements.scheme.clone(),
network: requirements.network.clone(),
});
}
Err(err) => return Err(BuildError::PaymentFlow(err)),
};
if flow == PaymentFlowName::Escrow && !scheme.settles_on_cancel() {
return Err(BuildError::MissingSettleOnCancel {
scheme: requirements.scheme.clone(),
});
}
schedule(flow.phases(), mode)?;
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct X402Middleware {
server: ResourceServer,
base_url: Option<Url>,
#[cfg(feature = "siwx")]
siwx: Option<Arc<super::SiwxGate>>,
}
impl X402Middleware {
#[must_use]
pub fn from_facilitator(fac: impl Facilitator + 'static) -> Self {
Self::from_resource_server(ResourceServer::new(Arc::new(fac)))
}
#[must_use]
pub const fn from_resource_server(server: ResourceServer) -> Self {
Self {
server,
base_url: None,
#[cfg(feature = "siwx")]
siwx: None,
}
}
pub fn try_new(url: &str) -> Result<Self, FacilitatorClientError> {
Ok(Self::from_facilitator(FacilitatorClient::try_from(url)?))
}
#[must_use]
pub fn with_scheme(
mut self,
network: ChainIdPattern,
scheme: impl SchemeNetworkServer + 'static,
) -> Self {
self.server.register_scheme(network, scheme);
self
}
#[must_use]
pub fn with_extension(mut self, extension: impl Extension + 'static) -> Self {
self.server = self.server.with_extension(extension);
self
}
#[must_use]
pub fn with_base_url(&self, base_url: Url) -> Self {
let mut this = self.clone();
this.base_url = Some(base_url);
this
}
#[must_use]
pub const fn resource_server(&self) -> &ResourceServer {
&self.server
}
#[cfg(feature = "siwx")]
#[must_use]
pub fn with_siwx(&self, gate: super::SiwxGate) -> Self {
let mut this = self.clone();
this.siwx = Some(Arc::new(gate));
this
}
#[cfg(feature = "siwx")]
#[must_use]
pub fn with_auth_only(&self, gate: super::SiwxGate) -> Self {
self.with_siwx(gate.with_auth_only())
}
fn auth_only(&self) -> bool {
#[cfg(feature = "siwx")]
{
self.siwx.as_ref().is_some_and(|g| g.is_auth_only())
}
#[cfg(not(feature = "siwx"))]
{
let _ = self;
false
}
}
pub fn with_price_tag(
&self,
price_tag: PriceTag,
) -> Result<X402Layer<StaticPriceTags>, BuildError> {
self.with_price_tags(vec![price_tag])
}
pub fn with_price_tags(
&self,
price_tags: Vec<PriceTag>,
) -> Result<X402Layer<StaticPriceTags>, BuildError> {
let base_url = Arc::new(self.base_url.clone().ok_or(BuildError::MissingBaseUrl)?);
validate_static_layer(
&self.server,
&price_tags,
SettlementMode::default(),
self.auth_only(),
)?;
Ok(X402Layer {
server: self.server.clone(),
price_source: StaticPriceTags::new(price_tags),
base_url,
resource: Arc::new(ResourceTemplate::default()),
settlement_mode: SettlementMode::default(),
settlement_tracker: None,
hooks: None,
#[cfg(feature = "siwx")]
siwx: self.siwx.clone(),
})
}
pub fn with_dynamic_price<F, Fut>(
&self,
callback: F,
) -> Result<X402Layer<DynamicPriceTags>, BuildError>
where
F: Fn(&HeaderMap, &Uri, &Url) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Vec<PriceTag>> + Send + 'static,
{
Ok(X402Layer {
server: self.server.clone(),
price_source: DynamicPriceTags::new(callback),
base_url: Arc::new(self.base_url.clone().ok_or(BuildError::MissingBaseUrl)?),
resource: Arc::new(ResourceTemplate::default()),
settlement_mode: SettlementMode::default(),
settlement_tracker: None,
hooks: None,
#[cfg(feature = "siwx")]
siwx: self.siwx.clone(),
})
}
}
impl TryFrom<&str> for X402Middleware {
type Error = FacilitatorClientError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::try_new(value)
}
}
impl TryFrom<String> for X402Middleware {
type Error = FacilitatorClientError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_new(&value)
}
}