1use crate::environment::Environment;
2use crate::error::{
3 unexpected_response, user_input, FetchMarketError, FetchMidpointError, FetchOrderBookError,
4 ListEventsError, ListMarketsError, RateLimitError, TransportError, UserInputError,
5};
6use crate::http::ServiceClient;
7use crate::pagination::{ListEventsPaginator, ListMarketsPaginator, Page, Paginator};
8use crate::params::{as_reqwest_pairs, events_query, markets_query};
9use polymarket_bindings::clob::{MidpointResponse, OrderBook};
10use polymarket_bindings::gamma::{
11 Event, GammaMarket, ListEventsKeysetRaw, ListEventsKeysetResponse, ListMarketsKeysetRaw,
12 ListMarketsKeysetResponse, Market,
13};
14use polymarket_types::{MarketId, PaginationCursor, TokenId};
15
16#[derive(Clone)]
18pub struct PublicClient {
19 environment: Environment,
20 gamma: ServiceClient,
21 clob: ServiceClient,
22 #[cfg(feature = "account")]
23 pub(crate) data: polymarket_client_sdk_v2::data::Client,
24 #[cfg(feature = "websockets")]
25 pub(crate) ws: std::sync::Arc<crate::subscriptions::WebSocketClients>,
26}
27
28#[derive(Clone, Debug, Default)]
30pub struct PublicClientBuilder {
31 environment: Option<Environment>,
32}
33
34impl PublicClientBuilder {
35 #[must_use]
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 #[must_use]
41 pub fn environment(mut self, environment: Environment) -> Self {
42 self.environment = Some(environment);
43 self
44 }
45
46 pub fn build(self) -> Result<PublicClient, TransportError> {
47 PublicClient::with_environment(self.environment.unwrap_or_else(Environment::production))
48 }
49}
50
51impl PublicClient {
52 pub fn new(environment: Environment) -> Self {
53 Self::with_environment(environment).expect("failed to construct HTTP clients")
54 }
55
56 pub fn builder() -> PublicClientBuilder {
57 PublicClientBuilder::new()
58 }
59
60 pub fn with_environment(environment: Environment) -> Result<Self, TransportError> {
61 Ok(Self {
62 gamma: ServiceClient::new(environment.gamma)?,
63 clob: ServiceClient::new(environment.clob)?,
64 #[cfg(feature = "account")]
65 data: polymarket_client_sdk_v2::data::Client::new(environment.data)
66 .map_err(|e| TransportError(e.to_string()))?,
67 #[cfg(feature = "websockets")]
68 ws: std::sync::Arc::new(
69 crate::subscriptions::WebSocketClients::new(&environment)
70 .map_err(|e| TransportError(e.to_string()))?,
71 ),
72 environment,
73 })
74 }
75
76 #[must_use]
77 pub fn environment(&self) -> &Environment {
78 &self.environment
79 }
80
81 pub fn list_markets(
82 &self,
83 request: ListMarketsRequest,
84 ) -> Result<ListMarketsPaginator, UserInputError> {
85 validate_page_size(request.page_size)?;
86
87 let gamma = self.gamma.clone();
88 let base_request = request.clone();
89 let initial_cursor = request.cursor;
90
91 Ok(Paginator::new(
92 move |cursor| {
93 let gamma = gamma.clone();
94 let mut req = base_request.clone();
95 req.cursor = cursor;
96 Box::pin(async move { fetch_markets_page(&gamma, &req).await })
97 },
98 initial_cursor,
99 ))
100 }
101
102 pub async fn fetch_market(
103 &self,
104 request: FetchMarketRequest,
105 ) -> Result<Market, FetchMarketError> {
106 let path = match request {
107 FetchMarketRequest::Id { id } => {
108 let id = MarketId::parse(id)
109 .map_err(|e| FetchMarketError::from(user_input(e.message)))?;
110 format!("/markets/{id}")
111 }
112 FetchMarketRequest::Slug { slug } => {
113 if slug.trim().is_empty() {
114 return Err(FetchMarketError::from(user_input("slug cannot be empty")));
115 }
116 format!("/markets/slug/{slug}")
117 }
118 FetchMarketRequest::Url { url } => {
119 let slug = parse_polymarket_slug(&url, "market")
120 .map_err(|e| FetchMarketError::from(user_input(e)))?;
121 format!("/markets/slug/{slug}")
122 }
123 };
124
125 let response = self
126 .gamma
127 .get(&path, &[])
128 .await
129 .map_err(FetchMarketError::from)?;
130 let response = ServiceClient::ensure_success(response)
131 .await
132 .map_err(FetchMarketError::from)?;
133 let raw: GammaMarket = ServiceClient::json(response)
134 .await
135 .map_err(FetchMarketError::from)?;
136
137 polymarket_bindings::gamma::try_normalize_market(raw)
138 .map_err(|e| FetchMarketError::from(unexpected_response(e)))
139 }
140
141 pub fn list_events(
142 &self,
143 request: ListEventsRequest,
144 ) -> Result<ListEventsPaginator, UserInputError> {
145 validate_page_size(request.page_size)?;
146
147 let gamma = self.gamma.clone();
148 let base_request = request.clone();
149 let initial_cursor = request.cursor;
150
151 Ok(Paginator::new(
152 move |cursor| {
153 let gamma = gamma.clone();
154 let mut req = base_request.clone();
155 req.cursor = cursor;
156 Box::pin(async move { fetch_events_page(&gamma, &req).await })
157 },
158 initial_cursor,
159 ))
160 }
161
162 pub async fn fetch_midpoint(
163 &self,
164 request: FetchMidpointRequest,
165 ) -> Result<String, FetchMidpointError> {
166 let token_id = TokenId::parse(request.token_id)
167 .map_err(|e| FetchMidpointError::from(user_input(e.message)))?;
168
169 let query = vec![("token_id", token_id.as_str().to_string())];
170 let response = self
171 .clob
172 .get("/midpoint", &query)
173 .await
174 .map_err(FetchMidpointError::from)?;
175 let response = ServiceClient::ensure_success(response)
176 .await
177 .map_err(FetchMidpointError::from)?;
178 let parsed: MidpointResponse = ServiceClient::json(response)
179 .await
180 .map_err(FetchMidpointError::from)?;
181
182 parsed
183 .into_mid()
184 .map_err(|e| FetchMidpointError::from(unexpected_response(e)))
185 }
186
187 pub async fn fetch_order_book(
188 &self,
189 request: FetchOrderBookRequest,
190 ) -> Result<OrderBook, FetchOrderBookError> {
191 let token_id = TokenId::parse(request.token_id)
192 .map_err(|e| FetchOrderBookError::from(user_input(e.message)))?;
193
194 let query = vec![("token_id", token_id.as_str().to_string())];
195 let response = self
196 .clob
197 .get("/book", &query)
198 .await
199 .map_err(FetchOrderBookError::from)?;
200 let response = ServiceClient::ensure_success(response)
201 .await
202 .map_err(FetchOrderBookError::from)?;
203 ServiceClient::json(response)
204 .await
205 .map_err(FetchOrderBookError::from)
206 }
207}
208
209#[derive(Clone, Debug, Default)]
210pub struct ListMarketsRequest {
211 pub closed: Option<bool>,
212 pub page_size: Option<u32>,
213 pub cursor: Option<PaginationCursor>,
214 pub order: Option<String>,
215 pub ascending: Option<bool>,
216 pub slug: Option<Vec<String>>,
217 pub tag_id: Option<i64>,
218}
219
220#[derive(Clone, Debug, Default)]
221pub struct ListEventsRequest {
222 pub closed: Option<bool>,
223 pub page_size: Option<u32>,
224 pub cursor: Option<PaginationCursor>,
225 pub order: Option<String>,
226 pub ascending: Option<bool>,
227 pub featured: Option<bool>,
228}
229
230pub enum FetchMarketRequest {
231 Id { id: String },
232 Slug { slug: String },
233 Url { url: String },
234}
235
236pub struct FetchMidpointRequest {
237 pub token_id: String,
238}
239
240pub struct FetchOrderBookRequest {
241 pub token_id: String,
242}
243
244async fn fetch_markets_page(
245 gamma: &ServiceClient,
246 request: &ListMarketsRequest,
247) -> Result<Page<Vec<Market>>, ListMarketsError> {
248 let query = markets_query(request);
249 let pairs = as_reqwest_pairs(&query);
250 let response = gamma.get("/markets/keyset", &pairs).await?;
251 if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
252 return Err(ListMarketsError::from(RateLimitError));
253 }
254 let response = ServiceClient::ensure_success(response).await?;
255 let raw: ListMarketsKeysetRaw = ServiceClient::json(response).await?;
256 let parsed = ListMarketsKeysetResponse::from_raw(raw);
257
258 Ok(Page {
259 has_more: parsed.next_cursor.is_some(),
260 next_cursor: parsed.next_cursor,
261 items: parsed.items,
262 })
263}
264
265async fn fetch_events_page(
266 gamma: &ServiceClient,
267 request: &ListEventsRequest,
268) -> Result<Page<Vec<Event>>, ListEventsError> {
269 let query = events_query(request);
270 let pairs = as_reqwest_pairs(&query);
271 let response = gamma.get("/events/keyset", &pairs).await?;
272 if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
273 return Err(ListEventsError::from(RateLimitError));
274 }
275 let response = ServiceClient::ensure_success(response).await?;
276 let raw: ListEventsKeysetRaw = ServiceClient::json(response).await?;
277 let parsed = ListEventsKeysetResponse::from_raw(raw);
278
279 Ok(Page {
280 has_more: parsed.next_cursor.is_some(),
281 next_cursor: parsed.next_cursor,
282 items: parsed.items,
283 })
284}
285
286fn validate_page_size(page_size: Option<u32>) -> Result<(), UserInputError> {
287 if let Some(size) = page_size {
288 if size == 0 {
289 return Err(user_input("page_size must be positive"));
290 }
291 }
292 Ok(())
293}
294
295fn parse_polymarket_slug(url: &str, kind: &str) -> Result<String, String> {
296 let parsed = url::Url::parse(url).map_err(|e| format!("invalid url: {e}"))?;
297 let segments: Vec<_> = parsed
298 .path_segments()
299 .ok_or_else(|| "url has no path".to_string())?
300 .filter(|s| !s.is_empty())
301 .collect();
302
303 let pos = segments
304 .iter()
305 .position(|s| *s == kind)
306 .ok_or_else(|| format!("url does not contain /{kind}/ segment"))?;
307 let slug = segments
308 .get(pos + 1)
309 .ok_or_else(|| format!("missing slug after /{kind}/"))?;
310 Ok((*slug).to_string())
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn parses_market_url() {
319 let slug = parse_polymarket_slug(
320 "https://polymarket.com/market/eth-flipped-in-2026",
321 "market",
322 )
323 .unwrap();
324 assert_eq!(slug, "eth-flipped-in-2026");
325 }
326}