1#![allow(dead_code)]
15
16use borsh::BorshDeserialize;
17use solana_program::{
18 account_info::AccountInfo,
19 entrypoint::ProgramResult,
20 program_error::ProgramError,
21};
22
23use crate::consts::{BTC_VOID_THRESHOLD_BPS, PYTH_MAX_PRICE_AGE_SECS, PYTH_RECEIVER_PROGRAM};
24use crate::error::StreakError;
25use crate::state::Market;
26
27#[derive(Clone, Copy, Debug)]
32pub struct Price {
33 pub price: i64,
34 pub conf: u64,
35 pub expo: i32,
36 pub publish_time: i64,
37}
38
39impl Price {
40 pub fn scale_to_exponent(&self, target_expo: i32) -> Option<Price> {
42 let diff = self.expo - target_expo;
43 if diff == 0 {
44 return Some(*self);
45 }
46 if diff > 0 {
47 let factor = 10i64.checked_pow(diff as u32)?;
49 Some(Price {
50 price: self.price.checked_div(factor)?,
51 conf: self.conf / (factor as u64),
52 expo: target_expo,
53 publish_time: self.publish_time,
54 })
55 } else {
56 let factor = 10i64.checked_pow((-diff) as u32)?;
58 Some(Price {
59 price: self.price.checked_mul(factor)?,
60 conf: self.conf.checked_mul(factor as u64)?,
61 expo: target_expo,
62 publish_time: self.publish_time,
63 })
64 }
65 }
66}
67
68#[derive(BorshDeserialize, Clone, Debug)]
73#[allow(dead_code)]
74enum VerificationLevel {
75 Partial { num_signatures: u8 },
76 Full,
77}
78
79#[derive(BorshDeserialize, Clone, Debug)]
80struct PriceFeedMessage {
81 pub feed_id: [u8; 32],
82 pub price: i64,
83 pub conf: u64,
84 pub exponent: i32,
85 pub publish_time: i64,
86 pub prev_publish_time: i64,
87 pub ema_price: i64,
88 pub ema_conf: u64,
89}
90
91#[derive(BorshDeserialize, Clone, Debug)]
92struct RawPriceUpdateV2 {
93 pub write_authority: [u8; 32],
94 pub verification_level: VerificationLevel,
95 pub price_message: PriceFeedMessage,
96 pub posted_slot: u64,
97}
98
99pub fn assert_pyth_receiver_owner(info: &AccountInfo) -> ProgramResult {
104 if *info.key == solana_program::pubkey::Pubkey::default() {
105 return Err(StreakError::PythInvalidAccount.into());
106 }
107 if info.owner != &PYTH_RECEIVER_PROGRAM {
108 return Err(StreakError::PythBadOwner.into());
109 }
110 Ok(())
111}
112
113pub fn load_fresh_price_hermes(
120 info: &AccountInfo,
121 expected_feed_id: &[u8; 32],
122 unix_timestamp: i64,
123) -> Result<Price, ProgramError> {
124 assert_pyth_receiver_owner(info)?;
125
126 let data = info.try_borrow_data()?;
127 if data.len() < 8 {
129 return Err(StreakError::PythInvalidAccount.into());
130 }
131 let raw = RawPriceUpdateV2::deserialize(&mut &data[8..])
132 .map_err(|_| ProgramError::from(StreakError::PythInvalidAccount))?;
133
134 if &raw.price_message.feed_id != expected_feed_id {
135 return Err(StreakError::PythInvalidAccount.into());
136 }
137
138 let age = unix_timestamp.saturating_sub(raw.price_message.publish_time);
139 if age < 0 || age as u64 > PYTH_MAX_PRICE_AGE_SECS {
140 return Err(StreakError::OraclePriceStale.into());
141 }
142
143 Ok(Price {
144 price: raw.price_message.price,
145 conf: raw.price_message.conf,
146 expo: raw.price_message.exponent,
147 publish_time: raw.price_message.publish_time,
148 })
149}
150
151pub fn anchor_open_snapshot(
159 market: &mut Market,
160 pyth_price_feed_info: &AccountInfo,
161 unix_timestamp: i64,
162) -> Result<(), ProgramError> {
163 let feed_id: [u8; 32] = market.pyth_price_feed.to_bytes();
164 let px = load_fresh_price_hermes(pyth_price_feed_info, &feed_id, unix_timestamp)?;
165 market.open_ref_price = px.price;
166 market.open_ref_expo = px.expo;
167 market.open_ref_publish_time = px.publish_time;
168 Ok(())
169}
170
171pub fn outcome_from_open_close(open: Price, close: Price) -> Result<Option<u8>, ProgramError> {
180 const EXP: i32 = -8;
181 let o = open
182 .scale_to_exponent(EXP)
183 .ok_or(StreakError::OracleNormalize)?;
184 let c = close
185 .scale_to_exponent(EXP)
186 .ok_or(StreakError::OracleNormalize)?;
187
188 if o.price > 0 {
191 let delta = (c.price - o.price).unsigned_abs() as u128;
192 let open_abs = o.price.unsigned_abs() as u128;
193 let move_bps = delta.saturating_mul(10_000) / open_abs;
194 if move_bps < BTC_VOID_THRESHOLD_BPS as u128 {
195 return Ok(None); }
197 }
198
199 if c.price > o.price {
200 Ok(Some(Market::SIDE_UP))
201 } else {
202 Ok(Some(Market::SIDE_DOWN))
203 }
204}