1use anchor_lang::prelude::*;
2use solana_program::{clock::Clock, pubkey as key, pubkey::Pubkey};
3
4pub const PYTH_PUSH_ORACLE_ID: Pubkey = key!("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT");
5pub const ID: Pubkey = key!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ");
6
7pub type FeedId = [u8; 32];
8
9#[error_code]
10#[derive(PartialEq)]
11pub enum GetPriceError {
12 #[msg("This price feed update's age exceeds the requested maximum age")]
13 PriceTooOld = 10000, #[msg("The price feed update doesn't match the requested feed id")]
15 MismatchedFeedId,
16 #[msg("This price feed update has a lower verification level than the one requested")]
17 InsufficientVerificationLevel,
18 #[msg("Feed id must be 32 Bytes, that's 64 hex characters or 66 with a 0x prefix")]
19 FeedIdMustBe32Bytes,
20 #[msg("Feed id contains non-hex characters")]
21 FeedIdNonHexCharacter,
22}
23
24macro_rules! check {
25 ($cond:expr, $err:expr) => {
26 if !$cond {
27 return Err($err);
28 }
29 };
30}
31
32#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
33pub struct PriceFeedMessage {
34 pub feed_id: FeedId,
35 pub price: i64,
36 pub conf: u64,
37 pub exponent: i32,
38 pub publish_time: i64,
39 pub prev_publish_time: i64,
40 pub ema_price: i64,
41 pub ema_conf: u64,
42}
43
44#[derive(AnchorSerialize, AnchorDeserialize)]
45pub enum VerificationLevel {
46 Partial { num_signatures: u8 },
47 Full,
48}
49
50impl VerificationLevel {
51 pub fn gte(&self, other: VerificationLevel) -> bool {
52 match self {
53 VerificationLevel::Full => true,
54 VerificationLevel::Partial { num_signatures } => match other {
55 VerificationLevel::Full => false,
56 VerificationLevel::Partial {
57 num_signatures: other_num_signatures,
58 } => *num_signatures >= other_num_signatures,
59 },
60 }
61 }
62}
63
64pub struct Price {
65 pub price: i64,
66 pub conf: u64,
67 pub exponent: i32,
68 pub publish_time: i64,
69}
70
71#[derive(AnchorSerialize, AnchorDeserialize)]
72pub struct PriceUpdateV2 {
73 pub write_authority: Pubkey,
74 pub verification_level: VerificationLevel,
75 pub price_message: PriceFeedMessage,
76 pub posted_slot: u64,
77}
78
79impl PriceUpdateV2 {
80 pub fn get_price_unchecked(
81 &self,
82 feed_id: &FeedId,
83 ) -> std::result::Result<Price, GetPriceError> {
84 check!(
85 self.price_message.feed_id == *feed_id,
86 GetPriceError::MismatchedFeedId
87 );
88 Ok(Price {
89 price: self.price_message.price,
90 conf: self.price_message.conf,
91 exponent: self.price_message.exponent,
92 publish_time: self.price_message.publish_time,
93 })
94 }
95
96 pub fn get_price_no_older_than_with_custom_verification_level(
97 &self,
98 clock: &Clock,
99 maximum_age: u64,
100 feed_id: &FeedId,
101 verification_level: VerificationLevel,
102 ) -> std::result::Result<Price, GetPriceError> {
103 check!(
104 self.verification_level.gte(verification_level),
105 GetPriceError::InsufficientVerificationLevel
106 );
107 let price = self.get_price_unchecked(feed_id)?;
108 check!(
109 price
110 .publish_time
111 .saturating_add(maximum_age.try_into().unwrap())
112 >= clock.unix_timestamp,
113 GetPriceError::PriceTooOld
114 );
115 Ok(price)
116 }
117
118 pub fn get_price_no_older_than(
119 &self,
120 clock: &Clock,
121 maximum_age: u64,
122 feed_id: &FeedId,
123 ) -> std::result::Result<Price, GetPriceError> {
124 self.get_price_no_older_than_with_custom_verification_level(
125 clock,
126 maximum_age,
127 feed_id,
128 VerificationLevel::Full,
129 )
130 }
131}
132
133impl anchor_lang::AccountDeserialize for PriceUpdateV2 {
134 fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
135 if buf.len() < 8 {
136 return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into());
137 }
138 let given_disc = &buf[..8];
139 if [34, 241, 35, 99, 157, 126, 244, 205] != given_disc {
140 return Err(anchor_lang::error::Error::from(
141 anchor_lang::error::AnchorError {
142 error_name: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.name(),
143 error_code_number: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch
144 .into(),
145 error_msg: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch
146 .to_string(),
147 error_origin: Some(anchor_lang::error::ErrorOrigin::AccountName(
148 "PriceUpdateV2".to_string(),
149 )),
150 compared_values: None,
151 },
152 ));
153 }
154 Self::try_deserialize_unchecked(buf)
155 }
156 fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
157 let mut data: &[u8] = &buf[8..];
158 AnchorDeserialize::deserialize(&mut data)
159 .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
160 }
161}
162
163pub fn get_oracle_key(shard_id: u16, feed_id: FeedId) -> Pubkey {
164 let (pubkey, _) =
165 Pubkey::find_program_address(&[&shard_id.to_be_bytes(), &feed_id], &PYTH_PUSH_ORACLE_ID);
166 pubkey
167}
168
169#[cfg(test)]
170mod tests {
171 use base64::Engine;
172
173 use super::*;
174
175 #[test]
176 fn test_price_update_v2() {
177 let data = base64::engine::general_purpose::STANDARD.decode("IvEjY51+9M1gMUcENA3t3zcf1CRyFI8kjp0abRpesqw6zYt/1dayQwHvDYtv2izrpB2hXUCV0do5Kg0vjtDGx7wPTPrIwoC1bdYkWB4DAAAAPA/bAAAAAAD4////rbTWZgAAAACttNZmAAAAAOx1oSEDAAAAtpvRAAAAAADMB0YTAAAAAAA=").unwrap();
178
179 let price_update = PriceUpdateV2::try_deserialize(&mut data.as_slice()).unwrap();
180
181 assert_eq!(
182 price_update.write_authority,
183 key!("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE")
184 );
185 }
186}