polymarket_sdk/
types.rs

1//! Common types for Polymarket SDK
2//!
3//! This module contains shared types used across different API clients,
4//! including trading types, market data structures, and authentication types.
5
6#[cfg(feature = "auth")]
7use alloy_primitives::U256;
8use chrono::{DateTime, Utc};
9use rust_decimal::Decimal;
10use serde::{de, Deserialize, Deserializer, Serialize};
11use serde_json::Value;
12use std::str::FromStr;
13
14// ============================================================================
15// Trading Types
16// ============================================================================
17
18/// Trading side for orders
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "UPPERCASE")]
21pub enum Side {
22    Buy,
23    Sell,
24}
25
26impl Side {
27    /// Get string representation
28    #[must_use]
29    pub fn as_str(&self) -> &'static str {
30        match self {
31            Self::Buy => "BUY",
32            Self::Sell => "SELL",
33        }
34    }
35
36    /// Get opposite side
37    #[must_use]
38    pub fn opposite(&self) -> Self {
39        match self {
40            Self::Buy => Self::Sell,
41            Self::Sell => Self::Buy,
42        }
43    }
44}
45
46/// Order book level (price/size pair)
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct BookLevel {
49    #[serde(with = "rust_decimal::serde::str")]
50    pub price: Decimal,
51    #[serde(with = "rust_decimal::serde::str")]
52    pub size: Decimal,
53}
54
55// ============================================================================
56// Authentication Types
57// ============================================================================
58
59/// API credentials for Polymarket authentication
60#[derive(Debug, Clone, Serialize, Deserialize, Default)]
61pub struct ApiCredentials {
62    /// API key
63    #[serde(rename = "apiKey")]
64    pub api_key: String,
65    /// API secret (base64 encoded)
66    pub secret: String,
67    /// API passphrase
68    pub passphrase: String,
69}
70
71impl ApiCredentials {
72    /// Create new API credentials
73    #[must_use]
74    pub fn new(
75        api_key: impl Into<String>,
76        secret: impl Into<String>,
77        passphrase: impl Into<String>,
78    ) -> Self {
79        Self {
80            api_key: api_key.into(),
81            secret: secret.into(),
82            passphrase: passphrase.into(),
83        }
84    }
85
86    /// Check if credentials are configured
87    #[must_use]
88    pub fn is_configured(&self) -> bool {
89        !self.api_key.is_empty() && !self.secret.is_empty() && !self.passphrase.is_empty()
90    }
91}
92
93// ============================================================================
94// Order Types
95// ============================================================================
96
97/// Configuration options for order creation
98#[derive(Debug, Clone, Default)]
99pub struct OrderOptions {
100    /// Tick size for price rounding
101    pub tick_size: Option<Decimal>,
102    /// Whether to use negative risk contracts
103    pub neg_risk: Option<bool>,
104    /// Fee rate in basis points
105    pub fee_rate_bps: Option<u32>,
106}
107
108impl OrderOptions {
109    /// Create new order options
110    #[must_use]
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    /// Set tick size
116    #[must_use]
117    pub fn with_tick_size(mut self, tick_size: Decimal) -> Self {
118        self.tick_size = Some(tick_size);
119        self
120    }
121
122    /// Set negative risk flag
123    #[must_use]
124    pub fn with_neg_risk(mut self, neg_risk: bool) -> Self {
125        self.neg_risk = Some(neg_risk);
126        self
127    }
128
129    /// Set fee rate in basis points
130    #[must_use]
131    pub fn with_fee_rate_bps(mut self, fee_rate_bps: u32) -> Self {
132        self.fee_rate_bps = Some(fee_rate_bps);
133        self
134    }
135}
136
137/// Extra arguments for order creation
138#[cfg(feature = "auth")]
139#[derive(Debug, Clone)]
140pub struct ExtraOrderArgs {
141    /// Fee rate in basis points
142    pub fee_rate_bps: u32,
143    /// Nonce for replay protection
144    pub nonce: U256,
145    /// Taker address (usually zero address)
146    pub taker: String,
147}
148
149#[cfg(feature = "auth")]
150impl Default for ExtraOrderArgs {
151    fn default() -> Self {
152        Self {
153            fee_rate_bps: 0,
154            nonce: U256::ZERO,
155            taker: "0x0000000000000000000000000000000000000000".to_string(),
156        }
157    }
158}
159
160#[cfg(feature = "auth")]
161impl ExtraOrderArgs {
162    /// Create new extra order args
163    #[must_use]
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    /// Set fee rate in basis points
169    #[must_use]
170    pub fn with_fee_rate_bps(mut self, fee_rate_bps: u32) -> Self {
171        self.fee_rate_bps = fee_rate_bps;
172        self
173    }
174
175    /// Set nonce
176    #[must_use]
177    pub fn with_nonce(mut self, nonce: U256) -> Self {
178        self.nonce = nonce;
179        self
180    }
181
182    /// Set taker address
183    #[must_use]
184    pub fn with_taker(mut self, taker: impl Into<String>) -> Self {
185        self.taker = taker.into();
186        self
187    }
188}
189
190/// Market order arguments
191#[derive(Debug, Clone)]
192pub struct MarketOrderArgs {
193    /// Token ID (condition token)
194    pub token_id: String,
195    /// Amount to trade
196    pub amount: Decimal,
197}
198
199impl MarketOrderArgs {
200    /// Create new market order args
201    #[must_use]
202    pub fn new(token_id: impl Into<String>, amount: Decimal) -> Self {
203        Self {
204            token_id: token_id.into(),
205            amount,
206        }
207    }
208}
209
210/// Signed order request ready for submission
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct SignedOrderRequest {
214    /// Random salt for uniqueness
215    pub salt: u64,
216    /// Maker/funder address
217    pub maker: String,
218    /// Signer address
219    pub signer: String,
220    /// Taker address (usually zero)
221    pub taker: String,
222    /// Token ID
223    pub token_id: String,
224    /// Maker amount in token units
225    pub maker_amount: String,
226    /// Taker amount in token units
227    pub taker_amount: String,
228    /// Expiration timestamp
229    pub expiration: String,
230    /// Nonce for replay protection
231    pub nonce: String,
232    /// Fee rate in basis points
233    pub fee_rate_bps: String,
234    /// Order side (BUY/SELL)
235    pub side: String,
236    /// Signature type
237    pub signature_type: u8,
238    /// EIP-712 signature
239    pub signature: String,
240}
241
242/// Order type for CLOB orders
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
244#[serde(rename_all = "UPPERCASE")]
245pub enum OrderType {
246    /// Good Till Cancelled (default for limit orders)
247    GTC,
248    /// Fill Or Kill (for market orders)
249    FOK,
250    /// Good Till Date
251    GTD,
252    /// Fill And Kill
253    FAK,
254}
255
256impl Default for OrderType {
257    fn default() -> Self {
258        OrderType::GTC
259    }
260}
261
262/// NewOrder is the payload structure for posting orders to the Polymarket API
263/// It wraps order data with orderType, owner, and deferExec fields
264/// IMPORTANT: Field order MUST match TypeScript SDK for HMAC signature compatibility
265#[derive(Debug, Clone, Serialize, Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct NewOrder {
268    /// Whether to defer execution (MUST be first field for JSON field order)
269    #[serde(default)]
270    pub defer_exec: bool,
271    /// The order data
272    pub order: NewOrderData,
273    /// Owner - should be the API key, NOT the wallet address
274    pub owner: String,
275    /// Order type (GTC, FOK, etc.)
276    pub order_type: OrderType,
277}
278
279/// NewOrderData contains the actual order fields
280/// Note: salt must be a number (i64) in JSON, not a string
281/// IMPORTANT: Field order MUST match TypeScript SDK for HMAC signature compatibility
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct NewOrderData {
285    /// Random salt for uniqueness - MUST be a number in JSON
286    pub salt: i64,
287    /// Maker/funder address
288    pub maker: String,
289    /// Signer address
290    pub signer: String,
291    /// Taker address (usually zero)
292    pub taker: String,
293    /// Token ID
294    pub token_id: String,
295    /// Maker amount in token units
296    pub maker_amount: String,
297    /// Taker amount in token units
298    pub taker_amount: String,
299    /// Order side (BUY/SELL) - MUST come after takerAmount, before expiration
300    pub side: String,
301    /// Expiration timestamp
302    pub expiration: String,
303    /// Nonce for replay protection
304    pub nonce: String,
305    /// Fee rate in basis points
306    pub fee_rate_bps: String,
307    /// Signature type (0=EOA, 1=PolyProxy, 2=PolyGnosisSafe)
308    pub signature_type: u8,
309    /// EIP-712 signature
310    pub signature: String,
311}
312
313impl NewOrder {
314    /// Convert SignedOrderRequest to NewOrder format for API submission
315    /// Field initialization order matches struct field order for consistency
316    pub fn from_signed_order(
317        order: &SignedOrderRequest,
318        api_key: &str,
319        order_type: OrderType,
320        defer_exec: bool,
321    ) -> Self {
322        NewOrder {
323            defer_exec,
324            order: NewOrderData {
325                // Salt must be i64 for JSON serialization as number
326                salt: order.salt as i64,
327                maker: order.maker.clone(),
328                signer: order.signer.clone(),
329                taker: order.taker.clone(),
330                token_id: order.token_id.clone(),
331                maker_amount: order.maker_amount.clone(),
332                taker_amount: order.taker_amount.clone(),
333                side: order.side.clone(),
334                expiration: order.expiration.clone(),
335                nonce: order.nonce.clone(),
336                fee_rate_bps: order.fee_rate_bps.clone(),
337                signature_type: order.signature_type,
338                signature: order.signature.clone(),
339            },
340            owner: api_key.to_string(),
341            order_type,
342        }
343    }
344}
345
346// ============================================================================
347// Market Types
348// ============================================================================
349
350/// Market token information
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct Token {
353    /// Token ID (condition token)
354    pub token_id: String,
355    /// Outcome name (e.g., "Yes", "No")
356    pub outcome: String,
357    /// Current price if available
358    #[serde(skip_serializing_if = "Option::is_none")]
359    #[serde(default, deserialize_with = "deserialize_decimal_opt")]
360    pub price: Option<Decimal>,
361}
362
363/// Market information from Gamma API
364#[derive(Debug, Clone, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct Market {
367    /// Condition ID (market identifier)
368    pub condition_id: String,
369    /// Market slug for URL
370    pub slug: String,
371    /// Market question/title
372    #[serde(default)]
373    pub question: Option<String>,
374    /// Market description
375    #[serde(default)]
376    pub description: Option<String>,
377    /// Category/tag
378    #[serde(default)]
379    pub category: Option<String>,
380    /// Whether market is active
381    pub active: bool,
382    /// Whether market is closed
383    pub closed: bool,
384    /// Market end date
385    #[serde(default)]
386    pub end_date: Option<String>,
387    /// Market icon URL
388    #[serde(default)]
389    pub icon: Option<String>,
390    /// CLOB token IDs (JSON string array)
391    #[serde(default)]
392    pub clob_token_ids: Option<String>,
393    /// Outcomes (JSON string array)
394    #[serde(default)]
395    pub outcomes: Option<String>,
396    /// Outcome prices (JSON string array, e.g. "[\"0.95\", \"0.05\"]")
397    #[serde(default)]
398    pub outcome_prices: Option<String>,
399    /// Liquidity
400    #[serde(default, deserialize_with = "deserialize_decimal_opt")]
401    pub liquidity_num: Option<Decimal>,
402    /// 24-hour volume
403    #[serde(
404        default,
405        rename = "volume24hr",
406        deserialize_with = "deserialize_decimal_opt"
407    )]
408    pub volume_24hr: Option<Decimal>,
409    /// Total volume
410    #[serde(default, deserialize_with = "deserialize_decimal_opt")]
411    pub volume_num: Option<Decimal>,
412    /// Minimum order size
413    #[serde(default, deserialize_with = "deserialize_decimal_opt")]
414    pub order_min_size: Option<Decimal>,
415    /// Price tick size
416    #[serde(
417        default,
418        rename = "orderPriceMinTickSize",
419        deserialize_with = "deserialize_decimal_opt"
420    )]
421    pub order_tick_size: Option<Decimal>,
422}
423
424impl Market {
425    /// Parse CLOB token IDs from JSON string
426    #[must_use]
427    pub fn parse_token_ids(&self) -> Vec<String> {
428        self.clob_token_ids
429            .as_ref()
430            .and_then(|raw| serde_json::from_str(raw).ok())
431            .unwrap_or_default()
432    }
433
434    /// Parse outcomes from JSON string
435    #[must_use]
436    pub fn parse_outcomes(&self) -> Vec<String> {
437        self.outcomes
438            .as_ref()
439            .and_then(|raw| serde_json::from_str(raw).ok())
440            .unwrap_or_else(|| vec!["Yes".to_string(), "No".to_string()])
441    }
442
443    /// Parse outcome prices from JSON string
444    /// Returns (yes_price, no_price) as `Option<f64>` values
445    #[must_use]
446    pub fn parse_outcome_prices(&self) -> (Option<f64>, Option<f64>) {
447        let prices: Vec<String> = self
448            .outcome_prices
449            .as_ref()
450            .and_then(|raw| serde_json::from_str(raw).ok())
451            .unwrap_or_default();
452
453        let yes_price = prices.first().and_then(|s| s.parse::<f64>().ok());
454        let no_price = prices.get(1).and_then(|s| s.parse::<f64>().ok());
455
456        (yes_price, no_price)
457    }
458}
459
460/// Deserialize Option<Decimal> from string/number/null.
461fn deserialize_decimal_opt<'de, D>(deserializer: D) -> Result<Option<Decimal>, D::Error>
462where
463    D: Deserializer<'de>,
464{
465    match Value::deserialize(deserializer)? {
466        Value::Null => Ok(None),
467        Value::String(s) => {
468            if s.is_empty() {
469                Ok(None)
470            } else {
471                Decimal::from_str(&s).map(Some).map_err(de::Error::custom)
472            }
473        }
474        Value::Number(n) => Decimal::from_str(&n.to_string())
475            .map(Some)
476            .map_err(de::Error::custom),
477        other => Err(de::Error::custom(format!("expected decimal, got {other}"))),
478    }
479}
480
481/// Event metadata from Gamma API
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct Event {
484    /// Event ID
485    pub id: String,
486    /// Event slug
487    pub slug: String,
488    /// Event name
489    #[serde(default)]
490    pub name: Option<String>,
491    /// Event description
492    #[serde(default)]
493    pub description: Option<String>,
494    /// Whether event is active
495    #[serde(default)]
496    pub active: Option<bool>,
497    /// Whether event is closed
498    #[serde(default)]
499    pub closed: Option<bool>,
500    /// Start date
501    #[serde(default)]
502    pub start_date_iso: Option<String>,
503    /// End date
504    #[serde(default)]
505    pub end_date_iso: Option<String>,
506    /// Sport type (for sports events)
507    #[serde(default)]
508    pub sport: Option<String>,
509    /// Associated markets
510    #[serde(default)]
511    pub markets: Vec<EventMarket>,
512}
513
514/// Lightweight market info in events
515#[derive(Debug, Clone, Serialize, Deserialize)]
516#[serde(rename_all = "camelCase")]
517pub struct EventMarket {
518    /// Condition ID
519    pub condition_id: String,
520    /// Market ID
521    #[serde(default)]
522    pub market_id: Option<String>,
523    /// CLOB token IDs
524    #[serde(default)]
525    pub clob_token_ids: Option<String>,
526    /// Market slug
527    #[serde(default)]
528    pub slug: Option<String>,
529}
530
531/// Tag metadata
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct Tag {
534    /// Tag ID
535    #[serde(default)]
536    pub id: Option<String>,
537    /// Tag slug
538    #[serde(default)]
539    pub slug: Option<String>,
540    /// Tag name
541    #[serde(default)]
542    pub name: Option<String>,
543    /// Tag description
544    #[serde(default)]
545    pub description: Option<String>,
546}
547
548// ============================================================================
549// Profile Types
550// ============================================================================
551
552/// Trader profile information
553#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct TraderProfile {
555    /// Wallet address
556    pub address: String,
557    /// Display name
558    #[serde(default)]
559    pub name: Option<String>,
560    /// Username/pseudonym
561    #[serde(default)]
562    pub username: Option<String>,
563    /// Profile image URL
564    #[serde(default, rename = "profileImage")]
565    pub profile_image: Option<String>,
566    /// Bio/description
567    #[serde(default)]
568    pub bio: Option<String>,
569    /// Total volume traded
570    #[serde(default)]
571    pub volume: Option<Decimal>,
572    /// Number of markets traded
573    #[serde(default, rename = "marketsTraded")]
574    pub markets_traded: Option<i32>,
575    /// Profit and loss
576    #[serde(default)]
577    pub pnl: Option<Decimal>,
578}
579
580/// Leaderboard entry
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct LeaderboardEntry {
583    /// Rank position
584    pub rank: i32,
585    /// Wallet address
586    pub address: String,
587    /// Display name
588    #[serde(default)]
589    pub name: Option<String>,
590    /// Username
591    #[serde(default)]
592    pub username: Option<String>,
593    /// Profile image
594    #[serde(default, rename = "profileImage")]
595    pub profile_image: Option<String>,
596    /// Volume for the period
597    pub volume: Decimal,
598    /// Profit for the period
599    #[serde(default)]
600    pub profit: Option<Decimal>,
601}
602
603// ============================================================================
604// Query Parameter Types
605// ============================================================================
606
607/// Pagination parameters
608#[derive(Debug, Clone, Default)]
609pub struct PaginationParams {
610    /// Maximum number of results
611    pub limit: Option<u32>,
612    /// Offset for pagination
613    pub offset: Option<u32>,
614    /// Cursor for cursor-based pagination
615    pub cursor: Option<String>,
616}
617
618impl PaginationParams {
619    /// Create new pagination params
620    #[must_use]
621    pub fn new() -> Self {
622        Self::default()
623    }
624
625    /// Set limit
626    #[must_use]
627    pub fn with_limit(mut self, limit: u32) -> Self {
628        self.limit = Some(limit);
629        self
630    }
631
632    /// Set offset
633    #[must_use]
634    pub fn with_offset(mut self, offset: u32) -> Self {
635        self.offset = Some(offset);
636        self
637    }
638
639    /// Set cursor
640    #[must_use]
641    pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
642        self.cursor = Some(cursor.into());
643        self
644    }
645}
646
647/// Common query parameters for listing endpoints
648#[derive(Debug, Clone, Default)]
649pub struct ListParams {
650    /// Pagination
651    pub pagination: PaginationParams,
652    /// Filter by closed status
653    pub closed: Option<bool>,
654    /// Filter by active status
655    pub active: Option<bool>,
656    /// Sort field
657    pub order: Option<String>,
658    /// Sort ascending
659    pub ascending: Option<bool>,
660}
661
662impl ListParams {
663    /// Create new list params
664    #[must_use]
665    pub fn new() -> Self {
666        Self::default()
667    }
668
669    /// Set limit
670    #[must_use]
671    pub fn with_limit(mut self, limit: u32) -> Self {
672        self.pagination.limit = Some(limit);
673        self
674    }
675
676    /// Set offset
677    #[must_use]
678    pub fn with_offset(mut self, offset: u32) -> Self {
679        self.pagination.offset = Some(offset);
680        self
681    }
682
683    /// Filter by closed status
684    #[must_use]
685    pub fn with_closed(mut self, closed: bool) -> Self {
686        self.closed = Some(closed);
687        self
688    }
689
690    /// Filter by active status
691    #[must_use]
692    pub fn with_active(mut self, active: bool) -> Self {
693        self.active = Some(active);
694        self
695    }
696
697    /// Set sort order
698    #[must_use]
699    pub fn with_order(mut self, field: impl Into<String>, ascending: bool) -> Self {
700        self.order = Some(field.into());
701        self.ascending = Some(ascending);
702        self
703    }
704}
705
706// ============================================================================
707// Connection Statistics
708// ============================================================================
709
710/// WebSocket connection statistics
711#[derive(Debug, Clone, Default)]
712pub struct ConnectionStats {
713    /// Number of messages received
714    pub messages_received: u64,
715    /// Number of reconnection attempts
716    pub reconnect_attempts: u32,
717    /// Last message timestamp
718    pub last_message_at: Option<DateTime<Utc>>,
719    /// Connection established timestamp
720    pub connected_at: Option<DateTime<Utc>>,
721}
722
723impl ConnectionStats {
724    /// Create new stats
725    #[must_use]
726    pub fn new() -> Self {
727        Self::default()
728    }
729
730    /// Record a message received
731    pub fn record_message(&mut self) {
732        self.messages_received += 1;
733        self.last_message_at = Some(Utc::now());
734    }
735
736    /// Record a reconnection attempt
737    pub fn record_reconnect(&mut self) {
738        self.reconnect_attempts += 1;
739    }
740
741    /// Record connection established
742    pub fn record_connected(&mut self) {
743        self.connected_at = Some(Utc::now());
744        self.reconnect_attempts = 0;
745    }
746}
747
748// ============================================================================
749// Data API Types (for data-api.polymarket.com)
750// ============================================================================
751
752/// Polymarket trader profile from Data API
753#[derive(Debug, Clone, Serialize, Deserialize)]
754pub struct DataApiTrader {
755    /// Wallet address
756    pub address: String,
757    /// Display name
758    #[serde(rename = "displayName", default)]
759    pub display_name: Option<String>,
760    /// Profile image URL
761    #[serde(rename = "profileImage", default)]
762    pub profile_image: Option<String>,
763    /// Total PnL (as string)
764    #[serde(rename = "totalPnl", default)]
765    pub total_pnl: Option<String>,
766    /// Total volume (as string)
767    #[serde(rename = "totalVolume", default)]
768    pub total_volume: Option<String>,
769    /// Number of markets traded
770    #[serde(rename = "marketsTraded", default)]
771    pub markets_traded: Option<i32>,
772    /// Win rate (0.0-1.0)
773    #[serde(rename = "winRate", default)]
774    pub win_rate: Option<f64>,
775}
776
777/// Polymarket position from Data API
778#[derive(Debug, Clone, Serialize, Deserialize)]
779pub struct DataApiPosition {
780    /// Position ID
781    pub id: String,
782    /// Market condition ID
783    #[serde(rename = "conditionId")]
784    pub condition_id: String,
785    /// Outcome (Yes/No)
786    pub outcome: String,
787    /// Position size
788    pub size: String,
789    /// Average entry price
790    #[serde(rename = "avgPrice")]
791    pub avg_price: String,
792    /// Current value
793    #[serde(rename = "currentValue", default)]
794    pub current_value: Option<String>,
795    /// Realized PnL
796    #[serde(rename = "realizedPnl", default)]
797    pub realized_pnl: Option<String>,
798    /// Position status
799    pub status: String,
800    /// Created timestamp
801    #[serde(rename = "createdAt", default)]
802    pub created_at: Option<String>,
803    /// Closed timestamp
804    #[serde(rename = "closedAt", default)]
805    pub closed_at: Option<String>,
806}
807
808/// Polymarket trade from Data API
809#[derive(Debug, Clone, Serialize, Deserialize)]
810pub struct DataApiTrade {
811    /// Trade ID
812    pub id: String,
813    /// Market condition ID
814    #[serde(rename = "conditionId")]
815    pub condition_id: String,
816    /// Maker address
817    pub maker: String,
818    /// Taker address
819    pub taker: String,
820    /// Trade side
821    pub side: String,
822    /// Outcome
823    pub outcome: String,
824    /// Trade size
825    pub size: String,
826    /// Trade price
827    pub price: String,
828    /// Trade timestamp
829    pub timestamp: String,
830    /// Transaction hash
831    #[serde(rename = "transactionHash", default)]
832    pub transaction_hash: Option<String>,
833}
834
835/// User activity (trade/position change) from Data API
836#[derive(Debug, Clone, Serialize, Deserialize)]
837pub struct DataApiActivity {
838    /// Transaction hash
839    #[serde(rename = "transactionHash")]
840    pub transaction_hash: String,
841    /// Activity timestamp (unix)
842    pub timestamp: i64,
843    /// User's proxy wallet address
844    #[serde(rename = "proxyWallet")]
845    pub proxy_wallet: String,
846    /// User display name
847    #[serde(default)]
848    pub name: Option<String>,
849    /// User pseudonym
850    #[serde(default)]
851    pub pseudonym: Option<String>,
852    /// User bio
853    #[serde(default)]
854    pub bio: Option<String>,
855    /// User profile image
856    #[serde(rename = "profileImage", default)]
857    pub profile_image: Option<String>,
858    /// Optimized profile image
859    #[serde(rename = "profileImageOptimized", default)]
860    pub profile_image_optimized: Option<String>,
861    /// Trade side (BUY/SELL)
862    pub side: String,
863    /// Outcome (Yes/No)
864    pub outcome: String,
865    /// Outcome index (0 or 1)
866    #[serde(rename = "outcomeIndex")]
867    pub outcome_index: i32,
868    /// Trade price
869    pub price: f64,
870    /// Trade size
871    pub size: f64,
872    /// USDC size
873    #[serde(rename = "usdcSize", default)]
874    pub usdc_size: Option<f64>,
875    /// Asset/token ID
876    pub asset: String,
877    /// Market condition ID
878    #[serde(rename = "conditionId")]
879    pub condition_id: String,
880    /// Market title
881    pub title: String,
882    /// Market slug
883    pub slug: String,
884    /// Event slug
885    #[serde(rename = "eventSlug")]
886    pub event_slug: String,
887    /// Market icon
888    #[serde(default)]
889    pub icon: Option<String>,
890}
891
892/// Biggest Winner entry from Data API
893#[derive(Debug, Clone, Serialize, Deserialize)]
894pub struct BiggestWinner {
895    /// Rank (string format)
896    #[serde(rename = "winRank")]
897    pub win_rank: String,
898    /// Wallet address (0x...)
899    #[serde(rename = "proxyWallet")]
900    pub proxy_wallet: String,
901    /// User name
902    #[serde(rename = "userName", default)]
903    pub user_name: String,
904    /// Event slug
905    #[serde(rename = "eventSlug")]
906    pub event_slug: String,
907    /// Event title
908    #[serde(rename = "eventTitle")]
909    pub event_title: String,
910    /// Initial value (USD)
911    #[serde(rename = "initialValue")]
912    pub initial_value: f64,
913    /// Final value (USD)
914    #[serde(rename = "finalValue")]
915    pub final_value: f64,
916    /// Realized profit (USD)
917    pub pnl: f64,
918    /// Profile image URL
919    #[serde(rename = "profileImage", default)]
920    pub profile_image: String,
921}
922
923/// Query parameters for biggest winners API
924#[derive(Debug, Clone)]
925pub struct BiggestWinnersQuery {
926    /// Time period: day, week, month, all_time
927    pub time_period: String,
928    /// Max results (max 100 per request)
929    pub limit: usize,
930    /// Pagination offset
931    pub offset: usize,
932    /// Category filter (lowercase): all, politics, sports, crypto, etc.
933    pub category: String,
934}
935
936impl Default for BiggestWinnersQuery {
937    fn default() -> Self {
938        Self {
939            time_period: "all_time".to_string(),
940            limit: 100,
941            offset: 0,
942            category: "all".to_string(),
943        }
944    }
945}
946
947impl BiggestWinnersQuery {
948    /// Create new query with defaults
949    #[must_use]
950    pub fn new() -> Self {
951        Self::default()
952    }
953
954    /// Set time period
955    #[must_use]
956    pub fn with_time_period(mut self, period: impl Into<String>) -> Self {
957        self.time_period = period.into();
958        self
959    }
960
961    /// Set limit
962    #[must_use]
963    pub fn with_limit(mut self, limit: usize) -> Self {
964        self.limit = limit;
965        self
966    }
967
968    /// Set offset
969    #[must_use]
970    pub fn with_offset(mut self, offset: usize) -> Self {
971        self.offset = offset;
972        self
973    }
974
975    /// Set category
976    #[must_use]
977    pub fn with_category(mut self, category: impl Into<String>) -> Self {
978        self.category = category.into();
979        self
980    }
981}
982
983// ============================================================================
984// Public Search API Types
985// ============================================================================
986
987/// Search request parameters for /public-search endpoint
988#[derive(Debug, Clone, Serialize, Deserialize)]
989pub struct SearchRequest {
990    /// Search query string
991    pub q: String,
992    /// Limit per result type
993    #[serde(skip_serializing_if = "Option::is_none")]
994    pub limit_per_type: Option<u32>,
995    /// Whether to search profiles (traders)
996    #[serde(skip_serializing_if = "Option::is_none")]
997    pub search_profiles: Option<bool>,
998    /// Whether to search tags
999    #[serde(skip_serializing_if = "Option::is_none")]
1000    pub search_tags: Option<bool>,
1001}
1002
1003impl SearchRequest {
1004    /// Create a new search request
1005    #[must_use]
1006    pub fn new(query: impl Into<String>) -> Self {
1007        Self {
1008            q: query.into(),
1009            limit_per_type: None,
1010            search_profiles: None,
1011            search_tags: None,
1012        }
1013    }
1014
1015    /// Set limit per type
1016    #[must_use]
1017    pub fn with_limit(mut self, limit: u32) -> Self {
1018        self.limit_per_type = Some(limit);
1019        self
1020    }
1021
1022    /// Set whether to search profiles
1023    #[must_use]
1024    pub fn with_profiles(mut self, search_profiles: bool) -> Self {
1025        self.search_profiles = Some(search_profiles);
1026        self
1027    }
1028
1029    /// Set whether to search tags
1030    #[must_use]
1031    pub fn with_tags(mut self, search_tags: bool) -> Self {
1032        self.search_tags = Some(search_tags);
1033        self
1034    }
1035}
1036
1037/// Search response from /public-search endpoint
1038#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1039pub struct SearchResponse {
1040    /// Matching events/markets
1041    #[serde(default)]
1042    pub events: Vec<SearchEvent>,
1043    /// Matching profiles/traders
1044    #[serde(default)]
1045    pub profiles: Vec<SearchProfile>,
1046    /// Matching tags
1047    #[serde(default)]
1048    pub tags: Vec<SearchTag>,
1049}
1050
1051/// Search event result
1052#[derive(Debug, Clone, Serialize, Deserialize)]
1053pub struct SearchEvent {
1054    /// Event ID
1055    #[serde(default)]
1056    pub id: String,
1057    /// Event slug
1058    #[serde(default)]
1059    pub slug: String,
1060    /// Event question/title
1061    #[serde(default)]
1062    pub question: Option<String>,
1063    /// Event image
1064    #[serde(default)]
1065    pub image: Option<String>,
1066    /// Whether event is active
1067    #[serde(default)]
1068    pub active: bool,
1069    /// Whether event is closed
1070    #[serde(default)]
1071    pub closed: bool,
1072    /// Total volume
1073    #[serde(default)]
1074    pub volume: f64,
1075    /// 24-hour volume
1076    #[serde(rename = "volume24hr", default)]
1077    pub volume_24hr: Option<f64>,
1078    /// End date
1079    #[serde(rename = "endDate", default)]
1080    pub end_date: Option<String>,
1081}
1082
1083/// Search profile/trader result
1084#[derive(Debug, Clone, Serialize, Deserialize)]
1085pub struct SearchProfile {
1086    /// Profile ID
1087    #[serde(default)]
1088    pub id: Option<String>,
1089    /// Display name
1090    #[serde(default)]
1091    pub name: Option<String>,
1092    /// Old API field: imageURI
1093    #[serde(rename = "imageURI", default)]
1094    pub image_uri: Option<String>,
1095    /// New API field: profileImage
1096    #[serde(rename = "profileImage", default)]
1097    pub profile_image: Option<String>,
1098    /// Bio/description
1099    #[serde(default)]
1100    pub bio: Option<String>,
1101    /// Pseudonym
1102    #[serde(default)]
1103    pub pseudonym: Option<String>,
1104    /// Whether to display username publicly
1105    #[serde(rename = "displayUsernamePublic", default)]
1106    pub display_username_public: bool,
1107    /// Old API field: walletAddress
1108    #[serde(rename = "walletAddress", default)]
1109    pub wallet_address: Option<String>,
1110    /// New API field: proxyWallet
1111    #[serde(rename = "proxyWallet", default)]
1112    pub proxy_wallet: Option<String>,
1113}
1114
1115impl SearchProfile {
1116    /// Get wallet address (prefer proxy_wallet, fallback to wallet_address)
1117    #[must_use]
1118    pub fn get_wallet_address(&self) -> Option<String> {
1119        self.proxy_wallet
1120            .clone()
1121            .or_else(|| self.wallet_address.clone())
1122    }
1123
1124    /// Get profile image (prefer profile_image, fallback to image_uri)
1125    #[must_use]
1126    pub fn get_profile_image(&self) -> Option<String> {
1127        self.profile_image
1128            .clone()
1129            .or_else(|| self.image_uri.clone())
1130    }
1131
1132    /// Get display name (prefer name, fallback to pseudonym)
1133    #[must_use]
1134    pub fn get_display_name(&self) -> Option<String> {
1135        self.name.clone().or_else(|| self.pseudonym.clone())
1136    }
1137}
1138
1139/// Search tag result
1140#[derive(Debug, Clone, Serialize, Deserialize)]
1141pub struct SearchTag {
1142    /// Tag ID
1143    pub id: String,
1144    /// Tag label
1145    pub label: String,
1146    /// Tag slug
1147    #[serde(default)]
1148    pub slug: Option<String>,
1149}
1150
1151/// Closed position from Data API (for PnL calculation)
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1153pub struct ClosedPosition {
1154    /// Position ID
1155    #[serde(default)]
1156    pub id: Option<String>,
1157    /// Proxy wallet address
1158    #[serde(rename = "proxyWallet", default)]
1159    pub proxy_wallet: Option<String>,
1160    /// Token asset ID
1161    #[serde(default)]
1162    pub asset: Option<String>,
1163    /// Market condition ID
1164    #[serde(rename = "conditionId")]
1165    pub condition_id: String,
1166    /// Market title
1167    pub title: String,
1168    /// Market slug
1169    pub slug: String,
1170    /// Event slug
1171    #[serde(rename = "eventSlug")]
1172    pub event_slug: String,
1173    /// Outcome (Yes/No)
1174    pub outcome: String,
1175    /// Outcome index
1176    #[serde(rename = "outcomeIndex")]
1177    pub outcome_index: i32,
1178    /// Entry price
1179    #[serde(rename = "avgPrice")]
1180    pub avg_price: f64,
1181    /// Current price
1182    #[serde(rename = "curPrice", default)]
1183    pub cur_price: Option<f64>,
1184    /// Exit price
1185    #[serde(rename = "exitPrice", default)]
1186    pub exit_price: Option<f64>,
1187    /// Position size (shares)
1188    #[serde(default)]
1189    pub size: Option<f64>,
1190    /// Total bought amount (USDC)
1191    #[serde(rename = "totalBought", default)]
1192    pub total_bought: Option<f64>,
1193    /// Realized PnL
1194    #[serde(rename = "realizedPnl", default)]
1195    pub realized_pnl: Option<f64>,
1196    /// Cash out amount
1197    #[serde(rename = "cashOut", default)]
1198    pub cash_out: Option<f64>,
1199    /// Is winning position
1200    #[serde(rename = "isWinner", default)]
1201    pub is_winner: Option<bool>,
1202    /// Closed timestamp (unix)
1203    #[serde(default)]
1204    pub timestamp: Option<i64>,
1205    /// Closed timestamp (ISO string)
1206    #[serde(rename = "closedAt", default)]
1207    pub closed_at: Option<String>,
1208    /// End date
1209    #[serde(rename = "endDate", default)]
1210    pub end_date: Option<String>,
1211    /// Market icon
1212    #[serde(default)]
1213    pub icon: Option<String>,
1214    /// Opposite outcome name
1215    #[serde(rename = "oppositeOutcome", default)]
1216    pub opposite_outcome: Option<String>,
1217    /// Opposite asset ID
1218    #[serde(rename = "oppositeAsset", default)]
1219    pub opposite_asset: Option<String>,
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224    use super::*;
1225
1226    /// Test that NewOrder JSON serialization matches TypeScript SDK field order
1227    /// This is critical for HMAC signature compatibility
1228    #[test]
1229    fn test_new_order_json_field_order() {
1230        let order = NewOrder {
1231            defer_exec: false,
1232            order: NewOrderData {
1233                salt: 2915952280710976,
1234                maker: "0xc2ca793cf057d48a054bedabf625f301b40d38aa".to_string(),
1235                signer: "0xd13765b3e68431bf2b6e9994a0f4c3d2495799e9".to_string(),
1236                taker: "0x0000000000000000000000000000000000000000".to_string(),
1237                token_id: "21489772516410038586556744342392982044189999368638682594741395650226594484811".to_string(),
1238                maker_amount: "10000".to_string(),
1239                taker_amount: "1000000".to_string(),
1240                side: "BUY".to_string(),
1241                expiration: "0".to_string(),
1242                nonce: "0".to_string(),
1243                fee_rate_bps: "0".to_string(),
1244                signature_type: 1,
1245                signature: "0x0cfb0e318afe33e1189f23d4b11a1092963865d7ff7f7a035110d50d71a2ab484ae4828b3fcfcac2ada92fbd825eedfe4eb21d4e1cdd5aa1a47e23bf5d539b781c".to_string(),
1246            },
1247            owner: "fe9fb6b1-9ae6-6c5b-3cca-1ace6a8b1f29".to_string(),
1248            order_type: OrderType::GTC,
1249        };
1250
1251        let json = serde_json::to_string(&order).unwrap();
1252
1253        // Expected format from TypeScript SDK (field order matters for HMAC)
1254        let expected = r#"{"deferExec":false,"order":{"salt":2915952280710976,"maker":"0xc2ca793cf057d48a054bedabf625f301b40d38aa","signer":"0xd13765b3e68431bf2b6e9994a0f4c3d2495799e9","taker":"0x0000000000000000000000000000000000000000","tokenId":"21489772516410038586556744342392982044189999368638682594741395650226594484811","makerAmount":"10000","takerAmount":"1000000","side":"BUY","expiration":"0","nonce":"0","feeRateBps":"0","signatureType":1,"signature":"0x0cfb0e318afe33e1189f23d4b11a1092963865d7ff7f7a035110d50d71a2ab484ae4828b3fcfcac2ada92fbd825eedfe4eb21d4e1cdd5aa1a47e23bf5d539b781c"},"owner":"fe9fb6b1-9ae6-6c5b-3cca-1ace6a8b1f29","orderType":"GTC"}"#;
1255
1256        assert_eq!(
1257            json, expected,
1258            "\nJSON field order mismatch!\n\nGot:\n{}\n\nExpected:\n{}\n",
1259            json, expected
1260        );
1261    }
1262}