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