Skip to main content

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}