Skip to main content

r402_server/payment_flow/
mod.rs

1//! Closed payment-flow names, settle phases, and scheme-table resolution.
2
3mod extra;
4
5use std::collections::HashMap;
6use std::fmt::{self, Display, Formatter};
7use std::str::FromStr;
8
9pub use extra::{
10    apply_payment_flow_wire_extra, extra_payment_flow, is_authorization_payment_flow,
11    is_recognized_payment_flow,
12};
13use extra::{extra_string, requested_flow};
14use r402_protocol::payment::PaymentRequirements;
15use serde::{Deserialize, Serialize};
16
17/// SDK-only ATM key for schemes with no on-wire `assetTransferMethod`.
18///
19/// Never emit `assetTransferMethod: "default"` on the 402 wire.
20pub const SDK_DEFAULT_ASSET_TRANSFER_METHOD: &str = "default";
21
22/// Closed set of payment-flow names (when on-chain value moves relative to the handler).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum PaymentFlowName {
26    /// Verify before the handler; settle after (`authorization`).
27    Authorization,
28    /// Settle before the handler; skip facilitator verify (`upfront`).
29    Upfront,
30    /// Settle before and after the handler (`escrow`).
31    Escrow,
32}
33
34impl PaymentFlowName {
35    /// All closed payment-flow names, in declaration order.
36    pub const ALL: [Self; 3] = [Self::Authorization, Self::Upfront, Self::Escrow];
37
38    /// Wire string.
39    #[must_use]
40    pub const fn as_str(self) -> &'static str {
41        match self {
42            Self::Authorization => "authorization",
43            Self::Upfront => "upfront",
44            Self::Escrow => "escrow",
45        }
46    }
47
48    /// Phase flags for this name.
49    #[must_use]
50    pub const fn phases(self) -> PaymentFlowPhases {
51        match self {
52            Self::Authorization => PaymentFlowPhases {
53                verify_before_handler: true,
54                settle_before_handler: false,
55                settle_after_handler: true,
56            },
57            Self::Upfront => PaymentFlowPhases {
58                verify_before_handler: false,
59                settle_before_handler: true,
60                settle_after_handler: false,
61            },
62            Self::Escrow => PaymentFlowPhases {
63                verify_before_handler: false,
64                settle_before_handler: true,
65                settle_after_handler: true,
66            },
67        }
68    }
69}
70
71impl Display for PaymentFlowName {
72    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
73        f.write_str(self.as_str())
74    }
75}
76
77impl FromStr for PaymentFlowName {
78    type Err = PaymentFlowError;
79
80    fn from_str(s: &str) -> Result<Self, Self::Err> {
81        match s {
82            "authorization" => Ok(Self::Authorization),
83            "upfront" => Ok(Self::Upfront),
84            "escrow" => Ok(Self::Escrow),
85            other => Err(PaymentFlowError::UnknownPaymentFlow {
86                flow: other.to_owned(),
87            }),
88        }
89    }
90}
91
92/// Which settle invocation is running.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
94#[serde(rename_all = "kebab-case")]
95pub enum SettlePhase {
96    /// Settle before the resource handler (`before-handler`).
97    BeforeHandler,
98    /// Settle after the resource handler (`after-handler`).
99    AfterHandler,
100    /// Refund/close settle from verified-payment cancellation (`cancel`).
101    Cancel,
102}
103
104impl SettlePhase {
105    /// Wire string.
106    #[must_use]
107    pub const fn as_str(self) -> &'static str {
108        match self {
109            Self::BeforeHandler => "before-handler",
110            Self::AfterHandler => "after-handler",
111            Self::Cancel => "cancel",
112        }
113    }
114}
115
116impl Display for SettlePhase {
117    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
118        f.write_str(self.as_str())
119    }
120}
121
122impl FromStr for SettlePhase {
123    type Err = PaymentFlowError;
124
125    fn from_str(s: &str) -> Result<Self, Self::Err> {
126        match s {
127            "before-handler" => Ok(Self::BeforeHandler),
128            "after-handler" => Ok(Self::AfterHandler),
129            "cancel" => Ok(Self::Cancel),
130            other => Err(PaymentFlowError::UnknownSettlePhase {
131                phase: other.to_owned(),
132            }),
133        }
134    }
135}
136
137/// Verify/settle phase flags for a [`PaymentFlowName`].
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
139#[allow(
140    clippy::struct_excessive_bools,
141    reason = "PAYMENT_FLOWS phase flags are a closed triple"
142)]
143pub struct PaymentFlowPhases {
144    /// Run facilitator verify before the resource handler.
145    pub verify_before_handler: bool,
146    /// Run settle before the resource handler.
147    pub settle_before_handler: bool,
148    /// Run settle after the resource handler.
149    pub settle_after_handler: bool,
150}
151
152/// Supported payment flows for one `assetTransferMethod`.
153///
154/// `default` must be a member of `supported` (checked by [`resolve_payment_flow`]).
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct PaymentFlowConfig {
157    /// Flows this ATM accepts.
158    pub supported: Vec<PaymentFlowName>,
159    /// Used when `extra.paymentFlow` is omitted.
160    pub default: PaymentFlowName,
161}
162
163impl PaymentFlowConfig {
164    /// Constructs a per-ATM flow table.
165    #[must_use]
166    pub const fn new(supported: Vec<PaymentFlowName>, default: PaymentFlowName) -> Self {
167        Self { supported, default }
168    }
169
170    /// Exact-scheme row: authorization + upfront, default authorization.
171    #[must_use]
172    pub fn authorization_and_upfront() -> Self {
173        Self::new(
174            vec![PaymentFlowName::Authorization, PaymentFlowName::Upfront],
175            PaymentFlowName::Authorization,
176        )
177    }
178
179    /// Upto / batch-settlement row: authorization only.
180    #[must_use]
181    pub fn authorization_only() -> Self {
182        Self::new(
183            vec![PaymentFlowName::Authorization],
184            PaymentFlowName::Authorization,
185        )
186    }
187}
188
189/// Scheme fields required to resolve ATM + payment flow.
190#[derive(Debug, Clone, Copy)]
191pub struct PaymentFlowScheme<'a> {
192    /// Scheme name (e.g. `"exact"`).
193    pub scheme: &'a str,
194    /// ATM used when `extra.assetTransferMethod` is absent.
195    pub default_asset_transfer_method: &'a str,
196    /// Payment flows supported per ATM.
197    pub payment_flows: &'a HashMap<String, PaymentFlowConfig>,
198}
199
200/// Result of [`resolve_payment_flow`].
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct ResolvedPaymentFlow {
203    /// Resolved `assetTransferMethod`.
204    pub asset_transfer_method: String,
205    /// Resolved payment flow.
206    pub payment_flow: PaymentFlowName,
207}
208
209/// Closed `PAYMENT_FLOWS` table (authorization, upfront, escrow).
210pub const PAYMENT_FLOWS: [(PaymentFlowName, PaymentFlowPhases); 3] = [
211    (
212        PaymentFlowName::Authorization,
213        PaymentFlowName::Authorization.phases(),
214    ),
215    (PaymentFlowName::Upfront, PaymentFlowName::Upfront.phases()),
216    (PaymentFlowName::Escrow, PaymentFlowName::Escrow.phases()),
217];
218
219/// Failures from payment-flow resolution or wire-name parsing.
220#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
221pub enum PaymentFlowError {
222    /// Scheme table has no row for the resolved ATM.
223    #[error(
224        "[x402] Scheme \"{scheme}\" does not support assetTransferMethod \"{asset_transfer_method}\". Supported: {supported}."
225    )]
226    UnsupportedAssetTransferMethod {
227        /// Scheme name.
228        scheme: String,
229        /// Requested or default ATM.
230        asset_transfer_method: String,
231        /// Comma-separated ATM keys from the scheme table.
232        supported: String,
233    },
234    /// `paymentFlows[atm].default` is not listed in `supported`.
235    #[error(
236        "[x402] Scheme \"{scheme}\" paymentFlows[\"{asset_transfer_method}\"].default is not in supported."
237    )]
238    DefaultNotInSupported {
239        /// Scheme name.
240        scheme: String,
241        /// ATM whose default is invalid.
242        asset_transfer_method: String,
243    },
244    /// Requested `extra.paymentFlow` is not in the ATM's `supported` list.
245    #[error(
246        "[x402] Scheme \"{scheme}\" assetTransferMethod \"{asset_transfer_method}\" does not support paymentFlow \"{requested}\". Supported: {supported} (default: {default})."
247    )]
248    UnsupportedPaymentFlow {
249        /// Scheme name.
250        scheme: String,
251        /// Resolved ATM.
252        asset_transfer_method: String,
253        /// Requested flow as it appeared on extra (or the invalid default).
254        requested: String,
255        /// Comma-separated supported flow names.
256        supported: String,
257        /// ATM default flow.
258        default: String,
259    },
260    /// String is not a closed [`PaymentFlowName`].
261    #[error(
262        "[x402] Unknown payment flow \"{flow}\". Expected one of: authorization, upfront, escrow."
263    )]
264    UnknownPaymentFlow {
265        /// Rejected token.
266        flow: String,
267    },
268    /// String is not a closed [`SettlePhase`].
269    #[error(
270        "[x402] Unknown settle phase \"{phase}\". Expected one of: before-handler, after-handler, cancel."
271    )]
272    UnknownSettlePhase {
273        /// Rejected token.
274        phase: String,
275    },
276    /// No [`crate::SchemeNetworkServer`] is registered for this scheme/network.
277    #[error("[x402] No server implementation registered for scheme: {scheme}, network: {network}")]
278    UnregisteredScheme {
279        /// Requested scheme name.
280        scheme: String,
281        /// Requested CAIP-2 network.
282        network: String,
283    },
284}
285
286/// Resolves the phase table for a payment flow name.
287#[must_use]
288pub const fn resolve_payment_flow_phases(flow: PaymentFlowName) -> PaymentFlowPhases {
289    flow.phases()
290}
291
292/// Resolves `assetTransferMethod` and `paymentFlow` from a scheme table and requirements.
293///
294/// Omit ATM → `scheme.default_asset_transfer_method`. Omit `paymentFlow` → that ATM's default.
295///
296/// # Errors
297///
298/// Returns [`PaymentFlowError`] when the ATM is missing from the table, the table
299/// default is not in `supported`, or the requested flow is not supported.
300pub fn resolve_payment_flow(
301    scheme: &PaymentFlowScheme<'_>,
302    requirements: &PaymentRequirements,
303) -> Result<ResolvedPaymentFlow, PaymentFlowError> {
304    let atm = extra_string(requirements, "assetTransferMethod")
305        .unwrap_or(scheme.default_asset_transfer_method);
306
307    let Some(config) = scheme.payment_flows.get(atm) else {
308        return Err(PaymentFlowError::UnsupportedAssetTransferMethod {
309            scheme: scheme.scheme.to_owned(),
310            asset_transfer_method: atm.to_owned(),
311            supported: join_sorted_keys(scheme.payment_flows),
312        });
313    };
314
315    if !config.supported.contains(&config.default) {
316        return Err(PaymentFlowError::DefaultNotInSupported {
317            scheme: scheme.scheme.to_owned(),
318            asset_transfer_method: atm.to_owned(),
319        });
320    }
321
322    let flow = match requested_flow(requirements) {
323        None => config.default,
324        Some(label) => match PaymentFlowName::from_str(&label) {
325            Ok(name) if config.supported.contains(&name) => name,
326            Ok(name) => {
327                return Err(unsupported_flow(scheme, atm, name.as_str(), config));
328            }
329            Err(_) => return Err(unsupported_flow(scheme, atm, &label, config)),
330        },
331    };
332
333    Ok(ResolvedPaymentFlow {
334        asset_transfer_method: atm.to_owned(),
335        payment_flow: flow,
336    })
337}
338
339fn unsupported_flow(
340    scheme: &PaymentFlowScheme<'_>,
341    atm: &str,
342    requested: &str,
343    config: &PaymentFlowConfig,
344) -> PaymentFlowError {
345    PaymentFlowError::UnsupportedPaymentFlow {
346        scheme: scheme.scheme.to_owned(),
347        asset_transfer_method: atm.to_owned(),
348        requested: requested.to_owned(),
349        supported: join_names(&config.supported),
350        default: config.default.as_str().to_owned(),
351    }
352}
353
354fn join_names(names: &[PaymentFlowName]) -> String {
355    names
356        .iter()
357        .map(|name| name.as_str())
358        .collect::<Vec<_>>()
359        .join(", ")
360}
361
362fn join_sorted_keys(flows: &HashMap<String, PaymentFlowConfig>) -> String {
363    let mut keys: Vec<&str> = flows.keys().map(String::as_str).collect();
364    keys.sort_unstable();
365    keys.join(", ")
366}