Skip to content
This repository was archived by the owner on Jun 30, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
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)]
#[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.
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