1use crate::prelude::*;
34use crate::retry::RetryConfig;
35
36#[derive(Clone)]
50pub struct LimitlessClient {
51 client: Client,
53 config: Config,
55 retry_config: RetryConfig,
57}
58
59impl Limitless for LimitlessClient {
64 fn new(api_key: Option<String>, secret: Option<String>) -> Self {
65 Self::new_with_config(&Config::default(), api_key, secret)
66 }
67
68 fn new_with_config(config: &Config, api_key: Option<String>, secret: Option<String>) -> Self {
69 Self {
70 client: Client::new(
71 api_key.clone(),
72 secret.clone(),
73 config.rest_api_endpoint.to_string(),
74 ),
75 config: config.clone(),
76 retry_config: RetryConfig::default(),
77 }
78 }
79}
80
81impl LimitlessClient {
82 pub fn builder() -> LimitlessClientBuilder {
84 LimitlessClientBuilder::default()
85 }
86
87 pub fn raw_client(&self) -> &Client {
89 &self.client
90 }
91
92 pub fn set_credentials(&mut self, api_key: Option<String>, secret_key: Option<String>) {
94 self.client = Client::new(
95 api_key.clone(),
96 secret_key.clone(),
97 self.config.rest_api_endpoint.to_string(),
98 );
99 }
100
101 pub fn retry_config(&self) -> &RetryConfig {
103 &self.retry_config
104 }
105
106 pub fn base_url(&self) -> &str {
108 &self.config.rest_api_endpoint
109 }
110
111 pub fn markets(&self) -> Markets {
115 Markets::new_with_config(
116 &self.config,
117 self.client.api_key.clone(),
118 self.client.secret_key.clone(),
119 )
120 }
121
122 pub fn trader(&self) -> Trader {
124 Trader::new_with_config(
125 &self.config,
126 self.client.api_key.clone(),
127 self.client.secret_key.clone(),
128 )
129 }
130
131 pub fn portfolio(&self) -> Portfolio {
133 Portfolio::new_with_config(
134 &self.config,
135 self.client.api_key.clone(),
136 self.client.secret_key.clone(),
137 )
138 }
139
140 pub fn navigation(&self) -> Navigation {
142 Navigation::new_with_config(
143 &self.config,
144 self.client.api_key.clone(),
145 self.client.secret_key.clone(),
146 )
147 }
148
149 pub fn stream(&self) -> Stream {
151 Stream::new_with_config(
152 &self.config,
153 self.client.api_key.clone(),
154 self.client.secret_key.clone(),
155 )
156 }
157
158 pub async fn browse_active(
164 &self,
165 category_id: Option<u64>,
166 page: Option<u64>,
167 limit: Option<u64>,
168 sort_by: Option<String>,
169 trade_type: Option<String>,
170 automation_type: Option<String>,
171 ) -> Result<ActiveMarketsResponse, LimitlessError> {
172 self.markets()
173 .browse_active(
174 category_id,
175 page,
176 limit,
177 sort_by,
178 trade_type,
179 automation_type,
180 )
181 .await
182 }
183
184 pub async fn get_category_counts(&self) -> Result<CategoryCountResponse, LimitlessError> {
186 self.markets().get_category_counts().await
187 }
188
189 pub async fn get_active_slugs(&self) -> Result<Vec<ActiveSlug>, LimitlessError> {
191 self.markets().get_active_slugs().await
192 }
193
194 pub async fn get_market(&self, address_or_slug: &str) -> Result<MarketDetail, LimitlessError> {
196 self.markets().get_market(address_or_slug).await
197 }
198
199 pub async fn get_oracle_candles(
201 &self,
202 address_or_slug: &str,
203 interval: Option<&str>,
204 from: Option<u64>,
205 to: Option<u64>,
206 ) -> Result<OracleCandlesResponse, LimitlessError> {
207 self.markets()
208 .get_oracle_candles(address_or_slug, interval, from, to)
209 .await
210 }
211
212 pub async fn get_feed_events(
214 &self,
215 slug: &str,
216 page: Option<u64>,
217 limit: Option<u64>,
218 ) -> Result<FeedEventsResponse, LimitlessError> {
219 self.markets().get_feed_events(slug, page, limit).await
220 }
221
222 pub async fn search_markets(
224 &self,
225 query: &str,
226 limit: Option<u64>,
227 page: Option<u64>,
228 similarity_threshold: Option<f64>,
229 ) -> Result<SearchResponse, LimitlessError> {
230 self.markets()
231 .search(query, limit, page, similarity_threshold)
232 .await
233 }
234
235 pub async fn create_order(
241 &self,
242 order_request: &str,
243 ) -> Result<CreateOrderResponse, LimitlessError> {
244 self.trader().create_order(order_request).await
245 }
246
247 pub async fn order_status_batch(
249 &self,
250 request_body: &str,
251 ) -> Result<OrderStatusBatchResponse, LimitlessError> {
252 self.trader().order_status_batch(request_body).await
253 }
254
255 pub async fn cancel_combined(
257 &self,
258 request_body: &str,
259 ) -> Result<CancelOrderResponse, LimitlessError> {
260 self.trader().cancel_combined(request_body).await
261 }
262
263 pub async fn cancel_batch(
265 &self,
266 request_body: &str,
267 ) -> Result<CancelBatchResponse, LimitlessError> {
268 self.trader().cancel_batch(request_body).await
269 }
270
271 pub async fn cancel_order_by_id(
273 &self,
274 order_id: &str,
275 ) -> Result<CancelOrderResponse, LimitlessError> {
276 self.trader().cancel_order_by_id(order_id).await
277 }
278
279 pub async fn cancel_all_in_market(
281 &self,
282 slug: &str,
283 ) -> Result<CancelAllResponse, LimitlessError> {
284 self.trader().cancel_all_in_market(slug).await
285 }
286
287 pub async fn get_orderbook(&self, slug: &str) -> Result<OrderbookResponse, LimitlessError> {
289 self.trader().get_orderbook(slug).await
290 }
291
292 pub async fn get_historical_prices(
294 &self,
295 slug: &str,
296 interval: Option<&str>,
297 ) -> Result<Vec<HistoricalPriceData>, LimitlessError> {
298 self.trader().get_historical_prices(slug, interval).await
299 }
300
301 pub async fn get_locked_balance(
303 &self,
304 slug: &str,
305 ) -> Result<LockedBalanceResponse, LimitlessError> {
306 self.trader().get_locked_balance(slug).await
307 }
308
309 pub async fn get_user_orders(
311 &self,
312 slug: &str,
313 statuses: Option<&[&str]>,
314 limit: Option<u64>,
315 ) -> Result<UserOrdersResponse, LimitlessError> {
316 self.trader().get_user_orders(slug, statuses, limit).await
317 }
318
319 pub async fn get_market_events(
321 &self,
322 slug: &str,
323 page: Option<u64>,
324 limit: Option<u64>,
325 ) -> Result<MarketEventsResponse, LimitlessError> {
326 self.trader().get_market_events(slug, page, limit).await
327 }
328
329 pub async fn buy_gtc(
335 &self,
336 private_key: &str,
337 market_slug: &str,
338 token_id: &str,
339 price: f64,
340 size: f64,
341 owner_id: u64,
342 ) -> Result<CreateOrderResponse, LimitlessError> {
343 self.trader()
344 .buy_gtc(private_key, market_slug, token_id, price, size, owner_id)
345 .await
346 }
347
348 pub async fn sell_gtc(
350 &self,
351 private_key: &str,
352 market_slug: &str,
353 token_id: &str,
354 price: f64,
355 size: f64,
356 owner_id: u64,
357 ) -> Result<CreateOrderResponse, LimitlessError> {
358 self.trader()
359 .sell_gtc(private_key, market_slug, token_id, price, size, owner_id)
360 .await
361 }
362
363 pub async fn buy_fok(
365 &self,
366 private_key: &str,
367 market_slug: &str,
368 token_id: &str,
369 usdc_amount: f64,
370 owner_id: u64,
371 ) -> Result<CreateOrderResponse, LimitlessError> {
372 self.trader()
373 .buy_fok(private_key, market_slug, token_id, usdc_amount, owner_id)
374 .await
375 }
376
377 pub async fn sell_fok(
379 &self,
380 private_key: &str,
381 market_slug: &str,
382 token_id: &str,
383 share_amount: f64,
384 owner_id: u64,
385 ) -> Result<CreateOrderResponse, LimitlessError> {
386 self.trader()
387 .sell_fok(private_key, market_slug, token_id, share_amount, owner_id)
388 .await
389 }
390
391 pub async fn cancel_all(&self, slug: &str) -> Result<CancelAllResponse, LimitlessError> {
393 self.trader().cancel_all(slug).await
394 }
395
396 pub async fn get_profile(&self, account: &str) -> Result<ProfileResponse, LimitlessError> {
402 self.portfolio().get_profile(account).await
403 }
404
405 pub async fn get_trades(&self) -> Result<Vec<TradeEntry>, LimitlessError> {
407 self.portfolio().get_trades().await
408 }
409
410 pub async fn get_positions(&self) -> Result<PositionsResponse, LimitlessError> {
412 self.portfolio().get_positions().await
413 }
414
415 pub async fn get_pnl_chart(
417 &self,
418 timeframe: Option<&str>,
419 ) -> Result<PnlChartResponse, LimitlessError> {
420 self.portfolio().get_pnl_chart(timeframe).await
421 }
422
423 pub async fn get_points(&self) -> Result<PointsResponse, LimitlessError> {
425 self.portfolio().get_points().await
426 }
427
428 pub async fn get_history(
430 &self,
431 cursor: Option<&str>,
432 limit: Option<u64>,
433 ) -> Result<HistoryResponse, LimitlessError> {
434 self.portfolio().get_history(cursor, limit).await
435 }
436
437 pub async fn get_allowance(
439 &self,
440 allowance_type: &str,
441 spender: Option<&str>,
442 ) -> Result<AllowanceResponse, LimitlessError> {
443 self.portfolio()
444 .get_allowance(allowance_type, spender)
445 .await
446 }
447
448 pub async fn get_navigation_tree(&self) -> Result<Vec<NavigationNode>, LimitlessError> {
454 self.navigation().get_navigation_tree().await
455 }
456
457 pub async fn get_page_by_path(&self, path: &str) -> Result<MarketPage, LimitlessError> {
459 self.navigation().get_page_by_path(path).await
460 }
461
462 pub async fn list_page_markets(
464 &self,
465 page_id: &str,
466 cursor: Option<&str>,
467 page: Option<u64>,
468 limit: Option<u64>,
469 sort_by: Option<&str>,
470 filters: Option<&BTreeMap<String, String>>,
471 ) -> Result<PageMarketsResponse, LimitlessError> {
472 self.navigation()
473 .list_page_markets(page_id, cursor, page, limit, sort_by, filters)
474 .await
475 }
476
477 pub async fn list_property_keys(&self) -> Result<Vec<PropertyKey>, LimitlessError> {
479 self.navigation().list_property_keys().await
480 }
481
482 pub async fn get_property_key(&self, key_id: &str) -> Result<PropertyKey, LimitlessError> {
484 self.navigation().get_property_key(key_id).await
485 }
486
487 pub async fn list_property_options(
489 &self,
490 key_id: &str,
491 parent_id: Option<&str>,
492 ) -> Result<Vec<PropertyOption>, LimitlessError> {
493 self.navigation()
494 .list_property_options(key_id, parent_id)
495 .await
496 }
497
498 pub async fn ws_ping(&self) -> Result<(), LimitlessError> {
504 self.stream().ws_ping().await
505 }
506
507 pub async fn ws_subscribe<F>(&self, handler: F) -> Result<(), LimitlessError>
509 where
510 F: FnMut(Value) -> Result<(), LimitlessError> + 'static + Send,
511 {
512 self.stream().ws_subscribe(handler).await
513 }
514
515 pub async fn ws_subscribe_with_commands<F>(
517 &self,
518 cmd_receiver: tokio::sync::mpsc::UnboundedReceiver<String>,
519 handler: F,
520 ) -> Result<(), LimitlessError>
521 where
522 F: FnMut(Value) -> Result<(), LimitlessError> + 'static + Send,
523 {
524 self.stream()
525 .ws_subscribe_with_commands(cmd_receiver, handler)
526 .await
527 }
528
529 pub async fn ws_subscribe_authenticated_with_commands<F>(
533 &self,
534 cmd_receiver: tokio::sync::mpsc::UnboundedReceiver<String>,
535 handler: F,
536 ) -> Result<(), LimitlessError>
537 where
538 F: FnMut(Value) -> Result<(), LimitlessError> + 'static + Send,
539 {
540 self.stream()
541 .ws_subscribe_authenticated_with_commands(cmd_receiver, handler)
542 .await
543 }
544
545 pub async fn ws_subscribe_authenticated_events<F>(
549 &self,
550 handler: F,
551 ) -> Result<(), LimitlessError>
552 where
553 F: FnMut(WsEventKind) -> Result<(), LimitlessError> + 'static + Send,
554 {
555 self.stream()
556 .ws_subscribe_authenticated_events(handler)
557 .await
558 }
559}
560
561#[derive(Default)]
579pub struct LimitlessClientBuilder {
580 api_key: Option<String>,
581 secret_key: Option<String>,
582 rest_endpoint: Option<String>,
583 ws_endpoint: Option<String>,
584 recv_window: Option<u64>,
585 retry_config: Option<RetryConfig>,
586}
587
588impl LimitlessClientBuilder {
589 pub fn api_key(mut self, key: impl Into<String>) -> Self {
591 self.api_key = Some(key.into());
592 self
593 }
594
595 pub fn secret(mut self, secret: impl Into<String>) -> Self {
597 self.secret_key = Some(secret.into());
598 self
599 }
600
601 pub fn rest_endpoint(mut self, url: impl Into<String>) -> Self {
603 self.rest_endpoint = Some(url.into());
604 self
605 }
606
607 pub fn ws_endpoint(mut self, url: impl Into<String>) -> Self {
609 self.ws_endpoint = Some(url.into());
610 self
611 }
612
613 pub fn recv_window(mut self, ms: u64) -> Self {
615 self.recv_window = Some(ms);
616 self
617 }
618
619 pub fn testnet(mut self, use_testnet: bool) -> Self {
621 if use_testnet {
622 self.rest_endpoint = Some("https://api.testnet.limitless.exchange".into());
623 self.ws_endpoint = Some("wss://ws.testnet.limitless.exchange/markets".into());
624 }
625 self
626 }
627
628 pub fn retry_config(mut self, config: RetryConfig) -> Self {
630 self.retry_config = Some(config);
631 self
632 }
633
634 pub fn no_retry(mut self) -> Self {
636 self.retry_config = Some(RetryConfig::none());
637 self
638 }
639
640 pub fn build(self) -> Result<LimitlessClient, LimitlessError> {
644 let api_key = self
645 .api_key
646 .or_else(|| std::env::var("LIMITLESS_API_KEY").ok())
647 .filter(|k| !k.is_empty());
648
649 let secret_key = self
650 .secret_key
651 .or_else(|| std::env::var("LIMITLESS_API_SECRET").ok())
652 .filter(|s| !s.is_empty());
653
654 let rest_endpoint = self
655 .rest_endpoint
656 .unwrap_or_else(|| Config::DEFAULT_REST_API_ENDPOINT.into());
657
658 let ws_endpoint = self
659 .ws_endpoint
660 .unwrap_or_else(|| Config::DEFAULT_WS_ENDPOINT.into());
661
662 let recv_window = self.recv_window.unwrap_or(5000);
663
664 let config = Config::new(rest_endpoint, ws_endpoint, recv_window);
665 let retry_config = self.retry_config.unwrap_or_default();
666
667 if api_key.is_none() && secret_key.is_none() {
668 log::warn!(
669 "No API credentials provided — authenticated endpoints will fail. \
670 Set LIMITLESS_API_KEY + LIMITLESS_API_SECRET environment variables \
671 or pass credentials to the builder."
672 );
673 }
674
675 Ok(LimitlessClient {
676 client: Client::new(
677 api_key.clone(),
678 secret_key.clone(),
679 config.rest_api_endpoint.to_string(),
680 ),
681 config,
682 retry_config,
683 })
684 }
685}
686
687impl Default for LimitlessClient {
690 fn default() -> Self {
691 Self::builder()
692 .build()
693 .expect("Failed to create default LimitlessClient")
694 }
695}