use std::time::{SystemTime, UNIX_EPOCH};
use alloy_primitives::Address;
use alloy_signer::Signer;
use alloy_signer_local::PrivateKeySigner;
use alloy_sol_types::{SolStruct, sol};
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use super::types::{ApiKeyCreds, L1PolyHeader, L2PolyHeader};
use crate::error::{PolymarketError, Result};
sol! {
#[derive(Debug)]
struct ClobAuthDomain {
string name;
string version;
uint256 chainId;
}
#[derive(Debug)]
struct ClobAuthMessage {
string message;
uint256 timestamp;
uint256 nonce;
}
}
#[allow(dead_code)]
const DOMAIN_NAME: &str = "ClobAuthDomain";
#[allow(dead_code)]
const DOMAIN_VERSION: &str = "1";
#[allow(dead_code)]
const AUTH_MESSAGE: &str = "This message attests that I control the given wallet";
#[allow(dead_code)]
pub async fn create_l1_headers(
wallet: &PrivateKeySigner,
chain_id: u64,
nonce: Option<u64>,
timestamp: Option<String>,
) -> Result<L1PolyHeader> {
let ts = timestamp.unwrap_or_else(|| get_current_timestamp().to_string());
let nonce_val = nonce.unwrap_or(0);
let domain = alloy_sol_types::eip712_domain! {
name: DOMAIN_NAME,
version: DOMAIN_VERSION,
chain_id: chain_id,
};
let message = ClobAuthMessage {
message: AUTH_MESSAGE.to_string(),
timestamp: alloy_primitives::U256::from(ts.parse::<u64>().unwrap_or(0)),
nonce: alloy_primitives::U256::from(nonce_val),
};
let signing_hash = message.eip712_signing_hash(&domain);
let signature = wallet
.sign_hash(&signing_hash)
.await
.map_err(|e| PolymarketError::other(format!("failed to sign L1 message: {}", e)))?;
Ok(L1PolyHeader {
poly_address: format!("{:?}", wallet.address()),
poly_signature: format!("0x{}", hex::encode(signature.as_bytes())),
poly_timestamp: ts,
poly_nonce: nonce_val.to_string(),
})
}
pub async fn create_l2_headers(
wallet: &PrivateKeySigner,
creds: &ApiKeyCreds,
method: &str,
path: &str,
body: Option<&str>,
timestamp: Option<String>,
) -> Result<L2PolyHeader> {
let ts = timestamp.unwrap_or_else(|| get_current_timestamp().to_string());
let body_str = body.unwrap_or("");
let message = format!("{}{}{}{}", ts, method, path, body_str);
let secret_bytes = BASE64_STANDARD
.decode(&creds.secret)
.map_err(|e| PolymarketError::other(format!("failed to decode API secret: {}", e)))?;
let mut mac = Hmac::<Sha256>::new_from_slice(&secret_bytes)
.map_err(|e| PolymarketError::other(format!("failed to create HMAC: {}", e)))?;
mac.update(message.as_bytes());
let signature = BASE64_STANDARD.encode(mac.finalize().into_bytes());
Ok(L2PolyHeader {
poly_address: format!("{:?}", wallet.address()),
poly_signature: signature,
poly_timestamp: ts,
poly_api_key: creds.key.clone(),
poly_passphrase: creds.passphrase.clone(),
})
}
pub fn get_current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time went backwards")
.as_secs()
}
#[allow(dead_code)]
pub fn parse_address(addr: &str) -> Result<Address> {
addr.parse::<Address>()
.map_err(|e| PolymarketError::other(format!("invalid address '{}': {}", addr, e)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_current_timestamp() {
let ts = get_current_timestamp();
assert!(ts > 1577836800);
}
#[test]
fn test_parse_address() {
let valid = "0x0000000000000000000000000000000000000001";
assert!(parse_address(valid).is_ok());
let invalid = "not-an-address";
assert!(parse_address(invalid).is_err());
}
}