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