Skip to main content

precolator_sdk/
client.rs

1//! Async HTTP client for the Precolator REST API.
2
3use reqwest::Client;
4use url::Url;
5
6use crate::error::{PrecolatorError, Result};
7use crate::models::*;
8
9/// Async client for interacting with the Precolator API.
10///
11/// # Example
12/// ```rust,no_run
13/// use precolator_sdk::PrecolatorClient;
14///
15/// #[tokio::main]
16/// async fn main() -> precolator_sdk::Result<()> {
17///     let client = PrecolatorClient::new("https://precolator.xyz");
18///     let markets = client.get_markets().await?;
19///     Ok(())
20/// }
21/// ```
22#[derive(Debug, Clone)]
23pub struct PrecolatorClient {
24    http: Client,
25    base_url: Url,
26}
27
28impl PrecolatorClient {
29    /// Create a new client pointing at the given base URL.
30    ///
31    /// ```rust,no_run
32    /// let client = precolator_sdk::PrecolatorClient::new("https://precolator.xyz");
33    /// ```
34    pub fn new(base_url: &str) -> Self {
35        let base = Url::parse(base_url).expect("invalid base URL");
36        Self {
37            http: Client::builder()
38                .user_agent(format!("precolator-sdk/{}", crate::SDK_VERSION))
39                .build()
40                .expect("failed to build HTTP client"),
41            base_url: base,
42        }
43    }
44
45    /// Create a client with a custom `reqwest::Client` (e.g. custom timeouts).
46    pub fn with_client(base_url: &str, http: Client) -> Result<Self> {
47        Ok(Self {
48            http,
49            base_url: Url::parse(base_url)?,
50        })
51    }
52
53    // ── internal helpers ────────────────────────────────────────────────
54
55    fn url(&self, path: &str) -> Result<Url> {
56        self.base_url
57            .join(path)
58            .map_err(PrecolatorError::InvalidUrl)
59    }
60
61    async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
62        let url = self.url(path)?;
63        let resp = self.http.get(url).send().await?;
64
65        let status = resp.status().as_u16();
66        if status == 429 {
67            return Err(PrecolatorError::RateLimited {
68                retry_after_secs: 15,
69            });
70        }
71        if status == 404 {
72            return Err(PrecolatorError::NotFound(path.to_string()));
73        }
74        if !resp.status().is_success() {
75            let body = resp.text().await.unwrap_or_default();
76            return Err(PrecolatorError::Api {
77                status,
78                message: body,
79            });
80        }
81
82        let wrapper: ApiResponse<T> = resp.json().await?;
83        Ok(wrapper.data)
84    }
85
86    // ── Health ──────────────────────────────────────────────────────────
87
88    /// `GET /api/v1/health`
89    pub async fn health(&self) -> Result<HealthStatus> {
90        self.get("/api/v1/health").await
91    }
92
93    /// `GET /api/v1/health/detailed`
94    pub async fn health_detailed(&self) -> Result<DetailedHealth> {
95        self.get("/api/v1/health/detailed").await
96    }
97
98    // ── Markets ─────────────────────────────────────────────────────────
99
100    /// `GET /api/v1/markets` – list all markets.
101    pub async fn get_markets(&self) -> Result<Vec<Market>> {
102        self.get("/api/v1/markets").await
103    }
104
105    /// `GET /api/v1/markets/:id` – single market details.
106    pub async fn get_market(&self, id: &str) -> Result<Market> {
107        self.get(&format!("/api/v1/markets/{id}")).await
108    }
109
110    /// `GET /api/v1/markets/:id/stats` – market statistics.
111    pub async fn get_market_stats(&self, id: &str) -> Result<MarketStats> {
112        self.get(&format!("/api/v1/markets/{id}/stats")).await
113    }
114
115    // ── Trades ──────────────────────────────────────────────────────────
116
117    /// `GET /api/v1/trades` – recent trades across all markets.
118    pub async fn get_trades(&self) -> Result<Vec<Trade>> {
119        self.get("/api/v1/trades").await
120    }
121
122    /// `GET /api/v1/trades/:tradeId`
123    pub async fn get_trade(&self, trade_id: &str) -> Result<Trade> {
124        self.get(&format!("/api/v1/trades/{trade_id}")).await
125    }
126
127    /// `GET /api/v1/trades/user/:wallet`
128    pub async fn get_user_trades(&self, wallet: &str) -> Result<Vec<Trade>> {
129        self.get(&format!("/api/v1/trades/user/{wallet}")).await
130    }
131
132    // ── Portfolio ───────────────────────────────────────────────────────
133
134    /// `GET /api/v1/portfolio/:wallet`
135    pub async fn get_portfolio(&self, wallet: &str) -> Result<Portfolio> {
136        self.get(&format!("/api/v1/portfolio/{wallet}")).await
137    }
138
139    /// `GET /api/v1/portfolio/:wallet/history`
140    pub async fn get_portfolio_history(&self, wallet: &str) -> Result<PortfolioHistory> {
141        self.get(&format!("/api/v1/portfolio/{wallet}/history"))
142            .await
143    }
144
145    /// `GET /api/v1/portfolio/:wallet/stats`
146    pub async fn get_portfolio_stats(&self, wallet: &str) -> Result<PortfolioStats> {
147        self.get(&format!("/api/v1/portfolio/{wallet}/stats"))
148            .await
149    }
150
151    // ── Leaderboard ─────────────────────────────────────────────────────
152
153    /// `GET /api/v1/leaderboard`
154    pub async fn get_leaderboard(&self) -> Result<Vec<LeaderboardEntry>> {
155        self.get("/api/v1/leaderboard").await
156    }
157
158    /// `GET /api/v1/leaderboard/:timeframe` (e.g. `7d`, `30d`, `all`)
159    pub async fn get_leaderboard_by_timeframe(
160        &self,
161        timeframe: &str,
162    ) -> Result<Vec<LeaderboardEntry>> {
163        self.get(&format!("/api/v1/leaderboard/{timeframe}"))
164            .await
165    }
166
167    // ── Tokens ──────────────────────────────────────────────────────────
168
169    /// `GET /api/v1/tokens`
170    pub async fn get_tokens(&self) -> Result<Vec<Token>> {
171        self.get("/api/v1/tokens").await
172    }
173
174    /// `GET /api/v1/tokens/:symbol`
175    pub async fn get_token(&self, symbol: &str) -> Result<Token> {
176        self.get(&format!("/api/v1/tokens/{symbol}")).await
177    }
178
179    // ── Statistics ───────────────────────────────────────────────────────
180
181    /// `GET /api/v1/stats` – platform-wide statistics.
182    pub async fn get_platform_stats(&self) -> Result<PlatformStats> {
183        self.get("/api/v1/stats").await
184    }
185
186    /// `GET /api/v1/stats/markets/overview`
187    pub async fn get_market_overview(&self) -> Result<MarketOverview> {
188        self.get("/api/v1/stats/markets/overview").await
189    }
190
191    /// `GET /api/v1/stats/exchange/metrics`
192    pub async fn get_exchange_metrics(&self) -> Result<ExchangeMetrics> {
193        self.get("/api/v1/stats/exchange/metrics").await
194    }
195}