//! Authentication and cryptographic utilities for Polymarket API //! //! This module provides EIP-712 signing, HMAC authentication, and header generation //! for secure communication with the Polymarket CLOB API. use crate::errors::{PolyfillError, Result}; use crate::types::ApiCredentials; use alloy_primitives::{hex::encode_prefixed, Address, B256, U256}; use alloy_signer::SignerSync; use alloy_signer_local::PrivateKeySigner; use alloy_sol_types::{eip712_domain, sol}; use base64::engine::Engine; use hmac::{Hmac, Mac}; use serde::Serialize; use sha2::Sha256; use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; // Header constants const POLY_ADDR_HEADER: &str = "poly_address"; const POLY_SIG_HEADER: &str = "poly_signature"; const POLY_TS_HEADER: &str = "poly_timestamp"; const POLY_NONCE_HEADER: &str = "poly_nonce"; const POLY_API_KEY_HEADER: &str = "poly_api_key"; const POLY_PASS_HEADER: &str = "poly_passphrase"; type Headers = HashMap<&'static str, String>; // EIP-712 struct for CLOB authentication sol! { struct ClobAuth { address address; string timestamp; uint256 nonce; string message; } } // EIP-712 struct for order signing sol! { struct Order { uint256 salt; address maker; address signer; uint256 tokenId; uint256 makerAmount; uint256 takerAmount; uint8 side; uint8 signatureType; uint256 timestamp; bytes32 metadata; bytes32 builder; } } /// V2 order signing payload. The REST body still carries `expiration`, but the EIP-712 payload /// follows the V2 exchange struct. #[derive(Clone)] pub struct SignedOrderMessage { pub salt: U256, pub maker: Address, pub signer: Address, pub token_id: U256, pub maker_amount: U256, pub taker_amount: U256, pub side: u8, pub signature_type: u8, pub timestamp: U256, pub metadata: B256, pub builder: B256, } /// Get current Unix timestamp in seconds pub fn get_current_unix_time_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs() } /// Sign CLOB authentication message using EIP-712 pub fn sign_clob_auth_message( signer: &PrivateKeySigner, timestamp: String, nonce: U256, ) -> Result { let message = "This message attests that I control the given wallet".to_string(); let polygon = 137; let auth_struct = ClobAuth { address: signer.address(), timestamp, nonce, message, }; let domain = eip712_domain!( name: "ClobAuthDomain", version: "1", chain_id: polygon, ); let signature = signer .sign_typed_data_sync(&auth_struct, &domain) .map_err(|e| PolyfillError::crypto(format!("EIP-712 signature failed: {}", e)))?; Ok(encode_prefixed(signature.as_bytes())) } /// Sign order message using EIP-712 pub fn sign_order_message( signer: &PrivateKeySigner, order: SignedOrderMessage, chain_id: u64, verifying_contract: Address, ) -> Result { let order = Order { salt: order.salt, maker: order.maker, signer: order.signer, tokenId: order.token_id, makerAmount: order.maker_amount, takerAmount: order.taker_amount, side: order.side, signatureType: order.signature_type, timestamp: order.timestamp, metadata: order.metadata, builder: order.builder, }; let domain = eip712_domain!( name: "Polymarket CTF Exchange", version: "2", chain_id: chain_id, verifying_contract: verifying_contract, ); let signature = signer .sign_typed_data_sync(&order, &domain) .map_err(|e| PolyfillError::crypto(format!("Order signature failed: {}", e)))?; Ok(encode_prefixed(signature.as_bytes())) } /// Build HMAC signature for L2 authentication /// /// Performs cryptographic message authentication using SHA-256 with /// specialized key derivation and encoding schemes for API compliance. pub fn build_hmac_signature( secret: &str, timestamp: u64, method: &str, request_path: &str, body: Option<&T>, ) -> Result where T: ?Sized + Serialize, { // Apply inverse transformation to key material for digest initialization // This ensures compatibility with the expected cryptographic envelope format let decoded_secret = base64::engine::general_purpose::URL_SAFE .decode(secret) .map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {}", e)))?; // Initialize MAC with transformed key material to maintain protocol coherence let mut mac = Hmac::::new_from_slice(&decoded_secret) .map_err(|e| PolyfillError::crypto(format!("Invalid HMAC key: {}", e)))?; // Construct canonical message representation for signature verification // Message components are concatenated in strict order to preserve cryptographic binding let message = format!( "{}{}{}{}", timestamp, method.to_uppercase(), request_path, match body { Some(b) => serde_json::to_string(b).map_err(|e| PolyfillError::parse( format!("Failed to serialize body: {}", e), None ))?, None => String::new(), } ); // Compute authentication tag over canonical message form mac.update(message.as_bytes()); let result = mac.finalize(); // Apply URL-safe encoding transformation for transport layer compatibility // This encoding scheme ensures proper signature validation across network boundaries Ok(base64::engine::general_purpose::URL_SAFE.encode(result.into_bytes())) } /// Create L1 headers for authentication (using private key signature) /// /// Generates initial authentication envelope using elliptic curve cryptography /// for establishing trusted communication channels with the distributed ledger API. pub fn create_l1_headers(signer: &PrivateKeySigner, nonce: Option) -> Result { // Capture temporal context for replay prevention at protocol boundary let timestamp = get_current_unix_time_secs().to_string(); let nonce = nonce.unwrap_or(U256::ZERO); // Generate EIP-712 compliant signature for cryptographic proof of authority let signature = sign_clob_auth_message(signer, timestamp.clone(), nonce)?; let address = encode_prefixed(signer.address().as_slice()); // Assemble primary authentication header set with identity binding Ok(HashMap::from([ (POLY_ADDR_HEADER, address), (POLY_SIG_HEADER, signature), (POLY_TS_HEADER, timestamp), (POLY_NONCE_HEADER, nonce.to_string()), ])) } /// Create L2 headers for API calls (using API key and HMAC) /// /// Assembles authentication header set with computed signature digest /// to satisfy bilateral verification requirements at the protocol layer. pub fn create_l2_headers( signer: &PrivateKeySigner, api_creds: &ApiCredentials, method: &str, req_path: &str, body: Option<&T>, ) -> Result where T: ?Sized + Serialize, { // Extract identity from signing authority for header binding let address = encode_prefixed(signer.address().as_slice()); let timestamp = get_current_unix_time_secs(); // Generate cryptographic authenticator using temporal and message context let hmac_signature = build_hmac_signature(&api_creds.secret, timestamp, method, req_path, body)?; // Construct header map with authentication primitives in canonical order Ok(HashMap::from([ (POLY_ADDR_HEADER, address), (POLY_SIG_HEADER, hmac_signature), (POLY_TS_HEADER, timestamp.to_string()), (POLY_API_KEY_HEADER, api_creds.api_key.clone()), (POLY_PASS_HEADER, api_creds.passphrase.clone()), ])) } #[cfg(test)] mod tests { use super::*; #[test] fn test_unix_timestamp() { let timestamp = get_current_unix_time_secs(); assert!(timestamp > 1_600_000_000); // Should be after 2020 } #[test] fn test_hmac_signature() { let result = build_hmac_signature::( "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1", 1234567890, "GET", "/test", None, ); assert!(result.is_ok()); } #[test] fn test_hmac_signature_with_body() { let body = r#"{"test": "data"}"#; let result = build_hmac_signature( "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1", 1234567890, "POST", "/orders", Some(body), ); assert!(result.is_ok()); let signature = result.unwrap(); assert!(!signature.is_empty()); } #[test] fn test_hmac_signature_consistency() { let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1"; let timestamp = 1234567890; let method = "GET"; let path = "/test"; let sig1 = build_hmac_signature::(secret, timestamp, method, path, None).unwrap(); let sig2 = build_hmac_signature::(secret, timestamp, method, path, None).unwrap(); // Same inputs should produce same signature assert_eq!(sig1, sig2); } #[test] fn test_hmac_signature_different_inputs() { let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1"; let timestamp = 1234567890; let sig1 = build_hmac_signature::(secret, timestamp, "GET", "/test", None).unwrap(); let sig2 = build_hmac_signature::(secret, timestamp, "POST", "/test", None).unwrap(); let sig3 = build_hmac_signature::(secret, timestamp, "GET", "/other", None).unwrap(); // Different inputs should produce different signatures assert_ne!(sig1, sig2); assert_ne!(sig1, sig3); assert_ne!(sig2, sig3); } #[test] fn test_create_l1_headers() { use alloy_primitives::U256; use alloy_signer_local::PrivateKeySigner; let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234"; let signer: PrivateKeySigner = private_key.parse().expect("Valid private key"); let result = create_l1_headers(&signer, Some(U256::from(12345))); assert!(result.is_ok()); let headers = result.unwrap(); assert!(headers.contains_key("poly_address")); assert!(headers.contains_key("poly_signature")); assert!(headers.contains_key("poly_timestamp")); assert!(headers.contains_key("poly_nonce")); } #[test] fn test_create_l1_headers_different_nonces() { use alloy_primitives::U256; use alloy_signer_local::PrivateKeySigner; let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234"; let signer: PrivateKeySigner = private_key.parse().expect("Valid private key"); let headers_1 = create_l1_headers(&signer, Some(U256::from(12345))).unwrap(); let headers_2 = create_l1_headers(&signer, Some(U256::from(54321))).unwrap(); // Different nonces should produce different signatures assert_ne!( headers_1.get("poly_signature"), headers_2.get("poly_signature") ); // But same address assert_eq!(headers_1.get("poly_address"), headers_2.get("poly_address")); } #[test] fn test_create_l2_headers() { use alloy_signer_local::PrivateKeySigner; let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234"; let signer: PrivateKeySigner = private_key.parse().expect("Valid private key"); let api_creds = ApiCredentials { api_key: "test_key".to_string(), secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(), passphrase: "test_passphrase".to_string(), }; let result = create_l2_headers::(&signer, &api_creds, "GET", "/test", None); assert!(result.is_ok()); let headers = result.unwrap(); assert!(headers.contains_key("poly_api_key")); assert!(headers.contains_key("poly_signature")); assert!(headers.contains_key("poly_timestamp")); assert!(headers.contains_key("poly_passphrase")); assert_eq!(headers.get("poly_api_key").unwrap(), "test_key"); assert_eq!(headers.get("poly_passphrase").unwrap(), "test_passphrase"); } #[test] fn test_eip712_signature_format() { use alloy_primitives::U256; use alloy_signer_local::PrivateKeySigner; let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234"; let signer: PrivateKeySigner = private_key.parse().expect("Valid private key"); // Test that we can create and sign EIP-712 messages let result = create_l1_headers(&signer, Some(U256::from(12345))); assert!(result.is_ok()); let headers = result.unwrap(); let signature = headers.get("poly_signature").unwrap(); // EIP-712 signatures should be hex strings of specific length assert!(signature.starts_with("0x")); assert_eq!(signature.len(), 132); // 0x + 130 hex chars = 132 total } #[test] fn test_timestamp_generation() { let ts1 = get_current_unix_time_secs(); std::thread::sleep(std::time::Duration::from_millis(1)); let ts2 = get_current_unix_time_secs(); // Timestamps should be increasing assert!(ts2 >= ts1); // Should be reasonable current time (after 2020, before 2030) assert!(ts1 > 1_600_000_000); assert!(ts1 < 1_900_000_000); } }