1use std::future::Future;
4use std::sync::Arc;
5
6use compact_str::CompactString;
7use http::{HeaderMap, Uri};
8use r402_facilitator::{Facilitator, FacilitatorClient, FacilitatorClientError};
9use r402_protocol::network::{ChainId, ChainIdPattern};
10use r402_protocol::payment::PriceTag;
11use r402_server::{
12 PaymentFlowError, PaymentFlowName, ResourceServer, SchemeNetworkServer, schedule,
13};
14use url::Url;
15
16use super::SettlementMode;
17use super::layer::{ResourceTemplate, X402Layer};
18use super::pricing::{DynamicPriceTags, StaticPriceTags};
19
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22pub enum BuildError {
23 #[error("base_url is required; call with_base_url before with_price_tag")]
27 MissingBaseUrl,
28 #[error("missing scheme {scheme} on {network}")]
30 MissingScheme {
31 scheme: CompactString,
33 network: ChainId,
35 },
36 #[error(transparent)]
38 PaymentFlow(#[from] PaymentFlowError),
39 #[error(transparent)]
41 Mode(#[from] r402_server::IncompatibleSettlementMode),
42 #[error(
44 "with_price_tags([]) requires with_auth_only first; empty static tags are not a layer bypass"
45 )]
46 EmptyPriceTags,
47 #[error("escrow scheme {scheme} is missing settle_on_cancel")]
49 MissingSettleOnCancel {
50 scheme: CompactString,
52 },
53}
54
55pub(crate) fn validate_static_layer(
57 server: &ResourceServer,
58 tags: &[PriceTag],
59 mode: SettlementMode,
60 auth_only: bool,
61) -> Result<(), BuildError> {
62 if tags.is_empty() {
63 return if auth_only {
64 Ok(())
65 } else {
66 Err(BuildError::EmptyPriceTags)
67 };
68 }
69
70 for tag in tags {
71 let requirements = &tag.requirements;
72 let Some(scheme) =
73 server.registered_scheme(requirements.scheme.as_str(), &requirements.network)
74 else {
75 return Err(BuildError::MissingScheme {
76 scheme: requirements.scheme.clone(),
77 network: requirements.network.clone(),
78 });
79 };
80
81 let flow = match server.get_payment_flow(requirements) {
82 Ok(flow) => flow,
83 Err(PaymentFlowError::UnregisteredScheme { .. }) => {
84 return Err(BuildError::MissingScheme {
85 scheme: requirements.scheme.clone(),
86 network: requirements.network.clone(),
87 });
88 }
89 Err(err) => return Err(BuildError::PaymentFlow(err)),
90 };
91
92 if flow == PaymentFlowName::Escrow && !scheme.settles_on_cancel() {
93 return Err(BuildError::MissingSettleOnCancel {
94 scheme: requirements.scheme.clone(),
95 });
96 }
97
98 schedule(flow.phases(), mode)?;
99 }
100 Ok(())
101}
102
103#[derive(Clone, Debug)]
107pub struct X402Middleware {
108 server: ResourceServer,
109 base_url: Option<Url>,
110 #[cfg(feature = "siwx")]
111 siwx: Option<Arc<super::SiwxGate>>,
112}
113
114impl X402Middleware {
115 #[must_use]
117 pub fn from_facilitator(fac: impl Facilitator + 'static) -> Self {
118 Self::from_resource_server(ResourceServer::new(Arc::new(fac)))
119 }
120
121 #[must_use]
123 pub const fn from_resource_server(server: ResourceServer) -> Self {
124 Self {
125 server,
126 base_url: None,
127 #[cfg(feature = "siwx")]
128 siwx: None,
129 }
130 }
131
132 pub fn try_new(url: &str) -> Result<Self, FacilitatorClientError> {
138 Ok(Self::from_facilitator(FacilitatorClient::try_from(url)?))
139 }
140
141 #[must_use]
143 pub fn with_scheme(
144 mut self,
145 network: ChainIdPattern,
146 scheme: impl SchemeNetworkServer + 'static,
147 ) -> Self {
148 self.server.register_scheme(network, scheme);
149 self
150 }
151
152 #[must_use]
156 pub fn with_base_url(&self, base_url: Url) -> Self {
157 let mut this = self.clone();
158 this.base_url = Some(base_url);
159 this
160 }
161
162 #[must_use]
164 pub const fn resource_server(&self) -> &ResourceServer {
165 &self.server
166 }
167
168 #[cfg(feature = "siwx")]
173 #[must_use]
174 pub fn with_siwx(&self, gate: super::SiwxGate) -> Self {
175 let mut this = self.clone();
176 this.siwx = Some(Arc::new(gate));
177 this
178 }
179
180 #[cfg(feature = "siwx")]
185 #[must_use]
186 pub fn with_auth_only(&self, gate: super::SiwxGate) -> Self {
187 self.with_siwx(gate.with_auth_only())
188 }
189
190 fn auth_only(&self) -> bool {
191 #[cfg(feature = "siwx")]
192 {
193 self.siwx.as_ref().is_some_and(|g| g.is_auth_only())
194 }
195 #[cfg(not(feature = "siwx"))]
196 {
197 let _ = self;
198 false
199 }
200 }
201
202 pub fn with_price_tag(
209 &self,
210 price_tag: PriceTag,
211 ) -> Result<X402Layer<StaticPriceTags>, BuildError> {
212 self.with_price_tags(vec![price_tag])
213 }
214
215 pub fn with_price_tags(
226 &self,
227 price_tags: Vec<PriceTag>,
228 ) -> Result<X402Layer<StaticPriceTags>, BuildError> {
229 let base_url = Arc::new(self.base_url.clone().ok_or(BuildError::MissingBaseUrl)?);
230 validate_static_layer(
231 &self.server,
232 &price_tags,
233 SettlementMode::default(),
234 self.auth_only(),
235 )?;
236 Ok(X402Layer {
237 server: self.server.clone(),
238 price_source: StaticPriceTags::new(price_tags),
239 base_url,
240 resource: Arc::new(ResourceTemplate::default()),
241 settlement_mode: SettlementMode::default(),
242 settlement_tracker: None,
243 hooks: None,
244 #[cfg(feature = "siwx")]
245 siwx: self.siwx.clone(),
246 })
247 }
248
249 pub fn with_dynamic_price<F, Fut>(
255 &self,
256 callback: F,
257 ) -> Result<X402Layer<DynamicPriceTags>, BuildError>
258 where
259 F: Fn(&HeaderMap, &Uri, &Url) -> Fut + Send + Sync + 'static,
260 Fut: Future<Output = Vec<PriceTag>> + Send + 'static,
261 {
262 Ok(X402Layer {
263 server: self.server.clone(),
264 price_source: DynamicPriceTags::new(callback),
265 base_url: Arc::new(self.base_url.clone().ok_or(BuildError::MissingBaseUrl)?),
266 resource: Arc::new(ResourceTemplate::default()),
267 settlement_mode: SettlementMode::default(),
268 settlement_tracker: None,
269 hooks: None,
270 #[cfg(feature = "siwx")]
271 siwx: self.siwx.clone(),
272 })
273 }
274}
275
276impl TryFrom<&str> for X402Middleware {
277 type Error = FacilitatorClientError;
278
279 fn try_from(value: &str) -> Result<Self, Self::Error> {
280 Self::try_new(value)
281 }
282}
283
284impl TryFrom<String> for X402Middleware {
285 type Error = FacilitatorClientError;
286
287 fn try_from(value: String) -> Result<Self, Self::Error> {
288 Self::try_new(&value)
289 }
290}