Skip to main content

polyoxide_gamma/api/
markets.rs

1use polyoxide_core::{ApiError, HttpClient, QueryBuilder, Request, RequestError};
2
3use crate::{
4    error::GammaError,
5    types::{KeysetMarketsResponse, Market, MarketDescription, MarketsInformationBody, Tag},
6};
7
8/// Markets namespace for market-related operations
9#[derive(Clone)]
10pub struct Markets {
11    pub(crate) http_client: HttpClient,
12}
13
14impl Markets {
15    /// Get a specific market by ID
16    pub fn get(&self, id: impl Into<String>) -> GetMarket {
17        GetMarket {
18            request: Request::new(
19                self.http_client.clone(),
20                format!("/markets/{}", urlencoding::encode(&id.into())),
21            ),
22        }
23    }
24
25    /// Get a market by its slug
26    pub fn get_by_slug(&self, slug: impl Into<String>) -> GetMarket {
27        GetMarket {
28            request: Request::new(
29                self.http_client.clone(),
30                format!("/markets/slug/{}", urlencoding::encode(&slug.into())),
31            ),
32        }
33    }
34
35    /// List markets with optional filtering
36    pub fn list(&self) -> ListMarkets {
37        ListMarkets {
38            request: Request::new(self.http_client.clone(), "/markets"),
39        }
40    }
41
42    /// Look up markets by ID, returning both open and closed markets.
43    ///
44    /// Unlike [`Self::list`] with `.id(…)`, which inherits the upstream
45    /// `closed=false` default and silently drops closed markets, `get_many`
46    /// issues two parallel requests (`closed=true` and `closed=false`) and
47    /// merges the results, so callers get every matching market regardless of
48    /// status. This matches the semantics of the single-market [`Self::get`]
49    /// endpoint, batched.
50    ///
51    /// Safe batch size: ≤ 400 IDs per call (same URL-length ceiling as
52    /// [`ListMarkets::id`]). The two fan-out requests are issued concurrently,
53    /// so wall-clock latency is one round-trip, not two.
54    pub fn get_many(&self, ids: impl IntoIterator<Item = i64>) -> GetManyMarkets {
55        GetManyMarkets {
56            http_client: self.http_client.clone(),
57            ids: ids.into_iter().collect(),
58            include_tag: None,
59        }
60    }
61
62    /// Get tags for a market
63    pub fn tags(&self, id: impl Into<String>) -> Request<Vec<Tag>, GammaError> {
64        Request::new(
65            self.http_client.clone(),
66            format!("/markets/{}/tags", urlencoding::encode(&id.into())),
67        )
68    }
69
70    /// Get a market's description text (`GET /markets/{id}/description`).
71    pub fn get_description(&self, id: impl Into<String>) -> Request<MarketDescription, GammaError> {
72        Request::new(
73            self.http_client.clone(),
74            format!("/markets/{}/description", urlencoding::encode(&id.into())),
75        )
76    }
77
78    /// Query markets by an information filter body (`POST /markets/information`).
79    ///
80    /// Unlike [`Self::list`], the filter parameters are passed in the request
81    /// body rather than the query string, allowing larger batches of IDs,
82    /// slugs, token IDs, etc. without hitting URL-length limits.
83    ///
84    /// Pagination (`limit`, `offset`) is sent via query string — the upstream
85    /// server ignores those fields when present in the JSON body. A default
86    /// `limit=1000` (the upstream row ceiling) is applied so callers that
87    /// forget to paginate don't silently truncate at the server-side default
88    /// of 20. Override with [`QueryByInformation::limit`].
89    pub fn query_by_information(&self, body: MarketsInformationBody) -> QueryByInformation {
90        QueryByInformation {
91            http_client: self.http_client.clone(),
92            body,
93            limit: Some(1000),
94            offset: None,
95        }
96    }
97
98    /// Query abridged markets by an information filter body
99    /// (`POST /markets/abridged`). Returns a reduced-payload market list.
100    ///
101    /// Same pagination caveat as [`Self::query_by_information`]: the default
102    /// `limit=1000` is sent on the query string to avoid the silent 20-row
103    /// server default.
104    pub fn query_abridged(&self, body: MarketsInformationBody) -> QueryAbridged {
105        QueryAbridged {
106            http_client: self.http_client.clone(),
107            body,
108            limit: Some(1000),
109            offset: None,
110        }
111    }
112
113    /// List markets using cursor-based (keyset) pagination
114    /// (`GET /markets/keyset`).
115    ///
116    /// Prefer this over [`Self::list`] for stable paging through large result
117    /// sets. Use `next_cursor` from each response as `after_cursor` in the
118    /// next request; pagination is complete when `next_cursor` is `None`.
119    pub fn list_keyset(&self) -> ListKeysetMarkets {
120        ListKeysetMarkets {
121            request: Request::new(self.http_client.clone(), "/markets/keyset"),
122        }
123    }
124}
125
126/// POST a JSON body (with optional query string) and deserialize a JSON
127/// response. Shared by `query_by_information` and `query_abridged`.
128///
129/// `query` carries pagination (`limit`, `offset`) because the upstream server
130/// ignores those fields when sent inside the JSON body — they must be on the
131/// URL query string to take effect.
132async fn post_json<B: serde::Serialize, T: serde::de::DeserializeOwned>(
133    http: &HttpClient,
134    path: &str,
135    body: &B,
136    query: &[(&str, String)],
137) -> Result<T, GammaError> {
138    let url = http
139        .base_url
140        .join(path)
141        .map_err(|e| GammaError::Api(ApiError::from(e)))?;
142
143    http.acquire_rate_limit(path, Some(&reqwest::Method::POST))
144        .await;
145    let _permit = http.acquire_concurrency().await;
146    let response = http
147        .client
148        .post(url)
149        .query(query)
150        .json(body)
151        .send()
152        .await
153        .map_err(|e| GammaError::Api(ApiError::from(e)))?;
154
155    if !response.status().is_success() {
156        return Err(GammaError::from_response(response).await);
157    }
158
159    let text = response
160        .text()
161        .await
162        .map_err(|e| GammaError::Api(ApiError::from(e)))?;
163    serde_json::from_str(&text).map_err(|e| GammaError::Api(ApiError::from(e)))
164}
165
166/// Collect the `limit` / `offset` pair into a query slice, skipping unset
167/// values so the resulting URL contains only explicit pagination params.
168fn pagination_query(limit: Option<u32>, offset: Option<u32>) -> Vec<(&'static str, String)> {
169    let mut query = Vec::new();
170    if let Some(limit) = limit {
171        query.push(("limit", limit.to_string()));
172    }
173    if let Some(offset) = offset {
174        query.push(("offset", offset.to_string()));
175    }
176    query
177}
178
179/// Request builder for [`Markets::query_by_information`].
180///
181/// Posts a [`MarketsInformationBody`] filter to `/markets/information` and
182/// returns the matching markets. Pagination (`limit`, `offset`) is sent on
183/// the URL query string because the server ignores body-level pagination;
184/// the builder defaults `limit=1000` to prevent silent truncation.
185pub struct QueryByInformation {
186    http_client: HttpClient,
187    body: MarketsInformationBody,
188    limit: Option<u32>,
189    offset: Option<u32>,
190}
191
192impl QueryByInformation {
193    /// Override the default `limit=1000`. Upstream caps responses at 1000
194    /// rows; larger values are silently clamped.
195    pub fn limit(mut self, limit: u32) -> Self {
196        self.limit = Some(limit);
197        self
198    }
199
200    /// Paginate past the first response by offsetting into the result set.
201    pub fn offset(mut self, offset: u32) -> Self {
202        self.offset = Some(offset);
203        self
204    }
205
206    /// Execute the request.
207    pub async fn send(self) -> Result<Vec<Market>, GammaError> {
208        let query = pagination_query(self.limit, self.offset);
209        post_json(
210            &self.http_client,
211            "/markets/information",
212            &self.body,
213            &query,
214        )
215        .await
216    }
217}
218
219/// Request builder for [`Markets::query_abridged`].
220///
221/// Posts a [`MarketsInformationBody`] filter to `/markets/abridged` and
222/// returns a reduced-payload market list. Same pagination semantics as
223/// [`QueryByInformation`] — `limit` / `offset` travel on the query string.
224pub struct QueryAbridged {
225    http_client: HttpClient,
226    body: MarketsInformationBody,
227    limit: Option<u32>,
228    offset: Option<u32>,
229}
230
231impl QueryAbridged {
232    /// Override the default `limit=1000`.
233    pub fn limit(mut self, limit: u32) -> Self {
234        self.limit = Some(limit);
235        self
236    }
237
238    /// Paginate past the first response by offsetting into the result set.
239    pub fn offset(mut self, offset: u32) -> Self {
240        self.offset = Some(offset);
241        self
242    }
243
244    /// Execute the request.
245    pub async fn send(self) -> Result<Vec<Market>, GammaError> {
246        let query = pagination_query(self.limit, self.offset);
247        post_json(&self.http_client, "/markets/abridged", &self.body, &query).await
248    }
249}
250
251/// Request builder for getting a single market
252pub struct GetMarket {
253    request: Request<Market, GammaError>,
254}
255
256impl GetMarket {
257    /// Include tag data in response
258    pub fn include_tag(mut self, include: bool) -> Self {
259        self.request = self.request.query("include_tag", include);
260        self
261    }
262
263    /// Execute the request
264    pub async fn send(self) -> Result<Market, GammaError> {
265        self.request.send().await
266    }
267}
268
269/// Request builder for batch lookup of markets by ID.
270///
271/// On [`Self::send`] this issues two parallel `/markets` requests — one with
272/// `closed=true`, one with `closed=false` — and concatenates the results. The
273/// two upstream responses are disjoint (a market is either closed or not), so
274/// merging by concatenation is exact.
275pub struct GetManyMarkets {
276    http_client: HttpClient,
277    ids: Vec<i64>,
278    include_tag: Option<bool>,
279}
280
281impl GetManyMarkets {
282    /// Include tag data in results
283    pub fn include_tag(mut self, include: bool) -> Self {
284        self.include_tag = Some(include);
285        self
286    }
287
288    /// Execute the request.
289    ///
290    /// Returns an empty vec without hitting the network when called with no
291    /// IDs. Otherwise fans out `closed=true` and `closed=false` concurrently
292    /// and fails fast if either leg errors.
293    pub async fn send(self) -> Result<Vec<Market>, GammaError> {
294        if self.ids.is_empty() {
295            return Ok(Vec::new());
296        }
297
298        let mut req_closed: Request<Vec<Market>, GammaError> =
299            Request::new(self.http_client.clone(), "/markets")
300                .query_many("id", self.ids.iter().copied())
301                .query("closed", true);
302        let mut req_open: Request<Vec<Market>, GammaError> =
303            Request::new(self.http_client, "/markets")
304                .query_many("id", self.ids.iter().copied())
305                .query("closed", false);
306
307        if let Some(include) = self.include_tag {
308            req_closed = req_closed.query("include_tag", include);
309            req_open = req_open.query("include_tag", include);
310        }
311
312        let (mut closed_markets, open_markets) =
313            tokio::try_join!(req_closed.send(), req_open.send())?;
314        closed_markets.extend(open_markets);
315        Ok(closed_markets)
316    }
317}
318
319/// Request builder for listing markets
320pub struct ListMarkets {
321    request: Request<Vec<Market>, GammaError>,
322}
323
324impl ListMarkets {
325    /// Set maximum number of results (minimum: 0)
326    pub fn limit(mut self, limit: u32) -> Self {
327        self.request = self.request.query("limit", limit);
328        self
329    }
330
331    /// Set pagination offset (minimum: 0)
332    pub fn offset(mut self, offset: u32) -> Self {
333        self.request = self.request.query("offset", offset);
334        self
335    }
336
337    /// Set order fields (comma-separated list)
338    pub fn order(mut self, order: impl Into<String>) -> Self {
339        self.request = self.request.query("order", order.into());
340        self
341    }
342
343    /// Set sort direction
344    pub fn ascending(mut self, ascending: bool) -> Self {
345        self.request = self.request.query("ascending", ascending);
346        self
347    }
348
349    /// Filter by specific market IDs
350    ///
351    /// Safe batch size: ≤ 400 per request. URLs over ~8 KB are rejected
352    /// upstream with `414 URI Too Long`; empirically the ceiling is ~583.
353    ///
354    /// # Note on closed markets
355    ///
356    /// The upstream `/markets` endpoint applies an implicit `closed=false`
357    /// default when no `closed` param is sent, so this filter silently drops
358    /// closed markets unless `.closed(true)` is also set. For pinpoint lookup
359    /// by ID regardless of status, use [`Markets::get_many`].
360    pub fn id(mut self, ids: impl IntoIterator<Item = i64>) -> Self {
361        self.request = self.request.query_many("id", ids);
362        self
363    }
364
365    /// Filter by market slugs
366    ///
367    /// Safe batch size: ≤ 100 per request. URL length is capped at ~8 KB
368    /// upstream; slug entries vary so pick a cap based on your longest slug.
369    ///
370    /// # Note on closed markets
371    ///
372    /// Same trap as [`Self::id`]: upstream defaults to `closed=false`, so this
373    /// filter drops closed markets unless `.closed(true)` is also set.
374    pub fn slug(mut self, slugs: impl IntoIterator<Item = impl ToString>) -> Self {
375        self.request = self.request.query_many("slug", slugs);
376        self
377    }
378
379    /// Filter by CLOB token IDs
380    ///
381    /// Safe batch size: ≤ 50 per request. Token IDs are 77-digit decimals
382    /// (~90 B/entry on the wire); URLs over ~8 KB are rejected with `414`.
383    pub fn clob_token_ids(mut self, token_ids: impl IntoIterator<Item = impl ToString>) -> Self {
384        self.request = self.request.query_many("clob_token_ids", token_ids);
385        self
386    }
387
388    /// Filter by condition IDs
389    ///
390    /// Safe batch size: ≤ 60 per request. Condition IDs are 66-char hex
391    /// (~80 B/entry); empirically the upstream ceiling is exactly 100 before
392    /// `414 URI Too Long`.
393    ///
394    /// # Note on closed markets
395    ///
396    /// Same trap as [`Self::id`]: upstream defaults to `closed=false`, so this
397    /// filter drops closed markets unless `.closed(true)` is also set.
398    pub fn condition_ids(mut self, condition_ids: impl IntoIterator<Item = impl ToString>) -> Self {
399        self.request = self.request.query_many("condition_ids", condition_ids);
400        self
401    }
402
403    /// Filter by market maker addresses
404    ///
405    /// Safe batch size: ≤ 80 per request. Ethereum addresses are 42 chars
406    /// (~60 B/entry); URLs over ~8 KB are rejected upstream with `414`.
407    pub fn market_maker_address(
408        mut self,
409        addresses: impl IntoIterator<Item = impl ToString>,
410    ) -> Self {
411        self.request = self.request.query_many("market_maker_address", addresses);
412        self
413    }
414
415    /// Set minimum liquidity threshold
416    pub fn liquidity_num_min(mut self, min: f64) -> Self {
417        self.request = self.request.query("liquidity_num_min", min);
418        self
419    }
420
421    /// Set maximum liquidity threshold
422    pub fn liquidity_num_max(mut self, max: f64) -> Self {
423        self.request = self.request.query("liquidity_num_max", max);
424        self
425    }
426
427    /// Set minimum trading volume
428    pub fn volume_num_min(mut self, min: f64) -> Self {
429        self.request = self.request.query("volume_num_min", min);
430        self
431    }
432
433    /// Set maximum trading volume
434    pub fn volume_num_max(mut self, max: f64) -> Self {
435        self.request = self.request.query("volume_num_max", max);
436        self
437    }
438
439    /// Set earliest market start date (ISO 8601 format)
440    pub fn start_date_min(mut self, date: impl Into<String>) -> Self {
441        self.request = self.request.query("start_date_min", date.into());
442        self
443    }
444
445    /// Set latest market start date (ISO 8601 format)
446    pub fn start_date_max(mut self, date: impl Into<String>) -> Self {
447        self.request = self.request.query("start_date_max", date.into());
448        self
449    }
450
451    /// Set earliest market end date (ISO 8601 format)
452    pub fn end_date_min(mut self, date: impl Into<String>) -> Self {
453        self.request = self.request.query("end_date_min", date.into());
454        self
455    }
456
457    /// Set latest market end date (ISO 8601 format)
458    pub fn end_date_max(mut self, date: impl Into<String>) -> Self {
459        self.request = self.request.query("end_date_max", date.into());
460        self
461    }
462
463    /// Filter by tag identifier
464    pub fn tag_id(mut self, tag_id: i64) -> Self {
465        self.request = self.request.query("tag_id", tag_id);
466        self
467    }
468
469    /// Include related tags in response
470    pub fn related_tags(mut self, include: bool) -> Self {
471        self.request = self.request.query("related_tags", include);
472        self
473    }
474
475    /// Filter for create-your-own markets
476    pub fn cyom(mut self, cyom: bool) -> Self {
477        self.request = self.request.query("cyom", cyom);
478        self
479    }
480
481    /// Filter by UMA resolution status
482    pub fn uma_resolution_status(mut self, status: impl Into<String>) -> Self {
483        self.request = self.request.query("uma_resolution_status", status.into());
484        self
485    }
486
487    /// Filter by game identifier
488    pub fn game_id(mut self, game_id: impl Into<String>) -> Self {
489        self.request = self.request.query("game_id", game_id.into());
490        self
491    }
492
493    /// Filter by sports market types
494    ///
495    /// Safe batch size: ≤ 150 per request. URL length is capped at ~8 KB
496    /// upstream (`414 URI Too Long`).
497    pub fn sports_market_types(mut self, types: impl IntoIterator<Item = impl ToString>) -> Self {
498        self.request = self.request.query_many("sports_market_types", types);
499        self
500    }
501
502    /// Set minimum rewards threshold
503    pub fn rewards_min_size(mut self, min: f64) -> Self {
504        self.request = self.request.query("rewards_min_size", min);
505        self
506    }
507
508    /// Filter by question identifiers
509    ///
510    /// Safe batch size: ≤ 60 per request. Question IDs are 66-char hex
511    /// (~80 B/entry); URLs over ~8 KB are rejected upstream with `414`.
512    pub fn question_ids(mut self, question_ids: impl IntoIterator<Item = impl ToString>) -> Self {
513        self.request = self.request.query_many("question_ids", question_ids);
514        self
515    }
516
517    /// Include tag data in results
518    pub fn include_tag(mut self, include: bool) -> Self {
519        self.request = self.request.query("include_tag", include);
520        self
521    }
522
523    /// Filter for closed or active markets
524    pub fn closed(mut self, closed: bool) -> Self {
525        self.request = self.request.query("closed", closed);
526        self
527    }
528
529    /// Filter by open status (convenience method, opposite of closed)
530    pub fn open(mut self, open: bool) -> Self {
531        self.request = self.request.query("closed", !open);
532        self
533    }
534
535    /// Filter by archived status
536    pub fn archived(mut self, archived: bool) -> Self {
537        self.request = self.request.query("archived", archived);
538        self
539    }
540
541    /// Execute the request
542    pub async fn send(self) -> Result<Vec<Market>, GammaError> {
543        self.request.send().await
544    }
545}
546
547/// Request builder for [`Markets::list_keyset`].
548pub struct ListKeysetMarkets {
549    request: Request<KeysetMarketsResponse, GammaError>,
550}
551
552impl ListKeysetMarkets {
553    /// Maximum number of results to return (upstream max 1000).
554    pub fn limit(mut self, limit: u32) -> Self {
555        self.request = self.request.query("limit", limit);
556        self
557    }
558
559    /// Comma-separated list of JSON field names to order by.
560    pub fn order(mut self, order: impl Into<String>) -> Self {
561        self.request = self.request.query("order", order.into());
562        self
563    }
564
565    /// Sort direction (used only when `order` is set).
566    pub fn ascending(mut self, ascending: bool) -> Self {
567        self.request = self.request.query("ascending", ascending);
568        self
569    }
570
571    /// Opaque cursor token from a previous response's `next_cursor`.
572    pub fn after_cursor(mut self, cursor: impl Into<String>) -> Self {
573        self.request = self.request.query("after_cursor", cursor.into());
574        self
575    }
576
577    /// Filter by specific market IDs.
578    pub fn id(mut self, ids: impl IntoIterator<Item = i64>) -> Self {
579        self.request = self.request.query_many("id", ids);
580        self
581    }
582
583    /// Filter by market slugs.
584    pub fn slug(mut self, slugs: impl IntoIterator<Item = impl ToString>) -> Self {
585        self.request = self.request.query_many("slug", slugs);
586        self
587    }
588
589    /// Filter by closed status (defaults to `false` upstream).
590    pub fn closed(mut self, closed: bool) -> Self {
591        self.request = self.request.query("closed", closed);
592        self
593    }
594
595    /// Filter by CLOB token IDs.
596    pub fn clob_token_ids(mut self, ids: impl IntoIterator<Item = impl ToString>) -> Self {
597        self.request = self.request.query_many("clob_token_ids", ids);
598        self
599    }
600
601    /// Filter by condition IDs.
602    pub fn condition_ids(mut self, ids: impl IntoIterator<Item = impl ToString>) -> Self {
603        self.request = self.request.query_many("condition_ids", ids);
604        self
605    }
606
607    /// Filter by question IDs.
608    pub fn question_ids(mut self, ids: impl IntoIterator<Item = impl ToString>) -> Self {
609        self.request = self.request.query_many("question_ids", ids);
610        self
611    }
612
613    /// Filter by market-maker addresses.
614    pub fn market_maker_address(
615        mut self,
616        addresses: impl IntoIterator<Item = impl ToString>,
617    ) -> Self {
618        self.request = self.request.query_many("market_maker_address", addresses);
619        self
620    }
621
622    /// Set minimum liquidity threshold.
623    pub fn liquidity_num_min(mut self, min: f64) -> Self {
624        self.request = self.request.query("liquidity_num_min", min);
625        self
626    }
627
628    /// Set maximum liquidity threshold.
629    pub fn liquidity_num_max(mut self, max: f64) -> Self {
630        self.request = self.request.query("liquidity_num_max", max);
631        self
632    }
633
634    /// Set minimum trading volume.
635    pub fn volume_num_min(mut self, min: f64) -> Self {
636        self.request = self.request.query("volume_num_min", min);
637        self
638    }
639
640    /// Set maximum trading volume.
641    pub fn volume_num_max(mut self, max: f64) -> Self {
642        self.request = self.request.query("volume_num_max", max);
643        self
644    }
645
646    /// Set earliest market start date (ISO 8601 format).
647    pub fn start_date_min(mut self, date: impl Into<String>) -> Self {
648        self.request = self.request.query("start_date_min", date.into());
649        self
650    }
651
652    /// Set latest market start date (ISO 8601 format).
653    pub fn start_date_max(mut self, date: impl Into<String>) -> Self {
654        self.request = self.request.query("start_date_max", date.into());
655        self
656    }
657
658    /// Set earliest market end date (ISO 8601 format).
659    pub fn end_date_min(mut self, date: impl Into<String>) -> Self {
660        self.request = self.request.query("end_date_min", date.into());
661        self
662    }
663
664    /// Set latest market end date (ISO 8601 format).
665    pub fn end_date_max(mut self, date: impl Into<String>) -> Self {
666        self.request = self.request.query("end_date_max", date.into());
667        self
668    }
669
670    /// Filter by tag IDs.
671    pub fn tag_id(mut self, tag_ids: impl IntoIterator<Item = i64>) -> Self {
672        self.request = self.request.query_many("tag_id", tag_ids);
673        self
674    }
675
676    /// Include related tags in response.
677    pub fn related_tags(mut self, include: bool) -> Self {
678        self.request = self.request.query("related_tags", include);
679        self
680    }
681
682    /// Filter create-your-own markets.
683    pub fn cyom(mut self, cyom: bool) -> Self {
684        self.request = self.request.query("cyom", cyom);
685        self
686    }
687
688    /// Filter markets with RFQ enabled.
689    pub fn rfq_enabled(mut self, enabled: bool) -> Self {
690        self.request = self.request.query("rfq_enabled", enabled);
691        self
692    }
693
694    /// Filter by UMA resolution status.
695    pub fn uma_resolution_status(mut self, status: impl Into<String>) -> Self {
696        self.request = self.request.query("uma_resolution_status", status.into());
697        self
698    }
699
700    /// Filter by game identifier.
701    pub fn game_id(mut self, game_id: impl Into<String>) -> Self {
702        self.request = self.request.query("game_id", game_id.into());
703        self
704    }
705
706    /// Filter by sports market types.
707    pub fn sports_market_types(mut self, types: impl IntoIterator<Item = impl ToString>) -> Self {
708        self.request = self.request.query_many("sports_market_types", types);
709        self
710    }
711
712    /// Include tag data in results.
713    pub fn include_tag(mut self, include: bool) -> Self {
714        self.request = self.request.query("include_tag", include);
715        self
716    }
717
718    /// Execute the request.
719    pub async fn send(self) -> Result<KeysetMarketsResponse, GammaError> {
720        self.request.send().await
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use crate::Gamma;
727
728    fn gamma() -> Gamma {
729        Gamma::new().unwrap()
730    }
731
732    /// Verify that all builder methods chain correctly (compile-time type check)
733    /// and produce a valid builder ready to send.
734    #[test]
735    fn test_list_markets_full_chain() {
736        // This test verifies that every builder method returns Self and chains
737        let _list = gamma()
738            .markets()
739            .list()
740            .limit(25)
741            .offset(50)
742            .order("volume")
743            .ascending(false)
744            .id(vec![1i64, 2, 3])
745            .slug(vec!["slug-a"])
746            .clob_token_ids(vec!["token-1"])
747            .condition_ids(vec!["cond-1"])
748            .market_maker_address(vec!["0xaddr"])
749            .liquidity_num_min(1000.0)
750            .liquidity_num_max(50000.0)
751            .volume_num_min(100.0)
752            .volume_num_max(10000.0)
753            .start_date_min("2024-01-01")
754            .start_date_max("2025-01-01")
755            .end_date_min("2024-06-01")
756            .end_date_max("2025-12-31")
757            .tag_id(42)
758            .related_tags(true)
759            .cyom(false)
760            .uma_resolution_status("resolved")
761            .game_id("game-1")
762            .sports_market_types(vec!["moneyline"])
763            .rewards_min_size(10.0)
764            .question_ids(vec!["q1"])
765            .include_tag(true)
766            .closed(false)
767            .archived(false);
768    }
769
770    #[test]
771    fn test_open_and_closed_are_inverse() {
772        // Both should compile and produce a valid builder
773        let _open = gamma().markets().list().open(true);
774        let _closed = gamma().markets().list().closed(false);
775    }
776
777    #[test]
778    fn test_get_market_accepts_string_and_str() {
779        let _req1 = gamma().markets().get("12345");
780        let _req2 = gamma().markets().get(String::from("12345"));
781    }
782
783    #[test]
784    fn test_get_by_slug_accepts_string_and_str() {
785        let _req1 = gamma().markets().get_by_slug("my-slug");
786        let _req2 = gamma().markets().get_by_slug(String::from("my-slug"));
787    }
788
789    #[test]
790    fn test_get_market_with_include_tag() {
791        let _req = gamma().markets().get("12345").include_tag(true);
792    }
793
794    #[test]
795    fn test_market_tags_accepts_str_and_string() {
796        let _req1 = gamma().markets().tags("12345");
797        let _req2 = gamma().markets().tags(String::from("12345"));
798    }
799
800    #[test]
801    fn test_get_many_builds_with_include_tag() {
802        let _req = gamma()
803            .markets()
804            .get_many(vec![1i64, 2, 3])
805            .include_tag(true);
806    }
807
808    // ── new endpoints ───────────────────────────────────────────
809
810    #[test]
811    fn test_get_description_accepts_str_and_string() {
812        let _req1 = gamma().markets().get_description("12345");
813        let _req2 = gamma().markets().get_description(String::from("12345"));
814    }
815
816    #[test]
817    fn test_list_keyset_full_chain() {
818        let _req = gamma()
819            .markets()
820            .list_keyset()
821            .limit(50)
822            .order("volume_num,liquidity_num")
823            .ascending(false)
824            .after_cursor("opaque-cursor")
825            .id(vec![1i64, 2, 3])
826            .slug(vec!["a-slug"])
827            .closed(true)
828            .clob_token_ids(vec!["tok-a"])
829            .condition_ids(vec!["0xcond"])
830            .question_ids(vec!["q1"])
831            .market_maker_address(vec!["0xmm"])
832            .liquidity_num_min(1.0)
833            .liquidity_num_max(10.0)
834            .volume_num_min(1.0)
835            .volume_num_max(10.0)
836            .start_date_min("2024-01-01")
837            .start_date_max("2025-01-01")
838            .end_date_min("2024-01-01")
839            .end_date_max("2026-01-01")
840            .tag_id(vec![1i64, 2])
841            .related_tags(true)
842            .cyom(false)
843            .rfq_enabled(true)
844            .uma_resolution_status("resolved")
845            .game_id("game-1")
846            .sports_market_types(vec!["moneyline"])
847            .include_tag(true);
848    }
849}