1use ethers::prelude::*;
2use ethers::utils::keccak256;
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::config::CHAIN_ID;
8use crate::error::LimitlessError;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LimitlessSide {
13 Buy,
14 Sell,
15}
16
17impl LimitlessSide {
18 pub fn as_u8(&self) -> u8 {
19 match self {
20 LimitlessSide::Buy => 0,
21 LimitlessSide::Sell => 1,
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, Default)]
28pub enum LimitlessOrderType {
29 #[default]
30 Gtc,
31 Fok,
32}
33
34impl LimitlessOrderType {
35 pub fn as_str(&self) -> &'static str {
36 match self {
37 LimitlessOrderType::Gtc => "GTC",
38 LimitlessOrderType::Fok => "FOK",
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct SignedOrder {
47 pub salt: u64,
48 pub maker: String,
49 pub signer: String,
50 pub taker: String,
51 pub token_id: String,
52 pub maker_amount: u64,
53 pub taker_amount: u64,
54 pub expiration: String,
55 pub nonce: u64,
56 pub fee_rate_bps: u64,
57 pub side: u8,
58 pub signature_type: u8,
59 pub signature: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub price: Option<f64>,
62}
63
64#[derive(Debug, Clone, Serialize)]
66#[serde(rename_all = "camelCase")]
67pub struct CreateOrderRequest {
68 pub order: SignedOrder,
69 pub order_type: String,
70 pub market_slug: String,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub owner_id: Option<String>,
73}
74
75#[derive(Debug, Clone, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct OrderResponse {
79 pub id: Option<String>,
80 #[serde(rename = "orderId")]
81 pub order_id: Option<String>,
82 pub status: Option<String>,
83 pub filled: Option<f64>,
84 #[serde(rename = "errorMsg")]
85 pub error_msg: Option<String>,
86}
87
88#[derive(Debug, Clone, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct LimitlessOrderData {
92 pub id: Option<String>,
93 #[serde(rename = "orderId")]
94 pub order_id: Option<String>,
95 pub market_slug: Option<String>,
96 pub token_id: Option<String>,
97 pub side: Option<String>,
98 pub price: Option<f64>,
99 pub size: Option<f64>,
100 pub original_size: Option<f64>,
101 pub filled: Option<f64>,
102 pub status: Option<String>,
103 pub created_at: Option<String>,
104 pub updated_at: Option<String>,
105}
106
107#[derive(Debug, Clone, Deserialize)]
109pub struct OrderbookLevel {
110 pub price: String,
111 pub size: String,
112}
113
114#[derive(Debug, Clone, Deserialize)]
116pub struct OrderbookResponse {
117 pub bids: Vec<OrderbookLevel>,
118 pub asks: Vec<OrderbookLevel>,
119}
120
121#[derive(Debug, Clone, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct LimitlessPosition {
125 pub market_slug: Option<String>,
126 pub token_id: Option<String>,
127 pub outcome: Option<String>,
128 pub size: Option<f64>,
129 pub average_price: Option<f64>,
130 pub current_price: Option<f64>,
131}
132
133#[derive(Debug, Clone, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct BalanceResponse {
137 pub balance: Option<String>,
138 pub available: Option<String>,
139}
140
141#[derive(Debug, Clone, Deserialize)]
143pub struct UserProfile {
144 pub id: String,
145 #[serde(default)]
146 pub address: Option<String>,
147}
148
149#[derive(Debug, Clone, Deserialize)]
151pub struct LoginResponse {
152 pub user: Option<UserProfile>,
153 pub id: Option<String>,
154}
155
156pub struct LimitlessClobClient {
158 http: reqwest::Client,
159 wallet: LocalWallet,
160 address: Address,
161 host: String,
162 chain_id: u64,
163 authenticated: bool,
164 owner_id: Option<String>,
165 token_to_slug: std::collections::HashMap<String, String>,
166 no_tokens: HashSet<String>,
167}
168
169impl LimitlessClobClient {
170 pub fn new(private_key: &str, host: &str) -> Result<Self, LimitlessError> {
172 let wallet: LocalWallet = private_key
173 .parse()
174 .map_err(|e| LimitlessError::Auth(format!("invalid private key: {e}")))?;
175
176 let wallet = wallet.with_chain_id(CHAIN_ID);
177 let address = wallet.address();
178
179 Ok(Self {
180 http: reqwest::Client::new(),
181 wallet,
182 address,
183 host: host.to_string(),
184 chain_id: CHAIN_ID,
185 authenticated: false,
186 owner_id: None,
187 token_to_slug: std::collections::HashMap::new(),
188 no_tokens: HashSet::new(),
189 })
190 }
191
192 pub fn address(&self) -> Address {
194 self.address
195 }
196
197 pub fn is_authenticated(&self) -> bool {
199 self.authenticated
200 }
201
202 pub fn owner_id(&self) -> Option<&str> {
204 self.owner_id.as_deref()
205 }
206
207 pub async fn authenticate(&mut self) -> Result<(), LimitlessError> {
209 let url = format!("{}/auth/signing-message", self.host);
211 let response = self
212 .http
213 .get(&url)
214 .send()
215 .await
216 .map_err(LimitlessError::Http)?;
217
218 if !response.status().is_success() {
219 return Err(LimitlessError::Auth("failed to get signing message".into()));
220 }
221
222 let message = response
223 .text()
224 .await
225 .map_err(LimitlessError::Http)?
226 .trim()
227 .to_string();
228
229 if message.is_empty() {
230 return Err(LimitlessError::Auth("empty signing message".into()));
231 }
232
233 let signature = self
235 .wallet
236 .sign_message(&message)
237 .await
238 .map_err(|e| LimitlessError::Auth(format!("signing failed: {e}")))?;
239
240 let sig_hex = format!("0x{}", hex::encode(signature.to_vec()));
241 let message_hex = format!("0x{}", hex::encode(message.as_bytes()));
242
243 let login_url = format!("{}/auth/login", self.host);
245 let login_response = self
246 .http
247 .post(&login_url)
248 .header("x-account", format!("{:?}", self.address))
249 .header("x-signing-message", message_hex)
250 .header("x-signature", sig_hex)
251 .json(&serde_json::json!({"client": "eoa"}))
252 .send()
253 .await
254 .map_err(LimitlessError::Http)?;
255
256 if !login_response.status().is_success() {
257 let text = login_response.text().await.unwrap_or_default();
258 return Err(LimitlessError::Auth(format!("login failed: {text}")));
259 }
260
261 if let Ok(login_data) = login_response.json::<LoginResponse>().await {
263 self.owner_id = login_data.user.map(|u| u.id).or(login_data.id);
264 }
265
266 self.authenticated = true;
267 Ok(())
268 }
269
270 pub fn register_token_mapping(&mut self, token_id: &str, slug: &str, is_no_token: bool) {
272 self.token_to_slug
273 .insert(token_id.to_string(), slug.to_string());
274 if is_no_token {
275 self.no_tokens.insert(token_id.to_string());
276 }
277 }
278
279 pub fn get_slug_for_token(&self, token_id: &str) -> Option<&str> {
281 self.token_to_slug.get(token_id).map(|s| s.as_str())
282 }
283
284 pub fn is_no_token(&self, token_id: &str) -> bool {
286 self.no_tokens.contains(token_id)
287 }
288
289 #[allow(clippy::too_many_arguments)]
291 pub fn build_signed_order(
292 &self,
293 token_id: &str,
294 price: f64,
295 size: f64,
296 side: LimitlessSide,
297 order_type: LimitlessOrderType,
298 exchange_address: &str,
299 fee_rate_bps: u64,
300 ) -> Result<SignedOrder, LimitlessError> {
301 let timestamp_ms = SystemTime::now()
303 .duration_since(UNIX_EPOCH)
304 .unwrap()
305 .as_millis() as u64;
306 let salt = timestamp_ms * 1000 + (timestamp_ms % 1000) + 86400000; let shares_scale: u64 = 1_000_000;
310 let collateral_scale: u64 = 1_000_000;
311 let price_scale: u64 = 1_000_000;
312 let price_tick: u64 = 1000; let shares = (size * shares_scale as f64) as u64;
316 let price_int = (price * price_scale as f64) as u64;
317
318 let shares_step = price_scale / price_tick;
320 let shares = (shares / shares_step) * shares_step;
321
322 let numerator = shares as u128 * price_int as u128 * collateral_scale as u128;
324 let denominator = shares_scale as u128 * price_scale as u128;
325
326 let (maker_amount, taker_amount) = match side {
327 LimitlessSide::Buy => {
328 let collateral = numerator.div_ceil(denominator) as u64;
330 (collateral, shares)
331 }
332 LimitlessSide::Sell => {
333 let collateral = (numerator / denominator) as u64;
335 (shares, collateral)
336 }
337 };
338
339 let token_id_u256 = U256::from_dec_str(token_id)
340 .map_err(|e| LimitlessError::Api(format!("invalid token_id: {e}")))?;
341
342 let order_hash = self.compute_order_hash(
344 U256::from(salt),
345 self.address,
346 self.address,
347 Address::zero(),
348 token_id_u256,
349 U256::from(maker_amount),
350 U256::from(taker_amount),
351 U256::zero(), U256::zero(), U256::from(fee_rate_bps),
354 side.as_u8(),
355 0u8, exchange_address,
357 );
358
359 let signature = self
361 .wallet
362 .sign_hash(order_hash.into())
363 .map_err(|e| LimitlessError::Auth(format!("signing failed: {e}")))?;
364
365 let mut order = SignedOrder {
366 salt,
367 maker: format!("{:?}", self.address),
368 signer: format!("{:?}", self.address),
369 taker: format!("{:?}", Address::zero()),
370 token_id: token_id.to_string(),
371 maker_amount,
372 taker_amount,
373 expiration: "0".to_string(),
374 nonce: 0,
375 fee_rate_bps,
376 side: side.as_u8(),
377 signature_type: 0,
378 signature: format!("0x{}", hex::encode(signature.to_vec())),
379 price: None,
380 };
381
382 if matches!(order_type, LimitlessOrderType::Gtc) {
384 order.price = Some((price * 1000.0).round() / 1000.0);
385 }
386
387 Ok(order)
388 }
389
390 #[allow(clippy::too_many_arguments)]
391 fn compute_order_hash(
392 &self,
393 salt: U256,
394 maker: Address,
395 signer: Address,
396 taker: Address,
397 token_id: U256,
398 maker_amount: U256,
399 taker_amount: U256,
400 expiration: U256,
401 nonce: U256,
402 fee_rate_bps: U256,
403 side: u8,
404 signature_type: u8,
405 exchange_address: &str,
406 ) -> [u8; 32] {
407 let order_type_hash = keccak256(
408 b"Order(uint256 salt,address maker,address signer,address taker,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint256 expiration,uint256 nonce,uint256 feeRateBps,uint8 side,uint8 signatureType)"
409 );
410
411 let domain_separator = self.compute_domain_separator(exchange_address);
412
413 let struct_hash = keccak256(ethers::abi::encode(&[
414 ethers::abi::Token::FixedBytes(order_type_hash.to_vec()),
415 ethers::abi::Token::Uint(salt),
416 ethers::abi::Token::Address(maker),
417 ethers::abi::Token::Address(signer),
418 ethers::abi::Token::Address(taker),
419 ethers::abi::Token::Uint(token_id),
420 ethers::abi::Token::Uint(maker_amount),
421 ethers::abi::Token::Uint(taker_amount),
422 ethers::abi::Token::Uint(expiration),
423 ethers::abi::Token::Uint(nonce),
424 ethers::abi::Token::Uint(fee_rate_bps),
425 ethers::abi::Token::Uint(U256::from(side)),
426 ethers::abi::Token::Uint(U256::from(signature_type)),
427 ]));
428
429 let mut payload = vec![0x19, 0x01];
430 payload.extend_from_slice(&domain_separator);
431 payload.extend_from_slice(&struct_hash);
432
433 keccak256(&payload)
434 }
435
436 fn compute_domain_separator(&self, exchange_address: &str) -> [u8; 32] {
437 let domain_type_hash = keccak256(
438 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
439 );
440
441 let name_hash = keccak256(b"Limitless CTF Exchange");
442 let version_hash = keccak256(b"1");
443 let contract: Address = exchange_address.parse().expect("invalid exchange address");
444
445 keccak256(ethers::abi::encode(&[
446 ethers::abi::Token::FixedBytes(domain_type_hash.to_vec()),
447 ethers::abi::Token::FixedBytes(name_hash.to_vec()),
448 ethers::abi::Token::FixedBytes(version_hash.to_vec()),
449 ethers::abi::Token::Uint(U256::from(self.chain_id)),
450 ethers::abi::Token::Address(contract),
451 ]))
452 }
453
454 pub async fn post_order(
456 &self,
457 order: SignedOrder,
458 order_type: LimitlessOrderType,
459 market_slug: &str,
460 ) -> Result<OrderResponse, LimitlessError> {
461 if !self.authenticated {
462 return Err(LimitlessError::AuthRequired);
463 }
464
465 let request = CreateOrderRequest {
466 order,
467 order_type: order_type.as_str().to_string(),
468 market_slug: market_slug.to_string(),
469 owner_id: self.owner_id.clone(),
470 };
471
472 let url = format!("{}/orders", self.host);
473 let response = self
474 .http
475 .post(&url)
476 .json(&request)
477 .send()
478 .await
479 .map_err(LimitlessError::Http)?;
480
481 if response.status() == 429 {
482 return Err(LimitlessError::RateLimited);
483 }
484
485 if !response.status().is_success() {
486 let text = response.text().await.unwrap_or_default();
487 return Err(LimitlessError::Api(format!("post order failed: {text}")));
488 }
489
490 response
491 .json()
492 .await
493 .map_err(|e| LimitlessError::Api(format!("parse response failed: {e}")))
494 }
495
496 pub async fn cancel_order(&self, order_id: &str) -> Result<(), LimitlessError> {
498 if !self.authenticated {
499 return Err(LimitlessError::AuthRequired);
500 }
501
502 let url = format!("{}/orders/{}", self.host, order_id);
503 let response = self
504 .http
505 .delete(&url)
506 .send()
507 .await
508 .map_err(LimitlessError::Http)?;
509
510 if !response.status().is_success() {
511 let text = response.text().await.unwrap_or_default();
512 return Err(LimitlessError::Api(format!("cancel order failed: {text}")));
513 }
514
515 Ok(())
516 }
517
518 pub async fn cancel_all_orders(&self, market_slug: &str) -> Result<(), LimitlessError> {
520 if !self.authenticated {
521 return Err(LimitlessError::AuthRequired);
522 }
523
524 let url = format!("{}/orders/all/{}", self.host, market_slug);
525 let response = self
526 .http
527 .delete(&url)
528 .send()
529 .await
530 .map_err(LimitlessError::Http)?;
531
532 if !response.status().is_success() {
533 let text = response.text().await.unwrap_or_default();
534 return Err(LimitlessError::Api(format!(
535 "cancel all orders failed: {text}"
536 )));
537 }
538
539 Ok(())
540 }
541
542 pub async fn get_order(&self, order_id: &str) -> Result<LimitlessOrderData, LimitlessError> {
544 if !self.authenticated {
545 return Err(LimitlessError::AuthRequired);
546 }
547
548 let url = format!("{}/orders/{}", self.host, order_id);
549 let response = self
550 .http
551 .get(&url)
552 .send()
553 .await
554 .map_err(LimitlessError::Http)?;
555
556 if !response.status().is_success() {
557 let text = response.text().await.unwrap_or_default();
558 return Err(LimitlessError::Api(format!("get order failed: {text}")));
559 }
560
561 response
562 .json()
563 .await
564 .map_err(|e| LimitlessError::Api(format!("parse order failed: {e}")))
565 }
566
567 pub async fn get_open_orders(
569 &self,
570 market_slug: Option<&str>,
571 ) -> Result<Vec<LimitlessOrderData>, LimitlessError> {
572 if !self.authenticated {
573 return Err(LimitlessError::AuthRequired);
574 }
575
576 let mut url = format!("{}/orders", self.host);
577 if let Some(slug) = market_slug {
578 url.push_str(&format!("?marketSlug={slug}"));
579 }
580
581 let response = self
582 .http
583 .get(&url)
584 .send()
585 .await
586 .map_err(LimitlessError::Http)?;
587
588 if !response.status().is_success() {
589 let text = response.text().await.unwrap_or_default();
590 return Err(LimitlessError::Api(format!("get orders failed: {text}")));
591 }
592
593 let data: serde_json::Value = response
595 .json()
596 .await
597 .map_err(|e| LimitlessError::Api(format!("parse orders failed: {e}")))?;
598
599 let orders_arr = data
600 .get("data")
601 .and_then(|v| v.as_array())
602 .cloned()
603 .unwrap_or_else(|| data.as_array().cloned().unwrap_or_default());
604
605 let orders: Vec<LimitlessOrderData> = orders_arr
606 .into_iter()
607 .filter_map(|v| serde_json::from_value(v).ok())
608 .collect();
609
610 Ok(orders)
611 }
612
613 pub async fn get_positions(
615 &self,
616 market_slug: Option<&str>,
617 ) -> Result<Vec<LimitlessPosition>, LimitlessError> {
618 if !self.authenticated {
619 return Err(LimitlessError::AuthRequired);
620 }
621
622 let mut url = format!("{}/positions", self.host);
623 if let Some(slug) = market_slug {
624 url.push_str(&format!("?marketSlug={slug}"));
625 }
626
627 let response = self
628 .http
629 .get(&url)
630 .send()
631 .await
632 .map_err(LimitlessError::Http)?;
633
634 if !response.status().is_success() {
635 let text = response.text().await.unwrap_or_default();
636 return Err(LimitlessError::Api(format!("get positions failed: {text}")));
637 }
638
639 let data: serde_json::Value = response
640 .json()
641 .await
642 .map_err(|e| LimitlessError::Api(format!("parse positions failed: {e}")))?;
643
644 let positions_arr = data
645 .get("data")
646 .and_then(|v| v.as_array())
647 .cloned()
648 .unwrap_or_else(|| data.as_array().cloned().unwrap_or_default());
649
650 let positions: Vec<LimitlessPosition> = positions_arr
651 .into_iter()
652 .filter_map(|v| serde_json::from_value(v).ok())
653 .collect();
654
655 Ok(positions)
656 }
657
658 pub async fn get_balance(&self) -> Result<BalanceResponse, LimitlessError> {
660 if !self.authenticated {
661 return Err(LimitlessError::AuthRequired);
662 }
663
664 let url = format!("{}/balance", self.host);
665 let response = self
666 .http
667 .get(&url)
668 .send()
669 .await
670 .map_err(LimitlessError::Http)?;
671
672 if !response.status().is_success() {
673 let text = response.text().await.unwrap_or_default();
674 return Err(LimitlessError::Api(format!("get balance failed: {text}")));
675 }
676
677 response
678 .json()
679 .await
680 .map_err(|e| LimitlessError::Api(format!("parse balance failed: {e}")))
681 }
682}