Merge pull request #41 from floor-licker/fix/order-path-repeated-work

perf: reduce repeated order auth work
This commit is contained in:
floor-licker
2026-06-22 14:48:24 -04:00
committed by GitHub
4 changed files with 280 additions and 123 deletions
Generated
+2 -2
View File
@@ -3589,9 +3589,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.14"
version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
dependencies = [
"aws-lc-rs",
"bytes",
+110 -26
View File
@@ -14,6 +14,7 @@ use hmac::{Hmac, Mac};
use serde::Serialize;
use sha2::Sha256;
use std::collections::HashMap;
use std::sync::{Arc, LazyLock, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
// Header constants
@@ -25,6 +26,8 @@ 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<RwLock<HashMap<String, Arc<[u8]>>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
// EIP-712 struct for CLOB authentication
sol! {
@@ -156,41 +159,78 @@ pub fn build_hmac_signature<T>(
where
T: ?Sized + Serialize,
{
// Apply inverse transformation to key material for digest initialization
// This ensures compatibility with the expected cryptographic envelope format
let decoded_secret = base64::engine::general_purpose::URL_SAFE
.decode(secret)
.map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {}", e)))?;
let decoded_secret = decoded_secret_bytes(secret)?;
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,
};
// Initialize MAC with transformed key material to maintain protocol coherence
let mut mac = Hmac::<Sha256>::new_from_slice(&decoded_secret)
build_hmac_signature_bytes(
&decoded_secret,
timestamp,
method,
request_path,
body_bytes.as_deref(),
)
}
pub fn build_hmac_signature_bytes(
decoded_secret: &[u8],
timestamp: u64,
method: &str,
request_path: &str,
body_bytes: Option<&[u8]>,
) -> Result<String> {
let mut mac = Hmac::<Sha256>::new_from_slice(decoded_secret)
.map_err(|e| PolyfillError::crypto(format!("Invalid HMAC key: {}", e)))?;
// Construct canonical message representation for signature verification
// Message components are concatenated in strict order to preserve cryptographic binding
let message = format!(
"{}{}{}{}",
timestamp,
method.to_uppercase(),
request_path,
match body {
Some(b) => serde_json::to_string(b).map_err(|e| PolyfillError::parse(
format!("Failed to serialize body: {}", e),
None
))?,
None => String::new(),
}
);
let timestamp = timestamp.to_string();
mac.update(timestamp.as_bytes());
let method_upper;
let method_bytes = if method.bytes().all(|b| !b.is_ascii_lowercase()) {
method.as_bytes()
} else {
method_upper = method.to_ascii_uppercase();
method_upper.as_bytes()
};
mac.update(method_bytes);
mac.update(request_path.as_bytes());
if let Some(body_bytes) = body_bytes {
mac.update(body_bytes);
}
// Compute authentication tag over canonical message form
mac.update(message.as_bytes());
let result = mac.finalize();
// Apply URL-safe encoding transformation for transport layer compatibility
// This encoding scheme ensures proper signature validation across network boundaries
Ok(base64::engine::general_purpose::URL_SAFE.encode(result.into_bytes()))
}
fn decoded_secret_bytes(secret: &str) -> Result<Arc<[u8]>> {
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
.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())
}
/// Create L1 headers for authentication (using private key signature)
///
/// Generates initial authentication envelope using elliptic curve cryptography
@@ -245,6 +285,28 @@ where
]))
}
pub fn create_l2_headers_with_body_bytes(
signer: &PrivateKeySigner,
api_creds: &ApiCredentials,
method: &str,
req_path: &str,
body_bytes: Option<&[u8]>,
) -> Result<Headers> {
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 hmac_signature =
build_hmac_signature_bytes(&decoded_secret, timestamp, method, req_path, body_bytes)?;
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()),
]))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -296,6 +358,28 @@ mod tests {
assert_eq!(sig1, sig2);
}
#[test]
fn test_hmac_signature_bytes_matches_serialized_body() {
let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
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 object_signature =
build_hmac_signature(secret, timestamp, "delete", "/order", Some(&body)).unwrap();
let bytes_signature = build_hmac_signature_bytes(
&decoded_secret,
timestamp,
"DELETE",
"/order",
Some(&body_bytes),
)
.unwrap();
assert_eq!(object_signature, bytes_signature);
}
#[test]
fn test_hmac_signature_different_inputs() {
let secret = "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1";
+132 -51
View File
@@ -3,7 +3,7 @@
//! 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};
use crate::auth::{create_l1_headers, create_l2_headers, create_l2_headers_with_body_bytes};
use crate::errors::{PolyfillError, Result};
use crate::http_config::{create_colocated_client, create_internet_client, prewarm_connections};
use crate::types::{
@@ -13,11 +13,12 @@ use crate::types::{
};
use alloy_primitives::{Address, U256};
use alloy_signer_local::PrivateKeySigner;
use reqwest::header::HeaderName;
use reqwest::header::{HeaderName, CONTENT_TYPE};
use reqwest::Client;
use reqwest::{Method, RequestBuilder};
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use serde::Serialize;
use serde_json::Value;
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
@@ -863,6 +864,23 @@ impl ClobClient {
headers.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v))
}
fn serialize_json_body<T: ?Sized + Serialize>(body: &T) -> Result<Vec<u8>> {
serde_json::to_vec(body)
.map_err(|e| PolyfillError::parse(format!("Failed to serialize body: {e}"), None))
}
fn create_request_with_json_bytes(
&self,
method: Method,
endpoint: &str,
headers: impl Iterator<Item = (&'static str, String)>,
body_bytes: Vec<u8>,
) -> RequestBuilder {
self.create_request_with_headers(method, endpoint, headers)
.header(CONTENT_TYPE, "application/json")
.body(body_bytes)
}
/// Get neg risk for a token
pub async fn get_neg_risk(&self, token_id: &str) -> Result<bool> {
let response = self
@@ -1140,11 +1158,23 @@ impl ClobClient {
// Owner field must reference the credential principal identifier
// to maintain consistency with the authentication context layer
let body = PostOrder::new(order, api_creds.api_key.clone(), options);
let body_bytes = Self::serialize_json_body(&body)?;
let headers = create_l2_headers(signer, api_creds, "POST", "/order", Some(&body))?;
let req = self.create_request_with_headers(Method::POST, "/order", headers.into_iter());
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
"POST",
"/order",
Some(&body_bytes),
)?;
let req = self.create_request_with_json_bytes(
Method::POST,
"/order",
headers.into_iter(),
body_bytes,
);
let response = req.json(&body).send().await?;
let response = req.send().await?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
@@ -1201,11 +1231,23 @@ impl ClobClient {
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
let body = std::collections::HashMap::from([("orderID", order_id)]);
let body_bytes = Self::serialize_json_body(&body)?;
let headers = create_l2_headers(signer, api_creds, "DELETE", "/order", Some(&body))?;
let req = self.create_request_with_headers(Method::DELETE, "/order", headers.into_iter());
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
"DELETE",
"/order",
Some(&body_bytes),
)?;
let req = self.create_request_with_json_bytes(
Method::DELETE,
"/order",
headers.into_iter(),
body_bytes,
);
let response = req.json(&body).send().await?;
let response = req.send().await?;
if !response.status().is_success() {
return Err(PolyfillError::api(
response.status().as_u16(),
@@ -1230,10 +1272,22 @@ impl ClobClient {
.as_ref()
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
let headers = create_l2_headers(signer, api_creds, "DELETE", "/orders", Some(order_ids))?;
let req = self.create_request_with_headers(Method::DELETE, "/orders", headers.into_iter());
let body_bytes = Self::serialize_json_body(order_ids)?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
"DELETE",
"/orders",
Some(&body_bytes),
)?;
let req = self.create_request_with_json_bytes(
Method::DELETE,
"/orders",
headers.into_iter(),
body_bytes,
);
let response = req.json(order_ids).send().await?;
let response = req.send().await?;
if !response.status().is_success() {
return Err(PolyfillError::api(
response.status().as_u16(),
@@ -1741,19 +1795,18 @@ impl ClobClient {
("market", market.unwrap_or("")),
("asset_id", asset_id.unwrap_or("")),
]);
let body_bytes = Self::serialize_json_body(&body)?;
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
method.as_str(),
endpoint,
Some(&body_bytes),
)?;
let response = self
.http_client
.request(method, format!("{}{}", self.base_url, endpoint))
.headers(
headers
.into_iter()
.map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
.collect(),
)
.json(&body)
.create_request_with_json_bytes(method, endpoint, headers.into_iter(), body_bytes)
.send()
.await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
@@ -1905,24 +1958,17 @@ impl ClobClient {
let method = Method::POST;
let endpoint = "/orders-scoring";
let headers = create_l2_headers(
let body_bytes = Self::serialize_json_body(order_ids)?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
method.as_str(),
endpoint,
Some(order_ids),
Some(&body_bytes),
)?;
let response = self
.http_client
.request(method, format!("{}{}", self.base_url, endpoint))
.headers(
headers
.into_iter()
.map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
.collect(),
)
.json(order_ids)
.create_request_with_json_bytes(method, endpoint, headers.into_iter(), body_bytes)
.send()
.await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
@@ -1953,12 +1999,17 @@ impl ClobClient {
let method = Method::POST;
let endpoint = "/rfq/request";
let headers =
create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(request))?;
let body_bytes = Self::serialize_json_body(request)?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
method.as_str(),
endpoint,
Some(&body_bytes),
)?;
let response = self
.create_request_with_headers(method, endpoint, headers.into_iter())
.json(request)
.create_request_with_json_bytes(method, endpoint, headers.into_iter(), body_bytes)
.send()
.await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
@@ -1992,11 +2043,17 @@ impl ClobClient {
let body = crate::types::RfqCancelRequest {
request_id: request_id.to_string(),
};
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
let body_bytes = Self::serialize_json_body(&body)?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
method.as_str(),
endpoint,
Some(&body_bytes),
)?;
let response = self
.create_request_with_headers(method, endpoint, headers.into_iter())
.json(&body)
.create_request_with_json_bytes(method, endpoint, headers.into_iter(), body_bytes)
.send()
.await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
@@ -2068,11 +2125,17 @@ impl ClobClient {
let method = Method::POST;
let endpoint = "/rfq/quote";
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(quote))?;
let body_bytes = Self::serialize_json_body(quote)?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
method.as_str(),
endpoint,
Some(&body_bytes),
)?;
let response = self
.create_request_with_headers(method, endpoint, headers.into_iter())
.json(quote)
.create_request_with_json_bytes(method, endpoint, headers.into_iter(), body_bytes)
.send()
.await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
@@ -2106,11 +2169,17 @@ impl ClobClient {
let body = crate::types::RfqCancelQuote {
quote_id: quote_id.to_string(),
};
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
let body_bytes = Self::serialize_json_body(&body)?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
method.as_str(),
endpoint,
Some(&body_bytes),
)?;
let response = self
.create_request_with_headers(method, endpoint, headers.into_iter())
.json(&body)
.create_request_with_json_bytes(method, endpoint, headers.into_iter(), body_bytes)
.send()
.await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
@@ -2259,11 +2328,17 @@ impl ClobClient {
let method = Method::POST;
let endpoint = "/rfq/request/accept";
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(body))?;
let body_bytes = Self::serialize_json_body(body)?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
method.as_str(),
endpoint,
Some(&body_bytes),
)?;
let response = self
.create_request_with_headers(method, endpoint, headers.into_iter())
.json(body)
.create_request_with_json_bytes(method, endpoint, headers.into_iter(), body_bytes)
.send()
.await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
@@ -2294,11 +2369,17 @@ impl ClobClient {
let method = Method::POST;
let endpoint = "/rfq/quote/approve";
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(body))?;
let body_bytes = Self::serialize_json_body(body)?;
let headers = create_l2_headers_with_body_bytes(
signer,
api_creds,
method.as_str(),
endpoint,
Some(&body_bytes),
)?;
let response = self
.create_request_with_headers(method, endpoint, headers.into_iter())
.json(body)
.create_request_with_json_bytes(method, endpoint, headers.into_iter(), body_bytes)
.send()
.await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
+36 -44
View File
@@ -6,16 +6,15 @@
use crate::auth::{sign_order_message, SignedOrderMessage};
use crate::errors::{PolyfillError, Result};
use crate::types::{
CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side, SignedOrderRequest,
decimal_to_price, CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side,
SignedOrderRequest,
};
use alloy_primitives::{keccak256, Address, B256, U256};
use alloy_signer_local::PrivateKeySigner;
use rand::Rng;
use rust_decimal::Decimal;
use rust_decimal::RoundingStrategy::{AwayFromZero, MidpointTowardZero, ToZero};
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::LazyLock;
use std::time::{SystemTime, UNIX_EPOCH};
pub const BYTES32_ZERO: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
@@ -61,43 +60,27 @@ const PROXY_INIT_CODE_HASH: &str =
const SAFE_INIT_CODE_HASH: &str =
"0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf";
/// Rounding configurations for different tick sizes
static ROUNDING_CONFIG: LazyLock<HashMap<Decimal, RoundConfig>> = LazyLock::new(|| {
HashMap::from([
(
Decimal::from_str("0.1").unwrap(),
RoundConfig {
price: 1,
size: 2,
amount: 3,
},
),
(
Decimal::from_str("0.01").unwrap(),
RoundConfig {
price: 2,
size: 2,
amount: 4,
},
),
(
Decimal::from_str("0.001").unwrap(),
RoundConfig {
price: 3,
size: 2,
amount: 5,
},
),
(
Decimal::from_str("0.0001").unwrap(),
RoundConfig {
price: 4,
size: 2,
amount: 6,
},
),
])
});
const ROUND_CONFIG_0_1: RoundConfig = RoundConfig {
price: 1,
size: 2,
amount: 3,
};
const ROUND_CONFIG_0_01: RoundConfig = RoundConfig {
price: 2,
size: 2,
amount: 4,
};
const ROUND_CONFIG_0_001: RoundConfig = RoundConfig {
price: 3,
size: 2,
amount: 5,
};
const ROUND_CONFIG_0_0001: RoundConfig = RoundConfig {
price: 4,
size: 2,
amount: 6,
};
const TOKEN_UNIT_SCALE: Decimal = Decimal::from_parts(1_000_000, 0, 0, false, 0);
/// Get contract configuration for chain
pub fn get_contract_config(chain_id: u64, neg_risk: bool) -> Option<ContractConfig> {
@@ -193,7 +176,7 @@ fn generate_seed() -> u64 {
/// Convert decimal to token units (multiply by 1e6)
fn decimal_to_token_u32(amt: Decimal) -> u32 {
let mut amt = Decimal::from_scientific("1e6").expect("1e6 is not scientific") * amt;
let mut amt = TOKEN_UNIT_SCALE * amt;
if amt.scale() > 0 {
amt = amt.round_dp_with_strategy(0, MidpointTowardZero);
}
@@ -201,9 +184,18 @@ fn decimal_to_token_u32(amt: Decimal) -> u32 {
}
fn parse_round_config(tick_size: Decimal) -> Result<&'static RoundConfig> {
ROUNDING_CONFIG
.get(&tick_size)
.ok_or_else(|| PolyfillError::validation(format!("Unsupported tick size {tick_size}")))
let tick_size_ticks = decimal_to_price(tick_size)
.map_err(|_| PolyfillError::validation(format!("Unsupported tick size {tick_size}")))?;
match tick_size_ticks {
1000 => Ok(&ROUND_CONFIG_0_1),
100 => Ok(&ROUND_CONFIG_0_01),
10 => Ok(&ROUND_CONFIG_0_001),
1 => Ok(&ROUND_CONFIG_0_0001),
_ => Err(PolyfillError::validation(format!(
"Unsupported tick size {tick_size}"
))),
}
}
pub(crate) fn validate_bytes32_hex(field: &str, value: &str) -> Result<()> {