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}