1use serde::{Deserialize, Serialize};
2
3#[cfg_attr(feature = "specta", derive(specta::Type))]
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct UserValue {
7 pub user: String,
9 pub value: f64,
11}
12
13#[cfg_attr(feature = "specta", derive(specta::Type))]
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct OpenInterest {
17 pub market: String,
19 pub value: f64,
21}
22
23#[cfg_attr(feature = "specta", derive(specta::Type))]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
27pub enum PositionSortBy {
28 Current,
30 Initial,
32 Tokens,
34 CashPnl,
36 PercentPnl,
38 Title,
40 Resolving,
42 Price,
44 AvgPrice,
46}
47
48impl std::fmt::Display for PositionSortBy {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::Current => write!(f, "CURRENT"),
52 Self::Initial => write!(f, "INITIAL"),
53 Self::Tokens => write!(f, "TOKENS"),
54 Self::CashPnl => write!(f, "CASH_PNL"),
55 Self::PercentPnl => write!(f, "PERCENT_PNL"),
56 Self::Title => write!(f, "TITLE"),
57 Self::Resolving => write!(f, "RESOLVING"),
58 Self::Price => write!(f, "PRICE"),
59 Self::AvgPrice => write!(f, "AVG_PRICE"),
60 }
61 }
62}
63
64#[cfg_attr(feature = "specta", derive(specta::Type))]
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
67#[serde(rename_all = "UPPERCASE")]
68pub enum SortDirection {
69 Asc,
71 #[default]
73 Desc,
74}
75
76impl std::fmt::Display for SortDirection {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::Asc => write!(f, "ASC"),
80 Self::Desc => write!(f, "DESC"),
81 }
82 }
83}
84
85#[cfg_attr(feature = "specta", derive(specta::Type))]
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
88#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
89pub enum ClosedPositionSortBy {
90 #[default]
92 RealizedPnl,
93 Title,
95 Price,
97 AvgPrice,
99 Timestamp,
101}
102
103impl std::fmt::Display for ClosedPositionSortBy {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::RealizedPnl => write!(f, "REALIZED_PNL"),
107 Self::Title => write!(f, "TITLE"),
108 Self::Price => write!(f, "PRICE"),
109 Self::AvgPrice => write!(f, "AVG_PRICE"),
110 Self::Timestamp => write!(f, "TIMESTAMP"),
111 }
112 }
113}
114
115#[cfg_attr(feature = "specta", derive(specta::Type))]
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ClosedPosition {
120 pub proxy_wallet: String,
122 pub asset: String,
124 pub condition_id: String,
126 pub avg_price: f64,
128 pub total_bought: f64,
130 pub realized_pnl: f64,
132 pub cur_price: f64,
134 #[cfg_attr(feature = "specta", specta(type = f64))]
136 pub timestamp: i64,
137 pub title: String,
139 pub slug: String,
141 pub icon: Option<String>,
143 pub event_slug: Option<String>,
145 pub outcome: String,
147 pub outcome_index: u32,
149 pub opposite_outcome: String,
151 pub opposite_asset: String,
153 pub end_date: Option<String>,
155}
156
157#[cfg_attr(feature = "specta", derive(specta::Type))]
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "UPPERCASE")]
161pub enum TradeSide {
162 Buy,
164 Sell,
166}
167
168impl std::fmt::Display for TradeSide {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Self::Buy => write!(f, "BUY"),
172 Self::Sell => write!(f, "SELL"),
173 }
174 }
175}
176
177#[cfg_attr(feature = "specta", derive(specta::Type))]
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "UPPERCASE")]
181pub enum TradeFilterType {
182 Cash,
184 Tokens,
186}
187
188impl std::fmt::Display for TradeFilterType {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 match self {
191 Self::Cash => write!(f, "CASH"),
192 Self::Tokens => write!(f, "TOKENS"),
193 }
194 }
195}
196
197#[cfg_attr(feature = "specta", derive(specta::Type))]
199#[derive(Debug, Clone, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct Trade {
202 pub proxy_wallet: String,
204 pub side: TradeSide,
206 pub asset: String,
208 pub condition_id: String,
210 pub size: f64,
212 pub price: f64,
214 #[cfg_attr(feature = "specta", specta(type = f64))]
216 pub timestamp: i64,
217 pub title: String,
219 pub slug: String,
221 pub icon: Option<String>,
223 pub event_slug: Option<String>,
225 pub outcome: String,
227 pub outcome_index: u32,
229 pub name: Option<String>,
231 pub pseudonym: Option<String>,
233 pub bio: Option<String>,
235 pub profile_image: Option<String>,
237 pub profile_image_optimized: Option<String>,
239 pub transaction_hash: Option<String>,
241}
242
243#[cfg_attr(feature = "specta", derive(specta::Type))]
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "UPPERCASE")]
247pub enum ActivityType {
248 Trade,
250 Split,
252 Merge,
254 Redeem,
256 Reward,
258 Conversion,
260}
261
262impl std::fmt::Display for ActivityType {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 Self::Trade => write!(f, "TRADE"),
266 Self::Split => write!(f, "SPLIT"),
267 Self::Merge => write!(f, "MERGE"),
268 Self::Redeem => write!(f, "REDEEM"),
269 Self::Reward => write!(f, "REWARD"),
270 Self::Conversion => write!(f, "CONVERSION"),
271 }
272 }
273}
274
275#[cfg_attr(feature = "specta", derive(specta::Type))]
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
278#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
279pub enum ActivitySortBy {
280 #[default]
282 Timestamp,
283 Tokens,
285 Cash,
287}
288
289impl std::fmt::Display for ActivitySortBy {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 match self {
292 Self::Timestamp => write!(f, "TIMESTAMP"),
293 Self::Tokens => write!(f, "TOKENS"),
294 Self::Cash => write!(f, "CASH"),
295 }
296 }
297}
298
299#[cfg_attr(feature = "specta", derive(specta::Type))]
301#[derive(Debug, Clone, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct Activity {
304 pub proxy_wallet: String,
306 #[cfg_attr(feature = "specta", specta(type = f64))]
308 pub timestamp: i64,
309 pub condition_id: String,
311 #[serde(rename = "type")]
313 pub activity_type: ActivityType,
314 pub size: f64,
316 pub usdc_size: f64,
318 pub transaction_hash: Option<String>,
320 pub price: Option<f64>,
322 pub asset: Option<String>,
324 pub side: Option<String>,
327 pub outcome_index: Option<u32>,
329 pub title: Option<String>,
331 pub slug: Option<String>,
333 pub icon: Option<String>,
335 pub outcome: Option<String>,
337 pub name: Option<String>,
339 pub pseudonym: Option<String>,
341 pub bio: Option<String>,
343 pub profile_image: Option<String>,
345 pub profile_image_optimized: Option<String>,
347}
348
349#[cfg_attr(feature = "specta", derive(specta::Type))]
351#[derive(Debug, Clone, Serialize, Deserialize)]
352#[serde(rename_all = "camelCase")]
353pub struct Position {
354 pub proxy_wallet: String,
356 pub asset: String,
358 pub condition_id: String,
360 pub size: f64,
362 pub avg_price: f64,
364 pub initial_value: f64,
366 pub current_value: f64,
368 pub cash_pnl: f64,
370 pub percent_pnl: f64,
372 pub total_bought: f64,
374 pub realized_pnl: f64,
376 pub percent_realized_pnl: f64,
378 pub cur_price: f64,
380 pub redeemable: bool,
382 pub mergeable: bool,
384 pub title: String,
386 pub slug: String,
388 pub icon: Option<String>,
390 pub event_slug: Option<String>,
392 pub outcome: String,
394 pub outcome_index: u32,
396 pub opposite_outcome: String,
398 pub opposite_asset: String,
400 pub end_date: Option<String>,
402 pub negative_risk: bool,
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
412 fn position_sort_by_display_matches_serde() {
413 let variants = [
414 PositionSortBy::Current,
415 PositionSortBy::Initial,
416 PositionSortBy::Tokens,
417 PositionSortBy::CashPnl,
418 PositionSortBy::PercentPnl,
419 PositionSortBy::Title,
420 PositionSortBy::Resolving,
421 PositionSortBy::Price,
422 PositionSortBy::AvgPrice,
423 ];
424 for variant in variants {
425 let serialized = serde_json::to_value(variant).unwrap();
426 let display = variant.to_string();
427 assert_eq!(
428 format!("\"{}\"", display),
429 serialized.to_string(),
430 "Display mismatch for {:?}",
431 variant
432 );
433 }
434 }
435
436 #[test]
438 fn closed_position_sort_by_display_matches_serde() {
439 let variants = [
440 ClosedPositionSortBy::RealizedPnl,
441 ClosedPositionSortBy::Title,
442 ClosedPositionSortBy::Price,
443 ClosedPositionSortBy::AvgPrice,
444 ClosedPositionSortBy::Timestamp,
445 ];
446 for variant in variants {
447 let serialized = serde_json::to_value(variant).unwrap();
448 let display = variant.to_string();
449 assert_eq!(
450 format!("\"{}\"", display),
451 serialized.to_string(),
452 "Display mismatch for {:?}",
453 variant
454 );
455 }
456 }
457
458 #[test]
459 fn activity_sort_by_display_matches_serde() {
460 let variants = [
461 ActivitySortBy::Timestamp,
462 ActivitySortBy::Tokens,
463 ActivitySortBy::Cash,
464 ];
465 for variant in variants {
466 let serialized = serde_json::to_value(variant).unwrap();
467 let display = variant.to_string();
468 assert_eq!(
469 format!("\"{}\"", display),
470 serialized.to_string(),
471 "Display mismatch for {:?}",
472 variant
473 );
474 }
475 }
476
477 #[test]
478 fn sort_direction_display_matches_serde() {
479 let variants = [SortDirection::Asc, SortDirection::Desc];
480 for variant in variants {
481 let serialized = serde_json::to_value(variant).unwrap();
482 let display = variant.to_string();
483 assert_eq!(
484 format!("\"{}\"", display),
485 serialized.to_string(),
486 "Display mismatch for {:?}",
487 variant
488 );
489 }
490 }
491
492 #[test]
493 fn trade_side_display_matches_serde() {
494 let variants = [TradeSide::Buy, TradeSide::Sell];
495 for variant in variants {
496 let serialized = serde_json::to_value(variant).unwrap();
497 let display = variant.to_string();
498 assert_eq!(
499 format!("\"{}\"", display),
500 serialized.to_string(),
501 "Display mismatch for {:?}",
502 variant
503 );
504 }
505 }
506
507 #[test]
508 fn trade_filter_type_display_matches_serde() {
509 let variants = [TradeFilterType::Cash, TradeFilterType::Tokens];
510 for variant in variants {
511 let serialized = serde_json::to_value(variant).unwrap();
512 let display = variant.to_string();
513 assert_eq!(
514 format!("\"{}\"", display),
515 serialized.to_string(),
516 "Display mismatch for {:?}",
517 variant
518 );
519 }
520 }
521
522 #[test]
523 fn activity_type_display_matches_serde() {
524 let variants = [
525 ActivityType::Trade,
526 ActivityType::Split,
527 ActivityType::Merge,
528 ActivityType::Redeem,
529 ActivityType::Reward,
530 ActivityType::Conversion,
531 ];
532 for variant in variants {
533 let serialized = serde_json::to_value(variant).unwrap();
534 let display = variant.to_string();
535 assert_eq!(
536 format!("\"{}\"", display),
537 serialized.to_string(),
538 "Display mismatch for {:?}",
539 variant
540 );
541 }
542 }
543
544 #[test]
545 fn activity_type_roundtrip_serde() {
546 for variant in [
547 ActivityType::Trade,
548 ActivityType::Split,
549 ActivityType::Merge,
550 ActivityType::Redeem,
551 ActivityType::Reward,
552 ActivityType::Conversion,
553 ] {
554 let json = serde_json::to_string(&variant).unwrap();
555 let deserialized: ActivityType = serde_json::from_str(&json).unwrap();
556 assert_eq!(variant, deserialized);
557 }
558 }
559
560 #[test]
561 fn activity_type_rejects_unknown_variant() {
562 let result = serde_json::from_str::<ActivityType>("\"UNKNOWN\"");
563 assert!(result.is_err(), "should reject unknown activity type");
564 }
565
566 #[test]
567 fn activity_type_rejects_lowercase() {
568 let result = serde_json::from_str::<ActivityType>("\"trade\"");
569 assert!(result.is_err(), "should reject lowercase activity type");
570 }
571
572 #[test]
573 fn sort_direction_default_is_desc() {
574 assert_eq!(SortDirection::default(), SortDirection::Desc);
575 }
576
577 #[test]
578 fn closed_position_sort_by_default_is_realized_pnl() {
579 assert_eq!(
580 ClosedPositionSortBy::default(),
581 ClosedPositionSortBy::RealizedPnl
582 );
583 }
584
585 #[test]
586 fn activity_sort_by_default_is_timestamp() {
587 assert_eq!(ActivitySortBy::default(), ActivitySortBy::Timestamp);
588 }
589
590 #[test]
591 fn position_sort_by_serde_roundtrip() {
592 for variant in [
593 PositionSortBy::Current,
594 PositionSortBy::Initial,
595 PositionSortBy::Tokens,
596 PositionSortBy::CashPnl,
597 PositionSortBy::PercentPnl,
598 PositionSortBy::Title,
599 PositionSortBy::Resolving,
600 PositionSortBy::Price,
601 PositionSortBy::AvgPrice,
602 ] {
603 let json = serde_json::to_string(&variant).unwrap();
604 let deserialized: PositionSortBy = serde_json::from_str(&json).unwrap();
605 assert_eq!(variant, deserialized);
606 }
607 }
608
609 #[test]
610 fn deserialize_position_from_json() {
611 let json = r#"{
612 "proxyWallet": "0xabc123",
613 "asset": "token123",
614 "conditionId": "cond456",
615 "size": 100.5,
616 "avgPrice": 0.65,
617 "initialValue": 65.0,
618 "currentValue": 70.0,
619 "cashPnl": 5.0,
620 "percentPnl": 7.69,
621 "totalBought": 100.5,
622 "realizedPnl": 2.0,
623 "percentRealizedPnl": 3.08,
624 "curPrice": 0.70,
625 "redeemable": false,
626 "mergeable": true,
627 "title": "Will X happen?",
628 "slug": "will-x-happen",
629 "icon": "https://example.com/icon.png",
630 "eventSlug": "x-event",
631 "outcome": "Yes",
632 "outcomeIndex": 0,
633 "oppositeOutcome": "No",
634 "oppositeAsset": "token789",
635 "endDate": "2025-12-31",
636 "negativeRisk": false
637 }"#;
638
639 let pos: Position = serde_json::from_str(json).unwrap();
640 assert_eq!(pos.proxy_wallet, "0xabc123");
641 assert_eq!(pos.asset, "token123");
642 assert_eq!(pos.condition_id, "cond456");
643 assert!((pos.size - 100.5).abs() < f64::EPSILON);
644 assert!((pos.avg_price - 0.65).abs() < f64::EPSILON);
645 assert!((pos.initial_value - 65.0).abs() < f64::EPSILON);
646 assert!((pos.current_value - 70.0).abs() < f64::EPSILON);
647 assert!((pos.cash_pnl - 5.0).abs() < f64::EPSILON);
648 assert!(!pos.redeemable);
649 assert!(pos.mergeable);
650 assert_eq!(pos.title, "Will X happen?");
651 assert_eq!(pos.outcome, "Yes");
652 assert_eq!(pos.outcome_index, 0);
653 assert_eq!(pos.opposite_outcome, "No");
654 assert!(!pos.negative_risk);
655 assert_eq!(pos.icon, Some("https://example.com/icon.png".to_string()));
656 assert_eq!(pos.event_slug, Some("x-event".to_string()));
657 }
658
659 #[test]
660 fn deserialize_position_with_null_optionals() {
661 let json = r#"{
662 "proxyWallet": "0xabc123",
663 "asset": "token123",
664 "conditionId": "cond456",
665 "size": 0.0,
666 "avgPrice": 0.0,
667 "initialValue": 0.0,
668 "currentValue": 0.0,
669 "cashPnl": 0.0,
670 "percentPnl": 0.0,
671 "totalBought": 0.0,
672 "realizedPnl": 0.0,
673 "percentRealizedPnl": 0.0,
674 "curPrice": 0.0,
675 "redeemable": false,
676 "mergeable": false,
677 "title": "Test",
678 "slug": "test",
679 "icon": null,
680 "eventSlug": null,
681 "outcome": "No",
682 "outcomeIndex": 1,
683 "oppositeOutcome": "Yes",
684 "oppositeAsset": "token000",
685 "endDate": null,
686 "negativeRisk": true
687 }"#;
688
689 let pos: Position = serde_json::from_str(json).unwrap();
690 assert!(pos.icon.is_none());
691 assert!(pos.event_slug.is_none());
692 assert!(pos.end_date.is_none());
693 assert!(pos.negative_risk);
694 }
695
696 #[test]
697 fn deserialize_closed_position_from_json() {
698 let json = r#"{
699 "proxyWallet": "0xdef456",
700 "asset": "token_closed",
701 "conditionId": "cond_closed",
702 "avgPrice": 0.45,
703 "totalBought": 200.0,
704 "realizedPnl": -10.0,
705 "curPrice": 0.35,
706 "timestamp": 1700000000,
707 "title": "Closed market?",
708 "slug": "closed-market",
709 "icon": null,
710 "eventSlug": "closed-event",
711 "outcome": "No",
712 "outcomeIndex": 1,
713 "oppositeOutcome": "Yes",
714 "oppositeAsset": "token_opp",
715 "endDate": "2024-06-30"
716 }"#;
717
718 let closed: ClosedPosition = serde_json::from_str(json).unwrap();
719 assert_eq!(closed.proxy_wallet, "0xdef456");
720 assert!((closed.avg_price - 0.45).abs() < f64::EPSILON);
721 assert!((closed.realized_pnl - (-10.0)).abs() < f64::EPSILON);
722 assert_eq!(closed.timestamp, 1700000000);
723 assert_eq!(closed.outcome, "No");
724 assert_eq!(closed.outcome_index, 1);
725 assert!(closed.icon.is_none());
726 assert_eq!(closed.event_slug, Some("closed-event".to_string()));
727 }
728
729 #[test]
730 fn deserialize_trade_from_json() {
731 let json = r#"{
732 "proxyWallet": "0x1234",
733 "side": "BUY",
734 "asset": "token_buy",
735 "conditionId": "cond_trade",
736 "size": 50.0,
737 "price": 0.72,
738 "timestamp": 1700001000,
739 "title": "Trade market?",
740 "slug": "trade-market",
741 "icon": "https://example.com/trade.png",
742 "eventSlug": null,
743 "outcome": "Yes",
744 "outcomeIndex": 0,
745 "name": "TraderOne",
746 "pseudonym": "t1",
747 "bio": "A trader",
748 "profileImage": null,
749 "profileImageOptimized": null,
750 "transactionHash": "0xhash123"
751 }"#;
752
753 let trade: Trade = serde_json::from_str(json).unwrap();
754 assert_eq!(trade.proxy_wallet, "0x1234");
755 assert_eq!(trade.side, TradeSide::Buy);
756 assert!((trade.size - 50.0).abs() < f64::EPSILON);
757 assert!((trade.price - 0.72).abs() < f64::EPSILON);
758 assert_eq!(trade.timestamp, 1700001000);
759 assert_eq!(trade.name, Some("TraderOne".to_string()));
760 assert_eq!(trade.transaction_hash, Some("0xhash123".to_string()));
761 assert!(trade.profile_image.is_none());
762 }
763
764 #[test]
765 fn deserialize_trade_sell_side() {
766 let json = r#"{
767 "proxyWallet": "0x5678",
768 "side": "SELL",
769 "asset": "token_sell",
770 "conditionId": "cond_sell",
771 "size": 25.0,
772 "price": 0.30,
773 "timestamp": 1700002000,
774 "title": "Sell test",
775 "slug": "sell-test",
776 "icon": null,
777 "eventSlug": null,
778 "outcome": "No",
779 "outcomeIndex": 1,
780 "name": null,
781 "pseudonym": null,
782 "bio": null,
783 "profileImage": null,
784 "profileImageOptimized": null,
785 "transactionHash": null
786 }"#;
787
788 let trade: Trade = serde_json::from_str(json).unwrap();
789 assert_eq!(trade.side, TradeSide::Sell);
790 assert!(trade.name.is_none());
791 assert!(trade.transaction_hash.is_none());
792 }
793
794 #[test]
795 fn deserialize_activity_from_json() {
796 let json = r#"{
797 "proxyWallet": "0xact123",
798 "timestamp": 1700003000,
799 "conditionId": "cond_act",
800 "type": "TRADE",
801 "size": 10.0,
802 "usdcSize": 7.50,
803 "transactionHash": "0xacthash",
804 "price": 0.75,
805 "asset": "token_act",
806 "side": "BUY",
807 "outcomeIndex": 0,
808 "title": "Activity market",
809 "slug": "activity-market",
810 "icon": null,
811 "outcome": "Yes",
812 "name": null,
813 "pseudonym": null,
814 "bio": null,
815 "profileImage": null,
816 "profileImageOptimized": null
817 }"#;
818
819 let activity: Activity = serde_json::from_str(json).unwrap();
820 assert_eq!(activity.proxy_wallet, "0xact123");
821 assert_eq!(activity.activity_type, ActivityType::Trade);
822 assert!((activity.size - 10.0).abs() < f64::EPSILON);
823 assert!((activity.usdc_size - 7.50).abs() < f64::EPSILON);
824 assert_eq!(activity.side, Some("BUY".to_string()));
825 assert_eq!(activity.outcome_index, Some(0));
826 }
827
828 #[test]
829 fn deserialize_activity_merge_type() {
830 let json = r#"{
831 "proxyWallet": "0xmerge",
832 "timestamp": 1700004000,
833 "conditionId": "cond_merge",
834 "type": "MERGE",
835 "size": 5.0,
836 "usdcSize": 3.0,
837 "transactionHash": null,
838 "price": null,
839 "asset": null,
840 "side": "",
841 "outcomeIndex": null,
842 "title": null,
843 "slug": null,
844 "icon": null,
845 "outcome": null,
846 "name": null,
847 "pseudonym": null,
848 "bio": null,
849 "profileImage": null,
850 "profileImageOptimized": null
851 }"#;
852
853 let activity: Activity = serde_json::from_str(json).unwrap();
854 assert_eq!(activity.activity_type, ActivityType::Merge);
855 assert_eq!(activity.side, Some("".to_string()));
857 assert!(activity.price.is_none());
858 assert!(activity.asset.is_none());
859 assert!(activity.title.is_none());
860 }
861
862 #[test]
863 fn deserialize_user_value() {
864 let json = r#"{"user": "0xuser", "value": 1234.56}"#;
865 let uv: UserValue = serde_json::from_str(json).unwrap();
866 assert_eq!(uv.user, "0xuser");
867 assert!((uv.value - 1234.56).abs() < f64::EPSILON);
868 }
869
870 #[test]
871 fn deserialize_open_interest() {
872 let json = r#"{"market": "0xcond", "value": 50000.0}"#;
873 let oi: OpenInterest = serde_json::from_str(json).unwrap();
874 assert_eq!(oi.market, "0xcond");
875 assert!((oi.value - 50000.0).abs() < f64::EPSILON);
876 }
877}