1use {
7 crate::error::Result,
8 serde::{Deserialize, Serialize},
9};
10
11const DATA_API_BASE: &str = "https://data-api.polymarket.com";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DataTrade {
16 pub proxy_wallet: String,
17 pub side: String,
18 pub asset: String,
19 pub condition_id: String,
20 pub size: f64,
21 pub price: f64,
22 pub timestamp: i64,
23 pub title: String,
24 pub slug: String,
25 pub icon: Option<String>,
26 pub event_slug: String,
27 pub outcome: String,
28 pub outcome_index: i32,
29 pub name: String,
30 pub pseudonym: String,
31 pub bio: Option<String>,
32 pub profile_image: Option<String>,
33 pub profile_image_optimized: Option<String>,
34 pub transaction_hash: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Position {
40 #[serde(rename = "proxyWallet", default)]
41 pub proxy_wallet: Option<String>,
42 pub asset: String,
43 #[serde(rename = "conditionId", alias = "condition_id")]
44 pub condition_id: String,
45 #[serde(default)]
46 pub size: Option<f64>,
47 #[serde(rename = "avgPrice", default)]
48 pub avg_price: Option<f64>,
49 #[serde(rename = "initialValue", default)]
50 pub initial_value: Option<f64>,
51 #[serde(rename = "currentValue", default)]
52 pub current_value: Option<f64>,
53 #[serde(rename = "cashPnl", default)]
54 pub cash_pnl: Option<f64>,
55 #[serde(rename = "percentPnl", default)]
56 pub percent_pnl: Option<f64>,
57 #[serde(rename = "totalBought", default)]
58 pub total_bought: Option<f64>,
59 #[serde(rename = "realizedPnl", default)]
60 pub realized_pnl: Option<f64>,
61 #[serde(rename = "percentRealizedPnl", default)]
62 pub percent_realized_pnl: Option<f64>,
63 #[serde(rename = "curPrice", default)]
64 pub cur_price: Option<f64>,
65 #[serde(default)]
66 pub redeemable: Option<bool>,
67 #[serde(default)]
68 pub mergeable: Option<bool>,
69 pub title: String,
70 pub slug: String,
71 #[serde(default)]
72 pub icon: Option<String>,
73 #[serde(rename = "eventSlug", alias = "event_slug")]
74 pub event_slug: String,
75 pub outcome: String,
76 #[serde(rename = "outcomeIndex", alias = "outcome_index")]
77 pub outcome_index: i32,
78 #[serde(rename = "oppositeOutcome", default)]
79 pub opposite_outcome: Option<String>,
80 #[serde(rename = "oppositeAsset", default)]
81 pub opposite_asset: Option<String>,
82 #[serde(rename = "endDate", default)]
83 pub end_date: Option<String>,
84 #[serde(rename = "negativeRisk", default)]
85 pub negative_risk: Option<bool>,
86 #[serde(default)]
88 pub quantity: Option<String>,
89 #[serde(default)]
90 pub value: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Portfolio {
96 pub total_value: Option<String>,
97 pub positions: Vec<Position>,
98}
99
100#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
102#[serde(rename_all = "UPPERCASE")]
103pub enum ActivityType {
104 Trade,
105 Split,
106 Merge,
107 Redeem,
108 Reward,
109 Conversion,
110}
111
112impl ActivityType {
113 fn as_str(&self) -> &'static str {
114 match self {
115 ActivityType::Trade => "TRADE",
116 ActivityType::Split => "SPLIT",
117 ActivityType::Merge => "MERGE",
118 ActivityType::Redeem => "REDEEM",
119 ActivityType::Reward => "REWARD",
120 ActivityType::Conversion => "CONVERSION",
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy)]
127pub enum ActivitySortBy {
128 Timestamp,
129 Tokens,
130 Cash,
131}
132
133impl ActivitySortBy {
134 fn as_str(&self) -> &'static str {
135 match self {
136 ActivitySortBy::Timestamp => "TIMESTAMP",
137 ActivitySortBy::Tokens => "TOKENS",
138 ActivitySortBy::Cash => "CASH",
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy)]
145pub enum SortDirection {
146 Asc,
147 Desc,
148}
149
150impl SortDirection {
151 fn as_str(&self) -> &'static str {
152 match self {
153 SortDirection::Asc => "ASC",
154 SortDirection::Desc => "DESC",
155 }
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct Activity {
162 #[serde(rename = "proxyWallet")]
163 pub proxy_wallet: String,
164 pub timestamp: i64,
165 #[serde(rename = "conditionId")]
166 pub condition_id: String,
167 #[serde(rename = "type")]
168 pub activity_type: ActivityType,
169 #[serde(default)]
170 pub size: Option<f64>,
171 #[serde(rename = "usdcSize", default)]
172 pub usdc_size: Option<f64>,
173 #[serde(rename = "transactionHash")]
174 pub transaction_hash: String,
175 #[serde(default)]
176 pub price: Option<f64>,
177 #[serde(default)]
178 pub asset: Option<String>,
179 #[serde(default)]
180 pub side: Option<String>,
181 #[serde(rename = "outcomeIndex", default)]
182 pub outcome_index: Option<i32>,
183 #[serde(default)]
184 pub title: Option<String>,
185 #[serde(default)]
186 pub slug: Option<String>,
187 #[serde(default)]
188 pub icon: Option<String>,
189 #[serde(rename = "eventSlug", default)]
190 pub event_slug: Option<String>,
191 #[serde(default)]
192 pub outcome: Option<String>,
193 #[serde(default)]
194 pub name: Option<String>,
195 #[serde(default)]
196 pub pseudonym: Option<String>,
197 #[serde(default)]
198 pub bio: Option<String>,
199 #[serde(rename = "profileImage", default)]
200 pub profile_image: Option<String>,
201 #[serde(rename = "profileImageOptimized", default)]
202 pub profile_image_optimized: Option<String>,
203}
204
205#[derive(Debug, Clone, Copy)]
207pub enum TradeSide {
208 Buy,
209 Sell,
210}
211
212impl TradeSide {
213 fn as_str(&self) -> &'static str {
214 match self {
215 TradeSide::Buy => "BUY",
216 TradeSide::Sell => "SELL",
217 }
218 }
219}
220
221pub struct DataClient {
223 client: reqwest::Client,
224}
225
226impl DataClient {
227 pub fn new() -> Self {
229 Self {
230 client: reqwest::Client::new(),
231 }
232 }
233
234 pub async fn get_trades_by_event(
236 &self,
237 event_id: u64,
238 limit: Option<usize>,
239 offset: Option<usize>,
240 filter_type: Option<&str>,
241 filter_amount: Option<f64>,
242 ) -> Result<Vec<DataTrade>> {
243 let url = format!("{}/trades", DATA_API_BASE);
244 let mut params = vec![
245 ("eventId", event_id.to_string()),
246 ("limit", limit.unwrap_or(10).to_string()),
247 ("offset", offset.unwrap_or(0).to_string()),
248 ];
249
250 if let Some(filter_type) = filter_type {
251 params.push(("filterType", filter_type.to_string()));
252 }
253 if let Some(filter_amount) = filter_amount {
254 params.push(("filterAmount", filter_amount.to_string()));
255 }
256
257 let trades: Vec<DataTrade> = self
258 .client
259 .get(&url)
260 .query(¶ms)
261 .send()
262 .await?
263 .json()
264 .await?;
265 Ok(trades)
266 }
267
268 pub async fn get_trades_by_event_slug(
270 &self,
271 event_slug: &str,
272 limit: Option<usize>,
273 offset: Option<usize>,
274 ) -> Result<Vec<DataTrade>> {
275 let url = format!("{}/trades", DATA_API_BASE);
276 let params = vec![
277 ("eventSlug", event_slug.to_string()),
278 ("limit", limit.unwrap_or(10).to_string()),
279 ("offset", offset.unwrap_or(0).to_string()),
280 ];
281
282 let trades: Vec<DataTrade> = self
283 .client
284 .get(&url)
285 .query(¶ms)
286 .send()
287 .await?
288 .json()
289 .await?;
290 Ok(trades)
291 }
292
293 pub async fn get_trades_by_market(
295 &self,
296 condition_id: &str,
297 limit: Option<usize>,
298 offset: Option<usize>,
299 ) -> Result<Vec<DataTrade>> {
300 let url = format!("{}/trades", DATA_API_BASE);
301 let params = vec![
302 ("conditionId", condition_id.to_string()),
303 ("limit", limit.unwrap_or(10).to_string()),
304 ("offset", offset.unwrap_or(0).to_string()),
305 ];
306
307 let trades: Vec<DataTrade> = self
308 .client
309 .get(&url)
310 .query(¶ms)
311 .send()
312 .await?
313 .json()
314 .await?;
315 Ok(trades)
316 }
317
318 pub async fn get_positions(&self, user_address: &str) -> Result<Vec<Position>> {
320 let url = format!("{}/positions", DATA_API_BASE);
321 let params = [("user", user_address)];
322 let positions: Vec<Position> = self
323 .client
324 .get(&url)
325 .query(¶ms)
326 .send()
327 .await?
328 .json()
329 .await?;
330 Ok(positions)
331 }
332
333 pub async fn get_portfolio(&self, user_address: &str) -> Result<Portfolio> {
335 let url = format!("{}/portfolio", DATA_API_BASE);
336 let params = [("user", user_address)];
337 let portfolio: Portfolio = self
338 .client
339 .get(&url)
340 .query(¶ms)
341 .send()
342 .await?
343 .json()
344 .await?;
345 Ok(portfolio)
346 }
347
348 #[allow(clippy::too_many_arguments)]
363 pub async fn get_activity(
364 &self,
365 user_address: &str,
366 limit: Option<usize>,
367 offset: Option<usize>,
368 market: Option<&str>,
369 event_id: Option<u64>,
370 activity_types: Option<Vec<ActivityType>>,
371 start: Option<i64>,
372 end: Option<i64>,
373 sort_by: Option<ActivitySortBy>,
374 sort_direction: Option<SortDirection>,
375 side: Option<TradeSide>,
376 ) -> Result<Vec<Activity>> {
377 let url = format!("{}/activity", DATA_API_BASE);
378 let mut params = vec![
379 ("user", user_address.to_string()),
380 ("limit", limit.unwrap_or(100).to_string()),
381 ("offset", offset.unwrap_or(0).to_string()),
382 ];
383
384 if let Some(market) = market {
385 params.push(("market", market.to_string()));
386 }
387 if let Some(event_id) = event_id {
388 params.push(("eventId", event_id.to_string()));
389 }
390 if let Some(types) = activity_types {
391 let type_str = types
392 .iter()
393 .map(|t| t.as_str())
394 .collect::<Vec<_>>()
395 .join(",");
396 params.push(("type", type_str));
397 }
398 if let Some(start) = start {
399 params.push(("start", start.to_string()));
400 }
401 if let Some(end) = end {
402 params.push(("end", end.to_string()));
403 }
404 if let Some(sort_by) = sort_by {
405 params.push(("sortBy", sort_by.as_str().to_string()));
406 }
407 if let Some(sort_direction) = sort_direction {
408 params.push(("sortDirection", sort_direction.as_str().to_string()));
409 }
410 if let Some(side) = side {
411 params.push(("side", side.as_str().to_string()));
412 }
413
414 let activities: Vec<Activity> = self
415 .client
416 .get(&url)
417 .query(¶ms)
418 .send()
419 .await?
420 .json()
421 .await?;
422 Ok(activities)
423 }
424
425 #[allow(clippy::too_many_arguments)]
438 pub async fn get_trades(
439 &self,
440 user_address: Option<&str>,
441 market: Option<&str>,
442 event_id: Option<u64>,
443 limit: Option<usize>,
444 offset: Option<usize>,
445 taker_only: Option<bool>,
446 filter_type: Option<&str>,
447 filter_amount: Option<f64>,
448 side: Option<TradeSide>,
449 ) -> Result<Vec<DataTrade>> {
450 let url = format!("{}/trades", DATA_API_BASE);
451 let mut params = vec![
452 ("limit", limit.unwrap_or(100).to_string()),
453 ("offset", offset.unwrap_or(0).to_string()),
454 ];
455
456 if let Some(user) = user_address {
457 params.push(("user", user.to_string()));
458 }
459 if let Some(market) = market {
460 params.push(("market", market.to_string()));
461 }
462 if let Some(event_id) = event_id {
463 params.push(("eventId", event_id.to_string()));
464 }
465 if let Some(taker_only) = taker_only {
466 params.push(("takerOnly", taker_only.to_string()));
467 }
468 if let Some(filter_type) = filter_type {
469 params.push(("filterType", filter_type.to_string()));
470 }
471 if let Some(filter_amount) = filter_amount {
472 params.push(("filterAmount", filter_amount.to_string()));
473 }
474 if let Some(side) = side {
475 params.push(("side", side.as_str().to_string()));
476 }
477
478 let trades: Vec<DataTrade> = self
479 .client
480 .get(&url)
481 .query(¶ms)
482 .send()
483 .await?
484 .json()
485 .await?;
486 Ok(trades)
487 }
488
489 #[allow(clippy::too_many_arguments)]
501 pub async fn get_positions_filtered(
502 &self,
503 user_address: &str,
504 market: Option<&str>,
505 event_id: Option<u64>,
506 size_threshold: Option<f64>,
507 redeemable: Option<bool>,
508 mergeable: Option<bool>,
509 limit: Option<usize>,
510 offset: Option<usize>,
511 ) -> Result<Vec<Position>> {
512 let url = format!("{}/positions", DATA_API_BASE);
513 let mut params = vec![
514 ("user", user_address.to_string()),
515 ("limit", limit.unwrap_or(100).to_string()),
516 ("offset", offset.unwrap_or(0).to_string()),
517 ];
518
519 if let Some(market) = market {
520 params.push(("market", market.to_string()));
521 }
522 if let Some(event_id) = event_id {
523 params.push(("eventId", event_id.to_string()));
524 }
525 if let Some(size_threshold) = size_threshold {
526 params.push(("sizeThreshold", size_threshold.to_string()));
527 }
528 if let Some(redeemable) = redeemable {
529 params.push(("redeemable", redeemable.to_string()));
530 }
531 if let Some(mergeable) = mergeable {
532 params.push(("mergeable", mergeable.to_string()));
533 }
534
535 let positions: Vec<Position> = self
536 .client
537 .get(&url)
538 .query(¶ms)
539 .send()
540 .await?
541 .json()
542 .await?;
543 Ok(positions)
544 }
545}
546
547impl Default for DataClient {
548 fn default() -> Self {
549 Self::new()
550 }
551}