bonfida_utils/
pyth.rs

1use crate::{checks::check_account_owner, tokens::SupportedToken};
2use borsh::BorshDeserialize;
3use pyth_sdk_solana::{
4    state::{
5        load_mapping_account, load_product_account, CorpAction, PriceStatus, PriceType,
6        SolanaPriceAccount,
7    },
8    Price,
9};
10use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;
11use solana_program::{
12    account_info::AccountInfo, clock::Clock, msg, program_error::ProgramError, pubkey::Pubkey,
13};
14use solana_program::{pubkey, sysvar::Sysvar};
15use std::convert::TryInto;
16
17pub const DEFAULT_PYTH_PUSH: Pubkey = pubkey!("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT");
18pub const PRICE_FEED_DISCRIMATOR: [u8; 8] = [34, 241, 35, 99, 157, 126, 244, 205];
19
20pub fn check_price_acc_key(
21    mapping_acc_data: &[u8],
22    product_acc_key: &Pubkey,
23    product_acc_data: &[u8],
24    price_acc_key: &Pubkey,
25) -> Result<(), ProgramError> {
26    // Only checking the first mapping account
27    let map_acct = load_mapping_account(mapping_acc_data).unwrap();
28
29    // Get and print each Product in Mapping directory
30    for prod_key in &map_acct.products {
31        if product_acc_key != prod_key {
32            continue;
33        }
34        msg!("Found product in mapping.");
35
36        let prod_acc = load_product_account(product_acc_data).unwrap();
37
38        if prod_acc.px_acc == Pubkey::default() {
39            msg!("Price account is invalid.");
40            break;
41        }
42
43        // Check only the first price account
44        if price_acc_key == &prod_acc.px_acc {
45            msg!("Found correct price account in product.");
46            return Ok(());
47        }
48    }
49
50    msg!("Could not find product in mapping.");
51    Err(ProgramError::InvalidArgument)
52}
53
54pub fn get_oracle_price_fp32(
55    price_account_info: &AccountInfo,
56    no_older_than_s: u64,
57    base_decimals: u8,
58    quote_decimals: u8,
59) -> Result<u64, ProgramError> {
60    #[cfg(feature = "mock-oracle")]
61    {
62        // Mock testing oracle
63        if account_data.len() == 8 {
64            return Ok(u64::from_le_bytes(account_data[0..8].try_into().unwrap()));
65        }
66    };
67
68    // Pyth Oracle
69    let price_account = SolanaPriceAccount::account_info_to_feed(price_account_info)?;
70    // load_price_account(account_data)?;
71    let Price { price, expo, .. } = price_account
72        .get_price_no_older_than(Clock::get()?.unix_timestamp, no_older_than_s)
73        .ok_or_else(|| {
74            msg!("Cannot parse pyth price, information unavailable.");
75            ProgramError::InvalidAccountData
76        })?;
77    let price = if expo > 0 {
78        ((price as u128) << 32) * 10u128.pow(expo as u32)
79    } else {
80        ((price as u128) << 32) / 10u128.pow((-expo) as u32)
81    };
82
83    let corrected_price =
84        (price * 10u128.pow(quote_decimals as u32)) / 10u128.pow(base_decimals as u32);
85
86    let final_price = corrected_price.try_into().unwrap();
87
88    msg!("Pyth FP32 price value: {:?}", final_price);
89
90    Ok(final_price)
91}
92
93pub fn get_oracle_ema_price_fp32(
94    price_account_info: &AccountInfo,
95    no_older_than_s: u64,
96    base_decimals: u8,
97    quote_decimals: u8,
98) -> Result<u64, ProgramError> {
99    #[cfg(feature = "mock-oracle")]
100    {
101        // Mock testing oracle
102        if account_data.len() == 8 {
103            return Ok(u64::from_le_bytes(account_data[0..8].try_into().unwrap()));
104        }
105    };
106
107    // Pyth Oracle
108    let price_account = SolanaPriceAccount::account_info_to_feed(price_account_info)?;
109    let Price { price, expo, .. } = price_account
110        .get_ema_price_no_older_than(Clock::get()?.unix_timestamp, no_older_than_s)
111        .ok_or_else(|| {
112            msg!("Cannot parse pyth ema price, information unavailable.");
113            ProgramError::InvalidAccountData
114        })?;
115    let price = if expo > 0 {
116        ((price as u128) << 32) * 10u128.pow(expo as u32)
117    } else {
118        ((price as u128) << 32) / 10u128.pow((-expo) as u32)
119    };
120
121    let corrected_price =
122        (price * 10u128.pow(quote_decimals as u32)) / 10u128.pow(base_decimals as u32);
123
124    let final_price = corrected_price.try_into().unwrap();
125
126    msg!("Pyth FP32 price value: {:?}", final_price);
127
128    Ok(final_price)
129}
130
131pub fn get_oracle_price_or_ema_fp32(
132    price_account_info: &AccountInfo,
133    no_older_than_s: u64,
134    base_decimals: u8,
135    quote_decimals: u8,
136) -> Result<u64, ProgramError> {
137    #[cfg(feature = "mock-oracle")]
138    {
139        // Mock testing oracle
140        if account_data.len() == 8 {
141            return Ok(u64::from_le_bytes(account_data[0..8].try_into().unwrap()));
142        }
143    };
144
145    // Pyth Oracle
146    let price_feed = SolanaPriceAccount::account_info_to_feed(price_account_info)?;
147    let unix_timestamp = Clock::get()?.unix_timestamp;
148    let Price { price, expo, .. } = price_feed
149        .get_price_no_older_than(unix_timestamp, no_older_than_s)
150        .or_else(|| {
151            msg!("Cannot parse pyth price, information unavailable. Fallback on EMA");
152            price_feed.get_ema_price_no_older_than(unix_timestamp, no_older_than_s)
153        })
154        .unwrap();
155    let price = if expo > 0 {
156        ((price as u128) << 32) * 10u128.pow(expo as u32)
157    } else {
158        ((price as u128) << 32) / 10u128.pow((-expo) as u32)
159    };
160
161    let corrected_price =
162        (price * 10u128.pow(quote_decimals as u32)) / 10u128.pow(base_decimals as u32);
163
164    let final_price = corrected_price.try_into().unwrap();
165
166    msg!("Pyth FP32 price value: {:?}", final_price);
167
168    Ok(final_price)
169}
170
171pub fn parse_price_v2(data: &[u8]) -> Result<PriceUpdateV2, ProgramError> {
172    let tag = &data[..8];
173
174    if tag != PRICE_FEED_DISCRIMATOR {
175        return Err(ProgramError::InvalidAccountData);
176    }
177
178    let des = PriceUpdateV2::deserialize(&mut &data[8..]).unwrap();
179
180    Ok(des)
181}
182
183// Used for Pyth v2 i.e pull model
184pub fn get_oracle_price_fp32_v2(
185    token_mint: &Pubkey,
186    account: &AccountInfo,
187    base_decimals: u8,
188    quote_decimals: u8,
189    clock: &Clock,
190    maximum_age: u64,
191) -> Result<u64, ProgramError> {
192    check_account_owner(account, &pyth_solana_receiver_sdk::ID)?;
193
194    let data = &account.data.borrow() as &[u8];
195
196    let update = parse_price_v2(data).unwrap();
197
198    let feed_id = SupportedToken::from_mint(token_mint).unwrap().price_feed();
199
200    let pyth_solana_receiver_sdk::price_update::Price {
201        price, exponent, ..
202    } = update
203        .get_price_no_older_than(clock, maximum_age, &feed_id)
204        .unwrap();
205
206    let price = if exponent > 0 {
207        ((price as u128) << 32) * 10u128.pow(exponent as u32)
208    } else {
209        ((price as u128) << 32) / 10u128.pow((-exponent) as u32)
210    };
211
212    let corrected_price =
213        (price * 10u128.pow(quote_decimals as u32)) / 10u128.pow(base_decimals as u32);
214
215    let final_price = corrected_price.try_into().unwrap();
216
217    msg!("Pyth FP32 price value: {:?}", final_price);
218
219    Ok(final_price)
220}
221
222// Used for Pyth v2 to allow any feed id without validation, the token/asset validation must be done by the caller program
223pub fn get_oracle_price_from_feed_id_fp32(
224    feed_id: &[u8; 32],
225    account: &AccountInfo,
226    base_decimals: u8,
227    quote_decimals: u8,
228    clock: &Clock,
229    maximum_age: u64,
230) -> Result<u64, ProgramError> {
231    check_account_owner(account, &pyth_solana_receiver_sdk::ID)?;
232
233    let data = &account.data.borrow() as &[u8];
234
235    let update = parse_price_v2(data).unwrap();
236
237    let pyth_solana_receiver_sdk::price_update::Price {
238        price, exponent, ..
239    } = update
240        .get_price_no_older_than(clock, maximum_age, feed_id)
241        .unwrap();
242
243    let price = if exponent > 0 {
244        ((price as u128) << 32) * 10u128.pow(exponent as u32)
245    } else {
246        ((price as u128) << 32) / 10u128.pow((-exponent) as u32)
247    };
248
249    let corrected_price =
250        (price * 10u128.pow(quote_decimals as u32)) / 10u128.pow(base_decimals as u32);
251
252    let final_price = corrected_price.try_into().unwrap();
253
254    msg!("Pyth FP32 price value: {:?}", final_price);
255
256    Ok(final_price)
257}
258
259pub fn get_pyth_feed_account_key(shard: u16, price_feed: &[u8]) -> Pubkey {
260    let seeds = &[&shard.to_le_bytes() as &[u8], price_feed];
261    let (key, _) = Pubkey::find_program_address(seeds, &DEFAULT_PYTH_PUSH);
262    key
263}
264
265pub fn get_market_symbol(pyth_product_acc_data: &[u8]) -> Result<&str, ProgramError> {
266    let pyth_product = load_product_account(pyth_product_acc_data).unwrap();
267    for (k, v) in pyth_product.iter() {
268        if k == "symbol" {
269            return Ok(v);
270        }
271    }
272    msg!("The provided pyth product account has no attribute 'symbol'.");
273    Err(ProgramError::InvalidArgument)
274}
275
276//Utils
277
278pub fn get_price_type(ptype: &PriceType) -> &'static str {
279    match ptype {
280        PriceType::Unknown => "unknown",
281        PriceType::Price => "price",
282        // PriceType::TWAP => "twap",
283        // PriceType::Volatility => "volatility",
284    }
285}
286
287pub fn get_status(st: &PriceStatus) -> &'static str {
288    match st {
289        PriceStatus::Unknown => "unknown",
290        PriceStatus::Trading => "trading",
291        PriceStatus::Halted => "halted",
292        PriceStatus::Auction => "auction",
293        PriceStatus::Ignored => "ignored",
294    }
295}
296
297pub fn get_corp_act(cact: &CorpAction) -> &'static str {
298    match cact {
299        CorpAction::NoCorpAct => "nocorpact",
300    }
301}
302
303#[cfg(test)]
304mod test {
305    use std::{cell::RefCell, rc::Rc};
306
307    use super::*;
308    #[test]
309    pub fn test_sol() {
310        // use pyth_sdk_solana::lo;
311        use solana_client::rpc_client::RpcClient;
312        use solana_program::{account_info::IntoAccountInfo, pubkey};
313
314        let pyth_sol_prod_acc = pubkey!("ALP8SdU9oARYVLgLR7LrqMNCYBnhtnQz1cj6bwgwQmgj");
315        let pyth_sol_price_acc = pubkey!("H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG");
316        let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
317
318        let prod_data = rpc_client.get_account_data(&pyth_sol_prod_acc).unwrap();
319        let symbol = get_market_symbol(&prod_data).unwrap();
320        let mut price_data_account = rpc_client.get_account(&pyth_sol_price_acc).unwrap();
321
322        let price_data = (&pyth_sol_price_acc, &mut price_data_account).into_account_info();
323        let price = get_oracle_price_fp32(&price_data, 60, 6, 6).unwrap();
324        println!("Found: '{}' FP32 Price: {}", symbol, price);
325        let ema_price = get_oracle_ema_price_fp32(&price_data, 60, 6, 6).unwrap();
326        println!("Found: '{}' FP32 EMA Price: {}", symbol, ema_price);
327    }
328
329    #[test]
330    fn print_pyth_oracles() {
331        // use pyth_client::{load_mapping, load_price, load_product};
332        use pyth_sdk_solana::state::load_price_account;
333        use solana_client::rpc_client::RpcClient;
334        use solana_program::pubkey;
335        use solana_program::pubkey::Pubkey;
336
337        let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
338        let mut pyth_mapping_account = pubkey!("AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J");
339
340        loop {
341            // Get Mapping account from key
342            let map_data = rpc_client.get_account_data(&pyth_mapping_account).unwrap();
343            let map_acct = load_mapping_account(&map_data).unwrap();
344
345            // Get and print each Product in Mapping directory
346            let mut i = 0;
347            for prod_pkey in &map_acct.products {
348                let prod_data = rpc_client.get_account_data(prod_pkey).unwrap();
349                let prod_acc = load_product_account(&prod_data).unwrap();
350
351                // print key and reference data for this Product
352                println!("product_account .. {:?}", prod_pkey);
353                for (k, v) in prod_acc.iter() {
354                    if !k.is_empty() || !v.is_empty() {
355                        println!("{} {}", k, v);
356                    }
357                }
358
359                // print all Prices that correspond to this Product
360                if prod_acc.px_acc != Pubkey::default() {
361                    let mut px_pkey = prod_acc.px_acc;
362                    loop {
363                        let pd = rpc_client.get_account_data(&px_pkey).unwrap();
364                        let pa: &SolanaPriceAccount = load_price_account(&pd).unwrap();
365                        println!("  price_account .. {:?}", px_pkey);
366                        println!("    price_type ... {}", get_price_type(&pa.ptype));
367                        println!("    exponent ..... {}", pa.expo);
368                        println!("    status ....... {}", get_status(&pa.agg.status));
369                        println!("    corp_act ..... {}", get_corp_act(&pa.agg.corp_act));
370                        println!("    price ........ {}", pa.agg.price);
371                        println!("    conf ......... {}", pa.agg.conf);
372                        println!("    valid_slot ... {}", pa.valid_slot);
373                        println!("    publish_slot . {}", pa.agg.pub_slot);
374
375                        // go to next price account in list
376                        if pa.next != Pubkey::default() {
377                            px_pkey = pa.next;
378                        } else {
379                            break;
380                        }
381                    }
382                }
383                // go to next product
384                i += 1;
385                if i == map_acct.num {
386                    break;
387                }
388            }
389
390            // go to next Mapping account in list
391            if map_acct.next == Pubkey::default() {
392                break;
393            }
394            pyth_mapping_account = map_acct.next;
395        }
396    }
397
398    #[test]
399    fn test_price_v2() {
400        let feed = SupportedToken::Sol.price_feed();
401        let key = get_pyth_feed_account_key(0, &feed);
402
403        let mut account_data = [
404            34, 241, 35, 99, 157, 126, 244, 205, 96, 49, 71, 4, 52, 13, 237, 223, 55, 31, 212, 36,
405            114, 20, 143, 36, 142, 157, 26, 109, 26, 94, 178, 172, 58, 205, 139, 127, 213, 214,
406            178, 67, 1, 239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, 209, 218, 57,
407            42, 13, 47, 142, 208, 198, 199, 188, 15, 76, 250, 200, 194, 128, 181, 109, 151, 237,
408            87, 16, 3, 0, 0, 0, 135, 164, 49, 1, 0, 0, 0, 0, 248, 255, 255, 255, 103, 85, 30, 102,
409            0, 0, 0, 0, 103, 85, 30, 102, 0, 0, 0, 0, 208, 47, 218, 39, 3, 0, 0, 0, 14, 62, 204, 0,
410            0, 0, 0, 0, 255, 5, 134, 15, 0, 0, 0, 0, 0,
411        ];
412
413        let mut lamports = u64::MAX;
414        let account_info = AccountInfo {
415            data: Rc::new(RefCell::new(&mut account_data[..])),
416            key: &key,
417            lamports: Rc::new(RefCell::new(&mut lamports)),
418            owner: &pyth_solana_receiver_sdk::ID,
419            rent_epoch: u64::MAX,
420            is_signer: false,
421            is_writable: false,
422            executable: false,
423        };
424        let clock: Clock = Clock {
425            ..Default::default()
426        };
427        let price_fp32 = get_oracle_price_fp32_v2(
428            &SupportedToken::Sol.mint(),
429            &account_info,
430            9,
431            6,
432            &clock,
433            2 * 60,
434        )
435        .unwrap();
436
437        assert_eq!(565179032, price_fp32);
438
439        let price_fp32 = get_oracle_price_from_feed_id_fp32(
440            &SupportedToken::Sol.price_feed(),
441            &account_info,
442            9,
443            6,
444            &clock,
445            2 * 60,
446        )
447        .unwrap();
448        assert_eq!(565179032, price_fp32);
449    }
450}