From 6f856ab29c8473a0f713f46db9cdfa00085f4612 Mon Sep 17 00:00:00 2001 From: floor-licker Date: Mon, 22 Jun 2026 16:26:38 -0400 Subject: [PATCH] fix: prepare hmac credentials for auth paths --- benches/comparison_benchmarks.rs | 13 ++- src/auth.rs | 134 +++++++++++++++++++++++-------- src/client.rs | 12 ++- 3 files changed, 117 insertions(+), 42 deletions(-) diff --git a/benches/comparison_benchmarks.rs b/benches/comparison_benchmarks.rs index f9a7bf1..2cbb91d 100644 --- a/benches/comparison_benchmarks.rs +++ b/benches/comparison_benchmarks.rs @@ -1,7 +1,7 @@ use alloy_signer_local::PrivateKeySigner; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use polyfill_rs::{ - auth::create_l2_headers, + auth::{create_l2_headers_with_body_bytes, PreparedApiCredentials}, orders::{OrderBuilder, BYTES32_ZERO}, types::{ ApiCredentials, CreateOrderOptions, FastOrderDelta, OrderDelta, OrderType, PostOrder, @@ -108,6 +108,7 @@ fn benchmark_order_submit_payload_auth(c: &mut Criterion) { secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(), passphrase: "benchmark-passphrase".to_string(), }; + let prepared_api_creds = PreparedApiCredentials::new(api_creds.clone()); c.bench_function("order_submit_body_and_l2_headers", |b| { b.iter(|| { @@ -117,8 +118,14 @@ fn benchmark_order_submit_payload_auth(c: &mut Criterion) { black_box(post_options), ); let body_bytes = serde_json::to_vec(black_box(&body)).unwrap(); - let headers = - create_l2_headers(&signer, &api_creds, "POST", "/order", Some(&body)).unwrap(); + let headers = create_l2_headers_with_body_bytes( + &signer, + &prepared_api_creds, + "POST", + "/order", + Some(&body_bytes), + ) + .unwrap(); black_box((body_bytes, headers)) }) }); diff --git a/src/auth.rs b/src/auth.rs index c2326b9..7720814 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -13,8 +13,10 @@ use base64::engine::Engine; use hmac::{Hmac, Mac}; use serde::Serialize; use sha2::Sha256; +use std::borrow::Cow; use std::collections::HashMap; -use std::sync::{Arc, LazyLock, RwLock}; +use std::ops::Deref; +use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; // Header constants @@ -26,8 +28,75 @@ const POLY_API_KEY_HEADER: &str = "poly_api_key"; const POLY_PASS_HEADER: &str = "poly_passphrase"; type Headers = HashMap<&'static str, String>; -static DECODED_SECRET_CACHE: LazyLock>>> = - LazyLock::new(|| RwLock::new(HashMap::new())); + +pub trait HmacApiCredentials { + fn api_key(&self) -> &str; + fn passphrase(&self) -> &str; + fn decoded_secret_bytes(&self) -> Result>; +} + +#[derive(Debug, Clone)] +pub struct PreparedApiCredentials { + credentials: ApiCredentials, + decoded_secret: std::result::Result, String>, +} + +impl PreparedApiCredentials { + pub fn new(credentials: ApiCredentials) -> Self { + let decoded_secret = base64::engine::general_purpose::URL_SAFE + .decode(&credentials.secret) + .map(Into::into) + .map_err(|e| format!("Failed to decode base64 secret: {}", e)); + + Self { + credentials, + decoded_secret, + } + } + + pub fn credentials(&self) -> &ApiCredentials { + &self.credentials + } +} + +impl Deref for PreparedApiCredentials { + type Target = ApiCredentials; + + fn deref(&self) -> &Self::Target { + &self.credentials + } +} + +impl HmacApiCredentials for ApiCredentials { + fn api_key(&self) -> &str { + &self.api_key + } + + fn passphrase(&self) -> &str { + &self.passphrase + } + + fn decoded_secret_bytes(&self) -> Result> { + Ok(Cow::Owned(decode_secret_bytes(&self.secret)?)) + } +} + +impl HmacApiCredentials for PreparedApiCredentials { + fn api_key(&self) -> &str { + &self.credentials.api_key + } + + fn passphrase(&self) -> &str { + &self.credentials.passphrase + } + + fn decoded_secret_bytes(&self) -> Result> { + match &self.decoded_secret { + Ok(decoded_secret) => Ok(Cow::Borrowed(decoded_secret.as_ref())), + Err(err) => Err(PolyfillError::crypto(err.clone())), + } + } +} // EIP-712 struct for CLOB authentication sol! { @@ -159,7 +228,7 @@ pub fn build_hmac_signature( where T: ?Sized + Serialize, { - let decoded_secret = decoded_secret_bytes(secret)?; + let decoded_secret = decode_secret_bytes(secret)?; let body_bytes = match body { Some(b) => Some(serde_json::to_vec(b).map_err(|e| { @@ -207,28 +276,10 @@ pub fn build_hmac_signature_bytes( Ok(base64::engine::general_purpose::URL_SAFE.encode(result.into_bytes())) } -fn decoded_secret_bytes(secret: &str) -> Result> { - if let Some(decoded) = DECODED_SECRET_CACHE - .read() - .map_err(|_| PolyfillError::internal_simple("Decoded secret cache lock poisoned"))? - .get(secret) - .cloned() - { - return Ok(decoded); - } - - let decoded: Arc<[u8]> = base64::engine::general_purpose::URL_SAFE +fn decode_secret_bytes(secret: &str) -> Result> { + base64::engine::general_purpose::URL_SAFE .decode(secret) - .map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {}", e)))? - .into(); - - let mut cache = DECODED_SECRET_CACHE - .write() - .map_err(|_| PolyfillError::internal_simple("Decoded secret cache lock poisoned"))?; - Ok(cache - .entry(secret.to_string()) - .or_insert_with(|| decoded.clone()) - .clone()) + .map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {}", e))) } /// Create L1 headers for authentication (using private key signature) @@ -259,7 +310,7 @@ pub fn create_l1_headers(signer: &PrivateKeySigner, nonce: Option) -> Resu /// to satisfy bilateral verification requirements at the protocol layer. pub fn create_l2_headers( signer: &PrivateKeySigner, - api_creds: &ApiCredentials, + api_creds: &(impl HmacApiCredentials + ?Sized), method: &str, req_path: &str, body: Option<&T>, @@ -272,29 +323,42 @@ where let timestamp = get_current_unix_time_secs(); // Generate cryptographic authenticator using temporal and message context - let hmac_signature = - build_hmac_signature(&api_creds.secret, timestamp, method, req_path, body)?; + let decoded_secret = api_creds.decoded_secret_bytes()?; + let body_bytes = + match body { + Some(b) => Some(serde_json::to_vec(b).map_err(|e| { + PolyfillError::parse(format!("Failed to serialize body: {}", e), None) + })?), + None => None, + }; + let hmac_signature = build_hmac_signature_bytes( + &decoded_secret, + timestamp, + method, + req_path, + body_bytes.as_deref(), + )?; // Construct header map with authentication primitives in canonical order Ok(HashMap::from([ (POLY_ADDR_HEADER, address), (POLY_SIG_HEADER, hmac_signature), (POLY_TS_HEADER, timestamp.to_string()), - (POLY_API_KEY_HEADER, api_creds.api_key.clone()), - (POLY_PASS_HEADER, api_creds.passphrase.clone()), + (POLY_API_KEY_HEADER, api_creds.api_key().to_string()), + (POLY_PASS_HEADER, api_creds.passphrase().to_string()), ])) } pub fn create_l2_headers_with_body_bytes( signer: &PrivateKeySigner, - api_creds: &ApiCredentials, + api_creds: &(impl HmacApiCredentials + ?Sized), method: &str, req_path: &str, body_bytes: Option<&[u8]>, ) -> Result { let address = encode_prefixed(signer.address().as_slice()); let timestamp = get_current_unix_time_secs(); - let decoded_secret = decoded_secret_bytes(&api_creds.secret)?; + let decoded_secret = api_creds.decoded_secret_bytes()?; let hmac_signature = build_hmac_signature_bytes(&decoded_secret, timestamp, method, req_path, body_bytes)?; @@ -302,8 +366,8 @@ pub fn create_l2_headers_with_body_bytes( (POLY_ADDR_HEADER, address), (POLY_SIG_HEADER, hmac_signature), (POLY_TS_HEADER, timestamp.to_string()), - (POLY_API_KEY_HEADER, api_creds.api_key.clone()), - (POLY_PASS_HEADER, api_creds.passphrase.clone()), + (POLY_API_KEY_HEADER, api_creds.api_key().to_string()), + (POLY_PASS_HEADER, api_creds.passphrase().to_string()), ])) } @@ -364,7 +428,7 @@ mod tests { let timestamp = 1234567890; let body = serde_json::json!({"orderID": "abc123"}); let body_bytes = serde_json::to_vec(&body).unwrap(); - let decoded_secret = decoded_secret_bytes(secret).unwrap(); + let decoded_secret = decode_secret_bytes(secret).unwrap(); let object_signature = build_hmac_signature(secret, timestamp, "delete", "/order", Some(&body)).unwrap(); diff --git a/src/client.rs b/src/client.rs index ee28574..1b6bfff 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,9 @@ //! This module provides a production-ready client for interacting with //! Polymarket, optimized for high-frequency trading environments. -use crate::auth::{create_l1_headers, create_l2_headers, create_l2_headers_with_body_bytes}; +use crate::auth::{ + create_l1_headers, create_l2_headers, create_l2_headers_with_body_bytes, PreparedApiCredentials, +}; use crate::errors::{PolyfillError, Result}; use crate::http_config::{create_colocated_client, create_internet_client, prewarm_connections}; use crate::types::{ @@ -94,7 +96,7 @@ pub struct ClobClient { pub base_url: String, chain_id: u64, signer: Option, - api_creds: Option, + api_creds: Option, builder_code: Option, order_builder: Option, #[allow(dead_code)] @@ -134,12 +136,14 @@ impl ClobClient { .clone() .map(|signer| crate::orders::OrderBuilder::new(signer, auth.sig_type, auth.funder)); + let api_creds = auth.api_creds.map(PreparedApiCredentials::new); + Self { http_client, base_url: host.to_string(), chain_id, signer: auth.signer, - api_creds: auth.api_creds, + api_creds, builder_code: auth.builder_code, order_builder, dns_cache: None, @@ -257,7 +261,7 @@ impl ClobClient { /// Set API credentials pub fn set_api_creds(&mut self, api_creds: ApiCreds) { - self.api_creds = Some(api_creds); + self.api_creds = Some(PreparedApiCredentials::new(api_creds)); } /// Start background keep-alive to maintain warm connection