Skip to content
This repository has been archived by the owner on Jun 30, 2022. It is now read-only.

Feat/add price slot check (In on-chain programs) #16

Merged
merged 13 commits into from
Feb 17, 2022
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ The price is returned along with a confidence interval that represents the degre
Both values are represented as fixed-point numbers, `a * 10^e`.
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_price_status();
```

### Non-USD prices

Most assets in Pyth are priced in USD.
Expand Down
6 changes: 4 additions & 2 deletions examples/get_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 );
Expand Down
22 changes: 22 additions & 0 deletions src/instruction.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//! Program instructions for end-to-end testing and instruction counts

use bytemuck::bytes_of;

use crate::{PriceStatus, Price};

use {
crate::id,
borsh::{BorshDeserialize, BorshSerialize},
Expand Down Expand Up @@ -34,6 +38,13 @@ pub enum PythClientInstruction {
///
/// No accounts required for this instruction
Noop,

PriceStatusCheck {
// 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<u8>,
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
expected_price_status: PriceStatus
}
}

pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction {
Expand Down Expand Up @@ -94,3 +105,14 @@ pub fn noop() -> Instruction {
data: PythClientInstruction::Noop.try_to_vec().unwrap(),
}
}

// Returns ok if price account status matches given expected price status.
pub fn price_status_check(price: &Price, expected_price_status: PriceStatus) -> Instruction {
Instruction {
program_id: id(),
accounts: vec![],
data: PythClientInstruction::PriceStatusCheck { price_account_data: bytes_of(price).to_vec(), expected_price_status }
.try_to_vec()
.unwrap(),
}
}
51 changes: 37 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,25 @@ 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,
};

#[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_SLOT_DIFFERENCE : u64 = 25;

/// The type of Pyth account determines what data it contains
#[derive(Copy, Clone)]
Expand All @@ -40,7 +45,7 @@ pub enum AccountType
}

/// The current status of a price feed.
#[derive(Copy, Clone, PartialEq)]
#[derive(Copy, Clone, PartialEq, BorshSerialize, BorshDeserialize, Debug)]
Reisen marked this conversation as resolved.
Show resolved Hide resolved
#[repr(C)]
pub enum PriceStatus
{
Expand Down Expand Up @@ -146,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.
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
pub status : PriceStatus,
/// notification of any corporate action
pub corp_act : CorpAction,
Expand Down Expand Up @@ -180,9 +189,9 @@ 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.
Expand Down Expand Up @@ -243,13 +252,26 @@ unsafe impl Zeroable for Price {}
unsafe impl Pod for Price {}

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_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 {
return PriceStatus::Unknown;
}
self.agg.status
}

/**
* 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
* numbers. Returns `None` if price information is currently unavailable for any reason.
*/
pub fn get_current_price(&self) -> Option<PriceConf> {
if !matches!(self.agg.status, PriceStatus::Trading) {
if !matches!(self.get_current_price_status(), PriceStatus::Trading) {
None
} else {
Some(PriceConf {
Expand Down Expand Up @@ -394,6 +416,7 @@ pub fn load_price(data: &[u8]) -> Result<&Price, PythError> {
return Ok(pyth_price);
}


pub struct AttributeIter<'a> {
attrs: &'a [u8],
}
Expand Down
12 changes: 11 additions & 1 deletion src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

pub fn process_instruction(
Expand Down Expand Up @@ -41,5 +42,14 @@ pub fn process_instruction(
PythClientInstruction::Noop => {
Ok(())
}
PythClientInstruction::PriceStatusCheck { price_account_data, expected_price_status } => {
let price = load_price(&price_account_data[..])?;

if price.get_current_price_status() == expected_price_status {
Ok(())
} else {
Err(ProgramError::Custom(0))
}
}
}
}
24 changes: 24 additions & 0 deletions tests/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use {
pyth_client::id,
pyth_client::processor::process_instruction,
solana_program::instruction::Instruction,
solana_program_test::*,
solana_sdk::{signature::Signer, transaction::Transaction},
};

// 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(),
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()
}
46 changes: 15 additions & 31 deletions tests/instruction_count.rs
Original file line number Diff line number Diff line change
@@ -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_exec_ok;

fn pc(price: i64, conf: u64, expo: i32) -> PriceConf {
PriceConf {
Expand All @@ -32,71 +16,71 @@ 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;
}

#[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;
}

#[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;
}

#[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;
Expand Down
Loading