1use std::time::Duration;
2
3use reqwest::Client;
4use url::Url;
5
6use crate::{
7 account::{Account, Credentials},
8 api::{account::AccountApi, orders::OrderResponse, Markets, Orders},
9 core::chain::Chain,
10 error::{ClobError, Result},
11 request::{AuthMode, Request},
12 types::*,
13 utils::{calculate_order_amounts, current_timestamp, generate_salt},
14};
15
16const DEFAULT_BASE_URL: &str = "https://clob.polymarket.com";
17const DEFAULT_TIMEOUT_MS: u64 = 30_000;
18const DEFAULT_POOL_SIZE: usize = 10;
19
20#[derive(Clone)]
21pub struct Clob {
22 pub(crate) client: Client,
23 pub(crate) base_url: Url,
24 pub(crate) chain_id: u64,
25 pub(crate) account: Account,
26}
27
28impl Clob {
29 pub fn new(private_key: impl Into<String>, credentials: Credentials) -> Result<Self> {
31 Self::builder(private_key, credentials)?.build()
32 }
33
34 pub fn builder(
36 private_key: impl Into<String>,
37 credentials: Credentials,
38 ) -> Result<ClobBuilder> {
39 let account = Account::new(private_key, credentials)?;
40 Ok(ClobBuilder::new(account))
41 }
42
43 pub fn from_account(account: Account) -> Result<Self> {
45 ClobBuilder::new(account).build()
46 }
47
48 pub fn account(&self) -> &Account {
50 &self.account
51 }
52
53 pub fn markets(&self) -> Markets {
55 Markets {
56 client: self.client.clone(),
57 base_url: self.base_url.clone(),
58 chain_id: self.chain_id,
59 }
60 }
61
62 pub fn orders(&self) -> Orders {
64 Orders {
65 client: self.client.clone(),
66 base_url: self.base_url.clone(),
67 wallet: self.account.wallet().clone(),
68 credentials: self.account.credentials().clone(),
69 signer: self.account.signer().clone(),
70 chain_id: self.chain_id,
71 }
72 }
73
74 pub fn account_api(&self) -> AccountApi {
76 AccountApi {
77 client: self.client.clone(),
78 base_url: self.base_url.clone(),
79 wallet: self.account.wallet().clone(),
80 credentials: self.account.credentials().clone(),
81 signer: self.account.signer().clone(),
82 chain_id: self.chain_id,
83 }
84 }
85
86 pub async fn create_order(&self, params: &CreateOrderParams) -> Result<Order> {
88 params.validate()?;
89
90 let market = self.markets().get(¶ms.token_id).send().await?;
92 let tick_size = TickSize::from(market.minimum_tick_size);
93
94 let fee_rate_response: serde_json::Value = self
96 .client
97 .get(self.base_url.join("/fee-rate")?)
98 .send()
99 .await?
100 .json()
101 .await?;
102
103 let fee_rate_bps = fee_rate_response["feeRateBps"]
104 .as_str()
105 .unwrap_or("0")
106 .to_string();
107
108 let (maker_amount, taker_amount) =
110 calculate_order_amounts(params.price, params.size, params.side, tick_size);
111
112 Ok(Order {
113 salt: generate_salt(),
114 maker: self.account.address(),
115 signer: self.account.address(),
116 taker: alloy::primitives::Address::ZERO,
117 token_id: params.token_id.clone(),
118 maker_amount,
119 taker_amount,
120 expiration: params.expiration.unwrap_or(0).to_string(),
121 nonce: current_timestamp().to_string(),
122 fee_rate_bps,
123 side: params.side,
124 signature_type: SignatureType::default(),
125 })
126 }
127
128 pub async fn sign_order(&self, order: &Order) -> Result<SignedOrder> {
130 self.account.sign_order(order, self.chain_id).await
131 }
132
133 pub async fn post_order(&self, signed_order: &SignedOrder) -> Result<OrderResponse> {
135 let auth = AuthMode::L2 {
136 address: self.account.address(),
137 credentials: self.account.credentials().clone(),
138 signer: self.account.signer().clone(),
139 };
140
141 Request::post(
142 self.client.clone(),
143 self.base_url.clone(),
144 "/order".to_string(),
145 auth,
146 self.chain_id,
147 )
148 .body(signed_order)?
149 .send()
150 .await
151 }
152
153 pub async fn place_order(&self, params: &CreateOrderParams) -> Result<OrderResponse> {
155 let order = self.create_order(params).await?;
156 let signed_order = self.sign_order(&order).await?;
157 self.post_order(&signed_order).await
158 }
159}
160
161#[derive(Debug, Clone)]
163pub struct CreateOrderParams {
164 pub token_id: String,
165 pub price: f64,
166 pub size: f64,
167 pub side: OrderSide,
168 pub expiration: Option<u64>,
169}
170
171impl CreateOrderParams {
172 pub fn validate(&self) -> Result<()> {
173 if self.price <= 0.0 || self.price > 1.0 {
174 return Err(ClobError::validation(format!(
175 "Price must be between 0.0 and 1.0, got {}",
176 self.price
177 )));
178 }
179 if self.size <= 0.0 {
180 return Err(ClobError::validation(format!(
181 "Size must be positive, got {}",
182 self.size
183 )));
184 }
185 if self.price.is_nan() || self.size.is_nan() {
186 return Err(ClobError::validation("NaN values not allowed"));
187 }
188 Ok(())
189 }
190}
191
192pub struct ClobBuilder {
194 base_url: String,
195 timeout_ms: u64,
196 pool_size: usize,
197 chain: Chain,
198 account: Account,
199}
200
201impl ClobBuilder {
202 pub fn new(account: Account) -> Self {
204 Self {
205 base_url: DEFAULT_BASE_URL.to_string(),
206 timeout_ms: DEFAULT_TIMEOUT_MS,
207 pool_size: DEFAULT_POOL_SIZE,
208 chain: Chain::PolygonMainnet,
209 account,
210 }
211 }
212
213 pub fn base_url(mut self, url: impl Into<String>) -> Self {
215 self.base_url = url.into();
216 self
217 }
218
219 pub fn timeout_ms(mut self, timeout: u64) -> Self {
221 self.timeout_ms = timeout;
222 self
223 }
224
225 pub fn pool_size(mut self, size: usize) -> Self {
227 self.pool_size = size;
228 self
229 }
230
231 pub fn chain(mut self, chain: Chain) -> Self {
233 self.chain = chain;
234 self
235 }
236
237 pub fn build(self) -> Result<Clob> {
239 let client = Client::builder()
240 .timeout(Duration::from_millis(self.timeout_ms))
241 .pool_max_idle_per_host(self.pool_size)
242 .build()?;
243
244 let base_url = Url::parse(&self.base_url)?;
245
246 Ok(Clob {
247 client,
248 base_url,
249 chain_id: self.chain.chain_id(),
250 account: self.account,
251 })
252 }
253}