fyuz/models/user.rs
1//! Profiles, leaderboards, seasons, tiers and rewards.
2
3use serde::{Deserialize, Deserializer};
4
5use super::token::{Holder, Token};
6
7/// One row of the global points leaderboard.
8///
9/// The board resets after every Distributor payout: only activity since the
10/// last settled round counts.
11#[derive(Debug, Clone, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct LeaderboardEntry {
14 /// 1-based rank, already in returned order. Do not re-sort client side.
15 pub rank: i32,
16 /// Wallet address.
17 pub address: String,
18 /// Display name, when set.
19 #[serde(default)]
20 pub username: Option<String>,
21 /// Avatar URL, when set.
22 #[serde(default)]
23 pub avatar: Option<String>,
24 /// USD volume in the current round window.
25 pub volume_usd: f64,
26 /// Trade count in the current round window.
27 pub trades: i64,
28 /// Points that count toward the payout share — the exact number served to
29 /// the Distributor.
30 pub points: f64,
31 /// Total points earned including locked grants. Always >= [`points`](Self::points).
32 /// Display only.
33 pub points_total: f64,
34 /// Points at round close if the current position is held. Guidance, not a
35 /// payout.
36 pub points_projected: f64,
37 /// USD value currently held.
38 pub held_usd: f64,
39 /// Estimated BNB slice of the live pot: `90% x pot x (points / payout-set
40 /// points)`. `0.0` when below the payout floor.
41 pub reward_eth: f64,
42}
43
44/// Public profile record.
45#[derive(Debug, Clone, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct UserSummary {
48 /// Internal user id.
49 pub id: i32,
50 /// Wallet address.
51 pub address: String,
52 /// Display name, when set.
53 #[serde(default)]
54 pub username: Option<String>,
55 /// Avatar URL, when set.
56 #[serde(default)]
57 pub avatar: Option<String>,
58 /// Free-text bio.
59 #[serde(default)]
60 pub bio: Option<String>,
61 /// Profile likes received.
62 pub likes: i32,
63 /// `true` for Fyuz staff accounts.
64 #[serde(default)]
65 pub is_admin: Option<bool>,
66 /// X/Twitter handle, no `@`.
67 #[serde(default)]
68 pub twitter_username: Option<String>,
69}
70
71/// A comment posted by a user on a token page.
72#[derive(Debug, Clone, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct ChatMessage {
75 /// Internal message id.
76 pub id: i32,
77 /// Token the comment was posted on.
78 pub token_address: String,
79 /// Address replied to. Always `None` on this endpoint.
80 #[serde(default)]
81 pub reply_address: Option<String>,
82 /// Comment body.
83 pub comment: String,
84 /// Optional attached code/snippet reference.
85 #[serde(default)]
86 pub code: Option<String>,
87 /// Post time, RFC-3339 (unlike most timestamps on this API, which are UNIX
88 /// seconds).
89 pub date: String,
90 /// Chain slug.
91 pub network: String,
92}
93
94/// Public profile: identity, holdings, comments, launched tokens and
95/// current-round points.
96#[derive(Debug, Clone, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct UserProfile {
99 /// Identity record.
100 pub user: UserSummary,
101 /// Positive balances only.
102 pub holdings: Vec<Holder>,
103 /// Newest first, max 50.
104 pub chats: Vec<ChatMessage>,
105 /// Tokens launched by this address, newest first.
106 pub created_tokens: Vec<Token>,
107 /// Follower count.
108 pub followers: i64,
109 /// Followed-account count.
110 pub followees: i64,
111 /// Active referrals only.
112 pub referral_count: i64,
113 /// Referral earnings ledger balance.
114 pub points: i32,
115 /// Points in the current round window; matches the leaderboard's
116 /// [`LeaderboardEntry::points`].
117 pub trading_points: f64,
118 /// USD volume in the current round window.
119 pub trading_volume_usd: f64,
120 /// Estimated BNB slice of the live pot.
121 pub reward_eth: f64,
122}
123
124/// A wallet ranked by USD trading volume over the requested window.
125#[derive(Debug, Clone, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct TopHolderEntry {
128 /// Wallet address.
129 pub address: String,
130 /// Display name, when set.
131 #[serde(default)]
132 pub username: Option<String>,
133 /// Avatar URL, when set.
134 #[serde(default)]
135 pub avatar: Option<String>,
136 /// USD trading volume over the window.
137 pub volume: f64,
138}
139
140/// One King-of-the-Hill reign.
141#[derive(Debug, Clone, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct KingReign {
144 /// Token that held the crown.
145 pub token_address: String,
146 /// Token name.
147 pub name: String,
148 /// Token ticker.
149 pub symbol: String,
150 /// Token image URL.
151 #[serde(default)]
152 pub image: Option<String>,
153 /// Chain slug.
154 pub network: String,
155 /// Reign start, UNIX **seconds**.
156 pub started_at: i64,
157 /// Reign end, UNIX **seconds**. `None` while the reign is ongoing.
158 #[serde(default)]
159 pub ended_at: Option<i64>,
160 /// Reign length in seconds. Ongoing reigns count up to now.
161 pub duration_secs: i64,
162}
163
164/// One row of the season standings.
165#[derive(Debug, Clone, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct SeasonEntry {
168 /// 1-based rank, already in returned order.
169 pub rank: i32,
170 /// Wallet address.
171 pub address: String,
172 /// Display name, when set.
173 #[serde(default)]
174 pub username: Option<String>,
175 /// Avatar URL, when set.
176 #[serde(default)]
177 pub avatar: Option<String>,
178 /// Season points.
179 pub points: f64,
180}
181
182/// Current season window, prize pot and standings.
183#[derive(Debug, Clone, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct Season {
186 /// Season name.
187 pub name: String,
188 /// Season start, UNIX **seconds**.
189 pub starts_at: i64,
190 /// Season end, UNIX **seconds**.
191 pub ends_at: i64,
192 /// Prize pot in BNB.
193 pub prize_pot_eth: f64,
194 /// Top 50 with positive points, highest first.
195 pub leaderboard: Vec<SeasonEntry>,
196}
197
198/// One row of the referral leaderboard.
199///
200/// Only ACTIVE referrals (the referee has cleared the net-buy floor) count.
201#[derive(Debug, Clone, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct ReferralLeaderEntry {
204 /// 1-based rank, already in returned order.
205 pub rank: i32,
206 /// Referrer wallet address.
207 pub address: String,
208 /// Display name, when set.
209 #[serde(default)]
210 pub username: Option<String>,
211 /// Avatar URL, when set.
212 #[serde(default)]
213 pub avatar: Option<String>,
214 /// Active referrals only.
215 pub referral_count: i64,
216 /// USD volume traded by the referees.
217 pub referee_volume_usd: f64,
218}
219
220/// Display-only trader tier.
221///
222/// Deserialisation is tolerant: an unknown tier lands in [`Tier::Other`].
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum Tier {
225 /// From $0 of lifetime volume.
226 Bronze,
227 /// From $100 of lifetime volume.
228 Silver,
229 /// From $1,000 of lifetime volume.
230 Gold,
231 /// From $10,000 of lifetime volume. Top tier.
232 Diamond,
233 /// A tier this SDK version does not know about, kept verbatim.
234 Other(String),
235}
236
237impl Tier {
238 /// The wire representation of this tier.
239 pub fn as_str(&self) -> &str {
240 match self {
241 Tier::Bronze => "Bronze",
242 Tier::Silver => "Silver",
243 Tier::Gold => "Gold",
244 Tier::Diamond => "Diamond",
245 Tier::Other(s) => s,
246 }
247 }
248}
249
250impl std::fmt::Display for Tier {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.write_str(self.as_str())
253 }
254}
255
256impl<'de> Deserialize<'de> for Tier {
257 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
258 let raw = String::deserialize(deserializer)?;
259 Ok(match raw.as_str() {
260 "Bronze" => Tier::Bronze,
261 "Silver" => Tier::Silver,
262 "Gold" => Tier::Gold,
263 "Diamond" => Tier::Diamond,
264 _ => Tier::Other(raw),
265 })
266 }
267}
268
269/// Display-only trader tier derived from lifetime USD trade volume.
270#[derive(Debug, Clone, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct TierInfo {
273 /// Wallet address, lower-cased.
274 pub address: String,
275 /// Lifetime USD trade volume.
276 pub volume_usd: f64,
277 /// Current tier.
278 pub tier: Tier,
279 /// Next tier up. `None` at Diamond.
280 #[serde(default)]
281 pub next_tier: Option<Tier>,
282 /// Volume needed for the next tier. `None` at Diamond.
283 #[serde(default)]
284 pub next_threshold_usd: Option<f64>,
285 /// Progress toward the next tier, 0-100. `100.0` at Diamond.
286 pub progress_pct: f64,
287}
288
289/// A quest and one address's progress against it.
290#[derive(Debug, Clone, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct QuestState {
293 /// Stable machine key.
294 pub key: String,
295 /// Display title.
296 pub title: String,
297 /// Display description.
298 pub description: String,
299 /// Quest cadence, e.g. `daily` or `oneoff`.
300 pub kind: String,
301 /// Value that completes the quest.
302 pub target: f64,
303 /// Current progress, clamped to [`target`](Self::target).
304 pub progress: f64,
305 /// Points awarded on completion.
306 pub points: f64,
307 /// `true` once `progress >= target`.
308 pub completed: bool,
309 /// `true` once granted. Granted automatically by the indexer — there is no
310 /// claim call on this public surface.
311 pub claimed: bool,
312}
313
314/// A lifetime achievement and whether an address has earned it.
315#[derive(Debug, Clone, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct AchievementState {
318 /// Stable machine key.
319 pub key: String,
320 /// Display title.
321 pub title: String,
322 /// Display description.
323 pub description: String,
324 /// Display icon name.
325 pub icon: String,
326 /// Points awarded when earned.
327 pub points: f64,
328 /// `true` once earned.
329 pub earned: bool,
330}
331
332/// Rewards ledger for an address: streaks, points and quest/achievement state.
333#[derive(Debug, Clone, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct Rewards {
336 /// Wallet address.
337 pub address: String,
338 /// Consecutive active days.
339 pub current_streak: i64,
340 /// Longest streak ever reached.
341 pub longest_streak: i64,
342 /// Headline figure — identical to the leaderboard's
343 /// [`LeaderboardEntry::points`].
344 pub points: f64,
345 /// Grant subtotal only; not the number to lead with.
346 pub bonus_points: f64,
347 /// Quest progress.
348 pub quests: Vec<QuestState>,
349 /// Achievement progress.
350 pub achievements: Vec<AchievementState>,
351}