polyfill_rs/
auth.rs

1//! Authentication and cryptographic utilities for Polymarket API
2//!
3//! This module provides EIP-712 signing, HMAC authentication, and header generation
4//! for secure communication with the Polymarket CLOB API.
5
6use crate::errors::{PolyfillError, Result};
7use crate::types::ApiCredentials;
8use alloy_primitives::{hex::encode_prefixed, Address, U256};
9use alloy_signer::SignerSync;
10use alloy_signer_local::PrivateKeySigner;
11use alloy_sol_types::{eip712_domain, sol};
12use base64::engine::Engine;
13use hmac::{Hmac, Mac};
14use serde::Serialize;
15use sha2::Sha256;
16use std::collections::HashMap;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19// Header constants
20const POLY_ADDR_HEADER: &str = "poly_address";
21const POLY_SIG_HEADER: &str = "poly_signature";
22const POLY_TS_HEADER: &str = "poly_timestamp";
23const POLY_NONCE_HEADER: &str = "poly_nonce";
24const POLY_API_KEY_HEADER: &str = "poly_api_key";
25const POLY_PASS_HEADER: &str = "poly_passphrase";
26
27type Headers = HashMap<&'static str, String>;
28
29// EIP-712 struct for CLOB authentication
30sol! {
31    struct ClobAuth {
32        address address;
33        string timestamp;
34        uint256 nonce;
35        string message;
36    }
37}
38
39// EIP-712 struct for order signing
40sol! {
41    struct Order {
42        uint256 salt;
43        address maker;
44        address signer;
45        address taker;
46        uint256 tokenId;
47        uint256 makerAmount;
48        uint256 takerAmount;
49        uint256 expiration;
50        uint256 nonce;
51        uint256 feeRateBps;
52        uint8 side;
53        uint8 signatureType;
54    }
55}
56
57/// Get current Unix timestamp in seconds
58pub fn get_current_unix_time_secs() -> u64 {
59    SystemTime::now()
60        .duration_since(UNIX_EPOCH)
61        .expect("Time went backwards")
62        .as_secs()
63}
64
65/// Sign CLOB authentication message using EIP-712
66pub fn sign_clob_auth_message(
67    signer: &PrivateKeySigner,
68    timestamp: String,
69    nonce: U256,
70) -> Result<String> {
71    let message = "This message attests that I control the given wallet".to_string();
72    let polygon = 137;
73
74    let auth_struct = ClobAuth {
75        address: signer.address(),
76        timestamp,
77        nonce,
78        message,
79    };
80
81    let domain = eip712_domain!(
82        name: "ClobAuthDomain",
83        version: "1",
84        chain_id: polygon,
85    );
86
87    let signature = signer
88        .sign_typed_data_sync(&auth_struct, &domain)
89        .map_err(|e| PolyfillError::crypto(format!("EIP-712 signature failed: {}", e)))?;
90
91    Ok(encode_prefixed(signature.as_bytes()))
92}
93
94/// Sign order message using EIP-712
95pub fn sign_order_message(
96    signer: &PrivateKeySigner,
97    order: Order,
98    chain_id: u64,
99    verifying_contract: Address,
100) -> Result<String> {
101    let domain = eip712_domain!(
102        name: "Polymarket CTF Exchange",
103        version: "1",
104        chain_id: chain_id,
105        verifying_contract: verifying_contract,
106    );
107
108    let signature = signer
109        .sign_typed_data_sync(&order, &domain)
110        .map_err(|e| PolyfillError::crypto(format!("Order signature failed: {}", e)))?;
111
112    Ok(encode_prefixed(signature.as_bytes()))
113}
114
115/// Build HMAC signature for L2 authentication
116pub fn build_hmac_signature<T>(
117    secret: &str,
118    timestamp: u64,
119    method: &str,
120    request_path: &str,
121    body: Option<&T>,
122) -> Result<String>
123where
124    T: ?Sized + Serialize,
125{
126    let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
127        .map_err(|e| PolyfillError::crypto(format!("Invalid HMAC key: {}", e)))?;
128
129    // Build the message to sign: timestamp + method + path + body
130    let message = format!(
131        "{}{}{}{}",
132        timestamp,
133        method.to_uppercase(),
134        request_path,
135        match body {
136            Some(b) => serde_json::to_string(b).map_err(|e| PolyfillError::parse(
137                format!("Failed to serialize body: {}", e),
138                None
139            ))?,
140            None => String::new(),
141        }
142    );
143
144    mac.update(message.as_bytes());
145    let result = mac.finalize();
146    Ok(base64::engine::general_purpose::STANDARD.encode(result.into_bytes()))
147}
148
149/// Create L1 headers for authentication (using private key signature)
150pub fn create_l1_headers(signer: &PrivateKeySigner, nonce: Option<U256>) -> Result<Headers> {
151    let timestamp = get_current_unix_time_secs().to_string();
152    let nonce = nonce.unwrap_or(U256::ZERO);
153    let signature = sign_clob_auth_message(signer, timestamp.clone(), nonce)?;
154    let address = encode_prefixed(signer.address().as_slice());
155
156    Ok(HashMap::from([
157        (POLY_ADDR_HEADER, address),
158        (POLY_SIG_HEADER, signature),
159        (POLY_TS_HEADER, timestamp),
160        (POLY_NONCE_HEADER, nonce.to_string()),
161    ]))
162}
163
164/// Create L2 headers for API calls (using API key and HMAC)
165pub fn create_l2_headers<T>(
166    signer: &PrivateKeySigner,
167    api_creds: &ApiCredentials,
168    method: &str,
169    req_path: &str,
170    body: Option<&T>,
171) -> Result<Headers>
172where
173    T: ?Sized + Serialize,
174{
175    let address = encode_prefixed(signer.address().as_slice());
176    let timestamp = get_current_unix_time_secs();
177
178    let hmac_signature =
179        build_hmac_signature(&api_creds.secret, timestamp, method, req_path, body)?;
180
181    Ok(HashMap::from([
182        (POLY_ADDR_HEADER, address),
183        (POLY_SIG_HEADER, hmac_signature),
184        (POLY_TS_HEADER, timestamp.to_string()),
185        (POLY_API_KEY_HEADER, api_creds.api_key.clone()),
186        (POLY_PASS_HEADER, api_creds.passphrase.clone()),
187    ]))
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_unix_timestamp() {
196        let timestamp = get_current_unix_time_secs();
197        assert!(timestamp > 1_600_000_000); // Should be after 2020
198    }
199
200    #[test]
201    fn test_hmac_signature() {
202        let result =
203            build_hmac_signature::<String>("test_secret", 1234567890, "GET", "/test", None);
204        assert!(result.is_ok());
205    }
206
207    #[test]
208    fn test_hmac_signature_with_body() {
209        let body = r#"{"test": "data"}"#;
210        let result = build_hmac_signature("test_secret", 1234567890, "POST", "/orders", Some(body));
211        assert!(result.is_ok());
212        let signature = result.unwrap();
213        assert!(!signature.is_empty());
214    }
215
216    #[test]
217    fn test_hmac_signature_consistency() {
218        let secret = "test_secret";
219        let timestamp = 1234567890;
220        let method = "GET";
221        let path = "/test";
222
223        let sig1 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
224        let sig2 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
225
226        // Same inputs should produce same signature
227        assert_eq!(sig1, sig2);
228    }
229
230    #[test]
231    fn test_hmac_signature_different_inputs() {
232        let secret = "test_secret";
233        let timestamp = 1234567890;
234
235        let sig1 = build_hmac_signature::<String>(secret, timestamp, "GET", "/test", None).unwrap();
236        let sig2 =
237            build_hmac_signature::<String>(secret, timestamp, "POST", "/test", None).unwrap();
238        let sig3 =
239            build_hmac_signature::<String>(secret, timestamp, "GET", "/other", None).unwrap();
240
241        // Different inputs should produce different signatures
242        assert_ne!(sig1, sig2);
243        assert_ne!(sig1, sig3);
244        assert_ne!(sig2, sig3);
245    }
246
247    #[test]
248    fn test_create_l1_headers() {
249        use alloy_primitives::U256;
250        use alloy_signer_local::PrivateKeySigner;
251
252        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
253        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
254
255        let result = create_l1_headers(&signer, Some(U256::from(12345)));
256        assert!(result.is_ok());
257
258        let headers = result.unwrap();
259        assert!(headers.contains_key("poly_address"));
260        assert!(headers.contains_key("poly_signature"));
261        assert!(headers.contains_key("poly_timestamp"));
262        assert!(headers.contains_key("poly_nonce"));
263    }
264
265    #[test]
266    fn test_create_l1_headers_different_nonces() {
267        use alloy_primitives::U256;
268        use alloy_signer_local::PrivateKeySigner;
269
270        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
271        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
272
273        let headers_1 = create_l1_headers(&signer, Some(U256::from(12345))).unwrap();
274        let headers_2 = create_l1_headers(&signer, Some(U256::from(54321))).unwrap();
275
276        // Different nonces should produce different signatures
277        assert_ne!(
278            headers_1.get("poly_signature"),
279            headers_2.get("poly_signature")
280        );
281
282        // But same address
283        assert_eq!(headers_1.get("poly_address"), headers_2.get("poly_address"));
284    }
285
286    #[test]
287    fn test_create_l2_headers() {
288        use alloy_signer_local::PrivateKeySigner;
289
290        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
291        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
292
293        let api_creds = ApiCredentials {
294            api_key: "test_key".to_string(),
295            secret: "test_secret".to_string(),
296            passphrase: "test_passphrase".to_string(),
297        };
298
299        let result = create_l2_headers::<String>(&signer, &api_creds, "/test", "GET", None);
300        assert!(result.is_ok());
301
302        let headers = result.unwrap();
303        assert!(headers.contains_key("poly_api_key"));
304        assert!(headers.contains_key("poly_signature"));
305        assert!(headers.contains_key("poly_timestamp"));
306        assert!(headers.contains_key("poly_passphrase"));
307
308        assert_eq!(headers.get("poly_api_key").unwrap(), "test_key");
309        assert_eq!(headers.get("poly_passphrase").unwrap(), "test_passphrase");
310    }
311
312    #[test]
313    fn test_eip712_signature_format() {
314        use alloy_primitives::U256;
315        use alloy_signer_local::PrivateKeySigner;
316
317        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
318        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
319
320        // Test that we can create and sign EIP-712 messages
321        let result = create_l1_headers(&signer, Some(U256::from(12345)));
322        assert!(result.is_ok());
323
324        let headers = result.unwrap();
325        let signature = headers.get("poly_signature").unwrap();
326
327        // EIP-712 signatures should be hex strings of specific length
328        assert!(signature.starts_with("0x"));
329        assert_eq!(signature.len(), 132); // 0x + 130 hex chars = 132 total
330    }
331
332    #[test]
333    fn test_timestamp_generation() {
334        let ts1 = get_current_unix_time_secs();
335        std::thread::sleep(std::time::Duration::from_millis(1));
336        let ts2 = get_current_unix_time_secs();
337
338        // Timestamps should be increasing
339        assert!(ts2 >= ts1);
340
341        // Should be reasonable current time (after 2020, before 2030)
342        assert!(ts1 > 1_600_000_000);
343        assert!(ts1 < 1_900_000_000);
344    }
345}