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 let map_acct = load_mapping_account(mapping_acc_data).unwrap();
28
29 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 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 if account_data.len() == 8 {
64 return Ok(u64::from_le_bytes(account_data[0..8].try_into().unwrap()));
65 }
66 };
67
68 let price_account = SolanaPriceAccount::account_info_to_feed(price_account_info)?;
70 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 if account_data.len() == 8 {
103 return Ok(u64::from_le_bytes(account_data[0..8].try_into().unwrap()));
104 }
105 };
106
107 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 if account_data.len() == 8 {
141 return Ok(u64::from_le_bytes(account_data[0..8].try_into().unwrap()));
142 }
143 };
144
145 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
183pub 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
222pub 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
276pub fn get_price_type(ptype: &PriceType) -> &'static str {
279 match ptype {
280 PriceType::Unknown => "unknown",
281 PriceType::Price => "price",
282 }
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 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_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 let map_data = rpc_client.get_account_data(&pyth_mapping_account).unwrap();
343 let map_acct = load_mapping_account(&map_data).unwrap();
344
345 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 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 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 if pa.next != Pubkey::default() {
377 px_pkey = pa.next;
378 } else {
379 break;
380 }
381 }
382 }
383 i += 1;
385 if i == map_acct.num {
386 break;
387 }
388 }
389
390 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}