sablier_utils/
pyth.rs

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, // Big number to avoid conflicts with the SDK user's program error codes
14    #[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}