Skip to main content

r402_client/
select.rs

1//! Candidate selection and `extra.paymentFlow` preference.
2
3use r402_protocol::{ChainIdPattern, ClientError, PaymentRequired, PaymentRequirements};
4
5use crate::candidate::PaymentCandidate;
6use crate::register::PaymentClient;
7
8/// Selector that picks the best candidate from a slice.
9pub trait PaymentSelector: Send + Sync {
10    /// Returns the selected candidate (if any).
11    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate>;
12}
13
14/// [`PaymentSelector`] that returns the first candidate.
15#[derive(Debug, Clone, Copy, Default)]
16pub struct FirstMatch;
17
18impl PaymentSelector for FirstMatch {
19    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
20        candidates.first().copied()
21    }
22}
23
24/// [`PaymentSelector`] that prefers chains matching one of the given patterns.
25#[derive(Debug, Default)]
26pub struct PreferChain(Vec<ChainIdPattern>);
27
28impl PreferChain {
29    /// Constructs a selector from patterns.
30    #[must_use]
31    pub fn new<P: Into<Vec<ChainIdPattern>>>(patterns: P) -> Self {
32        Self(patterns.into())
33    }
34
35    /// Appends additional patterns with lower priority.
36    #[must_use]
37    pub fn or_chain<P: Into<Vec<ChainIdPattern>>>(mut self, patterns: P) -> Self {
38        self.0.extend(patterns.into());
39        self
40    }
41}
42
43impl PaymentSelector for PreferChain {
44    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
45        for pattern in &self.0 {
46            if let Some(hit) = candidates.iter().find(|c| pattern.matches(&c.chain_id)) {
47                return Some(*hit);
48            }
49        }
50        candidates.first().copied()
51    }
52}
53
54/// [`PaymentSelector`] that rejects candidates whose amount exceeds the budget.
55#[derive(Debug, Clone, Copy)]
56pub struct MaxAmount(pub u128);
57
58impl PaymentSelector for MaxAmount {
59    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
60        candidates
61            .iter()
62            .find(|c| c.amount.parse::<u128>().is_ok_and(|a| a <= self.0))
63            .copied()
64    }
65}
66
67impl<S: PaymentSelector> PaymentClient<S> {
68    /// Collects candidates from every registered scheme client.
69    #[must_use]
70    pub fn candidates(&self, payment_required: &PaymentRequired) -> Vec<PaymentCandidate> {
71        let mut out = Vec::new();
72        for client in &self.schemes {
73            out.extend(client.accept(payment_required));
74        }
75        out
76    }
77
78    /// Applies spend controls, policies, and the selector.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`ClientError::NoMatchingPaymentOption`] when nothing remains,
83    /// [`ClientError::UnrecognizedPaymentFlow`] when every candidate has an
84    /// unknown `extra.paymentFlow`, or [`ClientError::SpendControls`] when
85    /// spend controls reject every option.
86    pub fn select_candidate<'a>(
87        &self,
88        candidates: &'a [PaymentCandidate],
89    ) -> Result<&'a PaymentCandidate, ClientError> {
90        let recognized: Vec<&PaymentCandidate> = candidates
91            .iter()
92            .filter(|candidate| is_recognized_payment_flow(&candidate.requirements))
93            .collect();
94        if recognized.is_empty() {
95            return Err(if candidates.is_empty() {
96                ClientError::NoMatchingPaymentOption
97            } else {
98                ClientError::UnrecognizedPaymentFlow
99            });
100        }
101
102        let mut filtered = self.apply_spend_controls(recognized)?;
103        for policy in &self.policies {
104            filtered = policy.apply(filtered);
105            if filtered.is_empty() {
106                return Err(ClientError::NoMatchingPaymentOption);
107            }
108        }
109
110        let preferred = prefer_authorization(filtered);
111        self.selector
112            .select(&preferred)
113            .ok_or(ClientError::NoMatchingPaymentOption)
114    }
115}
116
117fn prefer_authorization(candidates: Vec<&PaymentCandidate>) -> Vec<&PaymentCandidate> {
118    let authorization: Vec<&PaymentCandidate> = candidates
119        .iter()
120        .copied()
121        .filter(|candidate| is_authorization_payment_flow(&candidate.requirements))
122        .collect();
123    if authorization.is_empty() {
124        candidates
125    } else {
126        authorization
127    }
128}
129
130#[derive(Clone, Copy)]
131enum WirePaymentFlow {
132    Absent,
133    Authorization,
134    Upfront,
135    Escrow,
136    Unknown,
137}
138
139fn wire_payment_flow(requirements: &PaymentRequirements) -> WirePaymentFlow {
140    let Some(flow) = requirements
141        .extra
142        .as_ref()
143        .and_then(|extra| extra.get("paymentFlow"))
144    else {
145        return WirePaymentFlow::Absent;
146    };
147    if flow.is_null() {
148        return WirePaymentFlow::Absent;
149    }
150    match flow.as_str() {
151        Some("authorization") => WirePaymentFlow::Authorization,
152        Some("upfront") => WirePaymentFlow::Upfront,
153        Some("escrow") => WirePaymentFlow::Escrow,
154        _ => WirePaymentFlow::Unknown,
155    }
156}
157
158fn is_recognized_payment_flow(requirements: &PaymentRequirements) -> bool {
159    !matches!(wire_payment_flow(requirements), WirePaymentFlow::Unknown)
160}
161
162fn is_authorization_payment_flow(requirements: &PaymentRequirements) -> bool {
163    matches!(
164        wire_payment_flow(requirements),
165        WirePaymentFlow::Absent | WirePaymentFlow::Authorization
166    )
167}