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}