use crate::errors::{ClobError, ClobResult};
use reqwest::{Client, Response};
use serde::Serialize;
use std::collections::HashMap;
pub struct HttpClient {
client: Client,
base_url: String,
geo_block_token: Option<String>,
}
impl HttpClient {
pub fn new(base_url: String) -> Self {
Self {
client: Client::new(),
base_url,
geo_block_token: None,
}
}
pub fn with_proxy(base_url: String, proxy_url: &str) -> ClobResult<Self> {
let proxy = reqwest::Proxy::all(proxy_url)
.map_err(|e| ClobError::Other(format!("Invalid proxy URL: {}", e)))?;
let client = Client::builder()
.proxy(proxy)
.build()
.map_err(|e| ClobError::Other(format!("Failed to build client with proxy: {}", e)))?;
Ok(Self {
client,
base_url,
geo_block_token: None,
})
}
pub fn with_geo_block_token(mut self, token: String) -> Self {
self.geo_block_token = Some(token);
self
}
fn add_default_headers(
&self,
method: &str,
headers: Option<HashMap<String, String>>,
) -> HashMap<String, String> {
let mut final_headers = headers.unwrap_or_default();
final_headers
.entry("User-Agent".to_string())
.or_insert_with(|| "@polymarket/clob-client".to_string());
final_headers
.entry("Accept".to_string())
.or_insert_with(|| "*/*".to_string());
final_headers
.entry("Connection".to_string())
.or_insert_with(|| "keep-alive".to_string());
final_headers
.entry("Content-Type".to_string())
.or_insert_with(|| "application/json".to_string());
if method == "GET" {
final_headers
.entry("Accept-Encoding".to_string())
.or_insert_with(|| "gzip".to_string());
}
final_headers
}
pub async fn get<T>(
&self,
endpoint: &str,
headers: Option<HashMap<String, String>>,
params: Option<HashMap<String, String>>,
) -> ClobResult<T>
where
T: serde::de::DeserializeOwned,
{
let url = format!("{}{}", self.base_url, endpoint);
let mut request = self.client.get(&url);
let final_headers = self.add_default_headers("GET", headers);
for (key, value) in final_headers {
request = request.header(key, value);
}
let mut query_params = params.unwrap_or_default();
if let Some(token) = &self.geo_block_token {
query_params.insert("geo_block_token".to_string(), token.clone());
}
if !query_params.is_empty() {
request = request.query(&query_params);
}
let response = request.send().await?;
self.handle_response(response).await
}
pub async fn post<T, B>(
&self,
endpoint: &str,
headers: Option<HashMap<String, String>>,
body: Option<B>,
params: Option<HashMap<String, String>>,
) -> ClobResult<T>
where
T: serde::de::DeserializeOwned,
B: Serialize,
{
let url = format!("{}{}", self.base_url, endpoint);
let mut request = self.client.post(&url);
let final_headers = self.add_default_headers("POST", headers);
for (key, value) in final_headers {
request = request.header(key, value);
}
if let Some(body_data) = body {
request = request.json(&body_data);
}
let mut query_params = params.unwrap_or_default();
if let Some(token) = &self.geo_block_token {
query_params.insert("geo_block_token".to_string(), token.clone());
}
if !query_params.is_empty() {
request = request.query(&query_params);
}
let response = request.send().await?;
self.handle_response(response).await
}
pub async fn delete<T, B>(
&self,
endpoint: &str,
headers: Option<HashMap<String, String>>,
body: Option<B>,
params: Option<HashMap<String, String>>,
) -> ClobResult<T>
where
T: serde::de::DeserializeOwned,
B: Serialize,
{
let url = format!("{}{}", self.base_url, endpoint);
let mut request = self.client.delete(&url);
let final_headers = self.add_default_headers("DELETE", headers);
for (key, value) in final_headers {
request = request.header(key, value);
}
if let Some(body_data) = body {
request = request.json(&body_data);
}
let mut query_params = params.unwrap_or_default();
if let Some(token) = &self.geo_block_token {
query_params.insert("geo_block_token".to_string(), token.clone());
}
if !query_params.is_empty() {
request = request.query(&query_params);
}
let response = request.send().await?;
self.handle_response(response).await
}
async fn handle_response<T>(&self, response: Response) -> ClobResult<T>
where
T: serde::de::DeserializeOwned,
{
let status = response.status();
let url = response.url().clone();
if status.is_success() {
let data = response.json::<T>().await.map_err(|e| {
let error_msg = format!("Failed to parse JSON response: {}", e);
eprintln!("[CLOB Client] request error: {}", error_msg);
ClobError::Other(error_msg)
})?;
Ok(data)
} else {
let status_code = status.as_u16();
let status_text = status.canonical_reason().unwrap_or("Unknown");
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
eprintln!(
"[CLOB Client] request error: {{\"status\": {}, \"statusText\": \"{}\", \"data\": \"{}\", \"url\": \"{}\"}}",
status_code, status_text, error_text, url
);
Err(ClobError::ApiError {
message: error_text,
status: status_code,
})
}
}
}