1use alloy::signers::Signer;
27use std::collections::HashMap;
28use std::marker::PhantomData;
29
30use crate::auth::{l1, l2};
31use crate::constants::{CLOB_API_URL, GAMMA_API_URL, POLYGON_CHAIN_ID};
32use crate::error::{PolymarketError, Result};
33use crate::http::HttpClient;
34use crate::signing::order_builder;
35use crate::types::*;
36
37#[derive(Debug, Clone, Copy)]
43pub struct Public;
44
45#[derive(Debug, Clone, Copy)]
47pub struct Authenticated;
48
49#[derive(Debug)]
59pub struct PolymarketClient<A = Public> {
60 clob: HttpClient,
62 gamma: HttpClient,
64 chain_id: u64,
66 creds: Option<ApiCredentials>,
68 funder: Option<alloy::primitives::Address>,
70 signature_type: SignatureType,
72 _auth: PhantomData<A>,
74}
75
76impl PolymarketClient<Public> {
81 pub fn new_public(host: Option<&str>) -> Result<Self> {
85 let base = host.unwrap_or(CLOB_API_URL);
86 Ok(Self {
87 clob: HttpClient::new(base)?,
88 gamma: HttpClient::new(GAMMA_API_URL)?,
89 chain_id: POLYGON_CHAIN_ID,
90 creds: None,
91 funder: None,
92 signature_type: SignatureType::Eoa,
93 _auth: PhantomData,
94 })
95 }
96
97 pub fn authenticate(self, creds: ApiCredentials) -> PolymarketClient<Authenticated> {
101 PolymarketClient {
102 clob: self.clob,
103 gamma: self.gamma,
104 chain_id: self.chain_id,
105 creds: Some(creds),
106 funder: self.funder,
107 signature_type: self.signature_type,
108 _auth: PhantomData,
109 }
110 }
111}
112
113impl PolymarketClient<Authenticated> {
114 pub fn with_l2(
116 host: Option<&str>,
117 chain_id: u64,
118 creds: ApiCredentials,
119 ) -> Result<Self> {
120 let base = host.unwrap_or(CLOB_API_URL);
121 Ok(Self {
122 clob: HttpClient::new(base)?,
123 gamma: HttpClient::new(GAMMA_API_URL)?,
124 chain_id,
125 creds: Some(creds),
126 funder: None,
127 signature_type: SignatureType::Eoa,
128 _auth: PhantomData,
129 })
130 }
131}
132
133#[derive(Debug)]
135pub struct PolymarketClientBuilder {
136 host: Option<String>,
137 gamma_host: Option<String>,
138 chain_id: u64,
139 creds: Option<ApiCredentials>,
140 funder: Option<alloy::primitives::Address>,
141 signature_type: SignatureType,
142}
143
144impl Default for PolymarketClientBuilder {
145 fn default() -> Self {
146 Self {
147 host: None,
148 gamma_host: None,
149 chain_id: POLYGON_CHAIN_ID,
150 creds: None,
151 funder: None,
152 signature_type: SignatureType::Eoa,
153 }
154 }
155}
156
157impl PolymarketClientBuilder {
158 pub fn host(mut self, host: impl Into<String>) -> Self {
160 self.host = Some(host.into());
161 self
162 }
163
164 pub fn gamma_host(mut self, host: impl Into<String>) -> Self {
166 self.gamma_host = Some(host.into());
167 self
168 }
169
170 pub fn chain_id(mut self, chain_id: u64) -> Self {
172 self.chain_id = chain_id;
173 self
174 }
175
176 pub fn credentials(mut self, creds: ApiCredentials) -> Self {
178 self.creds = Some(creds);
179 self
180 }
181
182 pub fn funder(mut self, funder: alloy::primitives::Address) -> Self {
184 self.funder = Some(funder);
185 self
186 }
187
188 pub fn signature_type(mut self, sig_type: SignatureType) -> Self {
190 self.signature_type = sig_type;
191 self
192 }
193
194 pub fn build_public(self) -> Result<PolymarketClient<Public>> {
196 let clob_url = self.host.as_deref().unwrap_or(CLOB_API_URL);
197 let gamma_url = self.gamma_host.as_deref().unwrap_or(GAMMA_API_URL);
198
199 Ok(PolymarketClient {
200 clob: HttpClient::new(clob_url)?,
201 gamma: HttpClient::new(gamma_url)?,
202 chain_id: self.chain_id,
203 creds: None,
204 funder: self.funder,
205 signature_type: self.signature_type,
206 _auth: PhantomData,
207 })
208 }
209
210 pub fn build(self) -> Result<PolymarketClient<Authenticated>> {
212 let creds = self.creds.ok_or_else(|| {
213 PolymarketError::Auth("Credentials required when building authenticated client. Use build_public() for public-only.".into())
214 })?;
215 let clob_url = self.host.as_deref().unwrap_or(CLOB_API_URL);
216 let gamma_url = self.gamma_host.as_deref().unwrap_or(GAMMA_API_URL);
217
218 Ok(PolymarketClient {
219 clob: HttpClient::new(clob_url)?,
220 gamma: HttpClient::new(gamma_url)?,
221 chain_id: self.chain_id,
222 creds: Some(creds),
223 funder: self.funder,
224 signature_type: self.signature_type,
225 _auth: PhantomData,
226 })
227 }
228}
229
230impl<A> PolymarketClient<A> {
235 pub fn builder() -> PolymarketClientBuilder {
237 PolymarketClientBuilder::default()
238 }
239
240 pub fn chain_id(&self) -> u64 {
242 self.chain_id
243 }
244
245 pub fn has_credentials(&self) -> bool {
247 self.creds.is_some()
248 }
249
250 pub fn set_funder(&mut self, funder: alloy::primitives::Address) {
252 self.funder = Some(funder);
253 }
254
255 pub fn set_signature_type(&mut self, sig_type: SignatureType) {
257 self.signature_type = sig_type;
258 }
259
260 #[cfg(feature = "clob")]
266 pub async fn get_sampling_simplified_markets(
267 &self,
268 next_cursor: Option<&str>,
269 ) -> Result<Vec<SimplifiedMarket>> {
270 let mut query = vec![];
271 if let Some(cursor) = next_cursor {
272 query.push(("next_cursor", cursor));
273 }
274 let q = if query.is_empty() { None } else { Some(query.as_slice()) };
275 self.clob.get("/sampling-simplified-markets", q, None).await
276 }
277
278 #[cfg(feature = "clob")]
280 pub async fn get_simplified_markets(
281 &self,
282 next_cursor: Option<&str>,
283 ) -> Result<Vec<SimplifiedMarket>> {
284 let mut query = vec![];
285 if let Some(cursor) = next_cursor {
286 query.push(("next_cursor", cursor));
287 }
288 let q = if query.is_empty() { None } else { Some(query.as_slice()) };
289 self.clob.get("/simplified-markets", q, None).await
290 }
291
292 #[cfg(feature = "clob")]
294 pub async fn get_markets(
295 &self,
296 next_cursor: Option<&str>,
297 ) -> Result<Vec<SimplifiedMarket>> {
298 let mut query = vec![];
299 if let Some(cursor) = next_cursor {
300 query.push(("next_cursor", cursor));
301 }
302 let q = if query.is_empty() { None } else { Some(query.as_slice()) };
303 self.clob.get("/markets", q, None).await
304 }
305
306 #[cfg(feature = "clob")]
308 pub async fn get_market(&self, condition_id: &str) -> Result<SimplifiedMarket> {
309 let path = format!("/markets/{}", condition_id);
310 self.clob.get(&path, None, None).await
311 }
312
313 #[cfg(feature = "clob")]
315 pub async fn get_order_book(&self, token_id: &str) -> Result<OrderBook> {
316 let query = [("token_id", token_id)];
317 self.clob.get("/book", Some(&query), None).await
318 }
319
320 #[cfg(feature = "clob")]
322 pub async fn get_midpoint(&self, token_id: &str) -> Result<PriceResponse> {
323 let query = [("token_id", token_id)];
324 self.clob.get("/midpoint", Some(&query), None).await
325 }
326
327 #[cfg(feature = "clob")]
329 pub async fn get_price(&self, token_id: &str) -> Result<PriceResponse> {
330 let query = [("token_id", token_id)];
331 self.clob.get("/price", Some(&query), None).await
332 }
333
334 #[cfg(feature = "clob")]
336 pub async fn get_spread(&self, token_id: &str) -> Result<SpreadResponse> {
337 let query = [("token_id", token_id)];
338 self.clob.get("/spread", Some(&query), None).await
339 }
340
341 #[cfg(feature = "clob")]
343 pub async fn get_last_trade_price(&self, token_id: &str) -> Result<LastTradePriceResponse> {
344 let query = [("token_id", token_id)];
345 self.clob.get("/last-trade-price", Some(&query), None).await
346 }
347
348 #[cfg(feature = "clob")]
350 pub async fn get_prices_history(
351 &self,
352 token_id: &str,
353 start_ts: Option<i64>,
354 end_ts: Option<i64>,
355 fidelity: Option<u32>,
356 ) -> Result<Vec<PriceHistoryEntry>> {
357 let mut query: Vec<(&str, String)> = vec![("token_id", token_id.to_string())];
358 if let Some(s) = start_ts {
359 query.push(("startTs", s.to_string()));
360 }
361 if let Some(e) = end_ts {
362 query.push(("endTs", e.to_string()));
363 }
364 if let Some(f) = fidelity {
365 query.push(("fidelity", f.to_string()));
366 }
367 let pairs: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
368 self.clob.get("/prices-history", Some(&pairs), None).await
369 }
370
371 #[cfg(feature = "clob")]
373 pub async fn get_tick_size(&self, token_id: &str) -> Result<TickSizeInfo> {
374 let query = [("token_id", token_id)];
375 self.clob.get("/tick-size", Some(&query), None).await
376 }
377
378 #[cfg(feature = "gamma")]
384 pub async fn get_gamma_markets(
385 &self,
386 limit: Option<u32>,
387 offset: Option<u32>,
388 ) -> Result<Vec<GammaMarket>> {
389 let mut query: Vec<(&str, String)> = vec![];
390 if let Some(l) = limit {
391 query.push(("limit", l.to_string()));
392 }
393 if let Some(o) = offset {
394 query.push(("offset", o.to_string()));
395 }
396 let pairs: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
397 let q = if pairs.is_empty() { None } else { Some(pairs.as_slice()) };
398 self.gamma.get("/markets", q, None).await
399 }
400
401 #[cfg(feature = "gamma")]
403 pub async fn get_gamma_market_by_slug(&self, slug: &str) -> Result<Vec<GammaMarket>> {
404 let query = [("slug", slug)];
405 self.gamma.get("/markets", Some(&query), None).await
406 }
407
408 #[cfg(feature = "gamma")]
410 pub async fn get_events(
411 &self,
412 limit: Option<u32>,
413 offset: Option<u32>,
414 ) -> Result<Vec<GammaEvent>> {
415 let mut query: Vec<(&str, String)> = vec![];
416 if let Some(l) = limit {
417 query.push(("limit", l.to_string()));
418 }
419 if let Some(o) = offset {
420 query.push(("offset", o.to_string()));
421 }
422 let pairs: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
423 let q = if pairs.is_empty() { None } else { Some(pairs.as_slice()) };
424 self.gamma.get("/events", q, None).await
425 }
426
427 #[cfg(feature = "gamma")]
429 pub async fn get_event(&self, event_id: &str) -> Result<GammaEvent> {
430 let path = format!("/events/{}", event_id);
431 self.gamma.get(&path, None, None).await
432 }
433
434 #[cfg(feature = "gamma")]
436 pub async fn search_markets(&self, query: &str) -> Result<Vec<GammaMarket>> {
437 let q = [("_q", query)];
438 self.gamma.get("/markets", Some(&q), None).await
439 }
440
441 pub async fn create_or_derive_api_key<S: Signer + Send + Sync>(
447 &self,
448 signer: &S,
449 nonce: Option<u64>,
450 ) -> Result<ApiKeyResponse> {
451 let headers = l1::create_l1_headers(signer, self.chain_id, nonce).await?;
452 self.clob.get("/auth/api-key", None, Some(&headers)).await
453 }
454
455 pub async fn derive_api_key<S: Signer + Send + Sync>(
457 &self,
458 signer: &S,
459 ) -> Result<ApiKeyResponse> {
460 let headers = l1::create_l1_headers(signer, self.chain_id, None).await?;
461 self.clob.get("/auth/derive-api-key", None, Some(&headers)).await
462 }
463
464 pub async fn delete_api_key<S: Signer + Send + Sync>(
466 &self,
467 signer: &S,
468 ) -> Result<serde_json::Value> {
469 let headers = l1::create_l1_headers(signer, self.chain_id, None).await?;
470 self.clob.delete("/auth/api-key", None, Some(&headers)).await
471 }
472}
473
474impl PolymarketClient<Authenticated> {
479 fn l2_headers(&self, method: &str, path: &str, body: &str) -> Result<HashMap<String, String>> {
482 let creds = self.creds.as_ref().expect("Authenticated client must have creds");
483 l2::create_l2_headers(creds, method, path, body)
484 }
485
486 pub async fn get_orders(&self) -> Result<Vec<OpenOrder>> {
492 let headers = self.l2_headers("GET", "/orders", "")?;
493 self.clob.get("/orders", None, Some(&headers)).await
494 }
495
496 pub async fn get_order(&self, order_id: &str) -> Result<OpenOrder> {
498 let path = format!("/orders/{}", order_id);
499 let headers = self.l2_headers("GET", &path, "")?;
500 self.clob.get(&path, None, Some(&headers)).await
501 }
502
503 pub async fn post_order(&self, signed_order: &SignedOrder) -> Result<OrderResponse> {
505 let body = serde_json::to_value(signed_order)?;
506 let body_str = serde_json::to_string(signed_order)?;
507 let headers = self.l2_headers("POST", "/order", &body_str)?;
508 self.clob.post("/order", &body, Some(&headers)).await
509 }
510
511 pub async fn create_and_post_order<S: Signer + Send + Sync>(
515 &self,
516 signer: &S,
517 params: &TradeParams,
518 ) -> Result<OrderResponse> {
519 let signed = order_builder::build_and_sign_order(
520 signer,
521 params,
522 self.chain_id,
523 self.signature_type,
524 self.funder,
525 )
526 .await?;
527
528 self.post_order(&signed).await
529 }
530
531 pub async fn cancel_order(&self, order_id: &str) -> Result<CancelResponse> {
533 let body = serde_json::json!({ "orderID": order_id });
534 let body_str = serde_json::to_string(&body)?;
535 let headers = self.l2_headers("DELETE", "/order", &body_str)?;
536 self.clob.delete("/order", Some(&body), Some(&headers)).await
537 }
538
539 pub async fn cancel_orders(&self, order_ids: &[&str]) -> Result<CancelResponse> {
541 let body = serde_json::json!({ "orderIDs": order_ids });
542 let body_str = serde_json::to_string(&body)?;
543 let headers = self.l2_headers("DELETE", "/orders", &body_str)?;
544 self.clob.delete("/orders", Some(&body), Some(&headers)).await
545 }
546
547 pub async fn cancel_all_orders(&self) -> Result<CancelResponse> {
549 let headers = self.l2_headers("DELETE", "/cancel-all", "")?;
550 self.clob.delete("/cancel-all", None, Some(&headers)).await
551 }
552
553 #[cfg(feature = "data")]
559 pub async fn get_trades(&self) -> Result<Vec<TradeRecord>> {
560 let headers = self.l2_headers("GET", "/trades", "")?;
561 self.clob.get("/trades", None, Some(&headers)).await
562 }
563
564 #[cfg(feature = "data")]
566 pub async fn get_balance_allowances(
567 &self,
568 asset_type: Option<&str>,
569 ) -> Result<serde_json::Value> {
570 let mut query = vec![];
571 if let Some(at) = asset_type {
572 query.push(("asset_type", at));
573 }
574 let q = if query.is_empty() { None } else { Some(query.as_slice()) };
575 let headers = self.l2_headers("GET", "/balance-allowance", "")?;
576 self.clob.get("/balance-allowance", q, Some(&headers)).await
577 }
578
579 #[cfg(feature = "data")]
581 pub async fn get_positions(&self) -> Result<Vec<Position>> {
582 let headers = self.l2_headers("GET", "/positions", "")?;
583 self.clob.get("/positions", None, Some(&headers)).await
584 }
585
586 #[cfg(feature = "data")]
588 pub async fn get_notifications(&self) -> Result<serde_json::Value> {
589 let headers = self.l2_headers("GET", "/notifications", "")?;
590 self.clob.get("/notifications", None, Some(&headers)).await
591 }
592}