Skip to main content

fly402_client/
auto_client.rs

1use fly402_core::{PaymentRequest, X402Error, X402Result};
2use reqwest::{Response, StatusCode};
3use solana_sdk::signature::Keypair;
4
5use crate::client::X402Client;
6
7/// Configuration options for the auto client
8#[derive(Debug, Clone)]
9pub struct AutoClientOptions {
10    /// Maximum amount willing to pay automatically (in USDC)
11    pub max_payment_amount: String,
12
13    /// Whether to automatically retry after payment
14    pub auto_retry: bool,
15
16    /// Maximum number of retry attempts
17    pub max_retries: u32,
18}
19
20impl Default for AutoClientOptions {
21    fn default() -> Self {
22        Self {
23            max_payment_amount: "10.0".to_string(),
24            auto_retry: true,
25            max_retries: 3,
26        }
27    }
28}
29
30/// X402 HTTP client with automatic payment handling
31///
32/// This client automatically detects 402 Payment Required responses,
33/// creates and sends payments, and retries the original request.
34pub struct X402AutoClient {
35    client: X402Client,
36    options: AutoClientOptions,
37}
38
39impl X402AutoClient {
40    /// Create a new auto client
41    ///
42    /// # Arguments
43    /// * `keypair` - Solana keypair for signing transactions
44    /// * `rpc_url` - Optional Solana RPC URL (defaults to devnet)
45    /// * `options` - Optional configuration (uses defaults if None)
46    pub fn new(
47        keypair: Keypair,
48        rpc_url: Option<&str>,
49        options: Option<AutoClientOptions>,
50    ) -> Self {
51        Self {
52            client: X402Client::new(keypair, rpc_url),
53            options: options.unwrap_or_default(),
54        }
55    }
56
57    /// Make a GET request with automatic payment handling
58    pub async fn get(&self, url: &str) -> X402Result<Response> {
59        self.request("GET", url, None).await
60    }
61
62    /// Make a POST request with automatic payment handling
63    pub async fn post(&self, url: &str, body: Option<String>) -> X402Result<Response> {
64        self.request("POST", url, body).await
65    }
66
67    /// Make an HTTP request with automatic payment handling
68    async fn request(&self, method: &str, url: &str, body: Option<String>) -> X402Result<Response> {
69        let mut retries = 0;
70
71        loop {
72            // Make initial request
73            let response = match method {
74                "GET" => self.client.get(url).await?,
75                "POST" => self.client.post(url, body.clone()).await?,
76                _ => {
77                    return Err(X402Error::Configuration(format!(
78                        "Unsupported HTTP method: {}",
79                        method
80                    )))
81                }
82            };
83
84            // Check if payment is required
85            if response.status() == StatusCode::PAYMENT_REQUIRED {
86                // Check retry limit
87                if retries >= self.options.max_retries {
88                    return Err(X402Error::PaymentRequired(
89                        "Maximum retry attempts reached".to_string(),
90                    ));
91                }
92                retries += 1;
93
94                // Parse payment request
95                let payment_request = self.client.parse_payment_request(response).await?;
96
97                // Check if amount is acceptable
98                self.check_payment_amount(&payment_request)?;
99
100                // Create and send payment
101                let authorization = self.client.create_payment(&payment_request).await?;
102
103                // Retry request with payment authorization
104                let retry_response = match method {
105                    "GET" => self.client.get_with_auth(url, &authorization).await?,
106                    "POST" => {
107                        self.client
108                            .post_with_auth(url, body.clone(), &authorization)
109                            .await?
110                    }
111                    _ => unreachable!(),
112                };
113
114                // Check if retry was successful
115                if retry_response.status().is_success() {
116                    return Ok(retry_response);
117                }
118
119                // If still getting 402, continue loop
120                if retry_response.status() == StatusCode::PAYMENT_REQUIRED {
121                    continue;
122                }
123
124                // Return other error responses
125                return Ok(retry_response);
126            }
127
128            // Return successful or non-402 error responses
129            return Ok(response);
130        }
131    }
132
133    /// Check if the payment amount is acceptable
134    fn check_payment_amount(&self, request: &PaymentRequest) -> X402Result<()> {
135        let max_amount: f64 = self.options.max_payment_amount.parse().map_err(|e| {
136            X402Error::Configuration(format!("Invalid max_payment_amount: {}", e))
137        })?;
138
139        let required_amount: f64 = request.max_amount_required.parse().map_err(|e| {
140            X402Error::InvalidPaymentRequest(format!("Invalid payment amount: {}", e))
141        })?;
142
143        if required_amount > max_amount {
144            return Err(X402Error::PaymentRequired(format!(
145                "Payment amount {} exceeds maximum allowed amount {}",
146                required_amount, max_amount
147            )));
148        }
149
150        Ok(())
151    }
152
153    /// Get the underlying client for manual operations
154    pub fn client(&self) -> &X402Client {
155        &self.client
156    }
157
158    /// Get the client options
159    pub fn options(&self) -> &AutoClientOptions {
160        &self.options
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_auto_client_creation() {
170        let keypair = Keypair::new();
171        let client = X402AutoClient::new(keypair, None, None);
172        assert_eq!(client.options().max_payment_amount, "10.0");
173        assert!(client.options().auto_retry);
174    }
175
176    #[test]
177    fn test_custom_options() {
178        let keypair = Keypair::new();
179        let options = AutoClientOptions {
180            max_payment_amount: "5.0".to_string(),
181            auto_retry: false,
182            max_retries: 1,
183        };
184        let client = X402AutoClient::new(keypair, None, Some(options));
185        assert_eq!(client.options().max_payment_amount, "5.0");
186        assert!(!client.options().auto_retry);
187    }
188}