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}