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

Commit 210087a

Browse files
authored
Feat/add price slot check (In on-chain programs) (#16)
Adds `get_current_price_status` to Price struct which returns the current price status. If run on-chain with the current slot it checks whether price information has got stale or not. `get_current_price` uses this function to make the interface consistent. Additional changes: - Moves `test_instr` for testing to `common.rs` (as it's used in the test for this feature) and renamed it to `test_instr_exex_ok` to be more clear. - Adds Borsh SerDe traits to PriceStatus as it is used for testing instructions. - Makes Ema fields public. It was required for creating testing Price object.
1 parent 961c49a commit 210087a

File tree

8 files changed

+200
-48
lines changed

8 files changed

+200
-48
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ The price is returned along with a confidence interval that represents the degre
7171
Both values are represented as fixed-point numbers, `a * 10^e`.
7272
The method will return `None` if the price is not currently available.
7373

74+
The status of the price feed determines if the price is available. You can get the current status using:
75+
76+
```rust
77+
let price_status: PriceStatus = price_account.get_current_price_status();
78+
```
79+
7480
### Non-USD prices
7581

7682
Most assets in Pyth are priced in USD.

examples/get_accounts.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ fn main() {
6666
// print key and reference data for this Product
6767
println!( "product_account .. {:?}", prod_pkey );
6868
for (key, val) in prod_acct.iter() {
69-
println!( " {:.<16} {}", key, val );
69+
if key.len() > 0 {
70+
println!( " {:.<16} {}", key, val );
71+
}
7072
}
7173

7274
// print all Prices that correspond to this Product
@@ -92,7 +94,7 @@ fn main() {
9294

9395
println!( " price_type ... {}", get_price_type(&pa.ptype));
9496
println!( " exponent ..... {}", pa.expo );
95-
println!( " status ....... {}", get_status(&pa.agg.status));
97+
println!( " status ....... {}", get_status(&pa.get_current_price_status()));
9698
println!( " corp_act ..... {}", get_corp_act(&pa.agg.corp_act));
9799

98100
println!( " num_qt ....... {}", pa.num_qt );

src/instruction.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
//! Program instructions for end-to-end testing and instruction counts
22
3+
use bytemuck::bytes_of;
4+
5+
use crate::{PriceStatus, Price};
6+
37
use {
48
crate::id,
59
borsh::{BorshDeserialize, BorshSerialize},
@@ -34,6 +38,13 @@ pub enum PythClientInstruction {
3438
///
3539
/// No accounts required for this instruction
3640
Noop,
41+
42+
PriceStatusCheck {
43+
// A Price serialized as a vector of bytes. This field is stored as a vector of bytes (instead of a Price)
44+
// so that we do not have to add Borsh serialization to all structs, which is expensive.
45+
price_account_data: Vec<u8>,
46+
expected_price_status: PriceStatus
47+
}
3748
}
3849

3950
pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction {
@@ -94,3 +105,14 @@ pub fn noop() -> Instruction {
94105
data: PythClientInstruction::Noop.try_to_vec().unwrap(),
95106
}
96107
}
108+
109+
// Returns ok if price account status matches given expected price status.
110+
pub fn price_status_check(price: &Price, expected_price_status: PriceStatus) -> Instruction {
111+
Instruction {
112+
program_id: id(),
113+
accounts: vec![],
114+
data: PythClientInstruction::PriceStatusCheck { price_account_data: bytes_of(price).to_vec(), expected_price_status }
115+
.try_to_vec()
116+
.unwrap(),
117+
}
118+
}

src/lib.rs

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,25 @@ pub mod processor;
1313
pub mod instruction;
1414

1515
use std::mem::size_of;
16+
use borsh::{BorshSerialize, BorshDeserialize};
1617
use bytemuck::{
1718
cast_slice, from_bytes, try_cast_slice,
1819
Pod, PodCastError, Zeroable,
1920
};
2021

22+
#[cfg(target_arch = "bpf")]
23+
use solana_program::{clock::Clock, sysvar::Sysvar};
24+
2125
solana_program::declare_id!("PythC11111111111111111111111111111111111111");
2226

23-
pub const MAGIC : u32 = 0xa1b2c3d4;
24-
pub const VERSION_2 : u32 = 2;
25-
pub const VERSION : u32 = VERSION_2;
26-
pub const MAP_TABLE_SIZE : usize = 640;
27-
pub const PROD_ACCT_SIZE : usize = 512;
28-
pub const PROD_HDR_SIZE : usize = 48;
29-
pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE;
27+
pub const MAGIC : u32 = 0xa1b2c3d4;
28+
pub const VERSION_2 : u32 = 2;
29+
pub const VERSION : u32 = VERSION_2;
30+
pub const MAP_TABLE_SIZE : usize = 640;
31+
pub const PROD_ACCT_SIZE : usize = 512;
32+
pub const PROD_HDR_SIZE : usize = 48;
33+
pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE;
34+
pub const MAX_SLOT_DIFFERENCE : u64 = 25;
3035

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

4247
/// The current status of a price feed.
43-
#[derive(Copy, Clone, PartialEq)]
48+
#[derive(Copy, Clone, PartialEq, BorshSerialize, BorshDeserialize, Debug)]
4449
#[repr(C)]
4550
pub enum PriceStatus
4651
{
@@ -146,11 +151,15 @@ unsafe impl Pod for Product {}
146151
#[repr(C)]
147152
pub struct PriceInfo
148153
{
149-
/// the current price
154+
/// the current price.
155+
/// For the aggregate price use price.get_current_price() whenever possible. It has more checks to make sure price is valid.
150156
pub price : i64,
151-
/// confidence interval around the price
157+
/// confidence interval around the price.
158+
/// For the aggregate confidence use price.get_current_price() whenever possible. It has more checks to make sure price is valid.
152159
pub conf : u64,
153-
/// status of price (Trading is valid)
160+
/// status of price (Trading is valid).
161+
/// For the aggregate status use price.get_current_status() whenever possible.
162+
/// Price data can sometimes go stale and the function handles the status in such cases.
154163
pub status : PriceStatus,
155164
/// notification of any corporate action
156165
pub corp_act : CorpAction,
@@ -180,9 +189,9 @@ pub struct Ema
180189
/// The current value of the EMA
181190
pub val : i64,
182191
/// numerator state for next update
183-
numer : i64,
192+
pub numer : i64,
184193
/// denominator state for next update
185-
denom : i64
194+
pub denom : i64
186195
}
187196

188197
/// Price accounts represent a continuously-updating price feed for a product.
@@ -243,13 +252,26 @@ unsafe impl Zeroable for Price {}
243252
unsafe impl Pod for Price {}
244253

245254
impl Price {
255+
/**
256+
* Get the current status of the aggregate price.
257+
* If this lib is used on-chain it will mark price status as unknown if price has not been updated for a while.
258+
*/
259+
pub fn get_current_price_status(&self) -> PriceStatus {
260+
#[cfg(target_arch = "bpf")]
261+
if matches!(self.agg.status, PriceStatus::Trading) &&
262+
Clock::get().unwrap().slot - self.agg.pub_slot > MAX_SLOT_DIFFERENCE {
263+
return PriceStatus::Unknown;
264+
}
265+
self.agg.status
266+
}
267+
246268
/**
247269
* Get the current price and confidence interval as fixed-point numbers of the form a * 10^e.
248270
* Returns a struct containing the current price, confidence interval, and the exponent for both
249271
* numbers. Returns `None` if price information is currently unavailable for any reason.
250272
*/
251273
pub fn get_current_price(&self) -> Option<PriceConf> {
252-
if !matches!(self.agg.status, PriceStatus::Trading) {
274+
if !matches!(self.get_current_price_status(), PriceStatus::Trading) {
253275
None
254276
} else {
255277
Some(PriceConf {
@@ -394,6 +416,7 @@ pub fn load_price(data: &[u8]) -> Result<&Price, PythError> {
394416
return Ok(pyth_price);
395417
}
396418

419+
397420
pub struct AttributeIter<'a> {
398421
attrs: &'a [u8],
399422
}

src/processor.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ use solana_program::{
55
account_info::AccountInfo,
66
entrypoint::ProgramResult,
77
pubkey::Pubkey,
8+
program_error::ProgramError
89
};
910

1011
use crate::{
11-
instruction::PythClientInstruction,
12+
instruction::PythClientInstruction, load_price,
1213
};
1314

1415
pub fn process_instruction(
@@ -41,5 +42,14 @@ pub fn process_instruction(
4142
PythClientInstruction::Noop => {
4243
Ok(())
4344
}
45+
PythClientInstruction::PriceStatusCheck { price_account_data, expected_price_status } => {
46+
let price = load_price(&price_account_data[..])?;
47+
48+
if price.get_current_price_status() == expected_price_status {
49+
Ok(())
50+
} else {
51+
Err(ProgramError::Custom(0))
52+
}
53+
}
4454
}
4555
}

tests/common.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use {
2+
pyth_client::id,
3+
pyth_client::processor::process_instruction,
4+
solana_program::instruction::Instruction,
5+
solana_program_test::*,
6+
solana_sdk::{signature::Signer, transaction::Transaction},
7+
};
8+
9+
// Panics if running instruction fails
10+
pub async fn test_instr_exec_ok(instr: Instruction) {
11+
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
12+
"pyth_client",
13+
id(),
14+
processor!(process_instruction),
15+
)
16+
.start()
17+
.await;
18+
let mut transaction = Transaction::new_with_payer(
19+
&[instr],
20+
Some(&payer.pubkey()),
21+
);
22+
transaction.sign(&[&payer], recent_blockhash);
23+
banks_client.process_transaction(transaction).await.unwrap()
24+
}

tests/instruction_count.rs

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,10 @@
11
use {
2-
pyth_client::{id, instruction, PriceConf},
3-
pyth_client::processor::process_instruction,
4-
solana_program::instruction::Instruction,
2+
pyth_client::{instruction, PriceConf},
53
solana_program_test::*,
6-
solana_sdk::{signature::Signer, transaction::Transaction},
74
};
85

9-
async fn test_instr(instr: Instruction) {
10-
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
11-
"pyth_client",
12-
id(),
13-
processor!(process_instruction),
14-
)
15-
.start()
16-
.await;
17-
let mut transaction = Transaction::new_with_payer(
18-
&[instr],
19-
Some(&payer.pubkey()),
20-
);
21-
transaction.sign(&[&payer], recent_blockhash);
22-
banks_client.process_transaction(transaction).await.unwrap();
23-
}
6+
mod common;
7+
use common::test_instr_exec_ok;
248

259
fn pc(price: i64, conf: u64, expo: i32) -> PriceConf {
2610
PriceConf {
@@ -32,71 +16,71 @@ fn pc(price: i64, conf: u64, expo: i32) -> PriceConf {
3216

3317
#[tokio::test]
3418
async fn test_noop() {
35-
test_instr(instruction::noop()).await;
19+
test_instr_exec_ok(instruction::noop()).await;
3620
}
3721

3822
#[tokio::test]
3923
async fn test_scale_to_exponent_down() {
40-
test_instr(instruction::scale_to_exponent(pc(1, u64::MAX, -1000), 1000)).await
24+
test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, -1000), 1000)).await
4125
}
4226

4327
#[tokio::test]
4428
async fn test_scale_to_exponent_up() {
45-
test_instr(instruction::scale_to_exponent(pc(1, u64::MAX, 1000), -1000)).await
29+
test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, 1000), -1000)).await
4630
}
4731

4832
#[tokio::test]
4933
async fn test_scale_to_exponent_best_case() {
50-
test_instr(instruction::scale_to_exponent(pc(1, u64::MAX, 10), 10)).await
34+
test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, 10), 10)).await
5135
}
5236

5337
#[tokio::test]
5438
async fn test_normalize_max_conf() {
55-
test_instr(instruction::normalize(pc(1, u64::MAX, 0))).await
39+
test_instr_exec_ok(instruction::normalize(pc(1, u64::MAX, 0))).await
5640
}
5741

5842
#[tokio::test]
5943
async fn test_normalize_max_price() {
60-
test_instr(instruction::normalize(pc(i64::MAX, 1, 0))).await
44+
test_instr_exec_ok(instruction::normalize(pc(i64::MAX, 1, 0))).await
6145
}
6246

6347
#[tokio::test]
6448
async fn test_normalize_min_price() {
65-
test_instr(instruction::normalize(pc(i64::MIN, 1, 0))).await
49+
test_instr_exec_ok(instruction::normalize(pc(i64::MIN, 1, 0))).await
6650
}
6751

6852
#[tokio::test]
6953
async fn test_normalize_best_case() {
70-
test_instr(instruction::normalize(pc(1, 1, 0))).await
54+
test_instr_exec_ok(instruction::normalize(pc(1, 1, 0))).await
7155
}
7256

7357
#[tokio::test]
7458
async fn test_div_max_price() {
75-
test_instr(instruction::divide(
59+
test_instr_exec_ok(instruction::divide(
7660
pc(i64::MAX, 1, 0),
7761
pc(1, 1, 0)
7862
)).await;
7963
}
8064

8165
#[tokio::test]
8266
async fn test_div_max_price_2() {
83-
test_instr(instruction::divide(
67+
test_instr_exec_ok(instruction::divide(
8468
pc(i64::MAX, 1, 0),
8569
pc(i64::MAX, 1, 0)
8670
)).await;
8771
}
8872

8973
#[tokio::test]
9074
async fn test_mul_max_price() {
91-
test_instr(instruction::multiply(
75+
test_instr_exec_ok(instruction::multiply(
9276
pc(i64::MAX, 1, 2),
9377
pc(123, 1, 2),
9478
)).await;
9579
}
9680

9781
#[tokio::test]
9882
async fn test_mul_max_price_2() {
99-
test_instr(instruction::multiply(
83+
test_instr_exec_ok(instruction::multiply(
10084
pc(i64::MAX, 1, 2),
10185
pc(i64::MAX, 1, 2),
10286
)).await;

0 commit comments

Comments
 (0)