streak_api/state/ledger.rs
1//! Per-player stats and ticket balance (`ledger` PDA).
2//!
3//! ## Lifetime stats (never reset)
4//! - [`Ledger::total_wins`]: total rounds where the player was on the winning side.
5//! - [`Ledger::total_bets`]: total ticket-debit events (PlaceBet calls that spend tickets).
6//! - [`Ledger::peak_win_streak`]: all-time longest consecutive win run on a single series.
7//!
8//! ## Running streak (breaks on loss)
9//! - [`Ledger::win_streak`]: consecutive wins on [`last_win_series_id`] with `period` stepping
10//! by `+1`. Incremented by `ExecutorTreasury` (distribute) on each confirmed win; reset to 0
11//! by `PlaceBet` when the previous-period position is finalized as a loss.
12//!
13//! ## Weekly stats (lazy reset keyed by [`Ledger::week_number`] vs [`Treasury::current_week`])
14//! - [`Ledger::week_wins`]: wins in the current executor-defined week.
15//! - [`Ledger::week_bets`]: bets placed in the current week.
16//! - [`Ledger::week_peak_streak`]: longest streak achieved this week.
17//! - [`Ledger::week_number`]: the [`Treasury::current_week`] value when weekly stats were last
18//! reset. On any instruction that touches weekly fields, call [`Ledger::maybe_reset_weekly`]
19//! first so stale weekly data auto-clears.
20
21use steel::*;
22
23use super::{ledger_pda, StreakAccount};
24
25#[repr(C)]
26#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
27pub struct Ledger {
28 pub authority: Pubkey,
29
30 // ── ticket balance ────────────────────────────────────────────────────
31 /// Whole tickets credited on `BuyTickets`; debited on `PlaceBet`.
32 pub tickets: u64,
33 /// Post-team-cut USDC micros mirroring `tickets` (1 ticket = `TICKET_MICROS` µ).
34 pub unstaked_micros: u64,
35
36 // ── running streak ────────────────────────────────────────────────────
37 /// Consecutive wins on [`last_win_series_id`] with `period + 1` steps.
38 pub win_streak: u64,
39 /// Last `Market::period` confirmed as a win (via `ExecutorTreasury` distribute). `0` = none.
40 pub last_win_period: u64,
41 /// `series_id` for [`last_win_period`] (streak concurrency key).
42 pub last_win_series_id: u16,
43 pub _pad0: [u8; 6],
44
45 // ── lifetime stats ────────────────────────────────────────────────────
46 /// Total rounds on the winning side (all-time correct calls).
47 pub total_wins: u64,
48 /// Total ticket-debit events across all `PlaceBet` calls (each stake spend = +1).
49 pub total_bets: u64,
50 /// All-time highest `win_streak` ever recorded for this player.
51 pub peak_win_streak: u64,
52
53 // ── weekly stats (reset lazily when week_number != Treasury::current_week) ──
54 /// Wins in the current executor week.
55 pub week_wins: u64,
56 /// Bets placed in the current executor week.
57 pub week_bets: u64,
58 /// Longest streak achieved in the current executor week.
59 pub week_peak_streak: u64,
60 /// Which executor week these `week_*` stats belong to. Compared against
61 /// `Treasury::current_week`; if stale, zero weekly fields and update this.
62 pub week_number: u64,
63}
64
65impl Ledger {
66 pub fn pda(authority: Pubkey) -> (Pubkey, u8) {
67 ledger_pda(authority)
68 }
69
70 /// Reset weekly counters if the executor has advanced [`Treasury::current_week`].
71 ///
72 /// Call at the top of any code path that reads or writes weekly stats, passing the
73 /// value read from `Treasury::current_week`.
74 #[inline(always)]
75 pub fn maybe_reset_weekly(&mut self, current_week: u64) {
76 if self.week_number != current_week {
77 self.week_number = current_week;
78 self.week_wins = 0;
79 self.week_bets = 0;
80 self.week_peak_streak = 0;
81 }
82 }
83
84 /// Record a confirmed win for this player. Called from `ExecutorTreasury` (distribute).
85 ///
86 /// Updates streak, peak streak, total wins, and weekly equivalents.
87 /// `current_week` must be read from `Treasury::current_week` before calling.
88 #[inline(always)]
89 pub fn record_win(&mut self, market_period: u64, series_id: u16, current_week: u64) {
90 self.maybe_reset_weekly(current_week);
91
92 // Advance running streak.
93 if self.last_win_period == 0 {
94 self.win_streak = 1;
95 } else if market_period == self.last_win_period.saturating_add(1)
96 && self.last_win_series_id == series_id
97 {
98 self.win_streak = self.win_streak.saturating_add(1);
99 } else {
100 self.win_streak = 1;
101 }
102 self.last_win_period = market_period;
103 self.last_win_series_id = series_id;
104
105 // Update peaks.
106 if self.win_streak > self.peak_win_streak {
107 self.peak_win_streak = self.win_streak;
108 }
109 if self.win_streak > self.week_peak_streak {
110 self.week_peak_streak = self.win_streak;
111 }
112
113 // Lifetime + weekly counters.
114 self.total_wins = self.total_wins.saturating_add(1);
115 self.week_wins = self.week_wins.saturating_add(1);
116 }
117
118 /// Record a loss (streak break). Called from `PlaceBet` when the previous period is settled
119 /// as a loss for this player.
120 ///
121 /// `current_week` must be read from `Treasury::current_week` before calling.
122 #[inline(always)]
123 pub fn record_loss(&mut self, current_week: u64) {
124 self.maybe_reset_weekly(current_week);
125 // streak breaks; weekly/lifetime peak already captured at last win — keep them
126 self.win_streak = 0;
127 self.last_win_period = 0;
128 self.last_win_series_id = 0;
129 }
130
131 /// Record a bet (ticket debit). Called from `PlaceBet` whenever tickets are actually spent.
132 ///
133 /// `current_week` must be read from `Treasury::current_week` before calling.
134 #[inline(always)]
135 pub fn record_bet(&mut self, current_week: u64) {
136 self.maybe_reset_weekly(current_week);
137 self.total_bets = self.total_bets.saturating_add(1);
138 self.week_bets = self.week_bets.saturating_add(1);
139 }
140}
141
142account!(StreakAccount, Ledger);