1use ahash::AHashMap;
19use derive_builder::Builder;
20use rust_decimal::Decimal;
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 common::{
25 enums::{PolymarketOrderType, SignatureType},
26 parse::{deserialize_decimal_from_str, deserialize_optional_decimal_from_str},
27 },
28 http::models::PolymarketOrder,
29};
30
31#[derive(Clone, Debug, Default, Serialize, Builder)]
33#[builder(setter(into, strip_option), default)]
34pub struct GetOrdersParams {
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub id: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub market: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub asset_id: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub next_cursor: Option<String>,
43}
44
45#[derive(Clone, Debug, Default, Serialize, Builder)]
47#[builder(setter(into, strip_option), default)]
48pub struct GetTradesParams {
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub id: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub maker_address: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub market: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub asset_id: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub before: Option<u64>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub after: Option<u64>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub next_cursor: Option<String>,
63}
64
65#[derive(Clone, Debug, Default, Serialize, Builder)]
67#[builder(setter(into, strip_option), default)]
68pub struct GetBalanceAllowanceParams {
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub asset_type: Option<AssetType>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub token_id: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub signature_type: Option<SignatureType>,
75}
76
77#[derive(Clone, Debug, Default, Serialize, Builder)]
79#[builder(setter(into, strip_option), default)]
80pub struct CancelMarketOrdersParams {
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub market: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub asset_id: Option<String>,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
90pub enum AssetType {
91 Collateral,
92 Conditional,
93}
94
95#[derive(Clone, Debug, Deserialize)]
97pub struct BalanceAllowance {
98 #[serde(deserialize_with = "deserialize_decimal_from_str")]
99 pub balance: Decimal,
100 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
101 pub allowance: Option<Decimal>,
102}
103
104#[derive(Clone, Debug, Deserialize)]
106pub struct OrderResponse {
107 pub success: bool,
108 #[serde(rename = "orderID")]
109 pub order_id: Option<String>,
110 #[serde(rename = "errorMsg")]
111 pub error_msg: Option<String>,
112}
113
114#[derive(Clone, Debug, Deserialize)]
120pub struct CancelResponse {
121 #[serde(default)]
122 pub canceled: Vec<String>,
123 #[serde(default)]
124 pub not_canceled: AHashMap<String, Option<String>>,
125}
126
127pub type BatchCancelResponse = CancelResponse;
129
130#[derive(Clone, Debug, Serialize)]
132#[serde(rename_all = "camelCase")]
133pub struct PostOrderParams {
134 pub order_type: PolymarketOrderType,
135 #[serde(skip_serializing_if = "std::ops::Not::not")]
136 pub post_only: bool,
137}
138
139#[derive(Clone, Debug, Serialize)]
141#[serde(rename_all = "camelCase")]
142pub struct OrderSubmission {
143 pub order: PolymarketOrder,
144 pub order_type: PolymarketOrderType,
145 #[serde(skip_serializing_if = "std::ops::Not::not")]
146 pub post_only: bool,
147}
148
149#[derive(Clone, Debug, Default, Serialize, Builder)]
151#[builder(setter(into, strip_option), default)]
152pub struct GetGammaMarketsParams {
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub active: Option<bool>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub closed: Option<bool>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub archived: Option<bool>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub id: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub limit: Option<u32>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub offset: Option<u32>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub order: Option<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub ascending: Option<bool>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub slug: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub clob_token_ids: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub condition_ids: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub liquidity_num_min: Option<f64>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub liquidity_num_max: Option<f64>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub volume_num_min: Option<f64>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub volume_num_max: Option<f64>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub start_date_min: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub start_date_max: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub end_date_min: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub end_date_max: Option<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub tag_id: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub related_tags: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub rewards_min_size: Option<f64>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub include_tag: Option<bool>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub question_ids: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub game_id: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub sports_market_types: Option<String>,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub market_maker_address: Option<String>,
214 #[serde(skip)]
218 pub max_markets: Option<u32>,
219}
220
221#[derive(Clone, Debug, Default, Serialize, Builder)]
223#[builder(setter(into, strip_option), default)]
224pub struct GetGammaEventsParams {
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub active: Option<bool>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub closed: Option<bool>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub archived: Option<bool>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub id: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub slug: Option<String>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub tag_id: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub tag_slug: Option<String>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub exclude_tag_id: Option<String>,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub featured: Option<bool>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub liquidity_min: Option<f64>,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub liquidity_max: Option<f64>,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub volume_min: Option<f64>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub volume_max: Option<f64>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub start_date_min: Option<String>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub start_date_max: Option<String>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub end_date_min: Option<String>,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub end_date_max: Option<String>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub order: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub ascending: Option<bool>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub limit: Option<u32>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub offset: Option<u32>,
271 #[serde(skip)]
273 pub max_events: Option<u32>,
274}
275
276#[derive(Clone, Debug, Default, Serialize, Builder)]
278#[builder(setter(into, strip_option), default)]
279pub struct GetSearchParams {
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub q: Option<String>,
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub events_status: Option<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub events_tag: Option<String>,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub sort: Option<String>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub ascending: Option<bool>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub limit_per_type: Option<u32>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub page: Option<u32>,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub keep_closed_markets: Option<bool>,
300}
301
302#[derive(Clone, Debug, Deserialize)]
304pub struct PaginatedResponse<T> {
305 pub data: Vec<T>,
306 pub next_cursor: String,
307}
308
309#[cfg(test)]
310mod tests {
311 use rstest::rstest;
312 use rust_decimal_macros::dec;
313
314 use super::*;
315 use crate::{
316 common::enums::{PolymarketOrderSide, PolymarketOrderType},
317 http::models::{PolymarketOpenOrder, PolymarketTradeReport},
318 };
319
320 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
321 let path = format!("test_data/{filename}");
322 let content = std::fs::read_to_string(path).expect("Failed to read test data");
323 serde_json::from_str(&content).expect("Failed to parse test data")
324 }
325
326 #[rstest]
327 fn test_paginated_orders_page() {
328 let page: PaginatedResponse<PolymarketOpenOrder> = load("http_open_orders_page.json");
329
330 assert_eq!(page.data.len(), 2);
331 assert_eq!(page.next_cursor, "LTE=");
332 assert_eq!(page.data[0].side, PolymarketOrderSide::Buy);
333 assert_eq!(page.data[1].side, PolymarketOrderSide::Sell);
334 }
335
336 #[rstest]
337 fn test_paginated_trades_page() {
338 let page: PaginatedResponse<PolymarketTradeReport> = load("http_trades_page.json");
339
340 assert_eq!(page.data.len(), 1);
341 assert_eq!(page.next_cursor, "LTE=");
342 assert_eq!(page.data[0].id, "trade-0x001");
343 }
344
345 #[rstest]
346 fn test_balance_allowance_with_allowance() {
347 let ba: BalanceAllowance = load("http_balance_allowance_collateral.json");
350
351 assert_eq!(ba.balance, dec!(1_000_000_000));
352 assert_eq!(ba.allowance, Some(dec!(999_999_999_000_000)));
353 }
354
355 #[rstest]
356 fn test_balance_allowance_no_allowance() {
357 let ba: BalanceAllowance = load("http_balance_allowance_no_allowance.json");
358
359 assert_eq!(ba.balance, dec!(250.500000));
360 assert!(ba.allowance.is_none());
361 }
362
363 #[rstest]
364 fn test_order_response_success() {
365 let resp: OrderResponse = load("http_order_response_ok.json");
366
367 assert!(resp.success);
368 assert_eq!(
369 resp.order_id.as_deref(),
370 Some("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12")
371 );
372 assert!(resp.error_msg.is_none());
373 }
374
375 #[rstest]
376 fn test_order_response_failure() {
377 let resp: OrderResponse = load("http_order_response_failed.json");
378
379 assert!(!resp.success);
380 assert!(resp.order_id.is_none());
381 assert_eq!(resp.error_msg.as_deref(), Some("Insufficient balance"));
382 }
383
384 #[rstest]
385 fn test_cancel_response_ok() {
386 let resp: CancelResponse = load("http_cancel_response_ok.json");
387
388 assert_eq!(resp.canceled.len(), 1);
389 assert!(resp.not_canceled.is_empty());
390 }
391
392 #[rstest]
393 fn test_cancel_response_failed() {
394 let resp: CancelResponse = load("http_cancel_response_failed.json");
395
396 assert!(resp.canceled.is_empty());
397 assert_eq!(resp.not_canceled.len(), 1);
398 let reason = resp.not_canceled.values().next().and_then(|v| v.as_deref());
399 assert_eq!(reason, Some("already canceled or matched"));
400 }
401
402 #[rstest]
403 fn test_batch_cancel_response() {
404 let resp: BatchCancelResponse = load("http_batch_cancel_response.json");
405
406 assert_eq!(resp.canceled.len(), 2);
407 assert!(resp.canceled[0].contains("1111"));
408 assert!(resp.canceled[1].contains("2222"));
409 assert_eq!(resp.not_canceled.len(), 1);
410 let reason = resp.not_canceled.values().next().and_then(|v| v.as_deref());
411 assert_eq!(reason, Some("already canceled or matched"));
412 }
413
414 #[rstest]
415 fn test_asset_type_serializes_screaming_snake() {
416 assert_eq!(
417 serde_json::to_string(&AssetType::Collateral).unwrap(),
418 "\"COLLATERAL\""
419 );
420 assert_eq!(
421 serde_json::to_string(&AssetType::Conditional).unwrap(),
422 "\"CONDITIONAL\""
423 );
424 }
425
426 #[rstest]
427 fn test_asset_type_deserializes() {
428 assert_eq!(
429 serde_json::from_str::<AssetType>("\"COLLATERAL\"").unwrap(),
430 AssetType::Collateral
431 );
432 assert_eq!(
433 serde_json::from_str::<AssetType>("\"CONDITIONAL\"").unwrap(),
434 AssetType::Conditional
435 );
436 }
437
438 #[rstest]
439 fn test_get_orders_params_skips_none() {
440 let params = GetOrdersParams::default();
441 let json = serde_json::to_string(¶ms).unwrap();
442 assert_eq!(json, "{}");
443 }
444
445 #[rstest]
446 fn test_get_orders_params_serializes_set_fields() {
447 let params = GetOrdersParams {
448 market: Some("0xmarket".to_string()),
449 asset_id: None,
450 next_cursor: Some("MA==".to_string()),
451 ..Default::default()
452 };
453 let json = serde_json::to_string(¶ms).unwrap();
454 assert!(json.contains("\"market\""));
455 assert!(json.contains("\"next_cursor\""));
456 assert!(!json.contains("\"asset_id\""));
457 }
458
459 #[rstest]
460 fn test_get_orders_params_id_filter() {
461 let params = GetOrdersParams {
462 id: Some("0xorder123".to_string()),
463 ..Default::default()
464 };
465 let json = serde_json::to_string(¶ms).unwrap();
466 assert!(json.contains("\"id\""));
467 assert!(json.contains("0xorder123"));
468 }
469
470 #[rstest]
471 fn test_get_gamma_markets_params_slug() {
472 let params = GetGammaMarketsParams {
473 slug: Some("btc-updown-15m-1741500000".to_string()),
474 ..Default::default()
475 };
476 let json = serde_json::to_string(¶ms).unwrap();
477 assert!(json.contains("\"slug\""));
478 assert!(json.contains("btc-updown-15m-1741500000"));
479 assert!(!json.contains("\"active\""));
480 }
481
482 #[rstest]
483 fn test_get_gamma_markets_params_skips_none_slug() {
484 let params = GetGammaMarketsParams {
485 active: Some(true),
486 ..Default::default()
487 };
488 let json = serde_json::to_string(¶ms).unwrap();
489 assert!(!json.contains("\"slug\""));
490 assert!(json.contains("\"active\""));
491 }
492
493 #[rstest]
494 fn test_get_gamma_markets_params_new_filter_fields() {
495 let params = GetGammaMarketsParams {
496 volume_num_min: Some(1000.0),
497 tag_id: Some("politics".to_string()),
498 end_date_min: Some("2025-06-01T00:00:00Z".to_string()),
499 ..Default::default()
500 };
501 let json = serde_json::to_string(¶ms).unwrap();
502 assert!(json.contains("\"volume_num_min\":1000.0"));
503 assert!(json.contains("\"tag_id\":\"politics\""));
504 assert!(json.contains("\"end_date_min\":\"2025-06-01T00:00:00Z\""));
505 assert!(!json.contains("\"active\""));
506 assert!(!json.contains("\"archived\""));
507 }
508
509 #[rstest]
510 fn test_get_gamma_markets_params_condition_ids() {
511 let params = GetGammaMarketsParams {
512 condition_ids: Some("0xcond1,0xcond2".to_string()),
513 liquidity_num_min: Some(500.0),
514 ..Default::default()
515 };
516 let json = serde_json::to_string(¶ms).unwrap();
517 assert!(json.contains("\"condition_ids\":\"0xcond1,0xcond2\""));
518 assert!(json.contains("\"liquidity_num_min\":500.0"));
519 }
520
521 #[rstest]
522 fn test_get_trades_params_skips_none() {
523 let params = GetTradesParams::default();
524 let json = serde_json::to_string(¶ms).unwrap();
525 assert_eq!(json, "{}");
526 }
527
528 #[rstest]
529 fn test_post_order_params_skips_post_only_when_false() {
530 let params = PostOrderParams {
531 order_type: PolymarketOrderType::GTC,
532 post_only: false,
533 };
534 let json = serde_json::to_string(¶ms).unwrap();
535 assert!(!json.contains("post_only"));
536 assert!(!json.contains("postOnly"));
537 }
538
539 #[rstest]
540 fn test_post_order_params_includes_post_only_when_true() {
541 let params = PostOrderParams {
542 order_type: PolymarketOrderType::GTC,
543 post_only: true,
544 };
545 let json = serde_json::to_string(¶ms).unwrap();
546 assert!(json.contains("postOnly"));
547 }
548}