#![allow(dead_code)]
use borsh::BorshDeserialize;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program_error::ProgramError,
};
use crate::consts::{BTC_VOID_THRESHOLD_BPS, PYTH_MAX_PRICE_AGE_SECS, PYTH_RECEIVER_PROGRAM};
use crate::error::StreakError;
use crate::state::Market;
#[derive(Clone, Copy, Debug)]
pub struct Price {
pub price: i64,
pub conf: u64,
pub expo: i32,
pub publish_time: i64,
}
impl Price {
pub fn scale_to_exponent(&self, target_expo: i32) -> Option<Price> {
let diff = self.expo - target_expo;
if diff == 0 {
return Some(*self);
}
if diff > 0 {
let factor = 10i64.checked_pow(diff as u32)?;
Some(Price {
price: self.price.checked_div(factor)?,
conf: self.conf / (factor as u64),
expo: target_expo,
publish_time: self.publish_time,
})
} else {
let factor = 10i64.checked_pow((-diff) as u32)?;
Some(Price {
price: self.price.checked_mul(factor)?,
conf: self.conf.checked_mul(factor as u64)?,
expo: target_expo,
publish_time: self.publish_time,
})
}
}
}
#[derive(BorshDeserialize, Clone, Debug)]
#[allow(dead_code)]
enum VerificationLevel {
Partial { num_signatures: u8 },
Full,
}
#[derive(BorshDeserialize, Clone, Debug)]
struct PriceFeedMessage {
pub feed_id: [u8; 32],
pub price: i64,
pub conf: u64,
pub exponent: i32,
pub publish_time: i64,
pub prev_publish_time: i64,
pub ema_price: i64,
pub ema_conf: u64,
}
#[derive(BorshDeserialize, Clone, Debug)]
struct RawPriceUpdateV2 {
pub write_authority: [u8; 32],
pub verification_level: VerificationLevel,
pub price_message: PriceFeedMessage,
pub posted_slot: u64,
}
pub fn assert_pyth_receiver_owner(info: &AccountInfo) -> ProgramResult {
if *info.key == solana_program::pubkey::Pubkey::default() {
return Err(StreakError::PythInvalidAccount.into());
}
if info.owner != &PYTH_RECEIVER_PROGRAM {
return Err(StreakError::PythBadOwner.into());
}
Ok(())
}
pub fn load_fresh_price_hermes(
info: &AccountInfo,
expected_feed_id: &[u8; 32],
unix_timestamp: i64,
) -> Result<Price, ProgramError> {
assert_pyth_receiver_owner(info)?;
let data = info.try_borrow_data()?;
if data.len() < 8 {
return Err(StreakError::PythInvalidAccount.into());
}
let raw = RawPriceUpdateV2::deserialize(&mut &data[8..])
.map_err(|_| ProgramError::from(StreakError::PythInvalidAccount))?;
if &raw.price_message.feed_id != expected_feed_id {
return Err(StreakError::PythInvalidAccount.into());
}
let age = unix_timestamp.saturating_sub(raw.price_message.publish_time);
if age < 0 || age as u64 > PYTH_MAX_PRICE_AGE_SECS {
return Err(StreakError::OraclePriceStale.into());
}
Ok(Price {
price: raw.price_message.price,
conf: raw.price_message.conf,
expo: raw.price_message.exponent,
publish_time: raw.price_message.publish_time,
})
}
pub fn anchor_open_snapshot(
market: &mut Market,
pyth_price_feed_info: &AccountInfo,
unix_timestamp: i64,
) -> Result<(), ProgramError> {
let feed_id: [u8; 32] = market.pyth_price_feed.to_bytes();
let px = load_fresh_price_hermes(pyth_price_feed_info, &feed_id, unix_timestamp)?;
market.open_ref_price = px.price;
market.open_ref_expo = px.expo;
market.open_ref_publish_time = px.publish_time;
Ok(())
}
pub fn outcome_from_open_close(open: Price, close: Price) -> Result<Option<u8>, ProgramError> {
const EXP: i32 = -8;
let o = open
.scale_to_exponent(EXP)
.ok_or(StreakError::OracleNormalize)?;
let c = close
.scale_to_exponent(EXP)
.ok_or(StreakError::OracleNormalize)?;
if o.price > 0 {
let delta = (c.price - o.price).unsigned_abs() as u128;
let open_abs = o.price.unsigned_abs() as u128;
let move_bps = delta.saturating_mul(10_000) / open_abs;
if move_bps < BTC_VOID_THRESHOLD_BPS as u128 {
return Ok(None); }
}
if c.price > o.price {
Ok(Some(Market::SIDE_UP))
} else {
Ok(Some(Market::SIDE_DOWN))
}
}