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}