Skip to main content

nautilus_polymarket/signing/
eip712.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! EIP-712 order signing for the Polymarket CTF Exchange.
17//!
18//! Orders on Polymarket are signed typed structured data (EIP-712) against the
19//! CTF Exchange contract on Polygon. Two exchange contracts exist:
20//! - [`CTF_EXCHANGE`]: Standard binary markets.
21//! - [`NEG_RISK_CTF_EXCHANGE`]: Negative-risk (multi-outcome) markets.
22//!
23//! Both share the same EIP-712 domain name and version; only the
24//! `verifyingContract` differs.
25//!
26//! This module also owns the CLOB V2 contract identities and ordered on-chain
27//! approval plan ([`approval_plan`]) used by the set-allowances binary.
28
29use std::str::FromStr;
30
31use alloy::{
32    signers::{SignerSync, local::PrivateKeySigner},
33    sol_types::{SolStruct, SolValue, eip712_domain},
34};
35use alloy_primitives::{Address, B256, Bytes, FixedBytes, U256, address, keccak256};
36#[cfg(test)]
37use nautilus_core::string::secret::SecretString;
38use rust_decimal::Decimal;
39
40use crate::{
41    common::{
42        credential::EvmPrivateKey,
43        enums::{PolymarketOrderSide, PolymarketSignatureType, PolymarketSignerType},
44    },
45    http::{
46        error::{Error, Result},
47        models::PolymarketOrder,
48    },
49};
50
51// L1 ClobAuth constants
52const CLOB_AUTH_DOMAIN_NAME: &str = "ClobAuthDomain";
53const CLOB_AUTH_DOMAIN_VERSION: &str = "1";
54const CLOB_AUTH_MESSAGE: &str = "This message attests that I control the given wallet";
55
56/// CTF Exchange contract address on Polygon mainnet (CLOB V2).
57pub const CTF_EXCHANGE: Address = address!("0xE111180000d2663C0091e4f400237545B87B996B");
58
59/// Neg Risk CTF Exchange contract address on Polygon mainnet (CLOB V2).
60pub const NEG_RISK_CTF_EXCHANGE: Address = address!("0xe2222d279d744050d28e00520010520000310F59");
61
62/// Standard CTF collateral adapter address on Polygon mainnet.
63pub const CTF_COLLATERAL_ADAPTER: Address = address!("0xAdA100Db00Ca00073811820692005400218FcE1f");
64
65/// Neg Risk CTF collateral adapter address on Polygon mainnet.
66pub const NEG_RISK_CTF_COLLATERAL_ADAPTER: Address =
67    address!("0xadA2005600Dec949baf300f4C6120000bDB6eAab");
68
69/// Deposit Wallet factory address on Polygon mainnet.
70pub const DEPOSIT_WALLET_FACTORY: Address = address!("0x00000000000Fb5C9ADea0298D729A0CB3823Cc07");
71
72/// Polymarket pUSD collateral token contract address on Polygon mainnet.
73pub const POLYMARKET_COLLATERAL_TOKEN: Address =
74    address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB");
75
76/// Conditional Tokens Framework contract address on Polygon mainnet.
77pub const CONDITIONAL_TOKENS: Address = address!("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045");
78
79/// Complete spender set requiring collateral approval for Polymarket CLOB V2 orders.
80pub const COLLATERAL_APPROVAL_TARGETS: &[Address] = &[
81    CTF_EXCHANGE,
82    NEG_RISK_CTF_EXCHANGE,
83    NEG_RISK_CTF_COLLATERAL_ADAPTER,
84];
85
86/// One transaction in the Polymarket approval plan.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum PolymarketApproval {
89    /// Approves a contract to spend pUSD collateral.
90    Collateral {
91        /// Collateral token contract receiving the approval call.
92        contract: Address,
93        /// Contract receiving the collateral allowance.
94        spender: Address,
95        /// Collateral allowance amount.
96        amount: U256,
97    },
98    /// Enables a contract as an operator for conditional tokens.
99    ConditionalTokens {
100        /// Conditional Tokens contract receiving the operator call.
101        contract: Address,
102        /// Contract receiving conditional-token operator authority.
103        operator: Address,
104        /// Whether operator authority is enabled.
105        approved: bool,
106    },
107}
108
109/// Returns the ordered approval plan for Polymarket CLOB V2.
110pub fn approval_plan() -> impl Iterator<Item = PolymarketApproval> {
111    COLLATERAL_APPROVAL_TARGETS
112        .iter()
113        .copied()
114        .flat_map(|target| {
115            [
116                PolymarketApproval::Collateral {
117                    contract: POLYMARKET_COLLATERAL_TOKEN,
118                    spender: target,
119                    amount: U256::MAX,
120                },
121                PolymarketApproval::ConditionalTokens {
122                    contract: CONDITIONAL_TOKENS,
123                    operator: target,
124                    approved: true,
125                },
126            ]
127        })
128}
129
130const DOMAIN_NAME: &str = "Polymarket CTF Exchange";
131const DOMAIN_VERSION: &str = "2";
132const POLYGON_CHAIN_ID: u64 = 137;
133const ORDER_TYPE_STRING: &str = concat!(
134    "Order(uint256 salt,address maker,address signer,uint256 tokenId,",
135    "uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
136    "uint256 timestamp,bytes32 metadata,bytes32 builder)",
137);
138const SOLADY_TYPE_STRING: &str = concat!(
139    "TypedDataSign(Order contents,string name,string version,uint256 chainId,",
140    "address verifyingContract,bytes32 salt)",
141    "Order(uint256 salt,address maker,address signer,uint256 tokenId,",
142    "uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
143    "uint256 timestamp,bytes32 metadata,bytes32 builder)",
144);
145const DOMAIN_TYPE_STRING: &str =
146    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
147const DEPOSIT_WALLET_DOMAIN_NAME: &str = "DepositWallet";
148const DEPOSIT_WALLET_DOMAIN_VERSION: &str = "1";
149
150// EIP-712 ClobAuth struct for L1 API authentication.
151//
152// Reference: <https://docs.polymarket.com/api-reference/authentication#l1-authentication>
153alloy::sol! {
154    struct ClobAuth {
155        address address;
156        string timestamp;
157        uint256 nonce;
158        string message;
159    }
160}
161
162// EIP-712 Deposit Wallet batch authorization.
163//
164// Reference: <https://docs.polymarket.com/trading/wallets-auth#execute-gasless-transactions>
165alloy::sol! {
166    struct Call {
167        address target;
168        uint256 value;
169        bytes data;
170    }
171
172    struct Batch {
173        address wallet;
174        uint256 nonce;
175        uint256 deadline;
176        Call[] calls;
177    }
178}
179
180// EIP-712 Order struct for CLOB V2 CTFExchange.
181//
182// Fees are set by the protocol at match time (not signed) and per-address
183// uniqueness comes from `timestamp` (milliseconds) rather than `nonce`.
184alloy::sol! {
185    struct Order {
186        uint256 salt;
187        address maker;
188        address signer;
189        uint256 tokenId;
190        uint256 makerAmount;
191        uint256 takerAmount;
192        uint8 side;
193        uint8 signatureType;
194        uint256 timestamp;
195        bytes32 metadata;
196        bytes32 builder;
197    }
198}
199
200/// EIP-712 order signer for the Polymarket CTF Exchange.
201#[derive(Debug)]
202pub struct OrderSigner {
203    signer: PrivateKeySigner,
204    signer_type: PolymarketSignerType,
205}
206
207impl OrderSigner {
208    /// Creates a new [`OrderSigner`] from an EVM private key.
209    pub fn new(private_key: &EvmPrivateKey) -> Result<Self> {
210        let key_hex = private_key
211            .as_hex()
212            .strip_prefix("0x")
213            .unwrap_or(private_key.as_hex());
214        let signer = PrivateKeySigner::from_str(key_hex)
215            .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
216        Ok(Self {
217            signer,
218            signer_type: PolymarketSignerType::Owner,
219        })
220    }
221
222    /// Selects the signer role. Session signing requires `POLY_1271` orders.
223    #[must_use]
224    pub fn with_signer_type(mut self, signer_type: PolymarketSignerType) -> Self {
225        self.signer_type = signer_type;
226        self
227    }
228
229    /// Returns the signer's Ethereum address.
230    #[must_use]
231    pub fn address(&self) -> Address {
232        self.signer.address()
233    }
234
235    /// Signs a [`PolymarketOrder`] and returns the hex-encoded ECDSA signature.
236    ///
237    /// The `neg_risk` flag selects which exchange contract to use as the
238    /// EIP-712 `verifyingContract`.
239    ///
240    /// # Errors
241    ///
242    /// Returns an error if a non-`POLY_1271` order signer does not match this
243    /// signer's address, or if a `POLY_1271` order does not use the deposit
244    /// wallet for both `maker` and `signer`.
245    pub fn sign_order(&self, order: &PolymarketOrder, neg_risk: bool) -> Result<String> {
246        if self.signer_type == PolymarketSignerType::Session
247            && order.signature_type != PolymarketSignatureType::Poly1271
248        {
249            return Err(Error::bad_request(
250                "Session signers require POLY_1271 orders",
251            ));
252        }
253
254        let order_signer = parse_address(&order.signer, "signer")?;
255        let order_maker = parse_address(&order.maker, "maker")?;
256        if order.signature_type == PolymarketSignatureType::Poly1271 {
257            if order_signer != order_maker {
258                return Err(Error::bad_request(format!(
259                    "POLY_1271 orders require maker and signer to both be the deposit wallet, maker was {order_maker}, signer was {order_signer}",
260                )));
261            }
262        } else if order_signer != self.signer.address() {
263            return Err(Error::bad_request(format!(
264                "Order signer {order_signer} does not match local signer {}",
265                self.signer.address(),
266            )));
267        }
268
269        let eip712_order = build_eip712_order(order)?;
270        let contract = exchange_contract(neg_risk);
271
272        if order.signature_type == PolymarketSignatureType::Poly1271 {
273            return self.sign_poly_1271_order(&eip712_order, contract);
274        }
275
276        let domain = eip712_domain! {
277            name: DOMAIN_NAME,
278            version: DOMAIN_VERSION,
279            chain_id: POLYGON_CHAIN_ID,
280            verifying_contract: contract,
281        };
282
283        let signing_hash = eip712_order.eip712_signing_hash(&domain);
284        self.sign_hash(&signing_hash.0)
285    }
286
287    fn sign_poly_1271_order(&self, order: &Order, contract: Address) -> Result<String> {
288        let contents_hash = poly_1271_contents_hash(order);
289        let wallet_struct_hash = poly_1271_wallet_struct_hash(contents_hash, order.signer);
290        let app_domain_separator = ctf_exchange_domain_separator(contract);
291        let signing_hash = typed_data_hash(app_domain_separator, wallet_struct_hash);
292        let signature = self.sign_hash_b256(&signing_hash)?;
293
294        let mut encoded =
295            Vec::with_capacity(65 + 32 + 32 + ORDER_TYPE_STRING.len() + std::mem::size_of::<u16>());
296        encoded.extend_from_slice(&signature);
297        encoded.extend_from_slice(app_domain_separator.as_slice());
298        encoded.extend_from_slice(contents_hash.as_slice());
299        encoded.extend_from_slice(ORDER_TYPE_STRING.as_bytes());
300        encoded.extend_from_slice(&(ORDER_TYPE_STRING.len() as u16).to_be_bytes());
301
302        if self.signer_type == PolymarketSignerType::Session {
303            // The session envelope wraps the complete Deposit Wallet signature.
304            let signer_id = B256::left_padding_from(self.address().as_slice());
305            encoded = (signer_id, B256::ZERO, Bytes::from(encoded)).abi_encode_params();
306            encoded.extend_from_slice(&[0x64, 0x92].repeat(16));
307        }
308
309        Ok(format!(
310            "0x{}",
311            alloy_primitives::hex::encode(encoded.as_slice())
312        ))
313    }
314
315    /// Signs a Deposit Wallet `Batch` and returns the hex-encoded ECDSA signature.
316    ///
317    /// # Errors
318    ///
319    /// Returns an error if `calls` is empty or signing fails.
320    pub fn sign_deposit_wallet_batch(
321        &self,
322        deposit_wallet: Address,
323        nonce: U256,
324        deadline: U256,
325        calls: &[DepositWalletCall],
326    ) -> Result<String> {
327        if calls.is_empty() {
328            return Err(Error::bad_request(
329                "Deposit Wallet batch must contain at least one call",
330            ));
331        }
332
333        let batch = Batch {
334            wallet: deposit_wallet,
335            nonce,
336            deadline,
337            calls: calls
338                .iter()
339                .map(|call| Call {
340                    target: call.target,
341                    value: call.value,
342                    data: call.data.clone(),
343                })
344                .collect(),
345        };
346
347        let domain = eip712_domain! {
348            name: DEPOSIT_WALLET_DOMAIN_NAME,
349            version: DEPOSIT_WALLET_DOMAIN_VERSION,
350            chain_id: POLYGON_CHAIN_ID,
351            verifying_contract: deposit_wallet,
352        };
353        let signing_hash = batch.eip712_signing_hash(&domain);
354        self.sign_hash(&signing_hash.0)
355    }
356
357    fn sign_hash_b256(&self, hash: &B256) -> Result<[u8; 65]> {
358        let signature = self
359            .signer
360            .sign_hash_sync(hash)
361            .map_err(|e| Error::bad_request(format!("Failed to sign order: {e}")))?;
362        Ok(signature.as_bytes())
363    }
364
365    fn sign_hash(&self, hash: &[u8; 32]) -> Result<String> {
366        let hash_b256 = B256::from(*hash);
367        let signature = self.sign_hash_b256(&hash_b256)?;
368        Ok(format!(
369            "0x{}",
370            alloy_primitives::hex::encode(signature.as_slice())
371        ))
372    }
373}
374
375/// One contract call in a Deposit Wallet batch.
376#[derive(Clone, Debug, PartialEq, Eq)]
377pub struct DepositWalletCall {
378    /// Contract receiving the call.
379    pub target: Address,
380    /// Native POL value in wei.
381    pub value: U256,
382    /// ABI-encoded calldata.
383    pub data: Bytes,
384}
385
386/// Computes the EIP-712 signing hash used by Polymarket as the order ID.
387///
388/// The `neg_risk` flag selects which exchange contract to use as the
389/// EIP-712 `verifyingContract`.
390pub fn order_hash(order: &PolymarketOrder, neg_risk: bool) -> Result<B256> {
391    let eip712_order = build_eip712_order(order)?;
392    let contract = exchange_contract(neg_risk);
393
394    let domain = eip712_domain! {
395        name: DOMAIN_NAME,
396        version: DOMAIN_VERSION,
397        chain_id: POLYGON_CHAIN_ID,
398        verifying_contract: contract,
399    };
400
401    Ok(eip712_order.eip712_signing_hash(&domain))
402}
403
404const fn exchange_contract(neg_risk: bool) -> Address {
405    if neg_risk {
406        NEG_RISK_CTF_EXCHANGE
407    } else {
408        CTF_EXCHANGE
409    }
410}
411
412fn order_type_hash() -> B256 {
413    keccak256(ORDER_TYPE_STRING.as_bytes())
414}
415
416fn solady_type_hash() -> B256 {
417    keccak256(SOLADY_TYPE_STRING.as_bytes())
418}
419
420fn domain_type_hash() -> B256 {
421    keccak256(DOMAIN_TYPE_STRING.as_bytes())
422}
423
424fn domain_name_hash() -> B256 {
425    keccak256(DOMAIN_NAME.as_bytes())
426}
427
428fn domain_version_hash() -> B256 {
429    keccak256(DOMAIN_VERSION.as_bytes())
430}
431
432fn deposit_wallet_name_hash() -> B256 {
433    keccak256(DEPOSIT_WALLET_DOMAIN_NAME.as_bytes())
434}
435
436fn deposit_wallet_version_hash() -> B256 {
437    keccak256(DEPOSIT_WALLET_DOMAIN_VERSION.as_bytes())
438}
439
440fn poly_1271_contents_hash(order: &Order) -> B256 {
441    let tuple = (
442        order_type_hash(),
443        order.salt,
444        order.maker,
445        order.signer,
446        order.tokenId,
447        order.makerAmount,
448        order.takerAmount,
449        U256::from(order.side),
450        U256::from(order.signatureType),
451        order.timestamp,
452        order.metadata,
453        order.builder,
454    );
455    keccak256(tuple.abi_encode())
456}
457
458fn poly_1271_wallet_struct_hash(contents_hash: B256, deposit_wallet: Address) -> B256 {
459    let tuple = (
460        solady_type_hash(),
461        contents_hash,
462        deposit_wallet_name_hash(),
463        deposit_wallet_version_hash(),
464        U256::from(POLYGON_CHAIN_ID),
465        deposit_wallet,
466        FixedBytes::<32>::ZERO,
467    );
468    keccak256(tuple.abi_encode())
469}
470
471fn ctf_exchange_domain_separator(contract: Address) -> B256 {
472    let tuple = (
473        domain_type_hash(),
474        domain_name_hash(),
475        domain_version_hash(),
476        U256::from(POLYGON_CHAIN_ID),
477        contract,
478    );
479    keccak256(tuple.abi_encode())
480}
481
482fn typed_data_hash(domain_separator: B256, struct_hash: B256) -> B256 {
483    let mut bytes = Vec::with_capacity(2 + 32 + 32);
484    bytes.push(0x19);
485    bytes.push(0x01);
486    bytes.extend_from_slice(domain_separator.as_slice());
487    bytes.extend_from_slice(struct_hash.as_slice());
488    keccak256(bytes)
489}
490
491/// Signs a ClobAuth EIP-712 message for L1 API authentication.
492///
493/// Used to create or derive API credentials via the CLOB `/auth/api-key`
494/// and `/auth/derive-api-key` endpoints.
495///
496/// Returns `(signer_address_hex, signature_hex)`.
497pub fn sign_clob_auth(
498    private_key: &EvmPrivateKey,
499    timestamp: &str,
500    nonce: u64,
501) -> Result<(String, String)> {
502    let key_hex = private_key
503        .as_hex()
504        .strip_prefix("0x")
505        .unwrap_or(private_key.as_hex());
506    let signer = PrivateKeySigner::from_str(key_hex)
507        .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
508
509    let address = signer.address();
510
511    let auth = ClobAuth {
512        address,
513        timestamp: timestamp.to_string(),
514        nonce: U256::from(nonce),
515        message: CLOB_AUTH_MESSAGE.to_string(),
516    };
517
518    let domain = eip712_domain! {
519        name: CLOB_AUTH_DOMAIN_NAME,
520        version: CLOB_AUTH_DOMAIN_VERSION,
521        chain_id: POLYGON_CHAIN_ID,
522    };
523
524    let signing_hash = auth.eip712_signing_hash(&domain);
525    let signature = signer
526        .sign_hash_sync(&signing_hash)
527        .map_err(|e| Error::bad_request(format!("Failed to sign ClobAuth: {e}")))?;
528
529    let r = signature.r();
530    let s = signature.s();
531    let v = if signature.v() { 28u8 } else { 27u8 };
532
533    Ok((
534        format!("{address:#x}"),
535        format!("0x{r:064x}{s:064x}{v:02x}"),
536    ))
537}
538
539// Converts a PolymarketOrder to the EIP-712 Order struct
540fn build_eip712_order(order: &PolymarketOrder) -> Result<Order> {
541    Ok(Order {
542        salt: U256::from(order.salt),
543        maker: parse_address(&order.maker, "maker")?,
544        signer: parse_address(&order.signer, "signer")?,
545        tokenId: U256::from_str(order.token_id.as_str())
546            .map_err(|e| Error::bad_request(format!("Invalid token ID: {e}")))?,
547        makerAmount: decimal_to_u256(order.maker_amount, "maker_amount")?,
548        takerAmount: decimal_to_u256(order.taker_amount, "taker_amount")?,
549        side: order_side_to_u8(order.side),
550        signatureType: order.signature_type as u8,
551        timestamp: U256::from_str(&order.timestamp)
552            .map_err(|e| Error::bad_request(format!("Invalid timestamp: {e}")))?,
553        metadata: parse_bytes32(&order.metadata, "metadata")?,
554        builder: parse_bytes32(&order.builder, "builder")?,
555    })
556}
557
558pub(crate) fn parse_address(addr: &str, field: &str) -> Result<Address> {
559    Address::from_str(addr).map_err(|e| Error::bad_request(format!("Invalid {field} address: {e}")))
560}
561
562pub(crate) fn parse_bytes32(value: &str, field: &str) -> Result<FixedBytes<32>> {
563    FixedBytes::<32>::from_str(value)
564        .map_err(|e| Error::bad_request(format!("Invalid {field} bytes32: {e}")))
565}
566
567fn decimal_to_u256(d: Decimal, field: &str) -> Result<U256> {
568    let normalized = d.normalize();
569    if normalized.scale() != 0 {
570        return Err(Error::bad_request(format!("{field} must be an integer")));
571    }
572    let mantissa = normalized.mantissa();
573    if mantissa < 0 {
574        return Err(Error::bad_request(format!("{field} must be non-negative")));
575    }
576    Ok(U256::from(mantissa as u128))
577}
578
579fn order_side_to_u8(side: PolymarketOrderSide) -> u8 {
580    match side {
581        PolymarketOrderSide::Buy => 0,
582        PolymarketOrderSide::Sell => 1,
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use alloy_primitives::{Signature, keccak256};
589    use nautilus_core::hex;
590    use rstest::rstest;
591    use rust_decimal_macros::dec;
592    use ustr::Ustr;
593
594    use super::*;
595    use crate::common::enums::PolymarketSignatureType;
596
597    const TEST_PRIVATE_KEY: &str =
598        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
599
600    fn test_signer() -> OrderSigner {
601        let pk = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
602        OrderSigner::new(&pk).unwrap()
603    }
604
605    const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
606
607    fn test_order() -> PolymarketOrder {
608        PolymarketOrder {
609            salt: 123456789,
610            maker: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
611            signer: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
612            token_id: Ustr::from(
613                "71321045679252212594626385532706912750332728571942532289631379312455583992563",
614            ),
615            maker_amount: dec!(100000000),
616            taker_amount: dec!(50000000),
617            side: PolymarketOrderSide::Buy,
618            signature_type: PolymarketSignatureType::Eoa,
619            expiration: "0".to_string(),
620            timestamp: "1713398400000".to_string(),
621            metadata: ZERO_BYTES32.to_string(),
622            builder: ZERO_BYTES32.to_string(),
623            signature: SecretString::default(),
624        }
625    }
626
627    #[rstest]
628    fn test_order_typehash_matches_contract() {
629        // ORDER_TYPEHASH from the CLOB V2 CTFExchange contract
630        let expected = keccak256(
631            "Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)",
632        );
633        let order = test_order();
634        let eip712_order = build_eip712_order(&order).unwrap();
635        assert_eq!(eip712_order.eip712_type_hash(), expected);
636    }
637
638    #[rstest]
639    fn test_signer_debug_redacts_private_key() {
640        let signer = test_signer();
641        let debug = format!("{signer:?}");
642
643        assert!(debug.contains(&format!("{:#x}", signer.address())));
644        assert!(!debug.contains(TEST_PRIVATE_KEY.trim_start_matches("0x")));
645    }
646
647    #[rstest]
648    fn test_signer_address_derivation() {
649        let signer = test_signer();
650        // Hardhat account #0
651        let expected = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
652        assert_eq!(signer.address(), expected);
653    }
654
655    #[rstest]
656    fn test_sign_clob_auth_matches_polymarket_mainnet_vector() {
657        let private_key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
658
659        let (address, signature) = sign_clob_auth(&private_key, "10000000", 23).unwrap();
660
661        // https://github.com/Polymarket/clob-client/blob/7df8257dc95f99edb257b53a7873e273a9b4a9b3/src/signing/eip712.ts
662        assert_eq!(address, "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266");
663        assert_eq!(
664            signature,
665            "0x1a7118db6100dfd8efd102be36f472b59475dcac56eb4c9a2a94748d3655ba7c3c89deb8c19ee79eceb0a531122fbfbe88ed118034f9d8212e2b725e7b296b9d1c",
666        );
667    }
668
669    #[rstest]
670    fn test_sign_order_format() {
671        let signer = test_signer();
672        let order = test_order();
673
674        let sig = signer.sign_order(&order, false).unwrap();
675
676        assert!(sig.starts_with("0x"));
677        assert_eq!(sig.len(), 132); // 0x + r(64) + s(64) + v(2)
678    }
679
680    #[rstest]
681    fn test_sign_poly_1271_order_format() {
682        let signer = test_signer();
683        let mut order = test_order();
684        order.maker = "0x1111111111111111111111111111111111111111".to_string();
685        order.signer = order.maker.clone();
686        order.signature_type = PolymarketSignatureType::Poly1271;
687
688        let sig = signer.sign_order(&order, false).unwrap();
689
690        assert!(sig.starts_with("0x"));
691        assert_eq!(sig.len(), 636);
692    }
693
694    #[rstest]
695    fn test_sign_poly_1271_order_requires_deposit_wallet_signer() {
696        let signer = test_signer();
697        let mut order = test_order();
698        order.maker = "0x1111111111111111111111111111111111111111".to_string();
699        order.signature_type = PolymarketSignatureType::Poly1271;
700
701        let err = signer.sign_order(&order, false).unwrap_err();
702
703        assert!(
704            err.to_string()
705                .contains("maker and signer to both be the deposit wallet")
706        );
707    }
708
709    #[rstest]
710    fn test_sign_order_deterministic() {
711        let signer = test_signer();
712        let order = test_order();
713
714        let sig1 = signer.sign_order(&order, false).unwrap();
715        let sig2 = signer.sign_order(&order, false).unwrap();
716        assert_eq!(sig1, sig2);
717    }
718
719    #[rstest]
720    fn test_sign_deposit_wallet_batch_format_and_recovery() {
721        let signer = test_signer();
722        let deposit_wallet = address!("0x1111111111111111111111111111111111111111");
723
724        let calls = [DepositWalletCall {
725            target: CTF_COLLATERAL_ADAPTER,
726            value: U256::ZERO,
727            data: Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
728        }];
729
730        let sig = signer
731            .sign_deposit_wallet_batch(
732                deposit_wallet,
733                U256::from(7u64),
734                U256::from(1_714_000_000u64),
735                &calls,
736            )
737            .unwrap();
738
739        assert!(sig.starts_with("0x"));
740        assert_eq!(sig.len(), 132);
741
742        let batch = Batch {
743            wallet: deposit_wallet,
744            nonce: U256::from(7u64),
745            deadline: U256::from(1_714_000_000u64),
746            calls: vec![Call {
747                target: CTF_COLLATERAL_ADAPTER,
748                value: U256::ZERO,
749                data: Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
750            }],
751        };
752
753        let domain = eip712_domain! {
754            name: DEPOSIT_WALLET_DOMAIN_NAME,
755            version: DEPOSIT_WALLET_DOMAIN_VERSION,
756            chain_id: POLYGON_CHAIN_ID,
757            verifying_contract: deposit_wallet,
758        };
759        let signing_hash = batch.eip712_signing_hash(&domain);
760        let recovered = Signature::from_str(&sig)
761            .unwrap()
762            .recover_address_from_prehash(&signing_hash)
763            .unwrap();
764        assert_eq!(recovered, signer.address());
765    }
766
767    #[rstest]
768    fn test_sign_deposit_wallet_batch_rejects_empty_calls() {
769        let signer = test_signer();
770        let err = signer
771            .sign_deposit_wallet_batch(
772                address!("0x1111111111111111111111111111111111111111"),
773                U256::from(1u64),
774                U256::from(1_714_000_000u64),
775                &[],
776            )
777            .unwrap_err();
778        assert!(
779            err.to_string()
780                .contains("Deposit Wallet batch must contain at least one call")
781        );
782    }
783
784    #[rstest]
785    fn test_sign_order_neg_risk_differs() {
786        let signer = test_signer();
787        let order = test_order();
788
789        let sig_normal = signer.sign_order(&order, false).unwrap();
790        let sig_neg_risk = signer.sign_order(&order, true).unwrap();
791        assert_ne!(sig_normal, sig_neg_risk);
792    }
793
794    #[rstest]
795    fn test_sign_order_sell_side() {
796        let signer = test_signer();
797        let mut order = test_order();
798        let sig_buy = signer.sign_order(&order, false).unwrap();
799
800        order.side = PolymarketOrderSide::Sell;
801        let sig_sell = signer.sign_order(&order, false).unwrap();
802        assert_ne!(sig_buy, sig_sell);
803    }
804
805    #[rstest]
806    fn test_sign_order_different_amounts() {
807        let signer = test_signer();
808        let mut order = test_order();
809        let sig1 = signer.sign_order(&order, false).unwrap();
810
811        order.maker_amount = dec!(200000000);
812        let sig2 = signer.sign_order(&order, false).unwrap();
813        assert_ne!(sig1, sig2);
814    }
815
816    #[rstest]
817    fn test_build_eip712_order() {
818        let order = test_order();
819        let eip712 = build_eip712_order(&order).unwrap();
820
821        assert_eq!(eip712.salt, U256::from(123456789u64));
822        assert_eq!(
823            eip712.maker,
824            Address::from_str("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
825        );
826        assert_eq!(eip712.makerAmount, U256::from(100000000u128));
827        assert_eq!(eip712.takerAmount, U256::from(50000000u128));
828        assert_eq!(eip712.side, 0); // BUY
829        assert_eq!(eip712.signatureType, 0); // EOA
830        assert_eq!(eip712.timestamp, U256::from(1713398400000u128));
831        assert_eq!(eip712.metadata, FixedBytes::<32>::ZERO);
832        assert_eq!(eip712.builder, FixedBytes::<32>::ZERO);
833    }
834
835    #[rstest]
836    fn test_build_eip712_order_with_builder_code() {
837        let mut order = test_order();
838        order.builder =
839            "0x0000000000000000000000000000000000000000000000000000000000000001".to_string();
840        let eip712 = build_eip712_order(&order).unwrap();
841
842        let mut expected = [0u8; 32];
843        expected[31] = 1;
844        assert_eq!(eip712.builder, FixedBytes::<32>::from(expected));
845    }
846
847    #[rstest]
848    fn test_decimal_to_u256_integer() {
849        let result = decimal_to_u256(dec!(100000000), "test").unwrap();
850        assert_eq!(result, U256::from(100000000u128));
851    }
852
853    #[rstest]
854    fn test_decimal_to_u256_zero() {
855        let result = decimal_to_u256(dec!(0), "test").unwrap();
856        assert_eq!(result, U256::ZERO);
857    }
858
859    #[rstest]
860    fn test_decimal_to_u256_rejects_fractional() {
861        let result = decimal_to_u256(dec!(100.5), "test");
862        assert!(result.is_err());
863    }
864
865    #[rstest]
866    fn test_decimal_to_u256_rejects_negative() {
867        let result = decimal_to_u256(dec!(-1), "test");
868        assert!(result.is_err());
869    }
870
871    #[rstest]
872    fn test_order_side_mapping() {
873        assert_eq!(order_side_to_u8(PolymarketOrderSide::Buy), 0);
874        assert_eq!(order_side_to_u8(PolymarketOrderSide::Sell), 1);
875    }
876
877    #[rstest]
878    fn test_contract_addresses_nonzero() {
879        assert_ne!(CTF_EXCHANGE, Address::ZERO);
880        assert_ne!(NEG_RISK_CTF_EXCHANGE, Address::ZERO);
881        assert_ne!(CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE);
882    }
883
884    #[rstest]
885    fn test_v2_contract_addresses_pinned() {
886        // Pin the V2 contract addresses so a revert to V1 is caught by unit tests.
887        // V1 addresses (must NOT match): 0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E,
888        // 0xC5d563A36AE78145C45a50134d48A1215220f80a.
889        assert_eq!(
890            format!("{CTF_EXCHANGE:#x}"),
891            "0xe111180000d2663c0091e4f400237545b87b996b"
892        );
893        assert_eq!(
894            format!("{NEG_RISK_CTF_EXCHANGE:#x}"),
895            "0xe2222d279d744050d28e00520010520000310f59"
896        );
897        assert_eq!(
898            format!("{CTF_COLLATERAL_ADAPTER:#x}"),
899            "0xada100db00ca00073811820692005400218fce1f"
900        );
901        assert_eq!(
902            format!("{NEG_RISK_CTF_COLLATERAL_ADAPTER:#x}"),
903            "0xada2005600dec949baf300f4c6120000bdb6eaab"
904        );
905        assert_eq!(
906            format!("{DEPOSIT_WALLET_FACTORY:#x}"),
907            "0x00000000000fb5c9adea0298d729a0cb3823cc07"
908        );
909        assert_eq!(
910            format!("{POLYMARKET_COLLATERAL_TOKEN:#x}"),
911            "0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb"
912        );
913        assert_eq!(
914            format!("{CONDITIONAL_TOKENS:#x}"),
915            "0x4d97dcd97ec945f40cf65f87097ace5ea0476045"
916        );
917        assert_eq!(
918            COLLATERAL_APPROVAL_TARGETS,
919            &[
920                CTF_EXCHANGE,
921                NEG_RISK_CTF_EXCHANGE,
922                NEG_RISK_CTF_COLLATERAL_ADAPTER,
923            ]
924        );
925    }
926
927    #[rstest]
928    fn test_approval_plan() {
929        let collateral_token = address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB");
930        let conditional_tokens = address!("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045");
931        let ctf_exchange = address!("0xE111180000d2663C0091e4f400237545B87B996B");
932        let neg_risk_ctf_exchange = address!("0xe2222d279d744050d28e00520010520000310F59");
933        let neg_risk_collateral_adapter = address!("0xadA2005600Dec949baf300f4C6120000bDB6eAab");
934        let expected = vec![
935            PolymarketApproval::Collateral {
936                contract: collateral_token,
937                spender: ctf_exchange,
938                amount: U256::MAX,
939            },
940            PolymarketApproval::ConditionalTokens {
941                contract: conditional_tokens,
942                operator: ctf_exchange,
943                approved: true,
944            },
945            PolymarketApproval::Collateral {
946                contract: collateral_token,
947                spender: neg_risk_ctf_exchange,
948                amount: U256::MAX,
949            },
950            PolymarketApproval::ConditionalTokens {
951                contract: conditional_tokens,
952                operator: neg_risk_ctf_exchange,
953                approved: true,
954            },
955            PolymarketApproval::Collateral {
956                contract: collateral_token,
957                spender: neg_risk_collateral_adapter,
958                amount: U256::MAX,
959            },
960            PolymarketApproval::ConditionalTokens {
961                contract: conditional_tokens,
962                operator: neg_risk_collateral_adapter,
963                approved: true,
964            },
965        ];
966
967        assert_eq!(approval_plan().collect::<Vec<_>>(), expected);
968    }
969
970    #[rstest]
971    fn test_domain_version_is_v2() {
972        // Domain version is embedded in the EIP-712 signing hash; a revert to
973        // "1" would silently break V2 order acceptance.
974        assert_eq!(DOMAIN_VERSION, "2");
975    }
976
977    #[rstest]
978    fn test_sign_order_recoverable() {
979        let signer = test_signer();
980        let order = test_order();
981        let sig_hex = signer.sign_order(&order, false).unwrap();
982
983        let sig_bytes = hex::decode(&sig_hex[2..]).unwrap();
984        assert_eq!(sig_bytes.len(), 65);
985
986        let r = U256::from_be_slice(&sig_bytes[..32]);
987        let s = U256::from_be_slice(&sig_bytes[32..64]);
988        let v = sig_bytes[64];
989        let y_parity = v == 28;
990
991        let signature = Signature::new(r, s, y_parity);
992
993        let eip712_order = build_eip712_order(&order).unwrap();
994        let domain = eip712_domain! {
995            name: DOMAIN_NAME,
996            version: DOMAIN_VERSION,
997            chain_id: POLYGON_CHAIN_ID,
998            verifying_contract: CTF_EXCHANGE,
999        };
1000        let signing_hash = eip712_order.eip712_signing_hash(&domain);
1001
1002        let recovered = signature
1003            .recover_address_from_prehash(&signing_hash)
1004            .unwrap();
1005        assert_eq!(recovered, signer.address());
1006    }
1007
1008    // Reference vectors generated with `py_clob_client_v2==1.0.0`'s
1009    // `ExchangeOrderBuilderV2`. Same test private key (Hardhat account #0),
1010    // same contract addresses and chain id. Locks our EIP-712 hash + ECDSA
1011    // signature output to the SDK's, so drift between the two signers (domain
1012    // typo, struct field reorder, bytes32 padding, etc.) is caught locally
1013    // before orders get sent to the venue.
1014    const PARITY_TOKEN_ID: &str =
1015        "71321045679252212594626385532706912750332728571942532289631379312455583992563";
1016
1017    fn parity_order(
1018        salt: u64,
1019        side: PolymarketOrderSide,
1020        signature_type: PolymarketSignatureType,
1021        maker_amount: Decimal,
1022        taker_amount: Decimal,
1023        timestamp: &str,
1024        builder: &str,
1025    ) -> PolymarketOrder {
1026        PolymarketOrder {
1027            salt,
1028            maker: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
1029            signer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
1030            token_id: Ustr::from(PARITY_TOKEN_ID),
1031            maker_amount,
1032            taker_amount,
1033            side,
1034            signature_type,
1035            expiration: "0".to_string(),
1036            timestamp: timestamp.to_string(),
1037            metadata: ZERO_BYTES32.to_string(),
1038            builder: builder.to_string(),
1039            signature: SecretString::default(),
1040        }
1041    }
1042
1043    #[rstest]
1044    #[case::buy_standard_eoa(
1045        parity_order(
1046            123456789,
1047            PolymarketOrderSide::Buy,
1048            PolymarketSignatureType::Eoa,
1049            dec!(100000000),
1050            dec!(50000000),
1051            "1713398400000",
1052            ZERO_BYTES32,
1053        ),
1054        false,
1055        "0x32961c48ddac87ed3582f8e02097cd0eff4fcf80460306bd44b3710438dfa64c",
1056        "0x89f178136333c8ebb32a19146cb891233e3202d474be6ef730c24dbc06ae4d2a0c99948a86d9b57de0f2c0bb8ec6964aa244ecf17cfa3a95b86878d0b64ad78a1b",
1057    )]
1058    #[case::sell_neg_risk_eoa(
1059        parity_order(
1060            987654321,
1061            PolymarketOrderSide::Sell,
1062            PolymarketSignatureType::Eoa,
1063            dec!(50000000),
1064            dec!(100000000),
1065            "1713398400000",
1066            ZERO_BYTES32,
1067        ),
1068        true,
1069        "0x8b878404bd92dea2bfea9975c9fcd816ec70a57ae431cb20d67bb773744aaef3",
1070        "0xf7d60d64364e2615b08d9f69f3ea9afd3b4f83ecfbf05ddd3ca83f4916277fb97d2d080758d17c8e91c928aceb8ff252477ebaec051b672832500f63b4d36b061c",
1071    )]
1072    #[case::buy_with_builder_code_eoa(
1073        parity_order(
1074            1,
1075            PolymarketOrderSide::Buy,
1076            PolymarketSignatureType::Eoa,
1077            dec!(100000000),
1078            dec!(50000000),
1079            "1713398500000",
1080            "0x0000000000000000000000000000000000000000000000000000000000000001",
1081        ),
1082        false,
1083        "0x3df0b6f6ddfca837bc36964cae968b34ad35640b5d98f557c104da97e804e36a",
1084        "0xf4d2b34659e8bc07a9572d40ee5a1639a1157409613b4c21566b1f33fd8fe11a364b3f306668cae7248ca7cdf72378f9266bc5628585aa939644400030671e081c",
1085    )]
1086    #[case::buy_poly_proxy(
1087        // V2 unblocks EIP-1271 smart contract wallet signing. signatureType
1088        // enters the typed-data hash directly, so a regression that only
1089        // manifests for proxy/safe wallets is undetectable from the Eoa
1090        // cases above.
1091        parity_order(
1092            111_111_111,
1093            PolymarketOrderSide::Buy,
1094            PolymarketSignatureType::PolyProxy,
1095            dec!(100000000),
1096            dec!(50000000),
1097            "1713398400000",
1098            ZERO_BYTES32,
1099        ),
1100        false,
1101        "0x8f88fe2fb3448f4b8ba639992029f0a47a01a14d15b5f2bf9833516571efd279",
1102        "0x71a63c85b730cc934688f23ea6374afffef57a61690eba63dcb97a706c8a8d0f3d2a8e0280f3e252eb83be088ebae6b461a8eda18e559e079f59050b90057afa1c",
1103    )]
1104    #[case::sell_neg_risk_poly_gnosis_safe(
1105        parity_order(
1106            222_222_222,
1107            PolymarketOrderSide::Sell,
1108            PolymarketSignatureType::PolyGnosisSafe,
1109            dec!(50000000),
1110            dec!(100000000),
1111            "1713398400000",
1112            ZERO_BYTES32,
1113        ),
1114        true,
1115        "0xb34248702810a1d76580234a33f942a9801c3680de54cb3ef104572a8d482190",
1116        "0xab9d33aee8b578fe5588c4a4b16bbef6fa05fc757020f95b98212e877a919e360a90296f02e97ecbabf3c200271ffe88eb9fd86912e8376cd27237bdad5f3abc1c",
1117    )]
1118    fn test_signature_matches_py_clob_client_v2(
1119        #[case] order: PolymarketOrder,
1120        #[case] neg_risk: bool,
1121        #[case] expected_hash_hex: &str,
1122        #[case] expected_signature_hex: &str,
1123    ) {
1124        let signer = test_signer();
1125
1126        let hash = order_hash(&order, neg_risk).unwrap();
1127        assert_eq!(format!("{hash:#x}"), expected_hash_hex, "signing hash");
1128
1129        let signature = signer.sign_order(&order, neg_risk).unwrap();
1130        assert_eq!(signature, expected_signature_hex, "signature");
1131    }
1132
1133    #[rstest]
1134    #[case::standard_exchange(
1135        false,
1136        "0x48cfd4c03dcee72230750e2dc5ea71048e91244c2a9fec2ed6ef790a74869596",
1137        concat!(
1138            "0x780beffb568c4510b8a135a92ca8071f124cf368fb0e902fe451c5f51e555791",
1139            "0a2a87968caa08a242de85e83c27375fc09242e897aa77680f56622a54e6e7c",
1140            "61c3264e159346253e26a64e00b69032db0e7d32f94628de3e6eecb50304d",
1141            "7af3d2d3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad5",
1142            "71e4f84a04f726465722875696e743235362073616c742c6164647265737320",
1143            "6d616b65722c61646472657373207369676e65722c75696e7432353620746f",
1144            "6b656e49642c75696e74323536206d616b6572416d6f756e742c75696e7432",
1145            "35362074616b6572416d6f756e742c75696e743820736964652c75696e7438",
1146            "207369676e6174757265547970652c75696e743235362074696d657374616d",
1147            "702c62797465733332206d657461646174612c62797465733332206275696c",
1148            "6465722900ba",
1149        ),
1150    )]
1151    #[case::neg_risk_exchange(
1152        true,
1153        "0x82b94b9d570fdd7b07f517ea848606a9b74029f2387fbf07d8b9c856d652e608",
1154        concat!(
1155            "0xa411986b67c58113f8787ee54c41def9376d57ef4262b56438438710720604b7",
1156            "00137e0175d7ae6afc40c2988179ad84b1b71a739f0974c8df3c12372f31b22c",
1157            "1c9b858f53327b0bd13af8ec14cfb35234fb9eb7b0504d1a4e61f433840d3",
1158            "0e81ad3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad571",
1159            "e4f84a04f726465722875696e743235362073616c742c61646472657373206d",
1160            "616b65722c61646472657373207369676e65722c75696e7432353620746f6b",
1161            "656e49642c75696e74323536206d616b6572416d6f756e742c75696e743235",
1162            "362074616b6572416d6f756e742c75696e743820736964652c75696e743820",
1163            "7369676e6174757265547970652c75696e743235362074696d657374616d70",
1164            "2c62797465733332206d657461646174612c62797465733332206275696c64",
1165            "65722900ba",
1166        ),
1167    )]
1168    fn test_poly_1271_signature_matches_py_clob_client_v2(
1169        #[case] neg_risk: bool,
1170        #[case] expected_hash: &str,
1171        #[case] expected_signature: &str,
1172    ) {
1173        let signer = test_signer();
1174        let mut order = parity_order(
1175            333_333_333,
1176            PolymarketOrderSide::Buy,
1177            PolymarketSignatureType::Poly1271,
1178            dec!(100000000),
1179            dec!(50000000),
1180            "1713398400000",
1181            ZERO_BYTES32,
1182        );
1183        order.maker = "0x1111111111111111111111111111111111111111".to_string();
1184        order.signer = order.maker.clone();
1185
1186        let hash = order_hash(&order, neg_risk).unwrap();
1187        assert_eq!(format!("{hash:#x}"), expected_hash, "signing hash");
1188
1189        let signature = signer.sign_order(&order, neg_risk).unwrap();
1190        assert_eq!(signature, expected_signature, "signature");
1191    }
1192
1193    #[rstest]
1194    #[case(false)]
1195    #[case(true)]
1196    fn test_session_signature_envelope(#[case] neg_risk: bool) {
1197        let mut order = test_order();
1198        order.signature_type = PolymarketSignatureType::Poly1271;
1199        order.maker = "0x1111111111111111111111111111111111111111".to_string();
1200        order.signer = order.maker.clone();
1201        let owner = test_signer();
1202        let inner = owner.sign_order(&order, neg_risk).unwrap();
1203        let session = test_signer().with_signer_type(PolymarketSignerType::Session);
1204        let signature = session.sign_order(&order, neg_risk).unwrap();
1205        let bytes = alloy_primitives::hex::decode(&signature[2..]).unwrap();
1206        let (payload, magic) = bytes.split_at(bytes.len() - 32);
1207        let (signer_id, validator, wrapped) =
1208            <(B256, B256, Bytes)>::abi_decode_params(payload).unwrap();
1209        assert_eq!(
1210            signer_id,
1211            B256::left_padding_from(owner.address().as_slice())
1212        );
1213        assert_eq!(validator, B256::ZERO);
1214        assert_eq!(
1215            format!("0x{}", alloy_primitives::hex::encode(wrapped)),
1216            inner
1217        );
1218        assert_eq!(magic, &[0x64, 0x92].repeat(16));
1219    }
1220
1221    #[rstest]
1222    #[case(PolymarketSignatureType::Eoa)]
1223    #[case(PolymarketSignatureType::PolyProxy)]
1224    #[case(PolymarketSignatureType::PolyGnosisSafe)]
1225    fn test_session_rejects_other_wallets(#[case] signature_type: PolymarketSignatureType) {
1226        let mut order = test_order();
1227        order.signature_type = signature_type;
1228        let signer = test_signer().with_signer_type(PolymarketSignerType::Session);
1229        assert_eq!(
1230            signer.sign_order(&order, false).unwrap_err().to_string(),
1231            "bad request: Session signers require POLY_1271 orders"
1232        );
1233    }
1234
1235    #[rstest]
1236    fn test_session_signatures_match_official_sdk() {
1237        #[derive(serde::Deserialize)]
1238        struct Fixture {
1239            private_key: String,
1240            order: PolymarketOrder,
1241            vectors: Vec<Vector>,
1242        }
1243
1244        #[derive(serde::Deserialize)]
1245        struct Vector {
1246            neg_risk: bool,
1247            signature_chunks: Vec<String>,
1248        }
1249
1250        let fixture: Fixture =
1251            serde_json::from_str(include_str!("../../test_data/session_signatures.json")).unwrap();
1252        let key = EvmPrivateKey::new(&fixture.private_key).unwrap();
1253        let signer = OrderSigner::new(&key)
1254            .unwrap()
1255            .with_signer_type(PolymarketSignerType::Session);
1256        assert_eq!(fixture.vectors.len(), 2);
1257
1258        for vector in fixture.vectors {
1259            assert_eq!(
1260                signer.sign_order(&fixture.order, vector.neg_risk).unwrap(),
1261                vector.signature_chunks.concat()
1262            );
1263        }
1264    }
1265}