Skip to main content

polyfill2/
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, B256, U256};
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
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195#[serde(rename_all = "UPPERCASE")]
196pub enum TraderSide {
197    Taker,
198    Maker,
199    #[serde(untagged)]
200    Unknown(String),
201}
202
203impl Default for TraderSide {
204    fn default() -> Self {
205        Self::Unknown("UNKNOWN".to_string())
206    }
207}
208
209/// Trade lifecycle status (Matched → Mined → Confirmed).
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub enum TradeMessageStatus {
212    #[serde(alias = "matched", alias = "MATCHED")]
213    Matched,
214    #[serde(alias = "mined", alias = "MINED")]
215    Mined,
216    #[serde(alias = "confirmed", alias = "CONFIRMED")]
217    Confirmed,
218    #[serde(alias = "retrying", alias = "RETRYING")]
219    Retrying,
220    #[serde(alias = "failed", alias = "FAILED")]
221    Failed,
222    /// Forward-compatible catch-all for unknown statuses.
223    #[serde(untagged)]
224    Unknown(String),
225}
226
227impl Default for TradeMessageStatus {
228    fn default() -> Self {
229        Self::Unknown("UNKNOWN".to_string())
230    }
231}
232
233/// Trade message type discriminator.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub enum TradeMessageType {
236    #[serde(alias = "trade", alias = "TRADE")]
237    Trade,
238    /// Forward-compatible catch-all.
239    #[serde(untagged)]
240    Unknown(String),
241}
242
243impl Side {
244    pub fn as_str(&self) -> &'static str {
245        match self {
246            Side::BUY => "BUY",
247            Side::SELL => "SELL",
248        }
249    }
250
251    pub fn opposite(&self) -> Self {
252        match self {
253            Side::BUY => Side::SELL,
254            Side::SELL => Side::BUY,
255        }
256    }
257}
258
259/// Order type specifications
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
261#[allow(clippy::upper_case_acronyms)]
262pub enum OrderType {
263    #[default]
264    GTC,
265    FOK,
266    FAK,
267    GTD,
268}
269
270impl OrderType {
271    pub fn as_str(&self) -> &'static str {
272        match self {
273            OrderType::GTC => "GTC",
274            OrderType::FOK => "FOK",
275            OrderType::FAK => "FAK",
276            OrderType::GTD => "GTD",
277        }
278    }
279}
280
281/// Order status in the system
282#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
283pub enum OrderStatus {
284    #[serde(rename = "LIVE")]
285    Live,
286    #[serde(rename = "CANCELLED")]
287    Cancelled,
288    #[serde(rename = "FILLED")]
289    Filled,
290    #[serde(rename = "PARTIAL")]
291    Partial,
292    #[serde(rename = "EXPIRED")]
293    Expired,
294}
295
296/// Market snapshot representing current state
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct MarketSnapshot {
299    pub token_id: String,
300    pub market_id: String,
301    pub timestamp: DateTime<Utc>,
302    pub bid: Option<Decimal>,
303    pub ask: Option<Decimal>,
304    pub mid: Option<Decimal>,
305    pub spread: Option<Decimal>,
306    pub last_price: Option<Decimal>,
307    pub volume_24h: Option<Decimal>,
308}
309
310/// Order book level (price/size pair) - EXTERNAL API VERSION
311///
312/// This is what we expose to users and serialize to JSON.
313/// It uses Decimal for precision and human readability.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct BookLevel {
316    #[serde(with = "rust_decimal::serde::str")]
317    pub price: Decimal,
318    #[serde(with = "rust_decimal::serde::str")]
319    pub size: Decimal,
320}
321
322/// Order book level (price/size pair) - INTERNAL HOT PATH VERSION
323///
324/// This is what we use internally for maximum performance.
325/// All order book operations use this to avoid Decimal overhead.
326///
327/// The performance difference is huge:
328/// - BookLevel: ~50ns per operation (Decimal math + allocation)
329/// - FastBookLevel: ~2ns per operation (integer math, no allocation)
330///
331/// That's a 25x speedup on the critical path
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333pub struct FastBookLevel {
334    pub price: Price, // Price in ticks (u32)
335    pub size: Qty,    // Size in fixed-point units (i64)
336}
337
338impl FastBookLevel {
339    /// Create a new fast book level
340    pub fn new(price: Price, size: Qty) -> Self {
341        Self { price, size }
342    }
343
344    /// Convert to external BookLevel for API responses
345    /// This is only called at the edges when we need to return data to users
346    pub fn to_book_level(self) -> BookLevel {
347        BookLevel {
348            price: price_to_decimal(self.price),
349            size: qty_to_decimal(self.size),
350        }
351    }
352
353    /// Create from external BookLevel (with validation)
354    /// This is called when we receive data from the API
355    pub fn from_book_level(level: &BookLevel) -> std::result::Result<Self, &'static str> {
356        let price = decimal_to_price(level.price)?;
357        let size = decimal_to_qty(level.size)?;
358        Ok(Self::new(price, size))
359    }
360
361    /// Calculate notional value (price * size) in fixed-point
362    /// Returns the result scaled appropriately to avoid overflow
363    ///
364    /// This is much faster than the Decimal equivalent:
365    /// - Decimal: price.mul(size) -> ~20ns + allocation
366    /// - Fixed-point: (price as i64 * size) / SCALE_FACTOR -> ~1ns, no allocation
367    pub fn notional(self) -> i64 {
368        // Convert price to i64 to avoid overflow in multiplication
369        let price_i64 = self.price as i64;
370        // Multiply and scale back down (we scaled both price and size up by SCALE_FACTOR)
371        (price_i64 * self.size) / SCALE_FACTOR
372    }
373}
374
375/// Full order book state
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct OrderBook {
378    /// Token ID
379    pub token_id: String,
380    /// Timestamp
381    pub timestamp: DateTime<Utc>,
382    /// Bid orders
383    pub bids: Vec<BookLevel>,
384    /// Ask orders
385    pub asks: Vec<BookLevel>,
386    /// Sequence number
387    pub sequence: u64,
388}
389
390/// Order book delta for streaming updates - EXTERNAL API VERSION
391///
392/// This is what we receive from WebSocket streams and REST API calls.
393/// It uses Decimal for compatibility with external systems.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct OrderDelta {
396    pub token_id: String,
397    pub timestamp: DateTime<Utc>,
398    pub side: Side,
399    pub price: Decimal,
400    pub size: Decimal, // 0 means remove level
401    pub sequence: u64,
402}
403
404/// Order book delta for streaming updates - INTERNAL HOT PATH VERSION
405///
406/// This is what we use internally for processing order book updates.
407/// Converting to this format on ingress gives us massive performance gains.
408///
409/// Why the performance matters:
410/// - We might process 10,000+ deltas per second in active markets
411/// - Each delta triggers multiple calculations (spread, impact, etc.)
412/// - Using integers instead of Decimal can make the difference between
413///   keeping up with the market feed vs falling behind
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub struct FastOrderDelta {
416    pub token_id_hash: u64, // Hash of token_id for fast lookup (avoids string comparisons)
417    pub timestamp: DateTime<Utc>,
418    pub side: Side,
419    pub price: Price, // Price in ticks
420    pub size: Qty,    // Size in fixed-point units (0 means remove level)
421    pub sequence: u64,
422}
423
424impl FastOrderDelta {
425    /// Create from external OrderDelta with validation and tick alignment
426    ///
427    /// This is where we enforce tick alignment - if the incoming price
428    /// doesn't align to valid ticks, we either reject it or round it.
429    /// This prevents bad data from corrupting our order book.
430    pub fn from_order_delta(
431        delta: &OrderDelta,
432        tick_size: Option<Decimal>,
433    ) -> std::result::Result<Self, &'static str> {
434        // Validate tick alignment if we have a tick size
435        if let Some(tick_size) = tick_size {
436            if !is_price_tick_aligned(delta.price, tick_size) {
437                return Err("Price not aligned to tick size");
438            }
439        }
440
441        // Convert to fixed-point with validation
442        let price = decimal_to_price(delta.price)?;
443        let size = decimal_to_qty(delta.size)?;
444
445        // Hash the token_id for fast lookups
446        // This avoids string comparisons in the hot path
447        let token_id_hash = {
448            use std::collections::hash_map::DefaultHasher;
449            use std::hash::{Hash, Hasher};
450            let mut hasher = DefaultHasher::new();
451            delta.token_id.hash(&mut hasher);
452            hasher.finish()
453        };
454
455        Ok(Self {
456            token_id_hash,
457            timestamp: delta.timestamp,
458            side: delta.side,
459            price,
460            size,
461            sequence: delta.sequence,
462        })
463    }
464
465    /// Convert back to external OrderDelta (for API responses)
466    /// We need the original token_id since we only store the hash
467    pub fn to_order_delta(self, token_id: String) -> OrderDelta {
468        OrderDelta {
469            token_id,
470            timestamp: self.timestamp,
471            side: self.side,
472            price: price_to_decimal(self.price),
473            size: qty_to_decimal(self.size),
474            sequence: self.sequence,
475        }
476    }
477
478    /// Check if this delta removes a level (size is zero)
479    pub fn is_removal(self) -> bool {
480        self.size == 0
481    }
482}
483
484/// Trade execution event
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct FillEvent {
487    pub id: String,
488    pub order_id: String,
489    pub token_id: String,
490    pub side: Side,
491    pub price: Decimal,
492    pub size: Decimal,
493    pub timestamp: DateTime<Utc>,
494    pub maker_address: Address,
495    pub taker_address: Address,
496    pub fee: Decimal,
497}
498
499/// Order creation parameters
500#[derive(Debug, Clone)]
501pub struct OrderRequest {
502    pub token_id: String,
503    pub side: Side,
504    pub price: Decimal,
505    pub size: Decimal,
506    pub order_type: OrderType,
507    pub expiration: Option<DateTime<Utc>>,
508    pub client_id: Option<String>,
509}
510
511/// Market order parameters
512#[derive(Debug, Clone)]
513pub struct MarketOrderRequest {
514    pub token_id: String,
515    pub side: Side,
516    pub amount: Decimal, // USD amount for buys, token amount for sells
517    pub slippage_tolerance: Option<Decimal>,
518    pub client_id: Option<String>,
519}
520
521/// Order state in the system
522#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct Order {
524    pub id: String,
525    pub token_id: String,
526    pub side: Side,
527    pub price: Decimal,
528    pub original_size: Decimal,
529    pub filled_size: Decimal,
530    pub remaining_size: Decimal,
531    pub status: OrderStatus,
532    pub order_type: OrderType,
533    pub created_at: DateTime<Utc>,
534    pub updated_at: DateTime<Utc>,
535    pub expiration: Option<DateTime<Utc>>,
536    pub client_id: Option<String>,
537}
538
539/// API credentials for authentication
540#[derive(Debug, Clone, Serialize, Deserialize, Default)]
541pub struct ApiCredentials {
542    #[serde(rename = "apiKey")]
543    pub api_key: String,
544    pub secret: String,
545    pub passphrase: String,
546}
547
548/// Configuration for order creation
549#[derive(Debug, Clone)]
550pub struct OrderOptions {
551    pub tick_size: Option<Decimal>,
552    pub neg_risk: Option<bool>,
553    pub fee_rate_bps: Option<u32>,
554}
555
556/// Extra arguments for order creation (V2).
557#[derive(Debug, Clone)]
558pub struct ExtraOrderArgs {
559    /// Optional 32-byte metadata attached to the signed order.
560    pub metadata: B256,
561    /// Optional 32-byte builder code for revenue attribution.
562    pub builder: B256,
563}
564
565impl Default for ExtraOrderArgs {
566    fn default() -> Self {
567        Self {
568            metadata: B256::ZERO,
569            builder: B256::ZERO,
570        }
571    }
572}
573
574/// V1-order extras (for RFQ accept/approve only).
575#[derive(Debug, Clone)]
576pub struct ExtraOrderArgsV1 {
577    pub fee_rate_bps: u32,
578    pub nonce: U256,
579    pub taker: String,
580}
581
582impl Default for ExtraOrderArgsV1 {
583    fn default() -> Self {
584        Self {
585            fee_rate_bps: 0,
586            nonce: U256::ZERO,
587            taker: "0x0000000000000000000000000000000000000000".to_string(),
588        }
589    }
590}
591
592/// Market order arguments
593#[derive(Debug, Clone)]
594pub struct MarketOrderArgs {
595    pub token_id: String,
596    pub side: Side,
597    /// Quote amount for buys, base token amount for sells.
598    pub amount: Decimal,
599}
600
601/// Signed order request ready for submission (V2).
602#[derive(Debug, Clone, Serialize, Deserialize)]
603#[serde(rename_all = "camelCase")]
604pub struct SignedOrderRequest {
605    pub salt: u64,
606    pub maker: String,
607    pub signer: String,
608    /// Taker address (always zero-address in V2 — not part of EIP-712 hash).
609    pub taker: String,
610    pub token_id: String,
611    pub maker_amount: String,
612    pub taker_amount: String,
613    pub side: String,
614    pub signature_type: u8,
615    /// Unix ms timestamp (V2).
616    pub timestamp: String,
617    /// Expiration Unix seconds (0 = no expiration; not part of EIP-712 hash).
618    pub expiration: String,
619    /// 32-byte metadata hex string (V2).
620    pub metadata: String,
621    /// 32-byte builder code hex string (V2).
622    pub builder: String,
623    pub signature: String,
624}
625
626/// Post order wrapper
627#[derive(Debug, Serialize)]
628#[serde(rename_all = "camelCase")]
629pub struct PostOrder {
630    pub order: SignedOrderRequest,
631    pub owner: String,
632    pub order_type: OrderType,
633    /// Defer execution until maker orders fill (V2). Defaults to false.
634    pub defer_exec: bool,
635    /// Post-only order: rejects if would cross spread (V2). Defaults to false.
636    pub post_only: bool,
637}
638
639impl PostOrder {
640    pub fn new(order: SignedOrderRequest, owner: String, order_type: OrderType) -> Self {
641        Self {
642            order,
643            owner,
644            order_type,
645            defer_exec: false,
646            post_only: false,
647        }
648    }
649
650    #[must_use]
651    pub fn with_flags(mut self, post_only: bool, defer_exec: bool) -> Self {
652        self.post_only = post_only;
653        self.defer_exec = defer_exec;
654        self
655    }
656}
657
658/// Market information
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct Market {
661    pub condition_id: String,
662    pub tokens: [Token; 2],
663    pub rewards: Rewards,
664    pub min_incentive_size: Option<String>,
665    pub max_incentive_spread: Option<String>,
666    pub active: bool,
667    pub closed: bool,
668    pub question_id: String,
669    pub minimum_order_size: Decimal,
670    pub minimum_tick_size: Decimal,
671    pub description: String,
672    pub category: Option<String>,
673    pub end_date_iso: Option<String>,
674    pub game_start_time: Option<String>,
675    pub question: String,
676    pub market_slug: String,
677    pub seconds_delay: Decimal,
678    pub icon: String,
679    pub fpmm: String,
680    // Additional fields from API
681    #[serde(default)]
682    pub enable_order_book: bool,
683    #[serde(default)]
684    pub archived: bool,
685    #[serde(default)]
686    pub accepting_orders: bool,
687    #[serde(default)]
688    pub accepting_order_timestamp: Option<String>,
689    #[serde(default)]
690    pub maker_base_fee: Decimal,
691    #[serde(default)]
692    pub taker_base_fee: Decimal,
693    #[serde(default)]
694    pub notifications_enabled: bool,
695    #[serde(default)]
696    pub neg_risk: bool,
697    #[serde(default)]
698    pub neg_risk_market_id: String,
699    #[serde(default)]
700    pub neg_risk_request_id: String,
701    #[serde(default)]
702    pub image: String,
703    #[serde(default)]
704    pub is_50_50_outcome: bool,
705}
706
707/// Token information within a market
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct Token {
710    pub token_id: String,
711    pub outcome: String,
712    pub price: Decimal,
713    #[serde(default)]
714    pub winner: bool,
715}
716
717/// Client configuration for PolyfillClient
718#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct ClientConfig {
720    /// Base URL for the API
721    pub base_url: String,
722    /// Chain ID for the network
723    pub chain_id: u64,
724    /// Private key for signing (optional)
725    pub private_key: Option<String>,
726    /// API credentials (optional)
727    pub api_credentials: Option<ApiCredentials>,
728    /// Maximum slippage tolerance
729    pub max_slippage: Option<Decimal>,
730    /// Fee rate in basis points
731    pub fee_rate: Option<Decimal>,
732    /// Request timeout
733    pub timeout: Option<std::time::Duration>,
734    /// Maximum number of connections
735    pub max_connections: Option<usize>,
736}
737
738impl Default for ClientConfig {
739    fn default() -> Self {
740        Self {
741            base_url: "https://clob.polymarket.com".to_string(),
742            chain_id: 137, // Polygon mainnet
743            private_key: None,
744            api_credentials: None,
745            timeout: Some(std::time::Duration::from_secs(30)),
746            max_connections: Some(100),
747            max_slippage: None,
748            fee_rate: None,
749        }
750    }
751}
752
753/// WebSocket authentication for Polymarket API user channel.
754///
755/// Polymarket's CLOB WebSocket expects the same L2 API credentials used for HTTP calls:
756/// `{ apiKey, secret, passphrase }`.
757pub type WssAuth = ApiCredentials;
758
759/// WebSocket subscription request
760#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct WssSubscription {
762    /// Channel type: "market" or "user"
763    #[serde(rename = "type")]
764    pub channel_type: String,
765    /// Operation type: "subscribe" or "unsubscribe"
766    #[serde(skip_serializing_if = "Option::is_none")]
767    pub operation: Option<String>,
768    /// Array of markets (condition IDs) for USER channel
769    #[serde(default)]
770    pub markets: Vec<String>,
771    /// Array of asset IDs (token IDs) for MARKET channel
772    /// Note: Field name is "assets_ids" (with 's') per Polymarket API spec
773    #[serde(rename = "assets_ids", default)]
774    pub asset_ids: Vec<String>,
775    /// Request initial state dump
776    #[serde(skip_serializing_if = "Option::is_none")]
777    pub initial_dump: Option<bool>,
778    /// Enable custom features (best_bid_ask, new_market, market_resolved)
779    #[serde(skip_serializing_if = "Option::is_none")]
780    pub custom_feature_enabled: Option<bool>,
781    /// Authentication information (only for USER channel)
782    #[serde(skip_serializing_if = "Option::is_none")]
783    pub auth: Option<WssAuth>,
784}
785
786/// WebSocket message types for streaming (official Polymarket `event_type` format).
787#[derive(Debug, Clone, Serialize, Deserialize)]
788#[serde(tag = "event_type")]
789pub enum StreamMessage {
790    /// Full or incremental orderbook update
791    #[serde(rename = "book")]
792    Book(BookUpdate),
793    /// Price change notification (single or batched)
794    #[serde(rename = "price_change")]
795    PriceChange(PriceChange),
796    /// Tick size change notification
797    #[serde(rename = "tick_size_change")]
798    TickSizeChange(TickSizeChange),
799    /// Last trade price update
800    #[serde(rename = "last_trade_price")]
801    LastTradePrice(LastTradePrice),
802    /// Best bid/ask update (requires `custom_feature_enabled`)
803    #[serde(rename = "best_bid_ask")]
804    BestBidAsk(BestBidAsk),
805    /// New market created (requires `custom_feature_enabled`)
806    #[serde(rename = "new_market")]
807    NewMarket(NewMarket),
808    /// Market resolved (requires `custom_feature_enabled`)
809    #[serde(rename = "market_resolved")]
810    MarketResolved(MarketResolved),
811    /// User trade execution (authenticated channel)
812    #[serde(rename = "trade")]
813    Trade(TradeMessage),
814    /// User order update (authenticated channel)
815    #[serde(rename = "order")]
816    Order(OrderMessage),
817    /// Forward-compatible catch-all for new/unknown event types.
818    #[serde(other)]
819    Unknown,
820}
821
822/// Orderbook update message (full snapshot or delta).
823#[derive(Debug, Clone, Serialize, Deserialize)]
824pub struct BookUpdate {
825    pub asset_id: String,
826    pub market: String,
827    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
828    pub timestamp: u64,
829    #[serde(
830        default,
831        deserialize_with = "crate::decode::deserializers::vec_from_null"
832    )]
833    pub bids: Vec<OrderSummary>,
834    #[serde(
835        default,
836        deserialize_with = "crate::decode::deserializers::vec_from_null"
837    )]
838    pub asks: Vec<OrderSummary>,
839    #[serde(default)]
840    pub hash: Option<String>,
841}
842
843/// Unified wire format for `price_change` events.
844#[derive(Debug, Clone, Serialize, Deserialize)]
845pub struct PriceChange {
846    pub market: String,
847    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
848    pub timestamp: u64,
849    #[serde(
850        default,
851        deserialize_with = "crate::decode::deserializers::vec_from_null"
852    )]
853    pub price_changes: Vec<PriceChangeEntry>,
854}
855
856#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct PriceChangeEntry {
858    pub asset_id: String,
859    pub price: Decimal,
860    #[serde(
861        default,
862        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
863    )]
864    pub size: Option<Decimal>,
865    pub side: Side,
866    #[serde(default)]
867    pub hash: Option<String>,
868    #[serde(
869        default,
870        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
871    )]
872    pub best_bid: Option<Decimal>,
873    #[serde(
874        default,
875        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
876    )]
877    pub best_ask: Option<Decimal>,
878}
879
880/// Tick size change event.
881#[derive(Debug, Clone, Serialize, Deserialize)]
882pub struct TickSizeChange {
883    pub asset_id: String,
884    pub market: String,
885    pub old_tick_size: Decimal,
886    pub new_tick_size: Decimal,
887    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
888    pub timestamp: u64,
889}
890
891/// Last trade price update.
892#[derive(Debug, Clone, Serialize, Deserialize)]
893pub struct LastTradePrice {
894    pub asset_id: String,
895    pub market: String,
896    pub price: Decimal,
897    #[serde(default)]
898    pub side: Option<Side>,
899    #[serde(
900        default,
901        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
902    )]
903    pub size: Option<Decimal>,
904    #[serde(
905        default,
906        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
907    )]
908    pub fee_rate_bps: Option<Decimal>,
909    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
910    pub timestamp: u64,
911}
912
913/// Best bid/ask update.
914#[derive(Debug, Clone, Serialize, Deserialize)]
915pub struct BestBidAsk {
916    pub market: String,
917    pub asset_id: String,
918    pub best_bid: Decimal,
919    pub best_ask: Decimal,
920    pub spread: Decimal,
921    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
922    pub timestamp: u64,
923}
924
925/// New market created event.
926#[derive(Debug, Clone, Serialize, Deserialize)]
927pub struct NewMarket {
928    pub id: String,
929    pub question: String,
930    pub market: String,
931    pub slug: String,
932    pub description: String,
933    #[serde(rename = "assets_ids", alias = "asset_ids")]
934    pub asset_ids: Vec<String>,
935    #[serde(
936        default,
937        deserialize_with = "crate::decode::deserializers::vec_from_null"
938    )]
939    pub outcomes: Vec<String>,
940    #[serde(default)]
941    pub event_message: Option<EventMessage>,
942    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
943    pub timestamp: u64,
944}
945
946/// Market resolved event.
947#[derive(Debug, Clone, Serialize, Deserialize)]
948pub struct MarketResolved {
949    pub id: String,
950    #[serde(default)]
951    pub question: Option<String>,
952    pub market: String,
953    #[serde(default)]
954    pub slug: Option<String>,
955    #[serde(default)]
956    pub description: Option<String>,
957    #[serde(rename = "assets_ids", alias = "asset_ids")]
958    pub asset_ids: Vec<String>,
959    #[serde(
960        default,
961        deserialize_with = "crate::decode::deserializers::vec_from_null"
962    )]
963    pub outcomes: Vec<String>,
964    pub winning_asset_id: String,
965    pub winning_outcome: String,
966    #[serde(default)]
967    pub event_message: Option<EventMessage>,
968    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
969    pub timestamp: u64,
970}
971
972/// Event message object for market events.
973#[derive(Debug, Clone, Serialize, Deserialize)]
974pub struct EventMessage {
975    pub id: String,
976    pub ticker: String,
977    pub slug: String,
978    pub title: String,
979    pub description: String,
980}
981
982/// User trade execution message (authenticated WebSocket channel).
983#[derive(Debug, Clone, Serialize, Deserialize)]
984pub struct TradeMessage {
985    pub id: String,
986    pub market: String,
987    pub asset_id: String,
988    pub side: Side,
989    pub size: Decimal,
990    pub price: Decimal,
991    /// Trade lifecycle status (Matched → Mined → Confirmed).
992    #[serde(default)]
993    pub status: TradeMessageStatus,
994    #[serde(rename = "type", default)]
995    pub msg_type: Option<TradeMessageType>,
996    #[serde(
997        default,
998        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
999    )]
1000    pub last_update: Option<u64>,
1001    #[serde(
1002        default,
1003        alias = "match_time",
1004        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1005    )]
1006    pub matchtime: Option<u64>,
1007    #[serde(
1008        default,
1009        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1010    )]
1011    pub timestamp: Option<u64>,
1012    /// Outcome (e.g. "Yes" / "No").
1013    #[serde(default)]
1014    pub outcome: Option<String>,
1015    /// API key of the event owner.
1016    #[serde(default)]
1017    pub owner: Option<String>,
1018    /// API key of the trade owner.
1019    #[serde(default)]
1020    pub trade_owner: Option<String>,
1021    /// Taker order ID.
1022    #[serde(default)]
1023    pub taker_order_id: Option<String>,
1024    /// Maker order details.
1025    #[serde(
1026        default,
1027        deserialize_with = "crate::decode::deserializers::vec_from_null"
1028    )]
1029    pub maker_orders: Vec<MakerOrder>,
1030    /// Fee rate in basis points.
1031    #[serde(
1032        default,
1033        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1034    )]
1035    pub fee_rate_bps: Option<Decimal>,
1036    /// On-chain transaction hash.
1037    #[serde(default)]
1038    pub transaction_hash: Option<String>,
1039    /// Whether user was maker or taker.
1040    #[serde(default)]
1041    pub trader_side: Option<TraderSide>,
1042    /// Batch-trade bucket index (AsyncAPI v2).
1043    #[serde(default)]
1044    pub bucket_index: Option<u32>,
1045}
1046
1047/// User order update message.
1048#[derive(Debug, Clone, Serialize, Deserialize)]
1049pub struct OrderMessage {
1050    pub id: String,
1051    pub market: String,
1052    pub asset_id: String,
1053    pub side: Side,
1054    pub price: Decimal,
1055    /// Event type: PLACEMENT, UPDATE, CANCELLATION.
1056    #[serde(rename = "type", default)]
1057    pub msg_type: Option<String>,
1058    #[serde(
1059        default,
1060        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1061    )]
1062    pub original_size: Option<Decimal>,
1063    #[serde(
1064        default,
1065        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1066    )]
1067    pub size_matched: Option<Decimal>,
1068    #[serde(
1069        default,
1070        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1071    )]
1072    pub timestamp: Option<u64>,
1073    #[serde(default)]
1074    pub associate_trades: Option<Vec<String>>,
1075    #[serde(default)]
1076    pub status: Option<String>,
1077
1078    // ── added to match AsyncAPI v2 user channel OrderEvent ─────────────────
1079    /// API key of the order owner (required per AsyncAPI, but tolerate absence).
1080    #[serde(default)]
1081    pub owner: Option<String>,
1082    /// Alias spelling sometimes emitted by server.
1083    #[serde(default)]
1084    pub order_owner: Option<String>,
1085    /// Outcome label (e.g. "Yes" / "No").
1086    #[serde(default)]
1087    pub outcome: Option<String>,
1088    /// Server-side creation timestamp (string).
1089    #[serde(default)]
1090    pub created_at: Option<String>,
1091    /// Expiration unix seconds (string).
1092    #[serde(default)]
1093    pub expiration: Option<String>,
1094    /// Order execution type — GTC / GTD / FOK / FAK.
1095    #[serde(default)]
1096    pub order_type: Option<String>,
1097    /// Maker address for the order.
1098    #[serde(default)]
1099    pub maker_address: Option<String>,
1100}
1101
1102/// Subscription parameters for streaming
1103#[derive(Debug, Clone, Serialize, Deserialize)]
1104pub struct Subscription {
1105    pub token_ids: Vec<String>,
1106    pub channels: Vec<String>,
1107}
1108
1109/// WebSocket channel types
1110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1111pub enum WssChannelType {
1112    #[serde(rename = "USER")]
1113    User,
1114    #[serde(rename = "MARKET")]
1115    Market,
1116}
1117
1118impl WssChannelType {
1119    pub fn as_str(&self) -> &'static str {
1120        match self {
1121            WssChannelType::User => "USER",
1122            WssChannelType::Market => "MARKET",
1123        }
1124    }
1125}
1126
1127/// Price quote response
1128#[derive(Debug, Clone, Serialize, Deserialize)]
1129pub struct Quote {
1130    pub token_id: String,
1131    pub side: Side,
1132    #[serde(with = "rust_decimal::serde::str")]
1133    pub price: Decimal,
1134    pub timestamp: DateTime<Utc>,
1135}
1136
1137/// Balance information
1138#[derive(Debug, Clone, Serialize, Deserialize)]
1139pub struct Balance {
1140    pub token_id: String,
1141    pub available: Decimal,
1142    pub locked: Decimal,
1143    pub total: Decimal,
1144}
1145
1146/// Performance metrics for monitoring
1147#[derive(Debug, Clone)]
1148pub struct Metrics {
1149    pub orders_per_second: f64,
1150    pub avg_latency_ms: f64,
1151    pub error_rate: f64,
1152    pub uptime_pct: f64,
1153}
1154
1155// Type aliases for common patterns
1156pub type TokenId = String;
1157pub type OrderId = String;
1158pub type MarketId = String;
1159pub type ClientId = String;
1160
1161/// Parameters for querying open orders
1162#[derive(Debug, Clone)]
1163pub struct OpenOrderParams {
1164    pub id: Option<String>,
1165    pub asset_id: Option<String>,
1166    pub market: Option<String>,
1167}
1168
1169impl OpenOrderParams {
1170    pub fn to_query_params(&self) -> Vec<(&str, &String)> {
1171        let mut params = Vec::with_capacity(3);
1172
1173        if let Some(x) = &self.id {
1174            params.push(("id", x));
1175        }
1176
1177        if let Some(x) = &self.asset_id {
1178            params.push(("asset_id", x));
1179        }
1180
1181        if let Some(x) = &self.market {
1182            params.push(("market", x));
1183        }
1184        params
1185    }
1186}
1187
1188/// Parameters for querying trades
1189#[derive(Debug, Clone)]
1190pub struct TradeParams {
1191    pub id: Option<String>,
1192    pub maker_address: Option<String>,
1193    pub market: Option<String>,
1194    pub asset_id: Option<String>,
1195    pub before: Option<u64>,
1196    pub after: Option<u64>,
1197}
1198
1199impl TradeParams {
1200    pub fn to_query_params(&self) -> Vec<(&str, String)> {
1201        let mut params = Vec::with_capacity(6);
1202
1203        if let Some(x) = &self.id {
1204            params.push(("id", x.clone()));
1205        }
1206
1207        if let Some(x) = &self.asset_id {
1208            params.push(("asset_id", x.clone()));
1209        }
1210
1211        if let Some(x) = &self.market {
1212            params.push(("market", x.clone()));
1213        }
1214
1215        if let Some(x) = &self.maker_address {
1216            params.push(("maker_address", x.clone()));
1217        }
1218
1219        if let Some(x) = &self.before {
1220            params.push(("before", x.to_string()));
1221        }
1222
1223        if let Some(x) = &self.after {
1224            params.push(("after", x.to_string()));
1225        }
1226
1227        params
1228    }
1229}
1230
1231/// Open order information
1232#[derive(Debug, Clone, Serialize, Deserialize)]
1233pub struct OpenOrder {
1234    pub associate_trades: Vec<String>,
1235    pub id: String,
1236    pub status: String,
1237    pub market: String,
1238    #[serde(with = "rust_decimal::serde::str")]
1239    pub original_size: Decimal,
1240    pub outcome: String,
1241    pub maker_address: String,
1242    pub owner: String,
1243    #[serde(with = "rust_decimal::serde::str")]
1244    pub price: Decimal,
1245    pub side: Side,
1246    #[serde(with = "rust_decimal::serde::str")]
1247    pub size_matched: Decimal,
1248    pub asset_id: String,
1249    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1250    pub expiration: u64,
1251    #[serde(rename = "type", alias = "order_type", alias = "orderType", default)]
1252    pub order_type: OrderType,
1253    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1254    pub created_at: u64,
1255}
1256
1257/// Response from posting a single order.
1258#[derive(Debug, Clone, Serialize, Deserialize)]
1259#[serde(rename_all = "camelCase")]
1260pub struct PostOrderResponse {
1261    #[serde(default)]
1262    pub error_msg: Option<String>,
1263    #[serde(
1264        default,
1265        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error"
1266    )]
1267    pub making_amount: Option<Decimal>,
1268    #[serde(
1269        default,
1270        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error"
1271    )]
1272    pub taking_amount: Option<Decimal>,
1273    #[serde(rename = "orderID")]
1274    pub order_id: String,
1275    #[serde(default)]
1276    pub status: Option<String>,
1277    pub success: bool,
1278    #[serde(default, alias = "transactionsHashes")]
1279    pub transaction_hashes: Vec<String>,
1280    #[serde(default)]
1281    pub trade_ids: Vec<String>,
1282}
1283
1284/// Response from cancel endpoints.
1285#[derive(Debug, Clone, Serialize, Deserialize)]
1286#[serde(rename_all = "camelCase")]
1287pub struct CancelOrdersResponse {
1288    #[serde(
1289        default,
1290        deserialize_with = "crate::decode::deserializers::vec_from_null"
1291    )]
1292    pub canceled: Vec<String>,
1293    #[serde(default, alias = "not_canceled")]
1294    pub not_canceled: std::collections::HashMap<String, String>,
1295}
1296
1297/// Trade item returned by `/data/trades`.
1298#[derive(Debug, Clone, Serialize, Deserialize)]
1299#[serde(rename_all = "snake_case")]
1300pub struct TradeResponse {
1301    pub id: String,
1302    #[serde(default)]
1303    pub taker_order_id: Option<String>,
1304    pub market: String,
1305    pub asset_id: String,
1306    pub side: Side,
1307    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1308    pub size: Decimal,
1309    #[serde(
1310        default,
1311        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1312    )]
1313    pub fee_rate_bps: Option<Decimal>,
1314    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1315    pub price: Decimal,
1316    #[serde(default)]
1317    pub status: Option<String>,
1318    #[serde(
1319        default,
1320        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1321    )]
1322    pub match_time: Option<u64>,
1323    #[serde(
1324        default,
1325        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1326    )]
1327    pub last_update: Option<u64>,
1328    #[serde(default)]
1329    pub outcome: Option<String>,
1330    #[serde(
1331        default,
1332        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
1333    )]
1334    pub bucket_index: Option<u64>,
1335    #[serde(default)]
1336    pub owner: Option<String>,
1337    #[serde(default)]
1338    pub maker_address: Option<String>,
1339    #[serde(
1340        default,
1341        deserialize_with = "crate::decode::deserializers::vec_from_null"
1342    )]
1343    pub maker_orders: Vec<MakerOrder>,
1344    #[serde(default)]
1345    pub transaction_hash: Option<String>,
1346    #[serde(default)]
1347    pub trader_side: TraderSide,
1348    #[serde(default, alias = "err_msg")]
1349    pub error_msg: Option<String>,
1350}
1351
1352#[derive(Debug, Clone, Serialize, Deserialize)]
1353pub struct MakerOrder {
1354    pub order_id: String,
1355    #[serde(default)]
1356    pub owner: Option<String>,
1357    #[serde(default)]
1358    pub maker_address: Option<String>,
1359    #[serde(
1360        default,
1361        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1362    )]
1363    pub matched_amount: Option<Decimal>,
1364    #[serde(
1365        default,
1366        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1367    )]
1368    pub price: Option<Decimal>,
1369    #[serde(
1370        default,
1371        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
1372    )]
1373    pub fee_rate_bps: Option<Decimal>,
1374    #[serde(default)]
1375    pub asset_id: Option<String>,
1376    #[serde(default)]
1377    pub outcome: Option<String>,
1378    #[serde(default)]
1379    pub side: Option<Side>,
1380}
1381
1382/// Balance allowance information
1383#[derive(Debug, Clone, Serialize, Deserialize)]
1384pub struct BalanceAllowance {
1385    pub asset_id: String,
1386    #[serde(with = "rust_decimal::serde::str")]
1387    pub balance: Decimal,
1388    #[serde(with = "rust_decimal::serde::str")]
1389    pub allowance: Decimal,
1390}
1391
1392/// Parameters for balance allowance queries (from reference implementation)
1393#[derive(Default)]
1394pub struct BalanceAllowanceParams {
1395    pub asset_type: Option<AssetType>,
1396    pub token_id: Option<String>,
1397    pub signature_type: Option<u8>,
1398}
1399
1400impl BalanceAllowanceParams {
1401    pub fn to_query_params(&self) -> Vec<(&str, String)> {
1402        let mut params = Vec::with_capacity(3);
1403
1404        if let Some(x) = &self.asset_type {
1405            params.push(("asset_type", x.to_string()));
1406        }
1407
1408        if let Some(x) = &self.token_id {
1409            params.push(("token_id", x.to_string()));
1410        }
1411
1412        if let Some(x) = &self.signature_type {
1413            params.push(("signature_type", x.to_string()));
1414        }
1415        params
1416    }
1417
1418    pub fn set_signature_type(&mut self, s: u8) {
1419        self.signature_type = Some(s);
1420    }
1421}
1422
1423/// Asset type enum for balance allowance queries
1424#[allow(clippy::upper_case_acronyms)]
1425pub enum AssetType {
1426    COLLATERAL,
1427    CONDITIONAL,
1428}
1429
1430impl std::fmt::Display for AssetType {
1431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1432        match self {
1433            AssetType::COLLATERAL => write!(f, "COLLATERAL"),
1434            AssetType::CONDITIONAL => write!(f, "CONDITIONAL"),
1435        }
1436    }
1437}
1438
1439/// Notification preferences
1440#[derive(Debug, Clone, Serialize, Deserialize)]
1441pub struct NotificationParams {
1442    pub signature: String,
1443    pub timestamp: u64,
1444}
1445
1446/// Batch midpoint request
1447#[derive(Debug, Clone, Serialize, Deserialize)]
1448pub struct BatchMidpointRequest {
1449    pub token_ids: Vec<String>,
1450}
1451
1452/// Batch midpoint response
1453#[derive(Debug, Clone, Serialize, Deserialize)]
1454pub struct BatchMidpointResponse {
1455    pub midpoints: std::collections::HashMap<String, Option<Decimal>>,
1456}
1457
1458/// Batch price request
1459#[derive(Debug, Clone, Serialize, Deserialize)]
1460pub struct BatchPriceRequest {
1461    pub token_ids: Vec<String>,
1462}
1463
1464/// Price information for a token
1465#[derive(Debug, Clone, Serialize, Deserialize)]
1466pub struct TokenPrice {
1467    pub token_id: String,
1468    #[serde(skip_serializing_if = "Option::is_none")]
1469    pub bid: Option<Decimal>,
1470    #[serde(skip_serializing_if = "Option::is_none")]
1471    pub ask: Option<Decimal>,
1472    #[serde(skip_serializing_if = "Option::is_none")]
1473    pub mid: Option<Decimal>,
1474}
1475
1476/// Batch price response
1477#[derive(Debug, Clone, Serialize, Deserialize)]
1478pub struct BatchPriceResponse {
1479    pub prices: Vec<TokenPrice>,
1480}
1481
1482// Additional types for API compatibility with reference implementation
1483#[derive(Debug, Deserialize)]
1484pub struct ApiKeysResponse {
1485    #[serde(rename = "apiKeys")]
1486    pub api_keys: Vec<String>,
1487}
1488
1489#[derive(Debug, Deserialize)]
1490pub struct MidpointResponse {
1491    #[serde(with = "rust_decimal::serde::str")]
1492    pub mid: Decimal,
1493}
1494
1495#[derive(Debug, Deserialize)]
1496pub struct PriceResponse {
1497    #[serde(with = "rust_decimal::serde::str")]
1498    pub price: Decimal,
1499}
1500
1501// ============================================================================
1502// PRICE HISTORY (ANALYTICS)
1503// ============================================================================
1504
1505/// Time bucket for the `/prices-history` endpoint.
1506///
1507/// Note: this endpoint uses a confusing query parameter name (`market`) but expects an
1508/// outcome asset id (`token_id` / `asset_id`) in **decimal string** form.
1509#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1510pub enum PricesHistoryInterval {
1511    OneMinute,
1512    OneHour,
1513    SixHours,
1514    OneDay,
1515    OneWeek,
1516}
1517
1518impl PricesHistoryInterval {
1519    pub const fn as_str(self) -> &'static str {
1520        match self {
1521            Self::OneMinute => "1m",
1522            Self::OneHour => "1h",
1523            Self::SixHours => "6h",
1524            Self::OneDay => "1d",
1525            Self::OneWeek => "1w",
1526        }
1527    }
1528}
1529
1530/// A single price-history datapoint from `/prices-history`.
1531///
1532/// Mirrors the official Polymarket SDK shape (`t`, `p`).
1533#[derive(Debug, Clone, Serialize, Deserialize)]
1534pub struct PriceHistoryPoint {
1535    pub t: i64,
1536    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1537    pub p: Decimal,
1538}
1539
1540/// Response from `/prices-history`.
1541#[derive(Debug, Clone, Serialize, Deserialize)]
1542pub struct PricesHistoryResponse {
1543    pub history: Vec<PriceHistoryPoint>,
1544}
1545
1546#[derive(Debug, Deserialize)]
1547pub struct SpreadResponse {
1548    pub spread: Decimal,
1549}
1550
1551#[derive(Debug, Deserialize)]
1552pub struct TickSizeResponse {
1553    pub minimum_tick_size: Decimal,
1554}
1555
1556#[derive(Debug, Deserialize)]
1557pub struct NegRiskResponse {
1558    pub neg_risk: bool,
1559}
1560
1561#[derive(Debug, Clone, Serialize, Deserialize)]
1562pub struct OrderScoringResponse {
1563    pub scoring: bool,
1564}
1565
1566#[derive(Debug, Serialize, Deserialize)]
1567pub struct BookParams {
1568    pub token_id: String,
1569    pub side: Side,
1570}
1571
1572#[derive(Debug, Deserialize)]
1573pub struct OrderBookSummary {
1574    pub market: String,
1575    pub asset_id: String,
1576    #[serde(default)]
1577    pub hash: Option<String>,
1578    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1579    pub timestamp: u64,
1580    #[serde(
1581        default,
1582        deserialize_with = "crate::decode::deserializers::vec_from_null"
1583    )]
1584    pub bids: Vec<OrderSummary>,
1585    #[serde(
1586        default,
1587        deserialize_with = "crate::decode::deserializers::vec_from_null"
1588    )]
1589    pub asks: Vec<OrderSummary>,
1590    pub min_order_size: Decimal,
1591    pub neg_risk: bool,
1592    pub tick_size: Decimal,
1593    #[serde(
1594        default,
1595        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error"
1596    )]
1597    pub last_trade_price: Option<Decimal>,
1598}
1599
1600#[derive(Debug, Clone, Serialize, Deserialize)]
1601pub struct OrderSummary {
1602    #[serde(with = "rust_decimal::serde::str")]
1603    pub price: Decimal,
1604    #[serde(with = "rust_decimal::serde::str")]
1605    pub size: Decimal,
1606}
1607
1608#[derive(Debug, Serialize, Deserialize)]
1609pub struct MarketsResponse {
1610    pub limit: usize,
1611    pub count: usize,
1612    pub next_cursor: Option<String>,
1613    pub data: Vec<Market>,
1614}
1615
1616#[derive(Debug, Serialize, Deserialize)]
1617pub struct SimplifiedMarketsResponse {
1618    pub limit: usize,
1619    pub count: usize,
1620    pub next_cursor: Option<String>,
1621    pub data: Vec<SimplifiedMarket>,
1622}
1623
1624/// Simplified market structure for batch operations
1625#[derive(Debug, Serialize, Deserialize)]
1626pub struct SimplifiedMarket {
1627    pub condition_id: String,
1628    pub tokens: [Token; 2],
1629    pub rewards: Rewards,
1630    pub min_incentive_size: Option<String>,
1631    pub max_incentive_spread: Option<String>,
1632    pub active: bool,
1633    pub closed: bool,
1634}
1635
1636/// Rewards structure for markets
1637#[derive(Debug, Clone, Serialize, Deserialize)]
1638pub struct Rewards {
1639    pub rates: Option<serde_json::Value>,
1640    // API returns these as plain numbers, not strings
1641    pub min_size: Decimal,
1642    pub max_spread: Decimal,
1643    #[serde(default)]
1644    pub event_start_date: Option<String>,
1645    #[serde(default)]
1646    pub event_end_date: Option<String>,
1647    #[serde(skip_serializing_if = "Option::is_none", default)]
1648    pub in_game_multiplier: Option<Decimal>,
1649    #[serde(skip_serializing_if = "Option::is_none", default)]
1650    pub reward_epoch: Option<Decimal>,
1651}
1652
1653// ============================================================================
1654// CLOB API: Fee Rate + RFQ (Market Maker) Types
1655// ============================================================================
1656
1657/// Fee rate in basis points for a given token.
1658#[derive(Debug, Clone, Serialize, Deserialize)]
1659pub struct FeeRateResponse {
1660    pub base_fee: u32,
1661}
1662
1663/// Create RFQ request (Requester).
1664#[derive(Debug, Clone, Serialize)]
1665#[serde(rename_all = "camelCase")]
1666pub struct RfqCreateRequest {
1667    pub asset_in: String,
1668    pub asset_out: String,
1669    pub amount_in: String,
1670    pub amount_out: String,
1671    pub user_type: u8,
1672}
1673
1674#[derive(Debug, Clone, Serialize, Deserialize)]
1675#[serde(rename_all = "camelCase")]
1676pub struct RfqCreateRequestResponse {
1677    pub request_id: String,
1678    pub expiry: u64,
1679}
1680
1681/// Cancel RFQ request (Requester).
1682#[derive(Debug, Clone, Serialize)]
1683#[serde(rename_all = "camelCase")]
1684pub struct RfqCancelRequest {
1685    pub request_id: String,
1686}
1687
1688/// RFQ request list query parameters.
1689#[derive(Debug, Clone, Default)]
1690pub struct RfqRequestsParams {
1691    pub offset: Option<String>,
1692    pub limit: Option<u32>,
1693    pub state: Option<String>,
1694    pub request_ids: Vec<String>,
1695    pub markets: Vec<String>,
1696    pub size_min: Option<Decimal>,
1697    pub size_max: Option<Decimal>,
1698    pub size_usdc_min: Option<Decimal>,
1699    pub size_usdc_max: Option<Decimal>,
1700    pub price_min: Option<Decimal>,
1701    pub price_max: Option<Decimal>,
1702    pub sort_by: Option<String>,
1703    pub sort_dir: Option<String>,
1704}
1705
1706impl RfqRequestsParams {
1707    pub fn to_query_params(&self) -> Vec<(String, String)> {
1708        let mut params = Vec::new();
1709
1710        if let Some(x) = &self.offset {
1711            params.push(("offset".to_string(), x.clone()));
1712        }
1713        if let Some(x) = self.limit {
1714            params.push(("limit".to_string(), x.to_string()));
1715        }
1716        if let Some(x) = &self.state {
1717            params.push(("state".to_string(), x.clone()));
1718        }
1719        for x in &self.request_ids {
1720            params.push(("requestIds[]".to_string(), x.clone()));
1721        }
1722        for x in &self.markets {
1723            params.push(("markets[]".to_string(), x.clone()));
1724        }
1725
1726        if let Some(x) = self.size_min {
1727            params.push(("sizeMin".to_string(), x.to_string()));
1728        }
1729        if let Some(x) = self.size_max {
1730            params.push(("sizeMax".to_string(), x.to_string()));
1731        }
1732        if let Some(x) = self.size_usdc_min {
1733            params.push(("sizeUsdcMin".to_string(), x.to_string()));
1734        }
1735        if let Some(x) = self.size_usdc_max {
1736            params.push(("sizeUsdcMax".to_string(), x.to_string()));
1737        }
1738        if let Some(x) = self.price_min {
1739            params.push(("priceMin".to_string(), x.to_string()));
1740        }
1741        if let Some(x) = self.price_max {
1742            params.push(("priceMax".to_string(), x.to_string()));
1743        }
1744
1745        if let Some(x) = &self.sort_by {
1746            params.push(("sortBy".to_string(), x.clone()));
1747        }
1748        if let Some(x) = &self.sort_dir {
1749            params.push(("sortDir".to_string(), x.clone()));
1750        }
1751
1752        params
1753    }
1754}
1755
1756/// RFQ request data.
1757#[derive(Debug, Clone, Serialize, Deserialize)]
1758#[serde(rename_all = "camelCase")]
1759pub struct RfqRequestData {
1760    pub request_id: String,
1761    pub user_address: String,
1762    pub proxy_address: String,
1763    pub condition: String,
1764    pub token: String,
1765    pub complement: String,
1766    pub side: Side,
1767    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1768    pub size_in: Decimal,
1769    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1770    pub size_out: Decimal,
1771    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1772    pub price: Decimal,
1773    pub state: String,
1774    pub expiry: u64,
1775}
1776
1777/// Create RFQ quote (Quoter).
1778#[derive(Debug, Clone, Serialize)]
1779#[serde(rename_all = "camelCase")]
1780pub struct RfqCreateQuote {
1781    pub request_id: String,
1782    pub asset_in: String,
1783    pub asset_out: String,
1784    pub amount_in: String,
1785    pub amount_out: String,
1786    pub user_type: u8,
1787}
1788
1789#[derive(Debug, Clone, Serialize, Deserialize)]
1790#[serde(rename_all = "camelCase")]
1791pub struct RfqCreateQuoteResponse {
1792    pub quote_id: String,
1793}
1794
1795/// Cancel RFQ quote (Quoter).
1796#[derive(Debug, Clone, Serialize)]
1797#[serde(rename_all = "camelCase")]
1798pub struct RfqCancelQuote {
1799    pub quote_id: String,
1800}
1801
1802/// RFQ quote list query parameters.
1803#[derive(Debug, Clone, Default)]
1804pub struct RfqQuotesParams {
1805    pub offset: Option<String>,
1806    pub limit: Option<u32>,
1807    pub state: Option<String>,
1808    pub quote_ids: Vec<String>,
1809    pub request_ids: Vec<String>,
1810    pub markets: Vec<String>,
1811    pub size_min: Option<Decimal>,
1812    pub size_max: Option<Decimal>,
1813    pub size_usdc_min: Option<Decimal>,
1814    pub size_usdc_max: Option<Decimal>,
1815    pub price_min: Option<Decimal>,
1816    pub price_max: Option<Decimal>,
1817    pub sort_by: Option<String>,
1818    pub sort_dir: Option<String>,
1819}
1820
1821impl RfqQuotesParams {
1822    pub fn to_query_params(&self) -> Vec<(String, String)> {
1823        let mut params = Vec::new();
1824
1825        if let Some(x) = &self.offset {
1826            params.push(("offset".to_string(), x.clone()));
1827        }
1828        if let Some(x) = self.limit {
1829            params.push(("limit".to_string(), x.to_string()));
1830        }
1831        if let Some(x) = &self.state {
1832            params.push(("state".to_string(), x.clone()));
1833        }
1834        for x in &self.quote_ids {
1835            params.push(("quoteIds[]".to_string(), x.clone()));
1836        }
1837        for x in &self.request_ids {
1838            params.push(("requestIds[]".to_string(), x.clone()));
1839        }
1840        for x in &self.markets {
1841            params.push(("markets[]".to_string(), x.clone()));
1842        }
1843
1844        if let Some(x) = self.size_min {
1845            params.push(("sizeMin".to_string(), x.to_string()));
1846        }
1847        if let Some(x) = self.size_max {
1848            params.push(("sizeMax".to_string(), x.to_string()));
1849        }
1850        if let Some(x) = self.size_usdc_min {
1851            params.push(("sizeUsdcMin".to_string(), x.to_string()));
1852        }
1853        if let Some(x) = self.size_usdc_max {
1854            params.push(("sizeUsdcMax".to_string(), x.to_string()));
1855        }
1856        if let Some(x) = self.price_min {
1857            params.push(("priceMin".to_string(), x.to_string()));
1858        }
1859        if let Some(x) = self.price_max {
1860            params.push(("priceMax".to_string(), x.to_string()));
1861        }
1862
1863        if let Some(x) = &self.sort_by {
1864            params.push(("sortBy".to_string(), x.clone()));
1865        }
1866        if let Some(x) = &self.sort_dir {
1867            params.push(("sortDir".to_string(), x.clone()));
1868        }
1869
1870        params
1871    }
1872}
1873
1874/// RFQ quote data.
1875#[derive(Debug, Clone, Serialize, Deserialize)]
1876#[serde(rename_all = "camelCase")]
1877pub struct RfqQuoteData {
1878    pub quote_id: String,
1879    pub request_id: String,
1880    pub user_address: String,
1881    pub proxy_address: String,
1882    pub condition: String,
1883    pub token: String,
1884    pub complement: String,
1885    pub side: Side,
1886    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1887    pub size_in: Decimal,
1888    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1889    pub size_out: Decimal,
1890    #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
1891    pub price: Decimal,
1892    pub match_type: String,
1893    pub state: String,
1894}
1895
1896/// Generic RFQ list response wrapper.
1897#[derive(Debug, Clone, Serialize, Deserialize)]
1898pub struct RfqListResponse<T> {
1899    pub data: Vec<T>,
1900    pub next_cursor: Option<String>,
1901    pub limit: u32,
1902    pub count: u32,
1903}
1904
1905/// RFQ order execution request (used for both accept + approve).
1906#[derive(Debug, Clone, Serialize)]
1907#[serde(rename_all = "camelCase")]
1908pub struct RfqOrderExecutionRequest {
1909    pub request_id: String,
1910    pub quote_id: String,
1911    pub maker: String,
1912    pub signer: String,
1913    pub taker: String,
1914    pub expiration: u64,
1915    pub nonce: String,
1916    pub fee_rate_bps: String,
1917    pub side: String,
1918    pub token_id: String,
1919    pub maker_amount: String,
1920    pub taker_amount: String,
1921    pub signature_type: u8,
1922    pub signature: String,
1923    pub salt: u64,
1924    pub owner: String,
1925}
1926
1927#[derive(Debug, Clone, Serialize, Deserialize)]
1928#[serde(rename_all = "camelCase")]
1929pub struct RfqApproveOrderResponse {
1930    pub trade_ids: Vec<String>,
1931}
1932
1933// For compatibility with reference implementation
1934pub type ClientResult<T> = anyhow::Result<T>;
1935
1936/// Result type used throughout the client
1937pub type Result<T> = std::result::Result<T, crate::errors::PolyfillError>;
1938
1939// Type aliases for 100% compatibility with baseline implementation
1940pub type ApiCreds = ApiCredentials;
1941pub type CreateOrderOptions = OrderOptions;
1942pub type OrderArgs = OrderRequest;
1943
1944#[cfg(test)]
1945mod tests {
1946    use super::{
1947        OrderMessage, OrderType, PostOrder, SignedOrderRequest, TradeMessage, TradeMessageStatus,
1948    };
1949
1950    #[test]
1951    fn test_order_type_fak_serde_and_string() {
1952        assert_eq!(OrderType::FAK.as_str(), "FAK");
1953
1954        let json = serde_json::to_string(&OrderType::FAK).unwrap();
1955        assert_eq!(json, "\"FAK\"");
1956
1957        let parsed: OrderType = serde_json::from_str("\"FAK\"").unwrap();
1958        assert_eq!(parsed, OrderType::FAK);
1959    }
1960
1961    #[test]
1962    fn test_post_order_v2_json_shape() {
1963        let order = SignedOrderRequest {
1964            salt: 42,
1965            maker: "0xAbC0000000000000000000000000000000000001".to_string(),
1966            signer: "0xAbC0000000000000000000000000000000000001".to_string(),
1967            taker: "0x0000000000000000000000000000000000000000".to_string(),
1968            token_id: "123456789".to_string(),
1969            maker_amount: "100000000".to_string(),
1970            taker_amount: "50000000".to_string(),
1971            side: "BUY".to_string(),
1972            signature_type: 0,
1973            timestamp: "1700000000000".to_string(),
1974            expiration: "0".to_string(),
1975            metadata: "0x0000000000000000000000000000000000000000000000000000000000000000"
1976                .to_string(),
1977            builder: "0x0000000000000000000000000000000000000000000000000000000000000000"
1978                .to_string(),
1979            signature: "0xdeadbeef".to_string(),
1980        };
1981        let body = PostOrder::new(order, "owner-uuid".to_string(), OrderType::GTC);
1982        let v = serde_json::to_value(&body).unwrap();
1983
1984        // V2 root fields
1985        let obj = v.as_object().expect("PostOrder must serialize as object");
1986        assert!(obj.contains_key("order"));
1987        assert!(obj.contains_key("owner"));
1988        assert!(obj.contains_key("orderType"));
1989        assert!(obj.contains_key("deferExec"));
1990        assert!(obj.contains_key("postOnly"));
1991        assert_eq!(obj["deferExec"], serde_json::Value::Bool(false));
1992        assert_eq!(obj["postOnly"], serde_json::Value::Bool(false));
1993        assert_eq!(obj["orderType"], serde_json::json!("GTC"));
1994
1995        // V2 order fields: taker + expiration present, nonce + feeRateBps absent
1996        let order = obj["order"].as_object().expect("order must be object");
1997        for key in [
1998            "salt",
1999            "maker",
2000            "signer",
2001            "taker",
2002            "tokenId",
2003            "makerAmount",
2004            "takerAmount",
2005            "side",
2006            "signatureType",
2007            "timestamp",
2008            "expiration",
2009            "metadata",
2010            "builder",
2011            "signature",
2012        ] {
2013            assert!(order.contains_key(key), "missing {key}");
2014        }
2015        assert!(!order.contains_key("nonce"), "V2 must not include nonce");
2016        assert!(
2017            !order.contains_key("feeRateBps"),
2018            "V2 must not include feeRateBps"
2019        );
2020    }
2021
2022    #[test]
2023    fn test_post_order_with_flags_sets_post_only_and_defer_exec() {
2024        let order = SignedOrderRequest {
2025            salt: 1,
2026            maker: "0x0".to_string(),
2027            signer: "0x0".to_string(),
2028            taker: "0x0".to_string(),
2029            token_id: "1".to_string(),
2030            maker_amount: "0".to_string(),
2031            taker_amount: "0".to_string(),
2032            side: "BUY".to_string(),
2033            signature_type: 0,
2034            timestamp: "0".to_string(),
2035            expiration: "0".to_string(),
2036            metadata: "0x0".to_string(),
2037            builder: "0x0".to_string(),
2038            signature: "0x0".to_string(),
2039        };
2040        let body = PostOrder::new(order, "o".to_string(), OrderType::GTC).with_flags(true, true);
2041        assert!(body.post_only);
2042        assert!(body.defer_exec);
2043    }
2044
2045    #[test]
2046    fn test_trade_message_status_retrying_failed() {
2047        // AsyncAPI canonical uppercase
2048        let s: TradeMessageStatus = serde_json::from_str("\"RETRYING\"").unwrap();
2049        assert_eq!(s, TradeMessageStatus::Retrying);
2050
2051        let s: TradeMessageStatus = serde_json::from_str("\"FAILED\"").unwrap();
2052        assert_eq!(s, TradeMessageStatus::Failed);
2053
2054        // lowercase aliases should also deserialize
2055        let s: TradeMessageStatus = serde_json::from_str("\"retrying\"").unwrap();
2056        assert_eq!(s, TradeMessageStatus::Retrying);
2057
2058        let s: TradeMessageStatus = serde_json::from_str("\"failed\"").unwrap();
2059        assert_eq!(s, TradeMessageStatus::Failed);
2060    }
2061
2062    #[test]
2063    fn test_trade_message_with_bucket_index() {
2064        let json_with = serde_json::json!({
2065            "id": "t1",
2066            "market": "0xmkt",
2067            "asset_id": "123",
2068            "side": "BUY",
2069            "size": "1.0",
2070            "price": "0.5",
2071            "bucket_index": 42
2072        });
2073        let tm: TradeMessage = serde_json::from_value(json_with).unwrap();
2074        assert_eq!(tm.bucket_index, Some(42));
2075
2076        let json_without = serde_json::json!({
2077            "id": "t2",
2078            "market": "0xmkt",
2079            "asset_id": "123",
2080            "side": "BUY",
2081            "size": "1.0",
2082            "price": "0.5"
2083        });
2084        let tm: TradeMessage = serde_json::from_value(json_without).unwrap();
2085        assert_eq!(tm.bucket_index, None);
2086    }
2087
2088    #[test]
2089    fn test_order_message_v2_fields() {
2090        let json = serde_json::json!({
2091            "id": "o1",
2092            "market": "0xmkt",
2093            "asset_id": "123",
2094            "side": "BUY",
2095            "price": "0.5",
2096            "owner": "owner-uuid",
2097            "expiration": "1700000000",
2098            "order_type": "GTD",
2099            "maker_address": "0xabc0000000000000000000000000000000000001"
2100        });
2101        let om: OrderMessage = serde_json::from_value(json).unwrap();
2102        assert_eq!(om.owner.as_deref(), Some("owner-uuid"));
2103        assert_eq!(om.expiration.as_deref(), Some("1700000000"));
2104        assert_eq!(om.order_type.as_deref(), Some("GTD"));
2105        assert_eq!(
2106            om.maker_address.as_deref(),
2107            Some("0xabc0000000000000000000000000000000000001")
2108        );
2109    }
2110}