polyte_clob/
client.rs

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    /// Create a new CLOB client with default configuration
30    pub fn new(private_key: impl Into<String>, credentials: Credentials) -> Result<Self> {
31        Self::builder(private_key, credentials)?.build()
32    }
33
34    /// Create a new CLOB client builder with required authentication
35    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    /// Create a new CLOB client from an Account
44    pub fn from_account(account: Account) -> Result<Self> {
45        ClobBuilder::new(account).build()
46    }
47
48    /// Get a reference to the account
49    pub fn account(&self) -> &Account {
50        &self.account
51    }
52
53    /// Get markets namespace
54    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    /// Get orders namespace
63    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    /// Get account API namespace
75    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    /// Create an unsigned order from parameters
87    pub async fn create_order(&self, params: &CreateOrderParams) -> Result<Order> {
88        params.validate()?;
89
90        // Fetch market info for tick size
91        let market = self.markets().get(&params.token_id).send().await?;
92        let tick_size = TickSize::from(market.minimum_tick_size);
93
94        // Get fee rate
95        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        // Calculate amounts
109        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    /// Sign an order
129    pub async fn sign_order(&self, order: &Order) -> Result<SignedOrder> {
130        self.account.sign_order(order, self.chain_id).await
131    }
132
133    /// Post a signed order
134    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    /// Create, sign, and post an order (convenience method)
154    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/// Parameters for creating an order
162#[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
192/// Builder for CLOB client
193pub 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    /// Create a new builder with an Account
203    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    /// 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        Ok(Clob {
247            client,
248            base_url,
249            chain_id: self.chain.chain_id(),
250            account: self.account,
251        })
252    }
253}