polyte_clob/
client.rs

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    /// Create a new CLOB client with default configuration
33    pub fn new(private_key: impl Into<String>, credentials: Credentials) -> Result<Self> {
34        Self::builder(private_key, credentials)?.build()
35    }
36
37    /// Create a new CLOB client builder with required authentication
38    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    /// Get markets namespace
47    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    /// Get orders namespace
56    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    /// Get account namespace
68    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    /// Create an unsigned order from parameters
80    pub async fn create_order(&self, params: &CreateOrderParams) -> Result<Order> {
81        params.validate()?;
82
83        // Fetch market info for tick size
84        let market = self.markets().get(&params.token_id).send().await?;
85        let tick_size = TickSize::from(market.minimum_tick_size);
86
87        // Get fee rate
88        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        // Calculate amounts
102        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    /// Sign an order
122    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    /// Post a signed order
132    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    /// Create, sign, and post an order (convenience method)
152    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/// Parameters for creating an order
160#[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
190/// Builder for CLOB client
191pub 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    /// Create a new builder with required wallet and credentials
202    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    /// Set base URL for the API
214    pub fn base_url(mut self, url: impl Into<String>) -> Self {
215        self.base_url = url.into();
216        self
217    }
218
219    /// Set request timeout in milliseconds
220    pub fn timeout_ms(mut self, timeout: u64) -> Self {
221        self.timeout_ms = timeout;
222        self
223    }
224
225    /// Set connection pool size
226    pub fn pool_size(mut self, size: usize) -> Self {
227        self.pool_size = size;
228        self
229    }
230
231    /// Set chain
232    pub fn chain(mut self, chain: Chain) -> Self {
233        self.chain = chain;
234        self
235    }
236
237    /// Build the CLOB client
238    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        // Build L2 signer from credentials
247        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}