Skip to main content

streak_api/state/
market.rs

1//! Period directional market (`market` PDA), scoped by **`series_id`** — concurrent **series** (e.g. **BTC 5m** vs **ETH 5m**).
2//!
3//! **`PlaceBet`** / **`BuyTickets`** use **`previous_*`** **`(series_id, period-1)`**.
4
5use crate::error::StreakError;
6use steel::*;
7
8use super::{market_pda, Position, StreakAccount};
9
10#[repr(C)]
11#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
12pub struct Market {
13    pub period: u64,
14    pub series_id: u16,
15    pub _pad_series: [u8; 6],
16    /// Inclusive Unix second when betting may begin (**after** open anchor within grace window).
17    pub open_ts: i64,
18    /// Exclusive Unix second end of betting window (**betting**: **`open_ts ≤ now < close_ts`**).
19    pub close_ts: i64,
20    /// Commitment to resolution rules (e.g. hash of IPFS CID); UX / audits — does not affect oracle math.
21    pub rules_hash: [u8; 32],
22    pub total_up: u64,
23    pub total_down: u64,
24    /// Losing-side stake totals at settle (for pro-rata pot).
25    pub loser_pot: u64,
26    /// Subsidy from [`Treasury`](super::Treasury)::`daily_jackpot` at settle (`USDC` base units).
27    pub reward_pool: u64,
28    /// Winning-side stake total at settle.
29    pub winning_total: u64,
30    /// Legacy Pyth **price account** pubkey (passed at **`InitMarket`**).
31    pub pyth_price_feed: Pubkey,
32    /// Pyth aggregate (**`price`**) frozen by **`InitMarket`** near **`open_ts`**.
33    pub open_ref_price: i64,
34    /// Matching Pyth **`publish_time`** (Unix seconds).
35    pub open_ref_publish_time: i64,
36    /// Pyth aggregate exponent for **`open_ref_price`**.
37    pub open_ref_expo: i32,
38    pub _pad_expo: [u8; 4],
39    pub outcome: u8,
40    pub status: u8,
41    pub _pad_pre_commit: [u8; 6],
42    /// Stakes held in **[`Position::STATE_COMMITTED_PREOPEN`]** (USDC micros, mirrors **`total_up/down`** units); must be **0** before **[`AdminInstantSettlement`].
43    pub committed_up: u64,
44    pub committed_down: u64,
45}
46
47impl Market {
48    /// Same encoding as [`Position::side`](super::Position::side): resolved **UP** won.
49    pub const SIDE_UP: u8 = 0;
50
51    /// Resolved **DOWN** won (includes oracle tie at normalized precision — strict-up wins only).
52    pub const SIDE_DOWN: u8 = 1;
53
54    /// Before **`Market`** (**`ADMIN_INSTANT`** / **`FINALIZE`**); replaced at settle by [`Self::SIDE_UP`] or [`Self::SIDE_DOWN`].
55    pub const OUTCOME_UNSET: u8 = 255;
56
57    /// Accepting stakes via **`PlaceBet`** while **`STATUS_OPEN`**.
58    pub const STATUS_OPEN: u8 = 0;
59
60    /// Resolved; executor **`ExecutorTreasury`** (**`distribute`**) for winners, or loss rollover on the next **`BuyTickets`** / **`PlaceBet`**.
61    pub const STATUS_SETTLED: u8 = 1;
62
63    /// Voided by **`AdminVoidMarket`** (e.g. BTC price move < 0.01%); positions refunded via **`AdminRefundVoidPosition`**.
64    pub const STATUS_VOIDED: u8 = 2;
65
66    /// Outcome written when a market is voided (distinct from `OUTCOME_UNSET = 255` and settled outcomes).
67    pub const OUTCOME_VOID: u8 = 254;
68
69    pub fn pda(series_id: u16, period: u64) -> (Pubkey, u8) {
70        market_pda(series_id, period)
71    }
72
73    pub fn init_open(
74        series_id: u16,
75        period: u64,
76        open_ts: i64,
77        close_ts: i64,
78        pyth_price_feed: Pubkey,
79        rules_hash: [u8; 32],
80    ) -> Self {
81        Self {
82            period,
83            series_id,
84            _pad_series: [0; 6],
85            open_ts,
86            close_ts,
87            rules_hash,
88            total_up: 0,
89            total_down: 0,
90            loser_pot: 0,
91            reward_pool: 0,
92            winning_total: 0,
93            pyth_price_feed,
94            open_ref_price: 0,
95            open_ref_publish_time: 0,
96            open_ref_expo: 0,
97            _pad_expo: [0; 4],
98            outcome: Self::OUTCOME_UNSET,
99            status: Self::STATUS_OPEN,
100            _pad_pre_commit: [0; 6],
101            committed_up: 0,
102            committed_down: 0,
103        }
104    }
105
106    #[inline(always)]
107    pub fn has_open_oracle_snapshot(&self) -> bool {
108        self.open_ref_publish_time != 0
109    }
110
111    /// Drain **[`Position::STATE_COMMITTED_PREOPEN`]** from **`committed_*`** into **`total_*`**, **`pos.state`** → **`STATE_PENDING`** ( **`PlaceBet`** live activation or **`ExecutorTreasury`** **`MERGE_COMMITTED`** ).
112    pub fn merge_committed_preopen_into_pool(&mut self, pos: &mut Position) -> Result<(), StreakError> {
113        if pos.state != Position::STATE_COMMITTED_PREOPEN {
114            return Err(StreakError::BadMarketState);
115        }
116        let s = pos.stake;
117        if s == 0 {
118            return Err(StreakError::BadMarketState);
119        }
120        match pos.side {
121            Self::SIDE_UP => {
122                self.committed_up = self
123                    .committed_up
124                    .checked_sub(s)
125                    .ok_or(StreakError::Overflow)?;
126                self.total_up = self.total_up.checked_add(s).ok_or(StreakError::Overflow)?;
127            }
128            Self::SIDE_DOWN => {
129                self.committed_down = self
130                    .committed_down
131                    .checked_sub(s)
132                    .ok_or(StreakError::Overflow)?;
133                self.total_down = self.total_down.checked_add(s).ok_or(StreakError::Overflow)?;
134            }
135            _ => return Err(StreakError::BadMarketState),
136        }
137        pos.state = Position::STATE_PENDING;
138        Ok(())
139    }
140}
141
142account!(StreakAccount, Market);