1#![allow(dead_code)]
18
19use borsh::BorshDeserialize;
20use solana_program::{
21 account_info::AccountInfo,
22 entrypoint::ProgramResult,
23 program_error::ProgramError,
24};
25
26use crate::consts::{
27 BTC_VOID_THRESHOLD_BPS, PYTH_MAX_PRICE_AGE_SECS, PYTH_PUSH_ORACLE_PROGRAM,
28 PYTH_RECEIVER_PROGRAM,
29};
30use crate::error::StreakError;
31use crate::state::Market;
32
33#[derive(Clone, Copy, Debug)]
38pub struct Price {
39 pub price: i64,
40 pub conf: u64,
41 pub expo: i32,
42 pub publish_time: i64,
43}
44
45impl Price {
46 pub fn scale_to_exponent(&self, target_expo: i32) -> Option<Price> {
48 let diff = self.expo - target_expo;
49 if diff == 0 {
50 return Some(*self);
51 }
52 if diff > 0 {
53 let factor = 10i64.checked_pow(diff as u32)?;
55 Some(Price {
56 price: self.price.checked_div(factor)?,
57 conf: self.conf / (factor as u64),
58 expo: target_expo,
59 publish_time: self.publish_time,
60 })
61 } else {
62 let factor = 10i64.checked_pow((-diff) as u32)?;
64 Some(Price {
65 price: self.price.checked_mul(factor)?,
66 conf: self.conf.checked_mul(factor as u64)?,
67 expo: target_expo,
68 publish_time: self.publish_time,
69 })
70 }
71 }
72}
73
74#[derive(BorshDeserialize, Clone, Debug)]
79#[allow(dead_code)]
80enum VerificationLevel {
81 Partial { num_signatures: u8 },
82 Full,
83}
84
85#[derive(BorshDeserialize, Clone, Debug)]
86struct PriceFeedMessage {
87 pub feed_id: [u8; 32],
88 pub price: i64,
89 pub conf: u64,
90 pub exponent: i32,
91 pub publish_time: i64,
92 pub prev_publish_time: i64,
93 pub ema_price: i64,
94 pub ema_conf: u64,
95}
96
97#[derive(BorshDeserialize, Clone, Debug)]
98struct RawPriceUpdateV2 {
99 pub write_authority: [u8; 32],
100 pub verification_level: VerificationLevel,
101 pub price_message: PriceFeedMessage,
102 pub posted_slot: u64,
103}
104
105pub fn assert_pyth_receiver_owner(info: &AccountInfo) -> ProgramResult {
114 if *info.key == solana_program::pubkey::Pubkey::default() {
115 return Err(StreakError::PythInvalidAccount.into());
116 }
117 if info.owner != &PYTH_RECEIVER_PROGRAM && info.owner != &PYTH_PUSH_ORACLE_PROGRAM {
118 return Err(StreakError::PythBadOwner.into());
119 }
120 Ok(())
121}
122
123pub fn load_fresh_price_hermes(
130 info: &AccountInfo,
131 expected_feed_id: &[u8; 32],
132 unix_timestamp: i64,
133) -> Result<Price, ProgramError> {
134 assert_pyth_receiver_owner(info)?;
135
136 let data = info.try_borrow_data()?;
137 if data.len() < 8 {
139 return Err(StreakError::PythInvalidAccount.into());
140 }
141 let raw = RawPriceUpdateV2::deserialize(&mut &data[8..])
142 .map_err(|_| ProgramError::from(StreakError::PythInvalidAccount))?;
143
144 if &raw.price_message.feed_id != expected_feed_id {
145 return Err(StreakError::PythInvalidAccount.into());
146 }
147
148 let age = unix_timestamp.saturating_sub(raw.price_message.publish_time);
149 if age < 0 || age as u64 > PYTH_MAX_PRICE_AGE_SECS {
150 return Err(StreakError::OraclePriceStale.into());
151 }
152
153 Ok(Price {
154 price: raw.price_message.price,
155 conf: raw.price_message.conf,
156 expo: raw.price_message.exponent,
157 publish_time: raw.price_message.publish_time,
158 })
159}
160
161pub fn anchor_open_snapshot(
169 market: &mut Market,
170 pyth_price_feed_info: &AccountInfo,
171 unix_timestamp: i64,
172) -> Result<(), ProgramError> {
173 let feed_id: [u8; 32] = market.pyth_price_feed.to_bytes();
174 let px = load_fresh_price_hermes(pyth_price_feed_info, &feed_id, unix_timestamp)?;
175 market.open_ref_price = px.price;
176 market.open_ref_expo = px.expo;
177 market.open_ref_publish_time = px.publish_time;
178 Ok(())
179}
180
181pub fn outcome_from_open_close(open: Price, close: Price) -> Result<Option<u8>, ProgramError> {
190 const EXP: i32 = -8;
191 let o = open
192 .scale_to_exponent(EXP)
193 .ok_or(StreakError::OracleNormalize)?;
194 let c = close
195 .scale_to_exponent(EXP)
196 .ok_or(StreakError::OracleNormalize)?;
197
198 if o.price > 0 {
201 let delta = (c.price - o.price).unsigned_abs() as u128;
202 let open_abs = o.price.unsigned_abs() as u128;
203 let move_bps = delta.saturating_mul(10_000) / open_abs;
204 if move_bps < BTC_VOID_THRESHOLD_BPS as u128 {
205 return Ok(None); }
207 }
208
209 if c.price > o.price {
210 Ok(Some(Market::SIDE_UP))
211 } else {
212 Ok(Some(Market::SIDE_DOWN))
213 }
214}