1use 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
19const 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
29sol! {
31 struct ClobAuth {
32 address address;
33 string timestamp;
34 uint256 nonce;
35 string message;
36 }
37}
38
39sol! {
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
57pub 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
65pub 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
94pub 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
115pub 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 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 let mut mac = Hmac::<Sha256>::new_from_slice(&decoded_secret)
137 .map_err(|e| PolyfillError::crypto(format!("Invalid HMAC key: {}", e)))?;
138
139 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 mac.update(message.as_bytes());
157 let result = mac.finalize();
158
159 Ok(base64::engine::general_purpose::URL_SAFE.encode(result.into_bytes()))
162}
163
164pub fn create_l1_headers(signer: &PrivateKeySigner, nonce: Option<U256>) -> Result<Headers> {
169 let timestamp = get_current_unix_time_secs().to_string();
171 let nonce = nonce.unwrap_or(U256::ZERO);
172
173 let signature = sign_clob_auth_message(signer, timestamp.clone(), nonce)?;
175 let address = encode_prefixed(signer.address().as_slice());
176
177 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
186pub 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 let address = encode_prefixed(signer.address().as_slice());
202 let timestamp = get_current_unix_time_secs();
203
204 let hmac_signature =
206 build_hmac_signature(&api_creds.secret, timestamp, method, req_path, body)?;
207
208 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); }
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 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 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 assert_ne!(
306 headers_1.get("poly_signature"),
307 headers_2.get("poly_signature")
308 );
309
310 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 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 assert!(signature.starts_with("0x"));
357 assert_eq!(signature.len(), 132); }
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 assert!(ts2 >= ts1);
368
369 assert!(ts1 > 1_600_000_000);
371 assert!(ts1 < 1_900_000_000);
372 }
373}