1use std::{
19 fmt::{Debug, Display},
20 str::FromStr,
21};
22
23use alloy::signers::local::PrivateKeySigner;
24use alloy_primitives::Address;
25use aws_lc_rs::hmac;
26use base64::{Engine, engine::general_purpose::URL_SAFE};
27use nautilus_core::{
28 env::{get_or_env_var, get_or_env_var_opt},
29 hex,
30 string::secret::{REDACTED, SecretString, mask_api_key},
31};
32use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
33
34use crate::http::error::{Error, Result};
35
36const API_KEY_VAR: &str = "POLYMARKET_API_KEY";
37const API_SECRET_VAR: &str = "POLYMARKET_API_SECRET";
38const PASSPHRASE_VAR: &str = "POLYMARKET_PASSPHRASE";
39const PRIVATE_KEY_VAR: &str = "POLYMARKET_PK";
40const FUNDER_VAR: &str = "POLYMARKET_FUNDER";
41const RELAYER_API_KEY_VAR: &str = "POLYMARKET_RELAYER_API_KEY";
42const RELAYER_SIGNER_ADDRESS_VAR: &str = "POLYMARKET_RELAYER_SIGNER_ADDRESS";
43
44#[must_use]
46pub const fn credential_env_vars() -> (
47 &'static str,
48 &'static str,
49 &'static str,
50 &'static str,
51 &'static str,
52) {
53 (
54 API_KEY_VAR,
55 API_SECRET_VAR,
56 PASSPHRASE_VAR,
57 PRIVATE_KEY_VAR,
58 FUNDER_VAR,
59 )
60}
61
62#[must_use]
64pub const fn relayer_credential_env_vars() -> (&'static str, &'static str) {
65 (RELAYER_API_KEY_VAR, RELAYER_SIGNER_ADDRESS_VAR)
66}
67
68#[derive(Clone, Zeroize, ZeroizeOnDrop)]
70pub struct EvmPrivateKey {
71 formatted_key: String,
72 raw_bytes: Vec<u8>,
73}
74
75impl EvmPrivateKey {
76 pub fn new(key: &str) -> Result<Self> {
78 let key = Zeroizing::new(key.trim().to_string());
79 let hex_key = key.strip_prefix("0x").unwrap_or(&key);
80
81 if hex_key.len() != 64 {
82 return Err(Error::bad_request(
83 "EVM private key must be 32 bytes (64 hex chars)",
84 ));
85 }
86
87 if !hex_key.chars().all(|c| c.is_ascii_hexdigit()) {
88 return Err(Error::bad_request("EVM private key must be valid hex"));
89 }
90
91 let normalized = Zeroizing::new(hex_key.to_lowercase());
92 let formatted = format!("0x{}", normalized.as_str());
93
94 let raw_bytes = hex::decode(&normalized)
95 .map_err(|_| Error::bad_request("Invalid hex in private key"))?;
96
97 if raw_bytes.len() != 32 {
98 return Err(Error::bad_request(
99 "EVM private key must be exactly 32 bytes",
100 ));
101 }
102
103 Ok(Self {
104 formatted_key: formatted,
105 raw_bytes,
106 })
107 }
108
109 pub fn as_hex(&self) -> &str {
110 &self.formatted_key
111 }
112
113 pub fn as_bytes(&self) -> &[u8] {
114 &self.raw_bytes
115 }
116}
117
118impl Debug for EvmPrivateKey {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(f, "EvmPrivateKey({REDACTED})")
121 }
122}
123
124impl Display for EvmPrivateKey {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "EvmPrivateKey({REDACTED})")
127 }
128}
129
130#[derive(Clone)]
137pub struct Credential {
138 api_key: Box<str>,
139 secret_bytes: Box<[u8]>,
140 signing_key: hmac::Key,
141 passphrase: String,
142}
143
144impl Credential {
145 #[expect(
147 clippy::needless_pass_by_value,
148 reason = "ownership ensures the encoded secret is zeroized immediately after decoding"
149 )]
150 pub fn new(
151 api_key: SecretString,
152 api_secret: SecretString,
153 passphrase: SecretString,
154 ) -> Result<Self> {
155 let secret_bytes = URL_SAFE
157 .decode(api_secret.expose_secret())
158 .map_err(|e| Error::auth(format!("Invalid base64 API secret: {e}")))?
159 .into_boxed_slice();
160 let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &secret_bytes);
161
162 Ok(Self {
163 api_key: api_key.into_inner().into_boxed_str(),
164 secret_bytes,
165 signing_key,
166 passphrase: passphrase.into_inner(),
167 })
168 }
169
170 pub(crate) fn api_key_str(&self) -> &str {
171 &self.api_key
172 }
173
174 pub fn passphrase(&self) -> &str {
175 &self.passphrase
176 }
177
178 pub fn api_secret(&self) -> SecretString {
183 URL_SAFE.encode(&*self.secret_bytes).into()
184 }
185
186 pub fn sign(&self, timestamp: &str, method: &str, request_path: &str, body: &str) -> String {
190 let mut context = hmac::Context::with_key(&self.signing_key);
191 context.update(timestamp.as_bytes());
192 context.update(method.as_bytes());
193 context.update(request_path.as_bytes());
194 context.update(body.as_bytes());
195 let tag = context.sign();
196 URL_SAFE.encode(tag.as_ref())
197 }
198
199 pub fn resolve(
201 api_key: Option<SecretString>,
202 api_secret: Option<SecretString>,
203 passphrase: Option<SecretString>,
204 ) -> Result<Self> {
205 let key = resolve_secret(api_key, API_KEY_VAR)?;
206 let secret = resolve_secret(api_secret, API_SECRET_VAR)?;
207 let pass = resolve_secret(passphrase, PASSPHRASE_VAR)?;
208
209 Self::new(key, secret, pass)
210 }
211
212 pub fn from_env() -> Result<Self> {
213 Self::resolve(None, None, None)
214 }
215}
216
217#[derive(Clone)]
219pub struct RelayerApiKey {
220 key: SecretString,
221 address: String,
222}
223
224impl RelayerApiKey {
225 pub fn new(key: SecretString, address: &str) -> Result<Self> {
231 if key.expose_secret().trim().is_empty() {
232 return Err(Error::bad_request("Relayer API key must not be empty"));
233 }
234
235 let parsed = Address::from_str(address.trim())
236 .map_err(|e| Error::bad_request(format!("Invalid Relayer API key address: {e}")))?;
237 Ok(Self {
238 key,
239 address: format!("{parsed:#x}"),
240 })
241 }
242
243 #[must_use]
244 pub fn key(&self) -> &str {
245 self.key.expose_secret()
246 }
247
248 #[must_use]
249 pub fn address(&self) -> &str {
250 &self.address
251 }
252
253 pub fn resolve(key: Option<SecretString>, address: Option<String>) -> Result<Self> {
259 let key = resolve_secret(key, RELAYER_API_KEY_VAR)?;
260
261 let address = match address.filter(|value| !value.trim().is_empty()) {
262 Some(address) => address,
263 None => get_or_env_var(None, RELAYER_SIGNER_ADDRESS_VAR).map_err(|_| {
264 Error::bad_request(format!(
265 "{RELAYER_SIGNER_ADDRESS_VAR} environment variable is not set"
266 ))
267 })?,
268 };
269
270 Self::new(key, &address)
271 }
272
273 pub fn from_env() -> Result<Self> {
279 Self::resolve(None, None)
280 }
281}
282
283impl Debug for RelayerApiKey {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 f.debug_struct(stringify!(RelayerApiKey))
286 .field("key", &REDACTED)
287 .field("address", &self.address)
288 .finish()
289 }
290}
291
292impl Drop for Credential {
293 fn drop(&mut self) {
294 self.api_key.zeroize();
295 self.secret_bytes.zeroize();
296 self.passphrase.zeroize();
297 }
298}
299
300impl Debug for Credential {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 f.debug_struct(stringify!(Credential))
303 .field("api_key", &REDACTED)
304 .field("secret_bytes", &REDACTED)
305 .field("passphrase", &REDACTED)
306 .finish()
307 }
308}
309
310#[derive(Clone)]
314pub struct Secrets {
315 pub private_key: EvmPrivateKey,
316 pub credential: Credential,
317 pub funder: Option<String>,
318 pub address: String,
319}
320
321impl Debug for Secrets {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 f.debug_struct(stringify!(Secrets))
324 .field("private_key", &self.private_key)
325 .field("credential", &self.credential)
326 .field("address", &self.address)
327 .field(
328 "funder",
329 &self.funder.as_deref().map(|s| {
330 if s.len() > 10 {
331 format!("{}...{}", &s[..6], &s[s.len() - 4..])
332 } else {
333 s.to_string()
334 }
335 }),
336 )
337 .finish()
338 }
339}
340
341impl Secrets {
342 pub fn resolve(
344 private_key: Option<SecretString>,
345 api_key: Option<SecretString>,
346 api_secret: Option<SecretString>,
347 passphrase: Option<SecretString>,
348 funder: Option<String>,
349 ) -> Result<Self> {
350 let pk_str = resolve_secret(private_key, PRIVATE_KEY_VAR)?;
351
352 let private_key = EvmPrivateKey::new(pk_str.expose_secret())?;
353 let credential = Credential::resolve(api_key, api_secret, passphrase)?;
354
355 let funder = get_or_env_var_opt(funder.filter(|s| !s.trim().is_empty()), FUNDER_VAR)
356 .filter(|s| !s.trim().is_empty());
357
358 let key_hex = private_key
359 .as_hex()
360 .strip_prefix("0x")
361 .unwrap_or(private_key.as_hex());
362 let signer = PrivateKeySigner::from_str(key_hex)
363 .map_err(|e| Error::bad_request(format!("Failed to derive address: {e}")))?;
364 let address = format!("{:#x}", signer.address());
365
366 log::debug!(
367 "Polymarket credentials resolved: address={}, funder={:?}, api_key={}",
368 address,
369 funder.as_deref().map(|s| &s[..10.min(s.len())]),
370 mask_api_key(credential.api_key_str()),
371 );
372
373 Ok(Self {
374 private_key,
375 credential,
376 funder,
377 address,
378 })
379 }
380
381 pub fn from_env() -> Result<Self> {
382 Self::resolve(None, None, None, None, None)
383 }
384}
385
386fn resolve_secret(value: Option<SecretString>, env_var: &str) -> Result<SecretString> {
387 if let Some(value) = value.filter(|value| !value.expose_secret().trim().is_empty()) {
388 return Ok(value);
389 }
390
391 get_or_env_var(None, env_var)
392 .map(SecretString::from)
393 .map_err(|_| Error::bad_request(format!("{env_var} environment variable is not set")))
394}
395
396#[cfg(test)]
397mod tests {
398 use rstest::rstest;
399
400 use super::*;
401
402 const TEST_PRIVATE_KEY: &str =
403 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
404
405 fn test_secret_b64() -> String {
406 URL_SAFE.encode(b"test_secret_key_32bytes_pad12345")
407 }
408
409 #[rstest]
410 fn test_evm_private_key_with_0x_prefix() {
411 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
412 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
413 assert_eq!(key.as_bytes().len(), 32);
414 }
415
416 #[rstest]
417 fn test_evm_private_key_without_0x_prefix() {
418 let key = EvmPrivateKey::new(&TEST_PRIVATE_KEY[2..]).unwrap();
419 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
420 }
421
422 #[rstest]
423 fn test_evm_private_key_invalid_length() {
424 assert!(EvmPrivateKey::new("0x123").is_err());
425 }
426
427 #[rstest]
428 fn test_evm_private_key_invalid_hex() {
429 let bad = "0x123g567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
430 assert!(EvmPrivateKey::new(bad).is_err());
431 }
432
433 #[rstest]
434 fn test_evm_private_key_debug_redacts() {
435 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
436 let debug = format!("{key:?}");
437 assert_eq!(debug, format!("EvmPrivateKey({REDACTED})"));
438 assert!(!debug.contains("1234"));
439 }
440
441 #[rstest]
442 fn test_credential_creation() {
443 let cred = Credential::new(
444 "test_api_key".into(),
445 test_secret_b64().into(),
446 "test_pass".into(),
447 )
448 .unwrap();
449 let api_secret = cred.api_secret();
450
451 assert_eq!(cred.api_key_str(), "test_api_key");
452 assert_eq!(cred.passphrase(), "test_pass");
453 assert_eq!(api_secret.expose_secret(), test_secret_b64());
454 assert_eq!(format!("{api_secret:?}"), REDACTED);
455 }
456
457 #[rstest]
458 fn test_credential_invalid_base64_secret() {
459 let result = Credential::new("key".into(), "not-valid-base64!!!".into(), "pass".into());
460 assert!(result.is_err());
461 }
462
463 #[rstest]
464 fn test_credential_sign_produces_base64() {
465 let cred = Credential::new(
466 "key".into(),
467 URL_SAFE.encode(b"test_secret").into(),
468 "pass".into(),
469 )
470 .unwrap();
471
472 let sig = cred.sign("1234567890", "GET", "/order", "");
473 assert!(URL_SAFE.decode(&sig).is_ok());
474 }
475
476 #[rstest]
477 fn test_credential_sign_deterministic() {
478 let cred = Credential::new(
479 "key".into(),
480 URL_SAFE.encode(b"deterministic_test").into(),
481 "pass".into(),
482 )
483 .unwrap();
484
485 let sig1 = cred.sign("1000", "POST", "/order", r#"{"price":"0.5"}"#);
486 let sig2 = cred.sign("1000", "POST", "/order", r#"{"price":"0.5"}"#);
487 assert_eq!(sig1, sig2);
488 }
489
490 #[rstest]
491 fn test_credential_sign_different_timestamps() {
492 let cred = Credential::new(
493 "key".into(),
494 URL_SAFE.encode(b"test_key").into(),
495 "pass".into(),
496 )
497 .unwrap();
498
499 let sig1 = cred.sign("1000", "GET", "/order", "");
500 let sig2 = cred.sign("1001", "GET", "/order", "");
501 assert_ne!(sig1, sig2);
502 }
503
504 #[rstest]
505 fn test_credential_sign_different_methods() {
506 let cred = Credential::new(
507 "key".into(),
508 URL_SAFE.encode(b"test_key").into(),
509 "pass".into(),
510 )
511 .unwrap();
512
513 let sig1 = cred.sign("1000", "GET", "/order", "");
514 let sig2 = cred.sign("1000", "POST", "/order", "");
515 assert_ne!(sig1, sig2);
516 }
517
518 #[rstest]
519 fn test_credential_sign_different_paths() {
520 let cred = Credential::new(
521 "key".into(),
522 URL_SAFE.encode(b"test_key").into(),
523 "pass".into(),
524 )
525 .unwrap();
526
527 let sig1 = cred.sign("1000", "GET", "/order", "");
528 let sig2 = cred.sign("1000", "GET", "/trades", "");
529 assert_ne!(sig1, sig2);
530 }
531
532 #[rstest]
533 fn test_credential_sign_different_bodies() {
534 let cred = Credential::new(
535 "key".into(),
536 URL_SAFE.encode(b"test_key").into(),
537 "pass".into(),
538 )
539 .unwrap();
540
541 let sig1 = cred.sign("1000", "POST", "/order", r#"{"a":1}"#);
542 let sig2 = cred.sign("1000", "POST", "/order", r#"{"a":2}"#);
543 assert_ne!(sig1, sig2);
544 }
545
546 #[rstest]
547 fn test_credential_sign_empty_body() {
548 let cred = Credential::new(
549 "key".into(),
550 URL_SAFE.encode(b"test_key").into(),
551 "pass".into(),
552 )
553 .unwrap();
554
555 let sig1 = cred.sign("1000", "GET", "/order", "");
556 let sig2 = cred.sign("1000", "GET", "/order", "{}");
557 assert_ne!(sig1, sig2);
558 }
559
560 const SDK_SECRET: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
562 const SDK_PASSPHRASE: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
563
564 #[rstest]
565 fn test_credential_sign_matches_sdk_l2_vector() {
566 let cred = Credential::new(
567 "00000000-0000-0000-0000-000000000000".into(),
568 SDK_SECRET.into(),
569 SDK_PASSPHRASE.into(),
570 )
571 .unwrap();
572
573 let sig = cred.sign("1", "GET", "/", "");
575 assert_eq!(sig, "eHaylCwqRSOa2LFD77Nt_SaTpbsxzN8eTEI3LryhEj4=");
576 }
577
578 #[rstest]
579 fn test_credential_sign_matches_sdk_hmac_vector() {
580 let cred = Credential::new("key".into(), SDK_SECRET.into(), "pass".into()).unwrap();
581
582 let sig = cred.sign("1000000", "test-sign", "/orders", r#"{"hash":"0x123"}"#);
584 assert_eq!(sig, "4gJVbox-R6XlDK4nlaicig0_ANVL1qdcahiL8CXfXLM=");
585 }
586
587 #[rstest]
588 fn test_credential_debug_redacts_secret() {
589 let cred = Credential::new(
590 "my_api_key_12345678".into(),
591 test_secret_b64().into(),
592 "my_passphrase".into(),
593 )
594 .unwrap();
595
596 let debug = format!("{cred:?}");
597 assert_eq!(debug.matches(REDACTED).count(), 3);
598 assert!(!debug.contains("my_api_key_12345678"));
599 assert!(!debug.contains("test_secret"));
600 assert!(!debug.contains("my_passphrase"));
601 }
602
603 #[rstest]
604 fn test_relayer_api_key_normalizes_address_and_redacts_key() {
605 let key = RelayerApiKey::new(
606 "relayer-secret".into(),
607 "0xF39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
608 )
609 .unwrap();
610 assert_eq!(key.key(), "relayer-secret");
611 assert_eq!(key.address(), "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266");
612 let debug = format!("{key:?}");
613 assert!(debug.contains(REDACTED));
614 assert!(!debug.contains("relayer-secret"));
615 }
616
617 #[rstest]
618 fn test_relayer_api_key_retains_secret_owner_and_allocation() {
619 let mut value = String::with_capacity(4096);
620 value.push_str("relayer-secret");
621 let allocation = value.as_ptr();
622 let key =
623 RelayerApiKey::new(value.into(), "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap();
624 let secret: &SecretString = &key.key;
625
626 assert_eq!(secret.expose_secret().as_ptr(), allocation);
627
628 let cloned = key.clone();
629 drop(key);
630
631 assert_eq!(cloned.key(), "relayer-secret");
632 assert_eq!(
633 cloned.address(),
634 "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
635 );
636 assert_eq!(
637 format!("{cloned:?}"),
638 "RelayerApiKey { key: \"<redacted>\", address: \"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266\" }",
639 );
640 }
641
642 #[rstest]
643 fn test_relayer_api_key_rejects_empty_key_and_invalid_address() {
644 assert!(
645 RelayerApiKey::new("".into(), "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").is_err()
646 );
647 assert!(RelayerApiKey::new("key".into(), "not-an-address").is_err());
648 }
649}