Skip to main content

polymarket_us/
client.rs

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