polyte_clob/core/
eip712.rs

1use alloy::{
2    primitives::{keccak256, Address, U256},
3    signers::Signer as AlloySigner,
4    sol,
5    sol_types::SolStruct,
6};
7
8use crate::{
9    core::chain::Chain,
10    error::ClobError,
11    types::{Order, SignatureType},
12};
13
14sol! {
15    #[derive(Debug, PartialEq, Eq)]
16    struct EIP712Domain {
17        string name;
18        string version;
19        uint256 chainId;
20        address verifyingContract;
21    }
22
23    #[derive(Debug, PartialEq, Eq)]
24    struct OrderStruct {
25        uint256 salt;
26        address maker;
27        address signer;
28        address taker;
29        uint256 tokenId;
30        uint256 makerAmount;
31        uint256 takerAmount;
32        uint256 expiration;
33        uint256 nonce;
34        uint256 feeRateBps;
35        uint8 side;
36        uint8 signatureType;
37    }
38}
39
40/// Sign an order with EIP-712
41pub async fn sign_order<S: AlloySigner>(
42    order: &Order,
43    signer: &S,
44    chain_id: u64,
45) -> Result<String, ClobError> {
46    let chain = Chain::from_chain_id(chain_id)
47        .ok_or_else(|| ClobError::Crypto(format!("Unsupported chain ID: {}", chain_id)))?;
48    let contracts = chain.contracts();
49
50    // Create EIP-712 domain
51    let domain = EIP712Domain {
52        name: "Polymarket CTF Exchange".to_string(),
53        version: "1".to_string(),
54        chainId: U256::from(chain_id),
55        verifyingContract: contracts.neg_risk_exchange,
56    };
57
58    // Convert order to struct
59    let order_struct = OrderStruct {
60        salt: U256::from_str_radix(&order.salt, 10)
61            .map_err(|e| ClobError::Crypto(format!("Invalid salt: {}", e)))?,
62        maker: order.maker,
63        signer: order.signer,
64        taker: order.taker,
65        tokenId: U256::from_str_radix(&order.token_id, 10)
66            .map_err(|e| ClobError::Crypto(format!("Invalid token_id: {}", e)))?,
67        makerAmount: U256::from_str_radix(&order.maker_amount, 10)
68            .map_err(|e| ClobError::Crypto(format!("Invalid maker_amount: {}", e)))?,
69        takerAmount: U256::from_str_radix(&order.taker_amount, 10)
70            .map_err(|e| ClobError::Crypto(format!("Invalid taker_amount: {}", e)))?,
71        expiration: U256::from_str_radix(&order.expiration, 10)
72            .map_err(|e| ClobError::Crypto(format!("Invalid expiration: {}", e)))?,
73        nonce: U256::from_str_radix(&order.nonce, 10)
74            .map_err(|e| ClobError::Crypto(format!("Invalid nonce: {}", e)))?,
75        feeRateBps: U256::from_str_radix(&order.fee_rate_bps, 10)
76            .map_err(|e| ClobError::Crypto(format!("Invalid fee_rate_bps: {}", e)))?,
77        side: match order.side {
78            crate::types::OrderSide::Buy => 0,
79            crate::types::OrderSide::Sell => 1,
80        },
81        signatureType: match order.signature_type {
82            SignatureType::Eoa => 0,
83            SignatureType::PolyProxy => 1,
84            SignatureType::PolyGnosisSafe => 2,
85        },
86    };
87
88    // Compute struct hash
89    let struct_hash = keccak256(order_struct.eip712_hash_struct());
90
91    // Compute domain separator
92    let domain_separator = keccak256(domain.eip712_hash_struct());
93
94    // Compute final hash
95    let mut message = Vec::new();
96    message.extend_from_slice(b"\x19\x01");
97    message.extend_from_slice(domain_separator.as_slice());
98    message.extend_from_slice(struct_hash.as_slice());
99    let digest = keccak256(&message);
100
101    // Sign the digest
102    let signature = signer.sign_hash(&digest).await?;
103
104    Ok(format!("0x{}", hex::encode(signature.as_bytes())))
105}
106
107/// Sign CLOB auth message for API key creation
108pub async fn sign_clob_auth<S: AlloySigner>(
109    signer: &S,
110    chain_id: u64,
111    timestamp: u64,
112    nonce: u32,
113) -> Result<String, ClobError> {
114    sol! {
115        #[derive(Debug, PartialEq, Eq)]
116        struct ClobAuth {
117            string message;
118        }
119    }
120
121    let domain = EIP712Domain {
122        name: "ClobAuthDomain".to_string(),
123        version: "1".to_string(),
124        chainId: U256::from(chain_id),
125        verifyingContract: Address::ZERO,
126    };
127
128    let message = format!(
129        "This message attests that I control the given wallet\ntimestamp: {}\nnonce: {}",
130        timestamp, nonce
131    );
132
133    let clob_auth = ClobAuth { message };
134
135    // Compute struct hash
136    let struct_hash = keccak256(clob_auth.eip712_hash_struct());
137
138    // Compute domain separator
139    let domain_separator = keccak256(domain.eip712_hash_struct());
140
141    // Compute final hash
142    let mut digest_message = Vec::new();
143    digest_message.extend_from_slice(b"\x19\x01");
144    digest_message.extend_from_slice(domain_separator.as_slice());
145    digest_message.extend_from_slice(struct_hash.as_slice());
146    let digest = keccak256(&digest_message);
147
148    // Sign the digest
149    let signature = signer.sign_hash(&digest).await?;
150
151    Ok(format!("0x{}", hex::encode(signature.as_bytes())))
152}