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 [`ComboSort::UpdatedAsc`].
103 pub fn updated_after(mut self, timestamp: i64) -> Self {
104 self.request = self.request.query("updatedAfter", timestamp);
105 self
106 }
107
108 /// Optional upper bound (epoch seconds, inclusive) for `updated_at`.
109 ///
110 /// Clamped to the safety lag; must be greater than or equal to
111 /// [`updated_after`](Self::updated_after).
112 pub fn updated_before(mut self, timestamp: i64) -> Self {
113 self.request = self.request.query("updatedBefore", timestamp);
114 self
115 }
116
117 /// Continue from a previous response's `pagination.next_cursor`.
118 ///
119 /// When present this supersedes [`offset`](Self::offset), which is ignored.
120 /// Keep the same [`sort`](Self::sort) across pages. Invalid, tampered, or
121 /// cross-endpoint tokens return a 400.
122 pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
123 self.request = self.request.query("cursor", cursor.into());
124 self
125 }
126
127 /// Execute the request.
128 pub async fn send(self) -> Result<CombosResponse, DataApiError> {
129 self.request.send().await
130 }
131}
132
133/// Request builder for listing combo activity.
134pub struct ListComboActivity {
135 request: Request<CombosActivityResponse, DataApiError>,
136}
137
138impl ListComboActivity {
139 /// Filter by combo condition ID(s) (`0x` + 62 hex).
140 pub fn market_id(mut self, ids: impl IntoIterator<Item = impl ToString>) -> Self {
141 let values: Vec<String> = ids.into_iter().map(|s| s.to_string()).collect();
142 if !values.is_empty() {
143 self.request = self.request.query("market_id", values.join(","));
144 }
145 self
146 }
147
148 /// Set results per page (0-500, default: 50).
149 pub fn limit(mut self, limit: u32) -> Self {
150 self.request = self.request.query("limit", limit);
151 self
152 }
153
154 /// Set the pagination offset (0-10000, default: 0).
155 ///
156 /// Ignored when [`cursor`](Self::cursor) is set.
157 pub fn offset(mut self, offset: u32) -> Self {
158 self.request = self.request.query("offset", offset);
159 self
160 }
161
162 /// Continue from a previous response's `pagination.next_cursor`.
163 ///
164 /// When present this supersedes [`offset`](Self::offset), which is ignored.
165 /// Invalid, tampered, or cross-endpoint tokens return a 400.
166 pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
167 self.request = self.request.query("cursor", cursor.into());
168 self
169 }
170
171 /// Execute the request.
172 pub async fn send(self) -> Result<CombosActivityResponse, DataApiError> {
173 self.request.send().await
174 }
175}