Skip to main content

fyuz/api/
users.rs

1//! Wallets, profiles, leaderboards, seasons, tiers and rewards.
2
3use crate::client::{seg, FyuzClient, QueryPairs};
4use crate::error::Result;
5use crate::models::{
6    KingReign, LeaderboardEntry, Portfolio, ReferralLeaderEntry, Rewards, Season, TierInfo,
7    TopHolderEntry, UserProfile, WalletStats,
8};
9use crate::params::{ToQuery, TopHoldersParams};
10
11impl FyuzClient {
12    /// `GET /wallet/{address}` — average-cost PnL rolled up across every token
13    /// the wallet has traded, plus win rate, volume and holding value.
14    ///
15    /// ```no_run
16    /// # async fn demo() -> Result<(), fyuz::Error> {
17    /// let client = fyuz::FyuzClient::new()?;
18    /// let w = client.get_wallet("0x1111111111111111111111111111111111111111").await?;
19    /// println!("{:+.2} USD over {} trades", w.total_pnl_usd, w.trade_count);
20    /// # Ok(())
21    /// # }
22    /// ```
23    pub async fn get_wallet(&self, address: &str) -> Result<WalletStats> {
24        self.get_json(&format!("/wallet/{}", seg(address)), QueryPairs::new())
25            .await
26    }
27
28    /// `GET /portfolio/{address}` — current holdings with per-position PnL.
29    ///
30    /// An address with no trades returns zeroed totals and an empty
31    /// `positions` list rather than a 404.
32    ///
33    /// ```no_run
34    /// # async fn demo() -> Result<(), fyuz::Error> {
35    /// let client = fyuz::FyuzClient::new()?;
36    /// let p = client.get_portfolio("0x1111111111111111111111111111111111111111").await?;
37    /// for pos in &p.positions {
38    ///     println!("{:<8} ${:>10.2} ({:+.1}%)", pos.symbol, pos.value_usd, pos.unrealized_pnl_pct);
39    /// }
40    /// # Ok(())
41    /// # }
42    /// ```
43    pub async fn get_portfolio(&self, address: &str) -> Result<Portfolio> {
44        self.get_json(&format!("/portfolio/{}", seg(address)), QueryPairs::new())
45            .await
46    }
47
48    /// `GET /users/leaderboard` — the global points leaderboard.
49    ///
50    /// The board resets after every Distributor payout: only activity since the
51    /// last settled round counts. `limit` is clamped by the server to 1-500;
52    /// `None` takes the server default of 100.
53    ///
54    /// Rows arrive pre-ranked — [`LeaderboardEntry::rank`] is authoritative, so
55    /// do not re-sort them.
56    ///
57    /// ```no_run
58    /// # async fn demo() -> Result<(), fyuz::Error> {
59    /// let client = fyuz::FyuzClient::new()?;
60    /// for row in client.get_user_leaderboard(Some(10)).await? {
61    ///     println!("#{:<3} {} {:.1} pts", row.rank, row.address, row.points);
62    /// }
63    /// # Ok(())
64    /// # }
65    /// ```
66    pub async fn get_user_leaderboard(&self, limit: Option<i64>) -> Result<Vec<LeaderboardEntry>> {
67        self.get_json("/users/leaderboard", limit_query(limit))
68            .await
69    }
70
71    /// `GET /users/profile/{address}` — public profile: identity, holdings,
72    /// comments, launched tokens and current-round points.
73    ///
74    /// Answers `404` for an address with no profile; check with
75    /// [`Error::is_not_found`](crate::Error::is_not_found).
76    ///
77    /// ```no_run
78    /// # async fn demo() -> Result<(), fyuz::Error> {
79    /// let client = fyuz::FyuzClient::new()?;
80    /// match client.get_user_profile("0x1111111111111111111111111111111111111111").await {
81    ///     Ok(profile) => println!("{:?}", profile.user.username),
82    ///     Err(e) if e.is_not_found() => println!("no profile"),
83    ///     Err(e) => return Err(e),
84    /// }
85    /// # Ok(())
86    /// # }
87    /// ```
88    pub async fn get_user_profile(&self, address: &str) -> Result<UserProfile> {
89        self.get_json(
90            &format!("/users/profile/{}", seg(address)),
91            QueryPairs::new(),
92        )
93        .await
94    }
95
96    /// `GET /users/top/{count}` — the wallets with the most USD volume over a
97    /// window.
98    ///
99    /// ```no_run
100    /// # async fn demo() -> Result<(), fyuz::Error> {
101    /// use fyuz::TopHoldersParams;
102    ///
103    /// let client = fyuz::FyuzClient::new()?;
104    /// let top = client
105    ///     .get_top_holders(10, &TopHoldersParams::new().network("bsc"))
106    ///     .await?;
107    ///
108    /// for row in &top {
109    ///     println!("{} ${:.0}", row.address, row.volume);
110    /// }
111    /// # Ok(())
112    /// # }
113    /// ```
114    pub async fn get_top_holders(
115        &self,
116        count: i64,
117        params: &TopHoldersParams,
118    ) -> Result<Vec<TopHolderEntry>> {
119        self.get_json(&format!("/users/top/{count}"), params.to_query())
120            .await
121    }
122
123    /// `GET /kings/history` — past King-of-the-Hill reigns, most recent first,
124    /// capped at 100.
125    ///
126    /// An ongoing reign has [`KingReign::ended_at`] of `None` and a
127    /// `duration_secs` that counts up to now.
128    ///
129    /// ```no_run
130    /// # async fn demo() -> Result<(), fyuz::Error> {
131    /// let client = fyuz::FyuzClient::new()?;
132    /// for reign in client.get_kings_history().await? {
133    ///     let state = if reign.ended_at.is_none() { "reigning" } else { "past" };
134    ///     println!("{:<8} {state} for {}s", reign.symbol, reign.duration_secs);
135    /// }
136    /// # Ok(())
137    /// # }
138    /// ```
139    pub async fn get_kings_history(&self) -> Result<Vec<KingReign>> {
140        self.get_json("/kings/history", QueryPairs::new()).await
141    }
142
143    /// `GET /season` — the current season window, prize pot and standings.
144    ///
145    /// ```no_run
146    /// # async fn demo() -> Result<(), fyuz::Error> {
147    /// let client = fyuz::FyuzClient::new()?;
148    /// let season = client.get_season().await?;
149    /// println!("{}: {} BNB pot", season.name, season.prize_pot_eth);
150    /// # Ok(())
151    /// # }
152    /// ```
153    pub async fn get_season(&self) -> Result<Season> {
154        self.get_json("/season", QueryPairs::new()).await
155    }
156
157    /// `GET /referrals/leaderboard` — referrers ranked by active referrals.
158    ///
159    /// `limit` is clamped by the server to 1-500; `None` takes the server
160    /// default of 100.
161    ///
162    /// ```no_run
163    /// # async fn demo() -> Result<(), fyuz::Error> {
164    /// let client = fyuz::FyuzClient::new()?;
165    /// for row in client.get_referral_leaderboard(None).await? {
166    ///     println!("#{} {} — {} referrals", row.rank, row.address, row.referral_count);
167    /// }
168    /// # Ok(())
169    /// # }
170    /// ```
171    pub async fn get_referral_leaderboard(
172        &self,
173        limit: Option<i64>,
174    ) -> Result<Vec<ReferralLeaderEntry>> {
175        self.get_json("/referrals/leaderboard", limit_query(limit))
176            .await
177    }
178
179    /// `GET /tier/{address}` — display-only trader tier from lifetime USD
180    /// volume: Bronze ($0), Silver ($100), Gold ($1,000), Diamond ($10,000).
181    ///
182    /// ```no_run
183    /// # async fn demo() -> Result<(), fyuz::Error> {
184    /// let client = fyuz::FyuzClient::new()?;
185    /// let tier = client.get_tier("0x1111111111111111111111111111111111111111").await?;
186    /// println!("{} — {:.0}% to {:?}", tier.tier, tier.progress_pct, tier.next_tier);
187    /// # Ok(())
188    /// # }
189    /// ```
190    pub async fn get_tier(&self, address: &str) -> Result<TierInfo> {
191        self.get_json(&format!("/tier/{}", seg(address)), QueryPairs::new())
192            .await
193    }
194
195    /// `GET /rewards/{address}` — streaks, points and quest/achievement state.
196    ///
197    /// Read-only: rewards are granted automatically by the indexer as they are
198    /// earned, so there is no claim step on this surface.
199    ///
200    /// ```no_run
201    /// # async fn demo() -> Result<(), fyuz::Error> {
202    /// let client = fyuz::FyuzClient::new()?;
203    /// let r = client.get_rewards("0x1111111111111111111111111111111111111111").await?;
204    /// println!("{} day streak, {:.1} pts", r.current_streak, r.points);
205    /// # Ok(())
206    /// # }
207    /// ```
208    pub async fn get_rewards(&self, address: &str) -> Result<Rewards> {
209        self.get_json(&format!("/rewards/{}", seg(address)), QueryPairs::new())
210            .await
211    }
212}
213
214/// The `limit` query the two leaderboards share.
215fn limit_query(limit: Option<i64>) -> QueryPairs {
216    match limit {
217        Some(limit) => vec![("limit", limit.to_string())],
218        None => QueryPairs::new(),
219    }
220}