streak-api 0.1.2

API for interacting with the STREAK directional markets protocol on Solana
//! Settlement helpers shared by **`Market`** ix (**`ADMIN_INSTANT`** and **`FINALIZE`** **`settlement_kind`** branches).

use solana_program::{account_info::AccountInfo, program_error::ProgramError};

use crate::error::StreakError;
use crate::pyth::{load_fresh_price, outcome_from_open_close, Price};
use crate::state::{Market, Treasury};
use steel::AccountInfoValidation;

/// UP/DOWN from **`Market::open_*`** vs fresh **Pyth** close (**tie → DOWN**).
pub fn oracle_outcome_market_vs_pyth(
    market: &Market,
    pyth_price_feed_info: &AccountInfo<'_>,
    unix_timestamp: i64,
) -> Result<u8, ProgramError> {
    if !market.has_open_oracle_snapshot() {
        return Err(StreakError::MarketNotOracleAnchored.into());
    }
    pyth_price_feed_info.has_address(&market.pyth_price_feed)?;

    let open_px = Price {
        price: market.open_ref_price,
        conf: 0,
        expo: market.open_ref_expo,
        publish_time: market.open_ref_publish_time,
    };
    let close_px = load_fresh_price(pyth_price_feed_info, unix_timestamp)?;
    outcome_from_open_close(open_px, close_px)
}

/// Applies oracle outcome and settles **`Market`**. Jackpot subsidy **`reward_pool`** is derived on-chain:
/// **`min(Treasury::daily_jackpot, loser_pot)`** when **`winner_total > 0`**, else **`0`** (no instruction argument).
pub fn apply_settlement_outcome(
    market: &mut Market,
    treasury: &mut Treasury,
    period: u64,
    outcome: u8,
) -> Result<(), ProgramError> {
    if market.period != period {
        return Err(StreakError::BadMarketState.into());
    }
    if market.status != Market::STATUS_OPEN {
        return Err(StreakError::BadMarketState.into());
    }

    let winner_total = if outcome == Market::SIDE_UP {
        market.total_up
    } else {
        market.total_down
    };
    let loser_pot = if outcome == Market::SIDE_UP {
        market.total_down
    } else {
        market.total_up
    };

    let reward_pool = if winner_total == 0 {
        0u64
    } else {
        treasury.daily_jackpot.min(loser_pot)
    };

    if reward_pool > 0 {
        treasury.daily_jackpot = treasury
            .daily_jackpot
            .checked_sub(reward_pool)
            .ok_or(StreakError::InsufficientTreasury)?;
    }

    market.loser_pot = loser_pot;
    market.reward_pool = reward_pool;
    market.winning_total = winner_total;
    market.outcome = outcome;
    market.status = Market::STATUS_SETTLED;

    // No winner DISTRIBUTE path exists (`EmptyWinningSide`) — advance the Polymarket-style line immediately.
    // If there are winners, `Treasury::next_market_period` stays at `period` until **`ExecutorTreasury`**
    // **`MARKET_LINE_RELEASE`** after the executor finishes **`DISTRIBUTE_MARKET_REWARD`** batch(es).
    if winner_total == 0 {
        treasury.next_market_period = period
            .checked_add(1)
            .ok_or(StreakError::Overflow)?;
    }

    Ok(())
}