1use rust_decimal::Decimal;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::{
23 enums::{
24 PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus, PolymarketOrderType,
25 PolymarketOutcome, PolymarketTradeStatus, SignatureType,
26 },
27 models::PolymarketMakerOrder,
28 parse::{
29 deserialize_decimal_from_json_number, deserialize_decimal_from_str,
30 deserialize_optional_decimal_from_json_number, deserialize_optional_polymarket_game_id,
31 serialize_decimal_as_json_number, serialize_decimal_as_str,
32 serialize_optional_decimal_as_json_number,
33 },
34};
35
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct PolymarketOrder {
47 pub salt: u64,
48 pub maker: String,
49 pub signer: String,
50 pub token_id: Ustr,
51 #[serde(
52 serialize_with = "serialize_decimal_as_str",
53 deserialize_with = "deserialize_decimal_from_str"
54 )]
55 pub maker_amount: Decimal,
56 #[serde(
57 serialize_with = "serialize_decimal_as_str",
58 deserialize_with = "deserialize_decimal_from_str"
59 )]
60 pub taker_amount: Decimal,
61 pub side: PolymarketOrderSide,
62 pub signature_type: SignatureType,
63 pub expiration: String,
66 pub timestamp: String,
69 pub metadata: String,
71 pub builder: String,
73 pub signature: String,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
80pub struct PolymarketOpenOrder {
81 pub associate_trades: Option<Vec<String>>,
82 pub id: String,
83 pub status: PolymarketOrderStatus,
84 pub market: Ustr,
85 #[serde(
86 serialize_with = "serialize_decimal_as_str",
87 deserialize_with = "deserialize_decimal_from_str"
88 )]
89 pub original_size: Decimal,
90 pub outcome: PolymarketOutcome,
91 pub maker_address: String,
92 pub owner: String,
93 #[serde(
94 serialize_with = "serialize_decimal_as_str",
95 deserialize_with = "deserialize_decimal_from_str"
96 )]
97 pub price: Decimal,
98 pub side: PolymarketOrderSide,
99 #[serde(
100 serialize_with = "serialize_decimal_as_str",
101 deserialize_with = "deserialize_decimal_from_str"
102 )]
103 pub size_matched: Decimal,
104 pub asset_id: Ustr,
105 pub expiration: Option<String>,
106 pub order_type: PolymarketOrderType,
107 pub created_at: u64,
108}
109
110#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
114pub struct PolymarketTradeReport {
115 pub id: String,
116 pub taker_order_id: String,
117 pub market: Ustr,
118 pub asset_id: Ustr,
119 pub side: PolymarketOrderSide,
120 #[serde(
121 serialize_with = "serialize_decimal_as_str",
122 deserialize_with = "deserialize_decimal_from_str"
123 )]
124 pub size: Decimal,
125 #[serde(
126 serialize_with = "serialize_decimal_as_str",
127 deserialize_with = "deserialize_decimal_from_str"
128 )]
129 pub fee_rate_bps: Decimal,
130 #[serde(
131 serialize_with = "serialize_decimal_as_str",
132 deserialize_with = "deserialize_decimal_from_str"
133 )]
134 pub price: Decimal,
135 pub status: PolymarketTradeStatus,
136 pub match_time: String,
137 pub last_update: String,
138 pub outcome: PolymarketOutcome,
139 pub bucket_index: u64,
140 pub owner: String,
141 pub maker_address: String,
142 pub transaction_hash: String,
143 pub maker_orders: Vec<PolymarketMakerOrder>,
144 pub trader_side: PolymarketLiquiditySide,
145}
146
147#[derive(Clone, Debug, Deserialize, Serialize)]
151#[serde(rename_all = "camelCase")]
152pub struct GammaMarket {
153 pub id: String,
155 pub condition_id: String,
157 #[serde(rename = "questionID")]
159 pub question_id: Option<String>,
160 #[serde(default)]
162 pub clob_token_ids: String,
163 #[serde(default)]
165 pub outcomes: String,
166 pub question: String,
168 pub description: Option<String>,
170 pub start_date: Option<String>,
172 pub event_start_time: Option<String>,
174 pub end_date: Option<String>,
176 pub active: Option<bool>,
178 pub closed: Option<bool>,
180 pub closed_time: Option<String>,
182 pub uma_resolution_status: Option<String>,
184 pub uma_resolution_statuses: Option<String>,
186 pub resolution_source: Option<String>,
188 pub crypto_market_config: Option<CryptoMarketConfig>,
190 pub accepting_orders: Option<bool>,
192 pub enable_order_book: Option<bool>,
194 #[serde(
196 default,
197 deserialize_with = "deserialize_optional_decimal_from_json_number",
198 serialize_with = "serialize_optional_decimal_as_json_number"
199 )]
200 pub order_price_min_tick_size: Option<Decimal>,
201 #[serde(
203 default,
204 deserialize_with = "deserialize_optional_decimal_from_json_number",
205 serialize_with = "serialize_optional_decimal_as_json_number"
206 )]
207 pub order_min_size: Option<Decimal>,
208 pub maker_base_fee: Option<i64>,
210 pub taker_base_fee: Option<i64>,
212 #[serde(rename = "slug")]
214 pub market_slug: Option<String>,
215 #[serde(rename = "negRisk")]
217 pub neg_risk: Option<bool>,
218 pub liquidity_num: Option<f64>,
220 pub volume_num: Option<f64>,
222 #[serde(rename = "volume24hr")]
224 pub volume_24hr: Option<f64>,
225 pub outcome_prices: Option<String>,
227 pub best_bid: Option<f64>,
229 pub best_ask: Option<f64>,
231 pub spread: Option<f64>,
233 pub last_trade_price: Option<f64>,
235 pub one_day_price_change: Option<f64>,
237 pub one_week_price_change: Option<f64>,
239 #[serde(rename = "volume1wk")]
241 pub volume_1wk: Option<f64>,
242 #[serde(rename = "volume1mo")]
244 pub volume_1mo: Option<f64>,
245 #[serde(rename = "volume1yr")]
247 pub volume_1yr: Option<f64>,
248 pub rewards_min_size: Option<f64>,
250 pub rewards_max_spread: Option<f64>,
252 pub competitive: Option<f64>,
254 pub category: Option<String>,
256 #[serde(rename = "negRiskMarketID")]
258 pub neg_risk_market_id: Option<String>,
259 pub fee_schedule: Option<FeeSchedule>,
261 #[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
266 pub game_id: Option<String>,
267 pub events: Option<Vec<GammaEvent>>,
269}
270
271#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
272#[serde(rename_all = "camelCase")]
273pub struct FeeSchedule {
274 #[serde(
275 serialize_with = "serialize_decimal_as_json_number",
276 deserialize_with = "deserialize_decimal_from_json_number"
277 )]
278 pub exponent: Decimal,
279 #[serde(
280 serialize_with = "serialize_decimal_as_json_number",
281 deserialize_with = "deserialize_decimal_from_json_number"
282 )]
283 pub rate: Decimal,
284 pub taker_only: bool,
285 #[serde(
286 serialize_with = "serialize_decimal_as_json_number",
287 deserialize_with = "deserialize_decimal_from_json_number"
288 )]
289 pub rebate_rate: Decimal,
290}
291
292#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
294#[serde(rename_all = "camelCase")]
295pub struct CryptoMarketConfig {
296 pub id: String,
297 pub asset: String,
298 pub duration: String,
299 pub twap_enabled: bool,
300 #[serde(
301 default,
302 skip_serializing_if = "Option::is_none",
303 deserialize_with = "deserialize_optional_non_null_i64"
304 )]
305 pub twap_lookback_seconds: Option<i64>,
306}
307
308fn deserialize_optional_non_null_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
309where
310 D: serde::Deserializer<'de>,
311{
312 i64::deserialize(deserializer).map(Some)
313}
314
315#[derive(Clone, Debug, Deserialize, Serialize)]
321#[serde(rename_all = "camelCase")]
322pub struct GammaEvent {
323 pub id: String,
324 pub slug: Option<String>,
325 pub title: Option<String>,
326 pub description: Option<String>,
327 pub start_date: Option<String>,
328 pub end_date: Option<String>,
329 pub active: Option<bool>,
330 pub closed: Option<bool>,
331 pub archived: Option<bool>,
332 #[serde(default)]
333 pub markets: Vec<GammaMarket>,
334 pub liquidity: Option<f64>,
336 pub volume: Option<f64>,
338 pub open_interest: Option<f64>,
340 #[serde(rename = "volume24hr")]
342 pub volume_24hr: Option<f64>,
343 pub category: Option<String>,
345 pub neg_risk: Option<bool>,
347 #[serde(rename = "negRiskMarketID")]
349 pub neg_risk_market_id: Option<String>,
350 pub featured: Option<bool>,
352 #[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
357 pub game_id: Option<String>,
358}
359
360#[derive(Clone, Debug, Deserialize, Serialize)]
362pub struct GammaTag {
363 pub id: String,
365 pub label: Option<String>,
367 pub slug: Option<String>,
369}
370
371#[derive(Clone, Debug, Deserialize, Serialize)]
373pub struct SearchResponse {
374 #[serde(default)]
376 pub markets: Option<Vec<GammaMarket>>,
377 #[serde(default)]
379 pub events: Option<Vec<GammaEvent>>,
380}
381
382#[derive(Clone, Debug, Deserialize)]
386pub struct TickSizeResponse {
387 #[serde(deserialize_with = "deserialize_decimal_from_json_number")]
389 pub minimum_tick_size: Decimal,
390}
391
392#[derive(Clone, Debug, Deserialize)]
396pub struct FeeRateResponse {
397 pub base_fee: Decimal,
399}
400
401#[derive(Clone, Debug, Deserialize)]
403pub struct ClobBookLevel {
404 pub price: String,
405 pub size: String,
406}
407
408#[derive(Clone, Debug, Deserialize)]
412pub struct ClobBookResponse {
413 pub bids: Vec<ClobBookLevel>,
414 pub asks: Vec<ClobBookLevel>,
415}
416
417#[derive(Clone, Debug, Deserialize, Serialize)]
419pub struct ClobMarketToken {
420 pub token_id: String,
421 pub outcome: String,
422 #[serde(
423 default,
424 deserialize_with = "deserialize_optional_decimal_from_json_number",
425 serialize_with = "serialize_optional_decimal_as_json_number"
426 )]
427 pub price: Option<Decimal>,
428 pub winner: bool,
429}
430
431#[derive(Clone, Debug, Deserialize, Serialize)]
433pub struct ClobMarketRewardRate {
434 pub asset_address: String,
435 #[serde(
436 deserialize_with = "deserialize_decimal_from_json_number",
437 serialize_with = "serialize_decimal_as_json_number"
438 )]
439 pub rewards_daily_rate: Decimal,
440}
441
442#[derive(Clone, Debug, Deserialize, Serialize)]
444pub struct ClobMarketRewards {
445 pub rates: Option<Vec<ClobMarketRewardRate>>,
446 #[serde(
447 default,
448 deserialize_with = "deserialize_optional_decimal_from_json_number",
449 serialize_with = "serialize_optional_decimal_as_json_number"
450 )]
451 pub min_size: Option<Decimal>,
452 #[serde(
453 default,
454 deserialize_with = "deserialize_optional_decimal_from_json_number",
455 serialize_with = "serialize_optional_decimal_as_json_number"
456 )]
457 pub max_spread: Option<Decimal>,
458}
459
460#[derive(Clone, Debug, Deserialize, Serialize)]
462pub struct ClobMarketResponse {
463 pub enable_order_book: Option<bool>,
464 pub active: Option<bool>,
465 pub condition_id: String,
466 pub closed: bool,
467 pub archived: Option<bool>,
468 pub accepting_orders: Option<bool>,
469 pub accepting_order_timestamp: Option<String>,
470 #[serde(
471 default,
472 deserialize_with = "deserialize_optional_decimal_from_json_number",
473 serialize_with = "serialize_optional_decimal_as_json_number"
474 )]
475 pub minimum_order_size: Option<Decimal>,
476 #[serde(
477 default,
478 deserialize_with = "deserialize_optional_decimal_from_json_number",
479 serialize_with = "serialize_optional_decimal_as_json_number"
480 )]
481 pub minimum_tick_size: Option<Decimal>,
482 pub question_id: Option<String>,
483 pub question: Option<String>,
484 pub description: Option<String>,
485 pub market_slug: Option<String>,
486 pub end_date_iso: Option<String>,
487 pub game_start_time: Option<String>,
488 pub seconds_delay: Option<i64>,
489 pub fpmm: Option<String>,
490 pub maker_base_fee: Option<i64>,
491 pub taker_base_fee: Option<i64>,
492 pub notifications_enabled: Option<bool>,
493 pub neg_risk: Option<bool>,
494 pub neg_risk_market_id: Option<String>,
495 pub neg_risk_request_id: Option<String>,
496 pub icon: Option<String>,
497 pub image: Option<String>,
498 pub rewards: Option<ClobMarketRewards>,
499 pub is_50_50_outcome: Option<bool>,
500 pub tokens: Vec<ClobMarketToken>,
501 pub tags: Option<Vec<String>>,
502}
503
504#[derive(Clone, Debug, Deserialize)]
506pub struct DataApiPosition {
507 pub asset: String,
508 #[serde(alias = "conditionId", alias = "condition_id")]
509 pub condition_id: String,
510 pub size: Decimal,
511 #[serde(alias = "avgPrice", alias = "avg_price")]
512 pub avg_price: Option<Decimal>,
513}
514
515#[derive(Clone, Debug, Deserialize)]
517#[serde(rename_all = "camelCase")]
518pub struct DataApiTrade {
519 pub proxy_wallet: Option<String>,
520 pub asset: String,
521 pub condition_id: String,
522 pub side: PolymarketOrderSide,
523 #[serde(deserialize_with = "deserialize_decimal_from_json_number")]
524 pub price: Decimal,
525 #[serde(deserialize_with = "deserialize_decimal_from_json_number")]
526 pub size: Decimal,
527 pub timestamp: i64,
528 pub title: Option<String>,
529 pub slug: Option<String>,
530 pub icon: Option<String>,
531 pub event_slug: Option<String>,
532 pub outcome: Option<String>,
533 pub outcome_index: Option<i64>,
534 pub name: Option<String>,
535 pub pseudonym: Option<String>,
536 pub bio: Option<String>,
537 pub profile_image: Option<String>,
538 pub profile_image_optimized: Option<String>,
539 pub transaction_hash: String,
540}
541
542#[cfg(test)]
543mod tests {
544 use rstest::rstest;
545 use rust_decimal_macros::dec;
546
547 use super::*;
548 use crate::common::enums::{PolymarketOrderStatus, PolymarketTradeStatus, SignatureType};
549
550 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
551 let path = format!("test_data/{filename}");
552 let content = std::fs::read_to_string(path).expect("Failed to read test data");
553 serde_json::from_str(&content).expect("Failed to parse test data")
554 }
555
556 #[rstest]
557 fn test_open_order_live_buy_gtc() {
558 let order: PolymarketOpenOrder = load("http_open_order.json");
559
560 assert_eq!(
561 order.id,
562 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
563 );
564 assert_eq!(order.status, PolymarketOrderStatus::Live);
565 assert_eq!(order.side, PolymarketOrderSide::Buy);
566 assert_eq!(order.order_type, PolymarketOrderType::GTC);
567 assert_eq!(order.outcome, PolymarketOutcome::yes());
568 assert_eq!(order.original_size, dec!(100.0000));
569 assert_eq!(order.price, dec!(0.5000));
570 assert_eq!(order.size_matched, dec!(25.0000));
571 assert_eq!(order.created_at, 1703875200);
572 assert!(order.expiration.is_none());
573 assert_eq!(order.associate_trades, Some(vec!["0xabc001".to_string()]));
574 }
575
576 #[rstest]
577 fn test_open_order_matched_sell_fok() {
578 let order: PolymarketOpenOrder = load("http_open_order_sell_fok.json");
579
580 assert_eq!(order.status, PolymarketOrderStatus::Matched);
581 assert_eq!(order.side, PolymarketOrderSide::Sell);
582 assert_eq!(order.order_type, PolymarketOrderType::FOK);
583 assert_eq!(order.outcome, PolymarketOutcome::no());
584 assert_eq!(order.size_matched, dec!(50.0000));
585 assert_eq!(order.expiration, Some("1735689600".to_string()));
586 assert!(order.associate_trades.is_none());
587 }
588
589 #[rstest]
590 fn test_open_order_roundtrip() {
591 let order: PolymarketOpenOrder = load("http_open_order.json");
592 let json = serde_json::to_string(&order).unwrap();
593 let order2: PolymarketOpenOrder = serde_json::from_str(&json).unwrap();
594 assert_eq!(order, order2);
595 }
596
597 #[rstest]
598 fn test_trade_report_fields() {
599 let trade: PolymarketTradeReport = load("http_trade_report.json");
600
601 assert_eq!(trade.id, "trade-0xabcdef1234");
602 assert_eq!(
603 trade.taker_order_id,
604 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
605 );
606 assert_eq!(trade.side, PolymarketOrderSide::Buy);
607 assert_eq!(trade.size, dec!(25.0000));
608 assert_eq!(trade.fee_rate_bps, dec!(0));
609 assert_eq!(trade.price, dec!(0.5000));
610 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
611 assert_eq!(trade.outcome, PolymarketOutcome::yes());
612 assert_eq!(trade.bucket_index, 0);
613 assert_eq!(trade.trader_side, PolymarketLiquiditySide::Taker);
614 assert_eq!(trade.maker_orders.len(), 2);
615 }
616
617 #[rstest]
618 fn test_trade_report_maker_orders() {
619 let trade: PolymarketTradeReport = load("http_trade_report.json");
620
621 let first = &trade.maker_orders[0];
622 assert_eq!(first.matched_amount, dec!(25.0000));
623 assert_eq!(first.price, dec!(0.5000));
624 assert_eq!(first.outcome, PolymarketOutcome::yes());
625
626 let second = &trade.maker_orders[1];
627 assert_eq!(second.matched_amount, dec!(5.0000));
628 }
629
630 #[rstest]
631 fn test_trade_report_roundtrip() {
632 let trade: PolymarketTradeReport = load("http_trade_report.json");
633 let json = serde_json::to_string(&trade).unwrap();
634 let trade2: PolymarketTradeReport = serde_json::from_str(&json).unwrap();
635 assert_eq!(trade, trade2);
636 }
637
638 #[rstest]
639 fn test_signed_order_camel_case_fields() {
640 let order: PolymarketOrder = load("http_signed_order.json");
641
642 assert_eq!(order.salt, 123456789);
643 assert_eq!(order.maker, "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266");
644 assert_eq!(order.maker_amount, dec!(100000000));
645 assert_eq!(order.taker_amount, dec!(50000000));
646 assert_eq!(order.expiration, "0");
647 assert_eq!(order.timestamp, "1713398400000");
648 assert_eq!(
649 order.metadata,
650 "0x0000000000000000000000000000000000000000000000000000000000000000"
651 );
652 assert_eq!(
653 order.builder,
654 "0x0000000000000000000000000000000000000000000000000000000000000000"
655 );
656 assert_eq!(order.side, PolymarketOrderSide::Buy);
657 assert_eq!(order.signature_type, SignatureType::Eoa);
658 }
659
660 #[rstest]
661 fn test_signed_order_roundtrip() {
662 let order: PolymarketOrder = load("http_signed_order.json");
663 let json = serde_json::to_string(&order).unwrap();
664 let order2: PolymarketOrder = serde_json::from_str(&json).unwrap();
665 assert_eq!(order, order2);
666 }
667
668 #[rstest]
669 fn test_signed_order_serializes_camel_case() {
670 let order: PolymarketOrder = load("http_signed_order.json");
671 let json = serde_json::to_string(&order).unwrap();
672
673 assert!(json.contains("\"tokenId\""));
675 assert!(json.contains("\"makerAmount\""));
676 assert!(json.contains("\"takerAmount\""));
677 assert!(json.contains("\"signatureType\""));
678 assert!(json.contains("\"expiration\""));
679 assert!(json.contains("\"timestamp\""));
680 assert!(json.contains("\"metadata\""));
681 assert!(json.contains("\"builder\""));
682 }
683
684 #[rstest]
685 fn test_signed_order_omits_v1_fields() {
686 let order: PolymarketOrder = load("http_signed_order.json");
690 let json = serde_json::to_string(&order).unwrap();
691
692 assert!(
693 !json.contains("\"taker\""),
694 "wire body must not include `taker`: {json}"
695 );
696 assert!(
697 !json.contains("\"nonce\""),
698 "wire body must not include `nonce`: {json}"
699 );
700 assert!(
701 !json.contains("\"feeRateBps\""),
702 "wire body must not include `feeRateBps`: {json}"
703 );
704 }
705
706 #[rstest]
707 fn test_signed_order_v2_docs_example_roundtrips() {
708 let docs_example = r#"{
711 "salt": 12345,
712 "maker": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
713 "signer": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
714 "tokenId": "102936",
715 "makerAmount": "1000000",
716 "takerAmount": "2000000",
717 "side": "BUY",
718 "signatureType": 1,
719 "expiration": "0",
720 "timestamp": "1713398400000",
721 "metadata": "0x0000000000000000000000000000000000000000000000000000000000000000",
722 "builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
723 "signature": "0xdeadbeef"
724 }"#;
725
726 let order: PolymarketOrder = serde_json::from_str(docs_example).unwrap();
727 assert_eq!(order.salt, 12345);
728 assert_eq!(order.token_id.as_str(), "102936");
729 assert_eq!(order.maker_amount, dec!(1000000));
730 assert_eq!(order.taker_amount, dec!(2000000));
731 assert_eq!(order.side, PolymarketOrderSide::Buy);
732 assert_eq!(order.signature_type, SignatureType::PolyProxy);
733 assert_eq!(order.expiration, "0");
734 assert_eq!(order.timestamp, "1713398400000");
735
736 let json = serde_json::to_string(&order).unwrap();
738 let order2: PolymarketOrder = serde_json::from_str(&json).unwrap();
739 assert_eq!(order, order2);
740 }
741
742 #[rstest]
743 fn test_gamma_event_deserialization() {
744 let events: Vec<GammaEvent> = load("gamma_event.json");
745
746 assert_eq!(events.len(), 1);
747 let event = &events[0];
748 assert_eq!(event.id, "30829");
749 assert_eq!(
750 event.slug.as_deref(),
751 Some("democratic-presidential-nominee-2028")
752 );
753 assert_eq!(
754 event.title.as_deref(),
755 Some("Democratic Presidential Nominee 2028")
756 );
757 assert_eq!(event.active, Some(true));
758 assert_eq!(event.closed, Some(false));
759 assert_eq!(event.archived, Some(false));
760 assert_eq!(event.markets.len(), 2);
761 assert_eq!(
762 event.markets[0].condition_id,
763 "0xc8f1cf5d4f26e0fd9c8fe89f2a7b3263b902cf14fde7bfccef525753bb492e47"
764 );
765 assert_eq!(
766 event.markets[1].condition_id,
767 "0xe39adea057926dc197fe30a441f57a340b2a232d5a687010f78bba9b6e02620f"
768 );
769 }
770
771 #[rstest]
772 fn test_gamma_event_empty_markets() {
773 let json = r#"[{"id": "evt-002"}]"#;
774 let events: Vec<GammaEvent> = serde_json::from_str(json).unwrap();
775
776 assert_eq!(events.len(), 1);
777 assert_eq!(events[0].id, "evt-002");
778 assert!(events[0].markets.is_empty());
779 assert!(events[0].slug.is_none());
780 }
781
782 #[rstest]
783 fn test_sports_market_are_weird() {
784 let money_line: GammaMarket = load("gamma_market_sports_market_money_line.json");
785 let map_handicap: GammaMarket = load("gamma_market_sports_market_map_handicap.json");
786
787 assert_eq!(
789 money_line.events.as_ref().unwrap()[0].game_id,
790 map_handicap.events.as_ref().unwrap()[0].game_id
791 );
792
793 assert!(map_handicap.game_id.is_none());
795 assert_eq!(money_line.game_id.as_deref(), Some("1427074"));
796 }
797
798 #[rstest]
799 fn test_gamma_event_composite_sports_game_id() {
800 let events: Vec<GammaEvent> = load("gamma_event_sports_composite_game_id.json");
803
804 assert_eq!(events.len(), 1);
805
806 let event = &events[0];
807
808 assert_eq!(event.id, "835109");
809 assert_eq!(event.game_id.as_deref(), Some("287011684"));
810 assert_eq!(event.markets.len(), 2);
811 assert_eq!(event.markets[0].id, "3524358");
812 assert_eq!(
813 event.markets[0].game_id.as_deref(),
814 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
815 );
816 assert_eq!(event.markets[1].id, "3554041");
817 assert_eq!(event.markets[1].game_id, None);
818
819 let encoded = serde_json::to_value(event).unwrap();
822
823 assert_eq!(encoded["gameId"], serde_json::json!("287011684"));
824 assert_eq!(
825 encoded["markets"][0]["gameId"],
826 serde_json::json!("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
827 );
828 }
829
830 #[rstest]
831 fn test_fee_schedule_decimal_fields() {
832 let market: GammaMarket = load("gamma_market_sports_market_money_line.json");
833 let schedule = market.fee_schedule.unwrap();
834
835 assert_eq!(schedule.exponent, Decimal::ONE);
836 assert_eq!(schedule.rate, dec!(0.03));
837 assert!(schedule.taker_only);
838 assert_eq!(schedule.rebate_rate, dec!(0.25));
839 }
840
841 #[rstest]
842 fn test_gamma_market_crypto_market_config_fields() {
843 let market: GammaMarket = load("gamma_market_crypto_twap.json");
844 let config = market.crypto_market_config.as_ref().unwrap();
845
846 assert_eq!(config.id, "btc-5m-twap-60");
847 assert_eq!(config.asset, "btc");
848 assert_eq!(config.duration, "5m");
849 assert!(config.twap_enabled);
850 assert_eq!(config.twap_lookback_seconds, Some(60));
851 assert_eq!(
852 market.resolution_source.as_deref(),
853 Some("https://data.chain.link/streams/btc-usd-twap-60s-streams")
854 );
855 assert_eq!(
856 market.event_start_time.as_deref(),
857 Some("2026-08-22T16:00:00Z")
858 );
859 }
860
861 #[rstest]
862 fn test_crypto_market_config_absent_twap_lookback_serializes_omitted() {
863 let crypto_market_config = serde_json::json!({
864 "id": "btc-5m",
865 "asset": "btc",
866 "duration": "5m",
867 "twapEnabled": false,
868 });
869
870 let config: CryptoMarketConfig = serde_json::from_value(crypto_market_config).unwrap();
871 let encoded = serde_json::to_value(config).unwrap();
872
873 assert!(encoded.get("twapLookbackSeconds").is_none());
874 }
875
876 #[rstest]
877 fn test_crypto_market_config_rejects_null_twap_lookback() {
878 let crypto_market_config = serde_json::json!({
879 "id": "btc-5m",
880 "asset": "btc",
881 "duration": "5m",
882 "twapEnabled": false,
883 "twapLookbackSeconds": null,
884 });
885
886 let result = serde_json::from_value::<CryptoMarketConfig>(crypto_market_config);
887
888 assert!(result.is_err());
889 }
890
891 #[rstest]
892 #[case(serde_json::json!(-37))]
893 #[case(serde_json::json!(i64::MIN))]
894 #[case(serde_json::json!(i64::MAX))]
895 fn test_crypto_market_config_signed_twap_lookback_roundtrip(
896 #[case] twap_lookback_seconds: serde_json::Value,
897 ) {
898 let crypto_market_config = serde_json::json!({
899 "id": "eth-15m",
900 "asset": "eth",
901 "duration": "15m",
902 "twapEnabled": true,
903 "twapLookbackSeconds": twap_lookback_seconds,
904 });
905
906 let config: CryptoMarketConfig = serde_json::from_value(crypto_market_config).unwrap();
907 let encoded = serde_json::to_value(config).unwrap();
908
909 assert_eq!(encoded["twapLookbackSeconds"], twap_lookback_seconds);
910 }
911
912 #[rstest]
913 fn test_crypto_market_config_rejects_twap_lookback_above_i64_max() {
914 let crypto_market_config = serde_json::json!({
915 "id": "eth-15m",
916 "asset": "eth",
917 "duration": "15m",
918 "twapEnabled": true,
919 "twapLookbackSeconds": 9_223_372_036_854_775_808u64,
920 });
921
922 let result = serde_json::from_value::<CryptoMarketConfig>(crypto_market_config);
923
924 assert!(result.is_err());
925 }
926
927 #[rstest]
928 fn test_gamma_market_enriched_fields() {
929 let market: GammaMarket = load("gamma_market.json");
930
931 assert_eq!(
932 market.event_start_time.as_deref(),
933 Some("2026-03-12T09:20:00Z")
934 );
935 assert_eq!(market.best_bid, Some(0.5));
936 assert_eq!(market.best_ask, Some(0.51));
937 assert_eq!(market.spread, Some(0.009));
938 assert_eq!(market.last_trade_price, Some(0.51));
939 assert!(market.one_day_price_change.is_none());
940 assert!(market.one_week_price_change.is_none());
941 assert_eq!(market.volume_1wk, Some(9.999997));
942 assert_eq!(market.volume_1mo, Some(9.999997));
943 assert_eq!(market.volume_1yr, Some(9.999997));
944 assert_eq!(market.rewards_min_size, Some(50.0));
945 assert_eq!(market.rewards_max_spread, Some(4.5));
946 assert_eq!(market.competitive, Some(0.9999750006249843));
947 assert!(market.category.is_none());
948 assert!(market.neg_risk_market_id.is_none());
949 assert!(market.uma_resolution_status.is_none());
950 assert_eq!(market.uma_resolution_statuses.as_deref(), Some("[]"));
951 assert_eq!(
952 market.outcome_prices.as_deref(),
953 Some("[\"0.505\", \"0.495\"]")
954 );
955 }
956
957 #[rstest]
958 fn test_gamma_market_uma_resolution_statuses() {
959 let market: GammaMarket = load("gamma_market.json");
960
961 assert!(market.uma_resolution_status.is_none());
962 assert_eq!(market.uma_resolution_statuses.as_deref(), Some("[]"));
963 }
964
965 #[rstest]
966 fn test_gamma_market_enriched_fields_default_to_none() {
967 let json = r#"{"id": "m1", "conditionId": "0xcond", "clobTokenIds": "[]", "outcomes": "[]", "question": "Q?"}"#;
969 let market: GammaMarket = serde_json::from_str(json).unwrap();
970
971 assert!(market.best_bid.is_none());
972 assert!(market.spread.is_none());
973 assert!(market.volume_1wk.is_none());
974 assert!(market.rewards_min_size.is_none());
975 assert!(market.competitive.is_none());
976 assert!(market.category.is_none());
977 assert!(market.neg_risk_market_id.is_none());
978 assert!(market.crypto_market_config.is_none());
979 assert!(market.event_start_time.is_none());
980 }
981
982 #[rstest]
983 fn test_gamma_event_enriched_fields() {
984 let events: Vec<GammaEvent> = load("gamma_event.json");
985 let event = &events[0];
986
987 assert_eq!(event.liquidity, Some(43042905.16152));
988 assert_eq!(event.volume, Some(799823812.487094));
989 assert_eq!(event.open_interest, Some(0.0));
990 assert_eq!(event.volume_24hr, Some(5669354.219446001));
991 assert!(event.category.is_none());
992 assert_eq!(event.neg_risk, Some(true));
993 assert_eq!(
994 event.neg_risk_market_id.as_deref(),
995 Some("0x2c3d7e0eee6f058be3006baabf0d54a07da254ba47fe6e3e095e7990c7814700")
996 );
997 assert_eq!(event.featured, Some(false));
998 }
999
1000 #[rstest]
1001 fn test_gamma_tag_deserialization() {
1002 let tags: Vec<GammaTag> = load("gamma_tags.json");
1003
1004 assert_eq!(tags.len(), 5);
1005 assert_eq!(tags[0].id, "101259");
1006 assert_eq!(tags[0].label.as_deref(), Some("Health and Human Services"));
1007 assert_eq!(tags[0].slug.as_deref(), Some("health-and-human-services"));
1008 assert_eq!(tags[2].slug.as_deref(), Some("attorney-general"));
1009 }
1010
1011 #[rstest]
1012 fn test_search_response_deserialization() {
1013 let response: SearchResponse = load("search_response.json");
1014
1015 assert!(response.markets.is_none());
1017
1018 let events = response.events.as_ref().unwrap();
1019 assert_eq!(events.len(), 1);
1020 assert_eq!(events[0].slug.as_deref(), Some("bitcoin-above-on-march-11"));
1021 assert_eq!(events[0].markets.len(), 1);
1022 }
1023
1024 #[rstest]
1025 fn test_search_response_empty_fields() {
1026 let json = "{}";
1027 let response: SearchResponse = serde_json::from_str(json).unwrap();
1028 assert!(response.markets.is_none());
1029 assert!(response.events.is_none());
1030 }
1031
1032 #[rstest]
1033 fn test_clob_book_response_deserialization() {
1034 let response: ClobBookResponse = load("clob_book_response.json");
1035
1036 assert_eq!(response.bids.len(), 3);
1037 assert_eq!(response.asks.len(), 3);
1038
1039 assert_eq!(response.bids[0].price, "0.48");
1040 assert_eq!(response.bids[0].size, "100.00");
1041 assert_eq!(response.bids[2].price, "0.50");
1042 assert_eq!(response.bids[2].size, "150.00");
1043
1044 assert_eq!(response.asks[0].price, "0.51");
1045 assert_eq!(response.asks[0].size, "120.00");
1046 assert_eq!(response.asks[2].price, "0.53");
1047 assert_eq!(response.asks[2].size, "90.00");
1048 }
1049
1050 #[rstest]
1051 fn test_clob_book_response_ignores_extra_fields() {
1052 let json = r#"{
1058 "market": "0xabc",
1059 "asset_id": "123",
1060 "hash": "0x1",
1061 "timestamp": "123",
1062 "bids": [],
1063 "asks": [],
1064 "tick_size": "0.01",
1065 "min_order_size": "5",
1066 "neg_risk": false,
1067 "last_trade_price": "0.55"
1068 }"#;
1069 let response: ClobBookResponse = serde_json::from_str(json).unwrap();
1070 assert!(response.bids.is_empty());
1071 assert!(response.asks.is_empty());
1072 }
1073
1074 #[rstest]
1075 fn test_clob_market_response_captured_fields() {
1076 let response: ClobMarketResponse = load("clob_market_response.json");
1077 let raw: serde_json::Value = load("clob_market_response.json");
1078
1079 assert_eq!(response.enable_order_book, Some(true));
1080 assert_eq!(response.active, Some(true));
1081 assert!(!response.closed);
1082 assert_eq!(response.archived, Some(false));
1083 assert_eq!(response.accepting_orders, Some(true));
1084 assert_eq!(
1085 response.accepting_order_timestamp.as_deref(),
1086 Some("2026-08-01T22:56:49Z")
1087 );
1088 assert_eq!(response.minimum_order_size, Some(dec!(5)));
1089 assert_eq!(response.minimum_tick_size, Some(dec!(0.01)));
1090 assert_eq!(
1091 response.condition_id,
1092 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1093 );
1094 assert_eq!(
1095 response.question_id.as_deref(),
1096 Some("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd")
1097 );
1098 assert_eq!(
1099 response.question.as_deref(),
1100 Some("LoL: T1 vs Hanwha Life Esports (BO3) - LCK Round 3-4 Legend Group")
1101 );
1102 assert_eq!(response.description.as_deref(), raw["description"].as_str());
1103 assert_eq!(
1104 response.market_slug.as_deref(),
1105 Some("sanitized-clob-market")
1106 );
1107 assert_eq!(
1108 response.end_date_iso.as_deref(),
1109 Some("2026-08-08T00:00:00Z")
1110 );
1111 assert_eq!(
1112 response.game_start_time.as_deref(),
1113 Some("2026-08-08T08:00:00Z")
1114 );
1115 assert_eq!(response.seconds_delay, Some(1));
1116 assert_eq!(response.fpmm.as_deref(), Some(""));
1117 assert_eq!(response.maker_base_fee, Some(1000));
1118 assert_eq!(response.taker_base_fee, Some(1000));
1119 assert_eq!(response.notifications_enabled, Some(true));
1120 assert_eq!(response.neg_risk, Some(false));
1121 assert_eq!(response.neg_risk_market_id.as_deref(), Some(""));
1122 assert_eq!(response.neg_risk_request_id.as_deref(), Some(""));
1123 assert_eq!(
1124 response.icon.as_deref(),
1125 Some("https://example.com/sanitized-market.png")
1126 );
1127 assert_eq!(
1128 response.image.as_deref(),
1129 Some("https://example.com/sanitized-market.png")
1130 );
1131 let rewards = response.rewards.as_ref().expect("captured rewards");
1132 assert!(rewards.rates.is_none());
1133 assert_eq!(rewards.min_size, Some(dec!(50)));
1134 assert_eq!(rewards.max_spread, Some(dec!(4.5)));
1135 assert_eq!(response.is_50_50_outcome, Some(false));
1136 assert_eq!(response.tokens.len(), 2);
1137 assert_eq!(
1138 response.tokens[0].token_id,
1139 "10000000000000000000000000000000000000000000000000000000000000000000000000001"
1140 );
1141 assert_eq!(response.tokens[0].outcome, "T1");
1142 assert_eq!(response.tokens[0].price, Some(dec!(0.715)));
1143 assert!(!response.tokens[0].winner);
1144 assert_eq!(
1145 response.tokens[1].token_id,
1146 "10000000000000000000000000000000000000000000000000000000000000000000000000002"
1147 );
1148 assert_eq!(response.tokens[1].outcome, "Hanwha Life Esports");
1149 assert_eq!(response.tokens[1].price, Some(dec!(0.285)));
1150 assert!(!response.tokens[1].winner);
1151 assert_eq!(
1152 response.tags.as_deref(),
1153 Some(
1154 &[
1155 "Sports".to_string(),
1156 "Esports".to_string(),
1157 "league of legends".to_string(),
1158 "Games".to_string(),
1159 ][..]
1160 )
1161 );
1162 }
1163
1164 #[rstest]
1165 fn test_clob_market_rewards_documented_rate_fields() {
1166 let json = r#"{
1168 "rates":[{"asset_address":"0x1111111111111111111111111111111111111111","rewards_daily_rate":12.5}],
1169 "min_size":25,
1170 "max_spread":3.5
1171 }"#;
1172 let rewards: ClobMarketRewards = serde_json::from_str(json).unwrap();
1173
1174 let rates = rewards.rates.as_deref().expect("documented reward rate");
1175 assert_eq!(rates.len(), 1);
1176 assert_eq!(
1177 rates[0].asset_address,
1178 "0x1111111111111111111111111111111111111111"
1179 );
1180 assert_eq!(rates[0].rewards_daily_rate, dec!(12.5));
1181 assert_eq!(rewards.min_size, Some(dec!(25)));
1182 assert_eq!(rewards.max_spread, Some(dec!(3.5)));
1183 }
1184
1185 #[rstest]
1186 fn test_clob_market_decimal_fields_preserve_precision() {
1187 let json = r#"{
1188 "condition_id":"0xcondition",
1189 "closed":false,
1190 "minimum_order_size":123456789.1234567890123456789,
1191 "minimum_tick_size":0.1234567890123456789012345678,
1192 "rewards":{
1193 "rates":[{
1194 "asset_address":"0x1111111111111111111111111111111111111111",
1195 "rewards_daily_rate":0.1234567890123456789012345678
1196 }],
1197 "min_size":123456789.1234567890123456789,
1198 "max_spread":0.1234567890123456789012345678
1199 },
1200 "tokens":[{
1201 "token_id":"token-1",
1202 "outcome":"Yes",
1203 "price":0.1234567890123456789012345678,
1204 "winner":false
1205 }]
1206 }"#;
1207 let market: ClobMarketResponse = serde_json::from_str(json).unwrap();
1208 let precise = Decimal::from_str_exact("0.1234567890123456789012345678").unwrap();
1209 let large = Decimal::from_str_exact("123456789.1234567890123456789").unwrap();
1210
1211 assert_eq!(market.minimum_order_size, Some(large));
1212 assert_eq!(market.minimum_tick_size, Some(precise));
1213 let rewards = market.rewards.as_ref().unwrap();
1214 assert_eq!(
1215 rewards.rates.as_ref().unwrap()[0].rewards_daily_rate,
1216 precise
1217 );
1218 assert_eq!(rewards.min_size, Some(large));
1219 assert_eq!(rewards.max_spread, Some(precise));
1220 assert_eq!(market.tokens[0].price, Some(precise));
1221 let serialized = serde_json::to_string(&market).unwrap();
1222 assert!(serialized.contains("\"minimum_order_size\":123456789.1234567890123456789"));
1223 assert!(serialized.contains("\"minimum_tick_size\":0.1234567890123456789012345678"));
1224 assert!(serialized.contains("\"rewards_daily_rate\":0.1234567890123456789012345678"));
1225 assert!(serialized.contains("\"min_size\":123456789.1234567890123456789"));
1226 assert!(serialized.contains("\"max_spread\":0.1234567890123456789012345678"));
1227 assert!(serialized.contains("\"price\":0.1234567890123456789012345678"));
1228 }
1229
1230 #[rstest]
1231 fn test_clob_market_response_deserialization_accepting_false() {
1232 let response: ClobMarketResponse = load("clob_market_closed_binary_accepting_false.json");
1233 assert_eq!(
1234 response.condition_id,
1235 "0x8ccc3f4951ff02c1d34b87988752b4444ad17228732780a6cf22afefe8478bb6"
1236 );
1237 assert!(response.closed);
1238 assert_eq!(response.tokens.len(), 2);
1239 assert_eq!(response.tokens[0].outcome, "Yes");
1240 assert!(!response.tokens[0].winner);
1241 assert_eq!(response.tokens[1].outcome, "No");
1242 assert!(response.tokens[1].winner);
1243 }
1244
1245 #[rstest]
1246 fn test_clob_market_response_deserialization_accepting_true() {
1247 let response: ClobMarketResponse = load("clob_market_closed_binary_accepting_true.json");
1248 assert_eq!(
1249 response.condition_id,
1250 "0xd57eed0d44f5b8ca54925d8d6ff440b146b3e6e071da18136ee3ee572d34479e"
1251 );
1252 assert!(response.closed);
1253 assert_eq!(response.tokens.len(), 2);
1254 assert_eq!(response.tokens[0].outcome, "Yes");
1255 assert!(response.tokens[0].winner);
1256 assert_eq!(response.tokens[1].outcome, "No");
1257 assert!(!response.tokens[1].winner);
1258 }
1259
1260 #[rstest]
1261 fn test_tick_size_response_preserves_json_number() {
1262 let response: TickSizeResponse =
1263 serde_json::from_str(r#"{"minimum_tick_size":0.1234567890123456789012345678}"#)
1264 .unwrap();
1265 let precise =
1266 rust_decimal::Decimal::from_str_exact("0.1234567890123456789012345678").unwrap();
1267
1268 assert_eq!(response.minimum_tick_size, precise);
1269 }
1270
1271 #[rstest]
1272 fn test_fee_rate_response_zero() {
1273 let response: FeeRateResponse = load("clob_fee_rate_response_zero.json");
1274 assert_eq!(response.base_fee, dec!(0));
1275 }
1276
1277 #[rstest]
1278 fn test_fee_rate_response_nonzero() {
1279 let response: FeeRateResponse = load("clob_fee_rate_response_nonzero.json");
1280 assert_eq!(response.base_fee, dec!(150));
1281 }
1282
1283 #[rstest]
1284 fn test_data_api_position_deserialization() {
1285 let positions: Vec<DataApiPosition> = load("data_api_positions_response.json");
1286
1287 assert_eq!(positions.len(), 4);
1288 assert_eq!(
1289 positions[0].asset,
1290 "71321045863084981365469005770620412523470745398083994982746259498689308907982"
1291 );
1292 assert_eq!(
1293 positions[0].condition_id,
1294 "0xc8f1cf5d4f26e0fd9c8fe89f2a7b3263b902cf14fde7bfccef525753bb492e47"
1295 );
1296 assert_eq!(positions[0].size, dec!(150.5));
1297 assert_eq!(positions[0].avg_price, Some(dec!(0.55)));
1298
1299 assert_eq!(positions[1].size, dec!(0));
1301 assert_eq!(positions[1].avg_price, Some(dec!(0.45)));
1302
1303 assert_eq!(
1305 positions[2].condition_id,
1306 "0xabc123def456789012345678901234567890abcdef1234567890abcdef123456"
1307 );
1308 assert_eq!(positions[2].size, dec!(42));
1309 assert_eq!(positions[2].avg_price, Some(dec!(0.3)));
1310
1311 assert_eq!(positions[3].size, dec!(0.005));
1313 assert_eq!(positions[3].avg_price, Some(dec!(0.7)));
1314 }
1315
1316 #[rstest]
1317 fn test_data_api_trade_deserialization() {
1318 let trades: Vec<DataApiTrade> = load("data_api_trades_captured_response.json");
1319
1320 assert_eq!(trades.len(), 3);
1321 assert_eq!(
1322 trades[0].asset,
1323 "10000000000000000000000000000000000000000000000000000000000000000000000000001"
1324 );
1325 assert_eq!(
1326 trades[0].condition_id,
1327 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1328 );
1329 assert_eq!(trades[0].side, PolymarketOrderSide::Sell);
1330 assert_eq!(trades[0].price, dec!(0.7));
1331 assert_eq!(trades[0].size, dec!(92.59));
1332 assert_eq!(trades[0].timestamp, 1786179735);
1333 assert_eq!(
1334 trades[0].transaction_hash,
1335 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1336 );
1337
1338 assert_eq!(trades[1].asset, trades[0].asset);
1339 assert_eq!(trades[1].condition_id, trades[0].condition_id);
1340 assert_eq!(trades[1].side, PolymarketOrderSide::Buy);
1341 assert_eq!(trades[1].price, dec!(0.709999959));
1342 assert_eq!(trades[1].size, dec!(1.464786));
1343 assert_eq!(trades[1].timestamp, 1786179730);
1344 assert_eq!(
1345 trades[1].transaction_hash,
1346 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1347 );
1348 assert_eq!(
1349 trades[2].asset,
1350 "10000000000000000000000000000000000000000000000000000000000000000000000000002"
1351 );
1352 assert_eq!(trades[2].condition_id, trades[0].condition_id);
1353 assert_eq!(trades[2].side, PolymarketOrderSide::Buy);
1354 assert_eq!(trades[2].price, dec!(0.2972581967));
1355 assert_eq!(trades[2].size, dec!(244));
1356 assert_eq!(trades[2].timestamp, 1786179726);
1357 assert_eq!(
1358 trades[2].transaction_hash,
1359 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1360 );
1361
1362 for (trade, outcome, outcome_index) in [
1363 (&trades[0], "T1", 0),
1364 (&trades[1], "T1", 0),
1365 (&trades[2], "Hanwha Life Esports", 1),
1366 ] {
1367 assert_eq!(
1368 trade.proxy_wallet.as_deref(),
1369 Some("0x1111111111111111111111111111111111111111")
1370 );
1371 assert_eq!(
1372 trade.title.as_deref(),
1373 Some("LoL: T1 vs Hanwha Life Esports (BO3) - LCK Round 3-4 Legend Group")
1374 );
1375 assert_eq!(trade.slug.as_deref(), Some("sanitized-market"));
1376 assert_eq!(
1377 trade.icon.as_deref(),
1378 Some("https://example.com/sanitized-market.png")
1379 );
1380 assert_eq!(trade.event_slug.as_deref(), Some("sanitized-event"));
1381 assert_eq!(trade.outcome.as_deref(), Some(outcome));
1382 assert_eq!(trade.outcome_index, Some(outcome_index));
1383 assert_eq!(trade.name.as_deref(), Some("Sanitized trader"));
1384 assert_eq!(trade.pseudonym.as_deref(), Some("sanitized-trader"));
1385 assert_eq!(trade.bio.as_deref(), Some("Sanitized profile"));
1386 assert_eq!(
1387 trade.profile_image.as_deref(),
1388 Some("https://example.com/sanitized-profile.png")
1389 );
1390 assert_eq!(
1391 trade.profile_image_optimized.as_deref(),
1392 Some("https://example.com/sanitized-profile-optimized.png")
1393 );
1394 }
1395 }
1396
1397 #[rstest]
1398 fn test_data_api_trade_decimal_fields_preserve_precision() {
1399 let json = r#"{
1400 "asset":"token-1",
1401 "conditionId":"0xcondition",
1402 "side":"BUY",
1403 "price":0.1234567890123456789012345678,
1404 "size":123456789.1234567890123456789,
1405 "timestamp":1786179735,
1406 "transactionHash":"0xtransaction"
1407 }"#;
1408 let trade: DataApiTrade = serde_json::from_str(json).unwrap();
1409
1410 assert_eq!(
1411 trade.price,
1412 Decimal::from_str_exact("0.1234567890123456789012345678").unwrap()
1413 );
1414 assert_eq!(
1415 trade.size,
1416 Decimal::from_str_exact("123456789.1234567890123456789").unwrap()
1417 );
1418 }
1419}