precolator-sdk 1.0.0

Rust async client SDK for the Precolator perpetual futures trading platform — markets, portfolio, leaderboard, and trading statistics.
Documentation
//! Async HTTP client for the Precolator REST API.

use reqwest::Client;
use url::Url;

use crate::error::{PrecolatorError, Result};
use crate::models::*;

/// Async client for interacting with the Precolator API.
///
/// # Example
/// ```rust,no_run
/// use precolator_sdk::PrecolatorClient;
///
/// #[tokio::main]
/// async fn main() -> precolator_sdk::Result<()> {
///     let client = PrecolatorClient::new("https://precolator.xyz");
///     let markets = client.get_markets().await?;
///     Ok(())
/// }
/// ```
#[derive(Debug, Clone)]
pub struct PrecolatorClient {
    http: Client,
    base_url: Url,
}

impl PrecolatorClient {
    /// Create a new client pointing at the given base URL.
    ///
    /// ```rust,no_run
    /// let client = precolator_sdk::PrecolatorClient::new("https://precolator.xyz");
    /// ```
    pub fn new(base_url: &str) -> Self {
        let base = Url::parse(base_url).expect("invalid base URL");
        Self {
            http: Client::builder()
                .user_agent(format!("precolator-sdk/{}", crate::SDK_VERSION))
                .build()
                .expect("failed to build HTTP client"),
            base_url: base,
        }
    }

    /// Create a client with a custom `reqwest::Client` (e.g. custom timeouts).
    pub fn with_client(base_url: &str, http: Client) -> Result<Self> {
        Ok(Self {
            http,
            base_url: Url::parse(base_url)?,
        })
    }

    // ── internal helpers ────────────────────────────────────────────────

    fn url(&self, path: &str) -> Result<Url> {
        self.base_url
            .join(path)
            .map_err(PrecolatorError::InvalidUrl)
    }

    async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
        let url = self.url(path)?;
        let resp = self.http.get(url).send().await?;

        let status = resp.status().as_u16();
        if status == 429 {
            return Err(PrecolatorError::RateLimited {
                retry_after_secs: 15,
            });
        }
        if status == 404 {
            return Err(PrecolatorError::NotFound(path.to_string()));
        }
        if !resp.status().is_success() {
            let body = resp.text().await.unwrap_or_default();
            return Err(PrecolatorError::Api {
                status,
                message: body,
            });
        }

        let wrapper: ApiResponse<T> = resp.json().await?;
        Ok(wrapper.data)
    }

    // ── Health ──────────────────────────────────────────────────────────

    /// `GET /api/v1/health`
    pub async fn health(&self) -> Result<HealthStatus> {
        self.get("/api/v1/health").await
    }

    /// `GET /api/v1/health/detailed`
    pub async fn health_detailed(&self) -> Result<DetailedHealth> {
        self.get("/api/v1/health/detailed").await
    }

    // ── Markets ─────────────────────────────────────────────────────────

    /// `GET /api/v1/markets` – list all markets.
    pub async fn get_markets(&self) -> Result<Vec<Market>> {
        self.get("/api/v1/markets").await
    }

    /// `GET /api/v1/markets/:id` – single market details.
    pub async fn get_market(&self, id: &str) -> Result<Market> {
        self.get(&format!("/api/v1/markets/{id}")).await
    }

    /// `GET /api/v1/markets/:id/stats` – market statistics.
    pub async fn get_market_stats(&self, id: &str) -> Result<MarketStats> {
        self.get(&format!("/api/v1/markets/{id}/stats")).await
    }

    // ── Trades ──────────────────────────────────────────────────────────

    /// `GET /api/v1/trades` – recent trades across all markets.
    pub async fn get_trades(&self) -> Result<Vec<Trade>> {
        self.get("/api/v1/trades").await
    }

    /// `GET /api/v1/trades/:tradeId`
    pub async fn get_trade(&self, trade_id: &str) -> Result<Trade> {
        self.get(&format!("/api/v1/trades/{trade_id}")).await
    }

    /// `GET /api/v1/trades/user/:wallet`
    pub async fn get_user_trades(&self, wallet: &str) -> Result<Vec<Trade>> {
        self.get(&format!("/api/v1/trades/user/{wallet}")).await
    }

    // ── Portfolio ───────────────────────────────────────────────────────

    /// `GET /api/v1/portfolio/:wallet`
    pub async fn get_portfolio(&self, wallet: &str) -> Result<Portfolio> {
        self.get(&format!("/api/v1/portfolio/{wallet}")).await
    }

    /// `GET /api/v1/portfolio/:wallet/history`
    pub async fn get_portfolio_history(&self, wallet: &str) -> Result<PortfolioHistory> {
        self.get(&format!("/api/v1/portfolio/{wallet}/history"))
            .await
    }

    /// `GET /api/v1/portfolio/:wallet/stats`
    pub async fn get_portfolio_stats(&self, wallet: &str) -> Result<PortfolioStats> {
        self.get(&format!("/api/v1/portfolio/{wallet}/stats"))
            .await
    }

    // ── Leaderboard ─────────────────────────────────────────────────────

    /// `GET /api/v1/leaderboard`
    pub async fn get_leaderboard(&self) -> Result<Vec<LeaderboardEntry>> {
        self.get("/api/v1/leaderboard").await
    }

    /// `GET /api/v1/leaderboard/:timeframe` (e.g. `7d`, `30d`, `all`)
    pub async fn get_leaderboard_by_timeframe(
        &self,
        timeframe: &str,
    ) -> Result<Vec<LeaderboardEntry>> {
        self.get(&format!("/api/v1/leaderboard/{timeframe}"))
            .await
    }

    // ── Tokens ──────────────────────────────────────────────────────────

    /// `GET /api/v1/tokens`
    pub async fn get_tokens(&self) -> Result<Vec<Token>> {
        self.get("/api/v1/tokens").await
    }

    /// `GET /api/v1/tokens/:symbol`
    pub async fn get_token(&self, symbol: &str) -> Result<Token> {
        self.get(&format!("/api/v1/tokens/{symbol}")).await
    }

    // ── Statistics ───────────────────────────────────────────────────────

    /// `GET /api/v1/stats` – platform-wide statistics.
    pub async fn get_platform_stats(&self) -> Result<PlatformStats> {
        self.get("/api/v1/stats").await
    }

    /// `GET /api/v1/stats/markets/overview`
    pub async fn get_market_overview(&self) -> Result<MarketOverview> {
        self.get("/api/v1/stats/markets/overview").await
    }

    /// `GET /api/v1/stats/exchange/metrics`
    pub async fn get_exchange_metrics(&self) -> Result<ExchangeMetrics> {
        self.get("/api/v1/stats/exchange/metrics").await
    }
}