Skip to main content

x402_rs/scheme/
client.rs

1//! Client-side payment scheme support.
2//!
3//! This module provides types for client applications (buyers) to select and
4//! sign payments in response to 402 Payment Required responses.
5//!
6//! # Payment Flow
7//!
8//! 1. Client receives a 402 response with payment requirements
9//! 2. [`X402SchemeClient`] implementations generate [`PaymentCandidate`]s
10//! 3. A [`PaymentSelector`] chooses the best candidate
11//! 4. The candidate is signed and sent back to the server
12//!
13//! # Payment Selection
14//!
15//! Multiple selectors are available:
16//! - [`FirstMatch`] - Takes the first available option
17//! - [`PreferChain`] - Prefers specific chains in priority order
18//! - [`MaxAmount`] - Only accepts payments up to a maximum amount
19
20use alloy_primitives::U256;
21use async_trait::async_trait;
22
23use crate::chain::{ChainId, ChainIdPattern};
24use crate::proto;
25use crate::scheme::X402SchemeId;
26
27/// A payment option that can be signed and submitted.
28///
29/// Payment candidates are generated by scheme clients when they find
30/// a matching payment requirement they can fulfill.
31#[allow(dead_code)] // Public for consumption by downstream crates.
32pub struct PaymentCandidate {
33    /// The chain where payment will be made.
34    pub chain_id: ChainId,
35    /// The token asset address.
36    pub asset: String,
37    /// The payment amount in token units.
38    pub amount: U256,
39    /// The payment scheme name.
40    pub scheme: String,
41    /// The x402 protocol version.
42    pub x402_version: u8,
43    /// The recipient address.
44    pub pay_to: String,
45    /// The signer that can authorize this payment.
46    pub signer: Box<dyn PaymentCandidateSigner + Send + Sync>,
47}
48
49impl PaymentCandidate {
50    /// Signs this payment candidate, producing a payment payload.
51    #[allow(dead_code)] // Public for consumption by downstream crates.
52    pub async fn sign(&self) -> Result<String, X402Error> {
53        self.signer.sign_payment().await
54    }
55}
56
57/// Trait for scheme clients that can process payment requirements.
58///
59/// Implementations examine 402 responses and generate payment candidates
60/// for requirements they can fulfill.
61#[async_trait]
62#[allow(dead_code)] // Public for consumption by downstream crates.
63pub trait X402SchemeClient: X402SchemeId + Send + Sync {
64    /// Generates payment candidates for the given payment requirements.
65    fn accept(&self, payment_required: &proto::PaymentRequired) -> Vec<PaymentCandidate>;
66}
67
68/// Trait for signing payment authorizations.
69#[async_trait]
70#[allow(dead_code)] // Public for consumption by downstream crates.
71pub trait PaymentCandidateSigner {
72    /// Signs a payment authorization.
73    async fn sign_payment(&self) -> Result<String, X402Error>;
74}
75
76/// Errors that can occur during client-side payment processing.
77#[derive(Debug, thiserror::Error)]
78#[allow(dead_code)] // Public for consumption by downstream crates.
79pub enum X402Error {
80    /// No payment option matched the client's capabilities.
81    #[error("No matching payment option found")]
82    NoMatchingPaymentOption,
83
84    /// The HTTP request body cannot be cloned (e.g., streaming).
85    #[error("Request is not cloneable (streaming body?)")]
86    RequestNotCloneable,
87
88    /// Failed to parse the 402 response body.
89    #[error("Failed to parse 402 response: {0}")]
90    ParseError(String),
91
92    /// Failed to sign the payment authorization.
93    #[error("Failed to sign payment: {0}")]
94    SigningError(String),
95
96    /// JSON serialization/deserialization error.
97    #[error("JSON error: {0}")]
98    JsonError(#[from] serde_json::Error),
99}
100
101// ============================================================================
102// PaymentSelector - Selection strategy
103// ============================================================================
104
105/// Trait for selecting the best payment candidate from available options.
106///
107/// Implement this trait to customize how payments are selected when
108/// multiple options are available.
109#[allow(dead_code)] // Public for consumption by downstream crates.
110pub trait PaymentSelector: Send + Sync {
111    /// Selects a payment candidate from the available options.
112    fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate>;
113}
114
115/// Selector that returns the first matching candidate.
116///
117/// This is the simplest selection strategy. The order of candidates
118/// is determined by the registration order of scheme clients.
119#[allow(dead_code)] // Public for consumption by downstream crates.
120pub struct FirstMatch;
121
122impl PaymentSelector for FirstMatch {
123    fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> {
124        candidates.first()
125    }
126}
127
128/// Selector that prefers specific chains in priority order.
129///
130/// Patterns are tried in order; the first matching candidate is returned.
131/// If no patterns match, falls back to the first available candidate.
132///
133/// # Example
134///
135/// ```ignore
136/// use x402::scheme::client::PreferChain;
137/// use x402::chain::ChainIdPattern;
138///
139/// // Prefer Base, then any EVM chain, then anything else
140/// let selector = PreferChain::new(vec![
141///     "eip155:8453".parse().unwrap(),  // Base mainnet
142///     "eip155:*".parse().unwrap(),     // Any EVM chain
143/// ]);
144/// ```
145#[allow(dead_code)] // Public for consumption by downstream crates.
146pub struct PreferChain(Vec<ChainIdPattern>);
147
148#[allow(dead_code)] // Public for consumption by downstream crates.
149impl PreferChain {
150    /// Creates a new chain preference selector.
151    pub fn new<P: Into<Vec<ChainIdPattern>>>(patterns: P) -> Self {
152        Self(patterns.into())
153    }
154
155    /// Adds additional chain patterns with lower priority.
156    pub fn or_chain<P: Into<Vec<ChainIdPattern>>>(self, patterns: P) -> PreferChain {
157        PreferChain(self.0.into_iter().chain(patterns.into()).collect())
158    }
159}
160
161impl PaymentSelector for PreferChain {
162    fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> {
163        // Try each pattern in priority order
164        for pattern in &self.0 {
165            if let Some(candidate) = candidates.iter().find(|c| pattern.matches(&c.chain_id)) {
166                return Some(candidate);
167            }
168        }
169        // Fall back to first match if no patterns matched
170        candidates.first()
171    }
172}
173
174/// Selector that only accepts payments up to a maximum amount.
175///
176/// Useful for limiting spending or implementing budget controls.
177#[allow(dead_code)]
178pub struct MaxAmount(pub U256);
179
180impl PaymentSelector for MaxAmount {
181    fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> {
182        candidates.iter().find(|c| c.amount <= self.0)
183    }
184}