Skip to main content

r402_core/scheme/
client.rs

1//! Client-side scheme abstractions.
2
3use std::fmt::{self, Debug, Formatter};
4use std::future::Future;
5
6use compact_str::CompactString;
7
8use crate::chain::{ChainId, ChainIdPattern};
9use crate::error::ClientError;
10use crate::scheme::sealed::Sealed;
11use crate::wire::PaymentRequired;
12
13/// Client-side scheme interface.
14///
15/// Sealed: only crates inside this workspace may implement it.
16pub trait SchemeClient: super::SchemeId + Sealed + Send + Sync {
17    /// Examines a 402 response and returns all payment candidates this
18    /// client can fulfil.
19    fn accept(&self, payment_required: &PaymentRequired) -> Vec<PaymentCandidate>;
20}
21
22/// A single payment option produced by [`SchemeClient::accept`].
23pub struct PaymentCandidate {
24    /// CAIP-2 chain id of the target chain.
25    pub chain_id: ChainId,
26    /// Token asset address / mint.
27    pub asset: CompactString,
28    /// Amount in the token's smallest unit, stringified.
29    pub amount: CompactString,
30    /// Scheme identifier (`"exact"`, `"upto"`, ...).
31    pub scheme: CompactString,
32    /// Recipient address.
33    pub pay_to: CompactString,
34    /// Signer that can produce the authorization.
35    pub signer: Box<dyn PaymentCandidateSigner + Send + Sync>,
36}
37
38impl Debug for PaymentCandidate {
39    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
40        f.debug_struct("PaymentCandidate")
41            .field("chain_id", &self.chain_id)
42            .field("asset", &self.asset)
43            .field("amount", &self.amount)
44            .field("scheme", &self.scheme)
45            .field("pay_to", &self.pay_to)
46            .finish_non_exhaustive()
47    }
48}
49
50impl PaymentCandidate {
51    /// Signs the candidate, returning the base64-encoded payload.
52    ///
53    /// # Errors
54    ///
55    /// Returns [`ClientError`] when signing fails.
56    pub async fn sign(&self) -> Result<String, ClientError> {
57        self.signer.sign_payment().await
58    }
59}
60
61/// Candidate-signer trait. Object-safe on purpose — each candidate
62/// carries its own `Box<dyn PaymentCandidateSigner>`.
63pub trait PaymentCandidateSigner: Send + Sync {
64    /// Produces the signed payment payload (base64-encoded).
65    fn sign_payment<'a>(
66        &'a self,
67    ) -> std::pin::Pin<Box<dyn Future<Output = Result<String, ClientError>> + Send + 'a>>;
68}
69
70/// Selector that picks the best candidate from a slice.
71pub trait PaymentSelector: Send + Sync {
72    /// Returns the selected candidate (if any).
73    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate>;
74}
75
76/// `PaymentSelector` that returns the first candidate.
77#[derive(Debug, Clone, Copy, Default)]
78pub struct FirstMatch;
79
80impl PaymentSelector for FirstMatch {
81    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
82        candidates.first().copied()
83    }
84}
85
86/// `PaymentSelector` that prefers chains matching one of the given patterns.
87#[derive(Debug, Default)]
88pub struct PreferChain(Vec<ChainIdPattern>);
89
90impl PreferChain {
91    /// Constructs a selector from patterns.
92    pub fn new<P: Into<Vec<ChainIdPattern>>>(patterns: P) -> Self {
93        Self(patterns.into())
94    }
95
96    /// Appends additional patterns with lower priority.
97    #[must_use]
98    pub fn or_chain<P: Into<Vec<ChainIdPattern>>>(mut self, patterns: P) -> Self {
99        self.0.extend(patterns.into());
100        self
101    }
102}
103
104impl PaymentSelector for PreferChain {
105    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
106        for pattern in &self.0 {
107            if let Some(hit) = candidates.iter().find(|c| pattern.matches(&c.chain_id)) {
108                return Some(*hit);
109            }
110        }
111        candidates.first().copied()
112    }
113}
114
115/// `PaymentSelector` that rejects candidates whose amount exceeds the budget.
116#[derive(Debug, Clone, Copy)]
117pub struct MaxAmount(pub u128);
118
119impl PaymentSelector for MaxAmount {
120    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
121        candidates
122            .iter()
123            .find(|c| c.amount.parse::<u128>().is_ok_and(|a| a <= self.0))
124            .copied()
125    }
126}
127
128/// Filter applied to the candidate list before selection.
129pub trait PaymentPolicy: Send + Sync {
130    /// Returns the subset of candidates that pass this policy.
131    fn apply<'a>(&self, candidates: Vec<&'a PaymentCandidate>) -> Vec<&'a PaymentCandidate>;
132}
133
134/// Keeps only candidates whose chain matches one of the patterns.
135#[derive(Debug, Default)]
136pub struct NetworkPolicy(Vec<ChainIdPattern>);
137
138impl NetworkPolicy {
139    /// Constructs the policy from a list of patterns.
140    pub fn new<P: Into<Vec<ChainIdPattern>>>(patterns: P) -> Self {
141        Self(patterns.into())
142    }
143}
144
145impl PaymentPolicy for NetworkPolicy {
146    fn apply<'a>(&self, candidates: Vec<&'a PaymentCandidate>) -> Vec<&'a PaymentCandidate> {
147        candidates
148            .into_iter()
149            .filter(|c| self.0.iter().any(|p| p.matches(&c.chain_id)))
150            .collect()
151    }
152}
153
154/// Keeps only candidates whose scheme name is in the allow-list.
155#[derive(Debug, Default)]
156pub struct SchemePolicy(Vec<CompactString>);
157
158impl SchemePolicy {
159    /// Constructs the policy from an iterator of names.
160    pub fn new<S: Into<CompactString>, I: IntoIterator<Item = S>>(schemes: I) -> Self {
161        Self(schemes.into_iter().map(Into::into).collect())
162    }
163}
164
165impl PaymentPolicy for SchemePolicy {
166    fn apply<'a>(&self, candidates: Vec<&'a PaymentCandidate>) -> Vec<&'a PaymentCandidate> {
167        candidates
168            .into_iter()
169            .filter(|c| self.0.iter().any(|s| s.as_str() == c.scheme.as_str()))
170            .collect()
171    }
172}
173
174/// Keeps only candidates whose amount is at most the configured bound.
175#[derive(Debug, Clone, Copy)]
176pub struct MaxAmountPolicy(pub u128);
177
178impl PaymentPolicy for MaxAmountPolicy {
179    fn apply<'a>(&self, candidates: Vec<&'a PaymentCandidate>) -> Vec<&'a PaymentCandidate> {
180        candidates
181            .into_iter()
182            .filter(|c| c.amount.parse::<u128>().is_ok_and(|a| a <= self.0))
183            .collect()
184    }
185}