use std::collections::HashMap;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::Duration;
use alloy_primitives::U256;
use alloy_signer_local::PrivateKeySigner;
use governor::{Quota, RateLimiter as GovRateLimiter};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{debug, info, instrument, warn};
use crate::auth::{
create_l1_headers, create_l2_headers_with_address, create_l2_headers_with_body_string,
get_current_unix_time_secs,
};
use crate::core::clob_api_url;
use crate::core::{PolymarketError, Result};
use crate::types::{
ApiCredentials, BalanceAllowanceParams, BalanceAllowanceResponse, OrderType,
SignedOrderRequest,
};
use crate::auth::{BuilderApiKeyCreds, BuilderSigner};
type RateLimiter = GovRateLimiter<
governor::state::NotKeyed,
governor::state::InMemoryState,
governor::clock::DefaultClock,
>;
#[derive(Debug, Clone)]
pub struct ClobConfig {
pub base_url: String,
pub timeout: Duration,
pub rate_limit_per_second: u32,
pub user_agent: String,
}
impl Default for ClobConfig {
fn default() -> Self {
Self {
base_url: clob_api_url(),
timeout: Duration::from_secs(30),
rate_limit_per_second: 5,
user_agent: "polymarket-sdk/0.1.0".to_string(),
}
}
}
impl ClobConfig {
#[must_use]
pub fn builder() -> Self {
Self::default()
}
#[must_use]
#[deprecated(
since = "0.1.0",
note = "Use ClobConfig::default() instead. URL override via POLYMARKET_CLOB_URL env var is already supported."
)]
pub fn from_env() -> Self {
Self::default()
}
#[must_use]
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_rate_limit(mut self, rate_limit: u32) -> Self {
self.rate_limit_per_second = rate_limit;
self
}
#[must_use]
pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = user_agent.into();
self
}
}
#[derive(Debug, Deserialize)]
pub struct DeriveApiKeyResponse {
#[serde(rename = "apiKey")]
pub api_key: String,
pub secret: String,
pub passphrase: String,
}
#[derive(Debug, Deserialize)]
pub struct ApiKeyResponse {
#[serde(rename = "apiKey")]
pub api_key: String,
pub secret: String,
pub passphrase: String,
}
#[derive(Debug, Serialize)]
struct CreateApiKeyRequest {
nonce: String,
}
#[derive(Debug, Deserialize)]
pub struct OrderResponse {
pub success: bool,
#[serde(rename = "errorMsg")]
pub error_msg: String,
#[serde(rename = "orderID")]
pub order_id: String,
#[serde(rename = "transactionsHashes", default)]
pub transactions_hashes: Vec<String>,
pub status: String,
}
#[derive(Debug, Deserialize)]
pub struct PaginatedResponse<T> {
pub limit: Option<i32>,
pub count: Option<i32>,
pub next_cursor: String,
pub data: Vec<T>,
}
#[derive(Debug, Deserialize)]
pub struct OpenOrder {
pub id: String,
pub status: String,
#[serde(default)]
pub owner: Option<String>,
pub maker_address: String,
#[serde(default)]
pub market: Option<String>,
pub asset_id: String,
pub side: String,
pub original_size: String,
pub size_matched: String,
pub price: String,
#[serde(default)]
pub associate_trades: Option<Vec<String>>,
#[serde(default)]
pub outcome: Option<String>,
pub created_at: Option<i64>,
#[serde(default)]
pub expiration: Option<String>,
#[serde(default, rename = "type")]
pub order_type: Option<String>,
}
impl OpenOrder {
#[must_use]
pub fn token_id(&self) -> &str {
&self.asset_id
}
#[must_use]
pub fn maker(&self) -> &str {
&self.maker_address
}
#[must_use]
pub fn signer(&self) -> &str {
&self.maker_address
}
}
#[derive(Debug, Serialize)]
struct CancelOrderRequest {
#[serde(rename = "orderID")]
order_id: String,
}
#[derive(Debug, Deserialize)]
pub struct CancelResponse {
pub canceled: Vec<String>,
#[serde(default)]
pub not_canceled: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
pub struct NegRiskResponse {
pub neg_risk: bool,
}
#[derive(Debug, Deserialize)]
pub struct TickSizeResponse {
pub minimum_tick_size: String,
}
#[derive(Debug, Deserialize)]
pub struct FeeRateResponse {
pub base_fee: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookLevel {
pub price: String,
pub size: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookSummary {
pub market: String,
pub asset_id: String,
pub timestamp: String,
pub hash: String,
pub bids: Vec<OrderBookLevel>,
pub asks: Vec<OrderBookLevel>,
pub min_order_size: String,
pub tick_size: String,
pub neg_risk: bool,
}
impl OrderBookSummary {
#[must_use]
pub fn has_liquidity(&self) -> bool {
!self.bids.is_empty() || !self.asks.is_empty()
}
#[must_use]
pub fn best_bid(&self) -> Option<&str> {
self.bids.first().map(|l| l.price.as_str())
}
#[must_use]
pub fn best_ask(&self) -> Option<&str> {
self.asks.first().map(|l| l.price.as_str())
}
#[must_use]
pub fn spread(&self) -> Option<f64> {
let bid: f64 = self.best_bid()?.parse().ok()?;
let ask: f64 = self.best_ask()?.parse().ok()?;
Some(ask - bid)
}
}
#[derive(Clone)]
pub struct ClobClient {
config: ClobConfig,
client: Client,
signer: PrivateKeySigner,
rate_limiter: Arc<RateLimiter>,
api_credentials: Option<ApiCredentials>,
auth_address: Option<String>,
builder_signer: Option<BuilderSigner>,
}
impl ClobClient {
pub fn new(config: ClobConfig, signer: PrivateKeySigner) -> Result<Self> {
let client = Client::builder()
.timeout(config.timeout)
.user_agent(&config.user_agent)
.gzip(true)
.build()
.map_err(|e| PolymarketError::config(format!("Failed to create HTTP client: {e}")))?;
let quota = Quota::per_second(
NonZeroU32::new(config.rate_limit_per_second).unwrap_or(NonZeroU32::new(5).unwrap()),
);
let rate_limiter = Arc::new(GovRateLimiter::direct(quota));
Ok(Self {
config,
client,
signer,
rate_limiter,
api_credentials: None,
auth_address: None,
builder_signer: None,
})
}
#[deprecated(
since = "0.1.0",
note = "Use ClobClient::new(ClobConfig::default(), signer) instead"
)]
#[allow(deprecated)]
pub fn from_env(signer: PrivateKeySigner) -> Result<Self> {
Self::new(ClobConfig::from_env(), signer)
}
#[must_use]
pub fn with_api_credentials(mut self, credentials: ApiCredentials) -> Self {
self.api_credentials = Some(credentials);
self
}
#[must_use]
pub fn with_auth_address(mut self, address: impl Into<String>) -> Self {
self.auth_address = Some(address.into());
self
}
#[must_use]
pub fn with_builder_signer(mut self, credentials: ApiCredentials) -> Self {
let builder_creds = BuilderApiKeyCreds {
key: credentials.api_key,
secret: credentials.secret, passphrase: credentials.passphrase,
};
self.builder_signer = Some(BuilderSigner::new(builder_creds));
self
}
#[must_use]
pub fn address(&self) -> String {
format!("{:?}", self.signer.address())
}
fn get_auth_address(&self) -> String {
if let Some(ref addr) = self.auth_address {
if addr.starts_with("0x") {
addr.clone()
} else {
format!("0x{}", addr)
}
} else {
format!("{:?}", self.signer.address())
}
}
async fn wait_for_rate_limit(&self) {
self.rate_limiter.until_ready().await;
}
#[instrument(skip(self))]
pub async fn derive_api_key(&self, nonce: Option<U256>) -> Result<DeriveApiKeyResponse> {
self.wait_for_rate_limit().await;
let endpoint = "/auth/derive-api-key";
let url = format!("{}{}", self.config.base_url, endpoint);
let headers = create_l1_headers(&self.signer, nonce)?;
debug!(address = %self.address(), "Deriving API key");
let mut req_builder = self.client.get(&url);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: DeriveApiKeyResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse derive response: {e}"), e)
})?;
info!(address = %self.address(), "API key derived successfully");
Ok(result)
}
#[instrument(skip(self, signature))]
pub async fn derive_api_key_with_signature(
&self,
address: &str,
signature: &str,
timestamp: &str,
nonce: U256,
) -> Result<DeriveApiKeyResponse> {
self.wait_for_rate_limit().await;
let endpoint = "/auth/derive-api-key";
let url = format!("{}{}", self.config.base_url, endpoint);
let address = if address.starts_with("0x") {
address.to_string()
} else {
format!("0x{}", address)
};
let signature = if signature.starts_with("0x") {
signature.to_string()
} else {
format!("0x{}", signature)
};
let headers = HashMap::from([
("poly_address", address.clone()),
("poly_signature", signature),
("poly_timestamp", timestamp.to_string()),
("poly_nonce", nonce.to_string()),
]);
debug!(address = %address, "Deriving API key with pre-computed signature");
let mut req_builder = self.client.get(&url);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: DeriveApiKeyResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse derive response: {e}"), e)
})?;
info!(address = %address, "API key derived successfully with pre-computed signature");
Ok(result)
}
#[instrument(skip(self, signature))]
pub async fn create_api_key_with_signature(
&self,
address: &str,
signature: &str,
timestamp: &str,
nonce: U256,
) -> Result<ApiKeyResponse> {
self.wait_for_rate_limit().await;
let endpoint = "/auth/api-key";
let url = format!("{}{}", self.config.base_url, endpoint);
let address = if address.starts_with("0x") {
address.to_string()
} else {
format!("0x{}", address)
};
let signature = if signature.starts_with("0x") {
signature.to_string()
} else {
format!("0x{}", signature)
};
let headers = HashMap::from([
("poly_address", address.clone()),
("poly_signature", signature),
("poly_timestamp", timestamp.to_string()),
("poly_nonce", nonce.to_string()),
]);
let body = CreateApiKeyRequest {
nonce: nonce.to_string(),
};
debug!(address = %address, "Creating API key with pre-computed signature (first-time registration)");
let mut req_builder = self.client.post(&url).json(&body);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: ApiKeyResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse create response: {e}"), e)
})?;
info!(address = %address, "API key created successfully (wallet registered)");
Ok(result)
}
#[instrument(skip(self, signature))]
pub async fn derive_or_create_api_key(
&self,
address: &str,
signature: &str,
timestamp: &str,
nonce: U256,
) -> Result<DeriveApiKeyResponse> {
match self
.derive_api_key_with_signature(address, signature, timestamp, nonce)
.await
{
Ok(response) => Ok(response),
Err(e) if e.is_wallet_not_registered() => {
info!(
address = %address,
"Wallet not registered with CLOB API, registering first"
);
self.create_api_key_with_signature(address, signature, timestamp, nonce)
.await?;
info!(address = %address, "Wallet registered, retrying derive");
self.derive_api_key_with_signature(address, signature, timestamp, nonce)
.await
}
Err(e) => Err(e),
}
}
#[instrument(skip(self))]
pub async fn create_api_key(&self, nonce: U256) -> Result<ApiKeyResponse> {
self.wait_for_rate_limit().await;
let endpoint = "/auth/api-key";
let url = format!("{}{}", self.config.base_url, endpoint);
let headers = create_l1_headers(&self.signer, Some(nonce))?;
let body = CreateApiKeyRequest {
nonce: nonce.to_string(),
};
debug!(address = %self.address(), "Creating API key");
let mut req_builder = self.client.post(&url).json(&body);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: ApiKeyResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse create response: {e}"), e)
})?;
info!(address = %self.address(), "API key created successfully");
Ok(result)
}
#[instrument(skip(self))]
pub async fn create_api_credentials(&self) -> Result<ApiCredentials> {
info!(address = %self.address(), "Creating API credentials");
let _derive_result = self.derive_api_key(None).await?;
let create_result = self.create_api_key(U256::ZERO).await?;
let credentials = ApiCredentials {
api_key: create_result.api_key,
secret: create_result.secret,
passphrase: create_result.passphrase,
};
info!(address = %self.address(), "API credentials created successfully");
Ok(credentials)
}
#[instrument(skip(self, order))]
pub async fn submit_order(
&self,
order: &SignedOrderRequest,
order_type: OrderType,
) -> Result<OrderResponse> {
use crate::types::NewOrder;
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for order submission")
})?;
let endpoint = "/order";
let url = format!("{}{}", self.config.base_url, endpoint);
let new_order = NewOrder::from_signed_order(order, &api_creds.api_key, order_type, false);
let body_str = serde_json::to_string(&new_order)
.map_err(|e| PolymarketError::parse(format!("Failed to serialize order: {}", e)))?;
let timestamp = get_current_unix_time_secs();
let address = if let Some(ref addr) = self.auth_address {
if addr.starts_with("0x") {
addr.clone()
} else {
format!("0x{}", addr)
}
} else {
format!("{:?}", self.signer.address())
};
let mut headers = create_l2_headers_with_body_string(
&address, api_creds, "POST", endpoint, &body_str, timestamp,
)?;
if let Some(ref builder) = self.builder_signer {
info!("Builder signer configured, generating Builder headers");
let builder_headers = builder
.create_builder_header_payload(
"POST",
endpoint,
Some(&body_str),
Some(timestamp as i64),
)
.map_err(|e| PolymarketError::internal(format!("Builder header error: {}", e)))?;
info!(
header_count = builder_headers.len(),
timestamp = timestamp,
"Builder headers generated with shared timestamp"
);
for (key, value) in builder_headers {
let static_key: &'static str = Box::leak(key.into_boxed_str());
headers.insert(static_key, value);
}
info!(token_id = %order.token_id, side = %order.side, "Submitting order with Builder authentication");
} else {
warn!("Builder signer NOT configured - order may fail with 401 Unauthorized");
info!(token_id = %order.token_id, side = %order.side, "Submitting order WITHOUT Builder authentication");
}
info!(order_json = %body_str, timestamp = timestamp, "Order JSON payload being sent to Polymarket (NewOrder format)");
let mut req_builder = self
.client
.post(&url)
.header("Content-Type", "application/json")
.body(body_str.clone());
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: OrderResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse order response: {e}"), e)
})?;
if !result.success {
return Err(PolymarketError::api(
400,
format!(
"Order rejected: {} (status: {})",
result.error_msg, result.status
),
));
}
info!(
order_id = %result.order_id,
status = %result.status,
success = result.success,
"Order submitted successfully"
);
Ok(result)
}
#[instrument(skip(self))]
pub async fn get_order(&self, order_id: &str) -> Result<Option<OpenOrder>> {
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for querying order")
})?;
let endpoint = format!("/data/order/{}", order_id);
let url = format!("{}{}", self.config.base_url, endpoint);
let address = self.get_auth_address();
let headers =
create_l2_headers_with_address::<String>(&address, api_creds, "GET", &endpoint, None)?;
debug!(order_id = %order_id, "Getting order details");
let mut req_builder = self.client.get(&url);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if status.as_u16() == 404 {
info!(order_id = %order_id, "Order not found (404)");
return Ok(None);
}
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let body = response.text().await.unwrap_or_default();
if body.is_empty() || body == "null" || body == "{}" {
info!(order_id = %order_id, "Order not found (empty/null response)");
return Ok(None);
}
let json_value: serde_json::Value = serde_json::from_str(&body).map_err(|e| {
PolymarketError::parse_with_source(
format!("Failed to parse order response as JSON: {e}"),
e,
)
})?;
if let Some(obj) = json_value.as_object() {
if obj.contains_key("error") {
let error_msg = obj
.get("error")
.and_then(|v| v.as_str())
.unwrap_or("unknown error");
if error_msg.to_lowercase().contains("not found")
|| error_msg.to_lowercase().contains("does not exist")
{
info!(order_id = %order_id, error = %error_msg, "Order not found (error response)");
return Ok(None);
}
return Err(PolymarketError::api(200, format!("API error: {error_msg}")));
}
if !obj.contains_key("id") && !obj.contains_key("order_id") {
info!(order_id = %order_id, "Order not found (missing required fields)");
return Ok(None);
}
} else if json_value.is_null() {
info!(order_id = %order_id, "Order not found (null JSON)");
return Ok(None);
}
let order: OpenOrder = serde_json::from_value(json_value).map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse order response: {e}"), e)
})?;
debug!(
order_id = %order_id,
status = %order.status,
price = %order.price,
"Retrieved order details"
);
Ok(Some(order))
}
#[instrument(skip(self))]
pub async fn get_open_orders(&self) -> Result<Vec<OpenOrder>> {
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for querying orders")
})?;
let endpoint = "/data/orders";
let url = format!("{}{}", self.config.base_url, endpoint);
let address = self.get_auth_address();
let headers =
create_l2_headers_with_address::<String>(&address, api_creds, "GET", endpoint, None)?;
debug!(address = %self.address(), "Getting open orders");
let mut req_builder = self.client.get(&url);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let paginated: PaginatedResponse<OpenOrder> = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse orders response: {e}"), e)
})?;
debug!(count = %paginated.data.len(), "Retrieved open orders");
Ok(paginated.data)
}
#[instrument(skip(self))]
pub async fn cancel_order(&self, order_id: &str) -> Result<CancelResponse> {
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for cancelling order")
})?;
let endpoint = "/order";
let url = format!("{}{}", self.config.base_url, endpoint);
let body = CancelOrderRequest {
order_id: order_id.to_string(),
};
let address = self.get_auth_address();
let body_str = serde_json::to_string(&body)
.map_err(|e| PolymarketError::parse(format!("Failed to serialize: {}", e)))?;
let timestamp = get_current_unix_time_secs();
let headers = create_l2_headers_with_body_string(
&address, api_creds, "DELETE", endpoint, &body_str, timestamp,
)?;
debug!(order_id = %order_id, "Cancelling single order");
let mut req_builder = self
.client
.delete(&url)
.header("Content-Type", "application/json")
.body(body_str);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: CancelResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse cancel response: {e}"), e)
})?;
info!(order_id = %order_id, cancelled = ?result.canceled, "Order cancelled");
Ok(result)
}
#[instrument(skip(self))]
pub async fn cancel_orders(&self, order_ids: Vec<String>) -> Result<CancelResponse> {
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for cancelling orders")
})?;
let endpoint = "/orders";
let url = format!("{}{}", self.config.base_url, endpoint);
let body_str = serde_json::to_string(&order_ids)
.map_err(|e| PolymarketError::parse(format!("Failed to serialize: {}", e)))?;
let address = self.get_auth_address();
let timestamp = get_current_unix_time_secs();
let headers = create_l2_headers_with_body_string(
&address, api_creds, "DELETE", endpoint, &body_str, timestamp,
)?;
debug!(count = order_ids.len(), "Cancelling multiple orders");
let mut req_builder = self
.client
.delete(&url)
.header("Content-Type", "application/json")
.body(body_str);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: CancelResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse cancel response: {e}"), e)
})?;
info!(cancelled = ?result.canceled, "Orders cancelled");
Ok(result)
}
#[instrument(skip(self))]
pub async fn cancel_all_orders(&self) -> Result<CancelResponse> {
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for cancelling orders")
})?;
let endpoint = "/cancel-all";
let url = format!("{}{}", self.config.base_url, endpoint);
let address = self.get_auth_address();
let headers = create_l2_headers_with_address::<String>(
&address, api_creds, "DELETE", endpoint, None,
)?;
debug!(address = %self.address(), "Cancelling all orders");
let mut req_builder = self.client.delete(&url);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: CancelResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse cancel response: {e}"), e)
})?;
info!(cancelled = ?result.canceled, "All orders cancelled");
Ok(result)
}
#[instrument(skip(self))]
pub async fn cancel_market_orders(
&self,
market: Option<&str>,
asset_id: Option<&str>,
) -> Result<CancelResponse> {
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for cancelling market orders")
})?;
let endpoint = "/cancel-market-orders";
let url = format!("{}{}", self.config.base_url, endpoint);
let mut body_map = serde_json::Map::new();
if let Some(m) = market {
body_map.insert(
"market".to_string(),
serde_json::Value::String(m.to_string()),
);
}
if let Some(a) = asset_id {
body_map.insert(
"asset_id".to_string(),
serde_json::Value::String(a.to_string()),
);
}
let body_str = serde_json::to_string(&body_map)
.map_err(|e| PolymarketError::parse(format!("Failed to serialize: {}", e)))?;
let address = self.get_auth_address();
let timestamp = get_current_unix_time_secs();
let headers = create_l2_headers_with_body_string(
&address, api_creds, "DELETE", endpoint, &body_str, timestamp,
)?;
debug!(market = ?market, asset_id = ?asset_id, "Cancelling market orders");
let mut req_builder = self
.client
.delete(&url)
.header("Content-Type", "application/json")
.body(body_str);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: CancelResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse cancel response: {e}"), e)
})?;
info!(market = ?market, asset_id = ?asset_id, cancelled = ?result.canceled, "Market orders cancelled");
Ok(result)
}
#[instrument(skip(self))]
pub async fn get_orderbook_summary(&self, token_id: &str) -> Result<Option<OrderBookSummary>> {
self.wait_for_rate_limit().await;
let endpoint = "/book";
let url = format!("{}{}?token_id={}", self.config.base_url, endpoint, token_id);
debug!(token_id = %token_id, "Fetching orderbook summary");
let response = self.client.get(&url).send().await?;
let status = response.status();
let body = response.text().await.unwrap_or_default();
if body.contains("does not exist") || body.contains("No orderbook") {
info!(token_id = %token_id, "Orderbook does not exist");
return Ok(None);
}
if status.as_u16() == 404 {
info!(token_id = %token_id, "Orderbook not found (404)");
return Ok(None);
}
if !status.is_success() {
return Err(PolymarketError::api(status.as_u16(), body));
}
let summary: OrderBookSummary = serde_json::from_str(&body).map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse orderbook summary: {e}"), e)
})?;
debug!(
token_id = %token_id,
neg_risk = %summary.neg_risk,
tick_size = %summary.tick_size,
min_order_size = %summary.min_order_size,
bids_count = %summary.bids.len(),
asks_count = %summary.asks.len(),
"Got orderbook summary"
);
Ok(Some(summary))
}
#[deprecated(
since = "0.2.0",
note = "Use get_orderbook_summary() instead, which returns neg_risk, tick_size, and min_order_size in a single API call"
)]
#[instrument(skip(self))]
pub async fn get_neg_risk(&self, token_id: &str) -> Result<bool> {
self.wait_for_rate_limit().await;
let endpoint = "/neg-risk";
let url = format!("{}{}?token_id={}", self.config.base_url, endpoint, token_id);
debug!(token_id = %token_id, "Querying neg_risk status");
let response = self.client.get(&url).send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: NegRiskResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse neg_risk response: {e}"), e)
})?;
debug!(token_id = %token_id, neg_risk = %result.neg_risk, "Got neg_risk status");
Ok(result.neg_risk)
}
#[deprecated(
since = "0.2.0",
note = "Use get_orderbook_summary() instead, which returns neg_risk, tick_size, and min_order_size in a single API call"
)]
#[instrument(skip(self))]
pub async fn get_tick_size(&self, token_id: &str) -> Result<String> {
self.wait_for_rate_limit().await;
let endpoint = "/tick-size";
let url = format!("{}{}?token_id={}", self.config.base_url, endpoint, token_id);
debug!(token_id = %token_id, "Querying tick size");
let response = self.client.get(&url).send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: TickSizeResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(
format!("Failed to parse tick_size response: {e}"),
e,
)
})?;
debug!(token_id = %token_id, tick_size = %result.minimum_tick_size, "Got tick size");
Ok(result.minimum_tick_size)
}
#[instrument(skip(self))]
pub async fn get_fee_rate(&self, token_id: &str) -> Result<u32> {
self.wait_for_rate_limit().await;
let endpoint = "/fee-rate";
let url = format!("{}{}?token_id={}", self.config.base_url, endpoint, token_id);
debug!(token_id = %token_id, "Querying fee rate");
let response = self.client.get(&url).send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: FeeRateResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(format!("Failed to parse fee_rate response: {e}"), e)
})?;
debug!(token_id = %token_id, fee_rate_bps = %result.base_fee, "Got fee rate");
Ok(result.base_fee)
}
#[deprecated(
since = "0.2.0",
note = "Use get_orderbook_summary() instead. Check for Some(...) vs None to determine if orderbook exists"
)]
#[instrument(skip(self))]
pub async fn check_orderbook_exists(&self, token_id: &str) -> Result<bool> {
self.wait_for_rate_limit().await;
let endpoint = "/book";
let url = format!("{}{}?token_id={}", self.config.base_url, endpoint, token_id);
debug!(token_id = %token_id, "Checking orderbook existence");
let response = self.client.get(&url).send().await?;
let status = response.status();
let body = response.text().await.unwrap_or_default();
if body.contains("does not exist") || body.contains("No orderbook") {
info!(token_id = %token_id, "Orderbook does not exist");
return Ok(false);
}
if !status.is_success() {
return Err(PolymarketError::api(status.as_u16(), body));
}
debug!(token_id = %token_id, "Orderbook exists");
Ok(true)
}
#[instrument(skip(self))]
pub async fn get_balance_allowance(
&self,
params: &BalanceAllowanceParams,
) -> Result<BalanceAllowanceResponse> {
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for querying balance allowance")
})?;
let endpoint = "/balance-allowance";
let query_string = params.to_query_string();
let url = format!("{}{}?{}", self.config.base_url, endpoint, query_string);
let address = self.get_auth_address();
let headers =
create_l2_headers_with_address::<String>(&address, api_creds, "GET", endpoint, None)?;
debug!(asset_type = %params.asset_type, "Getting balance allowance");
let mut req_builder = self.client.get(&url);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
let result: BalanceAllowanceResponse = response.json().await.map_err(|e| {
PolymarketError::parse_with_source(
format!("Failed to parse balance allowance response: {e}"),
e,
)
})?;
debug!(
balance = %result.balance,
allowance = %result.allowance,
"Got balance allowance"
);
Ok(result)
}
#[instrument(skip(self))]
pub async fn update_balance_allowance(
&self,
params: &BalanceAllowanceParams,
) -> Result<()> {
self.wait_for_rate_limit().await;
let api_creds = self.api_credentials.as_ref().ok_or_else(|| {
PolymarketError::config("API credentials required for updating balance allowance")
})?;
let endpoint = "/balance-allowance/update";
let query_string = params.to_query_string();
let url = format!("{}{}?{}", self.config.base_url, endpoint, query_string);
let address = self.get_auth_address();
let headers =
create_l2_headers_with_address::<String>(&address, api_creds, "GET", endpoint, None)?;
debug!(asset_type = %params.asset_type, "Updating balance allowance");
let mut req_builder = self.client.get(&url);
for (key, value) in &headers {
req_builder = req_builder.header(*key, value);
}
let response = req_builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(PolymarketError::api(status.as_u16(), body));
}
info!(asset_type = %params.asset_type, "Balance allowance updated");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clob_config_default() {
let config = ClobConfig::default();
assert_eq!(config.base_url, clob_api_url());
assert_eq!(config.timeout, Duration::from_secs(30));
assert_eq!(config.rate_limit_per_second, 5);
}
#[test]
fn test_clob_config_with_base_url() {
let config = ClobConfig::default().with_base_url("https://custom.example.com");
assert_eq!(config.base_url, "https://custom.example.com");
}
}