1use crate::types::*;
4use crate::{Result, X402Error};
5use http;
6use reqwest::{Client, Response};
7use std::time::Duration;
8
9#[derive(Debug, Clone)]
11pub struct X402Client {
12 client: Client,
14 facilitator_config: FacilitatorConfig,
16}
17
18impl X402Client {
19 pub fn new() -> Result<Self> {
21 Self::with_config(FacilitatorConfig::default())
22 }
23
24 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 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 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 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 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 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 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 let payment_header = payment_payload.to_base64()?;
96
97 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 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 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 response.status() == 402 {
144 if let Some(payload) = payment_payload {
145 return self.handle_payment_required(response, payload).await;
146 } else {
147 return Ok(response);
149 }
150 }
151
152 Ok(response)
153 }
154
155 pub fn facilitator_config(&self) -> &FacilitatorConfig {
157 &self.facilitator_config
158 }
159
160 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 Self {
172 client: Client::new(),
173 facilitator_config: FacilitatorConfig::default(),
174 }
175 })
176 }
177}
178
179#[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 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 pub fn headers(self, headers: reqwest::header::HeaderMap) -> Self {
218 Self {
219 request: self.request.headers(headers),
220 ..self
221 }
222 }
223
224 pub fn body(self, body: impl Into<reqwest::Body>) -> Self {
226 Self {
227 request: self.request.body(body),
228 ..self
229 }
230 }
231
232 pub fn json<T: serde::Serialize>(self, json: &T) -> Self {
234 Self {
235 request: self.request.json(json),
236 ..self
237 }
238 }
239
240 pub fn form<T: serde::Serialize>(self, form: &T) -> Self {
242 Self {
243 request: self.request.form(form),
244 ..self
245 }
246 }
247
248 pub fn query<T: serde::Serialize>(self, query: &T) -> Self {
250 Self {
251 request: self.request.query(query),
252 ..self
253 }
254 }
255
256 pub fn timeout(self, timeout: Duration) -> Self {
258 Self {
259 request: self.request.timeout(timeout),
260 ..self
261 }
262 }
263
264 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 pub async fn send(self) -> Result<Response> {
272 self.request.send().await.map_err(X402Error::from)
273 }
274
275 pub async fn send_with_payment(self, payment_payload: &PaymentPayload) -> Result<Response> {
277 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 let _payment_requirements: PaymentRequirementsResponse = response.json().await?;
286
287 let payment_header = payment_payload.to_base64()?;
289
290 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 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 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#[derive(Debug, Clone)]
322pub struct DiscoveryClient {
323 url: String,
325 client: Client,
327}
328
329impl DiscoveryClient {
330 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 pub fn default_client() -> Self {
341 Self::new("https://x402.org/discovery")
342 }
343
344 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 pub async fn get_all_resources(&self) -> Result<DiscoveryResponse> {
378 self.discover_resources(None).await
379 }
380
381 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 pub fn url(&self) -> &str {
393 &self.url
394 }
395}
396
397#[derive(Debug, Clone)]
399pub struct DiscoveryFilters {
400 pub resource_type: Option<String>,
402 pub limit: Option<u32>,
404 pub offset: Option<u32>,
406}
407
408impl DiscoveryFilters {
409 pub fn new() -> Self {
411 Self {
412 resource_type: None,
413 limit: None,
414 offset: None,
415 }
416 }
417
418 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 pub fn with_limit(mut self, limit: u32) -> Self {
426 self.limit = Some(limit);
427 self
428 }
429
430 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 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 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}