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::sign_order_message;
7use crate::client::OrderArgs;
8use crate::errors::{PolyfillError, Result};
9use crate::types::{ExtraOrderArgs, MarketOrderArgs, OrderOptions, Side, SignedOrderRequest};
10use alloy_primitives::{Address, U256};
11use alloy_signer_local::PrivateKeySigner;
12use rand::Rng;
13use rust_decimal::Decimal;
14use rust_decimal::RoundingStrategy::{AwayFromZero, MidpointTowardZero, ToZero};
15use std::collections::HashMap;
16use std::str::FromStr;
17use std::sync::LazyLock;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20/// Signature types for orders
21#[derive(Copy, Clone)]
22pub enum SigType {
23    /// ECDSA EIP712 signatures signed by EOAs
24    Eoa = 0,
25    /// EIP712 signatures signed by EOAs that own Polymarket Proxy wallets
26    PolyProxy = 1,
27    /// EIP712 signatures signed by EOAs that own Polymarket Gnosis safes
28    PolyGnosisSafe = 2,
29}
30
31/// Rounding configuration for different tick sizes
32pub struct RoundConfig {
33    price: u32,
34    size: u32,
35    amount: u32,
36}
37
38/// Contract configuration
39pub struct ContractConfig {
40    pub exchange: String,
41    pub collateral: String,
42    pub conditional_tokens: String,
43}
44
45/// Order builder for creating and signing orders
46pub struct OrderBuilder {
47    signer: PrivateKeySigner,
48    sig_type: SigType,
49    funder: Address,
50}
51
52/// Rounding configurations for different tick sizes
53static ROUNDING_CONFIG: LazyLock<HashMap<Decimal, RoundConfig>> = LazyLock::new(|| {
54    HashMap::from([
55        (
56            Decimal::from_str("0.1").unwrap(),
57            RoundConfig {
58                price: 1,
59                size: 2,
60                amount: 3,
61            },
62        ),
63        (
64            Decimal::from_str("0.01").unwrap(),
65            RoundConfig {
66                price: 2,
67                size: 2,
68                amount: 4,
69            },
70        ),
71        (
72            Decimal::from_str("0.001").unwrap(),
73            RoundConfig {
74                price: 3,
75                size: 2,
76                amount: 5,
77            },
78        ),
79        (
80            Decimal::from_str("0.0001").unwrap(),
81            RoundConfig {
82                price: 4,
83                size: 2,
84                amount: 6,
85            },
86        ),
87    ])
88});
89
90/// Get contract configuration for chain
91pub fn get_contract_config(chain_id: u64, neg_risk: bool) -> Option<ContractConfig> {
92    match (chain_id, neg_risk) {
93        (137, false) => Some(ContractConfig {
94            exchange: "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E".to_string(),
95            collateral: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174".to_string(),
96            conditional_tokens: "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045".to_string(),
97        }),
98        (137, true) => Some(ContractConfig {
99            exchange: "0xC5d563A36AE78145C45a50134d48A1215220f80a".to_string(),
100            collateral: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174".to_string(),
101            conditional_tokens: "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045".to_string(),
102        }),
103        _ => None,
104    }
105}
106
107/// Generate a random seed for order salt
108fn generate_seed() -> u64 {
109    let mut rng = rand::thread_rng();
110    let y: f64 = rng.gen();
111    let timestamp = SystemTime::now()
112        .duration_since(UNIX_EPOCH)
113        .expect("Time went backwards")
114        .as_secs();
115    (timestamp as f64 * y) as u64
116}
117
118/// Convert decimal to token units (multiply by 1e6)
119fn decimal_to_token_u32(amt: Decimal) -> u32 {
120    let mut amt = Decimal::from_scientific("1e6").expect("1e6 is not scientific") * amt;
121    if amt.scale() > 0 {
122        amt = amt.round_dp_with_strategy(0, MidpointTowardZero);
123    }
124    amt.try_into().expect("Couldn't round decimal to integer")
125}
126
127impl OrderBuilder {
128    /// Create a new order builder
129    pub fn new(
130        signer: PrivateKeySigner,
131        sig_type: Option<SigType>,
132        funder: Option<Address>,
133    ) -> Self {
134        let sig_type = sig_type.unwrap_or(SigType::Eoa);
135        let funder = funder.unwrap_or(signer.address());
136
137        OrderBuilder {
138            signer,
139            sig_type,
140            funder,
141        }
142    }
143
144    /// Get signature type as u8
145    pub fn get_sig_type(&self) -> u8 {
146        self.sig_type as u8
147    }
148
149    /// Fix amount rounding according to configuration
150    fn fix_amount_rounding(&self, mut amt: Decimal, round_config: &RoundConfig) -> Decimal {
151        if amt.scale() > round_config.amount {
152            amt = amt.round_dp_with_strategy(round_config.amount + 4, AwayFromZero);
153            if amt.scale() > round_config.amount {
154                amt = amt.round_dp_with_strategy(round_config.amount, ToZero);
155            }
156        }
157        amt
158    }
159
160    /// Get order amounts (maker and taker) for a regular order
161    fn get_order_amounts(
162        &self,
163        side: Side,
164        size: Decimal,
165        price: Decimal,
166        round_config: &RoundConfig,
167    ) -> (u32, u32) {
168        let raw_price = price.round_dp_with_strategy(round_config.price, MidpointTowardZero);
169
170        match side {
171            Side::BUY => {
172                let raw_taker_amt = size.round_dp_with_strategy(round_config.size, ToZero);
173                let raw_maker_amt = raw_taker_amt * raw_price;
174                let raw_maker_amt = self.fix_amount_rounding(raw_maker_amt, round_config);
175                (
176                    decimal_to_token_u32(raw_maker_amt),
177                    decimal_to_token_u32(raw_taker_amt),
178                )
179            },
180            Side::SELL => {
181                let raw_maker_amt = size.round_dp_with_strategy(round_config.size, ToZero);
182                let raw_taker_amt = raw_maker_amt * raw_price;
183                let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config);
184
185                (
186                    decimal_to_token_u32(raw_maker_amt),
187                    decimal_to_token_u32(raw_taker_amt),
188                )
189            },
190        }
191    }
192
193    /// Get order amounts for a market order
194    fn get_market_order_amounts(
195        &self,
196        amount: Decimal,
197        price: Decimal,
198        round_config: &RoundConfig,
199    ) -> (u32, u32) {
200        let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
201        let raw_price = price.round_dp_with_strategy(round_config.price, MidpointTowardZero);
202
203        let raw_taker_amt = raw_maker_amt / raw_price;
204        let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config);
205
206        (
207            decimal_to_token_u32(raw_maker_amt),
208            decimal_to_token_u32(raw_taker_amt),
209        )
210    }
211
212    /// Calculate market price from order book levels
213    pub fn calculate_market_price(
214        &self,
215        positions: &[crate::types::BookLevel],
216        amount_to_match: Decimal,
217    ) -> Result<Decimal> {
218        let mut sum = Decimal::ZERO;
219
220        for level in positions {
221            sum += level.size * level.price;
222            if sum >= amount_to_match {
223                return Ok(level.price);
224            }
225        }
226
227        Err(PolyfillError::order(
228            format!(
229                "Not enough liquidity to create market order with amount {}",
230                amount_to_match
231            ),
232            crate::errors::OrderErrorKind::InsufficientBalance,
233        ))
234    }
235
236    /// Create a market order
237    pub fn create_market_order(
238        &self,
239        chain_id: u64,
240        order_args: &MarketOrderArgs,
241        price: Decimal,
242        extras: &ExtraOrderArgs,
243        options: &OrderOptions,
244    ) -> Result<SignedOrderRequest> {
245        let tick_size = options
246            .tick_size
247            .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
248
249        let (maker_amount, taker_amount) =
250            self.get_market_order_amounts(order_args.amount, price, &ROUNDING_CONFIG[&tick_size]);
251
252        let neg_risk = options
253            .neg_risk
254            .ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
255
256        let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| {
257            PolyfillError::config("No contract found with given chain_id and neg_risk")
258        })?;
259
260        let exchange_address = Address::from_str(&contract_config.exchange)
261            .map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))?;
262
263        self.build_signed_order(
264            order_args.token_id.clone(),
265            Side::BUY,
266            chain_id,
267            exchange_address,
268            maker_amount,
269            taker_amount,
270            0,
271            extras,
272        )
273    }
274
275    /// Create a regular order
276    pub fn create_order(
277        &self,
278        chain_id: u64,
279        order_args: &OrderArgs,
280        expiration: u64,
281        extras: &ExtraOrderArgs,
282        options: &OrderOptions,
283    ) -> Result<SignedOrderRequest> {
284        let tick_size = options
285            .tick_size
286            .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
287
288        let (maker_amount, taker_amount) = self.get_order_amounts(
289            order_args.side,
290            order_args.size,
291            order_args.price,
292            &ROUNDING_CONFIG[&tick_size],
293        );
294
295        let neg_risk = options
296            .neg_risk
297            .ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
298
299        let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| {
300            PolyfillError::config("No contract found with given chain_id and neg_risk")
301        })?;
302
303        let exchange_address = Address::from_str(&contract_config.exchange)
304            .map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))?;
305
306        self.build_signed_order(
307            order_args.token_id.clone(),
308            order_args.side,
309            chain_id,
310            exchange_address,
311            maker_amount,
312            taker_amount,
313            expiration,
314            extras,
315        )
316    }
317
318    /// Build and sign an order
319    #[allow(clippy::too_many_arguments)]
320    fn build_signed_order(
321        &self,
322        token_id: String,
323        side: Side,
324        chain_id: u64,
325        exchange: Address,
326        maker_amount: u32,
327        taker_amount: u32,
328        expiration: u64,
329        extras: &ExtraOrderArgs,
330    ) -> Result<SignedOrderRequest> {
331        let seed = generate_seed();
332        let taker_address = Address::from_str(&extras.taker)
333            .map_err(|e| PolyfillError::validation(format!("Invalid taker address: {}", e)))?;
334
335        let u256_token_id = U256::from_str_radix(&token_id, 10)
336            .map_err(|e| PolyfillError::validation(format!("Incorrect tokenId format: {}", e)))?;
337
338        let order = crate::auth::Order {
339            salt: U256::from(seed),
340            maker: self.funder,
341            signer: self.signer.address(),
342            taker: taker_address,
343            tokenId: u256_token_id,
344            makerAmount: U256::from(maker_amount),
345            takerAmount: U256::from(taker_amount),
346            expiration: U256::from(expiration),
347            nonce: extras.nonce,
348            feeRateBps: U256::from(extras.fee_rate_bps),
349            side: side as u8,
350            signatureType: self.sig_type as u8,
351        };
352
353        let signature = sign_order_message(&self.signer, order, chain_id, exchange)?;
354
355        Ok(SignedOrderRequest {
356            salt: seed,
357            maker: self.funder.to_checksum(None),
358            signer: self.signer.address().to_checksum(None),
359            taker: taker_address.to_checksum(None),
360            token_id,
361            maker_amount: maker_amount.to_string(),
362            taker_amount: taker_amount.to_string(),
363            expiration: expiration.to_string(),
364            nonce: extras.nonce.to_string(),
365            fee_rate_bps: extras.fee_rate_bps.to_string(),
366            side: side.as_str().to_string(),
367            signature_type: self.sig_type as u8,
368            signature,
369        })
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_decimal_to_token_u32() {
379        let result = decimal_to_token_u32(Decimal::from_str("1.5").unwrap());
380        assert_eq!(result, 1_500_000);
381    }
382
383    #[test]
384    fn test_generate_seed() {
385        let seed1 = generate_seed();
386        let seed2 = generate_seed();
387        assert_ne!(seed1, seed2);
388    }
389
390    #[test]
391    fn test_decimal_to_token_u32_edge_cases() {
392        // Test zero
393        let result = decimal_to_token_u32(Decimal::ZERO);
394        assert_eq!(result, 0);
395
396        // Test small decimal
397        let result = decimal_to_token_u32(Decimal::from_str("0.000001").unwrap());
398        assert_eq!(result, 1);
399
400        // Test large number
401        let result = decimal_to_token_u32(Decimal::from_str("1000.0").unwrap());
402        assert_eq!(result, 1_000_000_000);
403    }
404
405    #[test]
406    fn test_get_contract_config() {
407        // Test Polygon mainnet
408        let config = get_contract_config(137, false);
409        assert!(config.is_some());
410
411        // Test with neg risk
412        let config_neg = get_contract_config(137, true);
413        assert!(config_neg.is_some());
414
415        // Test unsupported chain
416        let config_unsupported = get_contract_config(999, false);
417        assert!(config_unsupported.is_none());
418    }
419
420    #[test]
421    fn test_seed_generation_uniqueness() {
422        let mut seeds = std::collections::HashSet::new();
423
424        // Generate 1000 seeds and ensure they're all unique
425        for _ in 0..1000 {
426            let seed = generate_seed();
427            assert!(seeds.insert(seed), "Duplicate seed generated");
428        }
429    }
430
431    #[test]
432    fn test_seed_generation_range() {
433        for _ in 0..100 {
434            let seed = generate_seed();
435            // Seeds should be positive and within reasonable range
436            assert!(seed > 0);
437            assert!(seed < u64::MAX);
438        }
439    }
440}