Skip to main content

fly402_client/
client.rs

1use fly402_core::{
2    PaymentAuthorization, PaymentRequest, SolanaPaymentProcessor, X402Error, X402Result,
3};
4use reqwest::{Client, Response, StatusCode};
5use solana_sdk::signature::Keypair;
6
7/// X402 HTTP client with explicit payment control
8///
9/// This client provides full control over the payment flow, allowing you to
10/// decide when and how to handle payment requests.
11pub struct X402Client {
12    http_client: Client,
13    payment_processor: SolanaPaymentProcessor,
14    keypair: Keypair,
15}
16
17impl X402Client {
18    /// Create a new X402 client
19    ///
20    /// # Arguments
21    /// * `keypair` - Solana keypair for signing transactions
22    /// * `rpc_url` - Optional Solana RPC URL (defaults to devnet)
23    pub fn new(keypair: Keypair, rpc_url: Option<&str>) -> Self {
24        let rpc_url = rpc_url.unwrap_or("https://api.devnet.solana.com");
25        Self {
26            http_client: Client::new(),
27            payment_processor: SolanaPaymentProcessor::new(rpc_url, None),
28            keypair,
29        }
30    }
31
32    /// Make a GET request
33    pub async fn get(&self, url: &str) -> X402Result<Response> {
34        self.request("GET", url, None, None).await
35    }
36
37    /// Make a GET request with payment authorization
38    pub async fn get_with_auth(
39        &self,
40        url: &str,
41        authorization: &PaymentAuthorization,
42    ) -> X402Result<Response> {
43        self.request("GET", url, None, Some(authorization)).await
44    }
45
46    /// Make a POST request
47    pub async fn post(&self, url: &str, body: Option<String>) -> X402Result<Response> {
48        self.request("POST", url, body, None).await
49    }
50
51    /// Make a POST request with payment authorization
52    pub async fn post_with_auth(
53        &self,
54        url: &str,
55        body: Option<String>,
56        authorization: &PaymentAuthorization,
57    ) -> X402Result<Response> {
58        self.request("POST", url, body, Some(authorization)).await
59    }
60
61    /// Make an HTTP request
62    async fn request(
63        &self,
64        method: &str,
65        url: &str,
66        body: Option<String>,
67        authorization: Option<&PaymentAuthorization>,
68    ) -> X402Result<Response> {
69        let mut request = match method {
70            "GET" => self.http_client.get(url),
71            "POST" => {
72                let mut req = self.http_client.post(url);
73                if let Some(b) = body {
74                    req = req.body(b).header("Content-Type", "application/json");
75                }
76                req
77            }
78            _ => {
79                return Err(X402Error::Configuration(format!(
80                    "Unsupported HTTP method: {}",
81                    method
82                )))
83            }
84        };
85
86        // Add payment authorization header if provided
87        if let Some(auth) = authorization {
88            let header_value = auth.to_header_value()?;
89            request = request.header("X-Payment-Authorization", header_value);
90        }
91
92        // Send request
93        let response = request.send().await.map_err(|e| {
94            X402Error::Network(format!("HTTP request failed: {}", e))
95        })?;
96
97        Ok(response)
98    }
99
100    /// Check if a response requires payment (402 status code)
101    pub fn is_payment_required(&self, response: &Response) -> bool {
102        response.status() == StatusCode::PAYMENT_REQUIRED
103    }
104
105    /// Parse payment request from 402 response
106    pub async fn parse_payment_request(&self, response: Response) -> X402Result<PaymentRequest> {
107        if !self.is_payment_required(&response) {
108            return Err(X402Error::InvalidPaymentRequest(format!(
109                "Response status is not 402, got {}",
110                response.status()
111            )));
112        }
113
114        // Get payment request from response body
115        let body = response.text().await.map_err(|e| {
116            X402Error::Network(format!("Failed to read response body: {}", e))
117        })?;
118
119        PaymentRequest::from_json(&body)
120    }
121
122    /// Create a payment from a payment request
123    ///
124    /// This creates, signs, and broadcasts the payment transaction
125    pub async fn create_payment(
126        &self,
127        request: &PaymentRequest,
128    ) -> X402Result<PaymentAuthorization> {
129        self.payment_processor
130            .create_payment(request, &self.keypair)
131            .await
132    }
133
134    /// Verify a payment authorization
135    pub async fn verify_payment(
136        &self,
137        authorization: &PaymentAuthorization,
138        expected_amount: &str,
139    ) -> X402Result<bool> {
140        self.payment_processor
141            .verify_payment(authorization, expected_amount)
142            .await
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_client_creation() {
152        let keypair = Keypair::new();
153        let client = X402Client::new(keypair, None);
154        assert!(true); // Just verify it compiles
155    }
156}