fly402_client/
auto_client.rs1use fly402_core::{PaymentRequest, X402Error, X402Result};
2use reqwest::{Response, StatusCode};
3use solana_sdk::signature::Keypair;
4
5use crate::client::X402Client;
6
7#[derive(Debug, Clone)]
9pub struct AutoClientOptions {
10 pub max_payment_amount: String,
12
13 pub auto_retry: bool,
15
16 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
30pub struct X402AutoClient {
35 client: X402Client,
36 options: AutoClientOptions,
37}
38
39impl X402AutoClient {
40 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 pub async fn get(&self, url: &str) -> X402Result<Response> {
59 self.request("GET", url, None).await
60 }
61
62 pub async fn post(&self, url: &str, body: Option<String>) -> X402Result<Response> {
64 self.request("POST", url, body).await
65 }
66
67 async fn request(&self, method: &str, url: &str, body: Option<String>) -> X402Result<Response> {
69 let mut retries = 0;
70
71 loop {
72 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 if response.status() == StatusCode::PAYMENT_REQUIRED {
86 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 let payment_request = self.client.parse_payment_request(response).await?;
96
97 self.check_payment_amount(&payment_request)?;
99
100 let authorization = self.client.create_payment(&payment_request).await?;
102
103 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 if retry_response.status().is_success() {
116 return Ok(retry_response);
117 }
118
119 if retry_response.status() == StatusCode::PAYMENT_REQUIRED {
121 continue;
122 }
123
124 return Ok(retry_response);
126 }
127
128 return Ok(response);
130 }
131 }
132
133 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 pub fn client(&self) -> &X402Client {
155 &self.client
156 }
157
158 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}