1#![allow(dead_code)]
14
15use borsh::BorshDeserialize;
16use solana_program::{
17 account_info::AccountInfo,
18 entrypoint::ProgramResult,
19 program_error::ProgramError,
20};
21
22use crate::consts::{PYTH_MAX_PRICE_AGE_SECS, PYTH_PUSH_ORACLE_PROGRAM, PYTH_RECEIVER_PROGRAM};
23use crate::error::StreakError;
24
25#[derive(Clone, Copy, Debug)]
30pub struct Price {
31 pub price: i64,
32 pub conf: u64,
33 pub expo: i32,
34 pub publish_time: i64,
35}
36
37impl Price {
38 pub fn scale_to_exponent(&self, target_expo: i32) -> Option<Price> {
40 let diff = self.expo - target_expo;
41 if diff == 0 {
42 return Some(*self);
43 }
44 if diff > 0 {
45 let factor = 10i64.checked_pow(diff as u32)?;
46 Some(Price {
47 price: self.price.checked_div(factor)?,
48 conf: self.conf / (factor as u64),
49 expo: target_expo,
50 publish_time: self.publish_time,
51 })
52 } else {
53 let factor = 10i64.checked_pow((-diff) as u32)?;
54 Some(Price {
55 price: self.price.checked_mul(factor)?,
56 conf: self.conf.checked_mul(factor as u64)?,
57 expo: target_expo,
58 publish_time: self.publish_time,
59 })
60 }
61 }
62}
63
64#[derive(BorshDeserialize, Clone, Debug)]
69#[allow(dead_code)]
70enum VerificationLevel {
71 Partial { num_signatures: u8 },
72 Full,
73}
74
75#[derive(BorshDeserialize, Clone, Debug)]
76struct PriceFeedMessage {
77 pub feed_id: [u8; 32],
78 pub price: i64,
79 pub conf: u64,
80 pub exponent: i32,
81 pub publish_time: i64,
82 pub prev_publish_time: i64,
83 pub ema_price: i64,
84 pub ema_conf: u64,
85}
86
87#[derive(BorshDeserialize, Clone, Debug)]
88struct RawPriceUpdateV2 {
89 pub write_authority: [u8; 32],
90 pub verification_level: VerificationLevel,
91 pub price_message: PriceFeedMessage,
92 pub posted_slot: u64,
93}
94
95pub fn assert_pyth_receiver_owner(info: &AccountInfo) -> ProgramResult {
102 if *info.key == solana_program::pubkey::Pubkey::default() {
103 return Err(StreakError::PythInvalidAccount.into());
104 }
105 if info.owner != &PYTH_RECEIVER_PROGRAM && info.owner != &PYTH_PUSH_ORACLE_PROGRAM {
106 return Err(StreakError::PythBadOwner.into());
107 }
108 Ok(())
109}
110
111pub fn load_fresh_price(
118 info: &AccountInfo,
119 expected_feed_id: &[u8; 32],
120 unix_timestamp: i64,
121) -> Result<Price, ProgramError> {
122 assert_pyth_receiver_owner(info)?;
123
124 let data = info.try_borrow_data()?;
125 if data.len() < 8 {
126 return Err(StreakError::PythInvalidAccount.into());
127 }
128 let raw = RawPriceUpdateV2::deserialize(&mut &data[8..])
129 .map_err(|_| ProgramError::from(StreakError::PythInvalidAccount))?;
130
131 if &raw.price_message.feed_id != expected_feed_id {
132 return Err(StreakError::PythInvalidAccount.into());
133 }
134
135 let age = unix_timestamp.saturating_sub(raw.price_message.publish_time);
136 if age < 0 || age as u64 > PYTH_MAX_PRICE_AGE_SECS {
137 return Err(StreakError::OraclePriceStale.into());
138 }
139
140 Ok(Price {
141 price: raw.price_message.price,
142 conf: raw.price_message.conf,
143 expo: raw.price_message.exponent,
144 publish_time: raw.price_message.publish_time,
145 })
146}
147
148pub fn outcome_from_open_close(open: Price, close: Price) -> Result<u8, ProgramError> {
157 const EXP: i32 = -8;
158 let o = open
159 .scale_to_exponent(EXP)
160 .ok_or(StreakError::OracleNormalize)?;
161 let c = close
162 .scale_to_exponent(EXP)
163 .ok_or(StreakError::OracleNormalize)?;
164
165 if c.price > o.price {
166 Ok(0u8) } else {
168 Ok(1u8) }
170}