x402/client/
http_client.rs1use crate::constants::SCHEME_NAME;
2use crate::error::X402Error;
3use crate::payment::{PaymentPayload, PaymentRequiredBody};
4use crate::response::SettleResponse;
5use crate::scheme::SchemeClient;
6use base64::Engine;
7
8pub struct X402Client<S: SchemeClient> {
14 http: reqwest::Client,
15 scheme: S,
16}
17
18impl<S: SchemeClient> X402Client<S> {
19 pub fn new(scheme: S) -> Self {
20 Self {
21 http: reqwest::Client::builder()
22 .timeout(std::time::Duration::from_secs(30))
23 .redirect(reqwest::redirect::Policy::none())
24 .build()
25 .expect("failed to build HTTP client"),
26 scheme,
27 }
28 }
29
30 pub fn with_http_client(scheme: S, http: reqwest::Client) -> Self {
32 Self { http, scheme }
33 }
34
35 pub async fn fetch(
38 &self,
39 url: &str,
40 method: reqwest::Method,
41 ) -> Result<(reqwest::Response, Option<SettleResponse>), X402Error> {
42 self.fetch_with_body(url, method, None).await
43 }
44
45 pub async fn fetch_with_body(
47 &self,
48 url: &str,
49 method: reqwest::Method,
50 body: Option<Vec<u8>>,
51 ) -> Result<(reqwest::Response, Option<SettleResponse>), X402Error> {
52 let mut req = self.http.request(method.clone(), url);
54 if let Some(ref b) = body {
55 req = req.body(b.clone());
56 }
57
58 let resp = req
59 .send()
60 .await
61 .map_err(|e| X402Error::HttpError(format!("request failed: {e}")))?;
62
63 if resp.status().as_u16() != 402 {
64 return Ok((resp, None));
65 }
66
67 let body_402: PaymentRequiredBody = resp
69 .json()
70 .await
71 .map_err(|e| X402Error::HttpError(format!("failed to parse 402 body: {e}")))?;
72
73 let requirements = body_402
75 .accepts
76 .iter()
77 .find(|r| r.scheme == SCHEME_NAME)
78 .ok_or_else(|| {
79 X402Error::UnsupportedScheme(format!(
80 "no supported scheme found in {:?}",
81 body_402
82 .accepts
83 .iter()
84 .map(|r| &r.scheme)
85 .collect::<Vec<_>>()
86 ))
87 })?;
88
89 let payload = self
91 .scheme
92 .create_payment_payload(body_402.x402_version, requirements)
93 .await?;
94
95 let encoded = encode_payment(&payload)?;
97
98 let mut req = self.http.request(method, url);
99 req = req.header("PAYMENT-SIGNATURE", &encoded);
100 if let Some(b) = body {
101 req = req.body(b);
102 }
103
104 let resp = req
105 .send()
106 .await
107 .map_err(|e| X402Error::HttpError(format!("paid request failed: {e}")))?;
108
109 let settle = resp
112 .headers()
113 .get("payment-response")
114 .and_then(|v| v.to_str().ok())
115 .and_then(|s| {
116 let payload_part = s.split('.').next().unwrap_or(s);
118 base64::engine::general_purpose::STANDARD
119 .decode(payload_part)
120 .ok()
121 .and_then(|bytes| serde_json::from_slice::<SettleResponse>(&bytes).ok())
122 });
123
124 Ok((resp, settle))
125 }
126}
127
128pub fn encode_payment(payload: &PaymentPayload) -> Result<String, X402Error> {
130 let json = serde_json::to_vec(payload)?;
131 Ok(base64::engine::general_purpose::STANDARD.encode(&json))
132}
133
134pub fn decode_payment(encoded: &str) -> Result<PaymentPayload, X402Error> {
136 let bytes = base64::engine::general_purpose::STANDARD
137 .decode(encoded)
138 .map_err(|e| X402Error::InvalidPayment(format!("invalid base64: {e}")))?;
139 serde_json::from_slice(&bytes)
140 .map_err(|e| X402Error::InvalidPayment(format!("invalid JSON: {e}")))
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::payment::TempoPaymentData;
147 use alloy::primitives::{Address, FixedBytes};
148
149 fn sample_payload() -> PaymentPayload {
150 PaymentPayload {
151 x402_version: 1,
152 payload: TempoPaymentData {
153 from: Address::ZERO,
154 to: Address::ZERO,
155 value: "1000".to_string(),
156 token: Address::ZERO,
157 valid_after: 0,
158 valid_before: u64::MAX,
159 nonce: FixedBytes::ZERO,
160 signature: "0xdead".to_string(),
161 },
162 }
163 }
164
165 #[test]
166 fn test_encode_payment_roundtrip() {
167 let payload = sample_payload();
168 let encoded = encode_payment(&payload).unwrap();
169 let decoded = decode_payment(&encoded).unwrap();
170
171 assert_eq!(decoded.x402_version, payload.x402_version);
172 assert_eq!(decoded.payload.from, payload.payload.from);
173 assert_eq!(decoded.payload.value, payload.payload.value);
174 assert_eq!(decoded.payload.signature, payload.payload.signature);
175 }
176
177 #[test]
178 fn test_encode_produces_valid_base64() {
179 let payload = sample_payload();
180 let encoded = encode_payment(&payload).unwrap();
181
182 let result = base64::engine::general_purpose::STANDARD.decode(&encoded);
184 assert!(result.is_ok());
185
186 let json: Result<serde_json::Value, _> = serde_json::from_slice(&result.unwrap());
188 assert!(json.is_ok());
189 }
190}