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)]
128pub struct Credential {
129 api_key: Ustr,
130 secret_bytes: Box<[u8]>,
131 signing_key: hmac::Key,
132 passphrase: String,
133}
134
135impl Credential {
136 pub fn new(api_key: &str, api_secret: &str, passphrase: String) -> Result<Self> {
138 let secret_bytes = URL_SAFE
140 .decode(api_secret)
141 .map_err(|e| Error::auth(format!("Invalid base64 API secret: {e}")))?
142 .into_boxed_slice();
143 let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &secret_bytes);
144
145 Ok(Self {
146 api_key: Ustr::from(api_key),
147 secret_bytes,
148 signing_key,
149 passphrase,
150 })
151 }
152
153 pub fn api_key(&self) -> Ustr {
154 self.api_key
155 }
156
157 pub fn passphrase(&self) -> &str {
158 &self.passphrase
159 }
160
161 pub fn api_secret(&self) -> String {
166 URL_SAFE.encode(&*self.secret_bytes)
167 }
168
169 pub fn sign(&self, timestamp: &str, method: &str, request_path: &str, body: &str) -> String {
173 let mut context = hmac::Context::with_key(&self.signing_key);
174 context.update(timestamp.as_bytes());
175 context.update(method.as_bytes());
176 context.update(request_path.as_bytes());
177 context.update(body.as_bytes());
178 let tag = context.sign();
179 URL_SAFE.encode(tag.as_ref())
180 }
181
182 pub fn resolve(
184 api_key: Option<String>,
185 api_secret: Option<String>,
186 passphrase: Option<String>,
187 ) -> Result<Self> {
188 let key = get_or_env_var(api_key.filter(|s| !s.trim().is_empty()), API_KEY_VAR).map_err(
189 |_| Error::bad_request(format!("{API_KEY_VAR} environment variable is not set")),
190 )?;
191
192 let secret = get_or_env_var(api_secret.filter(|s| !s.trim().is_empty()), API_SECRET_VAR)
193 .map_err(|_| {
194 Error::bad_request(format!("{API_SECRET_VAR} environment variable is not set"))
195 })?;
196
197 let pass = get_or_env_var(passphrase.filter(|s| !s.trim().is_empty()), PASSPHRASE_VAR)
198 .map_err(|_| {
199 Error::bad_request(format!("{PASSPHRASE_VAR} environment variable is not set"))
200 })?;
201
202 Self::new(&key, &secret, pass)
203 }
204
205 pub fn from_env() -> Result<Self> {
206 Self::resolve(None, None, None)
207 }
208}
209
210impl Drop for Credential {
211 fn drop(&mut self) {
212 self.secret_bytes.zeroize();
213 self.passphrase.zeroize();
214 }
215}
216
217impl Debug for Credential {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct(stringify!(Credential))
220 .field(
221 "api_key",
222 &format!("{}...", &self.api_key.as_str()[..8.min(self.api_key.len())]),
223 )
224 .field("secret_bytes", &"***")
225 .field("passphrase", &"***")
226 .finish()
227 }
228}
229
230#[derive(Clone)]
234pub struct Secrets {
235 pub private_key: EvmPrivateKey,
236 pub credential: Credential,
237 pub funder: Option<String>,
238 pub address: String,
239}
240
241impl Debug for Secrets {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 f.debug_struct(stringify!(Secrets))
244 .field("private_key", &self.private_key)
245 .field("credential", &self.credential)
246 .field("address", &self.address)
247 .field(
248 "funder",
249 &self.funder.as_deref().map(|s| {
250 if s.len() > 10 {
251 format!("{}...{}", &s[..6], &s[s.len() - 4..])
252 } else {
253 s.to_string()
254 }
255 }),
256 )
257 .finish()
258 }
259}
260
261impl Secrets {
262 pub fn resolve(
264 private_key: Option<&str>,
265 api_key: Option<String>,
266 api_secret: Option<String>,
267 passphrase: Option<String>,
268 funder: Option<String>,
269 ) -> Result<Self> {
270 let pk_str = get_or_env_var(
271 private_key
272 .filter(|s| !s.trim().is_empty())
273 .map(String::from),
274 PRIVATE_KEY_VAR,
275 )
276 .map_err(|_| {
277 Error::bad_request(format!("{PRIVATE_KEY_VAR} environment variable is not set"))
278 })?;
279
280 let private_key = EvmPrivateKey::new(&pk_str)?;
281 let credential = Credential::resolve(api_key, api_secret, passphrase)?;
282
283 let funder = get_or_env_var_opt(funder.filter(|s| !s.trim().is_empty()), FUNDER_VAR)
284 .filter(|s| !s.trim().is_empty());
285
286 let key_hex = private_key
287 .as_hex()
288 .strip_prefix("0x")
289 .unwrap_or(private_key.as_hex());
290 let signer = PrivateKeySigner::from_str(key_hex)
291 .map_err(|e| Error::bad_request(format!("Failed to derive address: {e}")))?;
292 let address = format!("{:#x}", signer.address());
293
294 log::debug!(
295 "Polymarket credentials resolved: address={}, funder={:?}, api_key={}...)",
296 address,
297 funder.as_deref().map(|s| &s[..10.min(s.len())]),
298 &credential.api_key()[..8]
299 );
300
301 Ok(Self {
302 private_key,
303 credential,
304 funder,
305 address,
306 })
307 }
308
309 pub fn from_env() -> Result<Self> {
310 Self::resolve(None, None, None, None, None)
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use rstest::rstest;
317
318 use super::*;
319
320 const TEST_PRIVATE_KEY: &str =
321 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
322
323 fn test_secret_b64() -> String {
324 URL_SAFE.encode(b"test_secret_key_32bytes_pad12345")
325 }
326
327 #[rstest]
328 fn test_evm_private_key_with_0x_prefix() {
329 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
330 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
331 assert_eq!(key.as_bytes().len(), 32);
332 }
333
334 #[rstest]
335 fn test_evm_private_key_without_0x_prefix() {
336 let key = EvmPrivateKey::new(&TEST_PRIVATE_KEY[2..]).unwrap();
337 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
338 }
339
340 #[rstest]
341 fn test_evm_private_key_invalid_length() {
342 assert!(EvmPrivateKey::new("0x123").is_err());
343 }
344
345 #[rstest]
346 fn test_evm_private_key_invalid_hex() {
347 let bad = "0x123g567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
348 assert!(EvmPrivateKey::new(bad).is_err());
349 }
350
351 #[rstest]
352 fn test_evm_private_key_debug_redacts() {
353 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
354 let debug = format!("{key:?}");
355 assert_eq!(debug, "EvmPrivateKey(***)");
356 assert!(!debug.contains("1234"));
357 }
358
359 #[rstest]
360 fn test_credential_creation() {
361 let cred =
362 Credential::new("test_api_key", &test_secret_b64(), "test_pass".to_string()).unwrap();
363 assert_eq!(cred.api_key().as_str(), "test_api_key");
364 assert_eq!(cred.passphrase(), "test_pass");
365 }
366
367 #[rstest]
368 fn test_credential_invalid_base64_secret() {
369 let result = Credential::new("key", "not-valid-base64!!!", "pass".to_string());
370 assert!(result.is_err());
371 }
372
373 #[rstest]
374 fn test_credential_sign_produces_base64() {
375 let cred =
376 Credential::new("key", &URL_SAFE.encode(b"test_secret"), "pass".to_string()).unwrap();
377
378 let sig = cred.sign("1234567890", "GET", "/order", "");
379 assert!(URL_SAFE.decode(&sig).is_ok());
380 }
381
382 #[rstest]
383 fn test_credential_sign_deterministic() {
384 let cred = Credential::new(
385 "key",
386 &URL_SAFE.encode(b"deterministic_test"),
387 "pass".to_string(),
388 )
389 .unwrap();
390
391 let sig1 = cred.sign("1000", "POST", "/order", r#"{"price":"0.5"}"#);
392 let sig2 = cred.sign("1000", "POST", "/order", r#"{"price":"0.5"}"#);
393 assert_eq!(sig1, sig2);
394 }
395
396 #[rstest]
397 fn test_credential_sign_different_timestamps() {
398 let cred =
399 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
400
401 let sig1 = cred.sign("1000", "GET", "/order", "");
402 let sig2 = cred.sign("1001", "GET", "/order", "");
403 assert_ne!(sig1, sig2);
404 }
405
406 #[rstest]
407 fn test_credential_sign_different_methods() {
408 let cred =
409 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
410
411 let sig1 = cred.sign("1000", "GET", "/order", "");
412 let sig2 = cred.sign("1000", "POST", "/order", "");
413 assert_ne!(sig1, sig2);
414 }
415
416 #[rstest]
417 fn test_credential_sign_different_paths() {
418 let cred =
419 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
420
421 let sig1 = cred.sign("1000", "GET", "/order", "");
422 let sig2 = cred.sign("1000", "GET", "/trades", "");
423 assert_ne!(sig1, sig2);
424 }
425
426 #[rstest]
427 fn test_credential_sign_different_bodies() {
428 let cred =
429 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
430
431 let sig1 = cred.sign("1000", "POST", "/order", r#"{"a":1}"#);
432 let sig2 = cred.sign("1000", "POST", "/order", r#"{"a":2}"#);
433 assert_ne!(sig1, sig2);
434 }
435
436 #[rstest]
437 fn test_credential_sign_empty_body() {
438 let cred =
439 Credential::new("key", &URL_SAFE.encode(b"test_key"), "pass".to_string()).unwrap();
440
441 let sig1 = cred.sign("1000", "GET", "/order", "");
442 let sig2 = cred.sign("1000", "GET", "/order", "{}");
443 assert_ne!(sig1, sig2);
444 }
445
446 const SDK_SECRET: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
448 const SDK_PASSPHRASE: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
449
450 #[rstest]
451 fn test_credential_sign_matches_sdk_l2_vector() {
452 let cred = Credential::new(
453 "00000000-0000-0000-0000-000000000000",
454 SDK_SECRET,
455 SDK_PASSPHRASE.to_string(),
456 )
457 .unwrap();
458
459 let sig = cred.sign("1", "GET", "/", "");
461 assert_eq!(sig, "eHaylCwqRSOa2LFD77Nt_SaTpbsxzN8eTEI3LryhEj4=");
462 }
463
464 #[rstest]
465 fn test_credential_sign_matches_sdk_hmac_vector() {
466 let cred = Credential::new("key", SDK_SECRET, "pass".to_string()).unwrap();
467
468 let sig = cred.sign("1000000", "test-sign", "/orders", r#"{"hash":"0x123"}"#);
470 assert_eq!(sig, "4gJVbox-R6XlDK4nlaicig0_ANVL1qdcahiL8CXfXLM=");
471 }
472
473 #[rstest]
474 fn test_credential_debug_redacts_secret() {
475 let cred = Credential::new(
476 "my_api_key_12345678",
477 &test_secret_b64(),
478 "my_passphrase".to_string(),
479 )
480 .unwrap();
481
482 let debug = format!("{cred:?}");
483 assert!(debug.contains("my_api_k..."));
484 assert!(debug.contains("***"));
485 assert!(!debug.contains("test_secret"));
486 assert!(!debug.contains("my_passphrase"));
487 }
488}