use std::time::Duration;
use reqwest::Client;
use url::Url;
use crate::{
account::{Account, Credentials},
api::{account::AccountApi, orders::OrderResponse, Markets, Orders},
core::chain::Chain,
error::{ClobError, Result},
request::{AuthMode, Request},
types::*,
utils::{calculate_order_amounts, current_timestamp, generate_salt},
};
const DEFAULT_BASE_URL: &str = "https://clob.polymarket.com";
const DEFAULT_TIMEOUT_MS: u64 = 30_000;
const DEFAULT_POOL_SIZE: usize = 10;
#[derive(Clone)]
pub struct Clob {
pub(crate) client: Client,
pub(crate) base_url: Url,
pub(crate) chain_id: u64,
pub(crate) account: Account,
}
impl Clob {
pub fn new(private_key: impl Into<String>, credentials: Credentials) -> Result<Self> {
Self::builder(private_key, credentials)?.build()
}
pub fn builder(
private_key: impl Into<String>,
credentials: Credentials,
) -> Result<ClobBuilder> {
let account = Account::new(private_key, credentials)?;
Ok(ClobBuilder::new(account))
}
pub fn from_account(account: Account) -> Result<Self> {
ClobBuilder::new(account).build()
}
pub fn account(&self) -> &Account {
&self.account
}
pub fn markets(&self) -> Markets {
Markets {
client: self.client.clone(),
base_url: self.base_url.clone(),
chain_id: self.chain_id,
}
}
pub fn orders(&self) -> Orders {
Orders {
client: self.client.clone(),
base_url: self.base_url.clone(),
wallet: self.account.wallet().clone(),
credentials: self.account.credentials().clone(),
signer: self.account.signer().clone(),
chain_id: self.chain_id,
}
}
pub fn account_api(&self) -> AccountApi {
AccountApi {
client: self.client.clone(),
base_url: self.base_url.clone(),
wallet: self.account.wallet().clone(),
credentials: self.account.credentials().clone(),
signer: self.account.signer().clone(),
chain_id: self.chain_id,
}
}
pub async fn create_order(&self, params: &CreateOrderParams) -> Result<Order> {
params.validate()?;
let market = self.markets().get(¶ms.token_id).send().await?;
let tick_size = TickSize::from(market.minimum_tick_size);
let fee_rate_response: serde_json::Value = self
.client
.get(self.base_url.join("/fee-rate")?)
.send()
.await?
.json()
.await?;
let fee_rate_bps = fee_rate_response["feeRateBps"]
.as_str()
.unwrap_or("0")
.to_string();
let (maker_amount, taker_amount) =
calculate_order_amounts(params.price, params.size, params.side, tick_size);
Ok(Order {
salt: generate_salt(),
maker: self.account.address(),
signer: self.account.address(),
taker: alloy::primitives::Address::ZERO,
token_id: params.token_id.clone(),
maker_amount,
taker_amount,
expiration: params.expiration.unwrap_or(0).to_string(),
nonce: current_timestamp().to_string(),
fee_rate_bps,
side: params.side,
signature_type: SignatureType::default(),
})
}
pub async fn sign_order(&self, order: &Order) -> Result<SignedOrder> {
self.account.sign_order(order, self.chain_id).await
}
pub async fn post_order(&self, signed_order: &SignedOrder) -> Result<OrderResponse> {
let auth = AuthMode::L2 {
address: self.account.address(),
credentials: self.account.credentials().clone(),
signer: self.account.signer().clone(),
};
Request::post(
self.client.clone(),
self.base_url.clone(),
"/order".to_string(),
auth,
self.chain_id,
)
.body(signed_order)?
.send()
.await
}
pub async fn place_order(&self, params: &CreateOrderParams) -> Result<OrderResponse> {
let order = self.create_order(params).await?;
let signed_order = self.sign_order(&order).await?;
self.post_order(&signed_order).await
}
}
#[derive(Debug, Clone)]
pub struct CreateOrderParams {
pub token_id: String,
pub price: f64,
pub size: f64,
pub side: OrderSide,
pub expiration: Option<u64>,
}
impl CreateOrderParams {
pub fn validate(&self) -> Result<()> {
if self.price <= 0.0 || self.price > 1.0 {
return Err(ClobError::validation(format!(
"Price must be between 0.0 and 1.0, got {}",
self.price
)));
}
if self.size <= 0.0 {
return Err(ClobError::validation(format!(
"Size must be positive, got {}",
self.size
)));
}
if self.price.is_nan() || self.size.is_nan() {
return Err(ClobError::validation("NaN values not allowed"));
}
Ok(())
}
}
pub struct ClobBuilder {
base_url: String,
timeout_ms: u64,
pool_size: usize,
chain: Chain,
account: Account,
}
impl ClobBuilder {
pub fn new(account: Account) -> Self {
Self {
base_url: DEFAULT_BASE_URL.to_string(),
timeout_ms: DEFAULT_TIMEOUT_MS,
pool_size: DEFAULT_POOL_SIZE,
chain: Chain::PolygonMainnet,
account,
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub fn timeout_ms(mut self, timeout: u64) -> Self {
self.timeout_ms = timeout;
self
}
pub fn pool_size(mut self, size: usize) -> Self {
self.pool_size = size;
self
}
pub fn chain(mut self, chain: Chain) -> Self {
self.chain = chain;
self
}
pub fn build(self) -> Result<Clob> {
let client = Client::builder()
.timeout(Duration::from_millis(self.timeout_ms))
.pool_max_idle_per_host(self.pool_size)
.build()?;
let base_url = Url::parse(&self.base_url)?;
Ok(Clob {
client,
base_url,
chain_id: self.chain.chain_id(),
account: self.account,
})
}
}