rust_x402/
client.rs

1//! HTTP client with x402 payment support
2
3use crate::types::*;
4use crate::{Result, X402Error};
5use axum::http;
6use reqwest::{Client, Response};
7use std::time::Duration;
8
9/// HTTP client with x402 payment support
10#[derive(Debug, Clone)]
11pub struct X402Client {
12    /// Underlying HTTP client
13    client: Client,
14    /// Default facilitator configuration
15    facilitator_config: FacilitatorConfig,
16}
17
18impl X402Client {
19    /// Create a new x402 client
20    pub fn new() -> Result<Self> {
21        Self::with_config(FacilitatorConfig::default())
22    }
23
24    /// Create a new x402 client with custom configuration
25    pub fn with_config(facilitator_config: FacilitatorConfig) -> Result<Self> {
26        let client = Client::builder()
27            .timeout(Duration::from_secs(30))
28            .build()
29            .map_err(|e| X402Error::config(format!("Failed to create HTTP client: {}", e)))?;
30
31        Ok(Self {
32            client,
33            facilitator_config,
34        })
35    }
36
37    /// Create a GET request
38    pub fn get(&self, url: &str) -> X402RequestBuilder<'_> {
39        let mut builder = X402RequestBuilder::new(self, self.client.get(url));
40        builder.method = "GET".to_string();
41        builder.url = url.to_string();
42        builder
43    }
44
45    /// Create a POST request
46    pub fn post(&self, url: &str) -> X402RequestBuilder<'_> {
47        let mut builder = X402RequestBuilder::new(self, self.client.post(url));
48        builder.method = "POST".to_string();
49        builder.url = url.to_string();
50        builder
51    }
52
53    /// Create a PUT request
54    pub fn put(&self, url: &str) -> X402RequestBuilder<'_> {
55        let mut builder = X402RequestBuilder::new(self, self.client.put(url));
56        builder.method = "PUT".to_string();
57        builder.url = url.to_string();
58        builder
59    }
60
61    /// Create a DELETE request
62    pub fn delete(&self, url: &str) -> X402RequestBuilder<'_> {
63        let mut builder = X402RequestBuilder::new(self, self.client.delete(url));
64        builder.method = "DELETE".to_string();
65        builder.url = url.to_string();
66        builder
67    }
68
69    /// Handle a 402 payment required response with automatic retry
70    pub async fn handle_payment_required(
71        &self,
72        response: Response,
73        payment_payload: &PaymentPayload,
74    ) -> Result<Response> {
75        if response.status() != 402 {
76            return Ok(response);
77        }
78
79        let original_url = response.url().to_string();
80        let payment_requirements: PaymentRequirementsResponse = response.json().await?;
81
82        // Verify the payment with the facilitator
83        let facilitator = super::facilitator::FacilitatorClient::new(
84            self.facilitator_config.clone(),
85        )
86        .map_err(|e| {
87            X402Error::facilitator_error(format!("Failed to create facilitator client: {}", e))
88        })?;
89
90        for requirements in &payment_requirements.accepts {
91            let verify_response = facilitator.verify(payment_payload, requirements).await?;
92
93            if verify_response.is_valid {
94                // Retry the original request with payment
95                let payment_header = payment_payload.to_base64()?;
96
97                // Create a new request with payment header
98                let new_response = self
99                    .client
100                    .get(&original_url)
101                    .header("X-PAYMENT", payment_header)
102                    .send()
103                    .await?;
104
105                return Ok(new_response);
106            }
107        }
108
109        Err(X402Error::payment_verification_failed(
110            "Payment verification failed for all requirements",
111        ))
112    }
113
114    /// Make a request with automatic payment handling
115    pub async fn request_with_payment(
116        &self,
117        method: &str,
118        url: &str,
119        payment_payload: Option<&PaymentPayload>,
120    ) -> Result<Response> {
121        let mut request_builder = match method.to_uppercase().as_str() {
122            "GET" => self.get(url),
123            "POST" => self.post(url),
124            "PUT" => self.put(url),
125            "DELETE" => self.delete(url),
126            _ => {
127                return Err(X402Error::unexpected(format!(
128                    "Unsupported HTTP method: {}",
129                    method
130                )))
131            }
132        };
133
134        // Add payment header if provided
135        if let Some(payload) = payment_payload {
136            let payment_header = payload.to_base64()?;
137            request_builder = request_builder.header("X-PAYMENT", payment_header);
138        }
139
140        let response = request_builder.send().await?;
141
142        // If we get a 402 and have a payment payload, try to handle it
143        if response.status() == 402 {
144            if let Some(payload) = payment_payload {
145                return self.handle_payment_required(response, payload).await;
146            } else {
147                // Return the 402 response as-is if no payment payload provided
148                return Ok(response);
149            }
150        }
151
152        Ok(response)
153    }
154
155    /// Get the facilitator configuration
156    pub fn facilitator_config(&self) -> &FacilitatorConfig {
157        &self.facilitator_config
158    }
159
160    /// Set a new facilitator configuration
161    pub fn with_facilitator_config(mut self, config: FacilitatorConfig) -> Self {
162        self.facilitator_config = config;
163        self
164    }
165}
166
167impl Default for X402Client {
168    fn default() -> Self {
169        Self::with_config(FacilitatorConfig::default()).unwrap_or_else(|_| {
170            // Fallback to basic client if configuration fails
171            Self {
172                client: Client::new(),
173                facilitator_config: FacilitatorConfig::default(),
174            }
175        })
176    }
177}
178
179/// Request builder for x402 client
180#[derive(Debug)]
181pub struct X402RequestBuilder<'a> {
182    client: &'a X402Client,
183    request: reqwest::RequestBuilder,
184    method: String,
185    url: String,
186    _headers: std::collections::HashMap<String, String>,
187    _body: Option<Vec<u8>>,
188}
189
190impl<'a> X402RequestBuilder<'a> {
191    fn new(client: &'a X402Client, request: reqwest::RequestBuilder) -> Self {
192        Self {
193            client,
194            request,
195            method: String::new(),
196            url: String::new(),
197            _headers: std::collections::HashMap::new(),
198            _body: None,
199        }
200    }
201
202    /// Add a header to the request
203    pub fn header<K, V>(self, key: K, value: V) -> Self
204    where
205        reqwest::header::HeaderName: std::convert::TryFrom<K>,
206        <reqwest::header::HeaderName as std::convert::TryFrom<K>>::Error: Into<http::Error>,
207        reqwest::header::HeaderValue: std::convert::TryFrom<V>,
208        <reqwest::header::HeaderValue as std::convert::TryFrom<V>>::Error: Into<http::Error>,
209    {
210        Self {
211            request: self.request.header(key, value),
212            ..self
213        }
214    }
215
216    /// Add multiple headers to the request
217    pub fn headers(self, headers: reqwest::header::HeaderMap) -> Self {
218        Self {
219            request: self.request.headers(headers),
220            ..self
221        }
222    }
223
224    /// Set the request body
225    pub fn body(self, body: impl Into<reqwest::Body>) -> Self {
226        Self {
227            request: self.request.body(body),
228            ..self
229        }
230    }
231
232    /// Set JSON body
233    pub fn json<T: serde::Serialize>(self, json: &T) -> Self {
234        Self {
235            request: self.request.json(json),
236            ..self
237        }
238    }
239
240    /// Set form data
241    pub fn form<T: serde::Serialize>(self, form: &T) -> Self {
242        Self {
243            request: self.request.form(form),
244            ..self
245        }
246    }
247
248    /// Set query parameters
249    pub fn query<T: serde::Serialize>(self, query: &T) -> Self {
250        Self {
251            request: self.request.query(query),
252            ..self
253        }
254    }
255
256    /// Set timeout for the request
257    pub fn timeout(self, timeout: Duration) -> Self {
258        Self {
259            request: self.request.timeout(timeout),
260            ..self
261        }
262    }
263
264    /// Add a payment header to the request
265    pub fn payment(self, payment_payload: &PaymentPayload) -> Result<Self> {
266        let payment_header = payment_payload.to_base64()?;
267        Ok(self.header("X-PAYMENT", &payment_header))
268    }
269
270    /// Send the request
271    pub async fn send(self) -> Result<Response> {
272        self.request.send().await.map_err(X402Error::from)
273    }
274
275    /// Send the request and handle x402 payments automatically
276    pub async fn send_with_payment(self, payment_payload: &PaymentPayload) -> Result<Response> {
277        // Save values before consuming self
278        let original_url = self.url.clone();
279        let client = self.client.clone();
280
281        let response = self.send().await?;
282
283        if response.status() == 402 {
284            // Parse payment requirements from 402 response
285            let _payment_requirements: PaymentRequirementsResponse = response.json().await?;
286
287            // Create a new request with payment header
288            let payment_header = payment_payload.to_base64()?;
289
290            // Create a new request with payment header
291            let new_response = client
292                .client
293                .get(&original_url)
294                .header("X-PAYMENT", &payment_header)
295                .send()
296                .await?;
297
298            Ok(new_response)
299        } else {
300            Ok(response)
301        }
302    }
303
304    /// Send the request and return the response as text
305    pub async fn send_and_get_text(self) -> Result<String> {
306        let response = self.send().await?;
307        response.text().await.map_err(X402Error::from)
308    }
309
310    /// Send the request and return the response as JSON
311    pub async fn send_and_get_json<T>(self) -> Result<T>
312    where
313        T: serde::de::DeserializeOwned,
314    {
315        let response = self.send().await?;
316        response.json().await.map_err(X402Error::from)
317    }
318}
319
320/// Discovery client for finding x402 resources
321#[derive(Debug, Clone)]
322pub struct DiscoveryClient {
323    /// Base URL of the discovery service
324    url: String,
325    /// HTTP client
326    client: Client,
327}
328
329impl DiscoveryClient {
330    /// Create a new discovery client
331    pub fn new(url: impl Into<String>) -> Self {
332        let client = Client::new();
333        Self {
334            url: url.into(),
335            client,
336        }
337    }
338
339    /// Get the default discovery client
340    pub fn default_client() -> Self {
341        Self::new("https://x402.org/discovery")
342    }
343
344    /// Discover resources with optional filters
345    pub async fn discover_resources(
346        &self,
347        filters: Option<DiscoveryFilters>,
348    ) -> Result<DiscoveryResponse> {
349        let mut request = self.client.get(format!("{}/resources", self.url));
350
351        if let Some(filters) = filters {
352            if let Some(resource_type) = filters.resource_type {
353                request = request.query(&[("type", resource_type)]);
354            }
355            if let Some(limit) = filters.limit {
356                request = request.query(&[("limit", limit.to_string())]);
357            }
358            if let Some(offset) = filters.offset {
359                request = request.query(&[("offset", offset.to_string())]);
360            }
361        }
362
363        let response = request.send().await?;
364
365        if !response.status().is_success() {
366            return Err(X402Error::facilitator_error(format!(
367                "Discovery failed with status: {}",
368                response.status()
369            )));
370        }
371
372        let discovery_response: DiscoveryResponse = response.json().await?;
373        Ok(discovery_response)
374    }
375
376    /// Get all available resources
377    pub async fn get_all_resources(&self) -> Result<DiscoveryResponse> {
378        self.discover_resources(None).await
379    }
380
381    /// Get resources by type
382    pub async fn get_resources_by_type(&self, resource_type: &str) -> Result<DiscoveryResponse> {
383        self.discover_resources(Some(DiscoveryFilters {
384            resource_type: Some(resource_type.to_string()),
385            limit: None,
386            offset: None,
387        }))
388        .await
389    }
390
391    /// Get the base URL of this discovery service
392    pub fn url(&self) -> &str {
393        &self.url
394    }
395}
396
397/// Filters for discovery requests
398#[derive(Debug, Clone)]
399pub struct DiscoveryFilters {
400    /// Filter by resource type
401    pub resource_type: Option<String>,
402    /// Maximum number of results
403    pub limit: Option<u32>,
404    /// Number of results to skip
405    pub offset: Option<u32>,
406}
407
408impl DiscoveryFilters {
409    /// Create new discovery filters
410    pub fn new() -> Self {
411        Self {
412            resource_type: None,
413            limit: None,
414            offset: None,
415        }
416    }
417
418    /// Set resource type filter
419    pub fn with_resource_type(mut self, resource_type: impl Into<String>) -> Self {
420        self.resource_type = Some(resource_type.into());
421        self
422    }
423
424    /// Set limit
425    pub fn with_limit(mut self, limit: u32) -> Self {
426        self.limit = Some(limit);
427        self
428    }
429
430    /// Set offset
431    pub fn with_offset(mut self, offset: u32) -> Self {
432        self.offset = Some(offset);
433        self
434    }
435}
436
437impl Default for DiscoveryFilters {
438    fn default() -> Self {
439        Self::new()
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_client_creation() {
449        let client = X402Client::new().unwrap();
450        assert_eq!(
451            client.facilitator_config().url,
452            "https://x402.org/facilitator"
453        );
454    }
455
456    #[test]
457    fn test_client_with_config() {
458        let config = FacilitatorConfig::new("https://custom-facilitator.com");
459        let client = X402Client::with_config(config).unwrap();
460        assert_eq!(
461            client.facilitator_config().url,
462            "https://custom-facilitator.com"
463        );
464    }
465
466    #[test]
467    fn test_discovery_filters() {
468        let filters = DiscoveryFilters::new()
469            .with_resource_type("http")
470            .with_limit(10)
471            .with_offset(0);
472
473        assert_eq!(filters.resource_type, Some("http".to_string()));
474        assert_eq!(filters.limit, Some(10));
475        assert_eq!(filters.offset, Some(0));
476    }
477
478    #[test]
479    fn test_discovery_client_creation() {
480        let client = DiscoveryClient::new("https://example.com/discovery");
481        assert_eq!(client.url(), "https://example.com/discovery");
482    }
483
484    #[test]
485    fn test_client_with_payment_request() {
486        let client = X402Client::new().unwrap();
487
488        // Test that we can create requests with different methods
489        let get_request = client.get("https://example.com");
490        let post_request = client.post("https://example.com");
491        let put_request = client.put("https://example.com");
492        let delete_request = client.delete("https://example.com");
493
494        // These should not panic
495        assert_eq!(get_request.method, "GET");
496        assert_eq!(post_request.method, "POST");
497        assert_eq!(put_request.method, "PUT");
498        assert_eq!(delete_request.method, "DELETE");
499    }
500
501    #[test]
502    fn test_discovery_filters_builder() {
503        let filters = DiscoveryFilters::new()
504            .with_resource_type("http")
505            .with_limit(10)
506            .with_offset(5);
507
508        assert_eq!(filters.resource_type, Some("http".to_string()));
509        assert_eq!(filters.limit, Some(10));
510        assert_eq!(filters.offset, Some(5));
511    }
512}