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 = build_hmac_signature::<String>(
231 "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1",
232 1234567890,
233 "GET",
234 "/test",
235 None,
236 );
237 assert!(result.is_ok());
238 }
239
240 #[test]
241 fn test_hmac_signature_with_body() {
242 let body = r#"{"test": "data"}"#;
243 let result = build_hmac_signature(
244 "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1",
245 1234567890,
246 "POST",
247 "/orders",
248 Some(body),
249 );
250 assert!(result.is_ok());
251 let signature = result.unwrap();
252 assert!(!signature.is_empty());
253 }
254
255 #[test]
256 fn test_hmac_signature_consistency() {
257 let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
258 let timestamp = 1234567890;
259 let method = "GET";
260 let path = "/test";
261
262 let sig1 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
263 let sig2 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
264
265 assert_eq!(sig1, sig2);
267 }
268
269 #[test]
270 fn test_hmac_signature_different_inputs() {
271 let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
272 let timestamp = 1234567890;
273
274 let sig1 = build_hmac_signature::<String>(secret, timestamp, "GET", "/test", None).unwrap();
275 let sig2 =
276 build_hmac_signature::<String>(secret, timestamp, "POST", "/test", None).unwrap();
277 let sig3 =
278 build_hmac_signature::<String>(secret, timestamp, "GET", "/other", None).unwrap();
279
280 assert_ne!(sig1, sig2);
282 assert_ne!(sig1, sig3);
283 assert_ne!(sig2, sig3);
284 }
285
286 #[test]
287 fn test_create_l1_headers() {
288 use alloy_primitives::U256;
289 use alloy_signer_local::PrivateKeySigner;
290
291 let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
292 let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
293
294 let result = create_l1_headers(&signer, Some(U256::from(12345)));
295 assert!(result.is_ok());
296
297 let headers = result.unwrap();
298 assert!(headers.contains_key("poly_address"));
299 assert!(headers.contains_key("poly_signature"));
300 assert!(headers.contains_key("poly_timestamp"));
301 assert!(headers.contains_key("poly_nonce"));
302 }
303
304 #[test]
305 fn test_create_l1_headers_different_nonces() {
306 use alloy_primitives::U256;
307 use alloy_signer_local::PrivateKeySigner;
308
309 let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
310 let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
311
312 let headers_1 = create_l1_headers(&signer, Some(U256::from(12345))).unwrap();
313 let headers_2 = create_l1_headers(&signer, Some(U256::from(54321))).unwrap();
314
315 assert_ne!(
317 headers_1.get("poly_signature"),
318 headers_2.get("poly_signature")
319 );
320
321 assert_eq!(headers_1.get("poly_address"), headers_2.get("poly_address"));
323 }
324
325 #[test]
326 fn test_create_l2_headers() {
327 use alloy_signer_local::PrivateKeySigner;
328
329 let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
330 let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
331
332 let api_creds = ApiCredentials {
333 api_key: "test_key".to_string(),
334 secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
335 passphrase: "test_passphrase".to_string(),
336 };
337
338 let result = create_l2_headers::<String>(&signer, &api_creds, "/test", "GET", None);
339 assert!(result.is_ok());
340
341 let headers = result.unwrap();
342 assert!(headers.contains_key("poly_api_key"));
343 assert!(headers.contains_key("poly_signature"));
344 assert!(headers.contains_key("poly_timestamp"));
345 assert!(headers.contains_key("poly_passphrase"));
346
347 assert_eq!(headers.get("poly_api_key").unwrap(), "test_key");
348 assert_eq!(headers.get("poly_passphrase").unwrap(), "test_passphrase");
349 }
350
351 #[test]
352 fn test_eip712_signature_format() {
353 use alloy_primitives::U256;
354 use alloy_signer_local::PrivateKeySigner;
355
356 let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
357 let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
358
359 let result = create_l1_headers(&signer, Some(U256::from(12345)));
361 assert!(result.is_ok());
362
363 let headers = result.unwrap();
364 let signature = headers.get("poly_signature").unwrap();
365
366 assert!(signature.starts_with("0x"));
368 assert_eq!(signature.len(), 132); }
370
371 #[test]
372 fn test_timestamp_generation() {
373 let ts1 = get_current_unix_time_secs();
374 std::thread::sleep(std::time::Duration::from_millis(1));
375 let ts2 = get_current_unix_time_secs();
376
377 assert!(ts2 >= ts1);
379
380 assert!(ts1 > 1_600_000_000);
382 assert!(ts1 < 1_900_000_000);
383 }
384}