px_exchange_polymarket/
client.rs1use metrics::histogram;
2use reqwest::Client;
3use serde::de::DeserializeOwned;
4use std::time::Instant;
5
6use crate::config::PolymarketConfig;
7use crate::error::PolymarketError;
8
9pub struct HttpClient {
10 client: Client,
11 gamma_url: String,
12 clob_url: String,
13 verbose: bool,
14}
15
16impl HttpClient {
17 pub fn new(config: &PolymarketConfig) -> Result<Self, PolymarketError> {
18 let client = px_core::http::tuned_client_builder()
22 .timeout(config.base.timeout)
23 .build()?;
24
25 Ok(Self {
26 client,
27 gamma_url: config.gamma_url.clone(),
28 clob_url: config.clob_url.clone(),
29 verbose: config.base.verbose,
30 })
31 }
32
33 pub async fn get_gamma<T: DeserializeOwned>(
34 &self,
35 endpoint: &str,
36 ) -> Result<T, PolymarketError> {
37 let url = format!("{}{}", self.gamma_url, endpoint);
38 self.get(&url).await
39 }
40
41 pub async fn get_clob<T: DeserializeOwned>(
42 &self,
43 endpoint: &str,
44 ) -> Result<T, PolymarketError> {
45 let url = format!("{}{}", self.clob_url, endpoint);
46 self.get(&url).await
47 }
48
49 pub async fn get_response(&self, url: &str) -> Result<reqwest::Response, PolymarketError> {
50 if self.verbose {
51 tracing::debug!("GET {}", url);
52 }
53
54 let send_start = Instant::now();
55 let response = self
56 .client
57 .get(url)
58 .send()
59 .await
60 .map_err(|e| PolymarketError::Network(e.to_string()))?;
61 let send_us = send_start.elapsed().as_secs_f64() * 1_000_000.0;
62 histogram!("openpx.exchange.http_send_us", "exchange" => "polymarket").record(send_us);
63
64 Ok(response)
65 }
66
67 async fn get<T: DeserializeOwned>(&self, url: &str) -> Result<T, PolymarketError> {
68 if self.verbose {
69 tracing::debug!("GET {}", url);
70 }
71
72 let send_start = Instant::now();
73 let response = self.client.get(url).send().await?;
74 let send_us = send_start.elapsed().as_secs_f64() * 1_000_000.0;
75 histogram!("openpx.exchange.http_send_us", "exchange" => "polymarket").record(send_us);
76 let status = response.status();
77 let headers = response.headers().clone();
78
79 if status == 429 {
80 let retry_after = headers
81 .get("retry-after")
82 .and_then(|h| h.to_str().ok())
83 .and_then(|s| s.parse().ok())
84 .unwrap_or(1);
85 return Err(PolymarketError::RateLimited { retry_after });
86 }
87
88 let body_start = Instant::now();
89 let body = response.text().await?;
90 let body_us = body_start.elapsed().as_secs_f64() * 1_000_000.0;
91 histogram!("openpx.exchange.http_body_us", "exchange" => "polymarket").record(body_us);
92
93 if !status.is_success() {
94 return Err(PolymarketError::Api(format!("{status}: {body}")));
95 }
96
97 let parse_start = Instant::now();
98 let parsed = serde_json::from_str(&body)
99 .map_err(|e| PolymarketError::InvalidResponse(format!("parse error: {e}")))?;
100 let parse_us = parse_start.elapsed().as_secs_f64() * 1_000_000.0;
101 histogram!("openpx.exchange.json_parse_us", "exchange" => "polymarket").record(parse_us);
102
103 Ok(parsed)
104 }
105}