diff --git a/.github/workflows/pyth-sdk-example-anchor-contract.yml b/.github/workflows/pyth-sdk-example-anchor-contract.yml index 320da62..f8364c1 100644 --- a/.github/workflows/pyth-sdk-example-anchor-contract.yml +++ b/.github/workflows/pyth-sdk-example-anchor-contract.yml @@ -22,7 +22,7 @@ jobs: - name: Install solana binaries run: | # Installing 1.16.x cli tools to have sbf instead of bpf. bpf does not work anymore. - sh -c "$(curl -sSfL https://release.solana.com/v1.17.0/install)" + sh -c "$(curl -sSfL https://release.solana.com/v1.18.1/install)" echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - name: Install anchor binaries run: | diff --git a/.github/workflows/pyth-sdk-example-solana-contract.yml b/.github/workflows/pyth-sdk-example-solana-contract.yml index 82bb687..2628987 100644 --- a/.github/workflows/pyth-sdk-example-solana-contract.yml +++ b/.github/workflows/pyth-sdk-example-solana-contract.yml @@ -22,7 +22,7 @@ jobs: - name: Install solana binaries run: | # Installing 1.16.x cli tools to have sbf instead of bpf. bpf does not work anymore. - sh -c "$(curl -sSfL https://release.solana.com/v1.17.0/install)" + sh -c "$(curl -sSfL https://release.solana.com/v1.18.1/install)" echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - name: Build run: scripts/build.sh diff --git a/.github/workflows/pyth-sdk-solana.yml b/.github/workflows/pyth-sdk-solana.yml index 9b76483..2b25b08 100644 --- a/.github/workflows/pyth-sdk-solana.yml +++ b/.github/workflows/pyth-sdk-solana.yml @@ -35,7 +35,7 @@ jobs: - name: Install Solana Binaries run: | # Installing 1.17.x cli tools to have sbf instead of bpf. bpf does not work anymore. - sh -c "$(curl -sSfL https://release.solana.com/v1.17.0/install)" + sh -c "$(curl -sSfL https://release.solana.com/v1.18.1/install)" echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - name: Build run: cargo build --verbose diff --git a/examples/sol-anchor-contract/programs/sol-anchor-contract/Cargo.toml b/examples/sol-anchor-contract/programs/sol-anchor-contract/Cargo.toml index ccddfe4..93ba6d3 100644 --- a/examples/sol-anchor-contract/programs/sol-anchor-contract/Cargo.toml +++ b/examples/sol-anchor-contract/programs/sol-anchor-contract/Cargo.toml @@ -19,4 +19,4 @@ default = [] [dependencies] anchor-lang = "0.28.0" pyth-sdk = { path = "../../../../pyth-sdk", version = "0.8.0" } -pyth-sdk-solana = { path = "../../../../pyth-sdk-solana", version = "0.9.0" } +pyth-sdk-solana = { path = "../../../../pyth-sdk-solana", version = "0.10.0" } diff --git a/examples/sol-anchor-contract/programs/sol-anchor-contract/src/state.rs b/examples/sol-anchor-contract/programs/sol-anchor-contract/src/state.rs index 0aa8ed0..5ca8cba 100644 --- a/examples/sol-anchor-contract/programs/sol-anchor-contract/src/state.rs +++ b/examples/sol-anchor-contract/programs/sol-anchor-contract/src/state.rs @@ -1,5 +1,6 @@ use anchor_lang::prelude::*; use pyth_sdk_solana::state::load_price_account; +use pyth_sdk_solana::state::SolanaPriceAccount; use std::ops::Deref; use std::str::FromStr; @@ -25,7 +26,8 @@ impl anchor_lang::Owner for PriceFeed { impl anchor_lang::AccountDeserialize for PriceFeed { fn try_deserialize_unchecked(data: &mut &[u8]) -> Result { - let account = load_price_account(data).map_err(|_x| error!(ErrorCode::PythError))?; + let account: &SolanaPriceAccount = + load_price_account(data).map_err(|_x| error!(ErrorCode::PythError))?; // Use a dummy key since the key field will be removed from the SDK let zeros: [u8; 32] = [0; 32]; diff --git a/examples/sol-contract/Cargo.toml b/examples/sol-contract/Cargo.toml index ed2da1a..5d00052 100644 --- a/examples/sol-contract/Cargo.toml +++ b/examples/sol-contract/Cargo.toml @@ -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" } diff --git a/examples/sol-contract/src/processor.rs b/examples/sol-contract/src/processor.rs index ef5a488..e5d8cd3 100644 --- a/examples/sol-contract/src/processor.rs +++ b/examples/sol-contract/src/processor.rs @@ -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; @@ -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()?; @@ -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) @@ -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) diff --git a/pyth-sdk-solana/Cargo.toml b/pyth-sdk-solana/Cargo.toml index 81bc8cc..17617ad 100644 --- a/pyth-sdk-solana/Cargo.toml +++ b/pyth-sdk-solana/Cargo.toml @@ -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" diff --git a/pyth-sdk-solana/examples/eth_price.rs b/pyth-sdk-solana/examples/eth_price.rs index c2af89c..8a35bc3 100644 --- a/pyth-sdk-solana/examples/eth_price.rs +++ b/pyth-sdk-solana/examples/eth_price.rs @@ -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; @@ -25,7 +25,7 @@ fn main() { // get price data from key let mut eth_price_account = clnt.get_account(ð_price_key).unwrap(); let eth_price_feed = - load_price_feed_from_account(ð_price_key, &mut eth_price_account).unwrap(); + SolanaPriceAccount::account_to_feed(ð_price_key, &mut eth_price_account).unwrap(); println!(".....ETH/USD....."); diff --git a/pyth-sdk-solana/examples/get_accounts.rs b/pyth-sdk-solana/examples/get_accounts.rs index 9a876cd..de94c99 100644 --- a/pyth-sdk-solana/examples/get_accounts.rs +++ b/pyth-sdk-solana/examples/get_accounts.rs @@ -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; @@ -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); diff --git a/pyth-sdk-solana/src/lib.rs b/pyth-sdk-solana/src/lib.rs index 2169fec..e2a9864 100644 --- a/pyth-sdk-solana/src/lib.rs +++ b/pyth-sdk-solana/src/lib.rs @@ -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, @@ -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::account_info_to_feed instead.")] pub fn load_price_feed_from_account_info( price_account_info: &AccountInfo, ) -> Result { - 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::account_to_feed instead.")] pub fn load_price_feed_from_account( price_key: &Pubkey, price_account: &mut impl Account, ) -> Result { - 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 GenericPriceAccount +where + T: Default, + T: Copy, +{ + pub fn account_info_to_feed(price_account_info: &AccountInfo) -> Result { + load_price_account::( + *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 { + let price_account_info = (price_key, price_account).into_account_info(); + Self::account_info_to_feed(&price_account_info) + } } diff --git a/pyth-sdk-solana/src/state.rs b/pyth-sdk-solana/src/state.rs index 6202ba6..b0ba7ee 100644 --- a/pyth-sdk-solana/src/state.rs +++ b/pyth-sdk-solana/src/state.rs @@ -110,7 +110,6 @@ impl Default for PriceType { } } - /// Represents availability status of a price feed. #[derive( Copy, @@ -171,7 +170,6 @@ unsafe impl Zeroable for MappingAccount { unsafe impl Pod for MappingAccount { } - /// Product accounts contain metadata for a single product, such as its symbol ("Crypto.BTC/USD") /// and its base/quote currencies. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -283,10 +281,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 +where + T: Default, + T: Copy, +{ /// pyth magic number pub magic: u32, /// program version @@ -336,18 +337,103 @@ 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 Default for GenericPriceAccount +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 std::ops::Deref for GenericPriceAccount +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. +#[deprecated(note = "use an explicit SolanaPriceAccount or PythnetPriceAccount to avoid ambiguity")] +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<128, PriceAccountExt>; + #[cfg(target_endian = "little")] -unsafe impl Zeroable for PriceAccount { +unsafe impl Zeroable for GenericPriceAccount { } #[cfg(target_endian = "little")] -unsafe impl Pod for PriceAccount { +unsafe impl Pod for GenericPriceAccount { } -impl PriceAccount { +impl GenericPriceAccount +where + T: Default, + T: Copy, +{ pub fn get_publish_time(&self) -> UnixTimestamp { match self.agg.status { PriceStatus::Trading => self.timestamp, @@ -456,8 +542,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::(data).map_err(|_| PythError::InvalidAccountData)?; +pub fn load_price_account( + data: &[u8], +) -> Result<&GenericPriceAccount, PythError> { + let pyth_price = + load::>(data).map_err(|_| PythError::InvalidAccountData)?; if pyth_price.magic != MAGIC { return Err(PythError::InvalidAccountData); @@ -511,16 +600,15 @@ mod test { use solana_program::pubkey::Pubkey; use super::{ - PriceAccount, PriceInfo, PriceStatus, Rational, + SolanaPriceAccount, }; - #[test] fn test_trading_price_to_price_feed() { - let price_account = PriceAccount { + let price_account = SolanaPriceAccount { expo: 5, agg: PriceInfo { price: 10, @@ -568,7 +656,7 @@ mod test { #[test] fn test_non_trading_price_to_price_feed() { - let price_account = PriceAccount { + let price_account = SolanaPriceAccount { expo: 5, agg: PriceInfo { price: 10, @@ -616,7 +704,7 @@ mod test { #[test] fn test_happy_use_latest_price_in_price_no_older_than() { - let price_account = PriceAccount { + let price_account = SolanaPriceAccount { expo: 5, agg: PriceInfo { price: 10, @@ -650,7 +738,7 @@ mod test { #[test] fn test_happy_use_prev_price_in_price_no_older_than() { - let price_account = PriceAccount { + let price_account = SolanaPriceAccount { expo: 5, agg: PriceInfo { price: 10, @@ -685,7 +773,7 @@ mod test { #[test] fn test_sad_cur_price_unknown_in_price_no_older_than() { - let price_account = PriceAccount { + let price_account = SolanaPriceAccount { expo: 5, agg: PriceInfo { price: 10, @@ -713,7 +801,7 @@ mod test { #[test] fn test_sad_cur_price_stale_in_price_no_older_than() { - let price_account = PriceAccount { + let price_account = SolanaPriceAccount { expo: 5, agg: PriceInfo { price: 10, @@ -737,4 +825,142 @@ 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], + } + + // Would be better to fuzz this but better than no check. + let old = OldPriceAccount { + magic: 1, + ver: 2, + atype: 3, + size: 4, + ptype: crate::state::PriceType::Price, + expo: 5, + num: 6, + num_qt: 7, + last_slot: 8, + valid_slot: 9, + ema_price: Rational { + val: 1, + numer: 2, + denom: 3, + }, + ema_conf: Rational { + val: 1, + numer: 2, + denom: 3, + }, + timestamp: 12, + min_pub: 13, + drv2: 14, + drv3: 15, + drv4: 16, + prod: Pubkey::new_from_array([1; 32]), + next: Pubkey::new_from_array([2; 32]), + prev_slot: 19, + prev_price: 20, + prev_conf: 21, + prev_timestamp: 22, + agg: PriceInfo { + price: 1, + conf: 2, + status: PriceStatus::Trading, + corp_act: crate::state::CorpAction::NoCorpAct, + pub_slot: 5, + }, + comp: [Default::default(); 32], + }; + + let new = super::SolanaPriceAccount { + magic: 1, + ver: 2, + atype: 3, + size: 4, + ptype: crate::state::PriceType::Price, + expo: 5, + num: 6, + num_qt: 7, + last_slot: 8, + valid_slot: 9, + ema_price: Rational { + val: 1, + numer: 2, + denom: 3, + }, + ema_conf: Rational { + val: 1, + numer: 2, + denom: 3, + }, + timestamp: 12, + min_pub: 13, + drv2: 14, + drv3: 15, + drv4: 16, + prod: Pubkey::new_from_array([1; 32]), + next: Pubkey::new_from_array([2; 32]), + prev_slot: 19, + prev_price: 20, + prev_conf: 21, + prev_timestamp: 22, + agg: PriceInfo { + price: 1, + conf: 2, + status: PriceStatus::Trading, + corp_act: crate::state::CorpAction::NoCorpAct, + pub_slot: 5, + }, + comp: [Default::default(); 32], + extended: (), + }; + + // Equal Sized? + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::(), + ); + + // Equal Byte Representation? + unsafe { + let old_b = std::slice::from_raw_parts( + &old as *const OldPriceAccount as *const u8, + std::mem::size_of::(), + ); + let new_b = std::slice::from_raw_parts( + &new as *const super::SolanaPriceAccount as *const u8, + std::mem::size_of::(), + ); + assert_eq!(old_b, new_b); + } + } } diff --git a/pyth-sdk-solana/test-contract/Cargo.toml b/pyth-sdk-solana/test-contract/Cargo.toml index 645a72e..44b9ca4 100644 --- a/pyth-sdk-solana/test-contract/Cargo.toml +++ b/pyth-sdk-solana/test-contract/Cargo.toml @@ -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" diff --git a/pyth-sdk-solana/test-contract/rust-toolchain b/pyth-sdk-solana/test-contract/rust-toolchain index d13e4a7..ac0715d 100644 --- a/pyth-sdk-solana/test-contract/rust-toolchain +++ b/pyth-sdk-solana/test-contract/rust-toolchain @@ -1,5 +1,5 @@ # This is only used for tests [toolchain] -channel = "1.68.0" +channel = "1.71.0" profile = "minimal"