use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
use hmac::{Hmac, Mac};
use sha2::Sha256;
#[derive(Debug)]
pub enum AuthError {
TimeError(String),
InvalidSecret(String),
HmacError(String),
}
impl std::fmt::Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TimeError(msg) => write!(f, "Time error: {}", msg),
Self::InvalidSecret(msg) => write!(f, "Invalid secret: {}", msg),
Self::HmacError(msg) => write!(f, "HMAC error: {}", msg),
}
}
}
impl std::error::Error for AuthError {}
#[derive(Debug, Clone)]
pub struct PolymarketCredentials {
pub address: String,
pub api_key: String,
pub secret: String,
pub passphrase: String,
}
impl PolymarketCredentials {
pub fn new(
address: impl Into<String>,
api_key: impl Into<String>,
secret: impl Into<String>,
passphrase: impl Into<String>,
) -> Self {
Self {
address: address.into(),
api_key: api_key.into(),
secret: secret.into(),
passphrase: passphrase.into(),
}
}
pub fn from_env() -> Option<Self> {
let address = std::env::var("POLY_ADDRESS").ok()?;
let api_key = std::env::var("POLY_API_KEY").ok()?;
let secret = std::env::var("POLY_SECRET").ok()?;
let passphrase = std::env::var("POLY_PASSPHRASE").ok()?;
Some(Self {
address,
api_key,
secret,
passphrase,
})
}
}
#[derive(Clone)]
pub struct PolymarketAuth {
credentials: Option<PolymarketCredentials>,
}
impl PolymarketAuth {
pub fn new() -> Self {
Self { credentials: None }
}
pub fn from_env() -> Self {
Self {
credentials: PolymarketCredentials::from_env(),
}
}
pub fn with_credentials(creds: PolymarketCredentials) -> Self {
Self {
credentials: Some(creds),
}
}
pub fn is_authenticated(&self) -> bool {
self.credentials.is_some()
}
pub fn build_headers(
&self,
method: &str,
path: &str,
body: Option<&str>,
) -> Option<HashMap<String, String>> {
let creds = self.credentials.as_ref()?;
let timestamp = get_timestamp_ms().ok()?.to_string();
let signature = sign_request(&creds.secret, ×tamp, method, path, body).ok()?;
let mut headers = HashMap::new();
headers.insert("POLY_ADDRESS".to_string(), creds.address.clone());
headers.insert("POLY_API_KEY".to_string(), creds.api_key.clone());
headers.insert("POLY_SIGNATURE".to_string(), signature);
headers.insert("POLY_TIMESTAMP".to_string(), timestamp);
headers.insert("POLY_PASSPHRASE".to_string(), creds.passphrase.clone());
Some(headers)
}
}
impl Default for PolymarketAuth {
fn default() -> Self {
Self::new()
}
}
pub fn get_timestamp_ms() -> Result<u64, AuthError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.map_err(|e| AuthError::TimeError(e.to_string()))
}
fn base64_decode(encoded: &str) -> Result<Vec<u8>, AuthError> {
if let Ok(decoded) = URL_SAFE.decode(encoded) {
return Ok(decoded);
}
let standard = encoded.replace('-', "+").replace('_', "/");
let padded = match standard.len() % 4 {
2 => format!("{}==", standard),
3 => format!("{}=", standard),
_ => standard,
};
base64::engine::general_purpose::STANDARD
.decode(&padded)
.map_err(|e| AuthError::InvalidSecret(format!("Base64 decode failed: {}", e)))
}
fn base64_encode_url(data: &[u8]) -> String {
URL_SAFE.encode(data)
}
pub fn sign_request(
secret: &str,
timestamp: &str,
method: &str,
path: &str,
body: Option<&str>,
) -> Result<String, AuthError> {
let mut message = format!("{}{}{}", timestamp, method, path);
if let Some(b) = body {
if !b.is_empty() {
message.push_str(b);
}
}
let key = base64_decode(secret)?;
let mut mac = Hmac::<Sha256>::new_from_slice(&key)
.map_err(|e| AuthError::HmacError(format!("Invalid HMAC key: {}", e)))?;
mac.update(message.as_bytes());
let result = mac.finalize().into_bytes();
Ok(base64_encode_url(&result))
}
pub fn _create_ws_auth_message(creds: &PolymarketCredentials, markets: &[String]) -> String {
let markets_json: Vec<serde_json::Value> = markets
.iter()
.map(|m| serde_json::Value::String(m.clone()))
.collect();
serde_json::json!({
"type": "user",
"auth": {
"apiKey": creds.api_key,
"secret": creds.secret,
"passphrase": creds.passphrase
},
"markets": markets_json
})
.to_string()
}