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}