1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[cfg_attr(feature = "specta", derive(specta::Type))]
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub struct Market {
11 pub id: String,
12 pub condition_id: String,
13 #[serde(rename = "questionID")]
14 pub question_id: Option<String>,
15 pub slug: Option<String>,
16 #[serde(default)]
17 pub tokens: Vec<MarketToken>,
18 #[cfg_attr(feature = "specta", specta(type = Option<HashMap<String, String>>))]
19 pub rewards: Option<HashMap<String, serde_json::Value>>,
20 pub minimum_order_size: Option<String>,
21 pub minimum_tick_size: Option<String>,
22 pub description: String,
23 pub category: Option<String>,
24 pub end_date_iso: Option<String>,
25 pub start_date_iso: Option<String>,
26 pub question: String,
27 pub min_incentive_size: Option<String>,
28 pub max_incentive_spread: Option<String>,
29 #[serde(rename = "submitted_by")]
30 pub submitted_by: Option<String>,
31 #[serde(rename = "volume24hr")] pub volume_24hr: Option<f64>,
33 #[serde(rename = "volume1wk")] pub volume_1wk: Option<f64>,
35 #[serde(rename = "volume1mo")] pub volume_1mo: Option<f64>,
37 #[serde(rename = "volume1yr")] pub volume_1yr: Option<f64>,
39 pub liquidity: Option<String>,
40 #[serde(default)]
41 pub tags: Vec<Tag>,
42 pub neg_risk: Option<bool>,
43 pub neg_risk_market_id: Option<String>,
44 pub neg_risk_request_id: Option<String>,
45 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
47 pub comment_count: Option<i64>,
48 pub twitter_card_image: Option<String>,
49 pub resolution_source: Option<String>,
50 pub amm_type: Option<String>,
51 pub sponsor_name: Option<String>,
52 pub sponsor_image: Option<String>,
53 pub x_axis_value: Option<String>,
54 pub y_axis_value: Option<String>,
55 #[serde(rename = "denomationToken")]
56 pub denomination_token: Option<String>,
57 pub fee: Option<String>,
58 pub image: Option<String>,
59 pub icon: Option<String>,
60 pub lower_bound: Option<String>,
61 pub upper_bound: Option<String>,
62 pub outcomes: Option<String>,
63 pub outcome_prices: Option<String>,
64 pub volume: Option<String>,
65 pub active: Option<bool>,
66 pub market_type: Option<String>,
67 pub format_type: Option<String>,
68 pub lower_bound_date: Option<String>,
69 pub upper_bound_date: Option<String>,
70 pub closed: Option<bool>,
71 pub market_maker_address: String,
72 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
73 pub created_by: Option<i64>,
74 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
75 pub updated_by: Option<i64>,
76 pub created_at: Option<String>,
77 pub updated_at: Option<String>,
78 pub closed_time: Option<String>,
79 pub wide_format: Option<bool>,
80 pub new: Option<bool>,
81 pub mailchimp_tag: Option<String>,
82 pub featured: Option<bool>,
83 pub archived: Option<bool>,
84 pub resolved_by: Option<String>,
85 pub restricted: Option<bool>,
86 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
87 pub market_group: Option<i64>,
88 pub group_item_title: Option<String>,
89 pub group_item_threshold: Option<String>,
90 pub uma_end_date: Option<String>,
91 pub uma_resolution_status: Option<String>,
92 pub uma_end_date_iso: Option<String>,
93 pub uma_resolution_statuses: Option<String>,
94 pub enable_order_book: Option<bool>,
95 pub order_price_min_tick_size: Option<f64>,
96 pub order_min_size: Option<f64>,
97 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
98 pub curation_order: Option<i64>,
99 pub volume_num: Option<f64>,
100 pub liquidity_num: Option<f64>,
101 pub has_reviewed_dates: Option<bool>,
102 pub ready_for_cron: Option<bool>,
103 pub comments_enabled: Option<bool>,
104 pub game_start_time: Option<String>,
105 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
106 pub seconds_delay: Option<i64>,
107 pub clob_token_ids: Option<String>,
108 pub disqus_thread: Option<String>,
109 pub short_outcomes: Option<String>,
110 pub team_aid: Option<String>,
111 pub team_bid: Option<String>,
112 pub uma_bond: Option<String>,
113 pub uma_reward: Option<String>,
114 pub fpmm_live: Option<bool>,
115 #[serde(rename = "volume24hrAmm")] pub volume_24hr_amm: Option<f64>,
117 #[serde(rename = "volume1wkAmm")]
118 pub volume_1wk_amm: Option<f64>,
119 #[serde(rename = "volume1moAmm")]
120 pub volume_1mo_amm: Option<f64>,
121 #[serde(rename = "volume1yrAmm")]
122 pub volume_1yr_amm: Option<f64>,
123 #[serde(rename = "volume24hrClob")]
124 pub volume_24hr_clob: Option<f64>,
125 #[serde(rename = "volume1wkClob")]
126 pub volume_1wk_clob: Option<f64>,
127 #[serde(rename = "volume1moClob")]
128 pub volume_1mo_clob: Option<f64>,
129 #[serde(rename = "volume1yrClob")]
130 pub volume_1yr_clob: Option<f64>,
131 pub volume_amm: Option<f64>,
132 pub volume_clob: Option<f64>,
133 pub liquidity_amm: Option<f64>,
134 pub liquidity_clob: Option<f64>,
135 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
136 pub maker_base_fee: Option<i64>,
137 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
138 pub taker_base_fee: Option<i64>,
139 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
140 pub custom_liveness: Option<i64>,
141 pub accepting_orders: Option<bool>,
142 pub notifications_enabled: Option<bool>,
143 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
144 pub score: Option<i64>,
145 pub creator: Option<String>,
146 pub ready: Option<bool>,
147 pub funded: Option<bool>,
148 pub past_slugs: Option<String>,
149 pub ready_timestamp: Option<String>,
150 pub funded_timestamp: Option<String>,
151 pub accepting_orders_timestamp: Option<String>,
152 pub competitive: Option<f64>,
153 pub rewards_min_size: Option<f64>,
154 pub rewards_max_spread: Option<f64>,
155 pub spread: Option<f64>,
156 pub automatically_resolved: Option<bool>,
157 pub automatically_active: Option<bool>,
158 pub one_day_price_change: Option<f64>,
159 pub one_hour_price_change: Option<f64>,
160 pub one_week_price_change: Option<f64>,
161 pub one_month_price_change: Option<f64>,
162 pub one_year_price_change: Option<f64>,
163 pub last_trade_price: Option<f64>,
164 pub best_bid: Option<f64>,
165 pub best_ask: Option<f64>,
166 pub clear_book_on_start: Option<bool>,
167 pub chart_color: Option<String>,
168 pub series_color: Option<String>,
169 pub show_gmp_series: Option<bool>,
170 pub show_gmp_outcome: Option<bool>,
171 pub manual_activation: Option<bool>,
172 pub neg_risk_other: Option<bool>,
173 pub game_id: Option<String>,
174 pub group_item_range: Option<String>,
175 pub sports_market_type: Option<String>,
176 pub line: Option<f64>,
177 pub pending_deployment: Option<bool>,
178 pub deploying: Option<bool>,
179 pub deploying_timestamp: Option<String>,
180 pub schedule_deployment_timestamp: Option<String>,
181 pub rfq_enabled: Option<bool>,
182 pub event_start_time: Option<String>,
183}
184
185#[cfg_attr(feature = "specta", derive(specta::Type))]
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
188#[serde(rename_all = "camelCase")]
189pub struct MarketToken {
190 pub token_id: String,
191 pub outcome: String,
192 pub price: Option<String>,
193 pub winner: Option<bool>,
194}
195
196#[cfg_attr(feature = "specta", derive(specta::Type))]
197#[derive(Debug, Clone, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct Event {
200 pub id: String,
201 pub ticker: Option<String>,
202 pub slug: Option<String>,
203 pub title: Option<String>,
204 pub subtitle: Option<String>,
205 pub description: Option<String>,
206 pub resolution_source: Option<String>,
207 pub start_date: Option<String>,
208 pub creation_date: Option<String>,
209 pub end_date: Option<String>,
210 pub image: Option<String>,
211 pub icon: Option<String>,
212 pub start_date_iso: Option<String>,
213 pub end_date_iso: Option<String>,
214 pub active: Option<bool>,
215 pub closed: Option<bool>,
216 pub archived: Option<bool>,
217 pub new: Option<bool>,
218 pub featured: Option<bool>,
219 pub restricted: Option<bool>,
220 pub liquidity: Option<f64>,
221 pub open_interest: Option<f64>,
222 pub sort_by: Option<String>,
223 pub category: Option<String>,
224 pub subcategory: Option<String>,
225 pub is_template: Option<bool>,
226 pub template_variables: Option<String>,
227 #[serde(rename = "published_at")]
228 pub published_at: Option<String>,
229 pub created_by: Option<String>,
230 pub updated_by: Option<String>,
231 pub created_at: Option<String>,
232 pub updated_at: Option<String>,
233 pub comments_enabled: Option<bool>,
234 pub competitive: Option<f64>,
235 #[serde(rename = "volume24hr")]
236 pub volume_24hr: Option<f64>,
237 #[serde(rename = "volume1wk")]
238 pub volume_1wk: Option<f64>,
239 #[serde(rename = "volume1mo")]
240 pub volume_1mo: Option<f64>,
241 #[serde(rename = "volume1yr")]
242 pub volume_1yr: Option<f64>,
243 pub featured_image: Option<String>,
244 pub disqus_thread: Option<String>,
245 pub parent_event: Option<String>,
246 pub enable_order_book: Option<bool>,
247 pub liquidity_amm: Option<f64>,
248 pub liquidity_clob: Option<f64>,
249 pub neg_risk: Option<bool>,
250 pub neg_risk_market_id: Option<String>,
251 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
252 pub neg_risk_fee_bips: Option<i64>,
253 #[serde(default)]
254 pub sub_events: Vec<String>,
255 #[serde(default)]
256 pub markets: Vec<Market>,
257 #[serde(default)]
258 pub tags: Vec<Tag>,
259 #[serde(default)]
260 pub series: Vec<SeriesInfo>,
261 pub cyom: Option<bool>,
262 pub closed_time: Option<String>,
263 pub show_all_outcomes: Option<bool>,
264 pub show_market_images: Option<bool>,
265 pub automatically_resolved: Option<bool>,
266 #[serde(rename = "enableNegRisk")]
267 pub enable_neg_risk: Option<bool>,
268 pub automatically_active: Option<bool>,
269 pub event_date: Option<String>,
270 pub start_time: Option<String>,
271 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
272 pub event_week: Option<i64>,
273 pub series_slug: Option<String>,
274 pub score: Option<String>,
275 pub elapsed: Option<String>,
276 pub period: Option<String>,
277 pub live: Option<bool>,
278 pub ended: Option<bool>,
279 pub finished_timestamp: Option<String>,
280 pub gmp_chart_mode: Option<String>,
281 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
282 pub tweet_count: Option<i64>,
283 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
284 pub featured_order: Option<i64>,
285 pub estimate_value: Option<bool>,
286 pub cant_estimate: Option<bool>,
287 pub spreads_main_line: Option<f64>,
288 pub totals_main_line: Option<f64>,
289 pub carousel_map: Option<String>,
290 pub pending_deployment: Option<bool>,
291 pub deploying: Option<bool>,
292 pub deploying_timestamp: Option<String>,
293 pub schedule_deployment_timestamp: Option<String>,
294 pub game_status: Option<String>,
295}
296
297#[cfg_attr(feature = "specta", derive(specta::Type))]
299#[derive(Debug, Clone, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct SeriesInfo {
302 pub id: String,
303 pub slug: String,
304 pub title: String,
305 pub ticker: Option<String>,
306 pub series_type: Option<String>,
307 pub recurrence: Option<String>,
308 pub image: Option<String>,
309 pub icon: Option<String>,
310 pub layout: Option<String>,
311 pub active: Option<bool>,
312 pub closed: Option<bool>,
313 pub archived: Option<bool>,
314 pub new: Option<bool>,
315 pub featured: Option<bool>,
316 pub restricted: Option<bool>,
317 pub published_at: Option<String>,
318 pub created_by: Option<String>,
319 pub updated_by: Option<String>,
320 pub created_at: Option<String>,
321 pub updated_at: Option<String>,
322 pub comments_enabled: Option<bool>,
323 pub competitive: Option<String>,
324 #[serde(rename = "volume24hr")]
325 pub volume_24hr: Option<f64>,
326 pub start_date: Option<String>,
327 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
328 pub comment_count: Option<i64>,
329 pub requires_translation: Option<bool>,
330}
331
332#[cfg_attr(feature = "specta", derive(specta::Type))]
334#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase")]
336pub struct SeriesData {
337 pub id: String,
338 pub slug: String,
339 pub title: String,
340 pub description: Option<String>,
341 pub image: Option<String>,
342 pub icon: Option<String>,
343 pub active: bool,
344 pub closed: bool,
345 pub archived: bool,
346 #[serde(default)]
347 pub tags: Vec<String>,
348 pub volume: Option<f64>,
349 pub liquidity: Option<f64>,
350 #[serde(default)]
351 pub events: Vec<Event>,
352 pub competitive: Option<String>,
353}
354
355#[cfg_attr(feature = "specta", derive(specta::Type))]
357#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
358#[serde(rename_all = "camelCase")]
359pub struct Tag {
360 pub id: String,
361 pub slug: String,
362 pub label: String,
363 pub force_show: Option<bool>,
364 pub published_at: Option<String>,
365 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
366 pub created_by: Option<u64>,
367 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
368 pub updated_by: Option<u64>,
369 pub created_at: Option<String>,
370 pub updated_at: Option<String>,
371 pub force_hide: Option<bool>,
372 pub is_carousel: Option<bool>,
373}
374
375#[cfg_attr(feature = "specta", derive(specta::Type))]
377#[derive(Debug, Clone, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct SportMetadata {
380 #[cfg_attr(feature = "specta", specta(type = f64))]
381 pub id: u64,
382 pub sport: String,
383 pub image: Option<String>,
384 pub resolution: Option<String>,
385 pub ordering: Option<String>,
386 pub tags: Option<String>,
387 pub series: Option<String>,
388 pub created_at: Option<String>,
389}
390
391#[cfg_attr(feature = "specta", derive(specta::Type))]
393#[derive(Debug, Clone, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct Team {
396 #[cfg_attr(feature = "specta", specta(type = f64))]
397 pub id: i64,
398 pub name: Option<String>,
399 pub league: Option<String>,
400 pub record: Option<String>,
401 pub logo: Option<String>,
402 pub abbreviation: Option<String>,
403 pub alias: Option<String>,
404 pub created_at: Option<DateTime<Utc>>,
405 pub updated_at: Option<DateTime<Utc>>,
406}
407
408#[cfg_attr(feature = "specta", derive(specta::Type))]
410#[derive(Debug, Clone, Serialize, Deserialize)]
411#[serde(rename_all = "camelCase")]
412pub struct Comment {
413 pub id: String,
414 pub body: String,
415 pub created_at: DateTime<Utc>,
416 pub updated_at: DateTime<Utc>,
417 pub deleted_at: Option<DateTime<Utc>>,
418 pub user: CommentUser,
419 pub market_id: Option<String>,
420 pub event_id: Option<String>,
421 pub series_id: Option<String>,
422 pub parent_id: Option<String>,
423 #[serde(default)]
424 pub reactions: Vec<CommentReaction>,
425 #[serde(default)]
426 pub positions: Vec<CommentPosition>,
427 #[cfg_attr(feature = "specta", specta(type = f64))]
428 pub like_count: u32,
429 #[cfg_attr(feature = "specta", specta(type = f64))]
430 pub dislike_count: u32,
431 #[cfg_attr(feature = "specta", specta(type = f64))]
432 pub reply_count: u32,
433}
434
435#[cfg_attr(feature = "specta", derive(specta::Type))]
437#[derive(Debug, Clone, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct CommentUser {
440 pub id: String,
441 pub name: String,
442 pub avatar: Option<String>,
443}
444
445#[cfg_attr(feature = "specta", derive(specta::Type))]
447#[derive(Debug, Clone, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct CommentReaction {
450 pub user_id: String,
451 pub reaction_type: String,
452}
453
454#[cfg_attr(feature = "specta", derive(specta::Type))]
456#[derive(Debug, Clone, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase")]
458pub struct CommentPosition {
459 pub token_id: String,
460 pub outcome: String,
461 pub shares: String,
462}
463
464#[cfg_attr(feature = "specta", derive(specta::Type))]
466#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct CountResponse {
469 #[cfg_attr(feature = "specta", specta(type = f64))]
470 pub count: u64,
471}
472
473#[cfg_attr(feature = "specta", derive(specta::Type))]
475#[derive(Debug, Clone, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct Cursor {
478 pub next_cursor: Option<String>,
479}
480
481#[cfg_attr(feature = "specta", derive(specta::Type))]
483#[derive(Debug, Clone, Serialize, Deserialize)]
484#[serde(rename_all = "camelCase")]
485pub struct PaginatedResponse<T> {
486 pub data: Vec<T>,
487 pub next_cursor: Option<String>,
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
497 fn test_market_token_deserialization() {
498 let json = r#"{
499 "tokenId": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
500 "outcome": "Yes",
501 "price": "0.55",
502 "winner": false
503 }"#;
504 let token: MarketToken = serde_json::from_str(json).unwrap();
505 assert_eq!(token.outcome, "Yes");
506 assert_eq!(token.price.as_deref(), Some("0.55"));
507 assert_eq!(token.winner, Some(false));
508 }
509
510 #[test]
511 fn test_market_token_optional_fields() {
512 let json = r#"{"tokenId": "123", "outcome": "No"}"#;
513 let token: MarketToken = serde_json::from_str(json).unwrap();
514 assert!(token.price.is_none());
515 assert!(token.winner.is_none());
516 }
517
518 #[test]
521 fn test_tag_deserialization() {
522 let json = r#"{
523 "id": "42",
524 "slug": "politics",
525 "label": "Politics",
526 "forceShow": true,
527 "publishedAt": "2024-01-01T00:00:00Z",
528 "createdBy": 1,
529 "updatedBy": 2,
530 "createdAt": "2024-01-01T00:00:00Z",
531 "updatedAt": "2024-06-01T00:00:00Z",
532 "forceHide": false,
533 "isCarousel": true
534 }"#;
535 let tag: Tag = serde_json::from_str(json).unwrap();
536 assert_eq!(tag.slug, "politics");
537 assert_eq!(tag.force_show, Some(true));
538 assert_eq!(tag.is_carousel, Some(true));
539 }
540
541 #[test]
542 fn test_tag_minimal() {
543 let json = r#"{"id": "1", "slug": "test", "label": "Test"}"#;
544 let tag: Tag = serde_json::from_str(json).unwrap();
545 assert_eq!(tag.label, "Test");
546 assert!(tag.force_show.is_none());
547 assert!(tag.created_by.is_none());
548 }
549
550 #[test]
553 fn test_market_minimal_deserialization() {
554 let json = r#"{
555 "id": "12345",
556 "conditionId": "0xabc",
557 "description": "Will X happen?",
558 "question": "Will X happen by end of 2025?",
559 "marketMakerAddress": "0x1234567890abcdef"
560 }"#;
561 let market: Market = serde_json::from_str(json).unwrap();
562 assert_eq!(market.id, "12345");
563 assert_eq!(market.condition_id, "0xabc");
564 assert!(market.tokens.is_empty()); assert!(market.tags.is_empty()); assert!(market.slug.is_none());
567 assert!(market.volume_24hr.is_none());
568 }
569
570 #[test]
571 fn test_market_with_tokens() {
572 let json = r#"{
573 "id": "1",
574 "conditionId": "0xcond",
575 "description": "Test",
576 "question": "Test?",
577 "marketMakerAddress": "0xaddr",
578 "tokens": [
579 {"tokenId": "t1", "outcome": "Yes", "price": "0.7", "winner": true},
580 {"tokenId": "t2", "outcome": "No", "price": "0.3", "winner": false}
581 ]
582 }"#;
583 let market: Market = serde_json::from_str(json).unwrap();
584 assert_eq!(market.tokens.len(), 2);
585 assert_eq!(market.tokens[0].outcome, "Yes");
586 assert_eq!(market.tokens[1].price.as_deref(), Some("0.3"));
587 }
588
589 #[test]
590 fn test_market_volume_fields() {
591 let json = r#"{
592 "id": "1",
593 "conditionId": "0xcond",
594 "description": "Test",
595 "question": "Test?",
596 "marketMakerAddress": "0xaddr",
597 "volume24hr": 1500.5,
598 "volume1wk": 10000.0,
599 "volume1mo": 50000.0,
600 "volume1yr": 200000.0,
601 "volume24hrAmm": 100.0,
602 "volume1wkClob": 9900.0
603 }"#;
604 let market: Market = serde_json::from_str(json).unwrap();
605 assert_eq!(market.volume_24hr, Some(1500.5));
606 assert_eq!(market.volume_1wk, Some(10000.0));
607 assert_eq!(market.volume_24hr_amm, Some(100.0));
608 assert_eq!(market.volume_1wk_clob, Some(9900.0));
609 }
610
611 #[test]
612 fn test_market_denomination_token_rename() {
613 let json = r#"{
615 "id": "1",
616 "conditionId": "0xcond",
617 "description": "Test",
618 "question": "Test?",
619 "marketMakerAddress": "0xaddr",
620 "denomationToken": "USDC"
621 }"#;
622 let market: Market = serde_json::from_str(json).unwrap();
623 assert_eq!(market.denomination_token.as_deref(), Some("USDC"));
624 }
625
626 #[test]
627 fn test_market_rewards_as_map() {
628 let json = r#"{
629 "id": "1",
630 "conditionId": "0xcond",
631 "description": "Test",
632 "question": "Test?",
633 "marketMakerAddress": "0xaddr",
634 "rewards": {"min_size": "100", "max_spread": "0.05"}
635 }"#;
636 let market: Market = serde_json::from_str(json).unwrap();
637 assert!(market.rewards.is_some());
638 let rewards = market.rewards.unwrap();
639 assert_eq!(rewards["min_size"], "100");
640 }
641
642 #[test]
643 fn test_market_null_rewards() {
644 let json = r#"{
645 "id": "1",
646 "conditionId": "0xcond",
647 "description": "Test",
648 "question": "Test?",
649 "marketMakerAddress": "0xaddr",
650 "rewards": null
651 }"#;
652 let market: Market = serde_json::from_str(json).unwrap();
653 assert!(market.rewards.is_none());
654 }
655
656 #[test]
659 fn test_event_minimal() {
660 let json = r#"{"id": "evt-1"}"#;
661 let event: Event = serde_json::from_str(json).unwrap();
662 assert_eq!(event.id, "evt-1");
663 assert!(event.markets.is_empty()); assert!(event.tags.is_empty());
665 assert!(event.series.is_empty());
666 assert!(event.sub_events.is_empty());
667 }
668
669 #[test]
670 fn test_event_with_nested_markets() {
671 let json = r#"{
672 "id": "evt-1",
673 "title": "2025 Election",
674 "markets": [
675 {
676 "id": "mkt-1",
677 "conditionId": "0xabc",
678 "description": "Who wins?",
679 "question": "Who wins the election?",
680 "marketMakerAddress": "0xaddr"
681 }
682 ]
683 }"#;
684 let event: Event = serde_json::from_str(json).unwrap();
685 assert_eq!(event.markets.len(), 1);
686 assert_eq!(event.markets[0].id, "mkt-1");
687 }
688
689 #[test]
690 fn test_event_volume_24h_rename() {
691 let json = r#"{
692 "id": "evt-1",
693 "volume24hr": 5000.0
694 }"#;
695 let event: Event = serde_json::from_str(json).unwrap();
696 assert_eq!(event.volume_24hr, Some(5000.0));
697 }
698
699 #[test]
700 fn test_event_volume_24h_old_key_ignored() {
701 let json = r#"{
703 "id": "evt-1",
704 "volume24h": 5000.0
705 }"#;
706 let event: Event = serde_json::from_str(json).unwrap();
707 assert_eq!(event.volume_24hr, None);
708 }
709
710 #[test]
711 fn test_event_enable_neg_risk_rename() {
712 let json = r#"{
713 "id": "evt-1",
714 "enableNegRisk": true
715 }"#;
716 let event: Event = serde_json::from_str(json).unwrap();
717 assert_eq!(event.enable_neg_risk, Some(true));
718 }
719
720 #[test]
721 fn test_event_published_at_snake_case() {
722 let json = r#"{
723 "id": "evt-1",
724 "published_at": "2024-01-01T00:00:00Z"
725 }"#;
726 let event: Event = serde_json::from_str(json).unwrap();
727 assert_eq!(event.published_at.as_deref(), Some("2024-01-01T00:00:00Z"));
728 }
729
730 #[test]
731 fn test_market_question_id_capital() {
732 let json = r#"{
733 "id": "1",
734 "conditionId": "0xcond",
735 "description": "Test",
736 "question": "Test?",
737 "marketMakerAddress": "0xaddr",
738 "questionID": "0xabc123"
739 }"#;
740 let market: Market = serde_json::from_str(json).unwrap();
741 assert_eq!(market.question_id.as_deref(), Some("0xabc123"));
742 }
743
744 #[test]
745 fn test_market_has_reviewed_dates() {
746 let json = r#"{
747 "id": "1",
748 "conditionId": "0xcond",
749 "description": "Test",
750 "question": "Test?",
751 "marketMakerAddress": "0xaddr",
752 "hasReviewedDates": true
753 }"#;
754 let market: Market = serde_json::from_str(json).unwrap();
755 assert_eq!(market.has_reviewed_dates, Some(true));
756 }
757
758 #[test]
759 fn test_market_rewards_max_spread_singular() {
760 let json = r#"{
761 "id": "1",
762 "conditionId": "0xcond",
763 "description": "Test",
764 "question": "Test?",
765 "marketMakerAddress": "0xaddr",
766 "rewardsMaxSpread": 0.05
767 }"#;
768 let market: Market = serde_json::from_str(json).unwrap();
769 assert_eq!(market.rewards_max_spread, Some(0.05));
770 }
771
772 #[test]
773 fn test_market_submitted_by_snake_case() {
774 let json = r#"{
775 "id": "1",
776 "conditionId": "0xcond",
777 "description": "Test",
778 "question": "Test?",
779 "marketMakerAddress": "0xaddr",
780 "submitted_by": "0xdeadbeef"
781 }"#;
782 let market: Market = serde_json::from_str(json).unwrap();
783 assert_eq!(market.submitted_by.as_deref(), Some("0xdeadbeef"));
784 }
785
786 #[test]
789 fn test_series_info_minimal() {
790 let json = r#"{"id": "s1", "slug": "nfl-2025", "title": "NFL 2025"}"#;
791 let si: SeriesInfo = serde_json::from_str(json).unwrap();
792 assert_eq!(si.slug, "nfl-2025");
793 assert_eq!(si.title, "NFL 2025");
794 assert!(si.ticker.is_none());
795 assert!(si.active.is_none());
796 }
797
798 #[test]
799 fn test_series_info_full() {
800 let json = r#"{
801 "id": "2",
802 "ticker": "nba",
803 "slug": "nba",
804 "title": "NBA",
805 "seriesType": "single",
806 "recurrence": "daily",
807 "image": "https://example.com/nba.png",
808 "icon": "https://example.com/nba-icon.png",
809 "layout": "default",
810 "active": true,
811 "closed": false,
812 "archived": false,
813 "new": false,
814 "featured": false,
815 "restricted": true,
816 "publishedAt": "2023-01-30T17:13:39Z",
817 "createdBy": "15",
818 "updatedBy": "15",
819 "createdAt": "2022-10-13T00:36:01Z",
820 "updatedAt": "2026-03-04T12:03:42Z",
821 "commentsEnabled": false,
822 "competitive": "0",
823 "volume24hr": 11.07,
824 "startDate": "2021-01-01T17:00:00Z",
825 "commentCount": 6274,
826 "requiresTranslation": false
827 }"#;
828 let si: SeriesInfo = serde_json::from_str(json).unwrap();
829 assert_eq!(si.ticker.as_deref(), Some("nba"));
830 assert_eq!(si.series_type.as_deref(), Some("single"));
831 assert_eq!(si.active, Some(true));
832 assert_eq!(si.closed, Some(false));
833 assert_eq!(si.volume_24hr, Some(11.07));
834 assert_eq!(si.comment_count, Some(6274));
835 assert_eq!(si.requires_translation, Some(false));
836 }
837
838 #[test]
841 fn test_market_old_question_id_ignored() {
842 let json = r#"{
844 "id": "1",
845 "conditionId": "0xcond",
846 "description": "Test",
847 "question": "Test?",
848 "marketMakerAddress": "0xaddr",
849 "questionId": "0xwrong"
850 }"#;
851 let market: Market = serde_json::from_str(json).unwrap();
852 assert!(market.question_id.is_none());
853 }
854
855 #[test]
856 fn test_market_old_has_review_dates_ignored() {
857 let json = r#"{
859 "id": "1",
860 "conditionId": "0xcond",
861 "description": "Test",
862 "question": "Test?",
863 "marketMakerAddress": "0xaddr",
864 "hasReviewDates": true
865 }"#;
866 let market: Market = serde_json::from_str(json).unwrap();
867 assert!(market.has_reviewed_dates.is_none());
868 }
869
870 #[test]
871 fn test_market_old_rewards_max_spreads_ignored() {
872 let json = r#"{
874 "id": "1",
875 "conditionId": "0xcond",
876 "description": "Test",
877 "question": "Test?",
878 "marketMakerAddress": "0xaddr",
879 "rewardsMaxSpreads": 0.05
880 }"#;
881 let market: Market = serde_json::from_str(json).unwrap();
882 assert!(market.rewards_max_spread.is_none());
883 }
884
885 #[test]
886 fn test_event_old_enalbe_neg_risk_ignored() {
887 let json = r#"{
889 "id": "evt-1",
890 "enalbeNegRisk": true
891 }"#;
892 let event: Event = serde_json::from_str(json).unwrap();
893 assert!(event.enable_neg_risk.is_none());
894 }
895
896 #[test]
897 fn test_event_camel_published_at_ignored() {
898 let json = r#"{
900 "id": "evt-1",
901 "publishedAt": "2024-01-01T00:00:00Z"
902 }"#;
903 let event: Event = serde_json::from_str(json).unwrap();
904 assert!(event.published_at.is_none());
905 }
906
907 #[test]
908 fn test_market_camel_submitted_by_ignored() {
909 let json = r#"{
911 "id": "1",
912 "conditionId": "0xcond",
913 "description": "Test",
914 "question": "Test?",
915 "marketMakerAddress": "0xaddr",
916 "submittedBy": "0xwrong"
917 }"#;
918 let market: Market = serde_json::from_str(json).unwrap();
919 assert!(market.submitted_by.is_none());
920 }
921
922 #[test]
925 fn test_series_data_minimal() {
926 let json = r#"{
927 "id": "s1",
928 "slug": "nfl",
929 "title": "NFL",
930 "active": true,
931 "closed": false,
932 "archived": false
933 }"#;
934 let sd: SeriesData = serde_json::from_str(json).unwrap();
935 assert!(sd.active);
936 assert!(!sd.closed);
937 assert!(sd.events.is_empty()); assert!(sd.tags.is_empty());
939 }
940
941 #[test]
944 fn test_sport_metadata() {
945 let json = r#"{
946 "id": 1,
947 "sport": "Basketball",
948 "image": "https://example.com/nba.png",
949 "createdAt": "2024-01-01T00:00:00Z"
950 }"#;
951 let sm: SportMetadata = serde_json::from_str(json).unwrap();
952 assert_eq!(sm.id, 1);
953 assert_eq!(sm.sport, "Basketball");
954 }
955
956 #[test]
959 fn test_team() {
960 let json = r#"{
961 "id": 42,
962 "name": "Lakers",
963 "league": "NBA",
964 "abbreviation": "LAL",
965 "createdAt": "2024-01-01T00:00:00Z",
966 "updatedAt": "2024-06-15T12:00:00Z"
967 }"#;
968 let team: Team = serde_json::from_str(json).unwrap();
969 assert_eq!(team.id, 42);
970 assert_eq!(team.name.as_deref(), Some("Lakers"));
971 assert!(team.created_at.is_some());
972 }
973
974 #[test]
977 fn test_comment_deserialization() {
978 let json = r#"{
979 "id": "c1",
980 "body": "I think this market will resolve yes.",
981 "createdAt": "2024-06-01T10:00:00Z",
982 "updatedAt": "2024-06-01T10:00:00Z",
983 "deletedAt": null,
984 "user": {"id": "u1", "name": "trader1", "avatar": null},
985 "marketId": "mkt-1",
986 "eventId": null,
987 "seriesId": null,
988 "parentId": null,
989 "reactions": [],
990 "positions": [
991 {"tokenId": "t1", "outcome": "Yes", "shares": "100.5"}
992 ],
993 "likeCount": 5,
994 "dislikeCount": 1,
995 "replyCount": 3
996 }"#;
997 let comment: Comment = serde_json::from_str(json).unwrap();
998 assert_eq!(comment.id, "c1");
999 assert_eq!(comment.user.name, "trader1");
1000 assert_eq!(comment.like_count, 5);
1001 assert_eq!(comment.positions.len(), 1);
1002 assert_eq!(comment.positions[0].shares, "100.5");
1003 assert!(comment.deleted_at.is_none());
1004 }
1005
1006 #[test]
1009 fn test_user_response() {
1010 let json = r#"{
1011 "proxyWallet": "0xproxy",
1012 "address": "0xsigner",
1013 "id": "u1",
1014 "name": "polytrader"
1015 }"#;
1016 let user: crate::api::user::UserResponse = serde_json::from_str(json).unwrap();
1017 assert_eq!(user.proxy.as_deref(), Some("0xproxy"));
1018 assert_eq!(user.name.as_deref(), Some("polytrader"));
1019 }
1020
1021 #[test]
1022 fn test_user_response_all_null() {
1023 let json = r#"{}"#;
1024 let user: crate::api::user::UserResponse = serde_json::from_str(json).unwrap();
1025 assert!(user.proxy.is_none());
1026 assert!(user.address.is_none());
1027 assert!(user.id.is_none());
1028 assert!(user.name.is_none());
1029 assert!(user.created_at.is_none());
1030 assert!(user.profile_image.is_none());
1031 assert!(user.display_username_public.is_none());
1032 assert!(user.bio.is_none());
1033 assert!(user.pseudonym.is_none());
1034 assert!(user.x_username.is_none());
1035 assert!(user.verified_badge.is_none());
1036 assert!(user.users.is_empty());
1037 }
1038
1039 #[test]
1040 fn test_user_response_full_profile() {
1041 let json = r#"{
1042 "proxyWallet": "0xproxy",
1043 "address": "0xsigner",
1044 "id": "u1",
1045 "name": "polytrader",
1046 "createdAt": "2024-01-15T10:00:00Z",
1047 "profileImage": "https://example.com/avatar.png",
1048 "displayUsernamePublic": true,
1049 "bio": "DeFi enthusiast",
1050 "pseudonym": "poly_anon",
1051 "xUsername": "polytrader_x",
1052 "verifiedBadge": true,
1053 "users": [
1054 {"id": "uid-1", "creator": true, "mod": false},
1055 {"id": "uid-2", "creator": false, "mod": true}
1056 ]
1057 }"#;
1058 let user: crate::api::user::UserResponse = serde_json::from_str(json).unwrap();
1059 assert_eq!(user.proxy.as_deref(), Some("0xproxy"));
1060 assert_eq!(user.name.as_deref(), Some("polytrader"));
1061 assert_eq!(user.created_at.as_deref(), Some("2024-01-15T10:00:00Z"));
1062 assert_eq!(
1063 user.profile_image.as_deref(),
1064 Some("https://example.com/avatar.png")
1065 );
1066 assert_eq!(user.display_username_public, Some(true));
1067 assert_eq!(user.bio.as_deref(), Some("DeFi enthusiast"));
1068 assert_eq!(user.pseudonym.as_deref(), Some("poly_anon"));
1069 assert_eq!(user.x_username.as_deref(), Some("polytrader_x"));
1070 assert_eq!(user.verified_badge, Some(true));
1071 assert_eq!(user.users.len(), 2);
1072 assert!(user.users[0].creator);
1073 assert!(!user.users[0].moderator);
1074 assert!(!user.users[1].creator);
1075 assert!(user.users[1].moderator);
1076 }
1077
1078 #[test]
1079 fn test_user_info_deserialization() {
1080 let json = r#"{"id": "uid-1", "creator": true, "mod": false}"#;
1081 let info: crate::api::user::UserInfo = serde_json::from_str(json).unwrap();
1082 assert_eq!(info.id.as_deref(), Some("uid-1"));
1083 assert!(info.creator);
1084 assert!(!info.moderator);
1085 }
1086
1087 #[test]
1088 fn test_user_info_defaults() {
1089 let json = r#"{}"#;
1090 let info: crate::api::user::UserInfo = serde_json::from_str(json).unwrap();
1091 assert!(info.id.is_none());
1092 assert!(!info.creator);
1093 assert!(!info.moderator);
1094 }
1095
1096 #[test]
1099 fn test_count_response() {
1100 let json = r#"{"count": 42}"#;
1101 let resp: CountResponse = serde_json::from_str(json).unwrap();
1102 assert_eq!(resp.count, 42);
1103 }
1104
1105 #[test]
1108 fn test_cursor_with_next() {
1109 let json = r#"{"nextCursor": "abc123"}"#;
1110 let cursor: Cursor = serde_json::from_str(json).unwrap();
1111 assert_eq!(cursor.next_cursor.as_deref(), Some("abc123"));
1112 }
1113
1114 #[test]
1115 fn test_cursor_without_next() {
1116 let json = r#"{"nextCursor": null}"#;
1117 let cursor: Cursor = serde_json::from_str(json).unwrap();
1118 assert!(cursor.next_cursor.is_none());
1119 }
1120
1121 #[test]
1122 fn test_paginated_response() {
1123 let json = r#"{
1124 "data": [{"tokenId": "t1", "outcome": "Yes"}],
1125 "nextCursor": "page2"
1126 }"#;
1127 let resp: PaginatedResponse<MarketToken> = serde_json::from_str(json).unwrap();
1128 assert_eq!(resp.data.len(), 1);
1129 assert_eq!(resp.next_cursor.as_deref(), Some("page2"));
1130 }
1131
1132 #[test]
1133 fn test_paginated_response_empty() {
1134 let json = r#"{"data": [], "nextCursor": null}"#;
1135 let resp: PaginatedResponse<MarketToken> = serde_json::from_str(json).unwrap();
1136 assert!(resp.data.is_empty());
1137 assert!(resp.next_cursor.is_none());
1138 }
1139
1140 #[test]
1143 fn test_market_token_roundtrip() {
1144 let token = MarketToken {
1145 token_id: "123".into(),
1146 outcome: "Yes".into(),
1147 price: Some("0.75".into()),
1148 winner: Some(true),
1149 };
1150 let json = serde_json::to_string(&token).unwrap();
1151 let back: MarketToken = serde_json::from_str(&json).unwrap();
1152 assert_eq!(token, back);
1153 }
1154
1155 #[test]
1156 fn test_tag_roundtrip() {
1157 let tag = Tag {
1158 id: "1".into(),
1159 slug: "test".into(),
1160 label: "Test".into(),
1161 force_show: None,
1162 published_at: None,
1163 created_by: None,
1164 updated_by: None,
1165 created_at: None,
1166 updated_at: None,
1167 force_hide: None,
1168 is_carousel: None,
1169 };
1170 let json = serde_json::to_string(&tag).unwrap();
1171 let back: Tag = serde_json::from_str(&json).unwrap();
1172 assert_eq!(tag, back);
1173 }
1174}