Skip to main content

fyuz/api/
market.rs

1//! Market data: service metadata, tokens, trades, candles and analytics.
2
3use serde_json::json;
4
5use crate::client::{seg, FyuzClient, QueryPairs};
6use crate::error::Result;
7use crate::models::{
8    Candle, ChainConfig, DiscoverToken, HealthStatus, KingResponse, RecentTrades, Token,
9    TokenAnalytics, TokenDetail, TokenPage, TopTrader, Trade,
10};
11use crate::pagination::TokenPager;
12use crate::params::{
13    ChartDataParams, DiscoverParams, ListTokensParams, RecentTradesParams, ToQuery,
14    TokenDetailParams, TokenTradesParams,
15};
16
17impl FyuzClient {
18    /// `GET /health` — liveness probe.
19    ///
20    /// ```no_run
21    /// # async fn demo() -> Result<(), fyuz::Error> {
22    /// let client = fyuz::FyuzClient::new()?;
23    /// assert_eq!(client.health().await?.status, "ok");
24    /// # Ok(())
25    /// # }
26    /// ```
27    pub async fn health(&self) -> Result<HealthStatus> {
28        self.get_json("/health", QueryPairs::new()).await
29    }
30
31    /// `GET /config` — the chains this deployment serves.
32    ///
33    /// ```no_run
34    /// # async fn demo() -> Result<(), fyuz::Error> {
35    /// let client = fyuz::FyuzClient::new()?;
36    /// for chain in client.get_config().await?.chains {
37    ///     println!("{} (chain id {})", chain.name, chain.chain_id);
38    /// }
39    /// # Ok(())
40    /// # }
41    /// ```
42    pub async fn get_config(&self) -> Result<ChainConfig> {
43        self.get_json("/config", QueryPairs::new()).await
44    }
45
46    /// `GET /discover` — the discovery feed, and the best single endpoint for
47    /// bulk market-data ingestion.
48    ///
49    /// One row per token with market cap, 24h volume, buy/sell counts, holder
50    /// count, price change and graduation progress.
51    ///
52    /// ```no_run
53    /// # async fn demo() -> Result<(), fyuz::Error> {
54    /// use fyuz::{DiscoverParams, DiscoverTab};
55    ///
56    /// let client = fyuz::FyuzClient::new()?;
57    /// let almost_there = client
58    ///     .discover(&DiscoverParams::new().tab(DiscoverTab::Graduating).limit(20))
59    ///     .await?;
60    ///
61    /// for token in &almost_there {
62    ///     println!("{:<8} {:>6.2}% of the way to a PancakeSwap pair", token.symbol, token.graduation_pct);
63    /// }
64    /// # Ok(())
65    /// # }
66    /// ```
67    pub async fn discover(&self, params: &DiscoverParams) -> Result<Vec<DiscoverToken>> {
68        self.get_json("/discover", params.to_query()).await
69    }
70
71    /// `GET /tokens` — one page of the token list.
72    ///
73    /// Use [`token_pages`](Self::token_pages) to walk every page instead of
74    /// paging by hand.
75    ///
76    /// ```no_run
77    /// # async fn demo() -> Result<(), fyuz::Error> {
78    /// use fyuz::{ListTokensParams, SortDirection};
79    ///
80    /// let client = fyuz::FyuzClient::new()?;
81    /// let page = client
82    ///     .list_tokens(
83    ///         &ListTokensParams::new()
84    ///             .order_type("marketcap")
85    ///             .order_flag(SortDirection::Desc)
86    ///             .page_size(50),
87    ///     )
88    ///     .await?;
89    ///
90    /// println!("{} of {} tokens", page.token_list.len(), page.token_count);
91    /// # Ok(())
92    /// # }
93    /// ```
94    pub async fn list_tokens(&self, params: &ListTokensParams) -> Result<TokenPage> {
95        self.get_json("/tokens", params.to_query()).await
96    }
97
98    /// Walk every page of `GET /tokens`.
99    ///
100    /// The returned [`TokenPager`] issues one request per page and stops when
101    /// the server's `tokenCount` is covered.
102    ///
103    /// ```no_run
104    /// # async fn demo() -> Result<(), fyuz::Error> {
105    /// use fyuz::ListTokensParams;
106    ///
107    /// let client = fyuz::FyuzClient::new()?;
108    /// let mut pager = client.token_pages(&ListTokensParams::new().page_size(100));
109    ///
110    /// while let Some(token) = pager.next_token().await? {
111    ///     println!("{} {}", token.token_symbol, token.marketcap);
112    /// }
113    /// # Ok(())
114    /// # }
115    /// ```
116    pub fn token_pages(&self, params: &ListTokensParams) -> TokenPager {
117        TokenPager::new(self.clone(), params.clone())
118    }
119
120    /// `GET /tokens/king` — the current king of the hill: the
121    /// highest-market-cap token still on the bonding curve.
122    ///
123    /// `None` means the hill is empty, which is a real state rather than an
124    /// error: a token leaves the moment it graduates, so between a graduation
125    /// and the next launch there is no king.
126    ///
127    /// The API wraps the token in a `{"king": …}` envelope; it is unwrapped
128    /// here.
129    ///
130    /// ```no_run
131    /// # async fn demo() -> Result<(), fyuz::Error> {
132    /// let client = fyuz::FyuzClient::new()?;
133    /// match client.get_king().await? {
134    ///     Some(king) => println!("{} at ${}", king.token_name, king.marketcap),
135    ///     None => println!("no king right now"),
136    /// }
137    /// # Ok(())
138    /// # }
139    /// ```
140    pub async fn get_king(&self) -> Result<Option<Token>> {
141        let body: KingResponse = self.get_json("/tokens/king", QueryPairs::new()).await?;
142        Ok(body.king)
143    }
144
145    /// `GET /tokens/{network}/{tokenAddress}` — full token record plus recent
146    /// trades, holders, 15-minute price and 1-day liquidity.
147    ///
148    /// `curve_holding` is `None` when the on-chain read failed. That means
149    /// **unknown**, not zero — do not render it as `0`.
150    ///
151    /// Answers `404` for an unknown token; check with
152    /// [`Error::is_not_found`](crate::Error::is_not_found).
153    ///
154    /// ```no_run
155    /// # async fn demo() -> Result<(), fyuz::Error> {
156    /// use fyuz::TokenDetailParams;
157    ///
158    /// let client = fyuz::FyuzClient::new()?;
159    /// let detail = client
160    ///     .get_token("bsc", "0x23a84ba5bf7bf3236491fa2b5a9807a274337135", &TokenDetailParams::new())
161    ///     .await?;
162    ///
163    /// let graduated = detail.token_details.pair_address.is_some();
164    /// println!("graduated: {graduated}, {} trades", detail.trades_count);
165    /// # Ok(())
166    /// # }
167    /// ```
168    pub async fn get_token(
169        &self,
170        network: &str,
171        token_address: &str,
172        params: &TokenDetailParams,
173    ) -> Result<TokenDetail> {
174        self.get_json(
175            &format!("/tokens/{}/{}", seg(network), seg(token_address)),
176            params.to_query(),
177        )
178        .await
179    }
180
181    /// `GET /trades/recent` — the platform-wide trade feed.
182    ///
183    /// Pass the highest [`Trade::id`] you have already seen as
184    /// [`RecentTradesParams::latest_trade_id`] to poll incrementally.
185    ///
186    /// ```no_run
187    /// # async fn demo() -> Result<(), fyuz::Error> {
188    /// use fyuz::RecentTradesParams;
189    ///
190    /// let client = fyuz::FyuzClient::new()?;
191    /// let mut cursor = 0i64;
192    ///
193    /// let feed = client
194    ///     .get_recent_trades(&RecentTradesParams::new().latest_trade_id(cursor))
195    ///     .await?;
196    ///
197    /// if let Some(newest) = feed.trades.first() {
198    ///     cursor = newest.id;
199    /// }
200    /// # let _ = cursor;
201    /// # Ok(())
202    /// # }
203    /// ```
204    pub async fn get_recent_trades(&self, params: &RecentTradesParams) -> Result<RecentTrades> {
205        self.get_json("/trades/recent", params.to_query()).await
206    }
207
208    /// `POST /trades` — one token's trade history, newest first.
209    ///
210    /// This is a `POST` only because the token address travels in the body; it
211    /// is a read, it changes nothing, and it needs no authentication.
212    ///
213    /// ```no_run
214    /// # async fn demo() -> Result<(), fyuz::Error> {
215    /// use fyuz::TokenTradesParams;
216    ///
217    /// let client = fyuz::FyuzClient::new()?;
218    /// let trades = client
219    ///     .get_token_trades(
220    ///         "0x23a84ba5bf7bf3236491fa2b5a9807a274337135",
221    ///         &TokenTradesParams::new().limit(100),
222    ///     )
223    ///     .await?;
224    ///
225    /// for trade in &trades {
226    ///     // eth_amount is a decimal string: keep it as one.
227    ///     println!("{} {} BNB", trade.trade_side, trade.eth_amount);
228    /// }
229    /// # Ok(())
230    /// # }
231    /// ```
232    pub async fn get_token_trades(
233        &self,
234        token_address: &str,
235        params: &TokenTradesParams,
236    ) -> Result<Vec<Trade>> {
237        self.post_json(
238            "/trades",
239            params.to_query(),
240            json!({ "tokenAddress": token_address }),
241        )
242        .await
243    }
244
245    /// `GET /trades/getChartData` — OHLCV candles, shaped for TradingView-style
246    /// charting libraries.
247    ///
248    /// ```no_run
249    /// # async fn demo() -> Result<(), fyuz::Error> {
250    /// use fyuz::ChartDataParams;
251    ///
252    /// let client = fyuz::FyuzClient::new()?;
253    /// let candles = client
254    ///     .get_chart_data(&ChartDataParams::new(
255    ///         "0x23a84ba5bf7bf3236491fa2b5a9807a274337135",
256    ///         "5",
257    ///         1_750_000_000,
258    ///         1_750_086_400,
259    ///     ))
260    ///     .await?;
261    ///
262    /// println!("{} candles", candles.len());
263    /// # Ok(())
264    /// # }
265    /// ```
266    pub async fn get_chart_data(&self, params: &ChartDataParams) -> Result<Vec<Candle>> {
267        self.get_json("/trades/getChartData", params.to_query())
268            .await
269    }
270
271    /// `GET /analytics/token/{network}/{address}` — holder concentration,
272    /// buy/sell split, volume, graduation progress and bundle-buyer detection.
273    ///
274    /// Answers `404` for an unknown token.
275    ///
276    /// ```no_run
277    /// # async fn demo() -> Result<(), fyuz::Error> {
278    /// let client = fyuz::FyuzClient::new()?;
279    /// let a = client
280    ///     .get_token_analytics("bsc", "0x23a84ba5bf7bf3236491fa2b5a9807a274337135")
281    ///     .await?;
282    ///
283    /// if a.bundle_flag {
284    ///     println!("{} addresses bought in the launch block", a.bundle_buyers);
285    /// }
286    /// # Ok(())
287    /// # }
288    /// ```
289    pub async fn get_token_analytics(
290        &self,
291        network: &str,
292        address: &str,
293    ) -> Result<TokenAnalytics> {
294        self.get_json(
295            &format!("/analytics/token/{}/{}", seg(network), seg(address)),
296            QueryPairs::new(),
297        )
298        .await
299    }
300
301    /// `GET /analytics/top-traders/{network}/{address}` — a token's most
302    /// profitable traders, with the creator flagged.
303    ///
304    /// ```no_run
305    /// # async fn demo() -> Result<(), fyuz::Error> {
306    /// let client = fyuz::FyuzClient::new()?;
307    /// let traders = client
308    ///     .get_top_traders("bsc", "0x23a84ba5bf7bf3236491fa2b5a9807a274337135")
309    ///     .await?;
310    ///
311    /// for t in traders.iter().take(5) {
312    ///     println!("{} {:+.2} USD", t.address, t.total_pnl_usd);
313    /// }
314    /// # Ok(())
315    /// # }
316    /// ```
317    pub async fn get_top_traders(&self, network: &str, address: &str) -> Result<Vec<TopTrader>> {
318        self.get_json(
319            &format!("/analytics/top-traders/{}/{}", seg(network), seg(address)),
320            QueryPairs::new(),
321        )
322        .await
323    }
324}