Skip to main content

polyfill_rs/
orders.rs

1//! Order creation and signing functionality
2//!
3//! This module handles the complex process of creating and signing orders
4//! for the Polymarket CLOB, including EIP-712 signature generation.
5
6use crate::auth::{
7    sign_order_message, sign_order_message_with_domain, sign_poly1271_order_message_with_domain,
8    PreparedOrderDomain, SignedOrderMessage,
9};
10use crate::errors::{PolyfillError, Result};
11use crate::types::{
12    CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side, SignedOrderRequest,
13    SCALE_FACTOR,
14};
15use alloy_primitives::{keccak256, Address, B256, U256};
16use alloy_signer_local::PrivateKeySigner;
17use rand::Rng;
18use rust_decimal::Decimal;
19use rust_decimal::RoundingStrategy::{AwayFromZero, MidpointTowardZero, ToZero};
20use std::str::FromStr;
21use std::time::{SystemTime, UNIX_EPOCH};
22
23pub const BYTES32_ZERO: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
24
25/// Signature types for orders
26#[derive(Copy, Clone, Debug, PartialEq, Eq)]
27pub enum SigType {
28    /// ECDSA EIP712 signatures signed by EOAs
29    Eoa = 0,
30    /// EIP712 signatures signed by EOAs that own Polymarket Proxy wallets
31    PolyProxy = 1,
32    /// EIP712 signatures signed by EOAs that own Polymarket Gnosis safes
33    PolyGnosisSafe = 2,
34    /// EIP-1271 smart contract wallet signatures (V2 orders only)
35    Poly1271 = 3,
36}
37
38/// Rounding configuration for different tick sizes
39#[derive(Clone, Copy)]
40pub struct RoundConfig {
41    price: u32,
42    size: u32,
43    amount: u32,
44}
45
46/// Contract configuration
47pub struct ContractConfig {
48    pub exchange: String,
49    pub collateral: String,
50    pub conditional_tokens: String,
51}
52
53/// Order builder for creating and signing orders
54#[derive(Clone)]
55pub struct OrderBuilder {
56    signer: PrivateKeySigner,
57    signer_address: Address,
58    signer_checksum: String,
59    sig_type: SigType,
60    funder: Address,
61    funder_checksum: String,
62}
63
64/// Prepared low-latency order path for a single market/token configuration.
65#[derive(Clone)]
66pub struct PreparedOrderPath {
67    builder: OrderBuilder,
68    chain_id: u64,
69    token_id: String,
70    token_id_u256: U256,
71    round_config: RoundConfig,
72    domain: PreparedOrderDomain,
73    builder_bytes: B256,
74    builder_code: String,
75    metadata_bytes: B256,
76    metadata: String,
77}
78
79const POLYGON_PROXY_FACTORY: &str = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
80const POLYGON_SAFE_FACTORY: &str = "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b";
81const PROXY_INIT_CODE_HASH: &str =
82    "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b";
83const SAFE_INIT_CODE_HASH: &str =
84    "0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf";
85
86const ROUND_CONFIG_0_1: RoundConfig = RoundConfig {
87    price: 1,
88    size: 2,
89    amount: 3,
90};
91const ROUND_CONFIG_0_01: RoundConfig = RoundConfig {
92    price: 2,
93    size: 2,
94    amount: 4,
95};
96const ROUND_CONFIG_0_001: RoundConfig = RoundConfig {
97    price: 3,
98    size: 2,
99    amount: 5,
100};
101const ROUND_CONFIG_0_0001: RoundConfig = RoundConfig {
102    price: 4,
103    size: 2,
104    amount: 6,
105};
106const TOKEN_UNIT_SCALE: Decimal = Decimal::from_parts(1_000_000, 0, 0, false, 0);
107
108/// Get contract configuration for chain
109pub fn get_contract_config(chain_id: u64, neg_risk: bool) -> Option<ContractConfig> {
110    match (chain_id, neg_risk) {
111        (137, false) => Some(ContractConfig {
112            exchange: "0xE111180000d2663C0091e4f400237545B87B996B".to_string(),
113            collateral: "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB".to_string(),
114            conditional_tokens: "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045".to_string(),
115        }),
116        (137, true) => Some(ContractConfig {
117            exchange: "0xe2222d279d744050d28e00520010520000310F59".to_string(),
118            collateral: "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB".to_string(),
119            conditional_tokens: "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045".to_string(),
120        }),
121        _ => None,
122    }
123}
124
125fn exchange_address_for(chain_id: u64, neg_risk: bool) -> Result<Address> {
126    let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| {
127        PolyfillError::config("No contract found with given chain_id and neg_risk")
128    })?;
129
130    Address::from_str(&contract_config.exchange)
131        .map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))
132}
133
134fn parse_token_id(token_id: &str) -> Result<U256> {
135    U256::from_str_radix(token_id, 10)
136        .map_err(|e| PolyfillError::validation(format!("Incorrect tokenId format: {}", e)))
137}
138
139fn parse_optional_bytes32(name: &str, value: Option<&str>) -> Result<(B256, String)> {
140    let normalized = normalize_optional_bytes32(name, value)?;
141    let parsed = B256::from_str(&normalized)
142        .map_err(|e| PolyfillError::validation(format!("Invalid {name} bytes32 value: {e}")))?;
143
144    Ok((parsed, normalized))
145}
146
147pub fn sig_type_from_u8(signature_type: u8) -> Result<SigType> {
148    match signature_type {
149        0 => Ok(SigType::Eoa),
150        1 => Ok(SigType::PolyProxy),
151        2 => Ok(SigType::PolyGnosisSafe),
152        3 => Ok(SigType::Poly1271),
153        other => Err(PolyfillError::validation(format!(
154            "Unsupported signature_type {other}"
155        ))),
156    }
157}
158
159pub fn derive_proxy_wallet(eoa_address: Address, chain_id: u64) -> Result<Address> {
160    if chain_id != 137 {
161        return Err(PolyfillError::config(
162            "Proxy wallet auto-derivation is only configured for Polygon mainnet",
163        ));
164    }
165
166    let factory = Address::from_str(POLYGON_PROXY_FACTORY)
167        .map_err(|e| PolyfillError::config(format!("Invalid proxy factory address: {e}")))?;
168    let init_code_hash = B256::from_str(PROXY_INIT_CODE_HASH)
169        .map_err(|e| PolyfillError::config(format!("Invalid proxy init code hash: {e}")))?;
170    let salt = keccak256(eoa_address);
171    Ok(factory.create2(salt, init_code_hash))
172}
173
174pub fn derive_safe_wallet(eoa_address: Address, chain_id: u64) -> Result<Address> {
175    if chain_id != 137 {
176        return Err(PolyfillError::config(
177            "Safe wallet auto-derivation is only configured for Polygon mainnet",
178        ));
179    }
180
181    let factory = Address::from_str(POLYGON_SAFE_FACTORY)
182        .map_err(|e| PolyfillError::config(format!("Invalid safe factory address: {e}")))?;
183    let init_code_hash = B256::from_str(SAFE_INIT_CODE_HASH)
184        .map_err(|e| PolyfillError::config(format!("Invalid safe init code hash: {e}")))?;
185    let mut padded = [0_u8; 32];
186    padded[12..].copy_from_slice(eoa_address.as_slice());
187    let salt = keccak256(padded);
188    Ok(factory.create2(salt, init_code_hash))
189}
190
191pub fn resolve_funder(
192    signer_address: Address,
193    chain_id: u64,
194    sig_type: SigType,
195    funder: Option<Address>,
196) -> Result<Option<Address>> {
197    match (sig_type, funder) {
198        (SigType::Eoa, Some(_)) => Err(PolyfillError::validation(
199            "funder cannot be set for EOA signature_type",
200        )),
201        (SigType::PolyProxy, None) => derive_proxy_wallet(signer_address, chain_id).map(Some),
202        (SigType::PolyGnosisSafe, None) => derive_safe_wallet(signer_address, chain_id).map(Some),
203        (SigType::Poly1271, None) => Err(PolyfillError::validation(
204            "funder is required for Poly1271 signature_type",
205        )),
206        (_, Some(Address::ZERO)) => Err(PolyfillError::validation("funder cannot be zero address")),
207        (_, explicit) => Ok(explicit),
208    }
209}
210
211/// Generate a random seed for order salt
212fn generate_seed() -> u64 {
213    let mut rng = rand::thread_rng();
214    let y: f64 = rng.gen();
215    let timestamp = SystemTime::now()
216        .duration_since(UNIX_EPOCH)
217        .expect("Time went backwards")
218        .as_secs();
219    (timestamp as f64 * y) as u64
220}
221
222/// Convert decimal to token units (multiply by 1e6)
223fn decimal_to_token_units(amt: Decimal) -> Result<U256> {
224    let mut amt = TOKEN_UNIT_SCALE * amt;
225    if amt.scale() > 0 {
226        amt = amt.round_dp_with_strategy(0, MidpointTowardZero);
227    }
228    let units: u128 = amt
229        .try_into()
230        .map_err(|_| PolyfillError::validation(format!("Invalid token amount {amt}")))?;
231    Ok(U256::from(units))
232}
233
234fn parse_round_config(tick_size: Decimal) -> Result<&'static RoundConfig> {
235    let scaled = tick_size * Decimal::from(SCALE_FACTOR);
236    if !scaled.is_integer() {
237        return Err(PolyfillError::validation(format!(
238            "Unsupported tick size {tick_size}"
239        )));
240    }
241
242    let tick_size_ticks: u32 = scaled
243        .try_into()
244        .map_err(|_| PolyfillError::validation(format!("Unsupported tick size {tick_size}")))?;
245
246    match tick_size_ticks {
247        1000 => Ok(&ROUND_CONFIG_0_1),
248        100 => Ok(&ROUND_CONFIG_0_01),
249        10 => Ok(&ROUND_CONFIG_0_001),
250        1 => Ok(&ROUND_CONFIG_0_0001),
251        _ => Err(PolyfillError::validation(format!(
252            "Unsupported tick size {tick_size}"
253        ))),
254    }
255}
256
257pub(crate) fn validate_bytes32_hex(field: &str, value: &str) -> Result<()> {
258    if value == BYTES32_ZERO {
259        return Ok(());
260    }
261
262    if !value.starts_with("0x") {
263        return Err(PolyfillError::validation(format!(
264            "{field} must be a 0x-prefixed 32-byte hex string"
265        )));
266    }
267
268    if value.len() != 66 {
269        return Err(PolyfillError::validation(format!(
270            "{field} must be exactly 32 bytes (64 hex chars)"
271        )));
272    }
273
274    if !value
275        .as_bytes()
276        .iter()
277        .skip(2)
278        .all(|byte| byte.is_ascii_hexdigit())
279    {
280        return Err(PolyfillError::validation(format!(
281            "{field} must contain only hexadecimal characters"
282        )));
283    }
284
285    Ok(())
286}
287
288fn normalize_optional_bytes32(field: &str, value: Option<&str>) -> Result<String> {
289    let value = value.unwrap_or(BYTES32_ZERO);
290    validate_bytes32_hex(field, value)?;
291    Ok(value.to_string())
292}
293
294pub fn adjust_buy_amount_for_fees(
295    amount: Decimal,
296    price: Decimal,
297    user_usdc_balance: Decimal,
298    fee_rate: Decimal,
299    fee_exponent: u32,
300    builder_taker_fee_rate: Decimal,
301) -> Result<Decimal> {
302    if price <= Decimal::ZERO {
303        return Err(PolyfillError::validation(
304            "Market buy fee adjustment requires a positive price",
305        ));
306    }
307
308    let base = price * (Decimal::ONE - price);
309    let base_f64: f64 = base
310        .try_into()
311        .map_err(|_| PolyfillError::validation(format!("Invalid fee base {base}")))?;
312    let exp_f64: f64 = Decimal::from(fee_exponent)
313        .try_into()
314        .map_err(|_| PolyfillError::validation(format!("Invalid fee exponent {fee_exponent}")))?;
315    let platform_fee_rate = fee_rate
316        * Decimal::try_from(base_f64.powf(exp_f64)).map_err(|_| {
317            PolyfillError::validation(format!(
318                "Invalid platform fee rate for price {price} and exponent {fee_exponent}"
319            ))
320        })?;
321
322    let platform_fee = amount / price * platform_fee_rate;
323    let total_cost = amount + platform_fee + amount * builder_taker_fee_rate;
324
325    let raw = if user_usdc_balance <= total_cost {
326        let divisor = Decimal::ONE + platform_fee_rate / price + builder_taker_fee_rate;
327        user_usdc_balance / divisor
328    } else {
329        amount
330    };
331
332    let adjusted = raw.trunc_with_scale(6);
333    if adjusted.is_zero() {
334        return Err(PolyfillError::validation(format!(
335            "user_usdc_balance {user_usdc_balance} too small to cover fees at price {price}; \
336             fee-adjusted amount truncated to zero"
337        )));
338    }
339
340    Ok(adjusted)
341}
342
343impl OrderBuilder {
344    /// Create a new order builder
345    pub fn new(
346        signer: PrivateKeySigner,
347        sig_type: Option<SigType>,
348        funder: Option<Address>,
349    ) -> Self {
350        let sig_type = sig_type.unwrap_or(SigType::Eoa);
351        let signer_address = signer.address();
352        let signer_checksum = signer_address.to_checksum(None);
353        let funder = funder.unwrap_or(signer_address);
354        let funder_checksum = funder.to_checksum(None);
355
356        OrderBuilder {
357            signer,
358            signer_address,
359            signer_checksum,
360            sig_type,
361            funder,
362            funder_checksum,
363        }
364    }
365
366    /// Get signature type as u8
367    pub fn get_sig_type(&self) -> u8 {
368        self.sig_type as u8
369    }
370
371    /// Prepare reusable order-path state for one market/token.
372    ///
373    /// This caches tick-size rounding, exchange address parsing, token ID parsing, normalized
374    /// bytes32 fields, and the EIP-712 domain. Use this for latency-sensitive repeated order
375    /// creation on the same token.
376    pub fn prepare_order_path(
377        &self,
378        chain_id: u64,
379        token_id: impl Into<String>,
380        tick_size: Decimal,
381        neg_risk: bool,
382        builder_code: Option<&str>,
383        metadata: Option<&str>,
384    ) -> Result<PreparedOrderPath> {
385        let token_id = token_id.into();
386        let token_id_u256 = parse_token_id(&token_id)?;
387        let round_config = *parse_round_config(tick_size)?;
388        let exchange = exchange_address_for(chain_id, neg_risk)?;
389        let domain = PreparedOrderDomain::new(chain_id, exchange);
390        let (builder_bytes, builder_code) = parse_optional_bytes32("builder_code", builder_code)?;
391        let (metadata_bytes, metadata) = parse_optional_bytes32("metadata", metadata)?;
392
393        Ok(PreparedOrderPath {
394            builder: self.clone(),
395            chain_id,
396            token_id,
397            token_id_u256,
398            round_config,
399            domain,
400            builder_bytes,
401            builder_code,
402            metadata_bytes,
403            metadata,
404        })
405    }
406
407    /// Fix amount rounding according to configuration
408    fn fix_amount_rounding(&self, mut amt: Decimal, round_config: &RoundConfig) -> Decimal {
409        if amt.scale() > round_config.amount {
410            amt = amt.round_dp_with_strategy(round_config.amount + 4, AwayFromZero);
411            if amt.scale() > round_config.amount {
412                amt = amt.round_dp_with_strategy(round_config.amount, ToZero);
413            }
414        }
415        amt
416    }
417
418    /// Get order amounts (maker and taker) for a regular order
419    fn get_order_amounts(
420        &self,
421        side: Side,
422        size: Decimal,
423        price: Decimal,
424        round_config: &RoundConfig,
425    ) -> Result<(U256, U256)> {
426        let raw_price = price.round_dp_with_strategy(round_config.price, MidpointTowardZero);
427
428        let amounts = match side {
429            Side::BUY => {
430                let raw_taker_amt = size.round_dp_with_strategy(round_config.size, ToZero);
431                let raw_maker_amt = raw_taker_amt * raw_price;
432                let raw_maker_amt = self.fix_amount_rounding(raw_maker_amt, round_config);
433                (
434                    decimal_to_token_units(raw_maker_amt)?,
435                    decimal_to_token_units(raw_taker_amt)?,
436                )
437            },
438            Side::SELL => {
439                let raw_maker_amt = size.round_dp_with_strategy(round_config.size, ToZero);
440                let raw_taker_amt = raw_maker_amt * raw_price;
441                let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config);
442
443                (
444                    decimal_to_token_units(raw_maker_amt)?,
445                    decimal_to_token_units(raw_taker_amt)?,
446                )
447            },
448        };
449
450        Ok(amounts)
451    }
452
453    /// Get order amounts for a market order
454    fn get_market_order_amounts(
455        &self,
456        side: Side,
457        amount: Decimal,
458        price: Decimal,
459        round_config: &RoundConfig,
460    ) -> Result<(U256, U256)> {
461        let raw_price = price.round_dp_with_strategy(round_config.price, MidpointTowardZero);
462
463        let amounts = match side {
464            Side::BUY => {
465                let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
466                let raw_taker_amt =
467                    self.fix_amount_rounding(raw_maker_amt / raw_price, round_config);
468
469                (
470                    decimal_to_token_units(raw_maker_amt)?,
471                    decimal_to_token_units(raw_taker_amt)?,
472                )
473            },
474            Side::SELL => {
475                let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
476                let raw_taker_amt =
477                    self.fix_amount_rounding(raw_maker_amt * raw_price, round_config);
478
479                (
480                    decimal_to_token_units(raw_maker_amt)?,
481                    decimal_to_token_units(raw_taker_amt)?,
482                )
483            },
484        };
485
486        Ok(amounts)
487    }
488
489    /// Calculate market price from order book levels
490    pub fn calculate_market_price(
491        &self,
492        positions: &[crate::types::BookLevel],
493        amount_to_match: Decimal,
494        side: Side,
495        order_type: OrderType,
496    ) -> Result<Decimal> {
497        let mut sum = Decimal::ZERO;
498        let mut last_price = None;
499
500        for level in positions {
501            sum += match side {
502                Side::BUY => level.size * level.price,
503                Side::SELL => level.size,
504            };
505            last_price = Some(level.price);
506            if sum >= amount_to_match {
507                return Ok(level.price);
508            }
509        }
510
511        match (order_type, last_price) {
512            (OrderType::FAK, Some(price)) => Ok(price),
513            _ => Err(PolyfillError::order(
514                format!(
515                    "Not enough liquidity to create market order with amount {}",
516                    amount_to_match
517                ),
518                crate::errors::OrderErrorKind::InsufficientBalance,
519            )),
520        }
521    }
522
523    /// Create a market order
524    pub fn create_market_order(
525        &self,
526        chain_id: u64,
527        order_args: &MarketOrderArgs,
528        price: Decimal,
529        options: &CreateOrderOptions,
530    ) -> Result<SignedOrderRequest> {
531        if !matches!(order_args.order_type, OrderType::FOK | OrderType::FAK) {
532            return Err(PolyfillError::validation(
533                "Market orders only support FOK and FAK order types",
534            ));
535        }
536
537        let tick_size = options
538            .tick_size
539            .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
540        let round_config = parse_round_config(tick_size)?;
541
542        let (maker_amount, taker_amount) =
543            self.get_market_order_amounts(order_args.side, order_args.amount, price, round_config)?;
544
545        let neg_risk = options
546            .neg_risk
547            .ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
548
549        let exchange_address = exchange_address_for(chain_id, neg_risk)?;
550
551        self.build_signed_order(
552            order_args.token_id.clone(),
553            order_args.side,
554            chain_id,
555            exchange_address,
556            maker_amount,
557            taker_amount,
558            0,
559            order_args.builder_code.as_deref(),
560            order_args.metadata.as_deref(),
561        )
562    }
563
564    /// Create a regular order
565    pub fn create_order(
566        &self,
567        chain_id: u64,
568        order_args: &OrderArgs,
569        options: &CreateOrderOptions,
570    ) -> Result<SignedOrderRequest> {
571        let tick_size = options
572            .tick_size
573            .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
574        let round_config = parse_round_config(tick_size)?;
575
576        let (maker_amount, taker_amount) = self.get_order_amounts(
577            order_args.side,
578            order_args.size,
579            order_args.price,
580            round_config,
581        )?;
582
583        let neg_risk = options
584            .neg_risk
585            .ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
586
587        let exchange_address = exchange_address_for(chain_id, neg_risk)?;
588
589        self.build_signed_order(
590            order_args.token_id.clone(),
591            order_args.side,
592            chain_id,
593            exchange_address,
594            maker_amount,
595            taker_amount,
596            order_args.expiration.unwrap_or(0),
597            order_args.builder_code.as_deref(),
598            order_args.metadata.as_deref(),
599        )
600    }
601
602    /// Build and sign an order
603    #[allow(clippy::too_many_arguments)]
604    fn build_signed_order(
605        &self,
606        token_id: String,
607        side: Side,
608        chain_id: u64,
609        exchange: Address,
610        maker_amount: U256,
611        taker_amount: U256,
612        expiration: u64,
613        builder_code: Option<&str>,
614        metadata: Option<&str>,
615    ) -> Result<SignedOrderRequest> {
616        let seed = generate_seed();
617        let timestamp = SystemTime::now()
618            .duration_since(UNIX_EPOCH)
619            .expect("Time went backwards")
620            .as_millis();
621
622        let u256_token_id = parse_token_id(&token_id)?;
623        let (builder_bytes, builder) = parse_optional_bytes32("builder_code", builder_code)?;
624        let (metadata_bytes, metadata) = parse_optional_bytes32("metadata", metadata)?;
625
626        let signer_address = self.order_signer_address();
627        let signer_checksum = self.order_signer_checksum();
628        let order = SignedOrderMessage {
629            salt: U256::from(seed),
630            maker: self.funder,
631            signer: signer_address,
632            token_id: u256_token_id,
633            maker_amount,
634            taker_amount,
635            side: side as u8,
636            signature_type: self.sig_type as u8,
637            timestamp: U256::from(timestamp),
638            metadata: metadata_bytes,
639            builder: builder_bytes,
640        };
641
642        let signature = match self.sig_type {
643            SigType::Poly1271 => sign_poly1271_order_message_with_domain(
644                &self.signer,
645                order,
646                &PreparedOrderDomain::new(chain_id, exchange),
647                self.funder,
648                chain_id,
649            )?,
650            _ => sign_order_message(&self.signer, order, chain_id, exchange)?,
651        };
652
653        Ok(SignedOrderRequest {
654            salt: seed,
655            maker: self.funder_checksum.clone(),
656            signer: signer_checksum,
657            token_id,
658            maker_amount: maker_amount.to_string(),
659            taker_amount: taker_amount.to_string(),
660            expiration: expiration.to_string(),
661            side: side.as_str().to_string(),
662            signature_type: self.sig_type as u8,
663            timestamp: timestamp.to_string(),
664            metadata,
665            builder,
666            signature,
667        })
668    }
669}
670
671impl OrderBuilder {
672    fn order_signer_address(&self) -> Address {
673        match self.sig_type {
674            SigType::Poly1271 => self.funder,
675            _ => self.signer_address,
676        }
677    }
678
679    fn order_signer_checksum(&self) -> String {
680        match self.sig_type {
681            SigType::Poly1271 => self.funder_checksum.clone(),
682            _ => self.signer_checksum.clone(),
683        }
684    }
685}
686
687impl PreparedOrderPath {
688    /// Create and sign a limit order using the cached market/token context.
689    pub fn create_limit_order(
690        &self,
691        side: Side,
692        price: Decimal,
693        size: Decimal,
694        expiration: Option<u64>,
695    ) -> Result<SignedOrderRequest> {
696        let (maker_amount, taker_amount) =
697            self.builder
698                .get_order_amounts(side, size, price, &self.round_config)?;
699
700        self.build_signed_order(side, maker_amount, taker_amount, expiration.unwrap_or(0))
701    }
702
703    /// Create and sign a market order using the cached market/token context.
704    pub fn create_market_order(
705        &self,
706        side: Side,
707        amount: Decimal,
708        price: Decimal,
709        order_type: OrderType,
710    ) -> Result<SignedOrderRequest> {
711        if !matches!(order_type, OrderType::FOK | OrderType::FAK) {
712            return Err(PolyfillError::validation(
713                "Market orders only support FOK and FAK order types",
714            ));
715        }
716
717        let (maker_amount, taker_amount) =
718            self.builder
719                .get_market_order_amounts(side, amount, price, &self.round_config)?;
720
721        self.build_signed_order(side, maker_amount, taker_amount, 0)
722    }
723
724    fn build_signed_order(
725        &self,
726        side: Side,
727        maker_amount: U256,
728        taker_amount: U256,
729        expiration: u64,
730    ) -> Result<SignedOrderRequest> {
731        let seed = generate_seed();
732        let timestamp = SystemTime::now()
733            .duration_since(UNIX_EPOCH)
734            .expect("Time went backwards")
735            .as_millis();
736
737        let signer_address = self.builder.order_signer_address();
738        let signer_checksum = self.builder.order_signer_checksum();
739        let order = SignedOrderMessage {
740            salt: U256::from(seed),
741            maker: self.builder.funder,
742            signer: signer_address,
743            token_id: self.token_id_u256,
744            maker_amount,
745            taker_amount,
746            side: side as u8,
747            signature_type: self.builder.sig_type as u8,
748            timestamp: U256::from(timestamp),
749            metadata: self.metadata_bytes,
750            builder: self.builder_bytes,
751        };
752
753        let signature = match self.builder.sig_type {
754            SigType::Poly1271 => sign_poly1271_order_message_with_domain(
755                &self.builder.signer,
756                order,
757                &self.domain,
758                self.builder.funder,
759                self.chain_id,
760            )?,
761            _ => sign_order_message_with_domain(&self.builder.signer, order, &self.domain)?,
762        };
763
764        Ok(SignedOrderRequest {
765            salt: seed,
766            maker: self.builder.funder_checksum.clone(),
767            signer: signer_checksum,
768            token_id: self.token_id.clone(),
769            maker_amount: maker_amount.to_string(),
770            taker_amount: taker_amount.to_string(),
771            expiration: expiration.to_string(),
772            side: side.as_str().to_string(),
773            signature_type: self.builder.sig_type as u8,
774            timestamp: timestamp.to_string(),
775            metadata: self.metadata.clone(),
776            builder: self.builder_code.clone(),
777            signature,
778        })
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use alloy_signer_local::PrivateKeySigner;
786    use serde_json::Value;
787
788    fn test_builder() -> OrderBuilder {
789        let signer: PrivateKeySigner =
790            "0x1234567890123456789012345678901234567890123456789012345678901234"
791                .parse()
792                .expect("valid private key");
793        OrderBuilder::new(signer, None, None)
794    }
795
796    #[test]
797    fn test_decimal_to_token_units() {
798        let result = decimal_to_token_units(Decimal::from_str("1.5").unwrap()).unwrap();
799        assert_eq!(result, U256::from(1_500_000));
800    }
801
802    #[test]
803    fn test_generate_seed() {
804        let seed1 = generate_seed();
805        let seed2 = generate_seed();
806        assert_ne!(seed1, seed2);
807    }
808
809    #[test]
810    fn test_decimal_to_token_units_edge_cases() {
811        // Test zero
812        let result = decimal_to_token_units(Decimal::ZERO).unwrap();
813        assert_eq!(result, U256::ZERO);
814
815        // Test small decimal
816        let result = decimal_to_token_units(Decimal::from_str("0.000001").unwrap()).unwrap();
817        assert_eq!(result, U256::from(1));
818
819        // Test large number
820        let result = decimal_to_token_units(Decimal::from_str("1000.0").unwrap()).unwrap();
821        assert_eq!(result, U256::from(1_000_000_000));
822    }
823
824    #[test]
825    fn test_decimal_to_token_units_supports_amounts_above_u32() {
826        let result = decimal_to_token_units(Decimal::from_str("5000").unwrap()).unwrap();
827        assert_eq!(result, U256::from(5_000_000_000_u64));
828    }
829
830    #[test]
831    fn test_decimal_to_token_units_rejects_negative_amounts() {
832        let result = decimal_to_token_units(Decimal::from_str("-1").unwrap());
833        assert!(matches!(result, Err(PolyfillError::Validation { .. })));
834    }
835
836    #[test]
837    fn test_parse_round_config_accepts_exact_supported_tick_sizes() {
838        let config = parse_round_config(Decimal::from_str("0.1").unwrap()).unwrap();
839        assert_eq!(config.price, 1);
840
841        let config = parse_round_config(Decimal::from_str("0.0100").unwrap()).unwrap();
842        assert_eq!(config.price, 2);
843
844        let config = parse_round_config(Decimal::from_str("0.001").unwrap()).unwrap();
845        assert_eq!(config.price, 3);
846
847        let config = parse_round_config(Decimal::from_str("0.00010").unwrap()).unwrap();
848        assert_eq!(config.price, 4);
849    }
850
851    #[test]
852    fn test_parse_round_config_rejects_fractional_or_unsupported_tick_sizes() {
853        for tick_size in ["0.00005", "0.00009", "0.00011", "0.0002", "0", "-0.01"] {
854            let result = parse_round_config(Decimal::from_str(tick_size).unwrap());
855            assert!(matches!(result, Err(PolyfillError::Validation { .. })));
856        }
857    }
858
859    #[test]
860    fn test_get_contract_config() {
861        // Test Polygon mainnet
862        let config = get_contract_config(137, false).expect("polygon config");
863        assert_eq!(
864            config.exchange,
865            "0xE111180000d2663C0091e4f400237545B87B996B"
866        );
867        assert_eq!(
868            config.collateral,
869            "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
870        );
871        assert_eq!(
872            config.conditional_tokens,
873            "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
874        );
875
876        // Test with neg risk
877        let config_neg = get_contract_config(137, true).expect("neg risk polygon config");
878        assert_eq!(
879            config_neg.exchange,
880            "0xe2222d279d744050d28e00520010520000310F59"
881        );
882        assert_eq!(
883            config_neg.collateral,
884            "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
885        );
886
887        // Test unsupported chain
888        let config_unsupported = get_contract_config(999, false);
889        assert!(config_unsupported.is_none());
890    }
891
892    #[test]
893    fn test_signature_type_from_u8() {
894        assert_eq!(sig_type_from_u8(0).unwrap(), SigType::Eoa);
895        assert_eq!(sig_type_from_u8(1).unwrap(), SigType::PolyProxy);
896        assert_eq!(sig_type_from_u8(2).unwrap(), SigType::PolyGnosisSafe);
897        assert_eq!(sig_type_from_u8(3).unwrap(), SigType::Poly1271);
898        assert!(sig_type_from_u8(4).is_err());
899    }
900
901    #[test]
902    fn test_derive_polygon_funder_addresses() {
903        let eoa = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
904        assert_eq!(
905            derive_safe_wallet(eoa, 137).unwrap(),
906            Address::from_str("0xd93b25Cb943D14d0d34FBAf01fc93a0F8b5f6e47").unwrap()
907        );
908        assert_eq!(
909            derive_proxy_wallet(eoa, 137).unwrap(),
910            Address::from_str("0x365f0cA36ae1F641E02Fe3b7743673DA42A13a70").unwrap()
911        );
912    }
913
914    #[test]
915    fn test_normalize_optional_bytes32_defaults_to_zero() {
916        assert_eq!(
917            normalize_optional_bytes32("builder_code", None).unwrap(),
918            BYTES32_ZERO
919        );
920    }
921
922    #[test]
923    fn test_normalize_optional_bytes32_rejects_invalid_hex() {
924        let err = normalize_optional_bytes32("metadata", Some("deadbeef")).unwrap_err();
925        assert!(matches!(err, PolyfillError::Validation { .. }));
926    }
927
928    #[test]
929    fn test_create_order_serializes_v2_fields_without_legacy_fields() {
930        let builder = test_builder();
931        let order = builder
932            .create_order(
933                137,
934                &OrderArgs {
935                    token_id: "123456".to_string(),
936                    price: Decimal::from_str("0.45").unwrap(),
937                    size: Decimal::from_str("12.34").unwrap(),
938                    side: Side::BUY,
939                    expiration: Some(1_900_000_000),
940                    builder_code: Some(BYTES32_ZERO.to_string()),
941                    metadata: None,
942                },
943                &CreateOrderOptions {
944                    tick_size: Some(Decimal::from_str("0.01").unwrap()),
945                    neg_risk: Some(false),
946                },
947            )
948            .unwrap();
949
950        let serialized = serde_json::to_value(&order).unwrap();
951        let object = serialized.as_object().unwrap();
952        assert!(object.contains_key("timestamp"));
953        assert!(object.contains_key("metadata"));
954        assert!(object.contains_key("builder"));
955        assert!(object.contains_key("expiration"));
956        assert!(!object.contains_key("taker"));
957        assert!(!object.contains_key("nonce"));
958        assert!(!object.contains_key("feeRateBps"));
959        assert_eq!(order.builder, BYTES32_ZERO);
960        assert_eq!(order.metadata, BYTES32_ZERO);
961    }
962
963    #[test]
964    fn test_poly1271_order_uses_deposit_wallet_as_signer_and_wrapped_signature() {
965        let signer: PrivateKeySigner =
966            "0x2222222222222222222222222222222222222222222222222222222222222222"
967                .parse()
968                .expect("valid private key");
969        let funder = Address::from_str("0x000000000000000000000000000000000000d077").unwrap();
970        let builder = OrderBuilder::new(signer, Some(SigType::Poly1271), Some(funder));
971
972        let order = builder
973            .create_order(
974                137,
975                &OrderArgs {
976                    token_id: "123456".to_string(),
977                    price: Decimal::from_str("0.50").unwrap(),
978                    size: Decimal::from_str("10").unwrap(),
979                    side: Side::BUY,
980                    expiration: None,
981                    builder_code: Some(BYTES32_ZERO.to_string()),
982                    metadata: Some(BYTES32_ZERO.to_string()),
983                },
984                &CreateOrderOptions {
985                    tick_size: Some(Decimal::from_str("0.01").unwrap()),
986                    neg_risk: Some(false),
987                },
988            )
989            .unwrap();
990
991        let funder_checksum = funder.to_checksum(None);
992        assert_eq!(order.maker, funder_checksum);
993        assert_eq!(order.signer, funder_checksum);
994        assert_eq!(order.signature_type, SigType::Poly1271 as u8);
995        assert!(order.signature.starts_with("0x"));
996        assert!(
997            order.signature.len() > 600,
998            "POLY_1271 signature should be ERC-7739 wrapped"
999        );
1000    }
1001
1002    #[test]
1003    fn test_prepared_order_path_creates_equivalent_limit_order_fields() {
1004        let builder = test_builder();
1005        let args = OrderArgs {
1006            token_id: "123456".to_string(),
1007            price: Decimal::from_str("0.45").unwrap(),
1008            size: Decimal::from_str("12.34").unwrap(),
1009            side: Side::BUY,
1010            expiration: Some(1_900_000_000),
1011            builder_code: Some(BYTES32_ZERO.to_string()),
1012            metadata: None,
1013        };
1014        let options = CreateOrderOptions {
1015            tick_size: Some(Decimal::from_str("0.01").unwrap()),
1016            neg_risk: Some(false),
1017        };
1018
1019        let order = builder.create_order(137, &args, &options).unwrap();
1020        let prepared = builder
1021            .prepare_order_path(
1022                137,
1023                args.token_id.clone(),
1024                options.tick_size.unwrap(),
1025                options.neg_risk.unwrap(),
1026                args.builder_code.as_deref(),
1027                args.metadata.as_deref(),
1028            )
1029            .unwrap();
1030        let prepared_order = prepared
1031            .create_limit_order(args.side, args.price, args.size, args.expiration)
1032            .unwrap();
1033
1034        assert_eq!(prepared_order.maker, order.maker);
1035        assert_eq!(prepared_order.signer, order.signer);
1036        assert_eq!(prepared_order.token_id, order.token_id);
1037        assert_eq!(prepared_order.maker_amount, order.maker_amount);
1038        assert_eq!(prepared_order.taker_amount, order.taker_amount);
1039        assert_eq!(prepared_order.expiration, order.expiration);
1040        assert_eq!(prepared_order.side, order.side);
1041        assert_eq!(prepared_order.signature_type, order.signature_type);
1042        assert_eq!(prepared_order.metadata, order.metadata);
1043        assert_eq!(prepared_order.builder, order.builder);
1044        assert!(prepared_order.signature.starts_with("0x"));
1045    }
1046
1047    #[test]
1048    fn test_prepared_market_order_rejects_unsupported_order_type() {
1049        let builder = test_builder();
1050        let prepared = builder
1051            .prepare_order_path(
1052                137,
1053                "123456",
1054                Decimal::from_str("0.01").unwrap(),
1055                false,
1056                None,
1057                None,
1058            )
1059            .unwrap();
1060
1061        let err = prepared
1062            .create_market_order(
1063                Side::BUY,
1064                Decimal::from_str("10").unwrap(),
1065                Decimal::from_str("0.45").unwrap(),
1066                OrderType::GTC,
1067            )
1068            .unwrap_err();
1069
1070        assert!(matches!(err, PolyfillError::Validation { .. }));
1071    }
1072
1073    #[test]
1074    fn test_create_market_order_supports_fak() {
1075        let builder = test_builder();
1076        let order = builder
1077            .create_market_order(
1078                137,
1079                &MarketOrderArgs {
1080                    token_id: "123456".to_string(),
1081                    amount: Decimal::from_str("10.0").unwrap(),
1082                    side: Side::BUY,
1083                    order_type: OrderType::FAK,
1084                    price_limit: None,
1085                    user_usdc_balance: None,
1086                    builder_code: None,
1087                    metadata: None,
1088                },
1089                Decimal::from_str("0.25").unwrap(),
1090                &CreateOrderOptions {
1091                    tick_size: Some(Decimal::from_str("0.01").unwrap()),
1092                    neg_risk: Some(false),
1093                },
1094            )
1095            .unwrap();
1096
1097        assert_eq!(order.side, "BUY");
1098        assert!(!order.timestamp.is_empty());
1099    }
1100
1101    #[test]
1102    fn test_adjust_buy_amount_for_fees_uses_builder_rate_decimal() {
1103        let adjusted = adjust_buy_amount_for_fees(
1104            Decimal::from_str("100").unwrap(),
1105            Decimal::from_str("0.5").unwrap(),
1106            Decimal::from_str("100").unwrap(),
1107            Decimal::ZERO,
1108            0,
1109            Decimal::from_str("0.01").unwrap(),
1110        )
1111        .unwrap();
1112
1113        assert_eq!(adjusted, Decimal::from_str("99.009900").unwrap());
1114    }
1115
1116    #[test]
1117    fn test_adjust_buy_amount_for_fees_rejects_zero_after_truncation() {
1118        let err = adjust_buy_amount_for_fees(
1119            Decimal::from_str("1").unwrap(),
1120            Decimal::from_str("0.5").unwrap(),
1121            Decimal::from_str("0.0000009").unwrap(),
1122            Decimal::ZERO,
1123            0,
1124            Decimal::ZERO,
1125        )
1126        .unwrap_err();
1127
1128        assert!(matches!(err, PolyfillError::Validation { .. }));
1129    }
1130
1131    #[test]
1132    fn test_market_order_amounts_differ_for_buy_and_sell() {
1133        let builder = test_builder();
1134        let round_config = parse_round_config(Decimal::from_str("0.01").unwrap()).unwrap();
1135
1136        let (buy_maker, buy_taker) = builder
1137            .get_market_order_amounts(
1138                Side::BUY,
1139                Decimal::from_str("10").unwrap(),
1140                Decimal::from_str("0.25").unwrap(),
1141                round_config,
1142            )
1143            .unwrap();
1144        let (sell_maker, sell_taker) = builder
1145            .get_market_order_amounts(
1146                Side::SELL,
1147                Decimal::from_str("10").unwrap(),
1148                Decimal::from_str("0.25").unwrap(),
1149                round_config,
1150            )
1151            .unwrap();
1152
1153        assert_eq!(buy_maker, U256::from(10_000_000));
1154        assert_eq!(buy_taker, U256::from(40_000_000));
1155        assert_eq!(sell_maker, U256::from(10_000_000));
1156        assert_eq!(sell_taker, U256::from(2_500_000));
1157    }
1158
1159    #[test]
1160    fn test_calculate_market_price_returns_last_level_for_fak() {
1161        let builder = test_builder();
1162        let levels = vec![
1163            crate::types::BookLevel {
1164                price: Decimal::from_str("0.40").unwrap(),
1165                size: Decimal::from_str("2.0").unwrap(),
1166            },
1167            crate::types::BookLevel {
1168                price: Decimal::from_str("0.45").unwrap(),
1169                size: Decimal::from_str("1.0").unwrap(),
1170            },
1171        ];
1172
1173        let price = builder
1174            .calculate_market_price(
1175                &levels,
1176                Decimal::from_str("10.0").unwrap(),
1177                Side::SELL,
1178                OrderType::FAK,
1179            )
1180            .unwrap();
1181        assert_eq!(price, Decimal::from_str("0.45").unwrap());
1182    }
1183
1184    #[test]
1185    fn test_signed_order_json_uses_camel_case_wire_shape() {
1186        let builder = test_builder();
1187        let order = builder
1188            .create_order(
1189                137,
1190                &OrderArgs {
1191                    token_id: "123456".to_string(),
1192                    price: Decimal::from_str("0.55").unwrap(),
1193                    size: Decimal::from_str("5.0").unwrap(),
1194                    side: Side::SELL,
1195                    expiration: Some(1_900_000_000),
1196                    builder_code: None,
1197                    metadata: Some(BYTES32_ZERO.to_string()),
1198                },
1199                &CreateOrderOptions {
1200                    tick_size: Some(Decimal::from_str("0.01").unwrap()),
1201                    neg_risk: Some(true),
1202                },
1203            )
1204            .unwrap();
1205
1206        let json = serde_json::to_value(order).unwrap();
1207        assert!(matches!(json.get("tokenId"), Some(Value::String(_))));
1208        assert!(matches!(json.get("makerAmount"), Some(Value::String(_))));
1209        assert!(matches!(json.get("takerAmount"), Some(Value::String(_))));
1210        assert!(matches!(json.get("signatureType"), Some(Value::Number(_))));
1211    }
1212
1213    #[test]
1214    fn test_seed_generation_uniqueness() {
1215        let mut seeds = std::collections::HashSet::new();
1216
1217        // Generate 1000 seeds and ensure they're all unique
1218        for _ in 0..1000 {
1219            let seed = generate_seed();
1220            assert!(seeds.insert(seed), "Duplicate seed generated");
1221        }
1222    }
1223
1224    #[test]
1225    fn test_seed_generation_range() {
1226        for _ in 0..100 {
1227            let seed = generate_seed();
1228            // Seeds should be positive and within reasonable range
1229            assert!(seed > 0);
1230            assert!(seed < u64::MAX);
1231        }
1232    }
1233}