use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::collections::HashMap;
use super::constants::CLOB_HOST;
use super::types::{BuilderCredentials, CosignerRequest};
use crate::error::{Error, Result};
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone)]
pub struct CosignerConfig {
pub cosigner_url: String,
pub polynode_key: String,
pub fallback_direct: bool,
pub builder_credentials: Option<BuilderCredentials>,
}
pub fn build_l2_headers(
api_key: &str,
api_secret: &str,
api_passphrase: &str,
wallet_address: &str,
method: &str,
path: &str,
body: Option<&str>,
) -> HashMap<String, String> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string();
let mut message = format!("{}{}{}", timestamp, method, path);
if let Some(b) = body {
message.push_str(b);
}
let secret_bytes = base64::Engine::decode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
api_secret.trim_end_matches('='),
)
.or_else(|_| base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE, api_secret))
.or_else(|_| base64::Engine::decode(&base64::engine::general_purpose::STANDARD, api_secret))
.unwrap_or_default();
let mut mac = HmacSha256::new_from_slice(&secret_bytes).expect("HMAC can take key of any size");
mac.update(message.as_bytes());
let sig = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
mac.finalize().into_bytes(),
)
.replace('+', "-")
.replace('/', "_");
let mut headers = HashMap::new();
headers.insert("POLY_ADDRESS".into(), wallet_address.into());
headers.insert("POLY_SIGNATURE".into(), sig);
headers.insert("POLY_TIMESTAMP".into(), timestamp);
headers.insert("POLY_API_KEY".into(), api_key.into());
headers.insert("POLY_PASSPHRASE".into(), api_passphrase.into());
headers
}
pub async fn send_via_cosigner(
config: &CosignerConfig,
request: &CosignerRequest,
) -> Result<serde_json::Value> {
let client = reqwest::Client::new();
if !config.cosigner_url.is_empty() {
match try_cosigner(&client, config, request).await {
Ok(data) => return Ok(data),
Err(e) => {
if config.fallback_direct {
tracing::warn!("Co-signer failed, falling back to direct: {}", e);
return send_direct(&client, request).await;
}
return Err(e);
}
}
}
send_direct(&client, request).await
}
async fn try_cosigner(
client: &reqwest::Client,
config: &CosignerConfig,
request: &CosignerRequest,
) -> Result<serde_json::Value> {
let resp = client
.post(format!("{}/submit", config.cosigner_url))
.header("Content-Type", "application/json")
.header("X-PolyNode-Key", &config.polynode_key)
.json(request)
.send()
.await?;
let status = resp.status().as_u16();
if status >= 500 {
return Err(Error::Trading(format!("Co-signer error: {}", status)));
}
let data: serde_json::Value = resp.json().await?;
if status == 403 {
let body_str = data.to_string();
if body_str.contains("1010") || body_str.contains("1020") || body_str.contains("cloudflare")
{
return Err(Error::Trading(
"Cloudflare blocked CLOB request via cosigner".into(),
));
}
}
Ok(data)
}
fn clob_client() -> reqwest::Client {
reqwest::Client::builder()
.user_agent("@polymarket/clob-client")
.http1_only()
.build()
.expect("failed to build HTTP client")
}
async fn send_direct(
_client: &reqwest::Client,
request: &CosignerRequest,
) -> Result<serde_json::Value> {
let client = clob_client();
let host = request.clob_host.as_deref().unwrap_or(CLOB_HOST);
let url = format!("{}{}", host, request.path);
let mut builder = match request.method.as_str() {
"GET" => client.get(&url),
"POST" => client.post(&url),
"DELETE" => client.delete(&url),
"PUT" => client.put(&url),
_ => client.post(&url),
};
for (k, v) in &request.headers {
builder = builder.header(k, v);
}
builder = builder
.header("Accept", "*/*")
.header("Connection", "keep-alive")
.header("Content-Type", "application/json");
if let Some(body) = &request.body {
builder = builder.body(body.clone());
}
let resp = builder.send().await?;
let data: serde_json::Value = resp.json().await?;
Ok(data)
}