Skip to main content

r402_server/
scheme.rs

1//! Per-scheme resource-server adapter.
2
3use std::collections::HashMap;
4use std::future::Future;
5
6use compact_str::CompactString;
7use r402_facilitator::BoxFuture;
8use r402_protocol::error::FacilitatorError;
9use r402_protocol::network::ChainId;
10use r402_protocol::payment::{
11    PaymentRequired, PaymentRequirements, ResourceInfo, SupportedPaymentKind, SupportedResponse,
12};
13use serde_json::{Map, Value};
14
15use crate::hooks::{
16    SettleContext, SettleResultContext, VerifiedPaymentCanceledContext, WirePaymentPayload,
17};
18use crate::payment_flow::PaymentFlowConfig;
19
20/// Context for scheme 402 enrichment.
21#[derive(Debug)]
22#[non_exhaustive]
23pub struct SchemePaymentRequiredContext<'a> {
24    /// Accepts being enriched.
25    pub requirements: &'a [PaymentRequirements],
26    /// Client payload when enriching a paid 402 retry.
27    pub payment_payload: Option<&'a WirePaymentPayload>,
28    /// Resource metadata from the 402.
29    pub resource: &'a ResourceInfo,
30    /// Optional 402 error string.
31    pub error: Option<&'a str>,
32    /// Working payment-required response.
33    pub payment_required_response: &'a PaymentRequired,
34    /// Facilitator `GET /supported` snapshot.
35    pub supported: &'a SupportedResponse,
36}
37
38impl<'a> SchemePaymentRequiredContext<'a> {
39    /// Constructs a 402 enrich context.
40    #[must_use]
41    pub const fn new(
42        requirements: &'a [PaymentRequirements],
43        resource: &'a ResourceInfo,
44        payment_required_response: &'a PaymentRequired,
45        supported: &'a SupportedResponse,
46    ) -> Self {
47        Self {
48            requirements,
49            payment_payload: None,
50            resource,
51            error: None,
52            payment_required_response,
53            supported,
54        }
55    }
56}
57
58/// Scheme/network adapter registered on [`crate::ResourceServer`].
59///
60/// Enrich hooks are AFIT async. Defaults are no-ops.
61pub trait SchemeNetworkServer: Send + Sync {
62    /// Wire scheme name (e.g. `"exact"`).
63    fn scheme(&self) -> &str;
64
65    /// ATM used when `requirements.extra.assetTransferMethod` is absent.
66    fn default_asset_transfer_method(&self) -> &str;
67
68    /// Payment flows supported per asset-transfer method.
69    fn payment_flows(&self) -> &HashMap<String, PaymentFlowConfig>;
70
71    /// Extra keys omitted from requirement matching.
72    fn dynamic_extra_fields(&self) -> &[&str] {
73        &[]
74    }
75
76    /// Optional 402 accept enrichment. `None` leaves accepts unchanged.
77    fn enrich_payment_required_response<'a>(
78        &'a self,
79        _ctx: &'a SchemePaymentRequiredContext<'a>,
80    ) -> impl Future<Output = Option<Vec<PaymentRequirements>>> + Send + 'a {
81        async { None }
82    }
83
84    /// Optional additive settle-payload enrichment.
85    ///
86    /// `Ok(None)` leaves the client payload unchanged. `Ok(Some(map))` is
87    /// merged additively. `Err` fails the settle.
88    fn enrich_settlement_payload<'a>(
89        &'a self,
90        _ctx: &'a SettleContext,
91    ) -> impl Future<Output = Result<Option<Map<String, Value>>, FacilitatorError>> + Send + 'a
92    {
93        async { Ok(None) }
94    }
95
96    /// Optional additive settle-response enrichment.
97    fn enrich_settlement_response<'a>(
98        &'a self,
99        _ctx: &'a SettleResultContext,
100    ) -> impl Future<Output = Option<Map<String, Value>>> + Send + 'a {
101        async { None }
102    }
103
104    /// Requirements to settle when a verified payment is canceled.
105    ///
106    /// `None` skips cancel settle.
107    fn settle_on_cancel<'a>(
108        &'a self,
109        _ctx: &'a VerifiedPaymentCanceledContext,
110    ) -> impl Future<Output = Option<PaymentRequirements>> + Send + 'a {
111        async { None }
112    }
113
114    /// `true` iff [`Self::settle_on_cancel`] can return `Some`.
115    ///
116    /// HTTP construct cannot await [`Self::settle_on_cancel`]. Default `false`.
117    fn settles_on_cancel(&self) -> bool {
118        false
119    }
120
121    /// Advertised `/supported` kind extra for this scheme. Default `Ok(())`.
122    ///
123    /// # Errors
124    ///
125    /// [`FacilitatorSupportError`] when the advertised kind cannot serve this scheme.
126    fn validate_facilitator_support(
127        &self,
128        network: &ChainId,
129        kind: &SupportedPaymentKind,
130        facilitator_extensions: &[CompactString],
131    ) -> Result<(), FacilitatorSupportError> {
132        let _ = (network, kind, facilitator_extensions);
133        Ok(())
134    }
135}
136
137/// Facilitator `/supported` kind is missing or its extra is unusable.
138#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
139pub enum FacilitatorSupportError {
140    /// No matching kind for this accept.
141    #[error("{scheme} on {network}: facilitator does not advertise this kind")]
142    KindMissing {
143        /// Wire scheme name.
144        scheme: CompactString,
145        /// Accept network.
146        network: ChainId,
147    },
148    /// Advertised extra omits `feePayer`.
149    #[error("{scheme} on {network}: missing extra.feePayer")]
150    MissingFeePayer {
151        /// Wire scheme name.
152        scheme: CompactString,
153        /// Accept network.
154        network: ChainId,
155    },
156    /// Advertised `feePayer` is not a valid address.
157    #[error("{scheme} on {network}: invalid extra.feePayer")]
158    InvalidFeePayer {
159        /// Wire scheme name.
160        scheme: CompactString,
161        /// Accept network.
162        network: ChainId,
163    },
164    /// Advertised extra omits `receiverAuthorizer`.
165    #[error("{scheme} on {network}: missing extra.receiverAuthorizer")]
166    MissingReceiverAuthorizer {
167        /// Wire scheme name.
168        scheme: CompactString,
169        /// Accept network.
170        network: ChainId,
171    },
172    /// Advertised `receiverAuthorizer` is the zero address.
173    #[error("{scheme} on {network}: extra.receiverAuthorizer is the zero address")]
174    ZeroReceiverAuthorizer {
175        /// Wire scheme name.
176        scheme: CompactString,
177        /// Accept network.
178        network: ChainId,
179    },
180    /// Advertised `receiverAuthorizer` is not a usable address.
181    #[error("{scheme} on {network}: invalid extra.receiverAuthorizer")]
182    InvalidReceiverAuthorizer {
183        /// Wire scheme name.
184        scheme: CompactString,
185        /// Accept network.
186        network: ChainId,
187    },
188}
189
190impl FacilitatorSupportError {
191    /// Stable machine reason for HTTP/MCP mapping.
192    #[must_use]
193    pub const fn reason(&self) -> &'static str {
194        match self {
195            Self::KindMissing { .. } => "kind_missing",
196            Self::MissingFeePayer { .. } => "missing_fee_payer",
197            Self::InvalidFeePayer { .. } => "invalid_fee_payer",
198            Self::MissingReceiverAuthorizer { .. } => "missing_receiver_authorizer",
199            Self::ZeroReceiverAuthorizer { .. } => "zero_receiver_authorizer",
200            Self::InvalidReceiverAuthorizer { .. } => "invalid_receiver_authorizer",
201        }
202    }
203}
204
205/// Object-safe erasure of [`SchemeNetworkServer`].
206pub trait DynSchemeNetworkServer: Send + Sync {
207    /// See [`SchemeNetworkServer::scheme`].
208    fn scheme(&self) -> &str;
209
210    /// See [`SchemeNetworkServer::default_asset_transfer_method`].
211    fn default_asset_transfer_method(&self) -> &str;
212
213    /// See [`SchemeNetworkServer::payment_flows`].
214    fn payment_flows(&self) -> &HashMap<String, PaymentFlowConfig>;
215
216    /// See [`SchemeNetworkServer::dynamic_extra_fields`].
217    fn dynamic_extra_fields(&self) -> &[&str];
218
219    /// See [`SchemeNetworkServer::enrich_payment_required_response`].
220    fn enrich_payment_required_response<'a>(
221        &'a self,
222        ctx: &'a SchemePaymentRequiredContext<'a>,
223    ) -> BoxFuture<'a, Option<Vec<PaymentRequirements>>>;
224
225    /// See [`SchemeNetworkServer::enrich_settlement_payload`].
226    fn enrich_settlement_payload<'a>(
227        &'a self,
228        ctx: &'a SettleContext,
229    ) -> BoxFuture<'a, Result<Option<Map<String, Value>>, FacilitatorError>>;
230
231    /// See [`SchemeNetworkServer::enrich_settlement_response`].
232    fn enrich_settlement_response<'a>(
233        &'a self,
234        ctx: &'a SettleResultContext,
235    ) -> BoxFuture<'a, Option<Map<String, Value>>>;
236
237    /// See [`SchemeNetworkServer::settle_on_cancel`].
238    fn settle_on_cancel<'a>(
239        &'a self,
240        ctx: &'a VerifiedPaymentCanceledContext,
241    ) -> BoxFuture<'a, Option<PaymentRequirements>>;
242
243    /// See [`SchemeNetworkServer::settles_on_cancel`].
244    fn settles_on_cancel(&self) -> bool;
245
246    /// See [`SchemeNetworkServer::validate_facilitator_support`].
247    ///
248    /// # Errors
249    ///
250    /// [`FacilitatorSupportError`] when the advertised kind cannot serve this scheme.
251    fn validate_facilitator_support(
252        &self,
253        network: &ChainId,
254        kind: &SupportedPaymentKind,
255        facilitator_extensions: &[CompactString],
256    ) -> Result<(), FacilitatorSupportError>;
257}
258
259impl<T: SchemeNetworkServer + ?Sized> DynSchemeNetworkServer for T {
260    fn scheme(&self) -> &str {
261        <Self as SchemeNetworkServer>::scheme(self)
262    }
263
264    fn default_asset_transfer_method(&self) -> &str {
265        <Self as SchemeNetworkServer>::default_asset_transfer_method(self)
266    }
267
268    fn payment_flows(&self) -> &HashMap<String, PaymentFlowConfig> {
269        <Self as SchemeNetworkServer>::payment_flows(self)
270    }
271
272    fn dynamic_extra_fields(&self) -> &[&str] {
273        <Self as SchemeNetworkServer>::dynamic_extra_fields(self)
274    }
275
276    fn enrich_payment_required_response<'a>(
277        &'a self,
278        ctx: &'a SchemePaymentRequiredContext<'a>,
279    ) -> BoxFuture<'a, Option<Vec<PaymentRequirements>>> {
280        Box::pin(<Self as SchemeNetworkServer>::enrich_payment_required_response(self, ctx))
281    }
282
283    fn enrich_settlement_payload<'a>(
284        &'a self,
285        ctx: &'a SettleContext,
286    ) -> BoxFuture<'a, Result<Option<Map<String, Value>>, FacilitatorError>> {
287        Box::pin(<Self as SchemeNetworkServer>::enrich_settlement_payload(
288            self, ctx,
289        ))
290    }
291
292    fn enrich_settlement_response<'a>(
293        &'a self,
294        ctx: &'a SettleResultContext,
295    ) -> BoxFuture<'a, Option<Map<String, Value>>> {
296        Box::pin(<Self as SchemeNetworkServer>::enrich_settlement_response(
297            self, ctx,
298        ))
299    }
300
301    fn settle_on_cancel<'a>(
302        &'a self,
303        ctx: &'a VerifiedPaymentCanceledContext,
304    ) -> BoxFuture<'a, Option<PaymentRequirements>> {
305        Box::pin(<Self as SchemeNetworkServer>::settle_on_cancel(self, ctx))
306    }
307
308    fn settles_on_cancel(&self) -> bool {
309        <Self as SchemeNetworkServer>::settles_on_cancel(self)
310    }
311
312    fn validate_facilitator_support(
313        &self,
314        network: &ChainId,
315        kind: &SupportedPaymentKind,
316        facilitator_extensions: &[CompactString],
317    ) -> Result<(), FacilitatorSupportError> {
318        <Self as SchemeNetworkServer>::validate_facilitator_support(
319            self,
320            network,
321            kind,
322            facilitator_extensions,
323        )
324    }
325}