use borsh::{
BorshDeserialize,
BorshSerialize,
};
use bytemuck::{
cast_slice,
from_bytes,
try_cast_slice,
Pod,
PodCastError,
Zeroable,
};
use pyth_sdk::{
PriceIdentifier,
UnixTimestamp,
};
use solana_program::clock::Clock;
use solana_program::pubkey::Pubkey;
use std::cmp::min;
use std::mem::size_of;
pub use pyth_sdk::{
Price,
PriceFeed,
};
use crate::PythError;
pub const MAGIC: u32 = 0xa1b2c3d4;
pub const VERSION_2: u32 = 2;
pub const VERSION: u32 = VERSION_2;
pub const MAP_TABLE_SIZE: usize = 5000;
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;
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Eq,
BorshSerialize,
BorshDeserialize,
serde::Serialize,
serde::Deserialize,
Default,
)]
#[repr(u8)]
pub enum AccountType {
#[default]
Unknown,
Mapping,
Product,
Price,
}
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Eq,
BorshSerialize,
BorshDeserialize,
serde::Serialize,
serde::Deserialize,
Default,
)]
#[repr(u8)]
pub enum CorpAction {
#[default]
NoCorpAct,
}
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Eq,
BorshSerialize,
BorshDeserialize,
serde::Serialize,
serde::Deserialize,
Default,
)]
#[repr(u8)]
pub enum PriceType {
#[default]
Unknown,
Price,
}
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Eq,
BorshSerialize,
BorshDeserialize,
serde::Serialize,
serde::Deserialize,
Default,
)]
#[repr(u8)]
pub enum PriceStatus {
#[default]
Unknown,
Trading,
Halted,
Auction,
Ignored,
}
impl std::fmt::Display for PriceStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Unknown => "unknown",
Self::Trading => "trading",
Self::Halted => "halted",
Self::Auction => "auction",
Self::Ignored => "ignored",
}
)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct MappingAccount {
pub magic: u32,
pub ver: u32,
pub atype: u32,
pub size: u32,
pub num: u32,
pub unused: u32,
pub next: Pubkey,
pub products: [Pubkey; MAP_TABLE_SIZE],
}
#[cfg(target_endian = "little")]
unsafe impl Zeroable for MappingAccount {
}
#[cfg(target_endian = "little")]
unsafe impl Pod for MappingAccount {
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct ProductAccount {
pub magic: u32,
pub ver: u32,
pub atype: u32,
pub size: u32,
pub px_acc: Pubkey,
pub attr: [u8; PROD_ATTR_SIZE],
}
impl ProductAccount {
pub fn iter(&self) -> AttributeIter {
AttributeIter {
attrs: &self.attr[..min(
(self.size as usize).saturating_sub(PROD_HDR_SIZE),
PROD_ATTR_SIZE,
)],
}
}
}
#[cfg(target_endian = "little")]
unsafe impl Zeroable for ProductAccount {
}
#[cfg(target_endian = "little")]
unsafe impl Pod for ProductAccount {
}
#[derive(
Copy,
Clone,
Debug,
Default,
PartialEq,
Eq,
BorshSerialize,
BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[repr(C)]
pub struct PriceInfo {
pub price: i64,
pub conf: u64,
pub status: PriceStatus,
pub corp_act: CorpAction,
pub pub_slot: u64,
}
#[derive(
Copy,
Clone,
Debug,
Default,
PartialEq,
Eq,
BorshSerialize,
BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[repr(C)]
pub struct PriceComp {
pub publisher: Pubkey,
pub agg: PriceInfo,
pub latest: PriceInfo,
}
#[deprecated = "Type is renamed to Rational, please use the new name."]
pub type Ema = Rational;
#[derive(
Copy,
Clone,
Debug,
Default,
PartialEq,
Eq,
BorshSerialize,
BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[repr(C)]
pub struct Rational {
pub val: i64,
pub numer: i64,
pub denom: i64,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct GenericPriceAccount<const N: usize, T>
where
T: Default,
T: Copy,
{
pub magic: u32,
pub ver: u32,
pub atype: u32,
pub size: u32,
pub ptype: PriceType,
pub expo: i32,
pub num: u32,
pub num_qt: u32,
pub last_slot: u64,
pub valid_slot: u64,
pub ema_price: Rational,
pub ema_conf: Rational,
pub timestamp: i64,
pub min_pub: u8,
pub drv2: u8,
pub drv3: u16,
pub drv4: u32,
pub prod: Pubkey,
pub next: Pubkey,
pub prev_slot: u64,
pub prev_price: i64,
pub prev_conf: u64,
pub prev_timestamp: i64,
pub agg: PriceInfo,
pub comp: [PriceComp; N],
pub extended: T,
}
impl<const N: usize, T> Default for GenericPriceAccount<N, T>
where
T: Default,
T: Copy,
{
fn default() -> Self {
Self {
magic: Default::default(),
ver: Default::default(),
atype: Default::default(),
size: Default::default(),
ptype: Default::default(),
expo: Default::default(),
num: Default::default(),
num_qt: Default::default(),
last_slot: Default::default(),
valid_slot: Default::default(),
ema_price: Default::default(),
ema_conf: Default::default(),
timestamp: Default::default(),
min_pub: Default::default(),
drv2: Default::default(),
drv3: Default::default(),
drv4: Default::default(),
prod: Default::default(),
next: Default::default(),
prev_slot: Default::default(),
prev_price: Default::default(),
prev_conf: Default::default(),
prev_timestamp: Default::default(),
agg: Default::default(),
comp: [Default::default(); N],
extended: Default::default(),
}
}
}
impl<const N: usize, T> std::ops::Deref for GenericPriceAccount<N, T>
where
T: Default,
T: Copy,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.extended
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable, PartialEq, Eq)]
pub struct PriceCumulative {
pub price: i128,
pub conf: u128,
pub num_down_slots: u64,
pub unused: u64,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct PriceAccountExt {
pub price_cumulative: PriceCumulative,
}
#[deprecated(note = "use an explicit SolanaPriceAccount or PythnetPriceAccount to avoid ambiguity")]
pub type PriceAccount = GenericPriceAccount<32, ()>;
pub type SolanaPriceAccount = GenericPriceAccount<32, ()>;
pub type PythnetPriceAccount = GenericPriceAccount<128, PriceAccountExt>;
#[cfg(target_endian = "little")]
unsafe impl<const N: usize, T: Default + Copy> Zeroable for GenericPriceAccount<N, T> {
}
#[cfg(target_endian = "little")]
unsafe impl<const N: usize, T: Default + Copy + 'static> Pod for GenericPriceAccount<N, T> {
}
impl<const N: usize, T> GenericPriceAccount<N, T>
where
T: Default,
T: Copy,
{
pub fn get_publish_time(&self) -> UnixTimestamp {
match self.agg.status {
PriceStatus::Trading => self.timestamp,
_ => self.prev_timestamp,
}
}
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)
}
}
fn load<T: Pod>(data: &[u8]) -> Result<&T, PodCastError> {
let size = size_of::<T>();
if data.len() >= size {
Ok(from_bytes(cast_slice::<u8, u8>(try_cast_slice(
&data[0..size],
)?)))
} else {
Err(PodCastError::SizeMismatch)
}
}
pub fn load_mapping_account(data: &[u8]) -> Result<&MappingAccount, PythError> {
let pyth_mapping = load::<MappingAccount>(data).map_err(|_| PythError::InvalidAccountData)?;
if pyth_mapping.magic != MAGIC {
return Err(PythError::InvalidAccountData);
}
if pyth_mapping.ver != VERSION_2 {
return Err(PythError::BadVersionNumber);
}
if pyth_mapping.atype != AccountType::Mapping as u32 {
return Err(PythError::WrongAccountType);
}
Ok(pyth_mapping)
}
pub fn load_product_account(data: &[u8]) -> Result<&ProductAccount, PythError> {
let pyth_product = load::<ProductAccount>(data).map_err(|_| PythError::InvalidAccountData)?;
if pyth_product.magic != MAGIC {
return Err(PythError::InvalidAccountData);
}
if pyth_product.ver != VERSION_2 {
return Err(PythError::BadVersionNumber);
}
if pyth_product.atype != AccountType::Product as u32 {
return Err(PythError::WrongAccountType);
}
Ok(pyth_product)
}
pub fn load_price_account<const N: usize, T: Default + Copy + 'static>(
data: &[u8],
) -> Result<&GenericPriceAccount<N, T>, PythError> {
let pyth_price =
load::<GenericPriceAccount<N, T>>(data).map_err(|_| PythError::InvalidAccountData)?;
if pyth_price.magic != MAGIC {
return Err(PythError::InvalidAccountData);
}
if pyth_price.ver != VERSION_2 {
return Err(PythError::BadVersionNumber);
}
if pyth_price.atype != AccountType::Price as u32 {
return Err(PythError::WrongAccountType);
}
Ok(pyth_price)
}
pub struct AttributeIter<'a> {
attrs: &'a [u8],
}
impl<'a> Iterator for AttributeIter<'a> {
type Item = (&'a str, &'a str);
fn next(&mut self) -> Option<Self::Item> {
if self.attrs.is_empty() {
return None;
}
let (key, data) = get_attr_str(self.attrs)?;
let (val, data) = get_attr_str(data)?;
self.attrs = data;
Some((key, val))
}
}
fn get_attr_str(buf: &[u8]) -> Option<(&str, &[u8])> {
if buf.is_empty() {
return Some(("", &[]));
}
let len = buf[0] as usize;
let str = std::str::from_utf8(buf.get(1..len + 1)?).ok()?;
let remaining_buf = &buf.get(len + 1..)?;
Some((str, remaining_buf))
}
#[cfg(test)]
mod test {
use pyth_sdk::{
Identifier,
Price,
PriceFeed,
};
use solana_program::clock::Clock;
use solana_program::pubkey::Pubkey;
use crate::state::{
PROD_ACCT_SIZE,
PROD_HDR_SIZE,
};
use super::{
PriceInfo,
PriceStatus,
Rational,
SolanaPriceAccount,
};
#[test]
fn test_trading_price_to_price_feed() {
let price_account = SolanaPriceAccount {
expo: 5,
agg: PriceInfo {
price: 10,
conf: 20,
status: PriceStatus::Trading,
..Default::default()
},
timestamp: 200,
prev_timestamp: 100,
ema_price: Rational {
val: 40,
..Default::default()
},
ema_conf: Rational {
val: 50,
..Default::default()
},
prev_price: 60,
prev_conf: 70,
..Default::default()
};
let pubkey = Pubkey::new_from_array([3; 32]);
let price_feed = price_account.to_price_feed(&pubkey);
assert_eq!(
price_feed,
PriceFeed::new(
Identifier::new(pubkey.to_bytes()),
Price {
conf: 20,
price: 10,
expo: 5,
publish_time: 200,
},
Price {
conf: 50,
price: 40,
expo: 5,
publish_time: 200,
}
)
);
}
#[test]
fn test_non_trading_price_to_price_feed() {
let price_account = SolanaPriceAccount {
expo: 5,
agg: PriceInfo {
price: 10,
conf: 20,
status: PriceStatus::Unknown,
..Default::default()
},
timestamp: 200,
prev_timestamp: 100,
ema_price: Rational {
val: 40,
..Default::default()
},
ema_conf: Rational {
val: 50,
..Default::default()
},
prev_price: 60,
prev_conf: 70,
..Default::default()
};
let pubkey = Pubkey::new_from_array([3; 32]);
let price_feed = price_account.to_price_feed(&pubkey);
assert_eq!(
price_feed,
PriceFeed::new(
Identifier::new(pubkey.to_bytes()),
Price {
conf: 70,
price: 60,
expo: 5,
publish_time: 100,
},
Price {
conf: 50,
price: 40,
expo: 5,
publish_time: 100,
}
)
);
}
#[test]
fn test_happy_use_latest_price_in_price_no_older_than() {
let price_account = SolanaPriceAccount {
expo: 5,
agg: PriceInfo {
price: 10,
conf: 20,
status: PriceStatus::Trading,
pub_slot: 1,
..Default::default()
},
timestamp: 200,
prev_timestamp: 100,
prev_price: 60,
prev_conf: 70,
..Default::default()
};
let clock = Clock {
slot: 5,
..Default::default()
};
assert_eq!(
price_account.get_price_no_older_than(&clock, 4),
Some(Price {
conf: 20,
expo: 5,
price: 10,
publish_time: 200,
})
);
}
#[test]
fn test_happy_use_prev_price_in_price_no_older_than() {
let price_account = SolanaPriceAccount {
expo: 5,
agg: PriceInfo {
price: 10,
conf: 20,
status: PriceStatus::Unknown,
pub_slot: 3,
..Default::default()
},
timestamp: 200,
prev_timestamp: 100,
prev_price: 60,
prev_conf: 70,
prev_slot: 1,
..Default::default()
};
let clock = Clock {
slot: 5,
..Default::default()
};
assert_eq!(
price_account.get_price_no_older_than(&clock, 4),
Some(Price {
conf: 70,
expo: 5,
price: 60,
publish_time: 100,
})
);
}
#[test]
fn test_sad_cur_price_unknown_in_price_no_older_than() {
let price_account = SolanaPriceAccount {
expo: 5,
agg: PriceInfo {
price: 10,
conf: 20,
status: PriceStatus::Unknown,
pub_slot: 3,
..Default::default()
},
timestamp: 200,
prev_timestamp: 100,
prev_price: 60,
prev_conf: 70,
prev_slot: 1,
..Default::default()
};
let clock = Clock {
slot: 5,
..Default::default()
};
assert_eq!(price_account.get_price_no_older_than(&clock, 3), None);
}
#[test]
fn test_sad_cur_price_stale_in_price_no_older_than() {
let price_account = SolanaPriceAccount {
expo: 5,
agg: PriceInfo {
price: 10,
conf: 20,
status: PriceStatus::Trading,
pub_slot: 3,
..Default::default()
},
timestamp: 200,
prev_timestamp: 100,
prev_price: 60,
prev_conf: 70,
prev_slot: 1,
..Default::default()
};
let clock = Clock {
slot: 5,
..Default::default()
};
assert_eq!(price_account.get_price_no_older_than(&clock, 1), None);
}
#[test]
fn test_price_feed_representations_equal() {
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct OldPriceAccount {
pub magic: u32,
pub ver: u32,
pub atype: u32,
pub size: u32,
pub ptype: crate::state::PriceType,
pub expo: i32,
pub num: u32,
pub num_qt: u32,
pub last_slot: u64,
pub valid_slot: u64,
pub ema_price: Rational,
pub ema_conf: Rational,
pub timestamp: i64,
pub min_pub: u8,
pub drv2: u8,
pub drv3: u16,
pub drv4: u32,
pub prod: Pubkey,
pub next: Pubkey,
pub prev_slot: u64,
pub prev_price: i64,
pub prev_conf: u64,
pub prev_timestamp: i64,
pub agg: PriceInfo,
pub comp: [crate::state::PriceComp; 32],
}
let old = OldPriceAccount {
magic: 1,
ver: 2,
atype: 3,
size: 4,
ptype: crate::state::PriceType::Price,
expo: 5,
num: 6,
num_qt: 7,
last_slot: 8,
valid_slot: 9,
ema_price: Rational {
val: 1,
numer: 2,
denom: 3,
},
ema_conf: Rational {
val: 1,
numer: 2,
denom: 3,
},
timestamp: 12,
min_pub: 13,
drv2: 14,
drv3: 15,
drv4: 16,
prod: Pubkey::new_from_array([1; 32]),
next: Pubkey::new_from_array([2; 32]),
prev_slot: 19,
prev_price: 20,
prev_conf: 21,
prev_timestamp: 22,
agg: PriceInfo {
price: 1,
conf: 2,
status: PriceStatus::Trading,
corp_act: crate::state::CorpAction::NoCorpAct,
pub_slot: 5,
},
comp: [Default::default(); 32],
};
let new = super::SolanaPriceAccount {
magic: 1,
ver: 2,
atype: 3,
size: 4,
ptype: crate::state::PriceType::Price,
expo: 5,
num: 6,
num_qt: 7,
last_slot: 8,
valid_slot: 9,
ema_price: Rational {
val: 1,
numer: 2,
denom: 3,
},
ema_conf: Rational {
val: 1,
numer: 2,
denom: 3,
},
timestamp: 12,
min_pub: 13,
drv2: 14,
drv3: 15,
drv4: 16,
prod: Pubkey::new_from_array([1; 32]),
next: Pubkey::new_from_array([2; 32]),
prev_slot: 19,
prev_price: 20,
prev_conf: 21,
prev_timestamp: 22,
agg: PriceInfo {
price: 1,
conf: 2,
status: PriceStatus::Trading,
corp_act: crate::state::CorpAction::NoCorpAct,
pub_slot: 5,
},
comp: [Default::default(); 32],
extended: (),
};
assert_eq!(
std::mem::size_of::<OldPriceAccount>(),
std::mem::size_of::<super::SolanaPriceAccount>(),
);
unsafe {
let old_b = std::slice::from_raw_parts(
&old as *const OldPriceAccount as *const u8,
std::mem::size_of::<OldPriceAccount>(),
);
let new_b = std::slice::from_raw_parts(
&new as *const super::SolanaPriceAccount as *const u8,
std::mem::size_of::<super::SolanaPriceAccount>(),
);
assert_eq!(old_b, new_b);
}
}
#[test]
fn test_product_account_iter_works() {
let mut product = super::ProductAccount {
magic: 1,
ver: 2,
atype: super::AccountType::Product as u32,
size: PROD_HDR_SIZE as u32 + 10,
px_acc: Pubkey::new_from_array([3; 32]),
attr: [0; super::PROD_ATTR_SIZE],
};
product.attr[0] = 3; product.attr[1..4].copy_from_slice(b"key");
product.attr[4] = 5; product.attr[5..10].copy_from_slice(b"value");
let mut iter = product.iter();
assert_eq!(iter.next(), Some(("key", "value")));
assert_eq!(iter.next(), None);
product.size = PROD_HDR_SIZE as u32 - 10; let mut iter = product.iter();
assert_eq!(iter.next(), None);
product.size = PROD_ACCT_SIZE as u32 + 10; let mut iter = product.iter();
assert_eq!(iter.next(), Some(("key", "value")));
while iter.next().is_some() {}
product.attr[10] = 255;
for i in 11..266 {
product.attr[i] = b'a';
}
product.attr[266] = 255;
for i in 267..super::PROD_ATTR_SIZE {
product.attr[i] = b'b';
}
let mut iter = product.iter();
assert_eq!(iter.next(), Some(("key", "value")));
assert_eq!(iter.next(), None);
product.attr[266] = 10;
let mut iter = product.iter();
assert_eq!(iter.next(), Some(("key", "value")));
let (key, val) = iter.next().unwrap();
assert_eq!(key.len(), 255);
for byte in key.as_bytes() {
assert_eq!(byte, &b'a');
}
assert_eq!(val, "bbbbbbbbbb");
product.attr[1..4].copy_from_slice(b"\xff\xfe\xfa");
let mut iter = product.iter();
assert_eq!(iter.next(), None); }
}