use crate::prelude::*;
use crate::retry::RetryConfig;
#[derive(Clone)]
pub struct LimitlessClient {
client: Client,
config: Config,
retry_config: RetryConfig,
}
impl Limitless for LimitlessClient {
fn new(api_key: Option<String>, secret: Option<String>) -> Self {
Self::new_with_config(&Config::default(), api_key, secret)
}
fn new_with_config(config: &Config, api_key: Option<String>, secret: Option<String>) -> Self {
Self {
client: Client::new(
api_key.clone(),
secret.clone(),
config.rest_api_endpoint.to_string(),
),
config: config.clone(),
retry_config: RetryConfig::default(),
}
}
}
impl LimitlessClient {
pub fn builder() -> LimitlessClientBuilder {
LimitlessClientBuilder::default()
}
pub fn raw_client(&self) -> &Client {
&self.client
}
pub fn set_credentials(&mut self, api_key: Option<String>, secret_key: Option<String>) {
self.client = Client::new(
api_key.clone(),
secret_key.clone(),
self.config.rest_api_endpoint.to_string(),
);
}
pub fn retry_config(&self) -> &RetryConfig {
&self.retry_config
}
pub fn base_url(&self) -> &str {
&self.config.rest_api_endpoint
}
pub fn markets(&self) -> Markets {
Markets::new_with_config(
&self.config,
self.client.api_key.clone(),
self.client.secret_key.clone(),
)
}
pub fn trader(&self) -> Trader {
Trader::new_with_config(
&self.config,
self.client.api_key.clone(),
self.client.secret_key.clone(),
)
}
pub fn portfolio(&self) -> Portfolio {
Portfolio::new_with_config(
&self.config,
self.client.api_key.clone(),
self.client.secret_key.clone(),
)
}
pub fn navigation(&self) -> Navigation {
Navigation::new_with_config(
&self.config,
self.client.api_key.clone(),
self.client.secret_key.clone(),
)
}
pub fn stream(&self) -> Stream {
Stream::new_with_config(
&self.config,
self.client.api_key.clone(),
self.client.secret_key.clone(),
)
}
pub async fn browse_active(
&self,
category_id: Option<u64>,
page: Option<u64>,
limit: Option<u64>,
sort_by: Option<String>,
trade_type: Option<String>,
automation_type: Option<String>,
) -> Result<ActiveMarketsResponse, LimitlessError> {
self.markets()
.browse_active(
category_id,
page,
limit,
sort_by,
trade_type,
automation_type,
)
.await
}
pub async fn get_category_counts(&self) -> Result<CategoryCountResponse, LimitlessError> {
self.markets().get_category_counts().await
}
pub async fn get_active_slugs(&self) -> Result<Vec<ActiveSlug>, LimitlessError> {
self.markets().get_active_slugs().await
}
pub async fn get_market(&self, address_or_slug: &str) -> Result<MarketDetail, LimitlessError> {
self.markets().get_market(address_or_slug).await
}
pub async fn get_oracle_candles(
&self,
address_or_slug: &str,
interval: Option<&str>,
from: Option<u64>,
to: Option<u64>,
) -> Result<OracleCandlesResponse, LimitlessError> {
self.markets()
.get_oracle_candles(address_or_slug, interval, from, to)
.await
}
pub async fn get_feed_events(
&self,
slug: &str,
page: Option<u64>,
limit: Option<u64>,
) -> Result<FeedEventsResponse, LimitlessError> {
self.markets().get_feed_events(slug, page, limit).await
}
pub async fn search_markets(
&self,
query: &str,
limit: Option<u64>,
page: Option<u64>,
similarity_threshold: Option<f64>,
) -> Result<SearchResponse, LimitlessError> {
self.markets()
.search(query, limit, page, similarity_threshold)
.await
}
pub async fn create_order(
&self,
order_request: &str,
) -> Result<CreateOrderResponse, LimitlessError> {
self.trader().create_order(order_request).await
}
pub async fn order_status_batch(
&self,
request_body: &str,
) -> Result<OrderStatusBatchResponse, LimitlessError> {
self.trader().order_status_batch(request_body).await
}
pub async fn cancel_combined(
&self,
request_body: &str,
) -> Result<CancelOrderResponse, LimitlessError> {
self.trader().cancel_combined(request_body).await
}
pub async fn cancel_batch(
&self,
request_body: &str,
) -> Result<CancelBatchResponse, LimitlessError> {
self.trader().cancel_batch(request_body).await
}
pub async fn cancel_order_by_id(
&self,
order_id: &str,
) -> Result<CancelOrderResponse, LimitlessError> {
self.trader().cancel_order_by_id(order_id).await
}
pub async fn cancel_all_in_market(
&self,
slug: &str,
) -> Result<CancelAllResponse, LimitlessError> {
self.trader().cancel_all_in_market(slug).await
}
pub async fn get_orderbook(&self, slug: &str) -> Result<OrderbookResponse, LimitlessError> {
self.trader().get_orderbook(slug).await
}
pub async fn get_historical_prices(
&self,
slug: &str,
interval: Option<&str>,
) -> Result<Vec<HistoricalPriceData>, LimitlessError> {
self.trader().get_historical_prices(slug, interval).await
}
pub async fn get_locked_balance(
&self,
slug: &str,
) -> Result<LockedBalanceResponse, LimitlessError> {
self.trader().get_locked_balance(slug).await
}
pub async fn get_user_orders(
&self,
slug: &str,
statuses: Option<&[&str]>,
limit: Option<u64>,
) -> Result<UserOrdersResponse, LimitlessError> {
self.trader().get_user_orders(slug, statuses, limit).await
}
pub async fn get_market_events(
&self,
slug: &str,
page: Option<u64>,
limit: Option<u64>,
) -> Result<MarketEventsResponse, LimitlessError> {
self.trader().get_market_events(slug, page, limit).await
}
pub async fn buy_gtc(
&self,
private_key: &str,
market_slug: &str,
token_id: &str,
price: f64,
size: f64,
owner_id: u64,
) -> Result<CreateOrderResponse, LimitlessError> {
self.trader()
.buy_gtc(private_key, market_slug, token_id, price, size, owner_id)
.await
}
pub async fn sell_gtc(
&self,
private_key: &str,
market_slug: &str,
token_id: &str,
price: f64,
size: f64,
owner_id: u64,
) -> Result<CreateOrderResponse, LimitlessError> {
self.trader()
.sell_gtc(private_key, market_slug, token_id, price, size, owner_id)
.await
}
pub async fn buy_fok(
&self,
private_key: &str,
market_slug: &str,
token_id: &str,
usdc_amount: f64,
owner_id: u64,
) -> Result<CreateOrderResponse, LimitlessError> {
self.trader()
.buy_fok(private_key, market_slug, token_id, usdc_amount, owner_id)
.await
}
pub async fn sell_fok(
&self,
private_key: &str,
market_slug: &str,
token_id: &str,
share_amount: f64,
owner_id: u64,
) -> Result<CreateOrderResponse, LimitlessError> {
self.trader()
.sell_fok(private_key, market_slug, token_id, share_amount, owner_id)
.await
}
pub async fn cancel_all(&self, slug: &str) -> Result<CancelAllResponse, LimitlessError> {
self.trader().cancel_all(slug).await
}
pub async fn get_profile(&self, account: &str) -> Result<ProfileResponse, LimitlessError> {
self.portfolio().get_profile(account).await
}
pub async fn get_trades(&self) -> Result<Vec<TradeEntry>, LimitlessError> {
self.portfolio().get_trades().await
}
pub async fn get_positions(&self) -> Result<PositionsResponse, LimitlessError> {
self.portfolio().get_positions().await
}
pub async fn get_pnl_chart(
&self,
timeframe: Option<&str>,
) -> Result<PnlChartResponse, LimitlessError> {
self.portfolio().get_pnl_chart(timeframe).await
}
pub async fn get_points(&self) -> Result<PointsResponse, LimitlessError> {
self.portfolio().get_points().await
}
pub async fn get_history(
&self,
cursor: Option<&str>,
limit: Option<u64>,
) -> Result<HistoryResponse, LimitlessError> {
self.portfolio().get_history(cursor, limit).await
}
pub async fn get_allowance(
&self,
allowance_type: &str,
spender: Option<&str>,
) -> Result<AllowanceResponse, LimitlessError> {
self.portfolio()
.get_allowance(allowance_type, spender)
.await
}
pub async fn get_navigation_tree(&self) -> Result<Vec<NavigationNode>, LimitlessError> {
self.navigation().get_navigation_tree().await
}
pub async fn get_page_by_path(&self, path: &str) -> Result<MarketPage, LimitlessError> {
self.navigation().get_page_by_path(path).await
}
pub async fn list_page_markets(
&self,
page_id: &str,
cursor: Option<&str>,
page: Option<u64>,
limit: Option<u64>,
sort_by: Option<&str>,
filters: Option<&BTreeMap<String, String>>,
) -> Result<PageMarketsResponse, LimitlessError> {
self.navigation()
.list_page_markets(page_id, cursor, page, limit, sort_by, filters)
.await
}
pub async fn list_property_keys(&self) -> Result<Vec<PropertyKey>, LimitlessError> {
self.navigation().list_property_keys().await
}
pub async fn get_property_key(&self, key_id: &str) -> Result<PropertyKey, LimitlessError> {
self.navigation().get_property_key(key_id).await
}
pub async fn list_property_options(
&self,
key_id: &str,
parent_id: Option<&str>,
) -> Result<Vec<PropertyOption>, LimitlessError> {
self.navigation()
.list_property_options(key_id, parent_id)
.await
}
pub async fn ws_ping(&self) -> Result<(), LimitlessError> {
self.stream().ws_ping().await
}
pub async fn ws_subscribe<F>(&self, handler: F) -> Result<(), LimitlessError>
where
F: FnMut(Value) -> Result<(), LimitlessError> + 'static + Send,
{
self.stream().ws_subscribe(handler).await
}
pub async fn ws_subscribe_with_commands<F>(
&self,
cmd_receiver: tokio::sync::mpsc::UnboundedReceiver<String>,
handler: F,
) -> Result<(), LimitlessError>
where
F: FnMut(Value) -> Result<(), LimitlessError> + 'static + Send,
{
self.stream()
.ws_subscribe_with_commands(cmd_receiver, handler)
.await
}
pub async fn ws_subscribe_authenticated_with_commands<F>(
&self,
cmd_receiver: tokio::sync::mpsc::UnboundedReceiver<String>,
handler: F,
) -> Result<(), LimitlessError>
where
F: FnMut(Value) -> Result<(), LimitlessError> + 'static + Send,
{
self.stream()
.ws_subscribe_authenticated_with_commands(cmd_receiver, handler)
.await
}
pub async fn ws_subscribe_authenticated_events<F>(
&self,
handler: F,
) -> Result<(), LimitlessError>
where
F: FnMut(WsEventKind) -> Result<(), LimitlessError> + 'static + Send,
{
self.stream()
.ws_subscribe_authenticated_events(handler)
.await
}
}
#[derive(Default)]
pub struct LimitlessClientBuilder {
api_key: Option<String>,
secret_key: Option<String>,
rest_endpoint: Option<String>,
ws_endpoint: Option<String>,
recv_window: Option<u64>,
retry_config: Option<RetryConfig>,
}
impl LimitlessClientBuilder {
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
pub fn secret(mut self, secret: impl Into<String>) -> Self {
self.secret_key = Some(secret.into());
self
}
pub fn rest_endpoint(mut self, url: impl Into<String>) -> Self {
self.rest_endpoint = Some(url.into());
self
}
pub fn ws_endpoint(mut self, url: impl Into<String>) -> Self {
self.ws_endpoint = Some(url.into());
self
}
pub fn recv_window(mut self, ms: u64) -> Self {
self.recv_window = Some(ms);
self
}
pub fn testnet(mut self, use_testnet: bool) -> Self {
if use_testnet {
self.rest_endpoint = Some("https://api.testnet.limitless.exchange".into());
self.ws_endpoint = Some("wss://ws.testnet.limitless.exchange/markets".into());
}
self
}
pub fn retry_config(mut self, config: RetryConfig) -> Self {
self.retry_config = Some(config);
self
}
pub fn no_retry(mut self) -> Self {
self.retry_config = Some(RetryConfig::none());
self
}
pub fn build(self) -> Result<LimitlessClient, LimitlessError> {
let api_key = self
.api_key
.or_else(|| std::env::var("LIMITLESS_API_KEY").ok())
.filter(|k| !k.is_empty());
let secret_key = self
.secret_key
.or_else(|| std::env::var("LIMITLESS_API_SECRET").ok())
.filter(|s| !s.is_empty());
let rest_endpoint = self
.rest_endpoint
.unwrap_or_else(|| Config::DEFAULT_REST_API_ENDPOINT.into());
let ws_endpoint = self
.ws_endpoint
.unwrap_or_else(|| Config::DEFAULT_WS_ENDPOINT.into());
let recv_window = self.recv_window.unwrap_or(5000);
let config = Config::new(rest_endpoint, ws_endpoint, recv_window);
let retry_config = self.retry_config.unwrap_or_default();
if api_key.is_none() && secret_key.is_none() {
log::warn!(
"No API credentials provided — authenticated endpoints will fail. \
Set LIMITLESS_API_KEY + LIMITLESS_API_SECRET environment variables \
or pass credentials to the builder."
);
}
Ok(LimitlessClient {
client: Client::new(
api_key.clone(),
secret_key.clone(),
config.rest_api_endpoint.to_string(),
),
config,
retry_config,
})
}
}
impl Default for LimitlessClient {
fn default() -> Self {
Self::builder()
.build()
.expect("Failed to create default LimitlessClient")
}
}