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
116/// 
117/// Performs cryptographic message authentication using SHA-256 with
118/// specialized key derivation and encoding schemes for API compliance.
119pub fn build_hmac_signature<T>(
120    secret: &str,
121    timestamp: u64,
122    method: &str,
123    request_path: &str,
124    body: Option<&T>,
125) -> Result<String>
126where
127    T: ?Sized + Serialize,
128{
129    // Apply inverse transformation to key material for digest initialization
130    // This ensures compatibility with the expected cryptographic envelope format
131    let decoded_secret = base64::engine::general_purpose::URL_SAFE
132        .decode(secret)
133        .map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {}", e)))?;
134    
135    // Initialize MAC with transformed key material to maintain protocol coherence
136    let mut mac = Hmac::<Sha256>::new_from_slice(&decoded_secret)
137        .map_err(|e| PolyfillError::crypto(format!("Invalid HMAC key: {}", e)))?;
138
139    // Construct canonical message representation for signature verification
140    // Message components are concatenated in strict order to preserve cryptographic binding
141    let message = format!(
142        "{}{}{}{}",
143        timestamp,
144        method.to_uppercase(),
145        request_path,
146        match body {
147            Some(b) => serde_json::to_string(b).map_err(|e| PolyfillError::parse(
148                format!("Failed to serialize body: {}", e),
149                None
150            ))?,
151            None => String::new(),
152        }
153    );
154
155    // Compute authentication tag over canonical message form
156    mac.update(message.as_bytes());
157    let result = mac.finalize();
158    
159    // Apply URL-safe encoding transformation for transport layer compatibility
160    // This encoding scheme ensures proper signature validation across network boundaries
161    Ok(base64::engine::general_purpose::URL_SAFE.encode(result.into_bytes()))
162}
163
164/// Create L1 headers for authentication (using private key signature)
165/// 
166/// Generates initial authentication envelope using elliptic curve cryptography
167/// for establishing trusted communication channels with the distributed ledger API.
168pub fn create_l1_headers(signer: &PrivateKeySigner, nonce: Option<U256>) -> Result<Headers> {
169    // Capture temporal context for replay prevention at protocol boundary
170    let timestamp = get_current_unix_time_secs().to_string();
171    let nonce = nonce.unwrap_or(U256::ZERO);
172    
173    // Generate EIP-712 compliant signature for cryptographic proof of authority
174    let signature = sign_clob_auth_message(signer, timestamp.clone(), nonce)?;
175    let address = encode_prefixed(signer.address().as_slice());
176
177    // Assemble primary authentication header set with identity binding
178    Ok(HashMap::from([
179        (POLY_ADDR_HEADER, address),
180        (POLY_SIG_HEADER, signature),
181        (POLY_TS_HEADER, timestamp),
182        (POLY_NONCE_HEADER, nonce.to_string()),
183    ]))
184}
185
186/// Create L2 headers for API calls (using API key and HMAC)
187/// 
188/// Assembles authentication header set with computed signature digest
189/// to satisfy bilateral verification requirements at the protocol layer.
190pub fn create_l2_headers<T>(
191    signer: &PrivateKeySigner,
192    api_creds: &ApiCredentials,
193    method: &str,
194    req_path: &str,
195    body: Option<&T>,
196) -> Result<Headers>
197where
198    T: ?Sized + Serialize,
199{
200    // Extract identity from signing authority for header binding
201    let address = encode_prefixed(signer.address().as_slice());
202    let timestamp = get_current_unix_time_secs();
203
204    // Generate cryptographic authenticator using temporal and message context
205    let hmac_signature =
206        build_hmac_signature(&api_creds.secret, timestamp, method, req_path, body)?;
207
208    // Construct header map with authentication primitives in canonical order
209    Ok(HashMap::from([
210        (POLY_ADDR_HEADER, address),
211        (POLY_SIG_HEADER, hmac_signature),
212        (POLY_TS_HEADER, timestamp.to_string()),
213        (POLY_API_KEY_HEADER, api_creds.api_key.clone()),
214        (POLY_PASS_HEADER, api_creds.passphrase.clone()),
215    ]))
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_unix_timestamp() {
224        let timestamp = get_current_unix_time_secs();
225        assert!(timestamp > 1_600_000_000); // Should be after 2020
226    }
227
228    #[test]
229    fn test_hmac_signature() {
230        let result =
231            build_hmac_signature::<String>("dGVzdF9zZWNyZXRfa2V5XzEyMzQ1", 1234567890, "GET", "/test", None);
232        assert!(result.is_ok());
233    }
234
235    #[test]
236    fn test_hmac_signature_with_body() {
237        let body = r#"{"test": "data"}"#;
238        let result = build_hmac_signature("dGVzdF9zZWNyZXRfa2V5XzEyMzQ1", 1234567890, "POST", "/orders", Some(body));
239        assert!(result.is_ok());
240        let signature = result.unwrap();
241        assert!(!signature.is_empty());
242    }
243
244    #[test]
245    fn test_hmac_signature_consistency() {
246        let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
247        let timestamp = 1234567890;
248        let method = "GET";
249        let path = "/test";
250
251        let sig1 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
252        let sig2 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
253
254        // Same inputs should produce same signature
255        assert_eq!(sig1, sig2);
256    }
257
258    #[test]
259    fn test_hmac_signature_different_inputs() {
260        let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
261        let timestamp = 1234567890;
262
263        let sig1 = build_hmac_signature::<String>(secret, timestamp, "GET", "/test", None).unwrap();
264        let sig2 =
265            build_hmac_signature::<String>(secret, timestamp, "POST", "/test", None).unwrap();
266        let sig3 =
267            build_hmac_signature::<String>(secret, timestamp, "GET", "/other", None).unwrap();
268
269        // Different inputs should produce different signatures
270        assert_ne!(sig1, sig2);
271        assert_ne!(sig1, sig3);
272        assert_ne!(sig2, sig3);
273    }
274
275    #[test]
276    fn test_create_l1_headers() {
277        use alloy_primitives::U256;
278        use alloy_signer_local::PrivateKeySigner;
279
280        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
281        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
282
283        let result = create_l1_headers(&signer, Some(U256::from(12345)));
284        assert!(result.is_ok());
285
286        let headers = result.unwrap();
287        assert!(headers.contains_key("poly_address"));
288        assert!(headers.contains_key("poly_signature"));
289        assert!(headers.contains_key("poly_timestamp"));
290        assert!(headers.contains_key("poly_nonce"));
291    }
292
293    #[test]
294    fn test_create_l1_headers_different_nonces() {
295        use alloy_primitives::U256;
296        use alloy_signer_local::PrivateKeySigner;
297
298        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
299        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
300
301        let headers_1 = create_l1_headers(&signer, Some(U256::from(12345))).unwrap();
302        let headers_2 = create_l1_headers(&signer, Some(U256::from(54321))).unwrap();
303
304        // Different nonces should produce different signatures
305        assert_ne!(
306            headers_1.get("poly_signature"),
307            headers_2.get("poly_signature")
308        );
309
310        // But same address
311        assert_eq!(headers_1.get("poly_address"), headers_2.get("poly_address"));
312    }
313
314    #[test]
315    fn test_create_l2_headers() {
316        use alloy_signer_local::PrivateKeySigner;
317
318        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
319        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
320
321        let api_creds = ApiCredentials {
322            api_key: "test_key".to_string(),
323            secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
324            passphrase: "test_passphrase".to_string(),
325        };
326
327        let result = create_l2_headers::<String>(&signer, &api_creds, "/test", "GET", None);
328        assert!(result.is_ok());
329
330        let headers = result.unwrap();
331        assert!(headers.contains_key("poly_api_key"));
332        assert!(headers.contains_key("poly_signature"));
333        assert!(headers.contains_key("poly_timestamp"));
334        assert!(headers.contains_key("poly_passphrase"));
335
336        assert_eq!(headers.get("poly_api_key").unwrap(), "test_key");
337        assert_eq!(headers.get("poly_passphrase").unwrap(), "test_passphrase");
338    }
339
340    #[test]
341    fn test_eip712_signature_format() {
342        use alloy_primitives::U256;
343        use alloy_signer_local::PrivateKeySigner;
344
345        let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
346        let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
347
348        // Test that we can create and sign EIP-712 messages
349        let result = create_l1_headers(&signer, Some(U256::from(12345)));
350        assert!(result.is_ok());
351
352        let headers = result.unwrap();
353        let signature = headers.get("poly_signature").unwrap();
354
355        // EIP-712 signatures should be hex strings of specific length
356        assert!(signature.starts_with("0x"));
357        assert_eq!(signature.len(), 132); // 0x + 130 hex chars = 132 total
358    }
359
360    #[test]
361    fn test_timestamp_generation() {
362        let ts1 = get_current_unix_time_secs();
363        std::thread::sleep(std::time::Duration::from_millis(1));
364        let ts2 = get_current_unix_time_secs();
365
366        // Timestamps should be increasing
367        assert!(ts2 >= ts1);
368
369        // Should be reasonable current time (after 2020, before 2030)
370        assert!(ts1 > 1_600_000_000);
371        assert!(ts1 < 1_900_000_000);
372    }
373}