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