From 1c6958a53bb644063ab72c0f2f7f63343e373215 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Fri, 11 Feb 2022 17:15:12 +0000 Subject: [PATCH 01/13] Add slot check in load_price for bpf arch (on-chain programs) - Introduces another Price struct which doesn't have comps (published prices) and unused fields. The reason is that we want to mutate struct (to add the check) and Price Account Data has ~3kb so copying is expensive. This creates a significantly smaller one. This naming allow us to have least impact on existing consumers. The price account data is available as price account data if a consumer needs it. - Also make Ema fields public --- src/instruction.rs | 15 ++++++ src/lib.rs | 114 ++++++++++++++++++++++++++++++++++++------- src/processor.rs | 13 ++++- tests/stale_price.rs | 94 +++++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 18 deletions(-) create mode 100644 tests/stale_price.rs diff --git a/src/instruction.rs b/src/instruction.rs index e6718bd..834cb25 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -34,6 +34,10 @@ pub enum PythClientInstruction { /// /// No accounts required for this instruction Noop, + + PriceNotStale { + price_account_data: Vec + } } pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction { @@ -94,3 +98,14 @@ pub fn noop() -> Instruction { data: PythClientInstruction::Noop.try_to_vec().unwrap(), } } + +// Returns ok if price is not stale +pub fn price_not_stale(price_account_data: Vec) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::PriceNotStale { price_account_data } + .try_to_vec() + .unwrap(), + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c2047bf..55dc6c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,15 +18,19 @@ use bytemuck::{ Pod, PodCastError, Zeroable, }; +#[cfg(target_arch = "bpf")] +use solana_program::{clock::Clock, sysvar::Sysvar}; + solana_program::declare_id!("PythC11111111111111111111111111111111111111"); -pub const MAGIC : u32 = 0xa1b2c3d4; -pub const VERSION_2 : u32 = 2; -pub const VERSION : u32 = VERSION_2; -pub const MAP_TABLE_SIZE : usize = 640; -pub const PROD_ACCT_SIZE : usize = 512; -pub const PROD_HDR_SIZE : usize = 48; -pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; +pub const MAGIC : u32 = 0xa1b2c3d4; +pub const VERSION_2 : u32 = 2; +pub const VERSION : u32 = VERSION_2; +pub const MAP_TABLE_SIZE : usize = 640; +pub const PROD_ACCT_SIZE : usize = 512; +pub const PROD_HDR_SIZE : usize = 48; +pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; +pub const MAX_SEND_LATENCY : u64 = 25; /// The type of Pyth account determines what data it contains #[derive(Copy, Clone)] @@ -180,15 +184,15 @@ pub struct Ema /// The current value of the EMA pub val : i64, /// numerator state for next update - numer : i64, + pub numer : i64, /// denominator state for next update - denom : i64 + pub denom : i64 } /// Price accounts represent a continuously-updating price feed for a product. #[derive(Copy, Clone)] #[repr(C)] -pub struct Price +pub struct PriceAccountData { /// pyth magic number pub magic : u32, @@ -235,14 +239,71 @@ pub struct Price /// price components one per quoter pub comp : [PriceComp;32] } +#[derive(Copy, Clone)] +#[repr(C)] +pub struct Price { + /// account type + pub atype : u32, + /// price account size + pub size : u32, + /// price or calculation type + pub ptype : PriceType, + /// price exponent + pub expo : i32, + /// number of component prices + pub num : u32, + /// number of quoters that make up aggregate + pub num_qt : u32, + /// slot of last valid (not unknown) aggregate price + pub last_slot : u64, + /// valid slot-time of agg. price + pub valid_slot : u64, + /// time-weighted average price + pub twap : Ema, + /// time-weighted average confidence interval + pub twac : Ema, + /// product account key + pub prod : AccKey, + /// next Price account in linked list + pub next : AccKey, + /// valid slot of previous update + pub prev_slot : u64, + /// aggregate price of previous update + pub prev_price : i64, + /// confidence interval of previous update + pub prev_conf : u64, + /// aggregate price info + pub agg : PriceInfo, +} #[cfg(target_endian = "little")] -unsafe impl Zeroable for Price {} +unsafe impl Zeroable for PriceAccountData {} #[cfg(target_endian = "little")] -unsafe impl Pod for Price {} +unsafe impl Pod for PriceAccountData {} impl Price { + fn from_price_account_data(price_account_data: &PriceAccountData) -> Self { + Price { + atype: price_account_data.atype, + size: price_account_data.size, + ptype: price_account_data.ptype, + expo: price_account_data.expo, + num: price_account_data.num, + num_qt: price_account_data.num_qt, + last_slot: price_account_data.last_slot, + valid_slot: price_account_data.valid_slot, + twap: price_account_data.twap, + twac: price_account_data.twac, + prod: price_account_data.prod, + next: price_account_data.next, + prev_slot: price_account_data.prev_slot, + prev_price: price_account_data.prev_price, + prev_conf: price_account_data.prev_conf, + agg: price_account_data.agg + } + } + /** * Get the current price and confidence interval as fixed-point numbers of the form a * 10^e. * Returns a struct containing the current price, confidence interval, and the exponent for both @@ -378,19 +439,38 @@ pub fn load_product(data: &[u8]) -> Result<&Product, PythError> { } /** Get a `Price` account from the raw byte value of a Solana account. */ -pub fn load_price(data: &[u8]) -> Result<&Price, PythError> { - let pyth_price = load::(&data).map_err(|_| PythError::InvalidAccountData)?; +pub fn load_price_account_data(data: &[u8]) -> Result<&PriceAccountData, PythError> { + let price_account_data = load::(&data).map_err(|_| PythError::InvalidAccountData)?; - if pyth_price.magic != MAGIC { + if price_account_data.magic != MAGIC { return Err(PythError::InvalidAccountData); } - if pyth_price.ver != VERSION_2 { + if price_account_data.ver != VERSION_2 { return Err(PythError::BadVersionNumber); } - if pyth_price.atype != AccountType::Price as u32 { + if price_account_data.atype != AccountType::Price as u32 { return Err(PythError::WrongAccountType); } + return Ok(price_account_data); +} + +/** Get a modified `Price` struct from the raw byte value of a Solana Price account. + * If used on-chain it will update the status to unknown if price is not updated for a long time. +*/ +pub fn load_price(data: &[u8]) -> Result { + let price_account_data = load_price_account_data(data)?; + + #[allow(unused_mut)] + let mut pyth_price = Price::from_price_account_data(price_account_data); + + #[cfg(target_arch = "bpf")] + if let PriceStatus::Trading = pyth_price.agg.status { + if Clock::get().unwrap().slot - pyth_price.agg.pub_slot > MAX_SEND_LATENCY { + pyth_price.agg.status = PriceStatus::Unknown; + } + } + return Ok(pyth_price); } diff --git a/src/processor.rs b/src/processor.rs index 97e44e3..2502f8d 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -5,10 +5,11 @@ use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, + program_error::ProgramError }; use crate::{ - instruction::PythClientInstruction, + instruction::PythClientInstruction, load_price, PriceStatus, }; pub fn process_instruction( @@ -41,5 +42,15 @@ pub fn process_instruction( PythClientInstruction::Noop => { Ok(()) } + PythClientInstruction::PriceNotStale { price_account_data } => { + let price = load_price(&price_account_data[..])?; + + match price.agg.status { + PriceStatus::Trading => { + Ok(()) + } + _ => Err(ProgramError::Custom(0)) + } + } } } diff --git a/tests/stale_price.rs b/tests/stale_price.rs new file mode 100644 index 0000000..3c9dfbf --- /dev/null +++ b/tests/stale_price.rs @@ -0,0 +1,94 @@ +#![cfg(feature = "test-bpf")] // This only runs on bpf + +use { + bytemuck::bytes_of, + pyth_client::{id, MAGIC, VERSION_2, instruction, PriceType, PriceAccountData, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus}, + pyth_client::processor::process_instruction, + solana_program::instruction::Instruction, + solana_program_test::*, + solana_sdk::{signature::Signer, transaction::Transaction, transport::TransportError}, +}; + +async fn test_instr(instr: Instruction) -> Result<(), TransportError> { + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "pyth_client", + id(), + processor!(process_instruction), + ) + .start() + .await; + let mut transaction = Transaction::new_with_payer( + &[instr], + Some(&payer.pubkey()), + ); + transaction.sign(&[&payer], recent_blockhash); + banks_client.process_transaction(transaction).await +} + +fn price_all_zero() -> PriceAccountData { + let acc_key = AccKey { + val: [0; 32] + }; + + let ema = Ema { + val: 0, + numer: 0, + denom: 0 + }; + + let price_info = PriceInfo { + conf: 0, + corp_act: CorpAction::NoCorpAct, + price: 0, + pub_slot: 0, + status: PriceStatus::Unknown + }; + + let price_comp = PriceComp { + agg: price_info, + latest: price_info, + publisher: acc_key + }; + + PriceAccountData { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + size: 0, + ptype: PriceType::Price, + expo: 0, + num: 0, + num_qt: 0, + last_slot: 0, + valid_slot: 0, + twap: ema, + twac: ema, + drv1: 0, + drv2: 0, + prod: acc_key, + next: acc_key, + prev_slot: 0, + prev_price: 0, + prev_conf: 0, + drv3: 0, + agg: price_info, + comp: [price_comp; 32] + } +} + + +#[tokio::test] +async fn test_price_not_stale() { + let mut price = price_all_zero(); + price.agg.status = PriceStatus::Trading; + test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap(); +} + + +#[tokio::test] +async fn test_price_stale() { + let mut price = price_all_zero(); + price.agg.status = PriceStatus::Trading; + price.agg.pub_slot = 100; // It will cause an overflow because this is bigger than Solana slot which is impossible in reality + test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap_err(); +} \ No newline at end of file From b0269321507606441291f55d954e0da5b9d8589f Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Fri, 11 Feb 2022 17:27:08 +0000 Subject: [PATCH 02/13] Add newline to end of files --- src/instruction.rs | 2 +- tests/stale_price.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instruction.rs b/src/instruction.rs index 834cb25..dac1fb6 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -108,4 +108,4 @@ pub fn price_not_stale(price_account_data: Vec) -> Instruction { .try_to_vec() .unwrap(), } -} \ No newline at end of file +} diff --git a/tests/stale_price.rs b/tests/stale_price.rs index 3c9dfbf..c45bdd8 100644 --- a/tests/stale_price.rs +++ b/tests/stale_price.rs @@ -91,4 +91,4 @@ async fn test_price_stale() { price.agg.status = PriceStatus::Trading; price.agg.pub_slot = 100; // It will cause an overflow because this is bigger than Solana slot which is impossible in reality test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap_err(); -} \ No newline at end of file +} From b12f339e173bae0d422ce611bd0aa9da2502a490 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Mon, 14 Feb 2022 09:21:13 +0000 Subject: [PATCH 03/13] Move status logic to a getter function --- src/lib.rs | 102 +++++++++---------------------------------- src/processor.rs | 2 +- tests/stale_price.rs | 6 +-- 3 files changed, 24 insertions(+), 86 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 55dc6c0..c3192c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -192,7 +192,7 @@ pub struct Ema /// Price accounts represent a continuously-updating price feed for a product. #[derive(Copy, Clone)] #[repr(C)] -pub struct PriceAccountData +pub struct Price { /// pyth magic number pub magic : u32, @@ -239,69 +239,25 @@ pub struct PriceAccountData /// price components one per quoter pub comp : [PriceComp;32] } -#[derive(Copy, Clone)] -#[repr(C)] -pub struct Price { - /// account type - pub atype : u32, - /// price account size - pub size : u32, - /// price or calculation type - pub ptype : PriceType, - /// price exponent - pub expo : i32, - /// number of component prices - pub num : u32, - /// number of quoters that make up aggregate - pub num_qt : u32, - /// slot of last valid (not unknown) aggregate price - pub last_slot : u64, - /// valid slot-time of agg. price - pub valid_slot : u64, - /// time-weighted average price - pub twap : Ema, - /// time-weighted average confidence interval - pub twac : Ema, - /// product account key - pub prod : AccKey, - /// next Price account in linked list - pub next : AccKey, - /// valid slot of previous update - pub prev_slot : u64, - /// aggregate price of previous update - pub prev_price : i64, - /// confidence interval of previous update - pub prev_conf : u64, - /// aggregate price info - pub agg : PriceInfo, -} #[cfg(target_endian = "little")] -unsafe impl Zeroable for PriceAccountData {} +unsafe impl Zeroable for Price {} #[cfg(target_endian = "little")] -unsafe impl Pod for PriceAccountData {} +unsafe impl Pod for Price {} impl Price { - fn from_price_account_data(price_account_data: &PriceAccountData) -> Self { - Price { - atype: price_account_data.atype, - size: price_account_data.size, - ptype: price_account_data.ptype, - expo: price_account_data.expo, - num: price_account_data.num, - num_qt: price_account_data.num_qt, - last_slot: price_account_data.last_slot, - valid_slot: price_account_data.valid_slot, - twap: price_account_data.twap, - twac: price_account_data.twac, - prod: price_account_data.prod, - next: price_account_data.next, - prev_slot: price_account_data.prev_slot, - prev_price: price_account_data.prev_price, - prev_conf: price_account_data.prev_conf, - agg: price_account_data.agg + /** + * Get the current status of the aggregate price. + * If this lib is used on-chain it will mark price status as unknown if price has not been updated for a while. + */ + pub fn get_current_status(&self) -> PriceStatus { + #[cfg(target_arch = "bpf")] + if matches!(self.agg.status, PriceStatus::Trading) && + Clock::get().unwrap().slot - self.agg.pub_slot > MAX_SEND_LATENCY { + return PriceStatus::Unknown; } + self.agg.status } /** @@ -310,7 +266,7 @@ impl Price { * numbers. Returns `None` if price information is currently unavailable for any reason. */ pub fn get_current_price(&self) -> Option { - if !matches!(self.agg.status, PriceStatus::Trading) { + if !matches!(self.get_current_status(), PriceStatus::Trading) { None } else { Some(PriceConf { @@ -439,40 +395,22 @@ pub fn load_product(data: &[u8]) -> Result<&Product, PythError> { } /** Get a `Price` account from the raw byte value of a Solana account. */ -pub fn load_price_account_data(data: &[u8]) -> Result<&PriceAccountData, PythError> { - let price_account_data = load::(&data).map_err(|_| PythError::InvalidAccountData)?; +pub fn load_price(data: &[u8]) -> Result<&Price, PythError> { + let price = load::(&data).map_err(|_| PythError::InvalidAccountData)?; - if price_account_data.magic != MAGIC { + if price.magic != MAGIC { return Err(PythError::InvalidAccountData); } - if price_account_data.ver != VERSION_2 { + if price.ver != VERSION_2 { return Err(PythError::BadVersionNumber); } - if price_account_data.atype != AccountType::Price as u32 { + if price.atype != AccountType::Price as u32 { return Err(PythError::WrongAccountType); } - return Ok(price_account_data); + return Ok(price); } -/** Get a modified `Price` struct from the raw byte value of a Solana Price account. - * If used on-chain it will update the status to unknown if price is not updated for a long time. -*/ -pub fn load_price(data: &[u8]) -> Result { - let price_account_data = load_price_account_data(data)?; - - #[allow(unused_mut)] - let mut pyth_price = Price::from_price_account_data(price_account_data); - - #[cfg(target_arch = "bpf")] - if let PriceStatus::Trading = pyth_price.agg.status { - if Clock::get().unwrap().slot - pyth_price.agg.pub_slot > MAX_SEND_LATENCY { - pyth_price.agg.status = PriceStatus::Unknown; - } - } - - return Ok(pyth_price); -} pub struct AttributeIter<'a> { attrs: &'a [u8], diff --git a/src/processor.rs b/src/processor.rs index 2502f8d..5330056 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -45,7 +45,7 @@ pub fn process_instruction( PythClientInstruction::PriceNotStale { price_account_data } => { let price = load_price(&price_account_data[..])?; - match price.agg.status { + match price.get_current_status() { PriceStatus::Trading => { Ok(()) } diff --git a/tests/stale_price.rs b/tests/stale_price.rs index c45bdd8..ef83550 100644 --- a/tests/stale_price.rs +++ b/tests/stale_price.rs @@ -2,7 +2,7 @@ use { bytemuck::bytes_of, - pyth_client::{id, MAGIC, VERSION_2, instruction, PriceType, PriceAccountData, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus}, + pyth_client::{id, MAGIC, VERSION_2, instruction, PriceType, Price, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus}, pyth_client::processor::process_instruction, solana_program::instruction::Instruction, solana_program_test::*, @@ -25,7 +25,7 @@ async fn test_instr(instr: Instruction) -> Result<(), TransportError> { banks_client.process_transaction(transaction).await } -fn price_all_zero() -> PriceAccountData { +fn price_all_zero() -> Price { let acc_key = AccKey { val: [0; 32] }; @@ -50,7 +50,7 @@ fn price_all_zero() -> PriceAccountData { publisher: acc_key }; - PriceAccountData { + Price { magic: MAGIC, ver: VERSION_2, atype: AccountType::Price as u32, From aafac8c36ef689aaa5b0146ac21f33423fd371a9 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Mon, 14 Feb 2022 09:23:09 +0000 Subject: [PATCH 04/13] Revert load_price to previous naming --- src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c3192c0..d3037cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -396,19 +396,19 @@ pub fn load_product(data: &[u8]) -> Result<&Product, PythError> { /** Get a `Price` account from the raw byte value of a Solana account. */ pub fn load_price(data: &[u8]) -> Result<&Price, PythError> { - let price = load::(&data).map_err(|_| PythError::InvalidAccountData)?; + let pyth_price = load::(&data).map_err(|_| PythError::InvalidAccountData)?; - if price.magic != MAGIC { + if pyth_price.magic != MAGIC { return Err(PythError::InvalidAccountData); } - if price.ver != VERSION_2 { + if pyth_price.ver != VERSION_2 { return Err(PythError::BadVersionNumber); } - if price.atype != AccountType::Price as u32 { + if pyth_price.atype != AccountType::Price as u32 { return Err(PythError::WrongAccountType); } - return Ok(price); + return Ok(pyth_price); } From ad2c9b94856311ecb30c04ed64bf8dbba3807e87 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Mon, 14 Feb 2022 17:16:27 +0000 Subject: [PATCH 05/13] Rename max_sender_delay to max_slot_difference --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d3037cc..0c4bb81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ pub const MAP_TABLE_SIZE : usize = 640; pub const PROD_ACCT_SIZE : usize = 512; pub const PROD_HDR_SIZE : usize = 48; pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; -pub const MAX_SEND_LATENCY : u64 = 25; +pub const MAX_SLOT_DIFFERENCE : u64 = 25; /// The type of Pyth account determines what data it contains #[derive(Copy, Clone)] From dbbd5d69501fc7c13e43ddd0824db49926602ec4 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 15 Feb 2022 09:20:29 +0000 Subject: [PATCH 06/13] Refactor & Add more comments --- README.md | 6 +++++- src/instruction.rs | 13 +++++++++---- src/lib.rs | 15 ++++++++++----- src/processor.rs | 13 ++++++------- tests/common.rs | 23 +++++++++++++++++++++++ tests/instruction_count.rs | 22 +++------------------- tests/stale_price.rs | 32 ++++++++++---------------------- 7 files changed, 66 insertions(+), 58 deletions(-) create mode 100644 tests/common.rs diff --git a/README.md b/README.md index 74dabd6..c9208c7 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,11 @@ println!("price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo); The price is returned along with a confidence interval that represents the degree of uncertainty in the price. Both values are represented as fixed-point numbers, `a * 10^e`. -The method will return `None` if the price is not currently available. +The method will return `None` if the price is not currently available based on the price status. Get the current price status: + +```rust +let price_status: PriceStatus = price_account.get_current_status(); +``` ### Non-USD prices diff --git a/src/instruction.rs b/src/instruction.rs index dac1fb6..1af5366 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -1,5 +1,7 @@ //! Program instructions for end-to-end testing and instruction counts +use crate::PriceStatus; + use { crate::id, borsh::{BorshDeserialize, BorshSerialize}, @@ -35,8 +37,11 @@ pub enum PythClientInstruction { /// No accounts required for this instruction Noop, - PriceNotStale { - price_account_data: Vec + PriceStatusCheck { + // Adding Borsh SerDe can be expensive (Price Account is big) and requires to add Borsh SerDe, Debug, Default to all structs + // (also in enum Default is experimental which we should provide in another way). I think it's best to left structs intact for this purpose. + price_account_data: Vec, + expected_price_status: PriceStatus } } @@ -100,11 +105,11 @@ pub fn noop() -> Instruction { } // Returns ok if price is not stale -pub fn price_not_stale(price_account_data: Vec) -> Instruction { +pub fn price_status_check(price_account_data: Vec, expected_price_status: PriceStatus) -> Instruction { Instruction { program_id: id(), accounts: vec![], - data: PythClientInstruction::PriceNotStale { price_account_data } + data: PythClientInstruction::PriceStatusCheck { price_account_data, expected_price_status } .try_to_vec() .unwrap(), } diff --git a/src/lib.rs b/src/lib.rs index 0c4bb81..0f21698 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ pub mod processor; pub mod instruction; use std::mem::size_of; +use borsh::{BorshSerialize, BorshDeserialize}; use bytemuck::{ cast_slice, from_bytes, try_cast_slice, Pod, PodCastError, Zeroable, @@ -44,7 +45,7 @@ pub enum AccountType } /// The current status of a price feed. -#[derive(Copy, Clone, PartialEq)] +#[derive(Copy, Clone, PartialEq, BorshSerialize, BorshDeserialize, Debug)] #[repr(C)] pub enum PriceStatus { @@ -150,11 +151,15 @@ unsafe impl Pod for Product {} #[repr(C)] pub struct PriceInfo { - /// the current price + /// the current price. + /// For the aggregate price use price.get_current_price() whenever possible. It has more checks to make sure price is valid. pub price : i64, - /// confidence interval around the price + /// confidence interval around the price. + /// For the aggregate confidence use price.get_current_price() whenever possible. It has more checks to make sure price is valid. pub conf : u64, - /// status of price (Trading is valid) + /// status of price (Trading is valid). + /// For the aggregate status use price.get_current_status() whenever possible. + /// Price data can sometimes go stale and the function handles the status in such cases. pub status : PriceStatus, /// notification of any corporate action pub corp_act : CorpAction, @@ -254,7 +259,7 @@ impl Price { pub fn get_current_status(&self) -> PriceStatus { #[cfg(target_arch = "bpf")] if matches!(self.agg.status, PriceStatus::Trading) && - Clock::get().unwrap().slot - self.agg.pub_slot > MAX_SEND_LATENCY { + Clock::get().unwrap().slot - self.agg.pub_slot > MAX_SLOT_DIFFERENCE { return PriceStatus::Unknown; } self.agg.status diff --git a/src/processor.rs b/src/processor.rs index 5330056..19c733b 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -9,7 +9,7 @@ use solana_program::{ }; use crate::{ - instruction::PythClientInstruction, load_price, PriceStatus, + instruction::PythClientInstruction, load_price, }; pub fn process_instruction( @@ -42,14 +42,13 @@ pub fn process_instruction( PythClientInstruction::Noop => { Ok(()) } - PythClientInstruction::PriceNotStale { price_account_data } => { + PythClientInstruction::PriceStatusCheck { price_account_data, expected_price_status } => { let price = load_price(&price_account_data[..])?; - match price.get_current_status() { - PriceStatus::Trading => { - Ok(()) - } - _ => Err(ProgramError::Custom(0)) + if price.get_current_status() == expected_price_status { + Ok(()) + } else { + Err(ProgramError::Custom(0)) } } } diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..f89a40c --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,23 @@ +use { + pyth_client::id, + pyth_client::processor::process_instruction, + solana_program::instruction::Instruction, + solana_program_test::*, + solana_sdk::{signature::Signer, transaction::Transaction}, +}; + +pub async fn test_instr(instr: Instruction) { + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "pyth_client", + id(), + processor!(process_instruction), + ) + .start() + .await; + let mut transaction = Transaction::new_with_payer( + &[instr], + Some(&payer.pubkey()), + ); + transaction.sign(&[&payer], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap() +} \ No newline at end of file diff --git a/tests/instruction_count.rs b/tests/instruction_count.rs index 7082eac..900ed3e 100644 --- a/tests/instruction_count.rs +++ b/tests/instruction_count.rs @@ -1,26 +1,10 @@ use { - pyth_client::{id, instruction, PriceConf}, - pyth_client::processor::process_instruction, - solana_program::instruction::Instruction, + pyth_client::{instruction, PriceConf}, solana_program_test::*, - solana_sdk::{signature::Signer, transaction::Transaction}, }; -async fn test_instr(instr: Instruction) { - let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( - "pyth_client", - id(), - processor!(process_instruction), - ) - .start() - .await; - let mut transaction = Transaction::new_with_payer( - &[instr], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); -} +mod common; +use common::test_instr; fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { PriceConf { diff --git a/tests/stale_price.rs b/tests/stale_price.rs index ef83550..eebefd4 100644 --- a/tests/stale_price.rs +++ b/tests/stale_price.rs @@ -2,28 +2,13 @@ use { bytemuck::bytes_of, - pyth_client::{id, MAGIC, VERSION_2, instruction, PriceType, Price, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus}, - pyth_client::processor::process_instruction, - solana_program::instruction::Instruction, + pyth_client::{MAGIC, VERSION_2, instruction, PriceType, Price, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus}, solana_program_test::*, - solana_sdk::{signature::Signer, transaction::Transaction, transport::TransportError}, }; -async fn test_instr(instr: Instruction) -> Result<(), TransportError> { - let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( - "pyth_client", - id(), - processor!(process_instruction), - ) - .start() - .await; - let mut transaction = Transaction::new_with_payer( - &[instr], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await -} + +mod common; +use common::test_instr; fn price_all_zero() -> Price { let acc_key = AccKey { @@ -81,7 +66,7 @@ fn price_all_zero() -> Price { async fn test_price_not_stale() { let mut price = price_all_zero(); price.agg.status = PriceStatus::Trading; - test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap(); + test_instr(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Trading)).await; } @@ -89,6 +74,9 @@ async fn test_price_not_stale() { async fn test_price_stale() { let mut price = price_all_zero(); price.agg.status = PriceStatus::Trading; - price.agg.pub_slot = 100; // It will cause an overflow because this is bigger than Solana slot which is impossible in reality - test_instr(instruction::price_not_stale(bytes_of(&price).to_vec())).await.unwrap_err(); + // Value 100 will cause an overflow because this is bigger than Solana slot in the test suite (its ~1-5). + // As the check will be 5u - 100u ~= 1e18 > MAX_SLOT_DIFFERENCE. It can only break when Solana slot in the test suite becomes + // between 100 and 100+MAX_SLOT_DIFFERENCE. + price.agg.pub_slot = 100; + test_instr(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Unknown)).await; } From 508adb5ff1099c036d8342e882be39b32f424a3a Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 15 Feb 2022 09:22:28 +0000 Subject: [PATCH 07/13] Add newline to files --- tests/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common.rs b/tests/common.rs index f89a40c..c7f6e84 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -20,4 +20,4 @@ pub async fn test_instr(instr: Instruction) { ); transaction.sign(&[&payer], recent_blockhash); banks_client.process_transaction(transaction).await.unwrap() -} \ No newline at end of file +} From b1ae955a6deadf8cb7f686fde8e0f16a608aeeba Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 15 Feb 2022 15:33:05 +0000 Subject: [PATCH 08/13] Make comments and namings much better according to Jayant suggestions --- README.md | 4 +++- src/instruction.rs | 6 +++--- tests/common.rs | 3 ++- tests/instruction_count.rs | 26 +++++++++++++------------- tests/stale_price.rs | 6 +++--- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c9208c7..90016f5 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,9 @@ println!("price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo); The price is returned along with a confidence interval that represents the degree of uncertainty in the price. Both values are represented as fixed-point numbers, `a * 10^e`. -The method will return `None` if the price is not currently available based on the price status. Get the current price status: +The method will return `None` if the price is not currently available. + +The status of the price feed determines if the price is available. You can get the current status using: ```rust let price_status: PriceStatus = price_account.get_current_status(); diff --git a/src/instruction.rs b/src/instruction.rs index 1af5366..d998c51 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -38,8 +38,8 @@ pub enum PythClientInstruction { Noop, PriceStatusCheck { - // Adding Borsh SerDe can be expensive (Price Account is big) and requires to add Borsh SerDe, Debug, Default to all structs - // (also in enum Default is experimental which we should provide in another way). I think it's best to left structs intact for this purpose. + // A Price serialized as a vector of bytes. This field is stored as a vector of bytes (instead of a Price) + // so that we do not have to add Borsh serialization to all structs, which is expensive. price_account_data: Vec, expected_price_status: PriceStatus } @@ -104,7 +104,7 @@ pub fn noop() -> Instruction { } } -// Returns ok if price is not stale +// Returns ok if price account status matches given expected price status. pub fn price_status_check(price_account_data: Vec, expected_price_status: PriceStatus) -> Instruction { Instruction { program_id: id(), diff --git a/tests/common.rs b/tests/common.rs index c7f6e84..6d98755 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -6,7 +6,8 @@ use { solana_sdk::{signature::Signer, transaction::Transaction}, }; -pub async fn test_instr(instr: Instruction) { +// Panics if running instruction fails +pub async fn test_instr_exec_ok(instr: Instruction) { let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( "pyth_client", id(), diff --git a/tests/instruction_count.rs b/tests/instruction_count.rs index 900ed3e..5fe40fa 100644 --- a/tests/instruction_count.rs +++ b/tests/instruction_count.rs @@ -4,7 +4,7 @@ use { }; mod common; -use common::test_instr; +use common::test_instr_exec_ok; fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { PriceConf { @@ -16,47 +16,47 @@ fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { #[tokio::test] async fn test_noop() { - test_instr(instruction::noop()).await; + test_instr_exec_ok(instruction::noop()).await; } #[tokio::test] async fn test_scale_to_exponent_down() { - test_instr(instruction::scale_to_exponent(pc(1, u64::MAX, -1000), 1000)).await + test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, -1000), 1000)).await } #[tokio::test] async fn test_scale_to_exponent_up() { - test_instr(instruction::scale_to_exponent(pc(1, u64::MAX, 1000), -1000)).await + test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, 1000), -1000)).await } #[tokio::test] async fn test_scale_to_exponent_best_case() { - test_instr(instruction::scale_to_exponent(pc(1, u64::MAX, 10), 10)).await + test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, 10), 10)).await } #[tokio::test] async fn test_normalize_max_conf() { - test_instr(instruction::normalize(pc(1, u64::MAX, 0))).await + test_instr_exec_ok(instruction::normalize(pc(1, u64::MAX, 0))).await } #[tokio::test] async fn test_normalize_max_price() { - test_instr(instruction::normalize(pc(i64::MAX, 1, 0))).await + test_instr_exec_ok(instruction::normalize(pc(i64::MAX, 1, 0))).await } #[tokio::test] async fn test_normalize_min_price() { - test_instr(instruction::normalize(pc(i64::MIN, 1, 0))).await + test_instr_exec_ok(instruction::normalize(pc(i64::MIN, 1, 0))).await } #[tokio::test] async fn test_normalize_best_case() { - test_instr(instruction::normalize(pc(1, 1, 0))).await + test_instr_exec_ok(instruction::normalize(pc(1, 1, 0))).await } #[tokio::test] async fn test_div_max_price() { - test_instr(instruction::divide( + test_instr_exec_ok(instruction::divide( pc(i64::MAX, 1, 0), pc(1, 1, 0) )).await; @@ -64,7 +64,7 @@ async fn test_div_max_price() { #[tokio::test] async fn test_div_max_price_2() { - test_instr(instruction::divide( + test_instr_exec_ok(instruction::divide( pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0) )).await; @@ -72,7 +72,7 @@ async fn test_div_max_price_2() { #[tokio::test] async fn test_mul_max_price() { - test_instr(instruction::multiply( + test_instr_exec_ok(instruction::multiply( pc(i64::MAX, 1, 2), pc(123, 1, 2), )).await; @@ -80,7 +80,7 @@ async fn test_mul_max_price() { #[tokio::test] async fn test_mul_max_price_2() { - test_instr(instruction::multiply( + test_instr_exec_ok(instruction::multiply( pc(i64::MAX, 1, 2), pc(i64::MAX, 1, 2), )).await; diff --git a/tests/stale_price.rs b/tests/stale_price.rs index eebefd4..9258c26 100644 --- a/tests/stale_price.rs +++ b/tests/stale_price.rs @@ -8,7 +8,7 @@ use { mod common; -use common::test_instr; +use common::test_instr_exec_ok; fn price_all_zero() -> Price { let acc_key = AccKey { @@ -66,7 +66,7 @@ fn price_all_zero() -> Price { async fn test_price_not_stale() { let mut price = price_all_zero(); price.agg.status = PriceStatus::Trading; - test_instr(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Trading)).await; + test_instr_exec_ok(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Trading)).await; } @@ -78,5 +78,5 @@ async fn test_price_stale() { // As the check will be 5u - 100u ~= 1e18 > MAX_SLOT_DIFFERENCE. It can only break when Solana slot in the test suite becomes // between 100 and 100+MAX_SLOT_DIFFERENCE. price.agg.pub_slot = 100; - test_instr(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Unknown)).await; + test_instr_exec_ok(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Unknown)).await; } From b77cb07ef70b0163edbef0549f0778858b00104c Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 15 Feb 2022 15:44:57 +0000 Subject: [PATCH 09/13] Move Price deserialization in instruction api --- src/instruction.rs | 8 +++++--- tests/stale_price.rs | 5 ++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/instruction.rs b/src/instruction.rs index d998c51..32a095f 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -1,6 +1,8 @@ //! Program instructions for end-to-end testing and instruction counts -use crate::PriceStatus; +use bytemuck::bytes_of; + +use crate::{PriceStatus, Price}; use { crate::id, @@ -105,11 +107,11 @@ pub fn noop() -> Instruction { } // Returns ok if price account status matches given expected price status. -pub fn price_status_check(price_account_data: Vec, expected_price_status: PriceStatus) -> Instruction { +pub fn price_status_check(price: &Price, expected_price_status: PriceStatus) -> Instruction { Instruction { program_id: id(), accounts: vec![], - data: PythClientInstruction::PriceStatusCheck { price_account_data, expected_price_status } + data: PythClientInstruction::PriceStatusCheck { price_account_data: bytes_of(price).to_vec(), expected_price_status } .try_to_vec() .unwrap(), } diff --git a/tests/stale_price.rs b/tests/stale_price.rs index 9258c26..8315693 100644 --- a/tests/stale_price.rs +++ b/tests/stale_price.rs @@ -1,7 +1,6 @@ #![cfg(feature = "test-bpf")] // This only runs on bpf use { - bytemuck::bytes_of, pyth_client::{MAGIC, VERSION_2, instruction, PriceType, Price, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus}, solana_program_test::*, }; @@ -66,7 +65,7 @@ fn price_all_zero() -> Price { async fn test_price_not_stale() { let mut price = price_all_zero(); price.agg.status = PriceStatus::Trading; - test_instr_exec_ok(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Trading)).await; + test_instr_exec_ok(instruction::price_status_check(&price, PriceStatus::Trading)).await; } @@ -78,5 +77,5 @@ async fn test_price_stale() { // As the check will be 5u - 100u ~= 1e18 > MAX_SLOT_DIFFERENCE. It can only break when Solana slot in the test suite becomes // between 100 and 100+MAX_SLOT_DIFFERENCE. price.agg.pub_slot = 100; - test_instr_exec_ok(instruction::price_status_check(bytes_of(&price).to_vec(), PriceStatus::Unknown)).await; + test_instr_exec_ok(instruction::price_status_check(&price, PriceStatus::Unknown)).await; } From ffd8a89c20e4bb5c685c36a04f6ba047642bc58f Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Wed, 16 Feb 2022 11:16:25 +0000 Subject: [PATCH 10/13] Rename get_current_status to get_current_price_status --- src/lib.rs | 4 ++-- src/processor.rs | 2 +- tests/stale_price.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0f21698..3707e1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -256,7 +256,7 @@ impl Price { * Get the current status of the aggregate price. * If this lib is used on-chain it will mark price status as unknown if price has not been updated for a while. */ - pub fn get_current_status(&self) -> PriceStatus { + pub fn get_current_price_status(&self) -> PriceStatus { #[cfg(target_arch = "bpf")] if matches!(self.agg.status, PriceStatus::Trading) && Clock::get().unwrap().slot - self.agg.pub_slot > MAX_SLOT_DIFFERENCE { @@ -271,7 +271,7 @@ impl Price { * numbers. Returns `None` if price information is currently unavailable for any reason. */ pub fn get_current_price(&self) -> Option { - if !matches!(self.get_current_status(), PriceStatus::Trading) { + if !matches!(self.get_current_price_status(), PriceStatus::Trading) { None } else { Some(PriceConf { diff --git a/src/processor.rs b/src/processor.rs index 19c733b..fd0f324 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -45,7 +45,7 @@ pub fn process_instruction( PythClientInstruction::PriceStatusCheck { price_account_data, expected_price_status } => { let price = load_price(&price_account_data[..])?; - if price.get_current_status() == expected_price_status { + if price.get_current_price_status() == expected_price_status { Ok(()) } else { Err(ProgramError::Custom(0)) diff --git a/tests/stale_price.rs b/tests/stale_price.rs index 8315693..ee3ac63 100644 --- a/tests/stale_price.rs +++ b/tests/stale_price.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "test-bpf")] // This only runs on bpf +#![cfg(feature = "test-bpf")] // Only runs on bpf, where solana programs run use { pyth_client::{MAGIC, VERSION_2, instruction, PriceType, Price, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus}, From a49ad3833e02e1b9865ea6ecd60c3984286c7e20 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Wed, 16 Feb 2022 11:24:30 +0000 Subject: [PATCH 11/13] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 90016f5..294440b 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ The method will return `None` if the price is not currently available. The status of the price feed determines if the price is available. You can get the current status using: ```rust -let price_status: PriceStatus = price_account.get_current_status(); +let price_status: PriceStatus = price_account.get_current_price_status(); ``` ### Non-USD prices From 262144571eb71cac22756071de28c73b2fa7dc7b Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Wed, 16 Feb 2022 11:25:38 +0000 Subject: [PATCH 12/13] Fix formatting --- src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3707e1b..057f3ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,13 +24,13 @@ use solana_program::{clock::Clock, sysvar::Sysvar}; solana_program::declare_id!("PythC11111111111111111111111111111111111111"); -pub const MAGIC : u32 = 0xa1b2c3d4; -pub const VERSION_2 : u32 = 2; -pub const VERSION : u32 = VERSION_2; -pub const MAP_TABLE_SIZE : usize = 640; -pub const PROD_ACCT_SIZE : usize = 512; -pub const PROD_HDR_SIZE : usize = 48; -pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; +pub const MAGIC : u32 = 0xa1b2c3d4; +pub const VERSION_2 : u32 = 2; +pub const VERSION : u32 = VERSION_2; +pub const MAP_TABLE_SIZE : usize = 640; +pub const PROD_ACCT_SIZE : usize = 512; +pub const PROD_HDR_SIZE : usize = 48; +pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; pub const MAX_SLOT_DIFFERENCE : u64 = 25; /// The type of Pyth account determines what data it contains From 7b5dc226b0e8d91f82136fd2a23f043a4efa2026 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Thu, 17 Feb 2022 09:28:06 +0000 Subject: [PATCH 13/13] Update get_accounts example --- examples/get_accounts.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/get_accounts.rs b/examples/get_accounts.rs index e98d543..679db45 100644 --- a/examples/get_accounts.rs +++ b/examples/get_accounts.rs @@ -66,7 +66,9 @@ fn main() { // print key and reference data for this Product println!( "product_account .. {:?}", prod_pkey ); for (key, val) in prod_acct.iter() { - println!( " {:.<16} {}", key, val ); + if key.len() > 0 { + println!( " {:.<16} {}", key, val ); + } } // print all Prices that correspond to this Product @@ -92,7 +94,7 @@ fn main() { println!( " price_type ... {}", get_price_type(&pa.ptype)); println!( " exponent ..... {}", pa.expo ); - println!( " status ....... {}", get_status(&pa.agg.status)); + println!( " status ....... {}", get_status(&pa.get_current_price_status())); println!( " corp_act ..... {}", get_corp_act(&pa.agg.corp_act)); println!( " num_qt ....... {}", pa.num_qt );