Skip to main content

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);