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 #[serde(other)]
170 Unknown,
171}
172
173impl std::fmt::Display for TradeSide {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 match self {
176 Self::Buy => write!(f, "BUY"),
177 Self::Sell => write!(f, "SELL"),
178 Self::Unknown => write!(f, "UNKNOWN"),
179 }
180 }
181}
182
183#[cfg_attr(feature = "specta", derive(specta::Type))]
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "UPPERCASE")]
187pub enum TradeFilterType {
188 Cash,
190 Tokens,
192}
193
194impl std::fmt::Display for TradeFilterType {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 match self {
197 Self::Cash => write!(f, "CASH"),
198 Self::Tokens => write!(f, "TOKENS"),
199 }
200 }
201}
202
203#[cfg_attr(feature = "specta", derive(specta::Type))]
205#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct Trade {
208 pub proxy_wallet: String,
210 pub side: TradeSide,
212 pub asset: String,
214 pub condition_id: String,
216 pub size: f64,
218 pub price: f64,
220 #[cfg_attr(feature = "specta", specta(type = f64))]
222 pub timestamp: i64,
223 pub title: String,
225 pub slug: String,
227 pub icon: Option<String>,
229 pub event_slug: Option<String>,
231 pub outcome: String,
233 pub outcome_index: u32,
235 pub name: Option<String>,
237 pub pseudonym: Option<String>,
239 pub bio: Option<String>,
241 pub profile_image: Option<String>,
243 pub profile_image_optimized: Option<String>,
245 pub transaction_hash: Option<String>,
247}
248
249#[cfg_attr(feature = "specta", derive(specta::Type))]
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "UPPERCASE")]
253pub enum ActivityType {
254 Trade,
256 Split,
258 Merge,
260 Redeem,
262 Reward,
264 Conversion,
266 Deposit,
272 Withdrawal,
278 Yield,
280 #[serde(rename = "MAKER_REBATE")]
282 MakerRebate,
283 #[serde(rename = "REFERRAL_REWARD")]
285 ReferralReward,
286 #[serde(rename = "TAKER_REBATE")]
288 TakerRebate,
289 #[serde(other)]
293 Unknown,
294}
295
296impl std::fmt::Display for ActivityType {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 match self {
299 Self::Trade => write!(f, "TRADE"),
300 Self::Split => write!(f, "SPLIT"),
301 Self::Merge => write!(f, "MERGE"),
302 Self::Redeem => write!(f, "REDEEM"),
303 Self::Reward => write!(f, "REWARD"),
304 Self::Conversion => write!(f, "CONVERSION"),
305 Self::Deposit => write!(f, "DEPOSIT"),
306 Self::Withdrawal => write!(f, "WITHDRAWAL"),
307 Self::Yield => write!(f, "YIELD"),
308 Self::MakerRebate => write!(f, "MAKER_REBATE"),
309 Self::ReferralReward => write!(f, "REFERRAL_REWARD"),
310 Self::TakerRebate => write!(f, "TAKER_REBATE"),
311 Self::Unknown => write!(f, "UNKNOWN"),
312 }
313 }
314}
315
316#[cfg_attr(feature = "specta", derive(specta::Type))]
323#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
324#[serde(untagged)]
325pub enum Allowance {
326 Max,
328 Amount(rust_decimal::Decimal),
330 Unknown(String),
337}
338
339impl<'de> Deserialize<'de> for Allowance {
340 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
341 where
342 D: serde::Deserializer<'de>,
343 {
344 let raw = String::deserialize(deserializer)?;
345 if raw == "max" {
346 return Ok(Allowance::Max);
347 }
348 Ok(match raw.parse::<rust_decimal::Decimal>() {
349 Ok(amount) => Allowance::Amount(amount),
350 Err(_) => Allowance::Unknown(raw),
351 })
352 }
353}
354
355#[cfg_attr(feature = "specta", derive(specta::Type))]
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
361pub enum ApprovalFeature {
362 #[serde(rename = "trading")]
364 Trading,
365 #[serde(rename = "perps")]
367 Perps,
368 #[serde(rename = "rewards")]
370 Rewards,
371 #[serde(rename = "auto-redeem")]
373 AutoRedeem,
374 #[serde(other)]
376 Unknown,
377}
378
379#[cfg_attr(feature = "specta", derive(specta::Type))]
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
382pub enum ApprovalStandard {
383 #[serde(rename = "ERC20")]
385 Erc20,
386 #[serde(rename = "ERC1155")]
388 Erc1155,
389 #[serde(other)]
391 Unknown,
392}
393
394#[cfg_attr(feature = "specta", derive(specta::Type))]
396#[derive(Debug, Clone, Serialize, Deserialize)]
397#[serde(rename_all = "camelCase")]
398pub struct ApprovalContract {
399 pub id: String,
401 pub feature: ApprovalFeature,
403 pub token: String,
405 pub spender: String,
407 pub standard: ApprovalStandard,
409 #[serde(default)]
412 pub amount: Option<Allowance>,
413 pub approved: bool,
415}
416
417#[cfg_attr(feature = "specta", derive(specta::Type))]
419#[derive(Debug, Clone, Serialize, Deserialize)]
420#[serde(rename_all = "camelCase")]
421pub struct ApprovalsResponse {
422 pub address: String,
424 pub chain_id: u64,
426 pub checked_at: String,
432 pub contracts: Vec<ApprovalContract>,
438}
439
440#[cfg_attr(feature = "specta", derive(specta::Type))]
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
443#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
444pub enum ActivitySortBy {
445 #[default]
447 Timestamp,
448 Tokens,
450 Cash,
452}
453
454impl std::fmt::Display for ActivitySortBy {
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 match self {
457 Self::Timestamp => write!(f, "TIMESTAMP"),
458 Self::Tokens => write!(f, "TOKENS"),
459 Self::Cash => write!(f, "CASH"),
460 }
461 }
462}
463
464#[cfg_attr(feature = "specta", derive(specta::Type))]
466#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct Activity {
469 pub proxy_wallet: String,
471 #[cfg_attr(feature = "specta", specta(type = f64))]
473 pub timestamp: i64,
474 pub condition_id: String,
476 #[serde(rename = "type")]
478 pub activity_type: ActivityType,
479 pub size: f64,
481 pub usdc_size: f64,
483 pub transaction_hash: Option<String>,
485 pub price: Option<f64>,
487 pub asset: Option<String>,
489 pub side: Option<String>,
492 pub outcome_index: Option<u32>,
494 pub title: Option<String>,
496 pub slug: Option<String>,
498 pub icon: Option<String>,
500 pub outcome: Option<String>,
502 pub name: Option<String>,
504 pub pseudonym: Option<String>,
506 pub bio: Option<String>,
508 pub profile_image: Option<String>,
510 pub profile_image_optimized: Option<String>,
512}
513
514#[cfg_attr(feature = "specta", derive(specta::Type))]
516#[derive(Debug, Clone, Serialize, Deserialize)]
517#[serde(rename_all = "camelCase")]
518#[non_exhaustive]
519pub struct Position {
520 pub proxy_wallet: String,
522 pub asset: String,
524 pub condition_id: String,
526 pub size: f64,
528 pub avg_price: f64,
530 pub initial_value: f64,
532 #[serde(default)]
539 pub gross_initial_value: Option<f64>,
540 #[serde(default)]
546 pub entry_fees_usdc: Option<f64>,
547 pub current_value: f64,
549 pub cash_pnl: f64,
551 pub percent_pnl: f64,
553 pub total_bought: f64,
555 pub realized_pnl: f64,
557 pub percent_realized_pnl: f64,
559 pub cur_price: f64,
561 pub redeemable: bool,
563 pub mergeable: bool,
565 pub title: String,
567 pub slug: String,
569 pub icon: Option<String>,
571 pub event_slug: Option<String>,
573 pub outcome: String,
575 pub outcome_index: u32,
577 pub opposite_outcome: String,
579 pub opposite_asset: String,
581 pub end_date: Option<String>,
583 pub negative_risk: bool,
585}
586
587#[cfg_attr(feature = "specta", derive(specta::Type))]
592#[derive(Debug, Clone, Serialize, Deserialize)]
593#[serde(rename_all = "camelCase")]
594pub struct MarketPositionV1 {
595 pub proxy_wallet: String,
597 pub name: String,
599 pub profile_image: Option<String>,
601 pub verified: bool,
603 pub asset: String,
605 pub condition_id: String,
607 pub avg_price: f64,
609 pub size: f64,
611 #[serde(rename = "currPrice")]
613 pub curr_price: f64,
614 pub current_value: f64,
616 pub cash_pnl: f64,
618 pub total_bought: f64,
620 pub realized_pnl: f64,
622 pub total_pnl: f64,
624 pub outcome: String,
626 pub outcome_index: u32,
628}
629
630#[cfg_attr(feature = "specta", derive(specta::Type))]
632#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct MetaMarketPositionV1 {
634 pub token: String,
636 pub positions: Vec<MarketPositionV1>,
638}
639
640#[cfg_attr(feature = "specta", derive(specta::Type))]
642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
643#[serde(rename_all = "UPPERCASE")]
644pub enum MarketPositionStatus {
645 Open,
647 Closed,
649 #[default]
651 All,
652}
653
654impl std::fmt::Display for MarketPositionStatus {
655 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656 match self {
657 Self::Open => write!(f, "OPEN"),
658 Self::Closed => write!(f, "CLOSED"),
659 Self::All => write!(f, "ALL"),
660 }
661 }
662}
663
664#[cfg_attr(feature = "specta", derive(specta::Type))]
666#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
667#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
668pub enum MarketPositionSortBy {
669 Tokens,
671 CashPnl,
673 RealizedPnl,
675 #[default]
677 TotalPnl,
678}
679
680impl std::fmt::Display for MarketPositionSortBy {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 match self {
683 Self::Tokens => write!(f, "TOKENS"),
684 Self::CashPnl => write!(f, "CASH_PNL"),
685 Self::RealizedPnl => write!(f, "REALIZED_PNL"),
686 Self::TotalPnl => write!(f, "TOTAL_PNL"),
687 }
688 }
689}
690
691#[cfg_attr(feature = "specta", derive(specta::Type))]
693#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
694#[serde(rename_all = "UPPERCASE")]
695pub enum TimePeriod {
696 #[default]
698 Day,
699 Week,
701 Month,
703 All,
705}
706
707impl std::fmt::Display for TimePeriod {
708 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
709 match self {
710 Self::Day => write!(f, "DAY"),
711 Self::Week => write!(f, "WEEK"),
712 Self::Month => write!(f, "MONTH"),
713 Self::All => write!(f, "ALL"),
714 }
715 }
716}
717
718#[cfg_attr(feature = "specta", derive(specta::Type))]
730#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
731#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
732pub enum ComboStatus {
733 Open,
735 Partial,
737 ResolvedPartial,
740 ResolvedWin,
742 ResolvedLoss,
744 #[serde(other)]
748 Unknown,
749}
750
751impl std::fmt::Display for ComboStatus {
752 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
753 match self {
754 Self::Open => write!(f, "OPEN"),
755 Self::Partial => write!(f, "PARTIAL"),
756 Self::ResolvedPartial => write!(f, "RESOLVED_PARTIAL"),
757 Self::ResolvedWin => write!(f, "RESOLVED_WIN"),
758 Self::ResolvedLoss => write!(f, "RESOLVED_LOSS"),
759 Self::Unknown => write!(f, "UNKNOWN"),
760 }
761 }
762}
763
764#[cfg_attr(feature = "specta", derive(specta::Type))]
766#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
767#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
768pub enum ComboLegStatus {
769 Open,
771 ResolvedPartial,
773 ResolvedWin,
775 ResolvedLoss,
777 #[serde(other)]
779 Unknown,
780}
781
782impl std::fmt::Display for ComboLegStatus {
783 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
784 match self {
785 Self::Open => write!(f, "OPEN"),
786 Self::ResolvedPartial => write!(f, "RESOLVED_PARTIAL"),
787 Self::ResolvedWin => write!(f, "RESOLVED_WIN"),
788 Self::ResolvedLoss => write!(f, "RESOLVED_LOSS"),
789 Self::Unknown => write!(f, "UNKNOWN"),
790 }
791 }
792}
793
794#[cfg_attr(feature = "specta", derive(specta::Type))]
796#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
797#[serde(rename_all = "snake_case")]
798pub enum ComboSort {
799 #[default]
801 CurrentValueDesc,
802 EntryCostDesc,
804 FirstEntryDesc,
806 ResolvedAtDesc,
808 UpdatedAsc,
811}
812
813impl std::fmt::Display for ComboSort {
814 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
815 match self {
816 Self::CurrentValueDesc => write!(f, "current_value_desc"),
817 Self::EntryCostDesc => write!(f, "entry_cost_desc"),
818 Self::FirstEntryDesc => write!(f, "first_entry_desc"),
819 Self::ResolvedAtDesc => write!(f, "resolved_at_desc"),
820 Self::UpdatedAsc => write!(f, "updated_asc"),
821 }
822 }
823}
824
825#[cfg_attr(feature = "specta", derive(specta::Type))]
829#[derive(Debug, Clone, Serialize, Deserialize)]
830pub struct Pagination {
831 pub limit: i64,
833 pub offset: i64,
835 pub has_more: bool,
837 pub next_cursor: Option<String>,
843}
844
845#[cfg_attr(feature = "specta", derive(specta::Type))]
847#[derive(Debug, Clone, Serialize, Deserialize)]
848pub struct ComboEvent {
849 pub event_id: Option<String>,
851 pub event_slug: Option<String>,
853 pub event_title: Option<String>,
855 pub event_image: Option<String>,
857}
858
859#[cfg_attr(feature = "specta", derive(specta::Type))]
861#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct ComboMarket {
863 pub market_id: Option<String>,
865 pub slug: Option<String>,
867 pub title: Option<String>,
869 pub outcome: Option<String>,
871 pub image_url: Option<String>,
873 pub icon_url: Option<String>,
875 pub category: Option<String>,
877 pub subcategory: Option<String>,
879 #[serde(default)]
881 pub tags: Vec<String>,
882 pub end_date: Option<String>,
884 pub event: Option<ComboEvent>,
886}
887
888#[cfg_attr(feature = "specta", derive(specta::Type))]
890#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct ComboLeg {
892 pub leg_index: i64,
894 pub leg_position_id: Option<String>,
896 pub leg_condition_id: Option<String>,
898 pub leg_outcome_index: Option<i64>,
900 pub leg_outcome_label: Option<String>,
902 pub leg_status: Option<ComboLegStatus>,
905 pub leg_resolved_at: Option<String>,
908 pub leg_current_price: Option<String>,
911 pub market: Option<ComboMarket>,
913}
914
915#[cfg_attr(feature = "specta", derive(specta::Type))]
917#[derive(Debug, Clone, Serialize, Deserialize)]
918pub struct ComboPosition {
919 pub combo_condition_id: String,
922 pub combo_position_id: Option<String>,
924 pub module_id: Option<i64>,
926 pub user_address: Option<String>,
928 pub shares_balance: Option<String>,
930 pub entry_avg_price_usdc: Option<String>,
932 pub entry_cost_usdc: Option<String>,
938 pub realized_payout_usdc: Option<String>,
944 pub total_cost_usdc: Option<String>,
947 pub gross_entry_cost_usdc: Option<String>,
954 pub entry_fees_usdc: Option<String>,
957 pub status: Option<ComboStatus>,
959 pub first_entry_at: Option<String>,
961 pub resolved_at: Option<String>,
963 pub updated_at: Option<String>,
968 pub legs_total: Option<i64>,
970 pub legs_resolved: Option<i64>,
972 pub legs_pending: Option<i64>,
974 #[serde(default)]
976 pub legs: Vec<ComboLeg>,
977}
978
979#[cfg_attr(feature = "specta", derive(specta::Type))]
981#[derive(Debug, Clone, Serialize, Deserialize)]
982pub struct CombosResponse {
983 #[serde(default)]
985 pub combos: Vec<ComboPosition>,
986 pub pagination: Option<Pagination>,
988}
989
990#[cfg_attr(feature = "specta", derive(specta::Type))]
992#[derive(Debug, Clone, Serialize, Deserialize)]
993pub struct ComboActivity {
994 pub id: Option<String>,
996 #[serde(rename = "type")]
1003 pub activity_type: Option<String>,
1004 #[deprecated(note = "upstream deprecated this field; use `activity_type` instead")]
1006 pub event_kind: Option<String>,
1007 #[deprecated(note = "upstream deprecated this field; use `activity_type` instead")]
1009 pub side: Option<String>,
1010 pub module_kind: Option<String>,
1012 pub user_address: Option<String>,
1014 pub combo_condition_id: Option<String>,
1016 pub combo_position_id: Option<String>,
1018 pub module_id: Option<i64>,
1020 pub amount_usdc: Option<f64>,
1022 pub payout_usdc: Option<f64>,
1024 pub timestamp: Option<i64>,
1026 pub tx_dttm: Option<String>,
1028 pub tx_hash: Option<String>,
1030 pub log_index: Option<i64>,
1032 pub block_number: Option<i64>,
1034 #[serde(default)]
1036 pub legs: Vec<ComboLeg>,
1037}
1038
1039#[cfg_attr(feature = "specta", derive(specta::Type))]
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1042pub struct CombosActivityResponse {
1043 #[serde(default)]
1045 pub activity: Vec<ComboActivity>,
1046 pub pagination: Option<Pagination>,
1048}
1049
1050#[cfg_attr(feature = "specta", derive(specta::Type))]
1064#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1065pub enum PnlFidelity {
1066 #[default]
1068 #[serde(rename = "1d")]
1069 OneDay,
1070 #[serde(rename = "18h")]
1072 EighteenHours,
1073 #[serde(rename = "12h")]
1075 TwelveHours,
1076 #[serde(rename = "3h")]
1078 ThreeHours,
1079 #[serde(rename = "1h")]
1081 OneHour,
1082}
1083
1084impl std::fmt::Display for PnlFidelity {
1085 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1086 match self {
1087 Self::OneDay => write!(f, "1d"),
1088 Self::EighteenHours => write!(f, "18h"),
1089 Self::TwelveHours => write!(f, "12h"),
1090 Self::ThreeHours => write!(f, "3h"),
1091 Self::OneHour => write!(f, "1h"),
1092 }
1093 }
1094}
1095
1096#[cfg_attr(feature = "specta", derive(specta::Type))]
1098#[derive(Debug, Clone, Serialize, Deserialize)]
1099pub struct PnlPoint {
1100 #[serde(rename = "t")]
1102 pub timestamp: i64,
1103 #[serde(rename = "p")]
1105 pub pnl: f64,
1106}
1107
1108#[cfg_attr(feature = "specta", derive(specta::Type))]
1112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1113pub enum RankingWindow {
1114 #[default]
1116 #[serde(rename = "all")]
1117 All,
1118 #[serde(rename = "1d")]
1120 OneDay,
1121 #[serde(rename = "7d")]
1123 SevenDays,
1124 #[serde(rename = "30d")]
1126 ThirtyDays,
1127}
1128
1129impl std::fmt::Display for RankingWindow {
1130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1131 match self {
1132 Self::All => write!(f, "all"),
1133 Self::OneDay => write!(f, "1d"),
1134 Self::SevenDays => write!(f, "7d"),
1135 Self::ThirtyDays => write!(f, "30d"),
1136 }
1137 }
1138}
1139
1140#[cfg_attr(feature = "specta", derive(specta::Type))]
1142#[derive(Debug, Clone, Serialize, Deserialize)]
1143#[serde(rename_all = "camelCase")]
1144pub struct RankingEntry {
1145 pub proxy_wallet: Option<String>,
1147 pub amount: Option<f64>,
1150 pub name: Option<String>,
1152 pub pseudonym: Option<String>,
1154 pub bio: Option<String>,
1156 pub profile_image: Option<String>,
1158 pub profile_image_optimized: Option<String>,
1160}
1161
1162#[cfg_attr(feature = "specta", derive(specta::Type))]
1164#[derive(Debug, Clone, Serialize, Deserialize)]
1165pub struct OtherSize {
1166 pub id: Option<i64>,
1168 pub user: Option<String>,
1170 pub size: Option<f64>,
1172}
1173
1174#[cfg_attr(feature = "specta", derive(specta::Type))]
1176#[derive(Debug, Clone, Serialize, Deserialize)]
1177pub struct RevisionEntry {
1178 pub revision: Option<String>,
1180 pub timestamp: Option<i64>,
1182}
1183
1184#[cfg_attr(feature = "specta", derive(specta::Type))]
1186#[derive(Debug, Clone, Serialize, Deserialize)]
1187pub struct RevisionPayload {
1188 #[serde(rename = "questionID")]
1190 pub question_id: Option<String>,
1191 #[serde(default)]
1193 pub revisions: Vec<RevisionEntry>,
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199
1200 #[test]
1202 fn position_sort_by_display_matches_serde() {
1203 let variants = [
1204 PositionSortBy::Current,
1205 PositionSortBy::Initial,
1206 PositionSortBy::Tokens,
1207 PositionSortBy::CashPnl,
1208 PositionSortBy::PercentPnl,
1209 PositionSortBy::Title,
1210 PositionSortBy::Resolving,
1211 PositionSortBy::Price,
1212 PositionSortBy::AvgPrice,
1213 ];
1214 for variant in variants {
1215 let serialized = serde_json::to_value(variant).unwrap();
1216 let display = variant.to_string();
1217 assert_eq!(
1218 format!("\"{}\"", display),
1219 serialized.to_string(),
1220 "Display mismatch for {:?}",
1221 variant
1222 );
1223 }
1224 }
1225
1226 #[test]
1228 fn closed_position_sort_by_display_matches_serde() {
1229 let variants = [
1230 ClosedPositionSortBy::RealizedPnl,
1231 ClosedPositionSortBy::Title,
1232 ClosedPositionSortBy::Price,
1233 ClosedPositionSortBy::AvgPrice,
1234 ClosedPositionSortBy::Timestamp,
1235 ];
1236 for variant in variants {
1237 let serialized = serde_json::to_value(variant).unwrap();
1238 let display = variant.to_string();
1239 assert_eq!(
1240 format!("\"{}\"", display),
1241 serialized.to_string(),
1242 "Display mismatch for {:?}",
1243 variant
1244 );
1245 }
1246 }
1247
1248 #[test]
1249 fn activity_sort_by_display_matches_serde() {
1250 let variants = [
1251 ActivitySortBy::Timestamp,
1252 ActivitySortBy::Tokens,
1253 ActivitySortBy::Cash,
1254 ];
1255 for variant in variants {
1256 let serialized = serde_json::to_value(variant).unwrap();
1257 let display = variant.to_string();
1258 assert_eq!(
1259 format!("\"{}\"", display),
1260 serialized.to_string(),
1261 "Display mismatch for {:?}",
1262 variant
1263 );
1264 }
1265 }
1266
1267 #[test]
1268 fn sort_direction_display_matches_serde() {
1269 let variants = [SortDirection::Asc, SortDirection::Desc];
1270 for variant in variants {
1271 let serialized = serde_json::to_value(variant).unwrap();
1272 let display = variant.to_string();
1273 assert_eq!(
1274 format!("\"{}\"", display),
1275 serialized.to_string(),
1276 "Display mismatch for {:?}",
1277 variant
1278 );
1279 }
1280 }
1281
1282 #[test]
1283 fn trade_side_display_matches_serde() {
1284 let variants = [TradeSide::Buy, TradeSide::Sell, TradeSide::Unknown];
1285 for variant in variants {
1286 let serialized = serde_json::to_value(variant).unwrap();
1287 let display = variant.to_string();
1288 assert_eq!(
1289 format!("\"{}\"", display),
1290 serialized.to_string(),
1291 "Display mismatch for {:?}",
1292 variant
1293 );
1294 }
1295 }
1296
1297 #[test]
1298 fn trade_side_falls_back_to_unknown_for_future_values() {
1299 let result: TradeSide = serde_json::from_str("\"SOME_FUTURE_SIDE\"").unwrap();
1300 assert_eq!(result, TradeSide::Unknown);
1301 }
1302
1303 #[test]
1304 fn trade_filter_type_display_matches_serde() {
1305 let variants = [TradeFilterType::Cash, TradeFilterType::Tokens];
1306 for variant in variants {
1307 let serialized = serde_json::to_value(variant).unwrap();
1308 let display = variant.to_string();
1309 assert_eq!(
1310 format!("\"{}\"", display),
1311 serialized.to_string(),
1312 "Display mismatch for {:?}",
1313 variant
1314 );
1315 }
1316 }
1317
1318 #[test]
1319 fn activity_type_display_matches_serde() {
1320 let variants = [
1321 ActivityType::Trade,
1322 ActivityType::Split,
1323 ActivityType::Merge,
1324 ActivityType::Redeem,
1325 ActivityType::Reward,
1326 ActivityType::Conversion,
1327 ActivityType::MakerRebate,
1328 ActivityType::ReferralReward,
1329 ActivityType::TakerRebate,
1330 ActivityType::Unknown,
1331 ];
1332 for variant in variants {
1333 let serialized = serde_json::to_value(variant).unwrap();
1334 let display = variant.to_string();
1335 assert_eq!(
1336 format!("\"{}\"", display),
1337 serialized.to_string(),
1338 "Display mismatch for {:?}",
1339 variant
1340 );
1341 }
1342 }
1343
1344 #[test]
1345 fn activity_type_roundtrip_serde() {
1346 for variant in [
1347 ActivityType::Trade,
1348 ActivityType::Split,
1349 ActivityType::Merge,
1350 ActivityType::Redeem,
1351 ActivityType::Reward,
1352 ActivityType::Conversion,
1353 ActivityType::MakerRebate,
1354 ActivityType::ReferralReward,
1355 ActivityType::TakerRebate,
1356 ] {
1357 let json = serde_json::to_string(&variant).unwrap();
1358 let deserialized: ActivityType = serde_json::from_str(&json).unwrap();
1359 assert_eq!(variant, deserialized);
1360 }
1361 }
1362
1363 #[test]
1364 fn activity_type_falls_back_to_unknown_for_future_types() {
1365 let result: ActivityType = serde_json::from_str("\"SOME_FUTURE_TYPE\"").unwrap();
1367 assert_eq!(result, ActivityType::Unknown);
1368
1369 let result: ActivityType = serde_json::from_str("\"trade\"").unwrap();
1371 assert_eq!(result, ActivityType::Unknown);
1372 }
1373
1374 #[test]
1375 fn sort_direction_default_is_desc() {
1376 assert_eq!(SortDirection::default(), SortDirection::Desc);
1377 }
1378
1379 #[test]
1380 fn closed_position_sort_by_default_is_realized_pnl() {
1381 assert_eq!(
1382 ClosedPositionSortBy::default(),
1383 ClosedPositionSortBy::RealizedPnl
1384 );
1385 }
1386
1387 #[test]
1388 fn activity_sort_by_default_is_timestamp() {
1389 assert_eq!(ActivitySortBy::default(), ActivitySortBy::Timestamp);
1390 }
1391
1392 #[test]
1393 fn position_sort_by_serde_roundtrip() {
1394 for variant in [
1395 PositionSortBy::Current,
1396 PositionSortBy::Initial,
1397 PositionSortBy::Tokens,
1398 PositionSortBy::CashPnl,
1399 PositionSortBy::PercentPnl,
1400 PositionSortBy::Title,
1401 PositionSortBy::Resolving,
1402 PositionSortBy::Price,
1403 PositionSortBy::AvgPrice,
1404 ] {
1405 let json = serde_json::to_string(&variant).unwrap();
1406 let deserialized: PositionSortBy = serde_json::from_str(&json).unwrap();
1407 assert_eq!(variant, deserialized);
1408 }
1409 }
1410
1411 #[test]
1412 fn deserialize_position_from_json() {
1413 let json = r#"{
1414 "proxyWallet": "0xabc123",
1415 "asset": "token123",
1416 "conditionId": "cond456",
1417 "size": 100.5,
1418 "avgPrice": 0.65,
1419 "initialValue": 65.0,
1420 "currentValue": 70.0,
1421 "cashPnl": 5.0,
1422 "percentPnl": 7.69,
1423 "totalBought": 100.5,
1424 "realizedPnl": 2.0,
1425 "percentRealizedPnl": 3.08,
1426 "curPrice": 0.70,
1427 "redeemable": false,
1428 "mergeable": true,
1429 "title": "Will X happen?",
1430 "slug": "will-x-happen",
1431 "icon": "https://example.com/icon.png",
1432 "eventSlug": "x-event",
1433 "outcome": "Yes",
1434 "outcomeIndex": 0,
1435 "oppositeOutcome": "No",
1436 "oppositeAsset": "token789",
1437 "endDate": "2025-12-31",
1438 "negativeRisk": false
1439 }"#;
1440
1441 let pos: Position = serde_json::from_str(json).unwrap();
1442 assert_eq!(pos.proxy_wallet, "0xabc123");
1443 assert_eq!(pos.asset, "token123");
1444 assert_eq!(pos.condition_id, "cond456");
1445 assert!((pos.size - 100.5).abs() < f64::EPSILON);
1446 assert!((pos.avg_price - 0.65).abs() < f64::EPSILON);
1447 assert!((pos.initial_value - 65.0).abs() < f64::EPSILON);
1448 assert!((pos.current_value - 70.0).abs() < f64::EPSILON);
1449 assert!((pos.cash_pnl - 5.0).abs() < f64::EPSILON);
1450 assert!(!pos.redeemable);
1451 assert!(pos.mergeable);
1452 assert_eq!(pos.title, "Will X happen?");
1453 assert_eq!(pos.outcome, "Yes");
1454 assert_eq!(pos.outcome_index, 0);
1455 assert_eq!(pos.opposite_outcome, "No");
1456 assert!(!pos.negative_risk);
1457 assert_eq!(pos.icon, Some("https://example.com/icon.png".to_string()));
1458 assert_eq!(pos.event_slug, Some("x-event".to_string()));
1459 }
1460
1461 #[test]
1462 fn deserialize_position_with_null_optionals() {
1463 let json = r#"{
1464 "proxyWallet": "0xabc123",
1465 "asset": "token123",
1466 "conditionId": "cond456",
1467 "size": 0.0,
1468 "avgPrice": 0.0,
1469 "initialValue": 0.0,
1470 "currentValue": 0.0,
1471 "cashPnl": 0.0,
1472 "percentPnl": 0.0,
1473 "totalBought": 0.0,
1474 "realizedPnl": 0.0,
1475 "percentRealizedPnl": 0.0,
1476 "curPrice": 0.0,
1477 "redeemable": false,
1478 "mergeable": false,
1479 "title": "Test",
1480 "slug": "test",
1481 "icon": null,
1482 "eventSlug": null,
1483 "outcome": "No",
1484 "outcomeIndex": 1,
1485 "oppositeOutcome": "Yes",
1486 "oppositeAsset": "token000",
1487 "endDate": null,
1488 "negativeRisk": true
1489 }"#;
1490
1491 let pos: Position = serde_json::from_str(json).unwrap();
1492 assert!(pos.icon.is_none());
1493 assert!(pos.event_slug.is_none());
1494 assert!(pos.end_date.is_none());
1495 assert!(pos.negative_risk);
1496 }
1497
1498 #[test]
1499 fn deserialize_closed_position_from_json() {
1500 let json = r#"{
1501 "proxyWallet": "0xdef456",
1502 "asset": "token_closed",
1503 "conditionId": "cond_closed",
1504 "avgPrice": 0.45,
1505 "totalBought": 200.0,
1506 "realizedPnl": -10.0,
1507 "curPrice": 0.35,
1508 "timestamp": 1700000000,
1509 "title": "Closed market?",
1510 "slug": "closed-market",
1511 "icon": null,
1512 "eventSlug": "closed-event",
1513 "outcome": "No",
1514 "outcomeIndex": 1,
1515 "oppositeOutcome": "Yes",
1516 "oppositeAsset": "token_opp",
1517 "endDate": "2024-06-30"
1518 }"#;
1519
1520 let closed: ClosedPosition = serde_json::from_str(json).unwrap();
1521 assert_eq!(closed.proxy_wallet, "0xdef456");
1522 assert!((closed.avg_price - 0.45).abs() < f64::EPSILON);
1523 assert!((closed.realized_pnl - (-10.0)).abs() < f64::EPSILON);
1524 assert_eq!(closed.timestamp, 1700000000);
1525 assert_eq!(closed.outcome, "No");
1526 assert_eq!(closed.outcome_index, 1);
1527 assert!(closed.icon.is_none());
1528 assert_eq!(closed.event_slug, Some("closed-event".to_string()));
1529 }
1530
1531 #[test]
1532 fn deserialize_trade_from_json() {
1533 let json = r#"{
1534 "proxyWallet": "0x1234",
1535 "side": "BUY",
1536 "asset": "token_buy",
1537 "conditionId": "cond_trade",
1538 "size": 50.0,
1539 "price": 0.72,
1540 "timestamp": 1700001000,
1541 "title": "Trade market?",
1542 "slug": "trade-market",
1543 "icon": "https://example.com/trade.png",
1544 "eventSlug": null,
1545 "outcome": "Yes",
1546 "outcomeIndex": 0,
1547 "name": "TraderOne",
1548 "pseudonym": "t1",
1549 "bio": "A trader",
1550 "profileImage": null,
1551 "profileImageOptimized": null,
1552 "transactionHash": "0xhash123"
1553 }"#;
1554
1555 let trade: Trade = serde_json::from_str(json).unwrap();
1556 assert_eq!(trade.proxy_wallet, "0x1234");
1557 assert_eq!(trade.side, TradeSide::Buy);
1558 assert!((trade.size - 50.0).abs() < f64::EPSILON);
1559 assert!((trade.price - 0.72).abs() < f64::EPSILON);
1560 assert_eq!(trade.timestamp, 1700001000);
1561 assert_eq!(trade.name, Some("TraderOne".to_string()));
1562 assert_eq!(trade.transaction_hash, Some("0xhash123".to_string()));
1563 assert!(trade.profile_image.is_none());
1564 }
1565
1566 #[test]
1567 fn deserialize_trade_sell_side() {
1568 let json = r#"{
1569 "proxyWallet": "0x5678",
1570 "side": "SELL",
1571 "asset": "token_sell",
1572 "conditionId": "cond_sell",
1573 "size": 25.0,
1574 "price": 0.30,
1575 "timestamp": 1700002000,
1576 "title": "Sell test",
1577 "slug": "sell-test",
1578 "icon": null,
1579 "eventSlug": null,
1580 "outcome": "No",
1581 "outcomeIndex": 1,
1582 "name": null,
1583 "pseudonym": null,
1584 "bio": null,
1585 "profileImage": null,
1586 "profileImageOptimized": null,
1587 "transactionHash": null
1588 }"#;
1589
1590 let trade: Trade = serde_json::from_str(json).unwrap();
1591 assert_eq!(trade.side, TradeSide::Sell);
1592 assert!(trade.name.is_none());
1593 assert!(trade.transaction_hash.is_none());
1594 }
1595
1596 #[test]
1597 fn deserialize_activity_from_json() {
1598 let json = r#"{
1599 "proxyWallet": "0xact123",
1600 "timestamp": 1700003000,
1601 "conditionId": "cond_act",
1602 "type": "TRADE",
1603 "size": 10.0,
1604 "usdcSize": 7.50,
1605 "transactionHash": "0xacthash",
1606 "price": 0.75,
1607 "asset": "token_act",
1608 "side": "BUY",
1609 "outcomeIndex": 0,
1610 "title": "Activity market",
1611 "slug": "activity-market",
1612 "icon": null,
1613 "outcome": "Yes",
1614 "name": null,
1615 "pseudonym": null,
1616 "bio": null,
1617 "profileImage": null,
1618 "profileImageOptimized": null
1619 }"#;
1620
1621 let activity: Activity = serde_json::from_str(json).unwrap();
1622 assert_eq!(activity.proxy_wallet, "0xact123");
1623 assert_eq!(activity.activity_type, ActivityType::Trade);
1624 assert!((activity.size - 10.0).abs() < f64::EPSILON);
1625 assert!((activity.usdc_size - 7.50).abs() < f64::EPSILON);
1626 assert_eq!(activity.side, Some("BUY".to_string()));
1627 assert_eq!(activity.outcome_index, Some(0));
1628 }
1629
1630 #[test]
1631 fn deserialize_activity_merge_type() {
1632 let json = r#"{
1633 "proxyWallet": "0xmerge",
1634 "timestamp": 1700004000,
1635 "conditionId": "cond_merge",
1636 "type": "MERGE",
1637 "size": 5.0,
1638 "usdcSize": 3.0,
1639 "transactionHash": null,
1640 "price": null,
1641 "asset": null,
1642 "side": "",
1643 "outcomeIndex": null,
1644 "title": null,
1645 "slug": null,
1646 "icon": null,
1647 "outcome": null,
1648 "name": null,
1649 "pseudonym": null,
1650 "bio": null,
1651 "profileImage": null,
1652 "profileImageOptimized": null
1653 }"#;
1654
1655 let activity: Activity = serde_json::from_str(json).unwrap();
1656 assert_eq!(activity.activity_type, ActivityType::Merge);
1657 assert_eq!(activity.side, Some("".to_string()));
1659 assert!(activity.price.is_none());
1660 assert!(activity.asset.is_none());
1661 assert!(activity.title.is_none());
1662 }
1663
1664 #[test]
1665 fn deserialize_user_value() {
1666 let json = r#"{"user": "0xuser", "value": 1234.56}"#;
1667 let uv: UserValue = serde_json::from_str(json).unwrap();
1668 assert_eq!(uv.user, "0xuser");
1669 assert!((uv.value - 1234.56).abs() < f64::EPSILON);
1670 }
1671
1672 #[test]
1673 fn deserialize_open_interest() {
1674 let json = r#"{"market": "0xcond", "value": 50000.0}"#;
1675 let oi: OpenInterest = serde_json::from_str(json).unwrap();
1676 assert_eq!(oi.market, "0xcond");
1677 assert!((oi.value - 50000.0).abs() < f64::EPSILON);
1678 }
1679
1680 #[test]
1681 fn market_position_status_display_matches_serde() {
1682 for variant in [
1683 MarketPositionStatus::Open,
1684 MarketPositionStatus::Closed,
1685 MarketPositionStatus::All,
1686 ] {
1687 let serialized = serde_json::to_value(variant).unwrap();
1688 assert_eq!(format!("\"{}\"", variant), serialized.to_string());
1689 }
1690 }
1691
1692 #[test]
1693 fn market_position_status_default_is_all() {
1694 assert_eq!(MarketPositionStatus::default(), MarketPositionStatus::All);
1695 }
1696
1697 #[test]
1698 fn market_position_sort_by_display_matches_serde() {
1699 for variant in [
1700 MarketPositionSortBy::Tokens,
1701 MarketPositionSortBy::CashPnl,
1702 MarketPositionSortBy::RealizedPnl,
1703 MarketPositionSortBy::TotalPnl,
1704 ] {
1705 let serialized = serde_json::to_value(variant).unwrap();
1706 assert_eq!(format!("\"{}\"", variant), serialized.to_string());
1707 }
1708 }
1709
1710 #[test]
1711 fn market_position_sort_by_default_is_total_pnl() {
1712 assert_eq!(
1713 MarketPositionSortBy::default(),
1714 MarketPositionSortBy::TotalPnl
1715 );
1716 }
1717
1718 #[test]
1719 fn deserialize_market_position_v1() {
1720 let json = r#"{
1722 "proxyWallet": "0xabc",
1723 "name": "Alice",
1724 "profileImage": "https://example.com/a.png",
1725 "verified": true,
1726 "asset": "token_a",
1727 "conditionId": "cond_mp",
1728 "avgPrice": 0.42,
1729 "size": 1234.5,
1730 "currPrice": 0.51,
1731 "currentValue": 629.60,
1732 "cashPnl": 110.0,
1733 "totalBought": 520.0,
1734 "realizedPnl": 15.5,
1735 "totalPnl": 125.5,
1736 "outcome": "Yes",
1737 "outcomeIndex": 0
1738 }"#;
1739
1740 let pos: MarketPositionV1 = serde_json::from_str(json).unwrap();
1741 assert_eq!(pos.proxy_wallet, "0xabc");
1742 assert_eq!(pos.name, "Alice");
1743 assert_eq!(
1744 pos.profile_image.as_deref(),
1745 Some("https://example.com/a.png")
1746 );
1747 assert!(pos.verified);
1748 assert_eq!(pos.asset, "token_a");
1749 assert_eq!(pos.condition_id, "cond_mp");
1750 assert!((pos.avg_price - 0.42).abs() < f64::EPSILON);
1751 assert!((pos.size - 1234.5).abs() < f64::EPSILON);
1752 assert!((pos.curr_price - 0.51).abs() < f64::EPSILON);
1753 assert!((pos.current_value - 629.60).abs() < f64::EPSILON);
1754 assert!((pos.cash_pnl - 110.0).abs() < f64::EPSILON);
1755 assert!((pos.total_bought - 520.0).abs() < f64::EPSILON);
1756 assert!((pos.realized_pnl - 15.5).abs() < f64::EPSILON);
1757 assert!((pos.total_pnl - 125.5).abs() < f64::EPSILON);
1758 assert_eq!(pos.outcome, "Yes");
1759 assert_eq!(pos.outcome_index, 0);
1760 }
1761
1762 #[test]
1763 fn market_position_v1_roundtrip() {
1764 let original = MarketPositionV1 {
1765 proxy_wallet: "0xabc".into(),
1766 name: "Alice".into(),
1767 profile_image: None,
1768 verified: false,
1769 asset: "token_a".into(),
1770 condition_id: "cond_mp".into(),
1771 avg_price: 0.5,
1772 size: 10.0,
1773 curr_price: 0.6,
1774 current_value: 6.0,
1775 cash_pnl: 1.0,
1776 total_bought: 5.0,
1777 realized_pnl: 0.0,
1778 total_pnl: 1.0,
1779 outcome: "No".into(),
1780 outcome_index: 1,
1781 };
1782 let json = serde_json::to_string(&original).unwrap();
1783 assert!(json.contains("\"currPrice\""));
1785 let back: MarketPositionV1 = serde_json::from_str(&json).unwrap();
1786 assert_eq!(back.proxy_wallet, original.proxy_wallet);
1787 assert_eq!(back.outcome_index, original.outcome_index);
1788 assert!((back.curr_price - original.curr_price).abs() < f64::EPSILON);
1789 }
1790
1791 #[test]
1792 fn deserialize_meta_market_position_v1() {
1793 let json = r#"{
1794 "token": "token_a",
1795 "positions": [
1796 {
1797 "proxyWallet": "0xabc",
1798 "name": "Alice",
1799 "profileImage": null,
1800 "verified": false,
1801 "asset": "token_a",
1802 "conditionId": "cond_mp",
1803 "avgPrice": 0.42,
1804 "size": 100.0,
1805 "currPrice": 0.51,
1806 "currentValue": 51.0,
1807 "cashPnl": 9.0,
1808 "totalBought": 42.0,
1809 "realizedPnl": 0.0,
1810 "totalPnl": 9.0,
1811 "outcome": "Yes",
1812 "outcomeIndex": 0
1813 }
1814 ]
1815 }"#;
1816
1817 let meta: MetaMarketPositionV1 = serde_json::from_str(json).unwrap();
1818 assert_eq!(meta.token, "token_a");
1819 assert_eq!(meta.positions.len(), 1);
1820 assert_eq!(meta.positions[0].name, "Alice");
1821 assert!(meta.positions[0].profile_image.is_none());
1822 }
1823
1824 #[test]
1825 fn deserialize_position_fee_basis() {
1826 let json = r#"{
1830 "proxyWallet": "0xabc123",
1831 "asset": "token123",
1832 "conditionId": "cond456",
1833 "size": 100.5,
1834 "avgPrice": 0.65,
1835 "initialValue": 65.0,
1836 "grossInitialValue": 65.5,
1837 "entryFeesUsdc": 0,
1838 "currentValue": 70.0,
1839 "cashPnl": 5.0,
1840 "percentPnl": 7.69,
1841 "totalBought": 100.5,
1842 "realizedPnl": 2.0,
1843 "percentRealizedPnl": 3.08,
1844 "curPrice": 0.70,
1845 "redeemable": false,
1846 "mergeable": true,
1847 "title": "Will X happen?",
1848 "slug": "will-x-happen",
1849 "outcome": "Yes",
1850 "outcomeIndex": 0,
1851 "oppositeOutcome": "No",
1852 "oppositeAsset": "token789",
1853 "negativeRisk": false
1854 }"#;
1855
1856 let pos: Position = serde_json::from_str(json).unwrap();
1857 assert_eq!(pos.gross_initial_value, Some(65.5));
1858 assert_eq!(pos.entry_fees_usdc, Some(0.0));
1859 assert!((pos.initial_value - 65.0).abs() < f64::EPSILON);
1861 }
1862
1863 #[test]
1864 fn deserialize_position_without_fee_basis_is_none() {
1865 let json = r#"{
1867 "proxyWallet": "0xabc123",
1868 "asset": "token123",
1869 "conditionId": "cond456",
1870 "size": 100.5,
1871 "avgPrice": 0.65,
1872 "initialValue": 65.0,
1873 "currentValue": 70.0,
1874 "cashPnl": 5.0,
1875 "percentPnl": 7.69,
1876 "totalBought": 100.5,
1877 "realizedPnl": 2.0,
1878 "percentRealizedPnl": 3.08,
1879 "curPrice": 0.70,
1880 "redeemable": false,
1881 "mergeable": true,
1882 "title": "Will X happen?",
1883 "slug": "will-x-happen",
1884 "outcome": "Yes",
1885 "outcomeIndex": 0,
1886 "oppositeOutcome": "No",
1887 "oppositeAsset": "token789",
1888 "negativeRisk": false
1889 }"#;
1890
1891 let pos: Position = serde_json::from_str(json).unwrap();
1892 assert_eq!(pos.gross_initial_value, None);
1893 assert_eq!(pos.entry_fees_usdc, None);
1894 }
1895
1896 #[test]
1897 fn deserialize_allowance_max_sentinel() {
1898 let v: Allowance = serde_json::from_str(r#""max""#).unwrap();
1899 assert_eq!(v, Allowance::Max);
1900 }
1901
1902 #[test]
1903 fn deserialize_allowance_decimal_amount() {
1904 let v: Allowance = serde_json::from_str(r#""1000000""#).unwrap();
1905 assert_eq!(
1906 v,
1907 Allowance::Amount(rust_decimal::Decimal::new(1_000_000, 0))
1908 );
1909 }
1910
1911 #[test]
1912 fn deserialize_allowance_beyond_decimal_range_is_unknown() {
1913 let huge = "1".repeat(40);
1916 let json = format!(r#""{huge}""#);
1917 let v: Allowance = serde_json::from_str(&json).unwrap();
1918 assert_eq!(v, Allowance::Unknown(huge));
1919 }
1920
1921 #[test]
1922 fn deserialize_allowance_unrecognized_sentinel_is_unknown() {
1923 let v: Allowance = serde_json::from_str(r#""unlimited""#).unwrap();
1924 assert_eq!(v, Allowance::Unknown("unlimited".to_string()));
1925 }
1926
1927 #[test]
1928 fn deserialize_approvals_response() {
1929 let json = r#"{
1930 "address": "0xabc123",
1931 "chainId": 137,
1932 "checkedAt": "2026-08-10T12:34:56Z",
1933 "contracts": [
1934 {
1935 "id": "UsdcExchange",
1936 "feature": "trading",
1937 "token": "0xtoken",
1938 "spender": "0xspender",
1939 "standard": "ERC20",
1940 "amount": "max",
1941 "approved": true
1942 },
1943 {
1944 "id": "CtfExchangeIsApprovedForAll",
1945 "feature": "auto-redeem",
1946 "token": "0xctf",
1947 "spender": "0xspender2",
1948 "standard": "ERC1155",
1949 "approved": false
1950 }
1951 ]
1952 }"#;
1953
1954 let resp: ApprovalsResponse = serde_json::from_str(json).unwrap();
1955 assert_eq!(resp.chain_id, 137);
1956 assert_eq!(resp.contracts.len(), 2);
1957
1958 let erc20 = &resp.contracts[0];
1959 assert_eq!(erc20.feature, ApprovalFeature::Trading);
1960 assert_eq!(erc20.standard, ApprovalStandard::Erc20);
1961 assert_eq!(erc20.amount, Some(Allowance::Max));
1962 assert!(erc20.approved);
1963
1964 let erc1155 = &resp.contracts[1];
1966 assert_eq!(erc1155.feature, ApprovalFeature::AutoRedeem);
1967 assert_eq!(erc1155.standard, ApprovalStandard::Erc1155);
1968 assert_eq!(erc1155.amount, None);
1969 assert!(!erc1155.approved);
1970 }
1971
1972 #[test]
1973 fn deserialize_approval_enums_tolerate_unknown_variants() {
1974 let json = r#"{
1977 "id": "SomethingNew",
1978 "feature": "staking",
1979 "token": "0xtoken",
1980 "spender": "0xspender",
1981 "standard": "ERC721",
1982 "approved": true
1983 }"#;
1984
1985 let c: ApprovalContract = serde_json::from_str(json).unwrap();
1986 assert_eq!(c.feature, ApprovalFeature::Unknown);
1987 assert_eq!(c.standard, ApprovalStandard::Unknown);
1988 }
1989}