x402_types/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 std::fmt::Debug for PaymentCandidate {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("PaymentCandidate")
52 .field("chain_id", &self.chain_id)
53 .field("asset", &self.asset)
54 .field("amount", &self.amount)
55 .field("scheme", &self.scheme)
56 .field("x402_version", &self.x402_version)
57 .field("pay_to", &self.pay_to)
58 .field("signer", &"<dyn PaymentCandidateSigner>")
59 .finish()
60 }
61}
62
63impl PaymentCandidate {
64 /// Signs this payment candidate, producing a payment payload.
65 #[allow(dead_code)] // Public for consumption by downstream crates.
66 pub async fn sign(&self) -> Result<String, X402Error> {
67 self.signer.sign_payment().await
68 }
69}
70
71/// Trait for scheme clients that can process payment requirements.
72///
73/// Implementations examine 402 responses and generate payment candidates
74/// for requirements they can fulfill.
75#[async_trait]
76#[allow(dead_code)] // Public for consumption by downstream crates.
77pub trait X402SchemeClient: X402SchemeId + Send + Sync {
78 /// Generates payment candidates for the given payment requirements.
79 fn accept(&self, payment_required: &proto::PaymentRequired) -> Vec<PaymentCandidate>;
80}
81
82/// Trait for signing payment authorizations.
83#[async_trait]
84#[allow(dead_code)] // Public for consumption by downstream crates.
85pub trait PaymentCandidateSigner {
86 /// Signs a payment authorization.
87 async fn sign_payment(&self) -> Result<String, X402Error>;
88}
89
90/// Errors that can occur during client-side payment processing.
91#[derive(Debug, thiserror::Error)]
92#[allow(dead_code)] // Public for consumption by downstream crates.
93pub enum X402Error {
94 /// No payment option matched the client's capabilities.
95 #[error("No matching payment option found")]
96 NoMatchingPaymentOption,
97
98 /// The HTTP request body cannot be cloned (e.g., streaming).
99 #[error("Request is not cloneable (streaming body?)")]
100 RequestNotCloneable,
101
102 /// Failed to parse the 402 response body.
103 #[error("Failed to parse 402 response: {0}")]
104 ParseError(String),
105
106 /// Failed to sign the payment authorization.
107 #[error("Failed to sign payment: {0}")]
108 SigningError(String),
109
110 /// JSON serialization/deserialization error.
111 #[error("JSON error: {0}")]
112 JsonError(#[from] serde_json::Error),
113}
114
115// ============================================================================
116// PaymentSelector - Selection strategy
117// ============================================================================
118
119/// Trait for selecting the best payment candidate from available options.
120///
121/// Implement this trait to customize how payments are selected when
122/// multiple options are available.
123#[allow(dead_code)] // Public for consumption by downstream crates.
124pub trait PaymentSelector: Send + Sync {
125 /// Selects a payment candidate from the available options.
126 fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate>;
127}
128
129/// Selector that returns the first matching candidate.
130///
131/// This is the simplest selection strategy. The order of candidates
132/// is determined by the registration order of scheme clients.
133#[allow(dead_code)] // Public for consumption by downstream crates.
134pub struct FirstMatch;
135
136impl PaymentSelector for FirstMatch {
137 fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> {
138 candidates.first()
139 }
140}
141
142/// Selector that prefers specific chains in priority order.
143///
144/// Patterns are tried in order; the first matching candidate is returned.
145/// If no patterns match, falls back to the first available candidate.
146///
147/// # Example
148///
149/// ```rust
150/// use x402_types::scheme::client::PreferChain;
151/// use x402_types::chain::ChainIdPattern;
152///
153/// // Prefer Base, then any EVM chain, then anything else
154/// let selector = PreferChain::new(vec![
155/// "eip155:8453".parse().unwrap(), // Base mainnet
156/// "eip155:*".parse().unwrap(), // Any EVM chain
157/// ]);
158/// ```
159#[allow(dead_code)] // Public for consumption by downstream crates.
160pub struct PreferChain(Vec<ChainIdPattern>);
161
162#[allow(dead_code)] // Public for consumption by downstream crates.
163impl PreferChain {
164 /// Creates a new chain preference selector.
165 pub fn new<P: Into<Vec<ChainIdPattern>>>(patterns: P) -> Self {
166 Self(patterns.into())
167 }
168
169 /// Adds additional chain patterns with lower priority.
170 pub fn or_chain<P: Into<Vec<ChainIdPattern>>>(self, patterns: P) -> PreferChain {
171 PreferChain(self.0.into_iter().chain(patterns.into()).collect())
172 }
173}
174
175impl PaymentSelector for PreferChain {
176 fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> {
177 // Try each pattern in priority order
178 for pattern in &self.0 {
179 if let Some(candidate) = candidates.iter().find(|c| pattern.matches(&c.chain_id)) {
180 return Some(candidate);
181 }
182 }
183 // Fall back to first match if no patterns matched
184 candidates.first()
185 }
186}
187
188/// Selector that only accepts payments up to a maximum amount.
189///
190/// Useful for limiting spending or implementing budget controls.
191#[allow(dead_code)]
192pub struct MaxAmount(pub U256);
193
194impl PaymentSelector for MaxAmount {
195 fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> {
196 candidates.iter().find(|c| c.amount <= self.0)
197 }
198}