Skip to content

Commit b51c507

Browse files
committed
feat!: allow loading network-specific PriceAccount
1 parent 1acfc3d commit b51c507

File tree

11 files changed

+210
-30
lines changed

11 files changed

+210
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
# This is only used for tests
3+
[toolchain]
4+
channel = "1.69.0"
5+
profile = "minimal"

examples/sol-contract/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ crate-type = ["cdylib", "lib"]
1111
borsh = "0.10.3"
1212
arrayref = "0.3.6"
1313
solana-program = ">= 1.10"
14-
pyth-sdk-solana = { path = "../../pyth-sdk-solana", version = "0.9.0" }
14+
pyth-sdk-solana = { path = "../../pyth-sdk-solana", version = "0.10.0" }

examples/sol-contract/rust-toolchain

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
# This is only used for tests
3+
[toolchain]
4+
channel = "1.69.0"
5+
profile = "minimal"

examples/sol-contract/src/processor.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use borsh::{
1818
BorshDeserialize,
1919
BorshSerialize,
2020
};
21-
use pyth_sdk_solana::load_price_feed_from_account_info;
21+
use pyth_sdk_solana::state::SolanaPriceAccount;
2222

2323
use crate::instruction::ExampleInstructions;
2424
use crate::state::AdminConfig;
@@ -53,8 +53,8 @@ pub fn process_instruction(
5353
config.collateral_price_feed_id = *pyth_collateral_account.key;
5454

5555
// Make sure these Pyth price accounts can be loaded
56-
load_price_feed_from_account_info(pyth_loan_account)?;
57-
load_price_feed_from_account_info(pyth_collateral_account)?;
56+
SolanaPriceAccount::account_info_to_feed(pyth_loan_account)?;
57+
SolanaPriceAccount::account_info_to_feed(pyth_collateral_account)?;
5858

5959
let config_data = config.try_to_vec()?;
6060
let config_dst = &mut admin_config_account.try_borrow_mut_data()?;
@@ -85,7 +85,7 @@ pub fn process_instruction(
8585
// (price + conf) * loan_qty * 10 ^ (expo).
8686
// Here is more explanation on confidence interval in Pyth:
8787
// https://docs.pyth.network/consume-data/best-practices
88-
let feed1 = load_price_feed_from_account_info(pyth_loan_account)?;
88+
let feed1 = SolanaPriceAccount::account_info_to_feed(pyth_loan_account)?;
8989
let current_timestamp1 = Clock::get()?.unix_timestamp;
9090
let result1 = feed1
9191
.get_price_no_older_than(current_timestamp1, 60)
@@ -107,7 +107,7 @@ pub fn process_instruction(
107107
// (price - conf) * collateral_qty * 10 ^ (expo).
108108
// Here is more explanation on confidence interval in Pyth:
109109
// https://docs.pyth.network/consume-data/best-practices
110-
let feed2 = load_price_feed_from_account_info(pyth_collateral_account)?;
110+
let feed2 = SolanaPriceAccount::account_info_to_feed(pyth_collateral_account)?;
111111
let current_timestamp2 = Clock::get()?.unix_timestamp;
112112
let result2 = feed2
113113
.get_price_no_older_than(current_timestamp2, 60)

pyth-sdk-solana/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-sdk-solana"
3-
version = "0.9.0"
3+
version = "0.10.0"
44
authors = ["Pyth Data Foundation"]
55
edition = "2018"
66
license = "Apache-2.0"

pyth-sdk-solana/examples/eth_price.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// example usage of reading pyth price from solana/pythnet price account
22

3-
use pyth_sdk_solana::load_price_feed_from_account;
3+
use pyth_sdk_solana::state::SolanaPriceAccount;
44
use solana_client::rpc_client::RpcClient;
55
use solana_program::pubkey::Pubkey;
66
use std::str::FromStr;
@@ -25,7 +25,7 @@ fn main() {
2525
// get price data from key
2626
let mut eth_price_account = clnt.get_account(&eth_price_key).unwrap();
2727
let eth_price_feed =
28-
load_price_feed_from_account(&eth_price_key, &mut eth_price_account).unwrap();
28+
SolanaPriceAccount::account_to_feed(&eth_price_key, &mut eth_price_account).unwrap();
2929

3030
println!(".....ETH/USD.....");
3131

pyth-sdk-solana/examples/get_accounts.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use pyth_sdk_solana::state::{
99
load_product_account,
1010
CorpAction,
1111
PriceType,
12+
SolanaPriceAccount,
1213
};
1314
use solana_client::rpc_client::RpcClient;
1415
use solana_program::pubkey::Pubkey;
@@ -62,7 +63,8 @@ fn main() {
6263
let mut px_pkey = prod_acct.px_acc;
6364
loop {
6465
let price_data = clnt.get_account_data(&px_pkey).unwrap();
65-
let price_account = load_price_account(&price_data).unwrap();
66+
let price_account: &SolanaPriceAccount =
67+
load_price_account(&price_data).unwrap();
6668
let price_feed = price_account.to_price_feed(&px_pkey);
6769

6870
println!(" price_account .. {:?}", px_pkey);

pyth-sdk-solana/src/lib.rs

+32-9
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ use solana_program::account_info::{
1414
};
1515
use solana_program::pubkey::Pubkey;
1616

17-
use state::load_price_account;
17+
use state::{
18+
load_price_account,
19+
GenericPriceAccount,
20+
SolanaPriceAccount,
21+
};
1822

1923
pub use pyth_sdk::{
2024
Price,
@@ -27,24 +31,43 @@ pub use pyth_sdk::{
2731
pub const VALID_SLOT_PERIOD: u64 = 25;
2832

2933
/// Loads Pyth Feed Price from Price Account Info.
34+
#[deprecated(note = "solana-specific, use SolanaPriceAccount::from_account_info instead.")]
3035
pub fn load_price_feed_from_account_info(
3136
price_account_info: &AccountInfo,
3237
) -> Result<PriceFeed, PythError> {
33-
let data = price_account_info
34-
.try_borrow_data()
35-
.map_err(|_| PythError::InvalidAccountData)?;
36-
let price_account = load_price_account(*data)?;
37-
38-
Ok(price_account.to_price_feed(price_account_info.key))
38+
SolanaPriceAccount::account_info_to_feed(price_account_info)
3939
}
4040

4141
/// Loads Pyth Price Feed from Account when using Solana Client.
4242
///
4343
/// It is a helper function which constructs Account Info when reading Account in clients.
44+
#[deprecated(note = "solana-specific, use SolanaPriceAccount::from_account instead.")]
4445
pub fn load_price_feed_from_account(
4546
price_key: &Pubkey,
4647
price_account: &mut impl Account,
4748
) -> Result<PriceFeed, PythError> {
48-
let price_account_info = (price_key, price_account).into_account_info();
49-
load_price_feed_from_account_info(&price_account_info)
49+
SolanaPriceAccount::account_to_feed(price_key, price_account)
50+
}
51+
52+
impl<const N: usize, T: 'static> GenericPriceAccount<N, T>
53+
where
54+
T: Default,
55+
T: Copy,
56+
{
57+
pub fn account_info_to_feed(price_account_info: &AccountInfo) -> Result<PriceFeed, PythError> {
58+
load_price_account::<N, T>(
59+
*price_account_info
60+
.try_borrow_data()
61+
.map_err(|_| PythError::InvalidAccountData)?,
62+
)
63+
.map(|acc| acc.to_price_feed(price_account_info.key))
64+
}
65+
66+
pub fn account_to_feed(
67+
price_key: &Pubkey,
68+
price_account: &mut impl Account,
69+
) -> Result<PriceFeed, PythError> {
70+
let price_account_info = (price_key, price_account).into_account_info();
71+
Self::account_info_to_feed(&price_account_info)
72+
}
5073
}

pyth-sdk-solana/src/state.rs

+154-9
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,13 @@ pub struct Rational {
283283
pub denom: i64,
284284
}
285285

286-
/// Price accounts represent a continuously-updating price feed for a product.
287-
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
288286
#[repr(C)]
289-
pub struct PriceAccount {
287+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
288+
pub struct GenericPriceAccount<const N: usize, T>
289+
where
290+
T: Default,
291+
T: Copy,
292+
{
290293
/// pyth magic number
291294
pub magic: u32,
292295
/// program version
@@ -336,18 +339,102 @@ pub struct PriceAccount {
336339
/// aggregate price info
337340
pub agg: PriceInfo,
338341
/// price components one per quoter
339-
pub comp: [PriceComp; 32],
342+
pub comp: [PriceComp; N],
343+
/// additional extended account data
344+
pub extended: T,
345+
}
346+
347+
impl<const N: usize, T> Default for GenericPriceAccount<N, T>
348+
where
349+
T: Default,
350+
T: Copy,
351+
{
352+
fn default() -> Self {
353+
Self {
354+
magic: Default::default(),
355+
ver: Default::default(),
356+
atype: Default::default(),
357+
size: Default::default(),
358+
ptype: Default::default(),
359+
expo: Default::default(),
360+
num: Default::default(),
361+
num_qt: Default::default(),
362+
last_slot: Default::default(),
363+
valid_slot: Default::default(),
364+
ema_price: Default::default(),
365+
ema_conf: Default::default(),
366+
timestamp: Default::default(),
367+
min_pub: Default::default(),
368+
drv2: Default::default(),
369+
drv3: Default::default(),
370+
drv4: Default::default(),
371+
prod: Default::default(),
372+
next: Default::default(),
373+
prev_slot: Default::default(),
374+
prev_price: Default::default(),
375+
prev_conf: Default::default(),
376+
prev_timestamp: Default::default(),
377+
agg: Default::default(),
378+
comp: [Default::default(); N],
379+
extended: Default::default(),
380+
}
381+
}
340382
}
341383

384+
impl<const N: usize, T> std::ops::Deref for GenericPriceAccount<N, T>
385+
where
386+
T: Default,
387+
T: Copy,
388+
{
389+
type Target = T;
390+
fn deref(&self) -> &Self::Target {
391+
&self.extended
392+
}
393+
}
394+
395+
#[repr(C)]
396+
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable, PartialEq, Eq)]
397+
pub struct PriceCumulative {
398+
/// Cumulative sum of price * slot_gap
399+
pub price: i128,
400+
/// Cumulative sum of conf * slot_gap
401+
pub conf: u128,
402+
/// Cumulative number of slots where the price wasn't recently updated (within
403+
/// PC_MAX_SEND_LATENCY slots). This field should be used to calculate the downtime
404+
/// as a percent of slots between two times `T` and `t` as follows:
405+
/// `(T.num_down_slots - t.num_down_slots) / (T.agg_.pub_slot_ - t.agg_.pub_slot_)`
406+
pub num_down_slots: u64,
407+
/// Padding for alignment
408+
pub unused: u64,
409+
}
410+
411+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
412+
pub struct PriceAccountExt {
413+
pub price_cumulative: PriceCumulative,
414+
}
415+
416+
/// Backwards compatibility.
417+
pub type PriceAccount = GenericPriceAccount<32, ()>;
418+
419+
/// Solana-specific Pyth account where the old 32-element publishers are present.
420+
pub type SolanaPriceAccount = GenericPriceAccount<32, ()>;
421+
422+
/// Pythnet-specific Price accountw ith upgraded 64-element publishers and extended fields.
423+
pub type PythnetPriceAccount = GenericPriceAccount<64, PriceAccountExt>;
424+
342425
#[cfg(target_endian = "little")]
343-
unsafe impl Zeroable for PriceAccount {
426+
unsafe impl<const N: usize, T: Default + Copy> Zeroable for GenericPriceAccount<N, T> {
344427
}
345428

346429
#[cfg(target_endian = "little")]
347-
unsafe impl Pod for PriceAccount {
430+
unsafe impl<const N: usize, T: Default + Copy + 'static> Pod for GenericPriceAccount<N, T> {
348431
}
349432

350-
impl PriceAccount {
433+
impl<const N: usize, T> GenericPriceAccount<N, T>
434+
where
435+
T: Default,
436+
T: Copy,
437+
{
351438
pub fn get_publish_time(&self) -> UnixTimestamp {
352439
match self.agg.status {
353440
PriceStatus::Trading => self.timestamp,
@@ -456,8 +543,11 @@ pub fn load_product_account(data: &[u8]) -> Result<&ProductAccount, PythError> {
456543
}
457544

458545
/// Get a `Price` account from the raw byte value of a Solana account.
459-
pub fn load_price_account(data: &[u8]) -> Result<&PriceAccount, PythError> {
460-
let pyth_price = load::<PriceAccount>(data).map_err(|_| PythError::InvalidAccountData)?;
546+
pub fn load_price_account<const N: usize, T: Default + Copy + 'static>(
547+
data: &[u8],
548+
) -> Result<&GenericPriceAccount<N, T>, PythError> {
549+
let pyth_price =
550+
load::<GenericPriceAccount<N, T>>(data).map_err(|_| PythError::InvalidAccountData)?;
461551

462552
if pyth_price.magic != MAGIC {
463553
return Err(PythError::InvalidAccountData);
@@ -737,4 +827,59 @@ mod test {
737827

738828
assert_eq!(price_account.get_price_no_older_than(&clock, 1), None);
739829
}
830+
831+
#[test]
832+
fn test_price_feed_representations_equal() {
833+
#[repr(C)]
834+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
835+
pub struct OldPriceAccount {
836+
pub magic: u32,
837+
pub ver: u32,
838+
pub atype: u32,
839+
pub size: u32,
840+
pub ptype: crate::state::PriceType,
841+
pub expo: i32,
842+
pub num: u32,
843+
pub num_qt: u32,
844+
pub last_slot: u64,
845+
pub valid_slot: u64,
846+
pub ema_price: Rational,
847+
pub ema_conf: Rational,
848+
pub timestamp: i64,
849+
pub min_pub: u8,
850+
pub drv2: u8,
851+
pub drv3: u16,
852+
pub drv4: u32,
853+
pub prod: Pubkey,
854+
pub next: Pubkey,
855+
pub prev_slot: u64,
856+
pub prev_price: i64,
857+
pub prev_conf: u64,
858+
pub prev_timestamp: i64,
859+
pub agg: PriceInfo,
860+
pub comp: [crate::state::PriceComp; 32],
861+
}
862+
863+
let old = OldPriceAccount::default();
864+
let new = PriceAccount::default();
865+
866+
// Equal Sized?
867+
assert_eq!(
868+
std::mem::size_of::<OldPriceAccount>(),
869+
std::mem::size_of::<PriceAccount>(),
870+
);
871+
872+
// Equal Byte Representation?
873+
unsafe {
874+
let old_b = std::slice::from_raw_parts(
875+
&old as *const OldPriceAccount as *const u8,
876+
std::mem::size_of::<OldPriceAccount>(),
877+
);
878+
let new_b = std::slice::from_raw_parts(
879+
&new as *const PriceAccount as *const u8,
880+
std::mem::size_of::<PriceAccount>(),
881+
);
882+
assert_eq!(old_b, new_b);
883+
}
884+
}
740885
}

pyth-sdk-solana/test-contract/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test-bpf = []
88
no-entrypoint = []
99

1010
[dependencies]
11-
pyth-sdk-solana = { path = "../", version = "0.9.0" }
11+
pyth-sdk-solana = { path = "../", version = "0.10.0" }
1212
solana-program = ">= 1.10, <= 1.16"
1313
bytemuck = "1.7.2"
1414
borsh = "0.10.3"
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

22
# This is only used for tests
33
[toolchain]
4-
channel = "1.68.0"
4+
channel = "1.69.0"
55
profile = "minimal"

0 commit comments

Comments
 (0)