Skip to main content

polyoxide_data/api/
combos.rs

1use polyoxide_core::{HttpClient, QueryBuilder, Request};
2
3use crate::{
4    error::DataApiError,
5    types::{ComboSort, ComboStatus, CombosActivityResponse, CombosResponse},
6};
7
8/// Combos namespace — combinatorial (multi-market) positions and their
9/// lifecycle activity.
10///
11/// A combo row on `/activity` (where `isCombo` is true) carries a `conditionId`
12/// equal to the combo's `combo_condition_id`; pass it to
13/// [`market_id`](ListComboPositions::market_id) here to fetch the combo's legs
14/// and detail.
15#[derive(Clone)]
16pub struct CombosApi {
17    pub(crate) http_client: HttpClient,
18}
19
20impl CombosApi {
21    /// List a user's combinatorial positions (`GET /v1/positions/combos`).
22    ///
23    /// Open positions with a `shares_balance` below 0.001 are omitted (a dust
24    /// floor for sub-0.001 remainders left by "sell all" cashouts); resolved
25    /// positions are served regardless of balance.
26    pub fn positions(&self, user_address: impl Into<String>) -> ListComboPositions {
27        ListComboPositions {
28            request: Request::new(self.http_client.clone(), "/v1/positions/combos")
29                .query("user", user_address.into()),
30        }
31    }
32
33    /// List a user's combo lifecycle and redeem events
34    /// (`GET /v1/activity/combos`).
35    ///
36    /// Covers split, merge, convert, compress, wrap, unwrap, and redeem — the
37    /// combo counterpart to the trade rows on `/activity`.
38    pub fn activity(&self, user_address: impl Into<String>) -> ListComboActivity {
39        ListComboActivity {
40            request: Request::new(self.http_client.clone(), "/v1/activity/combos")
41                .query("user", user_address.into()),
42        }
43    }
44}
45
46/// Request builder for listing combo positions.
47pub struct ListComboPositions {
48    request: Request<CombosResponse, DataApiError>,
49}
50
51impl ListComboPositions {
52    /// Filter by one or more resolution statuses.
53    ///
54    /// Omit for the default listing (open positions plus resolved positions
55    /// with a recorded resolution). [`ComboStatus::Unknown`] is dropped, since
56    /// the upstream API has no matching value to filter on.
57    pub fn status(mut self, statuses: impl IntoIterator<Item = ComboStatus>) -> Self {
58        let values: Vec<String> = statuses
59            .into_iter()
60            .filter(|s| *s != ComboStatus::Unknown)
61            .map(|s| s.to_string())
62            .collect();
63        if !values.is_empty() {
64            self.request = self.request.query("status", values.join(","));
65        }
66        self
67    }
68
69    /// Set the sort order (default: `current_value_desc`).
70    pub fn sort(mut self, sort: ComboSort) -> Self {
71        self.request = self.request.query("sort", sort);
72        self
73    }
74
75    /// Filter by combo condition ID(s) (`0x` + 62 hex).
76    pub fn market_id(mut self, ids: impl IntoIterator<Item = impl ToString>) -> Self {
77        let values: Vec<String> = ids.into_iter().map(|s| s.to_string()).collect();
78        if !values.is_empty() {
79            self.request = self.request.query("market_id", values.join(","));
80        }
81        self
82    }
83
84    /// Set results per page (0-1000, default: 20).
85    pub fn limit(mut self, limit: u32) -> Self {
86        self.request = self.request.query("limit", limit);
87        self
88    }
89
90    /// Set the pagination offset (0-100000, default: 0).
91    ///
92    /// Ignored when [`cursor`](Self::cursor) is set.
93    pub fn offset(mut self, offset: u32) -> Self {
94        self.request = self.request.query("offset", offset);
95        self
96    }
97
98    /// Incremental-sync watermark (epoch seconds, inclusive): return only rows
99    /// whose `updated_at` is at or after this time.
100    ///
101    /// Positions mutate on resolution and redemption, so this catches changes a
102    /// creation-time filter cannot. Pair with
103    /// [`ComboSort::UpdatedAsc`](crate::types::ComboSort::UpdatedAsc).
104    pub fn updated_after(mut self, timestamp: i64) -> Self {
105        self.request = self.request.query("updatedAfter", timestamp);
106        self
107    }
108
109    /// Optional upper bound (epoch seconds, inclusive) for `updated_at`.
110    ///
111    /// Clamped to the safety lag; must be greater than or equal to
112    /// [`updated_after`](Self::updated_after).
113    pub fn updated_before(mut self, timestamp: i64) -> Self {
114        self.request = self.request.query("updatedBefore", timestamp);
115        self
116    }
117
118    /// Continue from a previous response's `pagination.next_cursor`.
119    ///
120    /// When present this supersedes [`offset`](Self::offset), which is ignored.
121    /// Keep the same [`sort`](Self::sort) across pages. Invalid, tampered, or
122    /// cross-endpoint tokens return a 400.
123    pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
124        self.request = self.request.query("cursor", cursor.into());
125        self
126    }
127
128    /// Execute the request.
129    pub async fn send(self) -> Result<CombosResponse, DataApiError> {
130        self.request.send().await
131    }
132}
133
134/// Request builder for listing combo activity.
135pub struct ListComboActivity {
136    request: Request<CombosActivityResponse, DataApiError>,
137}
138
139impl ListComboActivity {
140    /// Filter by combo condition ID(s) (`0x` + 62 hex).
141    pub fn market_id(mut self, ids: impl IntoIterator<Item = impl ToString>) -> Self {
142        let values: Vec<String> = ids.into_iter().map(|s| s.to_string()).collect();
143        if !values.is_empty() {
144            self.request = self.request.query("market_id", values.join(","));
145        }
146        self
147    }
148
149    /// Set results per page (0-500, default: 50).
150    pub fn limit(mut self, limit: u32) -> Self {
151        self.request = self.request.query("limit", limit);
152        self
153    }
154
155    /// Set the pagination offset (0-10000, default: 0).
156    ///
157    /// Ignored when [`cursor`](Self::cursor) is set.
158    pub fn offset(mut self, offset: u32) -> Self {
159        self.request = self.request.query("offset", offset);
160        self
161    }
162
163    /// Continue from a previous response's `pagination.next_cursor`.
164    ///
165    /// When present this supersedes [`offset`](Self::offset), which is ignored.
166    /// Invalid, tampered, or cross-endpoint tokens return a 400.
167    pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
168        self.request = self.request.query("cursor", cursor.into());
169        self
170    }
171
172    /// Execute the request.
173    pub async fn send(self) -> Result<CombosActivityResponse, DataApiError> {
174        self.request.send().await
175    }
176}