Skip to main content

polyfill_rs/
types.rs

1//! Core types for the Polymarket client
2//!
3//! This module defines all the stable public types used throughout the client.
4//! These types are optimized for latency-sensitive trading environments.
5
6use alloy_primitives::Address;
7use chrono::{DateTime, Utc};
8use rust_decimal::prelude::ToPrimitive;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12// ============================================================================
13// FIXED-POINT OPTIMIZATION FOR HOT PATH PERFORMANCE
14// ============================================================================
15//
16// Instead of using rust_decimal::Decimal everywhere (which allocates),
17// I've used fixed-point integers for the performance-critical order book operations.
18//
19// Why this matters:
20// - Decimal operations can be 10-100x slower than integer operations
21// - Decimal allocates memory for each calculation
22// - In an order book like this we process thousands of price updates per second
23// - Most prices can be represented as integer ticks (e.g., $0.6543 = 6543 ticks)
24//
25// The strategy:
26// 1. Convert Decimal to fixed-point on ingress (when data comes in)
27// 2. Do all hot-path calculations with integers
28// 3. Convert back to Decimal only at the edges (API responses, user display)
29//
30// This is like how video games handle positions, they use integers internally
31// for speed, but show floating-point coordinates to players.
32/// Each tick represents 0.0001 (1/10,000) of the base unit
33/// Examples:
34/// - $0.6543 = 6543 ticks
35/// - $1.0000 = 10000 ticks  
36/// - $0.0001 = 1 tick (minimum price increment)
37///
38/// Why u32?
39/// - Can represent prices from $0.0001 to $429,496.7295 (way more than needed)
40/// - Fits in CPU register for fast operations
41/// - No sign bit needed since prices are always positive
42pub type Price = u32;
43
44/// Quantity/size represented as fixed-point integer for performance
45///
46/// Each unit represents 0.0001 (1/10,000) of a token
47/// Examples:
48/// - 100.0 tokens = 1,000,000 units
49/// - 0.0001 tokens = 1 unit (minimum size increment)
50///
51/// Why i64?
52/// - Can represent quantities from -922,337,203,685.4775 to +922,337,203,685.4775
53/// - Signed because we need to handle both buys (+) and sells (-)
54/// - Large enough for any realistic trading size
55pub type Qty = i64;
56
57/// Scale factor for converting between Decimal and fixed-point
58///
59/// We use 10,000 (1e4) as our scale factor, giving us 4 decimal places of precision.
60/// This is perfect for most prediction markets where prices are between $0.01-$0.99
61/// and we need precision to the nearest $0.0001.
62pub const SCALE_FACTOR: i64 = 10_000;
63
64/// Maximum valid price in ticks (prevents overflow)
65/// This represents $429,496.7295 which is way higher than any prediction market price
66pub const MAX_PRICE_TICKS: Price = Price::MAX;
67
68/// Minimum valid price in ticks (1 tick = $0.0001)
69pub const MIN_PRICE_TICKS: Price = 1;
70
71/// Maximum valid quantity (prevents overflow in calculations)
72pub const MAX_QTY: Qty = Qty::MAX / 2; // Leave room for intermediate calculations
73
74// ============================================================================
75// CONVERSION FUNCTIONS BETWEEN DECIMAL AND FIXED-POINT
76// ============================================================================
77//
78// These functions handle the conversion between the external Decimal API
79// and our internal fixed-point representation. They're designed to be fast
80// and handle edge cases gracefully.
81
82/// Convert a Decimal price to fixed-point ticks exactly.
83///
84/// This rejects values that cannot be represented exactly at our 4-decimal
85/// fixed-point scale. Use this for validation and market-data ingress.
86///
87/// Examples:
88/// - decimal_to_price(Decimal::from_str("0.6543")) = Ok(6543)
89/// - decimal_to_price(Decimal::from_str("1.0000")) = Ok(10000)
90/// - decimal_to_price(Decimal::from_str("0.00005")) = Err(...)
91pub fn decimal_to_price(decimal: Decimal) -> std::result::Result<Price, &'static str> {
92    decimal_to_price_exact(decimal)
93}
94
95/// Convert a Decimal price to fixed-point ticks exactly.
96///
97/// This rejects fractional ticks instead of rounding and rejects values outside
98/// the valid price range instead of clamping.
99pub fn decimal_to_price_exact(decimal: Decimal) -> std::result::Result<Price, &'static str> {
100    let scaled = decimal * Decimal::from(SCALE_FACTOR);
101
102    let ticks = scaled
103        .to_u32()
104        .ok_or("Price too large, negative, or fractional")?;
105
106    if Decimal::from(ticks) != scaled {
107        return Err("Price is not exactly representable at 4 decimal places");
108    }
109    if ticks < MIN_PRICE_TICKS {
110        return Err("Price below minimum");
111    }
112
113    Ok(ticks)
114}
115
116/// Convert a Decimal price to fixed-point ticks with rounding and clamping.
117///
118/// This is appropriate for UI/ergonomic input paths where quantizing to the
119/// nearest internal tick is desired. Do not use it for validation or market-data
120/// ingress.
121///
122/// Examples:
123/// - decimal_to_price_lossy(Decimal::from_str("0.65434")) = Ok(6543)
124/// - decimal_to_price_lossy(Decimal::from_str("0.65435")) = Ok(6544)
125/// - decimal_to_price_lossy(Decimal::from_str("0.00005")) = Ok(1)
126pub fn decimal_to_price_lossy(decimal: Decimal) -> std::result::Result<Price, &'static str> {
127    // Convert to fixed-point by multiplying by scale factor
128    let scaled = decimal * Decimal::from(SCALE_FACTOR);
129
130    // Round to nearest integer (this handles tick alignment automatically)
131    let rounded = scaled.round();
132
133    // Convert to u64 first to handle the conversion safely
134    let as_u64 = rounded.to_u64().ok_or("Price too large or negative")?;
135
136    // Check bounds
137    if as_u64 < MIN_PRICE_TICKS as u64 {
138        return Ok(MIN_PRICE_TICKS); // Clamp to minimum
139    }
140    if as_u64 > MAX_PRICE_TICKS as u64 {
141        return Err("Price exceeds maximum");
142    }
143
144    Ok(as_u64 as Price)
145}
146
147/// Convert fixed-point ticks back to Decimal price
148///
149/// This is called when we need to return price data to the API or display to users.
150/// It's the inverse of decimal_to_price_exact().
151///
152/// Examples:
153/// - price_to_decimal(6543) = Decimal::from_str("0.6543")
154/// - price_to_decimal(10000) = Decimal::from_str("1.0000")
155pub fn price_to_decimal(ticks: Price) -> Decimal {
156    Decimal::from(ticks) / Decimal::from(SCALE_FACTOR)
157}
158
159/// Convert a Decimal quantity to fixed-point units
160///
161/// Similar to decimal_to_price but handles signed quantities.
162/// Quantities can be negative (for sells or position changes).
163///
164/// Examples:
165/// - decimal_to_qty(Decimal::from_str("100.0")) = Ok(1000000)
166/// - decimal_to_qty(Decimal::from_str("-50.5")) = Ok(-505000)
167pub fn decimal_to_qty(decimal: Decimal) -> std::result::Result<Qty, &'static str> {
168    let scaled = decimal * Decimal::from(SCALE_FACTOR);
169    let rounded = scaled.round();
170
171    let as_i64 = rounded.to_i64().ok_or("Quantity too large")?;
172
173    if as_i64.abs() > MAX_QTY {
174        return Err("Quantity exceeds maximum");
175    }
176
177    Ok(as_i64)
178}
179
180/// Convert fixed-point units back to Decimal quantity
181///
182/// Examples:
183/// - qty_to_decimal(1000000) = Decimal::from_str("100.0")
184/// - qty_to_decimal(-505000) = Decimal::from_str("-50.5")
185pub fn qty_to_decimal(units: Qty) -> Decimal {
186    Decimal::from(units) / Decimal::from(SCALE_FACTOR)
187}
188
189/// Check if a price is properly tick-aligned
190///
191/// This is used to validate incoming price data. In a well-behaved system,
192/// all prices should already be tick-aligned, but we check anyway to catch
193/// bugs or malicious data.
194///
195/// A price is tick-aligned if it's an exact multiple of the minimum tick size.
196/// Since we use integer ticks internally, this just checks if the price
197/// converts cleanly to our internal representation.
198pub fn is_price_tick_aligned(decimal: Decimal, tick_size_decimal: Decimal) -> bool {
199    // Convert tick size to our internal representation
200    let tick_size_ticks = match decimal_to_price_exact(tick_size_decimal) {
201        Ok(ticks) => ticks,
202        Err(_) => return false,
203    };
204
205    // Convert the price to ticks
206    let price_ticks = match decimal_to_price_exact(decimal) {
207        Ok(ticks) => ticks,
208        Err(_) => return false,
209    };
210
211    // Check if price is a multiple of tick size
212    // If tick_size_ticks is 0, we consider everything aligned (no restrictions)
213    if tick_size_ticks == 0 {
214        return true;
215    }
216
217    price_ticks % tick_size_ticks == 0
218}
219
220/// Trading side for orders
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
222#[allow(clippy::upper_case_acronyms)]
223pub enum Side {
224    BUY = 0,
225    SELL = 1,
226}
227
228impl Side {
229    pub fn as_str(&self) -> &'static str {
230        match self {
231            Side::BUY => "BUY",
232            Side::SELL => "SELL",
233        }
234    }
235
236    pub fn opposite(&self) -> Self {
237        match self {
238            Side::BUY => Side::SELL,
239            Side::SELL => Side::BUY,
240        }
241    }
242}
243
244/// Order type specifications
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
246#[allow(clippy::upper_case_acronyms)]
247pub enum OrderType {
248    #[default]
249    GTC,
250    FOK,
251    GTD,
252    FAK,
253}
254
255impl OrderType {
256    pub fn as_str(&self) -> &'static str {
257        match self {
258            OrderType::GTC => "GTC",
259            OrderType::FOK => "FOK",
260            OrderType::GTD => "GTD",
261            OrderType::FAK => "FAK",
262        }
263    }
264}
265
266/// Order status in the system
267#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
268pub enum OrderStatus {
269    #[serde(rename = "LIVE")]
270    Live,
271    #[serde(rename = "CANCELLED")]
272    Cancelled,
273    #[serde(rename = "FILLED")]
274    Filled,
275    #[serde(rename = "PARTIAL")]
276    Partial,
277    #[serde(rename = "EXPIRED")]
278    Expired,
279}
280
281/// Market snapshot representing current state
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct MarketSnapshot {
284    pub token_id: String,
285    pub market_id: String,
286    pub timestamp: DateTime<Utc>,
287    pub bid: Option<Decimal>,
288    pub ask: Option<Decimal>,
289    pub mid: Option<Decimal>,
290    pub spread: Option<Decimal>,
291    pub last_price: Option<Decimal>,
292    pub volume_24h: Option<Decimal>,
293}
294
295/// Order book level (price/size pair) - EXTERNAL API VERSION
296///
297/// This is what we expose to users and serialize to JSON.
298/// It uses Decimal for precision and human readability.
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct BookLevel {
301    #[serde(with = "rust_decimal::serde::str")]
302    pub price: Decimal,
303    #[serde(with = "rust_decimal::serde::str")]
304    pub size: Decimal,
305}
306
307/// Order book level (price/size pair) - INTERNAL HOT PATH VERSION
308///
309/// This is what we use internally for maximum performance.
310/// All order book operations use this to avoid Decimal overhead.
311///
312/// The performance difference is huge:
313/// - BookLevel: ~50ns per operation (Decimal math + allocation)
314/// - FastBookLevel: ~2ns per operation (integer math, no allocation)
315///
316/// That's a 25x speedup on the critical path
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub struct FastBookLevel {
319    pub price: Price, // Price in ticks (u32)
320    pub size: Qty,    // Size in fixed-point units (i64)
321}
322
323impl FastBookLevel {
324    /// Create a new fast book level
325    pub fn new(price: Price, size: Qty) -> Self {
326        Self { price, size }
327    }
328
329    /// Convert to external BookLevel for API responses
330    /// This is only called at the edges when we need to return data to users
331    pub fn to_book_level(self) -> BookLevel {
332        BookLevel {
333            price: price_to_decimal(self.price),
334            size: qty_to_decimal(self.size),
335        }
336    }
337
338    /// Create from external BookLevel (with validation)
339    /// This is called when we receive data from the API
340    pub fn from_book_level(level: &BookLevel) -> std::result::Result<Self, &'static str> {
341        let price = decimal_to_price_exact(level.price)?;
342        let size = decimal_to_qty(level.size)?;
343        Ok(Self::new(price, size))
344    }
345
346    /// Calculate notional value (price * size) in fixed-point
347    /// Returns the result scaled appropriately to avoid overflow
348    ///
349    /// This is much faster than the Decimal equivalent:
350    /// - Decimal: price.mul(size) -> ~20ns + allocation
351    /// - Fixed-point: (price as i64 * size) / SCALE_FACTOR -> ~1ns, no allocation
352    pub fn notional(self) -> i64 {
353        // Convert price to i64 to avoid overflow in multiplication
354        let price_i64 = self.price as i64;
355        // Multiply and scale back down (we scaled both price and size up by SCALE_FACTOR)
356        (price_i64 * self.size) / SCALE_FACTOR
357    }
358}
359
360/// Full order book state
361#[derive(Debug, Clone, Serialize)]
362pub struct OrderBook {
363    /// Token ID
364    pub token_id: String,
365    /// Timestamp
366    pub timestamp: DateTime<Utc>,
367    /// Bid orders
368    pub bids: Vec<BookLevel>,
369    /// Ask orders
370    pub asks: Vec<BookLevel>,
371    /// Legacy/incremental delta sequence number.
372    ///
373    /// This is kept for compatibility. For snapshots produced by this client,
374    /// it is equivalent to [`Self::last_delta_sequence`]. WebSocket snapshot
375    /// timestamps are exposed separately in [`Self::last_snapshot_timestamp_ms`].
376    pub sequence: u64,
377    /// Last accepted legacy/incremental delta sequence number.
378    pub last_delta_sequence: u64,
379    /// Last accepted full-book snapshot timestamp in milliseconds.
380    pub last_snapshot_timestamp_ms: u64,
381}
382
383impl<'de> Deserialize<'de> for OrderBook {
384    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
385    where
386        D: serde::Deserializer<'de>,
387    {
388        #[derive(Deserialize)]
389        struct WireOrderBook {
390            token_id: String,
391            timestamp: DateTime<Utc>,
392            bids: Vec<BookLevel>,
393            asks: Vec<BookLevel>,
394            sequence: u64,
395            #[serde(default)]
396            last_delta_sequence: Option<u64>,
397            #[serde(default)]
398            last_snapshot_timestamp_ms: Option<u64>,
399        }
400
401        let wire = WireOrderBook::deserialize(deserializer)?;
402        Ok(Self {
403            token_id: wire.token_id,
404            timestamp: wire.timestamp,
405            bids: wire.bids,
406            asks: wire.asks,
407            sequence: wire.sequence,
408            last_delta_sequence: wire.last_delta_sequence.unwrap_or(wire.sequence),
409            last_snapshot_timestamp_ms: wire.last_snapshot_timestamp_ms.unwrap_or_default(),
410        })
411    }
412}
413
414/// Order book delta for streaming updates - EXTERNAL API VERSION
415///
416/// This is what we receive from WebSocket streams and REST API calls.
417/// It uses Decimal for compatibility with external systems.
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct OrderDelta {
420    pub token_id: String,
421    pub timestamp: DateTime<Utc>,
422    pub side: Side,
423    pub price: Decimal,
424    pub size: Decimal, // 0 means remove level
425    pub sequence: u64,
426}
427
428/// Order book delta for streaming updates - INTERNAL HOT PATH VERSION
429///
430/// This is what we use internally for processing order book updates.
431/// Converting to this format on ingress gives us massive performance gains.
432///
433/// Why the performance matters:
434/// - We might process 10,000+ deltas per second in active markets
435/// - Each delta triggers multiple calculations (spread, impact, etc.)
436/// - Using integers instead of Decimal can make the difference between
437///   keeping up with the market feed vs falling behind
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub struct FastOrderDelta {
440    pub token_id_hash: u64, // Hash of token_id for fast lookup (avoids string comparisons)
441    pub timestamp: DateTime<Utc>,
442    pub side: Side,
443    pub price: Price, // Price in ticks
444    pub size: Qty,    // Size in fixed-point units (0 means remove level)
445    pub sequence: u64,
446}
447
448impl FastOrderDelta {
449    /// Create from external OrderDelta with validation and tick alignment
450    ///
451    /// This is where we enforce tick alignment - if the incoming price
452    /// doesn't align to valid ticks, we either reject it or round it.
453    /// This prevents bad data from corrupting our order book.
454    pub fn from_order_delta(
455        delta: &OrderDelta,
456        tick_size: Option<Decimal>,
457    ) -> std::result::Result<Self, &'static str> {
458        // Validate tick alignment if we have a tick size
459        if let Some(tick_size) = tick_size {
460            if !is_price_tick_aligned(delta.price, tick_size) {
461                return Err("Price not aligned to tick size");
462            }
463        }
464
465        // Convert to fixed-point with validation
466        let price = decimal_to_price_exact(delta.price)?;
467        let size = decimal_to_qty(delta.size)?;
468
469        // Hash the token_id for fast lookups
470        // This avoids string comparisons in the hot path
471        let token_id_hash = {
472            use std::collections::hash_map::DefaultHasher;
473            use std::hash::{Hash, Hasher};
474            let mut hasher = DefaultHasher::new();
475            delta.token_id.hash(&mut hasher);
476            hasher.finish()
477        };
478
479        Ok(Self {
480            token_id_hash,
481            timestamp: delta.timestamp,
482            side: delta.side,
483            price,
484            size,
485            sequence: delta.sequence,
486        })
487    }
488
489    /// Convert back to external OrderDelta (for API responses)
490    /// We need the original token_id since we only store the hash
491    pub fn to_order_delta(self, token_id: String) -> OrderDelta {
492        OrderDelta {
493            token_id,
494            timestamp: self.timestamp,
495            side: self.side,
496            price: price_to_decimal(self.price),
497            size: qty_to_decimal(self.size),
498            sequence: self.sequence,
499        }
500    }
501
502    /// Check if this delta removes a level (size is zero)
503    pub fn is_removal(self) -> bool {
504        self.size == 0
505    }
506}
507
508/// Trade execution event
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct FillEvent {
511    pub id: String,
512    pub order_id: String,
513    pub token_id: String,
514    pub side: Side,
515    pub price: Decimal,
516    pub size: Decimal,
517    pub timestamp: DateTime<Utc>,
518    pub maker_address: Address,
519    pub taker_address: Address,
520    pub fee: Decimal,
521}
522
523/// Order creation parameters
524#[derive(Debug, Clone)]
525pub struct OrderRequest {
526    pub token_id: String,
527    pub side: Side,
528    pub price: Decimal,
529    pub size: Decimal,
530    pub order_type: OrderType,
531    pub expiration: Option<DateTime<Utc>>,
532    pub client_id: Option<String>,
533}
534
535/// Market order parameters
536#[derive(Debug, Clone)]
537pub struct MarketOrderRequest {
538    pub token_id: String,
539    pub side: Side,
540    pub amount: Decimal, // USD amount for buys, token amount for sells
541    pub slippage_tolerance: Option<Decimal>,
542    pub client_id: Option<String>,
543}
544
545/// Order state in the system
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct Order {
548    pub id: String,
549    pub token_id: String,
550    pub side: Side,
551    pub price: Decimal,
552    pub original_size: Decimal,
553    pub filled_size: Decimal,
554    pub remaining_size: Decimal,
555    pub status: OrderStatus,
556    pub order_type: OrderType,
557    pub created_at: DateTime<Utc>,
558    pub updated_at: DateTime<Utc>,
559    pub expiration: Option<DateTime<Utc>>,
560    pub client_id: Option<String>,
561}
562
563/// API credentials for authentication
564#[derive(Debug, Clone, Serialize, Deserialize, Default)]
565pub struct ApiCredentials {
566    #[serde(rename = "apiKey")]
567    pub api_key: String,
568    pub secret: String,
569    pub passphrase: String,
570}
571
572/// Limit order arguments for V2 order creation.
573#[derive(Debug, Clone, PartialEq)]
574pub struct OrderArgs {
575    pub token_id: String,
576    pub price: Decimal,
577    pub size: Decimal,
578    pub side: Side,
579    pub expiration: Option<u64>,
580    pub builder_code: Option<String>,
581    pub metadata: Option<String>,
582}
583
584impl OrderArgs {
585    pub fn new(token_id: &str, price: Decimal, size: Decimal, side: Side) -> Self {
586        Self {
587            token_id: token_id.to_string(),
588            price,
589            size,
590            side,
591            expiration: None,
592            builder_code: None,
593            metadata: None,
594        }
595    }
596}
597
598impl Default for OrderArgs {
599    fn default() -> Self {
600        Self {
601            token_id: String::new(),
602            price: Decimal::ZERO,
603            size: Decimal::ZERO,
604            side: Side::BUY,
605            expiration: None,
606            builder_code: None,
607            metadata: None,
608        }
609    }
610}
611
612/// Market order arguments for V2 order creation.
613#[derive(Debug, Clone, PartialEq)]
614pub struct MarketOrderArgs {
615    pub token_id: String,
616    pub amount: Decimal,
617    pub side: Side,
618    pub order_type: OrderType,
619    pub price_limit: Option<Decimal>,
620    pub user_usdc_balance: Option<Decimal>,
621    pub builder_code: Option<String>,
622    pub metadata: Option<String>,
623}
624
625impl MarketOrderArgs {
626    pub fn new(token_id: &str, amount: Decimal, side: Side, order_type: OrderType) -> Self {
627        Self {
628            token_id: token_id.to_string(),
629            amount,
630            side,
631            order_type,
632            price_limit: None,
633            user_usdc_balance: None,
634            builder_code: None,
635            metadata: None,
636        }
637    }
638}
639
640/// Options used while constructing an order.
641#[derive(Debug, Clone, Copy, Default, PartialEq)]
642pub struct CreateOrderOptions {
643    pub tick_size: Option<Decimal>,
644    pub neg_risk: Option<bool>,
645}
646
647/// Options used while posting a signed order.
648#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649pub struct PostOrderOptions {
650    pub order_type: OrderType,
651    pub post_only: bool,
652    pub defer_exec: bool,
653}
654
655impl Default for PostOrderOptions {
656    fn default() -> Self {
657        Self {
658            order_type: OrderType::GTC,
659            post_only: false,
660            defer_exec: false,
661        }
662    }
663}
664
665/// Signed order request ready for submission
666#[derive(Debug, Clone, Serialize, Deserialize)]
667#[serde(rename_all = "camelCase")]
668pub struct SignedOrderRequest {
669    pub salt: u64,
670    pub maker: String,
671    pub signer: String,
672    pub token_id: String,
673    pub maker_amount: String,
674    pub taker_amount: String,
675    pub expiration: String,
676    pub side: String,
677    pub signature_type: u8,
678    pub timestamp: String,
679    pub metadata: String,
680    pub builder: String,
681    pub signature: String,
682}
683
684/// Post order wrapper
685#[derive(Debug, Serialize)]
686#[serde(rename_all = "camelCase")]
687pub struct PostOrder {
688    pub order: SignedOrderRequest,
689    pub owner: String,
690    pub order_type: OrderType,
691    pub post_only: bool,
692    pub defer_exec: bool,
693}
694
695impl PostOrder {
696    pub fn new(order: SignedOrderRequest, owner: String, options: PostOrderOptions) -> Self {
697        Self {
698            order,
699            owner,
700            order_type: options.order_type,
701            post_only: options.post_only,
702            defer_exec: options.defer_exec,
703        }
704    }
705}
706
707/// Typed response from `POST /order`.
708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
709#[serde(rename_all = "camelCase")]
710pub struct PostOrderResponse {
711    pub success: bool,
712    #[serde(rename = "orderID")]
713    pub order_id: String,
714    pub status: String,
715    pub making_amount: String,
716    pub taking_amount: String,
717    #[serde(default)]
718    pub transactions_hashes: Vec<String>,
719    #[serde(default)]
720    pub trade_ids: Vec<String>,
721    #[serde(default)]
722    pub error_msg: String,
723}
724
725/// Typed response from cancel endpoints.
726#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
727#[serde(rename_all = "camelCase")]
728pub struct CancelOrdersResponse {
729    #[serde(default)]
730    pub canceled: Vec<String>,
731    #[serde(default, alias = "not_canceled")]
732    pub not_canceled: std::collections::HashMap<String, String>,
733}
734
735/// Token info returned by `GET /clob-markets/{condition_id}`.
736#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
737pub struct ClobTokenInfo {
738    pub t: String,
739    pub o: String,
740}
741
742/// Fee details returned by `GET /clob-markets/{condition_id}`.
743#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
744pub struct ClobFeeDetails {
745    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
746    pub r: Decimal,
747    pub e: u32,
748    #[serde(default)]
749    pub to: bool,
750}
751
752/// CLOB market info returned by `GET /clob-markets/{condition_id}`.
753#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
754pub struct ClobMarketInfo {
755    #[serde(default)]
756    pub c: Option<String>,
757    #[serde(default)]
758    pub gst: Option<String>,
759    #[serde(default)]
760    pub r: serde_json::Value,
761    #[serde(default)]
762    pub t: Vec<ClobTokenInfo>,
763    #[serde(
764        default,
765        deserialize_with = "crate::decode::deserializers::decimal_from_string_or_zero"
766    )]
767    pub mos: Decimal,
768    #[serde(
769        default,
770        deserialize_with = "crate::decode::deserializers::decimal_from_string_or_zero"
771    )]
772    pub mts: Decimal,
773    #[serde(
774        default,
775        deserialize_with = "crate::decode::deserializers::decimal_from_string_or_zero"
776    )]
777    pub mbf: Decimal,
778    #[serde(
779        default,
780        deserialize_with = "crate::decode::deserializers::decimal_from_string_or_zero"
781    )]
782    pub tbf: Decimal,
783    #[serde(default)]
784    pub rfqe: bool,
785    #[serde(default)]
786    pub itode: bool,
787    #[serde(default)]
788    pub ibce: bool,
789    #[serde(default)]
790    pub nr: Option<bool>,
791    #[serde(default)]
792    pub fd: Option<ClobFeeDetails>,
793    #[serde(
794        default,
795        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
796    )]
797    pub oas: Option<u64>,
798}
799
800/// Builder fee response returned by `GET /fees/builder-fees/{builder_code}`.
801#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
802#[serde(rename_all = "camelCase")]
803pub struct BuilderFeeRateResponse {
804    #[serde(alias = "builder_maker_fee_rate_bps")]
805    pub builder_maker_fee_rate_bps: u32,
806    #[serde(alias = "builder_taker_fee_rate_bps")]
807    pub builder_taker_fee_rate_bps: u32,
808}
809
810/// Market information
811#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct Market {
813    pub condition_id: String,
814    pub tokens: [Token; 2],
815    pub rewards: Rewards,
816    pub min_incentive_size: Option<String>,
817    pub max_incentive_spread: Option<String>,
818    pub active: bool,
819    pub closed: bool,
820    pub question_id: String,
821    pub minimum_order_size: Decimal,
822    pub minimum_tick_size: Decimal,
823    pub description: String,
824    pub category: Option<String>,
825    pub end_date_iso: Option<String>,
826    pub game_start_time: Option<String>,
827    pub question: String,
828    pub market_slug: String,
829    pub seconds_delay: Decimal,
830    pub icon: String,
831    pub fpmm: String,
832    // Additional fields from API
833    #[serde(default)]
834    pub enable_order_book: bool,
835    #[serde(default)]
836    pub archived: bool,
837    #[serde(default)]
838    pub accepting_orders: bool,
839    #[serde(default)]
840    pub accepting_order_timestamp: Option<String>,
841    #[serde(default)]
842    pub maker_base_fee: Decimal,
843    #[serde(default)]
844    pub taker_base_fee: Decimal,
845    #[serde(default)]
846    pub notifications_enabled: bool,
847    #[serde(default)]
848    pub neg_risk: bool,
849    #[serde(default)]
850    pub neg_risk_market_id: String,
851    #[serde(default)]
852    pub neg_risk_request_id: String,
853    #[serde(default)]
854    pub image: String,
855    #[serde(default)]
856    pub is_50_50_outcome: bool,
857}
858
859/// Token information within a market
860#[derive(Debug, Clone, Serialize, Deserialize)]
861pub struct Token {
862    pub token_id: String,
863    pub outcome: String,
864    pub price: Decimal,
865    #[serde(default)]
866    pub winner: bool,
867}
868
869/// Client configuration for PolyfillClient
870#[derive(Debug, Clone, Serialize, Deserialize)]
871pub struct ClientConfig {
872    /// Base URL for the API
873    pub base_url: String,
874    /// Chain ID for the network
875    pub chain: u64,
876    /// Private key for signing (optional)
877    pub private_key: Option<String>,
878    /// API credentials (optional)
879    pub api_credentials: Option<ApiCredentials>,
880    /// Builder code applied to orders when none is specified on the order itself.
881    pub builder_code: Option<String>,
882    /// Polymarket signature type: 0 EOA, 1 Proxy, 2 Gnosis Safe, 3 Poly1271.
883    pub signature_type: Option<u8>,
884    /// Address that holds funds for proxy/Safe/smart-contract wallet flows.
885    /// If omitted for signature type 1 or 2, the Polygon funder is derived from the signer.
886    pub funder: Option<String>,
887    /// Request timeout
888    pub timeout: Option<std::time::Duration>,
889    /// Maximum number of connections
890    pub max_connections: Option<usize>,
891}
892
893impl Default for ClientConfig {
894    fn default() -> Self {
895        Self {
896            base_url: "https://clob.polymarket.com".to_string(),
897            chain: 137, // Polygon mainnet
898            private_key: None,
899            api_credentials: None,
900            builder_code: None,
901            signature_type: None,
902            funder: None,
903            timeout: Some(std::time::Duration::from_secs(30)),
904            max_connections: Some(100),
905        }
906    }
907}
908
909/// WebSocket authentication for Polymarket API user channel.
910///
911/// Polymarket's CLOB WebSocket expects the same L2 API credentials used for HTTP calls:
912/// `{ apiKey, secret, passphrase }`.
913pub type WssAuth = ApiCredentials;
914
915/// WebSocket subscription request
916#[derive(Debug, Clone, Serialize, Deserialize)]
917pub struct WssSubscription {
918    /// Channel type: "market" or "user"
919    #[serde(rename = "type")]
920    pub channel_type: String,
921    /// Operation type: "subscribe" or "unsubscribe"
922    #[serde(skip_serializing_if = "Option::is_none")]
923    pub operation: Option<String>,
924    /// Array of markets (condition IDs) for USER channel
925    #[serde(default)]
926    pub markets: Vec<String>,
927    /// Array of asset IDs (token IDs) for MARKET channel
928    /// Note: Field name is "assets_ids" (with 's') per Polymarket API spec
929    #[serde(rename = "assets_ids", default)]
930    pub asset_ids: Vec<String>,
931    /// Request initial state dump
932    #[serde(skip_serializing_if = "Option::is_none")]
933    pub initial_dump: Option<bool>,
934    /// Enable custom features (best_bid_ask, new_market, market_resolved)
935    #[serde(skip_serializing_if = "Option::is_none")]
936    pub custom_feature_enabled: Option<bool>,
937    /// Authentication information (only for USER channel)
938    #[serde(skip_serializing_if = "Option::is_none")]
939    pub auth: Option<WssAuth>,
940}
941
942/// WebSocket message types for streaming (official Polymarket `event_type` format).
943#[derive(Debug, Clone, Serialize, Deserialize)]
944#[serde(tag = "event_type")]
945pub enum StreamMessage {
946    /// Full or incremental orderbook update
947    #[serde(rename = "book")]
948    Book(BookUpdate),
949    /// Price change notification (single or batched)
950    #[serde(rename = "price_change")]
951    PriceChange(PriceChange),
952    /// Tick size change notification
953    #[serde(rename = "tick_size_change")]
954    TickSizeChange(TickSizeChange),
955    /// Last trade price update
956    #[serde(rename = "last_trade_price")]
957    LastTradePrice(LastTradePrice),
958    /// Best bid/ask update (requires `custom_feature_enabled`)
959    #[serde(rename = "best_bid_ask")]
960    BestBidAsk(BestBidAsk),
961    /// New market created (requires `custom_feature_enabled`)
962    #[serde(rename = "new_market")]
963    NewMarket(NewMarket),
964    /// Market resolved (requires `custom_feature_enabled`)
965    #[serde(rename = "market_resolved")]
966    MarketResolved(MarketResolved),
967    /// User trade execution (authenticated channel)
968    #[serde(rename = "trade")]
969    Trade(TradeMessage),
970    /// User order update (authenticated channel)
971    #[serde(rename = "order")]
972    Order(OrderMessage),
973    /// Forward-compatible catch-all for new/unknown event types.
974    #[serde(other)]
975    Unknown,
976}
977
978/// Orderbook update message (full snapshot or delta).
979///
980/// WebSocket `book` messages expose a millisecond timestamp and optional book hash, but no
981/// monotonic server sequence/version. Same-timestamp messages with different hashes are therefore
982/// ordered by websocket arrival order by the book applier; the hash is a duplicate/state
983/// discriminator, not a logical ordering key.
984#[derive(Debug, Clone, Serialize, Deserialize)]
985pub struct BookUpdate {
986    pub asset_id: String,
987    pub market: String,
988    /// Exchange-provided snapshot timestamp in milliseconds.
989    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
990    pub timestamp: u64,
991    #[serde(deserialize_with = "crate::decode::deserializers::vec_from_null")]
992    pub bids: Vec<OrderSummary>,
993    #[serde(deserialize_with = "crate::decode::deserializers::vec_from_null")]
994    pub asks: Vec<OrderSummary>,
995    /// Exchange-provided snapshot hash.
996    ///
997    /// Used to suppress exact duplicate same-timestamp snapshots and to allow distinct
998    /// same-timestamp states. It does not encode ordering.
999    #[serde(default)]
1000    pub hash: Option<String>,
1001}
1002
1003/// Unified wire format for `price_change` events.
1004#[derive(Debug, Clone, Serialize, Deserialize)]
1005pub struct PriceChange {
1006    pub market: String,
1007    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1008    pub timestamp: u64,
1009    #[serde(
1010        default,
1011        deserialize_with = "crate::decode::deserializers::vec_from_null"
1012    )]
1013    pub price_changes: Vec<PriceChangeEntry>,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1017pub struct PriceChangeEntry {
1018    pub asset_id: String,
1019    pub price: Decimal,
1020    #[serde(
1021        default,
1022        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1023    )]
1024    pub size: Option<Decimal>,
1025    pub side: Side,
1026    #[serde(default)]
1027    pub hash: Option<String>,
1028    #[serde(
1029        default,
1030        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1031    )]
1032    pub best_bid: Option<Decimal>,
1033    #[serde(
1034        default,
1035        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1036    )]
1037    pub best_ask: Option<Decimal>,
1038}
1039
1040/// Tick size change event.
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1042pub struct TickSizeChange {
1043    pub asset_id: String,
1044    pub market: String,
1045    pub old_tick_size: Decimal,
1046    pub new_tick_size: Decimal,
1047    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1048    pub timestamp: u64,
1049}
1050
1051/// Last trade price update.
1052#[derive(Debug, Clone, Serialize, Deserialize)]
1053pub struct LastTradePrice {
1054    pub asset_id: String,
1055    pub market: String,
1056    pub price: Decimal,
1057    #[serde(default)]
1058    pub side: Option<Side>,
1059    #[serde(
1060        default,
1061        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1062    )]
1063    pub size: Option<Decimal>,
1064    #[serde(
1065        default,
1066        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1067    )]
1068    pub fee_rate_bps: Option<Decimal>,
1069    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1070    pub timestamp: u64,
1071}
1072
1073/// Best bid/ask update.
1074#[derive(Debug, Clone, Serialize, Deserialize)]
1075pub struct BestBidAsk {
1076    pub market: String,
1077    pub asset_id: String,
1078    pub best_bid: Decimal,
1079    pub best_ask: Decimal,
1080    pub spread: Decimal,
1081    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1082    pub timestamp: u64,
1083}
1084
1085/// New market created event.
1086#[derive(Debug, Clone, Serialize, Deserialize)]
1087pub struct NewMarket {
1088    pub id: String,
1089    pub question: String,
1090    pub market: String,
1091    pub slug: String,
1092    pub description: String,
1093    #[serde(rename = "assets_ids", alias = "asset_ids")]
1094    pub asset_ids: Vec<String>,
1095    #[serde(
1096        default,
1097        deserialize_with = "crate::decode::deserializers::vec_from_null"
1098    )]
1099    pub outcomes: Vec<String>,
1100    #[serde(default)]
1101    pub event_message: Option<EventMessage>,
1102    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1103    pub timestamp: u64,
1104}
1105
1106/// Market resolved event.
1107#[derive(Debug, Clone, Serialize, Deserialize)]
1108pub struct MarketResolved {
1109    pub id: String,
1110    #[serde(default)]
1111    pub question: Option<String>,
1112    pub market: String,
1113    #[serde(default)]
1114    pub slug: Option<String>,
1115    #[serde(default)]
1116    pub description: Option<String>,
1117    #[serde(rename = "assets_ids", alias = "asset_ids")]
1118    pub asset_ids: Vec<String>,
1119    #[serde(
1120        default,
1121        deserialize_with = "crate::decode::deserializers::vec_from_null"
1122    )]
1123    pub outcomes: Vec<String>,
1124    pub winning_asset_id: String,
1125    pub winning_outcome: String,
1126    #[serde(default)]
1127    pub event_message: Option<EventMessage>,
1128    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1129    pub timestamp: u64,
1130}
1131
1132/// Event message object for market events.
1133#[derive(Debug, Clone, Serialize, Deserialize)]
1134pub struct EventMessage {
1135    pub id: String,
1136    pub ticker: String,
1137    pub slug: String,
1138    pub title: String,
1139    pub description: String,
1140}
1141
1142/// User trade execution message.
1143#[derive(Debug, Clone, Serialize, Deserialize)]
1144pub struct TradeMessage {
1145    pub id: String,
1146    pub market: String,
1147    pub asset_id: String,
1148    pub side: Side,
1149    pub size: Decimal,
1150    pub price: Decimal,
1151    #[serde(default)]
1152    pub status: Option<String>,
1153    #[serde(rename = "type", default)]
1154    pub msg_type: Option<String>,
1155    #[serde(
1156        default,
1157        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1158    )]
1159    pub last_update: Option<u64>,
1160    #[serde(
1161        default,
1162        alias = "match_time",
1163        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1164    )]
1165    pub matchtime: Option<u64>,
1166    #[serde(
1167        default,
1168        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1169    )]
1170    pub timestamp: Option<u64>,
1171}
1172
1173/// User order update message.
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1175pub struct OrderMessage {
1176    pub id: String,
1177    pub market: String,
1178    pub asset_id: String,
1179    pub side: Side,
1180    pub price: Decimal,
1181    #[serde(rename = "type", default)]
1182    pub msg_type: Option<String>,
1183    #[serde(
1184        default,
1185        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1186    )]
1187    pub original_size: Option<Decimal>,
1188    #[serde(
1189        default,
1190        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1191    )]
1192    pub size_matched: Option<Decimal>,
1193    #[serde(
1194        default,
1195        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1196    )]
1197    pub timestamp: Option<u64>,
1198    #[serde(default)]
1199    pub associate_trades: Option<Vec<String>>,
1200    #[serde(default)]
1201    pub status: Option<String>,
1202}
1203
1204/// Subscription parameters for streaming
1205#[derive(Debug, Clone, Serialize, Deserialize)]
1206pub struct Subscription {
1207    pub token_ids: Vec<String>,
1208    pub channels: Vec<String>,
1209}
1210
1211/// WebSocket channel types
1212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1213pub enum WssChannelType {
1214    #[serde(rename = "USER")]
1215    User,
1216    #[serde(rename = "MARKET")]
1217    Market,
1218}
1219
1220impl WssChannelType {
1221    pub fn as_str(&self) -> &'static str {
1222        match self {
1223            WssChannelType::User => "USER",
1224            WssChannelType::Market => "MARKET",
1225        }
1226    }
1227}
1228
1229/// Price quote response
1230#[derive(Debug, Clone, Serialize, Deserialize)]
1231pub struct Quote {
1232    pub token_id: String,
1233    pub side: Side,
1234    #[serde(with = "rust_decimal::serde::str")]
1235    pub price: Decimal,
1236    pub timestamp: DateTime<Utc>,
1237}
1238
1239/// Balance information
1240#[derive(Debug, Clone, Serialize, Deserialize)]
1241pub struct Balance {
1242    pub token_id: String,
1243    pub available: Decimal,
1244    pub locked: Decimal,
1245    pub total: Decimal,
1246}
1247
1248/// Performance metrics for monitoring
1249#[derive(Debug, Clone)]
1250pub struct Metrics {
1251    pub orders_per_second: f64,
1252    pub avg_latency_ms: f64,
1253    pub error_rate: f64,
1254    pub uptime_pct: f64,
1255}
1256
1257// Type aliases for common patterns
1258pub type TokenId = String;
1259pub type OrderId = String;
1260pub type MarketId = String;
1261pub type ClientId = String;
1262
1263/// Parameters for querying open orders
1264#[derive(Debug, Clone)]
1265pub struct OpenOrderParams {
1266    pub id: Option<String>,
1267    pub asset_id: Option<String>,
1268    pub market: Option<String>,
1269}
1270
1271impl OpenOrderParams {
1272    pub fn to_query_params(&self) -> Vec<(&str, &String)> {
1273        let mut params = Vec::with_capacity(3);
1274
1275        if let Some(x) = &self.id {
1276            params.push(("id", x));
1277        }
1278
1279        if let Some(x) = &self.asset_id {
1280            params.push(("asset_id", x));
1281        }
1282
1283        if let Some(x) = &self.market {
1284            params.push(("market", x));
1285        }
1286        params
1287    }
1288}
1289
1290/// Parameters for querying trades
1291#[derive(Debug, Clone)]
1292pub struct TradeParams {
1293    pub id: Option<String>,
1294    pub maker_address: Option<String>,
1295    pub market: Option<String>,
1296    pub asset_id: Option<String>,
1297    pub before: Option<u64>,
1298    pub after: Option<u64>,
1299}
1300
1301impl TradeParams {
1302    pub fn to_query_params(&self) -> Vec<(&str, String)> {
1303        let mut params = Vec::with_capacity(6);
1304
1305        if let Some(x) = &self.id {
1306            params.push(("id", x.clone()));
1307        }
1308
1309        if let Some(x) = &self.asset_id {
1310            params.push(("asset_id", x.clone()));
1311        }
1312
1313        if let Some(x) = &self.market {
1314            params.push(("market", x.clone()));
1315        }
1316
1317        if let Some(x) = &self.maker_address {
1318            params.push(("maker_address", x.clone()));
1319        }
1320
1321        if let Some(x) = &self.before {
1322            params.push(("before", x.to_string()));
1323        }
1324
1325        if let Some(x) = &self.after {
1326            params.push(("after", x.to_string()));
1327        }
1328
1329        params
1330    }
1331}
1332
1333/// Open order information
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1335pub struct OpenOrder {
1336    pub associate_trades: Vec<String>,
1337    pub id: String,
1338    pub status: String,
1339    pub market: String,
1340    #[serde(with = "rust_decimal::serde::str")]
1341    pub original_size: Decimal,
1342    pub outcome: String,
1343    pub maker_address: String,
1344    pub owner: String,
1345    #[serde(with = "rust_decimal::serde::str")]
1346    pub price: Decimal,
1347    pub side: Side,
1348    #[serde(with = "rust_decimal::serde::str")]
1349    pub size_matched: Decimal,
1350    pub asset_id: String,
1351    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1352    pub expiration: u64,
1353    #[serde(rename = "type", alias = "order_type", alias = "orderType", default)]
1354    pub order_type: OrderType,
1355    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1356    pub created_at: u64,
1357}
1358
1359/// Balance allowance information
1360#[derive(Debug, Clone, Serialize, Deserialize)]
1361pub struct BalanceAllowance {
1362    pub asset_id: String,
1363    #[serde(with = "rust_decimal::serde::str")]
1364    pub balance: Decimal,
1365    #[serde(with = "rust_decimal::serde::str")]
1366    pub allowance: Decimal,
1367}
1368
1369/// Parameters for balance allowance queries (from reference implementation)
1370#[derive(Default)]
1371pub struct BalanceAllowanceParams {
1372    pub asset_type: Option<AssetType>,
1373    pub token_id: Option<String>,
1374    pub signature_type: Option<u8>,
1375}
1376
1377impl BalanceAllowanceParams {
1378    pub fn to_query_params(&self) -> Vec<(&str, String)> {
1379        let mut params = Vec::with_capacity(3);
1380
1381        if let Some(x) = &self.asset_type {
1382            params.push(("asset_type", x.to_string()));
1383        }
1384
1385        if let Some(x) = &self.token_id {
1386            params.push(("token_id", x.to_string()));
1387        }
1388
1389        if let Some(x) = &self.signature_type {
1390            params.push(("signature_type", x.to_string()));
1391        }
1392        params
1393    }
1394
1395    pub fn set_signature_type(&mut self, s: u8) {
1396        self.signature_type = Some(s);
1397    }
1398}
1399
1400/// Asset type enum for balance allowance queries
1401#[allow(clippy::upper_case_acronyms)]
1402pub enum AssetType {
1403    COLLATERAL,
1404    CONDITIONAL,
1405}
1406
1407impl std::fmt::Display for AssetType {
1408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1409        match self {
1410            AssetType::COLLATERAL => write!(f, "COLLATERAL"),
1411            AssetType::CONDITIONAL => write!(f, "CONDITIONAL"),
1412        }
1413    }
1414}
1415
1416/// Notification preferences
1417#[derive(Debug, Clone, Serialize, Deserialize)]
1418pub struct NotificationParams {
1419    pub signature: String,
1420    pub timestamp: u64,
1421}
1422
1423/// Batch midpoint request
1424#[derive(Debug, Clone, Serialize, Deserialize)]
1425pub struct BatchMidpointRequest {
1426    pub token_ids: Vec<String>,
1427}
1428
1429/// Batch midpoint response
1430#[derive(Debug, Clone, Serialize, Deserialize)]
1431pub struct BatchMidpointResponse {
1432    pub midpoints: std::collections::HashMap<String, Option<Decimal>>,
1433}
1434
1435/// Batch price request
1436#[derive(Debug, Clone, Serialize, Deserialize)]
1437pub struct BatchPriceRequest {
1438    pub token_ids: Vec<String>,
1439}
1440
1441/// Price information for a token
1442#[derive(Debug, Clone, Serialize, Deserialize)]
1443pub struct TokenPrice {
1444    pub token_id: String,
1445    #[serde(skip_serializing_if = "Option::is_none")]
1446    pub bid: Option<Decimal>,
1447    #[serde(skip_serializing_if = "Option::is_none")]
1448    pub ask: Option<Decimal>,
1449    #[serde(skip_serializing_if = "Option::is_none")]
1450    pub mid: Option<Decimal>,
1451}
1452
1453/// Batch price response
1454#[derive(Debug, Clone, Serialize, Deserialize)]
1455pub struct BatchPriceResponse {
1456    pub prices: Vec<TokenPrice>,
1457}
1458
1459// Additional types for API compatibility with reference implementation
1460#[derive(Debug, Deserialize)]
1461pub struct ApiKeysResponse {
1462    #[serde(rename = "apiKeys")]
1463    pub api_keys: Vec<String>,
1464}
1465
1466#[derive(Debug, Deserialize)]
1467pub struct MidpointResponse {
1468    #[serde(with = "rust_decimal::serde::str")]
1469    pub mid: Decimal,
1470}
1471
1472#[derive(Debug, Deserialize)]
1473pub struct PriceResponse {
1474    #[serde(with = "rust_decimal::serde::str")]
1475    pub price: Decimal,
1476}
1477
1478// ============================================================================
1479// PRICE HISTORY (ANALYTICS)
1480// ============================================================================
1481
1482/// Time bucket for the `/prices-history` endpoint.
1483///
1484/// Note: this endpoint uses a confusing query parameter name (`market`) but expects an
1485/// outcome asset id (`token_id` / `asset_id`) in **decimal string** form.
1486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1487pub enum PricesHistoryInterval {
1488    OneMinute,
1489    OneHour,
1490    SixHours,
1491    OneDay,
1492    OneWeek,
1493}
1494
1495impl PricesHistoryInterval {
1496    pub const fn as_str(self) -> &'static str {
1497        match self {
1498            Self::OneMinute => "1m",
1499            Self::OneHour => "1h",
1500            Self::SixHours => "6h",
1501            Self::OneDay => "1d",
1502            Self::OneWeek => "1w",
1503        }
1504    }
1505}
1506
1507/// Raw response from `/prices-history`.
1508///
1509/// We intentionally keep `history` entries as `serde_json::Value` because the upstream API has
1510/// no stable public schema here and currently may return empty history for many markets.
1511#[derive(Debug, Clone, Serialize, Deserialize)]
1512pub struct PricesHistoryResponse {
1513    pub history: Vec<serde_json::Value>,
1514}
1515
1516#[derive(Debug, Deserialize)]
1517pub struct SpreadResponse {
1518    #[serde(with = "rust_decimal::serde::str")]
1519    pub spread: Decimal,
1520}
1521
1522#[derive(Debug, Deserialize)]
1523pub struct TickSizeResponse {
1524    #[serde(with = "rust_decimal::serde::str")]
1525    pub minimum_tick_size: Decimal,
1526}
1527
1528#[derive(Debug, Deserialize)]
1529pub struct NegRiskResponse {
1530    pub neg_risk: bool,
1531}
1532
1533#[derive(Debug, Serialize, Deserialize)]
1534pub struct BookParams {
1535    pub token_id: String,
1536    pub side: Side,
1537}
1538
1539#[derive(Debug, Deserialize)]
1540pub struct OrderBookSummary {
1541    pub market: String,
1542    pub asset_id: String,
1543    #[serde(default)]
1544    pub hash: Option<String>,
1545    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1546    pub timestamp: u64,
1547    #[serde(
1548        default,
1549        deserialize_with = "crate::decode::deserializers::vec_from_null"
1550    )]
1551    pub bids: Vec<OrderSummary>,
1552    #[serde(
1553        default,
1554        deserialize_with = "crate::decode::deserializers::vec_from_null"
1555    )]
1556    pub asks: Vec<OrderSummary>,
1557    pub min_order_size: Decimal,
1558    pub neg_risk: bool,
1559    pub tick_size: Decimal,
1560    #[serde(
1561        default,
1562        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error"
1563    )]
1564    pub last_trade_price: Option<Decimal>,
1565}
1566
1567#[derive(Debug, Clone, Serialize, Deserialize)]
1568pub struct OrderSummary {
1569    #[serde(with = "rust_decimal::serde::str")]
1570    pub price: Decimal,
1571    #[serde(with = "rust_decimal::serde::str")]
1572    pub size: Decimal,
1573}
1574
1575#[derive(Debug, Serialize, Deserialize)]
1576pub struct MarketsResponse {
1577    pub limit: usize,
1578    pub count: usize,
1579    pub next_cursor: Option<String>,
1580    pub data: Vec<Market>,
1581}
1582
1583#[derive(Debug, Serialize, Deserialize)]
1584pub struct SimplifiedMarketsResponse {
1585    pub limit: usize,
1586    pub count: usize,
1587    pub next_cursor: Option<String>,
1588    pub data: Vec<SimplifiedMarket>,
1589}
1590
1591/// Simplified market structure for batch operations
1592#[derive(Debug, Serialize, Deserialize)]
1593pub struct SimplifiedMarket {
1594    pub condition_id: String,
1595    pub tokens: [Token; 2],
1596    pub rewards: Rewards,
1597    pub min_incentive_size: Option<String>,
1598    pub max_incentive_spread: Option<String>,
1599    pub active: bool,
1600    pub closed: bool,
1601}
1602
1603/// Rewards structure for markets
1604#[derive(Debug, Clone, Serialize, Deserialize)]
1605pub struct Rewards {
1606    pub rates: Option<serde_json::Value>,
1607    // API returns these as plain numbers, not strings
1608    pub min_size: Decimal,
1609    pub max_spread: Decimal,
1610    #[serde(default)]
1611    pub event_start_date: Option<String>,
1612    #[serde(default)]
1613    pub event_end_date: Option<String>,
1614    #[serde(skip_serializing_if = "Option::is_none", default)]
1615    pub in_game_multiplier: Option<Decimal>,
1616    #[serde(skip_serializing_if = "Option::is_none", default)]
1617    pub reward_epoch: Option<Decimal>,
1618}
1619
1620// ============================================================================
1621// CLOB API: Fee Rate + RFQ (Market Maker) Types
1622// ============================================================================
1623
1624/// Fee rate in basis points for a given token.
1625#[derive(Debug, Clone, Serialize, Deserialize)]
1626pub struct FeeRateResponse {
1627    #[serde(alias = "fee_rate_bps")]
1628    pub base_fee: u32,
1629}
1630
1631/// Create RFQ request (Requester).
1632#[derive(Debug, Clone, Serialize)]
1633#[serde(rename_all = "camelCase")]
1634pub struct RfqCreateRequest {
1635    pub asset_in: String,
1636    pub asset_out: String,
1637    pub amount_in: String,
1638    pub amount_out: String,
1639    pub user_type: u8,
1640}
1641
1642#[derive(Debug, Clone, Serialize, Deserialize)]
1643#[serde(rename_all = "camelCase")]
1644pub struct RfqCreateRequestResponse {
1645    pub request_id: String,
1646    pub expiry: u64,
1647}
1648
1649/// Cancel RFQ request (Requester).
1650#[derive(Debug, Clone, Serialize)]
1651#[serde(rename_all = "camelCase")]
1652pub struct RfqCancelRequest {
1653    pub request_id: String,
1654}
1655
1656/// RFQ request list query parameters.
1657#[derive(Debug, Clone, Default)]
1658pub struct RfqRequestsParams {
1659    pub offset: Option<String>,
1660    pub limit: Option<u32>,
1661    pub state: Option<String>,
1662    pub request_ids: Vec<String>,
1663    pub markets: Vec<String>,
1664    pub size_min: Option<Decimal>,
1665    pub size_max: Option<Decimal>,
1666    pub size_usdc_min: Option<Decimal>,
1667    pub size_usdc_max: Option<Decimal>,
1668    pub price_min: Option<Decimal>,
1669    pub price_max: Option<Decimal>,
1670    pub sort_by: Option<String>,
1671    pub sort_dir: Option<String>,
1672}
1673
1674impl RfqRequestsParams {
1675    pub fn to_query_params(&self) -> Vec<(String, String)> {
1676        let mut params = Vec::new();
1677
1678        if let Some(x) = &self.offset {
1679            params.push(("offset".to_string(), x.clone()));
1680        }
1681        if let Some(x) = self.limit {
1682            params.push(("limit".to_string(), x.to_string()));
1683        }
1684        if let Some(x) = &self.state {
1685            params.push(("state".to_string(), x.clone()));
1686        }
1687        for x in &self.request_ids {
1688            params.push(("requestIds[]".to_string(), x.clone()));
1689        }
1690        for x in &self.markets {
1691            params.push(("markets[]".to_string(), x.clone()));
1692        }
1693
1694        if let Some(x) = self.size_min {
1695            params.push(("sizeMin".to_string(), x.to_string()));
1696        }
1697        if let Some(x) = self.size_max {
1698            params.push(("sizeMax".to_string(), x.to_string()));
1699        }
1700        if let Some(x) = self.size_usdc_min {
1701            params.push(("sizeUsdcMin".to_string(), x.to_string()));
1702        }
1703        if let Some(x) = self.size_usdc_max {
1704            params.push(("sizeUsdcMax".to_string(), x.to_string()));
1705        }
1706        if let Some(x) = self.price_min {
1707            params.push(("priceMin".to_string(), x.to_string()));
1708        }
1709        if let Some(x) = self.price_max {
1710            params.push(("priceMax".to_string(), x.to_string()));
1711        }
1712
1713        if let Some(x) = &self.sort_by {
1714            params.push(("sortBy".to_string(), x.clone()));
1715        }
1716        if let Some(x) = &self.sort_dir {
1717            params.push(("sortDir".to_string(), x.clone()));
1718        }
1719
1720        params
1721    }
1722}
1723
1724/// RFQ request data.
1725#[derive(Debug, Clone, Serialize, Deserialize)]
1726#[serde(rename_all = "camelCase")]
1727pub struct RfqRequestData {
1728    pub request_id: String,
1729    pub user_address: String,
1730    pub proxy_address: String,
1731    pub condition: String,
1732    pub token: String,
1733    pub complement: String,
1734    pub side: Side,
1735    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1736    pub size_in: Decimal,
1737    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1738    pub size_out: Decimal,
1739    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1740    pub price: Decimal,
1741    pub state: String,
1742    pub expiry: u64,
1743}
1744
1745/// Create RFQ quote (Quoter).
1746#[derive(Debug, Clone, Serialize)]
1747#[serde(rename_all = "camelCase")]
1748pub struct RfqCreateQuote {
1749    pub request_id: String,
1750    pub asset_in: String,
1751    pub asset_out: String,
1752    pub amount_in: String,
1753    pub amount_out: String,
1754    pub user_type: u8,
1755}
1756
1757#[derive(Debug, Clone, Serialize, Deserialize)]
1758#[serde(rename_all = "camelCase")]
1759pub struct RfqCreateQuoteResponse {
1760    pub quote_id: String,
1761}
1762
1763/// Cancel RFQ quote (Quoter).
1764#[derive(Debug, Clone, Serialize)]
1765#[serde(rename_all = "camelCase")]
1766pub struct RfqCancelQuote {
1767    pub quote_id: String,
1768}
1769
1770/// RFQ quote list query parameters.
1771#[derive(Debug, Clone, Default)]
1772pub struct RfqQuotesParams {
1773    pub offset: Option<String>,
1774    pub limit: Option<u32>,
1775    pub state: Option<String>,
1776    pub quote_ids: Vec<String>,
1777    pub request_ids: Vec<String>,
1778    pub markets: Vec<String>,
1779    pub size_min: Option<Decimal>,
1780    pub size_max: Option<Decimal>,
1781    pub size_usdc_min: Option<Decimal>,
1782    pub size_usdc_max: Option<Decimal>,
1783    pub price_min: Option<Decimal>,
1784    pub price_max: Option<Decimal>,
1785    pub sort_by: Option<String>,
1786    pub sort_dir: Option<String>,
1787}
1788
1789impl RfqQuotesParams {
1790    pub fn to_query_params(&self) -> Vec<(String, String)> {
1791        let mut params = Vec::new();
1792
1793        if let Some(x) = &self.offset {
1794            params.push(("offset".to_string(), x.clone()));
1795        }
1796        if let Some(x) = self.limit {
1797            params.push(("limit".to_string(), x.to_string()));
1798        }
1799        if let Some(x) = &self.state {
1800            params.push(("state".to_string(), x.clone()));
1801        }
1802        for x in &self.quote_ids {
1803            params.push(("quoteIds[]".to_string(), x.clone()));
1804        }
1805        for x in &self.request_ids {
1806            params.push(("requestIds[]".to_string(), x.clone()));
1807        }
1808        for x in &self.markets {
1809            params.push(("markets[]".to_string(), x.clone()));
1810        }
1811
1812        if let Some(x) = self.size_min {
1813            params.push(("sizeMin".to_string(), x.to_string()));
1814        }
1815        if let Some(x) = self.size_max {
1816            params.push(("sizeMax".to_string(), x.to_string()));
1817        }
1818        if let Some(x) = self.size_usdc_min {
1819            params.push(("sizeUsdcMin".to_string(), x.to_string()));
1820        }
1821        if let Some(x) = self.size_usdc_max {
1822            params.push(("sizeUsdcMax".to_string(), x.to_string()));
1823        }
1824        if let Some(x) = self.price_min {
1825            params.push(("priceMin".to_string(), x.to_string()));
1826        }
1827        if let Some(x) = self.price_max {
1828            params.push(("priceMax".to_string(), x.to_string()));
1829        }
1830
1831        if let Some(x) = &self.sort_by {
1832            params.push(("sortBy".to_string(), x.clone()));
1833        }
1834        if let Some(x) = &self.sort_dir {
1835            params.push(("sortDir".to_string(), x.clone()));
1836        }
1837
1838        params
1839    }
1840}
1841
1842/// RFQ quote data.
1843#[derive(Debug, Clone, Serialize, Deserialize)]
1844#[serde(rename_all = "camelCase")]
1845pub struct RfqQuoteData {
1846    pub quote_id: String,
1847    pub request_id: String,
1848    pub user_address: String,
1849    pub proxy_address: String,
1850    pub condition: String,
1851    pub token: String,
1852    pub complement: String,
1853    pub side: Side,
1854    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1855    pub size_in: Decimal,
1856    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1857    pub size_out: Decimal,
1858    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1859    pub price: Decimal,
1860    pub match_type: String,
1861    pub state: String,
1862}
1863
1864/// Generic RFQ list response wrapper.
1865#[derive(Debug, Clone, Serialize, Deserialize)]
1866pub struct RfqListResponse<T> {
1867    pub data: Vec<T>,
1868    pub next_cursor: Option<String>,
1869    pub limit: u32,
1870    pub count: u32,
1871}
1872
1873/// RFQ order execution request (used for both accept + approve).
1874#[derive(Debug, Clone, Serialize)]
1875#[serde(rename_all = "camelCase")]
1876pub struct RfqOrderExecutionRequest {
1877    pub request_id: String,
1878    pub quote_id: String,
1879    pub maker: String,
1880    pub signer: String,
1881    pub taker: String,
1882    pub expiration: u64,
1883    pub nonce: String,
1884    pub fee_rate_bps: String,
1885    pub side: String,
1886    pub token_id: String,
1887    pub maker_amount: String,
1888    pub taker_amount: String,
1889    pub signature_type: u8,
1890    pub signature: String,
1891    pub salt: u64,
1892    pub owner: String,
1893}
1894
1895#[derive(Debug, Clone, Serialize, Deserialize)]
1896#[serde(rename_all = "camelCase")]
1897pub struct RfqApproveOrderResponse {
1898    pub trade_ids: Vec<String>,
1899}
1900
1901// For compatibility with reference implementation
1902pub type ClientResult<T> = anyhow::Result<T>;
1903
1904/// Result type used throughout the client
1905pub type Result<T> = std::result::Result<T, crate::errors::PolyfillError>;
1906
1907// Type aliases for 100% compatibility with baseline implementation
1908pub type ApiCreds = ApiCredentials;
1909
1910#[cfg(test)]
1911mod tests {
1912    use super::*;
1913    use std::str::FromStr;
1914
1915    #[test]
1916    fn decimal_to_price_exact_accepts_representable_prices() {
1917        assert_eq!(
1918            decimal_to_price_exact(Decimal::from_str("0.6543").unwrap()).unwrap(),
1919            6543
1920        );
1921        assert_eq!(
1922            decimal_to_price_exact(Decimal::from_str("1.0000").unwrap()).unwrap(),
1923            10_000
1924        );
1925        assert_eq!(
1926            decimal_to_price(Decimal::from_str("0.0001").unwrap()).unwrap(),
1927            MIN_PRICE_TICKS
1928        );
1929    }
1930
1931    #[test]
1932    fn decimal_to_price_exact_rejects_fractional_or_clamped_prices() {
1933        assert!(decimal_to_price_exact(Decimal::from_str("0.00005").unwrap()).is_err());
1934        assert!(decimal_to_price_exact(Decimal::from_str("0.00009").unwrap()).is_err());
1935        assert!(decimal_to_price_exact(Decimal::ZERO).is_err());
1936        assert!(decimal_to_price_exact(Decimal::from_str("-0.01").unwrap()).is_err());
1937    }
1938
1939    #[test]
1940    fn decimal_to_price_lossy_preserves_rounding_and_clamping_behavior() {
1941        assert_eq!(
1942            decimal_to_price_lossy(Decimal::from_str("0.65434").unwrap()).unwrap(),
1943            6543
1944        );
1945        assert_eq!(
1946            decimal_to_price_lossy(Decimal::from_str("0.65435").unwrap()).unwrap(),
1947            6544
1948        );
1949        assert_eq!(
1950            decimal_to_price_lossy(Decimal::from_str("0.00005").unwrap()).unwrap(),
1951            MIN_PRICE_TICKS
1952        );
1953    }
1954
1955    #[test]
1956    fn price_tick_alignment_uses_exact_conversion() {
1957        assert!(is_price_tick_aligned(
1958            Decimal::from_str("0.5100").unwrap(),
1959            Decimal::from_str("0.0100").unwrap()
1960        ));
1961        assert!(!is_price_tick_aligned(
1962            Decimal::from_str("0.5150").unwrap(),
1963            Decimal::from_str("0.0100").unwrap()
1964        ));
1965        assert!(!is_price_tick_aligned(
1966            Decimal::from_str("0.51005").unwrap(),
1967            Decimal::from_str("0.0100").unwrap()
1968        ));
1969        assert!(!is_price_tick_aligned(
1970            Decimal::from_str("0.5100").unwrap(),
1971            Decimal::from_str("0.00005").unwrap()
1972        ));
1973    }
1974
1975    #[test]
1976    fn order_book_snapshot_clocks_default_for_legacy_payloads() {
1977        let json = r#"{
1978            "token_id":"test_token",
1979            "timestamp":"2026-06-23T00:00:00Z",
1980            "bids":[],
1981            "asks":[],
1982            "sequence":42
1983        }"#;
1984
1985        let book: OrderBook = serde_json::from_str(json).unwrap();
1986        assert_eq!(book.sequence, 42);
1987        assert_eq!(book.last_delta_sequence, 42);
1988        assert_eq!(book.last_snapshot_timestamp_ms, 0);
1989    }
1990}