Skip to content

Commit

Permalink
feat!: allow loading network-specific PriceAccount
Browse files Browse the repository at this point in the history
  • Loading branch information
Reisen committed Feb 7, 2024
1 parent 1acfc3d commit 2823816
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 29 deletions.
2 changes: 1 addition & 1 deletion examples/sol-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ crate-type = ["cdylib", "lib"]
borsh = "0.10.3"
arrayref = "0.3.6"
solana-program = ">= 1.10"
pyth-sdk-solana = { path = "../../pyth-sdk-solana", version = "0.9.0" }
pyth-sdk-solana = { path = "../../pyth-sdk-solana", version = "0.10.0" }
10 changes: 5 additions & 5 deletions examples/sol-contract/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use borsh::{
BorshDeserialize,
BorshSerialize,
};
use pyth_sdk_solana::load_price_feed_from_account_info;
use pyth_sdk_solana::state::SolanaPriceAccount;

use crate::instruction::ExampleInstructions;
use crate::state::AdminConfig;
Expand Down Expand Up @@ -53,8 +53,8 @@ pub fn process_instruction(
config.collateral_price_feed_id = *pyth_collateral_account.key;

// Make sure these Pyth price accounts can be loaded
load_price_feed_from_account_info(pyth_loan_account)?;
load_price_feed_from_account_info(pyth_collateral_account)?;
SolanaPriceAccount::account_info_to_feed(pyth_loan_account)?;
SolanaPriceAccount::account_info_to_feed(pyth_collateral_account)?;

let config_data = config.try_to_vec()?;
let config_dst = &mut admin_config_account.try_borrow_mut_data()?;
Expand Down Expand Up @@ -85,7 +85,7 @@ pub fn process_instruction(
// (price + conf) * loan_qty * 10 ^ (expo).
// Here is more explanation on confidence interval in Pyth:
// https://docs.pyth.network/consume-data/best-practices
let feed1 = load_price_feed_from_account_info(pyth_loan_account)?;
let feed1 = SolanaPriceAccount::account_info_to_feed(pyth_loan_account)?;
let current_timestamp1 = Clock::get()?.unix_timestamp;
let result1 = feed1
.get_price_no_older_than(current_timestamp1, 60)
Expand All @@ -107,7 +107,7 @@ pub fn process_instruction(
// (price - conf) * collateral_qty * 10 ^ (expo).
// Here is more explanation on confidence interval in Pyth:
// https://docs.pyth.network/consume-data/best-practices
let feed2 = load_price_feed_from_account_info(pyth_collateral_account)?;
let feed2 = SolanaPriceAccount::account_info_to_feed(pyth_collateral_account)?;
let current_timestamp2 = Clock::get()?.unix_timestamp;
let result2 = feed2
.get_price_no_older_than(current_timestamp2, 60)
Expand Down
2 changes: 1 addition & 1 deletion pyth-sdk-solana/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-sdk-solana"
version = "0.9.0"
version = "0.10.0"
authors = ["Pyth Data Foundation"]
edition = "2018"
license = "Apache-2.0"
Expand Down
4 changes: 2 additions & 2 deletions pyth-sdk-solana/examples/eth_price.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// example usage of reading pyth price from solana/pythnet price account

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

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

Expand Down
4 changes: 3 additions & 1 deletion pyth-sdk-solana/examples/get_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use pyth_sdk_solana::state::{
load_product_account,
CorpAction,
PriceType,
SolanaPriceAccount,
};
use solana_client::rpc_client::RpcClient;
use solana_program::pubkey::Pubkey;
Expand Down Expand Up @@ -62,7 +63,8 @@ fn main() {
let mut px_pkey = prod_acct.px_acc;
loop {
let price_data = clnt.get_account_data(&px_pkey).unwrap();
let price_account = load_price_account(&price_data).unwrap();
let price_account: &SolanaPriceAccount =
load_price_account(&price_data).unwrap();
let price_feed = price_account.to_price_feed(&px_pkey);

println!(" price_account .. {:?}", px_pkey);
Expand Down
41 changes: 32 additions & 9 deletions pyth-sdk-solana/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ use solana_program::account_info::{
};
use solana_program::pubkey::Pubkey;

use state::load_price_account;
use state::{
load_price_account,
GenericPriceAccount,
SolanaPriceAccount,
};

pub use pyth_sdk::{
Price,
Expand All @@ -27,24 +31,43 @@ pub use pyth_sdk::{
pub const VALID_SLOT_PERIOD: u64 = 25;

/// Loads Pyth Feed Price from Price Account Info.
#[deprecated(note = "solana-specific, use SolanaPriceAccount::from_account_info instead.")]
pub fn load_price_feed_from_account_info(
price_account_info: &AccountInfo,
) -> Result<PriceFeed, PythError> {
let data = price_account_info
.try_borrow_data()
.map_err(|_| PythError::InvalidAccountData)?;
let price_account = load_price_account(*data)?;

Ok(price_account.to_price_feed(price_account_info.key))
SolanaPriceAccount::account_info_to_feed(price_account_info)
}

/// Loads Pyth Price Feed from Account when using Solana Client.
///
/// It is a helper function which constructs Account Info when reading Account in clients.
#[deprecated(note = "solana-specific, use SolanaPriceAccount::from_account instead.")]
pub fn load_price_feed_from_account(
price_key: &Pubkey,
price_account: &mut impl Account,
) -> Result<PriceFeed, PythError> {
let price_account_info = (price_key, price_account).into_account_info();
load_price_feed_from_account_info(&price_account_info)
SolanaPriceAccount::account_to_feed(price_key, price_account)
}

impl<const N: usize, T: 'static> GenericPriceAccount<N, T>
where
T: Default,
T: Copy,
{
pub fn account_info_to_feed(price_account_info: &AccountInfo) -> Result<PriceFeed, PythError> {
load_price_account::<N, T>(
*price_account_info
.try_borrow_data()
.map_err(|_| PythError::InvalidAccountData)?,
)
.map(|acc| acc.to_price_feed(price_account_info.key))
}

pub fn account_to_feed(
price_key: &Pubkey,
price_account: &mut impl Account,
) -> Result<PriceFeed, PythError> {
let price_account_info = (price_key, price_account).into_account_info();
Self::account_info_to_feed(&price_account_info)
}
}
163 changes: 154 additions & 9 deletions pyth-sdk-solana/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,13 @@ pub struct Rational {
pub denom: i64,
}

/// Price accounts represent a continuously-updating price feed for a product.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct PriceAccount {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct GenericPriceAccount<const N: usize, T>
where
T: Default,
T: Copy,
{
/// pyth magic number
pub magic: u32,
/// program version
Expand Down Expand Up @@ -336,18 +339,102 @@ pub struct PriceAccount {
/// aggregate price info
pub agg: PriceInfo,
/// price components one per quoter
pub comp: [PriceComp; 32],
pub comp: [PriceComp; N],
/// additional extended account data
pub extended: T,
}

impl<const N: usize, T> Default for GenericPriceAccount<N, T>
where
T: Default,
T: Copy,
{
fn default() -> Self {
Self {
magic: Default::default(),
ver: Default::default(),
atype: Default::default(),
size: Default::default(),
ptype: Default::default(),
expo: Default::default(),
num: Default::default(),
num_qt: Default::default(),
last_slot: Default::default(),
valid_slot: Default::default(),
ema_price: Default::default(),
ema_conf: Default::default(),
timestamp: Default::default(),
min_pub: Default::default(),
drv2: Default::default(),
drv3: Default::default(),
drv4: Default::default(),
prod: Default::default(),
next: Default::default(),
prev_slot: Default::default(),
prev_price: Default::default(),
prev_conf: Default::default(),
prev_timestamp: Default::default(),
agg: Default::default(),
comp: [Default::default(); N],
extended: Default::default(),
}
}
}

impl<const N: usize, T> std::ops::Deref for GenericPriceAccount<N, T>
where
T: Default,
T: Copy,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.extended
}
}

#[repr(C)]
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable, PartialEq, Eq)]
pub struct PriceCumulative {
/// Cumulative sum of price * slot_gap
pub price: i128,
/// Cumulative sum of conf * slot_gap
pub conf: u128,
/// Cumulative number of slots where the price wasn't recently updated (within
/// PC_MAX_SEND_LATENCY slots). This field should be used to calculate the downtime
/// as a percent of slots between two times `T` and `t` as follows:
/// `(T.num_down_slots - t.num_down_slots) / (T.agg_.pub_slot_ - t.agg_.pub_slot_)`
pub num_down_slots: u64,
/// Padding for alignment
pub unused: u64,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct PriceAccountExt {
pub price_cumulative: PriceCumulative,
}

/// Backwards compatibility.
pub type PriceAccount = GenericPriceAccount<32, ()>;

/// Solana-specific Pyth account where the old 32-element publishers are present.
pub type SolanaPriceAccount = GenericPriceAccount<32, ()>;

/// Pythnet-specific Price accountw ith upgraded 64-element publishers and extended fields.
pub type PythnetPriceAccount = GenericPriceAccount<64, PriceAccountExt>;

#[cfg(target_endian = "little")]
unsafe impl Zeroable for PriceAccount {
unsafe impl<const N: usize, T: Default + Copy> Zeroable for GenericPriceAccount<N, T> {
}

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

impl PriceAccount {
impl<const N: usize, T> GenericPriceAccount<N, T>
where
T: Default,
T: Copy,
{
pub fn get_publish_time(&self) -> UnixTimestamp {
match self.agg.status {
PriceStatus::Trading => self.timestamp,
Expand Down Expand Up @@ -456,8 +543,11 @@ pub fn load_product_account(data: &[u8]) -> Result<&ProductAccount, PythError> {
}

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

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

assert_eq!(price_account.get_price_no_older_than(&clock, 1), None);
}

#[test]
fn test_price_feed_representations_equal() {
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct OldPriceAccount {
pub magic: u32,
pub ver: u32,
pub atype: u32,
pub size: u32,
pub ptype: crate::state::PriceType,
pub expo: i32,
pub num: u32,
pub num_qt: u32,
pub last_slot: u64,
pub valid_slot: u64,
pub ema_price: Rational,
pub ema_conf: Rational,
pub timestamp: i64,
pub min_pub: u8,
pub drv2: u8,
pub drv3: u16,
pub drv4: u32,
pub prod: Pubkey,
pub next: Pubkey,
pub prev_slot: u64,
pub prev_price: i64,
pub prev_conf: u64,
pub prev_timestamp: i64,
pub agg: PriceInfo,
pub comp: [crate::state::PriceComp; 32],
}

let old = OldPriceAccount::default();
let new = PriceAccount::default();

// Equal Sized?
assert_eq!(
std::mem::size_of::<OldPriceAccount>(),
std::mem::size_of::<PriceAccount>(),
);

// Equal Byte Representation?
unsafe {
let old_b = std::slice::from_raw_parts(
&old as *const OldPriceAccount as *const u8,
std::mem::size_of::<OldPriceAccount>(),
);
let new_b = std::slice::from_raw_parts(
&new as *const PriceAccount as *const u8,
std::mem::size_of::<PriceAccount>(),
);
assert_eq!(old_b, new_b);
}
}
}
2 changes: 1 addition & 1 deletion pyth-sdk-solana/test-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test-bpf = []
no-entrypoint = []

[dependencies]
pyth-sdk-solana = { path = "../", version = "0.9.0" }
pyth-sdk-solana = { path = "../", version = "0.10.0" }
solana-program = ">= 1.10, <= 1.16"
bytemuck = "1.7.2"
borsh = "0.10.3"
Expand Down

0 comments on commit 2823816

Please sign in to comment.