1use fly402_core::{
2 PaymentAuthorization, PaymentRequest, SolanaPaymentProcessor, X402Error, X402Result,
3};
4use reqwest::{Client, Response, StatusCode};
5use solana_sdk::signature::Keypair;
6
7pub struct X402Client {
12 http_client: Client,
13 payment_processor: SolanaPaymentProcessor,
14 keypair: Keypair,
15}
16
17impl X402Client {
18 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 pub async fn get(&self, url: &str) -> X402Result<Response> {
34 self.request("GET", url, None, None).await
35 }
36
37 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 pub async fn post(&self, url: &str, body: Option<String>) -> X402Result<Response> {
48 self.request("POST", url, body, None).await
49 }
50
51 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 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 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 let response = request.send().await.map_err(|e| {
94 X402Error::Network(format!("HTTP request failed: {}", e))
95 })?;
96
97 Ok(response)
98 }
99
100 pub fn is_payment_required(&self, response: &Response) -> bool {
102 response.status() == StatusCode::PAYMENT_REQUIRED
103 }
104
105 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 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 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 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); }
156}