Skip to main content

polyoxide_data/api/
users.rs

1use polyoxide_core::{HttpClient, QueryBuilder, Request};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    error::DataApiError,
6    types::{
7        Activity, ActivitySortBy, ActivityType, ClosedPosition, ClosedPositionSortBy, Position,
8        PositionSortBy, SortDirection, Trade, TradeFilterType, TradeSide, UserValue,
9    },
10};
11
12/// User namespace for user-related operations
13#[derive(Clone)]
14pub struct UserApi {
15    pub(crate) http_client: HttpClient,
16    pub(crate) user_address: String,
17}
18
19impl UserApi {
20    /// List positions for this user
21    pub fn list_positions(&self) -> ListPositions {
22        let mut request = Request::new(self.http_client.clone(), "/positions");
23        request = request.query("user", &self.user_address);
24
25        ListPositions { request }
26    }
27
28    /// Get total value of this user's positions
29    pub fn positions_value(&self) -> GetPositionValue {
30        let mut request = Request::new(self.http_client.clone(), "/value");
31        request = request.query("user", &self.user_address);
32
33        GetPositionValue { request }
34    }
35
36    /// List closed positions for this user
37    pub fn closed_positions(&self) -> ListClosedPositions {
38        let mut request = Request::new(self.http_client.clone(), "/closed-positions");
39        request = request.query("user", &self.user_address);
40
41        ListClosedPositions { request }
42    }
43
44    /// List trades for this user
45    pub fn trades(&self) -> ListUserTrades {
46        let mut request = Request::new(self.http_client.clone(), "/trades");
47        request = request.query("user", &self.user_address);
48
49        ListUserTrades { request }
50    }
51
52    /// List activity for this user
53    pub fn activity(&self) -> ListActivity {
54        let mut request = Request::new(self.http_client.clone(), "/activity");
55        request = request.query("user", &self.user_address);
56
57        ListActivity { request }
58    }
59
60    /// Get total markets traded by this user
61    pub async fn traded(&self) -> Result<UserTraded, DataApiError> {
62        Request::<UserTraded, DataApiError>::new(self.http_client.clone(), "/traded")
63            .query("user", &self.user_address)
64            .send()
65            .await
66    }
67}
68
69/// User's total markets traded count
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct UserTraded {
72    /// User address
73    pub user: String,
74    /// Total count of distinct markets traded
75    pub traded: u64,
76}
77
78/// Request builder for listing user positions
79pub struct ListPositions {
80    request: Request<Vec<Position>, DataApiError>,
81}
82
83impl ListPositions {
84    /// Filter by specific market condition IDs (comma-separated)
85    pub fn market(mut self, condition_ids: impl IntoIterator<Item = impl ToString>) -> Self {
86        let ids: Vec<String> = condition_ids.into_iter().map(|s| s.to_string()).collect();
87        if !ids.is_empty() {
88            self.request = self.request.query("market", ids.join(","));
89        }
90        self
91    }
92
93    /// Filter by event IDs (comma-separated)
94    pub fn event_id(mut self, event_ids: impl IntoIterator<Item = impl ToString>) -> Self {
95        let ids: Vec<String> = event_ids.into_iter().map(|s| s.to_string()).collect();
96        if !ids.is_empty() {
97            self.request = self.request.query("eventId", ids.join(","));
98        }
99        self
100    }
101
102    /// Set minimum position size filter (default: 1)
103    pub fn size_threshold(mut self, threshold: f64) -> Self {
104        self.request = self.request.query("sizeThreshold", threshold);
105        self
106    }
107
108    /// Filter for redeemable positions only
109    pub fn redeemable(mut self, redeemable: bool) -> Self {
110        self.request = self.request.query("redeemable", redeemable);
111        self
112    }
113
114    /// Filter for mergeable positions only
115    pub fn mergeable(mut self, mergeable: bool) -> Self {
116        self.request = self.request.query("mergeable", mergeable);
117        self
118    }
119
120    /// Set maximum number of results (0-500, default: 100)
121    pub fn limit(mut self, limit: u32) -> Self {
122        self.request = self.request.query("limit", limit);
123        self
124    }
125
126    /// Set pagination offset (0-10000, default: 0)
127    pub fn offset(mut self, offset: u32) -> Self {
128        self.request = self.request.query("offset", offset);
129        self
130    }
131
132    /// Set sort field
133    pub fn sort_by(mut self, sort_by: PositionSortBy) -> Self {
134        self.request = self.request.query("sortBy", sort_by);
135        self
136    }
137
138    /// Set sort direction (default: DESC)
139    pub fn sort_direction(mut self, direction: SortDirection) -> Self {
140        self.request = self.request.query("sortDirection", direction);
141        self
142    }
143
144    /// Filter by market title (max 100 chars)
145    pub fn title(mut self, title: impl Into<String>) -> Self {
146        self.request = self.request.query("title", title.into());
147        self
148    }
149
150    /// Execute the request
151    pub async fn send(self) -> Result<Vec<Position>, DataApiError> {
152        self.request.send().await
153    }
154}
155
156/// Request builder for getting total position value
157pub struct GetPositionValue {
158    request: Request<Vec<UserValue>, DataApiError>,
159}
160
161impl GetPositionValue {
162    /// Filter by specific market condition IDs (comma-separated)
163    pub fn market(mut self, condition_ids: impl IntoIterator<Item = impl ToString>) -> Self {
164        let ids: Vec<String> = condition_ids.into_iter().map(|s| s.to_string()).collect();
165        if !ids.is_empty() {
166            self.request = self.request.query("market", ids.join(","));
167        }
168        self
169    }
170
171    /// Execute the request
172    pub async fn send(self) -> Result<Vec<UserValue>, DataApiError> {
173        self.request.send().await
174    }
175}
176
177/// Request builder for listing closed positions
178pub struct ListClosedPositions {
179    request: Request<Vec<ClosedPosition>, DataApiError>,
180}
181
182impl ListClosedPositions {
183    /// Filter by specific market condition IDs (comma-separated)
184    pub fn market(mut self, condition_ids: impl IntoIterator<Item = impl ToString>) -> Self {
185        let ids: Vec<String> = condition_ids.into_iter().map(|s| s.to_string()).collect();
186        if !ids.is_empty() {
187            self.request = self.request.query("market", ids.join(","));
188        }
189        self
190    }
191
192    /// Filter by event IDs (comma-separated)
193    pub fn event_id(mut self, event_ids: impl IntoIterator<Item = impl ToString>) -> Self {
194        let ids: Vec<String> = event_ids.into_iter().map(|s| s.to_string()).collect();
195        if !ids.is_empty() {
196            self.request = self.request.query("eventId", ids.join(","));
197        }
198        self
199    }
200
201    /// Filter by market title (max 100 chars)
202    pub fn title(mut self, title: impl Into<String>) -> Self {
203        self.request = self.request.query("title", title.into());
204        self
205    }
206
207    /// Set maximum number of results (0-50, default: 10)
208    pub fn limit(mut self, limit: u32) -> Self {
209        self.request = self.request.query("limit", limit);
210        self
211    }
212
213    /// Set pagination offset (0-100000, default: 0)
214    pub fn offset(mut self, offset: u32) -> Self {
215        self.request = self.request.query("offset", offset);
216        self
217    }
218
219    /// Set sort field (default: REALIZEDPNL)
220    pub fn sort_by(mut self, sort_by: ClosedPositionSortBy) -> Self {
221        self.request = self.request.query("sortBy", sort_by);
222        self
223    }
224
225    /// Set sort direction (default: DESC)
226    pub fn sort_direction(mut self, direction: SortDirection) -> Self {
227        self.request = self.request.query("sortDirection", direction);
228        self
229    }
230
231    /// Execute the request
232    pub async fn send(self) -> Result<Vec<ClosedPosition>, DataApiError> {
233        self.request.send().await
234    }
235}
236
237/// Request builder for listing user trades
238pub struct ListUserTrades {
239    request: Request<Vec<Trade>, DataApiError>,
240}
241
242impl ListUserTrades {
243    /// Filter by market condition IDs (comma-separated)
244    /// Note: Mutually exclusive with `event_id`
245    pub fn market(mut self, condition_ids: impl IntoIterator<Item = impl ToString>) -> Self {
246        let ids: Vec<String> = condition_ids.into_iter().map(|s| s.to_string()).collect();
247        if !ids.is_empty() {
248            self.request = self.request.query("market", ids.join(","));
249        }
250        self
251    }
252
253    /// Filter by event IDs (comma-separated)
254    /// Note: Mutually exclusive with `market`
255    pub fn event_id(mut self, event_ids: impl IntoIterator<Item = impl ToString>) -> Self {
256        let ids: Vec<String> = event_ids.into_iter().map(|s| s.to_string()).collect();
257        if !ids.is_empty() {
258            self.request = self.request.query("eventId", ids.join(","));
259        }
260        self
261    }
262
263    /// Filter by trade side (BUY or SELL)
264    pub fn side(mut self, side: TradeSide) -> Self {
265        self.request = self.request.query("side", side);
266        self
267    }
268
269    /// Filter for taker trades only (default: true)
270    pub fn taker_only(mut self, taker_only: bool) -> Self {
271        self.request = self.request.query("takerOnly", taker_only);
272        self
273    }
274
275    /// Set filter type (must be paired with `filter_amount`)
276    pub fn filter_type(mut self, filter_type: TradeFilterType) -> Self {
277        self.request = self.request.query("filterType", filter_type);
278        self
279    }
280
281    /// Set filter amount (must be paired with `filter_type`)
282    pub fn filter_amount(mut self, amount: f64) -> Self {
283        self.request = self.request.query("filterAmount", amount);
284        self
285    }
286
287    /// Set maximum number of results (0-10000, default: 100)
288    pub fn limit(mut self, limit: u32) -> Self {
289        self.request = self.request.query("limit", limit);
290        self
291    }
292
293    /// Set pagination offset (0-10000, default: 0)
294    pub fn offset(mut self, offset: u32) -> Self {
295        self.request = self.request.query("offset", offset);
296        self
297    }
298
299    /// Execute the request
300    pub async fn send(self) -> Result<Vec<Trade>, DataApiError> {
301        self.request.send().await
302    }
303}
304
305/// Request builder for listing user activity
306pub struct ListActivity {
307    request: Request<Vec<Activity>, DataApiError>,
308}
309
310impl ListActivity {
311    /// Filter by market condition IDs (comma-separated)
312    pub fn market(mut self, condition_ids: impl IntoIterator<Item = impl ToString>) -> Self {
313        let ids: Vec<String> = condition_ids.into_iter().map(|s| s.to_string()).collect();
314        if !ids.is_empty() {
315            self.request = self.request.query("market", ids.join(","));
316        }
317        self
318    }
319
320    /// Filter by event IDs (comma-separated)
321    pub fn event_id(mut self, event_ids: impl IntoIterator<Item = impl ToString>) -> Self {
322        let ids: Vec<String> = event_ids.into_iter().map(|s| s.to_string()).collect();
323        if !ids.is_empty() {
324            self.request = self.request.query("eventId", ids.join(","));
325        }
326        self
327    }
328
329    /// Filter by activity types (comma-separated)
330    pub fn activity_type(mut self, types: impl IntoIterator<Item = ActivityType>) -> Self {
331        let type_strs: Vec<String> = types.into_iter().map(|t| t.to_string()).collect();
332        if !type_strs.is_empty() {
333            self.request = self.request.query("type", type_strs.join(","));
334        }
335        self
336    }
337
338    /// Filter by trade side (BUY or SELL)
339    pub fn side(mut self, side: TradeSide) -> Self {
340        self.request = self.request.query("side", side);
341        self
342    }
343
344    /// Set start timestamp filter
345    pub fn start(mut self, timestamp: i64) -> Self {
346        self.request = self.request.query("start", timestamp);
347        self
348    }
349
350    /// Set end timestamp filter
351    pub fn end(mut self, timestamp: i64) -> Self {
352        self.request = self.request.query("end", timestamp);
353        self
354    }
355
356    /// Set maximum number of results (0-10000, default: 100)
357    pub fn limit(mut self, limit: u32) -> Self {
358        self.request = self.request.query("limit", limit);
359        self
360    }
361
362    /// Set pagination offset (0-10000, default: 0)
363    pub fn offset(mut self, offset: u32) -> Self {
364        self.request = self.request.query("offset", offset);
365        self
366    }
367
368    /// Set sort field (default: TIMESTAMP)
369    pub fn sort_by(mut self, sort_by: ActivitySortBy) -> Self {
370        self.request = self.request.query("sortBy", sort_by);
371        self
372    }
373
374    /// Set sort direction (default: DESC)
375    pub fn sort_direction(mut self, direction: SortDirection) -> Self {
376        self.request = self.request.query("sortDirection", direction);
377        self
378    }
379
380    /// Execute the request
381    pub async fn send(self) -> Result<Vec<Activity>, DataApiError> {
382        self.request.send().await
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn deserialize_user_traded() {
392        let json = r#"{"user": "0xabcdef1234567890", "traded": 42}"#;
393        let ut: UserTraded = serde_json::from_str(json).unwrap();
394        assert_eq!(ut.user, "0xabcdef1234567890");
395        assert_eq!(ut.traded, 42);
396    }
397
398    #[test]
399    fn deserialize_user_traded_zero() {
400        let json = r#"{"user": "0x0000000000000000000000000000000000000001", "traded": 0}"#;
401        let ut: UserTraded = serde_json::from_str(json).unwrap();
402        assert_eq!(ut.traded, 0);
403    }
404
405    #[test]
406    fn user_traded_roundtrip() {
407        let original = UserTraded {
408            user: "0x1234".to_string(),
409            traded: 100,
410        };
411        let json = serde_json::to_string(&original).unwrap();
412        let deserialized: UserTraded = serde_json::from_str(&json).unwrap();
413        assert_eq!(deserialized.user, original.user);
414        assert_eq!(deserialized.traded, original.traded);
415    }
416}