1use crate::errors::ClobError;
2use reqwest::Client;
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6use std::collections::HashMap;
7use std::sync::LazyLock;
8use std::time::Duration;
9
10const DEFAULT_TIMEOUT_SECS: u64 = 30;
12const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 10;
14
15static DEFAULT_CLIENT: LazyLock<Client> = LazyLock::new(|| {
17 Client::builder()
18 .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
19 .connect_timeout(Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS))
20 .pool_idle_timeout(Duration::from_secs(90))
21 .pool_max_idle_per_host(10)
22 .tcp_keepalive(Duration::from_secs(30))
23 .build()
24 .unwrap_or_else(|_| Client::new())
25});
26
27pub fn default_client() -> &'static Client {
28 &DEFAULT_CLIENT
29}
30
31pub const GET: &str = "GET";
32pub const POST: &str = "POST";
33pub const DELETE: &str = "DELETE";
34pub const PUT: &str = "PUT";
35
36pub type QueryParams = HashMap<String, String>;
37
38pub struct RequestOptions<B = Value> {
39 pub headers: Option<HashMap<String, String>>,
40 pub data: Option<B>,
41 pub params: Option<QueryParams>,
42}
43
44fn is_debug() -> bool {
45 std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok()
46}
47
48fn mask_header_value(key: &str, v: &str) -> String {
49 if key.contains("PASSPHRASE") || key.contains("API_KEY") {
50 if v.len() > 6 {
51 format!("{}***", &v[..6])
52 } else {
53 "***".to_string()
54 }
55 } else if key.contains("SIGNATURE") {
56 if v.len() > 12 {
57 format!("{}...", &v[..12])
58 } else {
59 "***".to_string()
60 }
61 } else {
62 v.to_string()
63 }
64}
65
66fn mask_headers(h: &HashMap<String, String>) -> HashMap<String, String> {
67 h.iter()
68 .map(|(k, v)| (k.clone(), mask_header_value(k, v)))
69 .collect()
70}
71
72fn handle_http_error(
74 status: reqwest::StatusCode,
75 method: &str,
76 endpoint: &str,
77 body_text: &str,
78) -> ClobError {
79 eprintln!("HTTP Error Response:");
80 eprintln!(" Status: {}", status);
81 eprintln!(" Endpoint: {} {}", method, endpoint);
82 eprintln!(" Response Body: {}", body_text);
83
84 ClobError::HttpError {
85 status: status.as_u16(),
86 method: method.to_string(),
87 endpoint: endpoint.to_string(),
88 body: body_text.to_string(),
89 }
90}
91
92fn handle_json_error(
93 endpoint: &str,
94 method: &str,
95 err: &serde_json::Error,
96 body_text: &str,
97) -> ClobError {
98 eprintln!("JSON Parse Error:");
99 eprintln!(" Endpoint: {} {}", method, endpoint);
100 eprintln!(" Error: {}", err);
101 eprintln!(" Response Body: {}", body_text);
102
103 let truncated = if body_text.len() > 500 {
104 format!(
105 "{}... (truncated, {} bytes total)",
106 &body_text[..500],
107 body_text.len()
108 )
109 } else {
110 body_text.to_string()
111 };
112
113 ClobError::Other(format!(
114 "Failed to parse JSON response from {} {}: {}. Response body: {}",
115 method, endpoint, err, truncated
116 ))
117}
118
119fn apply_headers(
120 req: reqwest::RequestBuilder,
121 h: &HashMap<String, String>,
122) -> reqwest::RequestBuilder {
123 let mut r = req;
124 for (k, v) in h.iter() {
125 r = r.header(k, v);
126 }
127 r
128}
129
130pub async fn post_typed<R, B>(
131 client: &Client,
132 endpoint: &str,
133 options: Option<RequestOptions<B>>,
134) -> Result<R, ClobError>
135where
136 R: DeserializeOwned,
137 B: Serialize,
138{
139 let mut req = client.post(endpoint);
140 let mut debug_headers = None;
141 let mut debug_body = None;
142 let mut debug_params = None;
143
144 if let Some(opts) = options {
145 if let Some(h) = opts.headers {
146 if is_debug() {
147 debug_headers = Some(mask_headers(&h));
148 }
149 req = apply_headers(req, &h);
150 }
151 if let Some(body) = opts.data {
152 if is_debug()
153 && let Ok(b) = serde_json::to_string(&body)
154 {
155 debug_body = Some(b);
156 }
157 req = req.json(&body);
158 }
159 if let Some(params) = opts.params {
160 if is_debug() {
161 debug_params = Some(params.clone());
162 }
163 req = req.query(¶ms);
164 }
165 }
166
167 if is_debug() {
168 eprintln!("[HTTP DEBUG] POST {}", endpoint);
169 if let Some(h) = &debug_headers {
170 eprintln!(" headers={:?}", h);
171 }
172 if let Some(p) = &debug_params {
173 eprintln!(" params={:?}", p);
174 }
175 if let Some(b) = &debug_body {
176 let preview = if b.len() > 800 {
177 format!("{}... ({} bytes)", &b[..800], b.len())
178 } else {
179 b.clone()
180 };
181 eprintln!(" body={}", preview);
182 }
183 }
184
185 let resp = req.send().await.map_err(ClobError::Reqwest)?;
186 let status = resp.status();
187
188 if !status.is_success() {
189 let body_text = resp
190 .text()
191 .await
192 .unwrap_or_else(|_| "<unable to read response body>".to_string());
193 return Err(handle_http_error(status, "POST", endpoint, &body_text));
194 }
195
196 let body_text = resp.text().await.map_err(ClobError::Reqwest)?;
197 serde_json::from_str::<R>(&body_text)
198 .map_err(|e| handle_json_error(endpoint, "POST", &e, &body_text))
199}
200
201pub async fn post(
202 client: &Client,
203 endpoint: &str,
204 options: Option<RequestOptions>,
205) -> Result<Value, ClobError> {
206 post_typed::<Value, Value>(client, endpoint, options).await
207}
208
209pub async fn put_typed<R, B>(
210 client: &Client,
211 endpoint: &str,
212 options: Option<RequestOptions<B>>,
213) -> Result<R, ClobError>
214where
215 R: DeserializeOwned,
216 B: Serialize,
217{
218 let mut req = client.put(endpoint);
219 let mut debug_headers = None;
220 let mut debug_body = None;
221 let mut debug_params = None;
222
223 if let Some(opts) = options {
224 if let Some(h) = opts.headers {
225 if is_debug() {
226 debug_headers = Some(mask_headers(&h));
227 }
228 req = apply_headers(req, &h);
229 }
230 if let Some(body) = opts.data {
231 if is_debug()
232 && let Ok(b) = serde_json::to_string(&body)
233 {
234 debug_body = Some(b);
235 }
236 req = req.json(&body);
237 }
238 if let Some(params) = opts.params {
239 if is_debug() {
240 debug_params = Some(params.clone());
241 }
242 req = req.query(¶ms);
243 }
244 }
245
246 if is_debug() {
247 eprintln!("[HTTP DEBUG] PUT {}", endpoint);
248 if let Some(h) = &debug_headers {
249 eprintln!(" headers={:?}", h);
250 }
251 if let Some(p) = &debug_params {
252 eprintln!(" params={:?}", p);
253 }
254 if let Some(b) = &debug_body {
255 let preview = if b.len() > 800 {
256 format!("{}... ({} bytes)", &b[..800], b.len())
257 } else {
258 b.clone()
259 };
260 eprintln!(" body={}", preview);
261 }
262 }
263
264 let resp = req.send().await.map_err(ClobError::Reqwest)?;
265 let status = resp.status();
266
267 if !status.is_success() {
268 let body_text = resp
269 .text()
270 .await
271 .unwrap_or_else(|_| "<unable to read response body>".to_string());
272 return Err(handle_http_error(status, "PUT", endpoint, &body_text));
273 }
274
275 let body_text = resp.text().await.map_err(ClobError::Reqwest)?;
276 serde_json::from_str::<R>(&body_text)
277 .map_err(|e| handle_json_error(endpoint, "PUT", &e, &body_text))
278}
279
280pub async fn put(
281 client: &Client,
282 endpoint: &str,
283 options: Option<RequestOptions>,
284) -> Result<Value, ClobError> {
285 put_typed::<Value, Value>(client, endpoint, options).await
286}
287
288pub async fn get_typed<R, B>(
289 client: &Client,
290 endpoint: &str,
291 options: Option<RequestOptions<B>>,
292) -> Result<R, ClobError>
293where
294 R: DeserializeOwned,
295 B: Serialize,
296{
297 let mut req = client.get(endpoint);
298 let mut debug_headers = None;
299 let mut debug_params = None;
300
301 if let Some(opts) = options {
302 if let Some(h) = opts.headers {
303 if is_debug() {
304 debug_headers = Some(mask_headers(&h));
305 }
306 req = apply_headers(req, &h);
307 }
308 if let Some(params) = opts.params {
309 if is_debug() {
310 debug_params = Some(params.clone());
311 }
312 req = req.query(¶ms);
313 }
314 }
315
316 if is_debug() {
317 eprintln!("[HTTP DEBUG] GET {}", endpoint);
318 if let Some(h) = &debug_headers {
319 eprintln!(" headers={:?}", h);
320 }
321 if let Some(p) = &debug_params {
322 eprintln!(" params={:?}", p);
323 }
324 }
325
326 let resp = req.send().await.map_err(ClobError::Reqwest)?;
327 let status = resp.status();
328
329 if !status.is_success() {
330 let body_text = resp
331 .text()
332 .await
333 .unwrap_or_else(|_| "<unable to read response body>".to_string());
334 return Err(handle_http_error(status, "GET", endpoint, &body_text));
335 }
336
337 let body_text = resp.text().await.map_err(ClobError::Reqwest)?;
338 serde_json::from_str::<R>(&body_text)
339 .map_err(|e| handle_json_error(endpoint, "GET", &e, &body_text))
340}
341
342pub async fn get(
343 client: &Client,
344 endpoint: &str,
345 options: Option<RequestOptions>,
346) -> Result<Value, ClobError> {
347 get_typed::<Value, Value>(client, endpoint, options).await
348}
349
350pub async fn del_typed<R, B>(
351 client: &Client,
352 endpoint: &str,
353 options: Option<RequestOptions<B>>,
354) -> Result<R, ClobError>
355where
356 R: DeserializeOwned,
357 B: Serialize,
358{
359 let mut req = client.delete(endpoint);
360 let mut debug_headers = None;
361 let mut debug_body = None;
362 let mut debug_params = None;
363
364 if let Some(opts) = options {
365 if let Some(h) = opts.headers {
366 if is_debug() {
367 debug_headers = Some(mask_headers(&h));
368 }
369 req = apply_headers(req, &h);
370 }
371 if let Some(body) = opts.data {
372 if is_debug()
373 && let Ok(b) = serde_json::to_string(&body)
374 {
375 debug_body = Some(b);
376 }
377 req = req.json(&body);
378 }
379 if let Some(params) = opts.params {
380 if is_debug() {
381 debug_params = Some(params.clone());
382 }
383 req = req.query(¶ms);
384 }
385 }
386
387 if is_debug() {
388 eprintln!("[HTTP DEBUG] DELETE {}", endpoint);
389 if let Some(h) = &debug_headers {
390 eprintln!(" headers={:?}", h);
391 }
392 if let Some(p) = &debug_params {
393 eprintln!(" params={:?}", p);
394 }
395 if let Some(b) = &debug_body {
396 let preview = if b.len() > 800 {
397 format!("{}... ({} bytes)", &b[..800], b.len())
398 } else {
399 b.clone()
400 };
401 eprintln!(" body={}", preview);
402 }
403 }
404
405 let resp = req.send().await.map_err(ClobError::Reqwest)?;
406 let status = resp.status();
407
408 if !status.is_success() {
409 let body_text = resp
410 .text()
411 .await
412 .unwrap_or_else(|_| "<unable to read response body>".to_string());
413 return Err(handle_http_error(status, "DELETE", endpoint, &body_text));
414 }
415
416 let body_text = resp.text().await.map_err(ClobError::Reqwest)?;
417 serde_json::from_str::<R>(&body_text)
418 .map_err(|e| handle_json_error(endpoint, "DELETE", &e, &body_text))
419}
420
421pub async fn del(
422 client: &Client,
423 endpoint: &str,
424 options: Option<RequestOptions>,
425) -> Result<Value, ClobError> {
426 del_typed::<Value, Value>(client, endpoint, options).await
427}
428
429pub fn parse_orders_scoring_params(order_ids: Option<&Vec<String>>) -> QueryParams {
430 let mut params = QueryParams::new();
431 if let Some(ids) = order_ids {
432 params.insert("order_ids".to_string(), ids.join(","));
433 }
434 params
435}
436
437pub fn parse_drop_notification_params(ids: Option<&Vec<String>>) -> QueryParams {
438 let mut params = QueryParams::new();
439 if let Some(arr) = ids {
440 params.insert("ids".to_string(), arr.join(","));
441 }
442 params
443}