1use std::{
19 fmt::{Debug, Display},
20 str::FromStr,
21};
22
23use alloy::signers::local::PrivateKeySigner;
24use aws_lc_rs::hmac;
25use base64::{Engine, engine::general_purpose::URL_SAFE};
26use nautilus_core::{
27 env::{get_or_env_var, get_or_env_var_opt},
28 hex,
29};
30use ustr::Ustr;
31use zeroize::{Zeroize, ZeroizeOnDrop};
32
33use crate::http::error::{Error, Result};
34
35const API_KEY_VAR: &str = "POLYMARKET_API_KEY";
36const API_SECRET_VAR: &str = "POLYMARKET_API_SECRET";
37const PASSPHRASE_VAR: &str = "POLYMARKET_PASSPHRASE";
38const PRIVATE_KEY_VAR: &str = "POLYMARKET_PK";
39const FUNDER_VAR: &str = "POLYMARKET_FUNDER";
40
41#[must_use]
43pub const fn credential_env_vars() -> (
44 &'static str,
45 &'static str,
46 &'static str,
47 &'static str,
48 &'static str,
49) {
50 (
51 API_KEY_VAR,
52 API_SECRET_VAR,
53 PASSPHRASE_VAR,
54 PRIVATE_KEY_VAR,
55 FUNDER_VAR,
56 )
57}
58
59#[derive(Clone, Zeroize, ZeroizeOnDrop)]
61pub struct EvmPrivateKey {
62 formatted_key: String,
63 raw_bytes: Vec<u8>,
64}
65
66impl EvmPrivateKey {
67 pub fn new(key: &str) -> Result<Self> {
69 let key = key.trim().to_string();
70 let hex_key = key.strip_prefix("0x").unwrap_or(&key);
71
72 if hex_key.len() != 64 {
73 return Err(Error::bad_request(
74 "EVM private key must be 32 bytes (64 hex chars)",
75 ));
76 }
77
78 if !hex_key.chars().all(|c| c.is_ascii_hexdigit()) {
79 return Err(Error::bad_request("EVM private key must be valid hex"));
80 }
81
82 let normalized = hex_key.to_lowercase();
83 let formatted = format!("0x{normalized}");
84
85 let raw_bytes = hex::decode(&normalized)
86 .map_err(|_| Error::bad_request("Invalid hex in private key"))?;
87
88 if raw_bytes.len() != 32 {
89 return Err(Error::bad_request(
90 "EVM private key must be exactly 32 bytes",
91 ));
92 }
93
94 Ok(Self {
95 formatted_key: formatted,
96 raw_bytes,
97 })
98 }
99
100 pub fn as_hex(&self) -> &str {
101 &self.formatted_key
102 }
103
104 pub fn as_bytes(&self) -> &[u8] {
105 &self.raw_bytes
106 }
107}
108
109impl Debug for EvmPrivateKey {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.write_str("EvmPrivateKey(***)")
112 }
113}
114
115impl Display for EvmPrivateKey {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.write_str("EvmPrivateKey(***)")
118 }
119}
120
121#[derive(Clone)]
127pub struct Credential {
128 api_key: Ustr,
129 secret_bytes: Box<[u8]>,
130 passphrase: String,
131}
132
133impl Credential {
134 pub fn new(api_key: &str, api_secret: &str, passphrase: String) -> Result<Self> {
136 let secret_bytes = URL_SAFE
138 .decode(api_secret)
139 .map_err(|e| Error::auth(format!("Invalid base64 API secret: {e}")))?
140 .into_boxed_slice();
141
142 Ok(Self {
143 api_key: Ustr::from(api_key),
144 secret_bytes,
145 passphrase,
146 })
147 }
148
149 pub fn api_key(&self) -> Ustr {
150 self.api_key
151 }
152
153 pub fn passphrase(&self) -> &str {
154 &self.passphrase
155 }
156
157 pub fn api_secret(&self) -> String {
162 URL_SAFE.encode(&*self.secret_bytes)
163 }
164
165 pub fn sign(&self, timestamp: &str, method: &str, request_path: &str, body: &str) -> String {
169 let message = format!("{timestamp}{method}{request_path}{body}");
170 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.secret_bytes);
171 let tag = hmac::sign(&key, message.as_bytes());
172 URL_SAFE.encode(tag.as_ref())
173 }
174
175 pub fn resolve(
177 api_key: Option<String>,
178 api_secret: Option<String>,
179 passphrase: Option<String>,
180 ) -> Result<Self> {
181 let key = get_or_env_var(api_key.filter(|s| !s.trim().is_empty()), API_KEY_VAR).map_err(
182 |_| Error::bad_request(format!("{API_KEY_VAR} environment variable is not set")),
183 )?;
184
185 let secret = get_or_env_var(api_secret.filter(|s| !s.trim().is_empty()), API_SECRET_VAR)
186 .map_err(|_| {
187 Error::bad_request(format!("{API_SECRET_VAR} environment variable is not set"))
188 })?;
189
190 let pass = get_or_env_var(passphrase.filter(|s| !s.trim().is_empty()), PASSPHRASE_VAR)
191 .map_err(|_| {
192 Error::bad_request(format!("{PASSPHRASE_VAR} environment variable is not set"))
193 })?;
194
195 Self::new(&key, &secret, pass)
196 }
197
198 pub fn from_env() -> Result<Self> {
199 Self::resolve(None, None, None)
200 }
201}
202
203impl Drop for Credential {
204 fn drop(&mut self) {
205 self.secret_bytes.zeroize();
206 self.passphrase.zeroize();
207 }
208}
209
210impl Debug for Credential {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 f.debug_struct(stringify!(Credential))
213 .field(
214 "api_key",
215 &format!("{}...", &self.api_key.as_str()[..8.min(self.api_key.len())]),
216 )
217 .field("secret_bytes", &"***")
218 .field("passphrase", &"***")
219 .finish()
220 }
221}
222
223#[derive(Clone)]
227pub struct Secrets {
228 pub private_key: EvmPrivateKey,
229 pub credential: Credential,
230 pub funder: Option<String>,
231 pub address: String,
232}
233
234impl Debug for Secrets {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 f.debug_struct(stringify!(Secrets))
237 .field("private_key", &self.private_key)
238 .field("credential", &self.credential)
239 .field("address", &self.address)
240 .field(
241 "funder",
242 &self.funder.as_deref().map(|s| {
243 if s.len() > 10 {
244 format!("{}...{}", &s[..6], &s[s.len() - 4..])
245 } else {
246 s.to_string()
247 }
248 }),
249 )
250 .finish()
251 }
252}
253
254impl Secrets {
255 pub fn resolve(
257 private_key: Option<&str>,
258 api_key: Option<String>,
259 api_secret: Option<String>,
260 passphrase: Option<String>,
261 funder: Option<String>,
262 ) -> Result<Self> {
263 let pk_str = get_or_env_var(
264 private_key
265 .filter(|s| !s.trim().is_empty())
266 .map(String::from),
267 PRIVATE_KEY_VAR,
268 )
269 .map_err(|_| {
270 Error::bad_request(format!("{PRIVATE_KEY_VAR} environment variable is not set"))
271 })?;
272
273 let private_key = EvmPrivateKey::new(&pk_str)?;
274 let credential = Credential::resolve(api_key, api_secret, passphrase)?;
275
276 let funder = get_or_env_var_opt(funder.filter(|s| !s.trim().is_empty()), FUNDER_VAR)
277 .filter(|s| !s.trim().is_empty());
278
279 let key_hex = private_key
280 .as_hex()
281 .strip_prefix("0x")
282 .unwrap_or(private_key.as_hex());
283 let signer = PrivateKeySigner::from_str(key_hex)
284 .map_err(|e| Error::bad_request(format!("Failed to derive address: {e}")))?;
285 let address = format!("{:#x}", signer.address());
286
287 log::info!(
288 "Polymarket credentials resolved: address={}, funder={:?}, api_key={}...)",
289 address,
290 funder.as_deref().map(|s| &s[..10.min(s.len())]),
291 &credential.api_key()[..8]
292 );
293
294 Ok(Self {
295 private_key,
296 credential,
297 funder,
298 address,
299 })
300 }
301
302 pub fn from_env() -> Result<Self> {
303 Self::resolve(None, None, None, None, None)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use rstest::rstest;
310
311 use super::*;
312
313 const TEST_PRIVATE_KEY: &str =
314 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
315
316 fn test_secret_b64() -> String {
317 URL_SAFE.encode(b"test_secret_key_32bytes_pad12345")
318 }
319
320 #[rstest]
321 fn test_evm_private_key_with_0x_prefix() {
322 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
323 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
324 assert_eq!(key.as_bytes().len(), 32);
325 }
326
327 #[rstest]
328 fn test_evm_private_key_without_0x_prefix() {
329 let key = EvmPrivateKey::new(&TEST_PRIVATE_KEY[2..]).unwrap();
330 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
331 }
332
333 #[rstest]
334 fn test_evm_private_key_invalid_length() {
335 assert!(EvmPrivateKey::new("0x123").is_err());
336 }
337
338 #[rstest]
339 fn test_evm_private_key_invalid_hex() {
340 let bad = "0x123g567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
341 assert!(EvmPrivateKey::new(bad).is_err());
342 }
343
344 #[rstest]
345 fn test_evm_private_key_debug_redacts() {
346 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
347 let debug = format!("{key:?}");
348 assert_eq!(debug, "EvmPrivateKey(***)");
349 assert!(!debug.contains("1234"));
350 }
351
352 #[rstest]
353 fn test_credential_creation() {
354 let cred =
355 Credential::new("test_api_key", &test_secret_b64(), "test_pass".to_string()).unwrap();
356 assert_eq!(cred.api_key().as_str(), "test_api_key");
357 assert_eq!(cred.passphrase(), "test_pass");
358 }
359
360 #[rstest]
361 fn test_credential_invalid_base64_secret() {
362 let result = Credential::new("key", "not-valid-base64!!!", "pass".to_string());
363 assert!(result.is_err());
364 }
365
366 #[rstest]
367 fn test_credential_sign_produces_base64() {
368 let cred =
369 Credential::new("key", &URL_SAFE.encode(b"test_secret"), "pass".to_string()).unwrap();
370
371 let sig = cred.sign("1234567890", "GET", "/order", "");
372 assert!(URL_SAFE.decode(&sig).is_ok());
373 }
374
375 #[rstest]
376 fn test_credential_sign_deterministic() {
377 let cred = Credential::new(
378 "key",
379 &URL_SAFE.encode(b"deterministic_test"),
380 "pass".to_string(),
381 )
382 .unwrap();
383
384 let sig1 = cred.sign("1000", "POST", "/order", r#"{"price":"0.5"}"#);
385 let sig2 = cred.sign("1000", "POST", "/order", r#"{"price":"0.5"}"#);
386 assert_eq!(sig1, sig2);
387 }
388
389 #[rstest]
390 fn test_credential_sign_different_timestamps() {
391 let cred =
392 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
393
394 let sig1 = cred.sign("1000", "GET", "/order", "");
395 let sig2 = cred.sign("1001", "GET", "/order", "");
396 assert_ne!(sig1, sig2);
397 }
398
399 #[rstest]
400 fn test_credential_sign_different_methods() {
401 let cred =
402 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
403
404 let sig1 = cred.sign("1000", "GET", "/order", "");
405 let sig2 = cred.sign("1000", "POST", "/order", "");
406 assert_ne!(sig1, sig2);
407 }
408
409 #[rstest]
410 fn test_credential_sign_different_paths() {
411 let cred =
412 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
413
414 let sig1 = cred.sign("1000", "GET", "/order", "");
415 let sig2 = cred.sign("1000", "GET", "/trades", "");
416 assert_ne!(sig1, sig2);
417 }
418
419 #[rstest]
420 fn test_credential_sign_different_bodies() {
421 let cred =
422 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
423
424 let sig1 = cred.sign("1000", "POST", "/order", r#"{"a":1}"#);
425 let sig2 = cred.sign("1000", "POST", "/order", r#"{"a":2}"#);
426 assert_ne!(sig1, sig2);
427 }
428
429 #[rstest]
430 fn test_credential_sign_empty_body() {
431 let cred =
432 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
433
434 let sig1 = cred.sign("1000", "GET", "/order", "");
435 let sig2 = cred.sign("1000", "GET", "/order", "{}");
436 assert_ne!(sig1, sig2);
437 }
438
439 const SDK_SECRET: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
441 const SDK_PASSPHRASE: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
442
443 #[rstest]
444 fn test_credential_sign_matches_sdk_l2_vector() {
445 let cred = Credential::new(
446 "00000000-0000-0000-0000-000000000000",
447 SDK_SECRET,
448 SDK_PASSPHRASE.to_string(),
449 )
450 .unwrap();
451
452 let sig = cred.sign("1", "GET", "/", "");
454 assert_eq!(sig, "eHaylCwqRSOa2LFD77Nt_SaTpbsxzN8eTEI3LryhEj4=");
455 }
456
457 #[rstest]
458 fn test_credential_sign_matches_sdk_hmac_vector() {
459 let cred = Credential::new("key", SDK_SECRET, "pass".to_string()).unwrap();
460
461 let sig = cred.sign("1000000", "test-sign", "/orders", r#"{"hash":"0x123"}"#);
463 assert_eq!(sig, "4gJVbox-R6XlDK4nlaicig0_ANVL1qdcahiL8CXfXLM=");
464 }
465
466 #[rstest]
467 fn test_credential_debug_redacts_secret() {
468 let cred = Credential::new(
469 "my_api_key_12345678",
470 &test_secret_b64(),
471 "my_passphrase".to_string(),
472 )
473 .unwrap();
474
475 let debug = format!("{cred:?}");
476 assert!(debug.contains("my_api_k..."));
477 assert!(debug.contains("***"));
478 assert!(!debug.contains("test_secret"));
479 assert!(!debug.contains("my_passphrase"));
480 }
481}