drm_exchange_limitless/
clob.rs

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/// Order side for Limitless CLOB
11#[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/// Order type for Limitless
27#[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/// Signed order for Limitless API
44#[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/// Request payload for creating an order
65#[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/// Order response from Limitless API
76#[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/// Order data from Limitless API
89#[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/// Orderbook level
108#[derive(Debug, Clone, Deserialize)]
109pub struct OrderbookLevel {
110    pub price: String,
111    pub size: String,
112}
113
114/// Orderbook response
115#[derive(Debug, Clone, Deserialize)]
116pub struct OrderbookResponse {
117    pub bids: Vec<OrderbookLevel>,
118    pub asks: Vec<OrderbookLevel>,
119}
120
121/// Position data from Limitless API
122#[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/// Balance data from Limitless API
134#[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/// User profile from login response
142#[derive(Debug, Clone, Deserialize)]
143pub struct UserProfile {
144    pub id: String,
145    #[serde(default)]
146    pub address: Option<String>,
147}
148
149/// Login response
150#[derive(Debug, Clone, Deserialize)]
151pub struct LoginResponse {
152    pub user: Option<UserProfile>,
153    pub id: Option<String>,
154}
155
156/// Limitless CLOB client for authenticated operations
157pub 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    /// Create a new CLOB client with private key
171    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    /// Get wallet address
193    pub fn address(&self) -> Address {
194        self.address
195    }
196
197    /// Check if authenticated
198    pub fn is_authenticated(&self) -> bool {
199        self.authenticated
200    }
201
202    /// Get owner ID from login
203    pub fn owner_id(&self) -> Option<&str> {
204        self.owner_id.as_deref()
205    }
206
207    /// Authenticate with Limitless API
208    pub async fn authenticate(&mut self) -> Result<(), LimitlessError> {
209        // Get signing message
210        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        // Sign the message using EIP-191 personal sign
234        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        // Login with signature
244        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        // Extract owner ID
262        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    /// Register token ID to slug mapping
271    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    /// Get market slug for token ID
280    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    /// Check if token is a No token (needs inverted orderbook)
285    pub fn is_no_token(&self, token_id: &str) -> bool {
286        self.no_tokens.contains(token_id)
287    }
288
289    /// Build and sign an order
290    #[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        // Generate salt (timestamp-based)
302        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; // +1 day offset
307
308        // Scale factors
309        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; // 0.001 * 1_000_000
313
314        // Scale inputs
315        let shares = (size * shares_scale as f64) as u64;
316        let price_int = (price * price_scale as f64) as u64;
317
318        // Align shares to tick
319        let shares_step = price_scale / price_tick;
320        let shares = (shares / shares_step) * shares_step;
321
322        // Calculate collateral
323        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                // BUY: Round UP
329                let collateral = numerator.div_ceil(denominator) as u64;
330                (collateral, shares)
331            }
332            LimitlessSide::Sell => {
333                // SELL: Round DOWN
334                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        // Compute order hash
343        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(), // expiration
352            U256::zero(), // nonce
353            U256::from(fee_rate_bps),
354            side.as_u8(),
355            0u8, // EOA signature type
356            exchange_address,
357        );
358
359        // Sign the hash
360        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        // Add price for GTC orders
383        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    /// Create and post an order
455    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    /// Cancel an order
497    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    /// Cancel all orders for a market
519    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    /// Get order by ID
543    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    /// Get open orders
568    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        // Response may be { "data": [...] } or just [...]
594        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    /// Get positions
614    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    /// Get balance
659    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}