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