Skip to main content

r402_client/
candidate.rs

1//! Scheme client, default-asset lookup, and signed payment candidates.
2
3use std::fmt::{self, Debug, Formatter};
4use std::future::Future;
5use std::pin::Pin;
6
7use compact_str::CompactString;
8use r402_protocol::{ChainId, ClientError, PaymentRequired, PaymentRequirements, SchemeId};
9
10/// USD-pegged default asset used for money strings and client spend caps.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct DefaultAssetInfo {
13    /// Asset id as advertised in payment requirements.
14    pub asset: CompactString,
15    /// Token decimal places.
16    pub decimals: u32,
17    /// Ticker (`"USDC"`, `"USDT0"`, `"RLUSD"`, …).
18    pub symbol: CompactString,
19    /// Transfer-method override when the default is not the scheme ATM default.
20    pub asset_transfer_method: Option<CompactString>,
21}
22
23impl DefaultAssetInfo {
24    /// Constructs a default-asset row without a transfer-method override.
25    #[must_use]
26    pub fn new(
27        asset: impl Into<CompactString>,
28        decimals: u32,
29        symbol: impl Into<CompactString>,
30    ) -> Self {
31        Self {
32            asset: asset.into(),
33            decimals,
34            symbol: symbol.into(),
35            asset_transfer_method: None,
36        }
37    }
38
39    /// Sets `assetTransferMethod`.
40    #[must_use]
41    pub fn with_asset_transfer_method(mut self, method: impl Into<CompactString>) -> Self {
42        self.asset_transfer_method = Some(method.into());
43        self
44    }
45}
46
47/// Buyer-side scheme: emit candidates from a 402 challenge.
48pub trait SchemeClient: SchemeId + Send + Sync {
49    /// Payment options this client can fulfil for `payment_required`.
50    fn accept(&self, payment_required: &PaymentRequired) -> Vec<PaymentCandidate>;
51
52    /// Reverse lookup of a USD-pegged default asset for spend-cap conversion.
53    fn find_default_asset(&self, asset: &str, network: &ChainId) -> Option<DefaultAssetInfo>;
54}
55
56/// One payment option produced by [`SchemeClient::accept`].
57pub struct PaymentCandidate {
58    /// CAIP-2 chain id of the target chain.
59    pub chain_id: ChainId,
60    /// Token asset address / mint.
61    pub asset: CompactString,
62    /// Amount in the token's smallest unit, stringified.
63    pub amount: CompactString,
64    /// Scheme identifier (`"exact"`, `"upto"`, ...).
65    pub scheme: CompactString,
66    /// Recipient address.
67    pub pay_to: CompactString,
68    /// Wire requirements this candidate was built from.
69    pub requirements: PaymentRequirements,
70    /// Signer that can produce the authorization.
71    pub signer: Box<dyn PaymentCandidateSigner>,
72}
73
74impl Debug for PaymentCandidate {
75    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
76        f.debug_struct("PaymentCandidate")
77            .field("chain_id", &self.chain_id)
78            .field("asset", &self.asset)
79            .field("amount", &self.amount)
80            .field("scheme", &self.scheme)
81            .field("pay_to", &self.pay_to)
82            .finish_non_exhaustive()
83    }
84}
85
86impl PaymentCandidate {
87    /// Signs the candidate, returning the base64-encoded payload.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`ClientError`] when signing fails.
92    pub async fn sign(&self) -> Result<String, ClientError> {
93        self.signer.sign_payment().await
94    }
95}
96
97/// Object-safe signer carried by each [`PaymentCandidate`].
98pub trait PaymentCandidateSigner: Send + Sync {
99    /// Produces the signed payment payload (base64-encoded).
100    fn sign_payment<'a>(
101        &'a self,
102    ) -> Pin<Box<dyn Future<Output = Result<String, ClientError>> + Send + 'a>>;
103}