streak-api 0.1.4

API for interacting with the STREAK directional markets protocol on Solana
Documentation
//! 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_hermes, outcome_from_open_close, Price};
use crate::state::{Market, Treasury};

/// UP/DOWN from **`Market::open_*`** vs fresh Hermes **`PriceUpdateV2`** close price.
///
/// Returns `Ok(None)` if the price move is below the void threshold (< 0.01%); the caller
/// should abort settlement and invoke `AdminVoidMarket` instead.
pub fn oracle_outcome_market_vs_pyth(
    market: &Market,
    pyth_price_feed_info: &AccountInfo<'_>,
    unix_timestamp: i64,
) -> Result<Option<u8>, ProgramError> {
    if !market.has_open_oracle_snapshot() {
        return Err(StreakError::MarketNotOracleAnchored.into());
    }

    let feed_id: [u8; 32] = market.pyth_price_feed.to_bytes();
    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_hermes(pyth_price_feed_info, &feed_id, 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_VOIDED {
        return Err(StreakError::MarketVoided.into());
    }
    if market.status != Market::STATUS_OPEN {
        return Err(StreakError::BadMarketState.into());
    }
    if market.committed_up != 0 || market.committed_down != 0 {
        return Err(StreakError::UnresolvedCommittedStakes.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;

    // Empty winning side — bump monotonic **`next_period`** floor (no DISTRIBUTE queue).
    // If **`winning_total > 0`**, payout then **`MARKET_LINE_RELEASE`** also bumps (**`max`** semantics).
    if winner_total == 0 {
        treasury.bump_next_period_floor(market.series_id, period)?;
    }

    Ok(())
}