streak_api/event.rs
1//! On-chain events (`sol_log_data` via Steel `event!` / `.log()`).
2//!
3//! ## Indexer contract
4//!
5//! The off-chain indexer **must subscribe to these events** to reconstruct daily and weekly
6//! leaderboard state. Accounts alone are insufficient for daily rank snapshots because the on-chain
7//! Ledger only maintains rolling weekly aggregates, not per-day history.
8//!
9//! ### Daily leaderboard reconstruction
10//!
11//! For each UTC day the indexer:
12//! 1. Groups `WinRecorded` events by `(player, UTC_day(period))` → daily wins + daily best streak.
13//! 2. Groups `BetRecorded` events by `(player, UTC_day(period))` → daily bet count.
14//! 3. Excludes periods matching any `MarketVoided` event from accuracy calculations.
15//! 4. Uses `StreakBroken` events to confirm streak resets (optional — can also derive from missing
16//! consecutive `WinRecorded` run).
17//! 5. At UTC midnight, snapshots rank order for each player → feeds into weekly scoring.
18//!
19//! The `period` field maps to a UTC day via the `Market::open_ts` / `Market::close_ts` stored
20//! on the Market PDA (288 periods per day for 5-minute rounds).
21//!
22//! ### Weekly score inputs (all on-chain, readable from events or Ledger state)
23//!
24//! | Component | Weight | Source |
25//! |---|---|---|
26//! | Best streak of the week | 50% | `WinRecorded.win_streak` peak per week |
27//! | Daily leaderboard placement | 30% | Off-chain daily rank snapshots |
28//! | Total correct calls | 10% | `WinRecorded.total_wins` at week end |
29//! | Accuracy rate | 10% | `total_wins / total_bets` (min ~50 bets enforced off-chain) |
30
31use steel::*;
32
33/// Emitted after successful [`Initialize`](crate::instruction::Initialize).
34#[repr(C)]
35#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
36pub struct Initialized {
37 pub admin: Pubkey,
38}
39
40/// Emitted by `ExecutorTreasury` DISTRIBUTE when a win is confirmed for a player.
41///
42/// Contains full stat snapshot at the moment of win for accurate daily/weekly indexing.
43#[repr(C)]
44#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
45pub struct WinRecorded {
46 pub player: Pubkey,
47 pub series_id: u16,
48 pub _pad: [u8; 6],
49 /// Market period that was won (derive UTC day from `Market::open_ts`).
50 pub period: u64,
51 /// Running win streak **after** this win.
52 pub win_streak: u64,
53 /// All-time highest streak ever (updated if this win set a new peak).
54 pub peak_win_streak: u64,
55 /// Lifetime correct calls after this win.
56 pub total_wins: u64,
57 /// Correct calls in the current executor week after this win.
58 pub week_wins: u64,
59 /// Best streak achieved in the current executor week after this win.
60 pub week_peak_streak: u64,
61}
62
63/// Emitted by `PlaceBet` when a player's streak is broken (previous period settled as a loss).
64///
65/// The indexer uses this to confirm streak resets and close the daily streak window.
66#[repr(C)]
67#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
68pub struct StreakBroken {
69 pub player: Pubkey,
70 pub series_id: u16,
71 pub _pad: [u8; 6],
72 /// The period whose settlement caused the streak break.
73 pub period: u64,
74}
75
76/// Emitted by `PlaceBet` when tickets are actually debited (both commit and live windows).
77///
78/// Enables accurate daily bet counting for accuracy-rate scoring.
79/// Void-refunded bets are NOT un-counted here; the indexer excludes voided periods
80/// from accuracy calculations using [`MarketVoided`] events instead.
81#[repr(C)]
82#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
83pub struct BetRecorded {
84 pub player: Pubkey,
85 pub series_id: u16,
86 /// `Market::SIDE_UP` (0) or `Market::SIDE_DOWN` (1).
87 pub side: u8,
88 pub _pad: [u8; 5],
89 pub period: u64,
90 /// Lifetime total bets after this one.
91 pub total_bets: u64,
92 /// Week bets after this one.
93 pub week_bets: u64,
94}
95
96/// Emitted by `AdminVoidMarket` when a market is voided.
97///
98/// The indexer must exclude this `(series_id, period)` from accuracy calculations
99/// and not count any wins/losses for it in the leaderboard.
100#[repr(C)]
101#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
102pub struct MarketVoided {
103 pub series_id: u16,
104 pub _pad: [u8; 6],
105 pub period: u64,
106}
107
108/// Emitted by `AdminRefundVoidPosition` for each refunded position.
109///
110/// Lets the indexer track that the player's ticket was returned and the bet should be excluded.
111#[repr(C)]
112#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
113pub struct VoidRefunded {
114 pub player: Pubkey,
115 pub series_id: u16,
116 pub _pad: [u8; 6],
117 pub period: u64,
118 pub tickets_refunded: u64,
119}
120
121event!(Initialized);
122event!(WinRecorded);
123event!(StreakBroken);
124event!(BetRecorded);
125event!(MarketVoided);
126event!(VoidRefunded);