Skip to main content

polymarket_us/
client.rs

1use crate::auth::UsAuth;
2use crate::error::PolymarketUsError;
3use crate::types;
4use reqwest::Method;
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7
8const DEFAULT_GATEWAY_BASE_URL: &str = "https://gateway.polymarket.us";
9const DEFAULT_API_BASE_URL: &str = "https://api.polymarket.us";
10
11#[derive(Clone)]
12pub struct PolymarketUsClient {
13    http: reqwest::Client,
14    gateway_base_url: String,
15    api_base_url: String,
16    auth: Option<UsAuth>,
17}
18
19pub struct PolymarketUsClientBuilder {
20    gateway_base_url: String,
21    api_base_url: String,
22    auth: Option<UsAuth>,
23    http: Option<reqwest::Client>,
24    timeout: std::time::Duration,
25}
26
27impl Default for PolymarketUsClientBuilder {
28    fn default() -> Self {
29        Self {
30            gateway_base_url: DEFAULT_GATEWAY_BASE_URL.to_string(),
31            api_base_url: DEFAULT_API_BASE_URL.to_string(),
32            auth: None,
33            http: None,
34            timeout: std::time::Duration::from_secs(30),
35        }
36    }
37}
38
39impl PolymarketUsClientBuilder {
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    pub fn gateway_base_url(mut self, url: impl Into<String>) -> Self {
45        self.gateway_base_url = url.into();
46        self
47    }
48
49    pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
50        self.api_base_url = url.into();
51        self
52    }
53
54    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
55        self.timeout = timeout;
56        self
57    }
58
59    pub fn auth(mut self, auth: UsAuth) -> Self {
60        self.auth = Some(auth);
61        self
62    }
63
64    pub fn http_client(mut self, http: reqwest::Client) -> Self {
65        self.http = Some(http);
66        self
67    }
68
69    pub fn build(self) -> Result<PolymarketUsClient, PolymarketUsError> {
70        let http = match self.http {
71            Some(http) => http,
72            None => reqwest::Client::builder().timeout(self.timeout).build()?,
73        };
74        Ok(PolymarketUsClient {
75            http,
76            gateway_base_url: self.gateway_base_url,
77            api_base_url: self.api_base_url,
78            auth: self.auth,
79        })
80    }
81}
82
83impl PolymarketUsClient {
84    pub fn builder() -> PolymarketUsClientBuilder {
85        PolymarketUsClientBuilder::new()
86    }
87
88    pub fn with_reqwest(http: reqwest::Client, auth: Option<UsAuth>) -> Self {
89        Self {
90            http,
91            gateway_base_url: DEFAULT_GATEWAY_BASE_URL.to_string(),
92            api_base_url: DEFAULT_API_BASE_URL.to_string(),
93            auth,
94        }
95    }
96
97    pub fn auth(&self) -> Option<&UsAuth> {
98        self.auth.as_ref()
99    }
100
101    pub fn api_base_url(&self) -> &str {
102        &self.api_base_url
103    }
104
105    pub async fn health(&self) -> Result<types::HealthResponse, PolymarketUsError> {
106        self.request::<(), (), types::HealthResponse>(Method::GET, "/v1/health", None, None, false)
107            .await
108    }
109
110    pub async fn markets_list(&self) -> Result<types::MarketsResponse, PolymarketUsError> {
111        self.markets_list_with_query::<()>(None).await
112    }
113
114    pub async fn markets_list_with_query<Q: Serialize>(
115        &self,
116        query: Option<&Q>,
117    ) -> Result<types::MarketsResponse, PolymarketUsError> {
118        self.request(Method::GET, "/v1/markets", query, None::<&()>, false)
119            .await
120    }
121
122    pub async fn markets_list_authenticated(
123        &self,
124    ) -> Result<types::MarketsResponse, PolymarketUsError> {
125        self.markets_list_authenticated_with_query::<()>(None).await
126    }
127
128    pub async fn markets_list_authenticated_with_query<Q: Serialize>(
129        &self,
130        query: Option<&Q>,
131    ) -> Result<types::MarketsResponse, PolymarketUsError> {
132        self.request(Method::GET, "/v1/markets", query, None::<&()>, true)
133            .await
134    }
135
136    pub async fn account_balances(
137        &self,
138    ) -> Result<types::AccountBalancesResponse, PolymarketUsError> {
139        self.request::<(), (), types::AccountBalancesResponse>(
140            Method::GET,
141            "/v1/account/balances",
142            None,
143            None,
144            true,
145        )
146        .await
147    }
148
149    pub async fn portfolio_positions(
150        &self,
151    ) -> Result<types::PortfolioPositionsResponse, PolymarketUsError> {
152        self.request::<(), (), types::PortfolioPositionsResponse>(
153            Method::GET,
154            "/v1/portfolio/positions",
155            None,
156            None,
157            true,
158        )
159        .await
160    }
161
162    pub async fn portfolio_activities<Q: Serialize>(
163        &self,
164        query: Option<&Q>,
165    ) -> Result<types::PortfolioActivitiesResponse, PolymarketUsError> {
166        self.request(
167            Method::GET,
168            "/v1/portfolio/activities",
169            query,
170            None::<&()>,
171            true,
172        )
173        .await
174    }
175
176    pub async fn place_order(
177        &self,
178        body: &types::PlaceOrderRequest,
179    ) -> Result<types::PlaceOrderResponse, PolymarketUsError> {
180        self.request(
181            Method::POST,
182            "/v1/trading/orders",
183            None::<&()>,
184            Some(body),
185            true,
186        )
187        .await
188    }
189
190    pub async fn place_batched_orders(
191        &self,
192        body: &types::BatchedOrderRequest,
193    ) -> Result<types::BatchedOrderResponse, PolymarketUsError> {
194        self.request(
195            Method::POST,
196            "/v1/orders/batched",
197            None::<&()>,
198            Some(body),
199            true,
200        )
201        .await
202    }
203
204    pub async fn cancel_trading_order(
205        &self,
206        order_id: &str,
207    ) -> Result<types::CancelOrderResponse, PolymarketUsError> {
208        self.request::<(), (), types::CancelOrderResponse>(
209            Method::DELETE,
210            &format!("/v1/trading/orders/{order_id}"),
211            None,
212            None,
213            true,
214        )
215        .await
216    }
217
218    pub async fn orders_create(
219        &self,
220        body: &types::PlaceOrderRequest,
221    ) -> Result<types::PlaceOrderResponse, PolymarketUsError> {
222        self.request(Method::POST, "/v1/orders", None::<&()>, Some(body), true)
223            .await
224    }
225
226    pub async fn orders_open<Q: Serialize>(
227        &self,
228        query: Option<&Q>,
229    ) -> Result<types::GetOpenOrdersResponse, PolymarketUsError> {
230        self.request(Method::GET, "/v1/orders/open", query, None::<&()>, true)
231            .await
232    }
233
234    pub async fn order_retrieve(
235        &self,
236        order_id: &str,
237    ) -> Result<types::PlaceOrderResponse, PolymarketUsError> {
238        self.request::<(), (), types::PlaceOrderResponse>(
239            Method::GET,
240            &format!("/v1/order/{order_id}"),
241            None,
242            None,
243            true,
244        )
245        .await
246    }
247
248    pub async fn order_cancel(
249        &self,
250        order_id: &str,
251        body: &types::CancelOrderParams,
252    ) -> Result<(), PolymarketUsError> {
253        let _: serde_json::Value = self
254            .request(
255                Method::POST,
256                &format!("/v1/order/{order_id}/cancel"),
257                None::<&()>,
258                Some(body),
259                true,
260            )
261            .await?;
262        Ok(())
263    }
264
265    pub async fn order_modify(
266        &self,
267        order_id: &str,
268        body: &types::ModifyOrderRequest,
269    ) -> Result<(), PolymarketUsError> {
270        let _: serde_json::Value = self
271            .request(
272                Method::POST,
273                &format!("/v1/order/{order_id}/modify"),
274                None::<&()>,
275                Some(body),
276                true,
277            )
278            .await?;
279        Ok(())
280    }
281
282    pub async fn orders_cancel_all(
283        &self,
284        body: &types::CancelAllOrdersParams,
285    ) -> Result<types::CancelAllOrdersResponse, PolymarketUsError> {
286        self.request(
287            Method::POST,
288            "/v1/orders/open/cancel",
289            None::<&()>,
290            Some(body),
291            true,
292        )
293        .await
294    }
295
296    pub async fn order_preview(
297        &self,
298        body: &types::PreviewOrderRequest,
299    ) -> Result<types::PreviewOrderResponse, PolymarketUsError> {
300        self.request(
301            Method::POST,
302            "/v1/order/preview",
303            None::<&()>,
304            Some(body),
305            true,
306        )
307        .await
308    }
309
310    pub async fn order_close_position(
311        &self,
312        body: &types::ClosePositionRequest,
313    ) -> Result<types::ClosePositionResponse, PolymarketUsError> {
314        self.request(
315            Method::POST,
316            "/v1/order/close-position",
317            None::<&()>,
318            Some(body),
319            true,
320        )
321        .await
322    }
323
324    async fn request<Q: Serialize, B: Serialize, T: DeserializeOwned>(
325        &self,
326        method: Method,
327        path: &str,
328        query: Option<&Q>,
329        body: Option<&B>,
330        authenticated: bool,
331    ) -> Result<T, PolymarketUsError> {
332        let base = if authenticated {
333            &self.api_base_url
334        } else {
335            &self.gateway_base_url
336        };
337        let url = format!("{}{}", base, path);
338
339        let mut rb = self
340            .http
341            .request(method.clone(), &url)
342            .header("Content-Type", "application/json");
343        if let Some(query) = query {
344            rb = rb.query(query);
345        }
346        if let Some(body) = body {
347            rb = rb.json(body);
348        }
349        if authenticated {
350            let auth = self
351                .auth
352                .as_ref()
353                .ok_or(PolymarketUsError::MissingAuth("authenticated endpoint"))?;
354            for (name, value) in auth.signed_headers(method.as_str(), path) {
355                rb = rb.header(name, value);
356            }
357        }
358
359        let response = rb.send().await?;
360        let status = response.status();
361        let text = response.text().await?;
362
363        if !status.is_success() {
364            let message = extract_error_message(&text).unwrap_or_else(|| text.clone());
365            return Err(PolymarketUsError::from_status(status, message));
366        }
367
368        if text.trim().is_empty() {
369            serde_json::from_str("{}")
370        } else {
371            serde_json::from_str(&text)
372        }
373        .map_err(PolymarketUsError::from)
374    }
375}
376
377fn extract_error_message(text: &str) -> Option<String> {
378    let json: serde_json::Value = serde_json::from_str(text).ok()?;
379    json.get("message")
380        .and_then(|v| v.as_str())
381        .map(ToOwned::to_owned)
382        .or_else(|| {
383            json.get("error")
384                .and_then(|v| v.as_str())
385                .map(ToOwned::to_owned)
386        })
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn builder_defaults_match_public_endpoints() {
395        let client = PolymarketUsClient::builder().build().unwrap();
396        assert_eq!(client.api_base_url(), "https://api.polymarket.us");
397    }
398}