streak-api 0.1.3

API for interacting with the STREAK directional markets protocol on Solana
//! Period directional market (`market` PDA), scoped by **`series_id`** — concurrent **series** (e.g. **BTC 5m** vs **ETH 5m**).
//!
//! **`PlaceBet`** / **`BuyTickets`** use **`previous_*`** **`(series_id, period-1)`**.

use crate::error::StreakError;
use steel::*;

use super::{market_pda, Position, StreakAccount};

#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Market {
    pub period: u64,
    pub series_id: u16,
    pub _pad_series: [u8; 6],
    /// Inclusive Unix second when betting may begin (**after** open anchor within grace window).
    pub open_ts: i64,
    /// Exclusive Unix second end of betting window (**betting**: **`open_ts ≤ now < close_ts`**).
    pub close_ts: i64,
    /// Commitment to resolution rules (e.g. hash of IPFS CID); UX / audits — does not affect oracle math.
    pub rules_hash: [u8; 32],
    pub total_up: u64,
    pub total_down: u64,
    /// Losing-side stake totals at settle (for pro-rata pot).
    pub loser_pot: u64,
    /// Subsidy from [`Treasury`](super::Treasury)::`daily_jackpot` at settle (`USDC` base units).
    pub reward_pool: u64,
    /// Winning-side stake total at settle.
    pub winning_total: u64,
    /// Legacy Pyth **price account** pubkey (passed at **`InitMarket`**).
    pub pyth_price_feed: Pubkey,
    /// Pyth aggregate (**`price`**) frozen by **`InitMarket`** near **`open_ts`**.
    pub open_ref_price: i64,
    /// Matching Pyth **`publish_time`** (Unix seconds).
    pub open_ref_publish_time: i64,
    /// Pyth aggregate exponent for **`open_ref_price`**.
    pub open_ref_expo: i32,
    pub _pad_expo: [u8; 4],
    pub outcome: u8,
    pub status: u8,
    pub _pad_pre_commit: [u8; 6],
    /// Stakes held in **[`Position::STATE_COMMITTED_PREOPEN`]** (USDC micros, mirrors **`total_up/down`** units); must be **0** before **[`AdminInstantSettlement`].
    pub committed_up: u64,
    pub committed_down: u64,
}

impl Market {
    /// Same encoding as [`Position::side`](super::Position::side): resolved **UP** won.
    pub const SIDE_UP: u8 = 0;

    /// Resolved **DOWN** won (includes oracle tie at normalized precision — strict-up wins only).
    pub const SIDE_DOWN: u8 = 1;

    /// Before **`Market`** (**`ADMIN_INSTANT`** / **`FINALIZE`**); replaced at settle by [`Self::SIDE_UP`] or [`Self::SIDE_DOWN`].
    pub const OUTCOME_UNSET: u8 = 255;

    /// Accepting stakes via **`PlaceBet`** while **`STATUS_OPEN`**.
    pub const STATUS_OPEN: u8 = 0;

    /// Resolved; executor **`ExecutorTreasury`** (**`distribute`**) for winners, or loss rollover on the next **`BuyTickets`** / **`PlaceBet`**.
    pub const STATUS_SETTLED: u8 = 1;

    pub fn pda(series_id: u16, period: u64) -> (Pubkey, u8) {
        market_pda(series_id, period)
    }

    pub fn init_open(
        series_id: u16,
        period: u64,
        open_ts: i64,
        close_ts: i64,
        pyth_price_feed: Pubkey,
        rules_hash: [u8; 32],
    ) -> Self {
        Self {
            period,
            series_id,
            _pad_series: [0; 6],
            open_ts,
            close_ts,
            rules_hash,
            total_up: 0,
            total_down: 0,
            loser_pot: 0,
            reward_pool: 0,
            winning_total: 0,
            pyth_price_feed,
            open_ref_price: 0,
            open_ref_publish_time: 0,
            open_ref_expo: 0,
            _pad_expo: [0; 4],
            outcome: Self::OUTCOME_UNSET,
            status: Self::STATUS_OPEN,
            _pad_pre_commit: [0; 6],
            committed_up: 0,
            committed_down: 0,
        }
    }

    #[inline(always)]
    pub fn has_open_oracle_snapshot(&self) -> bool {
        self.open_ref_publish_time != 0
    }

    /// Drain **[`Position::STATE_COMMITTED_PREOPEN`]** from **`committed_*`** into **`total_*`**, **`pos.state`** → **`STATE_PENDING`** ( **`PlaceBet`** live activation or **`ExecutorTreasury`** **`MERGE_COMMITTED`** ).
    pub fn merge_committed_preopen_into_pool(&mut self, pos: &mut Position) -> Result<(), StreakError> {
        if pos.state != Position::STATE_COMMITTED_PREOPEN {
            return Err(StreakError::BadMarketState);
        }
        let s = pos.stake;
        if s == 0 {
            return Err(StreakError::BadMarketState);
        }
        match pos.side {
            Self::SIDE_UP => {
                self.committed_up = self
                    .committed_up
                    .checked_sub(s)
                    .ok_or(StreakError::Overflow)?;
                self.total_up = self.total_up.checked_add(s).ok_or(StreakError::Overflow)?;
            }
            Self::SIDE_DOWN => {
                self.committed_down = self
                    .committed_down
                    .checked_sub(s)
                    .ok_or(StreakError::Overflow)?;
                self.total_down = self.total_down.checked_add(s).ok_or(StreakError::Overflow)?;
            }
            _ => return Err(StreakError::BadMarketState),
        }
        pos.state = Position::STATE_PENDING;
        Ok(())
    }
}

account!(StreakAccount, Market);