use fly402_core::{PaymentRequest, X402Error, X402Result};
use reqwest::{Response, StatusCode};
use solana_sdk::signature::Keypair;
use crate::client::X402Client;
#[derive(Debug, Clone)]
pub struct AutoClientOptions {
pub max_payment_amount: String,
pub auto_retry: bool,
pub max_retries: u32,
}
impl Default for AutoClientOptions {
fn default() -> Self {
Self {
max_payment_amount: "10.0".to_string(),
auto_retry: true,
max_retries: 3,
}
}
}
pub struct X402AutoClient {
client: X402Client,
options: AutoClientOptions,
}
impl X402AutoClient {
pub fn new(
keypair: Keypair,
rpc_url: Option<&str>,
options: Option<AutoClientOptions>,
) -> Self {
Self {
client: X402Client::new(keypair, rpc_url),
options: options.unwrap_or_default(),
}
}
pub async fn get(&self, url: &str) -> X402Result<Response> {
self.request("GET", url, None).await
}
pub async fn post(&self, url: &str, body: Option<String>) -> X402Result<Response> {
self.request("POST", url, body).await
}
async fn request(&self, method: &str, url: &str, body: Option<String>) -> X402Result<Response> {
let mut retries = 0;
loop {
let response = match method {
"GET" => self.client.get(url).await?,
"POST" => self.client.post(url, body.clone()).await?,
_ => {
return Err(X402Error::Configuration(format!(
"Unsupported HTTP method: {}",
method
)))
}
};
if response.status() == StatusCode::PAYMENT_REQUIRED {
if retries >= self.options.max_retries {
return Err(X402Error::PaymentRequired(
"Maximum retry attempts reached".to_string(),
));
}
retries += 1;
let payment_request = self.client.parse_payment_request(response).await?;
self.check_payment_amount(&payment_request)?;
let authorization = self.client.create_payment(&payment_request).await?;
let retry_response = match method {
"GET" => self.client.get_with_auth(url, &authorization).await?,
"POST" => {
self.client
.post_with_auth(url, body.clone(), &authorization)
.await?
}
_ => unreachable!(),
};
if retry_response.status().is_success() {
return Ok(retry_response);
}
if retry_response.status() == StatusCode::PAYMENT_REQUIRED {
continue;
}
return Ok(retry_response);
}
return Ok(response);
}
}
fn check_payment_amount(&self, request: &PaymentRequest) -> X402Result<()> {
let max_amount: f64 = self.options.max_payment_amount.parse().map_err(|e| {
X402Error::Configuration(format!("Invalid max_payment_amount: {}", e))
})?;
let required_amount: f64 = request.max_amount_required.parse().map_err(|e| {
X402Error::InvalidPaymentRequest(format!("Invalid payment amount: {}", e))
})?;
if required_amount > max_amount {
return Err(X402Error::PaymentRequired(format!(
"Payment amount {} exceeds maximum allowed amount {}",
required_amount, max_amount
)));
}
Ok(())
}
pub fn client(&self) -> &X402Client {
&self.client
}
pub fn options(&self) -> &AutoClientOptions {
&self.options
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auto_client_creation() {
let keypair = Keypair::new();
let client = X402AutoClient::new(keypair, None, None);
assert_eq!(client.options().max_payment_amount, "10.0");
assert!(client.options().auto_retry);
}
#[test]
fn test_custom_options() {
let keypair = Keypair::new();
let options = AutoClientOptions {
max_payment_amount: "5.0".to_string(),
auto_retry: false,
max_retries: 1,
};
let client = X402AutoClient::new(keypair, None, Some(options));
assert_eq!(client.options().max_payment_amount, "5.0");
assert!(!client.options().auto_retry);
}
}