Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: state.rs: Support Pythnet flavor prices (incl. 64 pubs) #112

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
2 changes: 1 addition & 1 deletion examples/sol-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
4 changes: 2 additions & 2 deletions pyth-sdk-solana/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -14,7 +14,7 @@ readme = "README.md"
solana-program = ">= 1.9"
borsh = "0.10.3"
borsh-derive = "0.10.3"
bytemuck = "1.7.2"
bytemuck = "1.11.0"
num-derive = "0.3"
num-traits = "0.2"
thiserror = "1.0"
Expand Down
224 changes: 160 additions & 64 deletions pyth-sdk-solana/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,87 @@ pub struct Rational {
pub denom: i64,
}

/// Macro for PriceAccountSolana/PriceAccountPythnet methods,
/// eliminates repetition for the shared methods. Self type name is the
/// only parameter.
macro_rules! price_account_impl {
($name:ty) => {
impl $name {
pub fn get_publish_time(&self) -> UnixTimestamp {
match self.agg.status {
PriceStatus::Trading => self.timestamp,
_ => self.prev_timestamp,
}
}

/// Get the last valid price as long as it was updated within `slot_threshold` slots of
/// the current slot.
pub fn get_price_no_older_than(
&self,
clock: &Clock,
slot_threshold: u64,
) -> Option<Price> {
if self.agg.status == PriceStatus::Trading
&& self.agg.pub_slot >= clock.slot - slot_threshold
{
return Some(Price {
conf: self.agg.conf,
expo: self.expo,
price: self.agg.price,
publish_time: self.timestamp,
});
}

if self.prev_slot >= clock.slot - slot_threshold {
return Some(Price {
conf: self.prev_conf,
expo: self.expo,
price: self.prev_price,
publish_time: self.prev_timestamp,
});
}

None
}

pub fn to_price_feed(&self, price_key: &Pubkey) -> PriceFeed {
let status = self.agg.status;

let price = match status {
PriceStatus::Trading => Price {
conf: self.agg.conf,
expo: self.expo,
price: self.agg.price,
publish_time: self.get_publish_time(),
},
_ => Price {
conf: self.prev_conf,
expo: self.expo,
price: self.prev_price,
publish_time: self.get_publish_time(),
},
};

let ema_price = Price {
conf: self.ema_conf.val as u64,
expo: self.expo,
price: self.ema_price.val,
publish_time: self.get_publish_time(),
};

PriceFeed::new(PriceIdentifier::new(price_key.to_bytes()), price, ema_price)
}
}
};
}

/// Backwards-compatible typedef
pub type PriceAccount = PriceAccountSolana;

/// Price accounts represent a continuously-updating price feed for a product.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct PriceAccount {
pub struct PriceAccountSolana {
/// pyth magic number
pub magic: u32,
/// program version
Expand Down Expand Up @@ -339,77 +416,96 @@ pub struct PriceAccount {
pub comp: [PriceComp; 32],
}

price_account_impl!(PriceAccountSolana);

#[cfg(target_endian = "little")]
unsafe impl Zeroable for PriceAccount {
unsafe impl Zeroable for PriceAccountSolana {
}

#[cfg(target_endian = "little")]
unsafe impl Pod for PriceAccount {
unsafe impl Pod for PriceAccountSolana {
}

impl PriceAccount {
pub fn get_publish_time(&self) -> UnixTimestamp {
match self.agg.status {
PriceStatus::Trading => self.timestamp,
_ => self.prev_timestamp,
}
}

/// Get the last valid price as long as it was updated within `slot_threshold` slots of the
/// current slot.
pub fn get_price_no_older_than(&self, clock: &Clock, slot_threshold: u64) -> Option<Price> {
if self.agg.status == PriceStatus::Trading
&& self.agg.pub_slot >= clock.slot - slot_threshold
{
return Some(Price {
conf: self.agg.conf,
expo: self.expo,
price: self.agg.price,
publish_time: self.timestamp,
});
}

if self.prev_slot >= clock.slot - slot_threshold {
return Some(Price {
conf: self.prev_conf,
expo: self.expo,
price: self.prev_price,
publish_time: self.prev_timestamp,
});
}

None
}

pub fn to_price_feed(&self, price_key: &Pubkey) -> PriceFeed {
let status = self.agg.status;

let price = match status {
PriceStatus::Trading => Price {
conf: self.agg.conf,
expo: self.expo,
price: self.agg.price,
publish_time: self.get_publish_time(),
},
_ => Price {
conf: self.prev_conf,
expo: self.expo,
price: self.prev_price,
publish_time: self.get_publish_time(),
},
};

let ema_price = Price {
conf: self.ema_conf.val as u64,
expo: self.expo,
price: self.ema_price.val,
publish_time: self.get_publish_time(),
};
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct PriceAccountPythnet {
/// pyth magic number
pub magic: u32,
/// program version
pub ver: u32,
/// 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,
/// exponentially moving average price
pub ema_price: Rational,
/// exponentially moving average confidence interval
pub ema_conf: Rational,
/// unix timestamp of aggregate price
pub timestamp: i64,
/// min publishers for valid price
pub min_pub: u8,
/// space for future derived values
pub drv2: u8,
/// space for future derived values
pub drv3: u16,
/// space for future derived values
pub drv4: u32,
/// product account key
pub prod: Pubkey,
/// next Price account in linked list
pub next: Pubkey,
/// valid slot of previous update
pub prev_slot: u64,
/// aggregate price of previous update with TRADING status
pub prev_price: i64,
/// confidence interval of previous update with TRADING status
pub prev_conf: u64,
/// unix timestamp of previous aggregate with TRADING status
pub prev_timestamp: i64,
/// aggregate price info
pub agg: PriceInfo,
/// price components one per quoter.
pub comp: [PriceComp; 32],
/// Rationale (2023-12-12): Rust is currently unable to derive Default for [PriceComp; 64]
pub comp2: [PriceComp; 32],
/// Cumulative sums of aggregative price and confidence used to compute arithmetic moving
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is wrong, there is room for 128 publishers afaik

/// averages
pub price_cumulative: PriceCumulative,
}

PriceFeed::new(PriceIdentifier::new(price_key.to_bytes()), price, ema_price)
}
/// NOTE(2023-12-12): Copied from pyth-client
#[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,
}

price_account_impl!(PriceAccountPythnet);

fn load<T: Pod>(data: &[u8]) -> Result<&T, PodCastError> {
let size = size_of::<T>();
if data.len() >= size {
Expand Down Expand Up @@ -456,8 +552,8 @@ 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> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok that this functions loads the account is PriceAccountSolana ? I think it's used in pyth-agent

let pyth_price = load::<PriceAccount>(data).map_err(|_| PythError::InvalidAccountData)?;
pub fn load_price_account(data: &[u8]) -> Result<&PriceAccountSolana, PythError> {
let pyth_price = load::<PriceAccountSolana>(data).map_err(|_| PythError::InvalidAccountData)?;

if pyth_price.magic != MAGIC {
return Err(PythError::InvalidAccountData);
Expand Down
2 changes: 1 addition & 1 deletion pyth-sdk-solana/test-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading