|
|
@@ -1,6 +1,13 @@
|
|
|
pub mod pyth_extensions;
|
|
|
|
|
|
-use std::mem;
|
|
|
+use std::{
|
|
|
+ convert::{
|
|
|
+ TryFrom,
|
|
|
+ TryInto,
|
|
|
+ },
|
|
|
+ io::Read,
|
|
|
+ mem,
|
|
|
+};
|
|
|
|
|
|
use borsh::BorshSerialize;
|
|
|
use pyth_client::{
|
|
|
@@ -11,9 +18,14 @@ use pyth_client::{
|
|
|
PriceStatus,
|
|
|
PriceType,
|
|
|
};
|
|
|
-use solana_program::{clock::UnixTimestamp, program_error::ProgramError, pubkey::Pubkey};
|
|
|
+use solana_program::{
|
|
|
+ clock::UnixTimestamp,
|
|
|
+ program_error::ProgramError,
|
|
|
+ pubkey::Pubkey,
|
|
|
+};
|
|
|
use solitaire::{
|
|
|
trace,
|
|
|
+ ErrBox,
|
|
|
Result as SoliResult,
|
|
|
SolitaireError,
|
|
|
};
|
|
|
@@ -33,6 +45,8 @@ pub const P2W_MAGIC: &'static [u8] = b"P2WH";
|
|
|
/// Format version used and understood by this codebase
|
|
|
pub const P2W_FORMAT_VERSION: u16 = 1;
|
|
|
|
|
|
+pub const PUBKEY_LEN: usize = 32;
|
|
|
+
|
|
|
/// Decides the format of following bytes
|
|
|
#[repr(u8)]
|
|
|
pub enum PayloadId {
|
|
|
@@ -42,6 +56,7 @@ pub enum PayloadId {
|
|
|
// On-chain data types
|
|
|
|
|
|
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
|
|
+#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
|
|
|
pub struct PriceAttestation {
|
|
|
pub product_id: Pubkey,
|
|
|
pub price_id: Pubkey,
|
|
|
@@ -57,7 +72,11 @@ pub struct PriceAttestation {
|
|
|
}
|
|
|
|
|
|
impl PriceAttestation {
|
|
|
- pub fn from_pyth_price_bytes(price_id: Pubkey, timestamp: UnixTimestamp, value: &[u8]) -> Result<Self, SolitaireError> {
|
|
|
+ pub fn from_pyth_price_bytes(
|
|
|
+ price_id: Pubkey,
|
|
|
+ timestamp: UnixTimestamp,
|
|
|
+ value: &[u8],
|
|
|
+ ) -> Result<Self, SolitaireError> {
|
|
|
let price = parse_pyth_price(value)?;
|
|
|
|
|
|
Ok(PriceAttestation {
|
|
|
@@ -71,14 +90,14 @@ impl PriceAttestation {
|
|
|
confidence_interval: price.agg.conf,
|
|
|
status: (&price.agg.status).into(),
|
|
|
corp_act: (&price.agg.corp_act).into(),
|
|
|
- timestamp: timestamp,
|
|
|
+ timestamp: timestamp,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
/// Serialize this attestation according to the Pyth-over-wormhole serialization format
|
|
|
pub fn serialize(&self) -> Vec<u8> {
|
|
|
// A nifty trick to get us yelled at if we forget to serialize a field
|
|
|
- #[deny(warnings)]
|
|
|
+ #[deny(warnings)]
|
|
|
let PriceAttestation {
|
|
|
product_id,
|
|
|
price_id,
|
|
|
@@ -90,7 +109,7 @@ impl PriceAttestation {
|
|
|
confidence_interval,
|
|
|
status,
|
|
|
corp_act,
|
|
|
- timestamp
|
|
|
+ timestamp,
|
|
|
} = self;
|
|
|
|
|
|
// magic
|
|
|
@@ -123,20 +142,136 @@ impl PriceAttestation {
|
|
|
// twac
|
|
|
buf.append(&mut twac.serialize());
|
|
|
|
|
|
- // confidence_interval
|
|
|
- buf.extend_from_slice(&confidence_interval.to_be_bytes()[..]);
|
|
|
+ // confidence_interval
|
|
|
+ buf.extend_from_slice(&confidence_interval.to_be_bytes()[..]);
|
|
|
|
|
|
- // status
|
|
|
- buf.push(status.clone() as u8);
|
|
|
+ // status
|
|
|
+ buf.push(status.clone() as u8);
|
|
|
|
|
|
- // corp_act
|
|
|
- buf.push(corp_act.clone() as u8);
|
|
|
+ // corp_act
|
|
|
+ buf.push(corp_act.clone() as u8);
|
|
|
|
|
|
- // timestamp
|
|
|
- buf.extend_from_slice(×tamp.to_be_bytes()[..]);
|
|
|
+ // timestamp
|
|
|
+ buf.extend_from_slice(×tamp.to_be_bytes()[..]);
|
|
|
|
|
|
buf
|
|
|
}
|
|
|
+ pub fn deserialize(mut bytes: impl Read) -> Result<Self, ErrBox> {
|
|
|
+ use P2WCorpAction::*;
|
|
|
+ use P2WPriceStatus::*;
|
|
|
+ use P2WPriceType::*;
|
|
|
+
|
|
|
+ println!("Using {} bytes for magic", P2W_MAGIC.len());
|
|
|
+ let mut magic_vec = vec![0u8; P2W_MAGIC.len()];
|
|
|
+
|
|
|
+ bytes.read_exact(magic_vec.as_mut_slice())?;
|
|
|
+
|
|
|
+ if magic_vec.as_slice() != P2W_MAGIC {
|
|
|
+ return Err(format!(
|
|
|
+ "Invalid magic {:02X?}, expected {:02X?}",
|
|
|
+ magic_vec, P2W_MAGIC,
|
|
|
+ )
|
|
|
+ .into());
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)];
|
|
|
+ bytes.read_exact(version_vec.as_mut_slice())?;
|
|
|
+ let mut version = u16::from_be_bytes(version_vec.as_slice().try_into()?);
|
|
|
+
|
|
|
+ if version != P2W_FORMAT_VERSION {
|
|
|
+ return Err(format!(
|
|
|
+ "Unsupported format version {}, expected {}",
|
|
|
+ version, P2W_FORMAT_VERSION
|
|
|
+ )
|
|
|
+ .into());
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut payload_id_vec = vec![0u8; mem::size_of::<PayloadId>()];
|
|
|
+ bytes.read_exact(payload_id_vec.as_mut_slice())?;
|
|
|
+
|
|
|
+ if PayloadId::PriceAttestation as u8 != payload_id_vec[0] {
|
|
|
+ return Err(format!(
|
|
|
+ "Invalid Payload ID {}, expected {}",
|
|
|
+ payload_id_vec[0],
|
|
|
+ PayloadId::PriceAttestation as u8,
|
|
|
+ )
|
|
|
+ .into());
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut product_id_vec = vec![0u8; PUBKEY_LEN];
|
|
|
+ bytes.read_exact(product_id_vec.as_mut_slice())?;
|
|
|
+ let product_id = Pubkey::new(product_id_vec.as_slice());
|
|
|
+
|
|
|
+ let mut price_id_vec = vec![0u8; PUBKEY_LEN];
|
|
|
+ bytes.read_exact(price_id_vec.as_mut_slice())?;
|
|
|
+ let price_id = Pubkey::new(price_id_vec.as_slice());
|
|
|
+
|
|
|
+ let mut price_type_vec = vec![0u8; mem::size_of::<P2WPriceType>()];
|
|
|
+ bytes.read_exact(price_type_vec.as_mut_slice())?;
|
|
|
+ let price_type = match price_type_vec[0] {
|
|
|
+ a if a == Price as u8 => Price,
|
|
|
+ a if a == P2WPriceType::Unknown as u8 => P2WPriceType::Unknown,
|
|
|
+ other => {
|
|
|
+ return Err(format!("Invalid price_type value {}", other).into());
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ let mut price_vec = vec![0u8; mem::size_of::<i64>()];
|
|
|
+ bytes.read_exact(price_vec.as_mut_slice())?;
|
|
|
+ let price = i64::from_be_bytes(price_vec.as_slice().try_into()?);
|
|
|
+
|
|
|
+ let mut expo_vec = vec![0u8; mem::size_of::<i32>()];
|
|
|
+ bytes.read_exact(expo_vec.as_mut_slice())?;
|
|
|
+ let expo = i32::from_be_bytes(expo_vec.as_slice().try_into()?);
|
|
|
+
|
|
|
+ let twap = P2WEma::deserialize(&mut bytes)?;
|
|
|
+ let twac = P2WEma::deserialize(&mut bytes)?;
|
|
|
+
|
|
|
+ println!("twac OK");
|
|
|
+ let mut confidence_interval_vec = vec![0u8; mem::size_of::<u64>()];
|
|
|
+ bytes.read_exact(confidence_interval_vec.as_mut_slice())?;
|
|
|
+ let confidence_interval = u64::from_be_bytes(confidence_interval_vec.as_slice().try_into()?);
|
|
|
+
|
|
|
+ let mut status_vec = vec![0u8; mem::size_of::<P2WPriceType>()];
|
|
|
+ bytes.read_exact(status_vec.as_mut_slice())?;
|
|
|
+ let status = match status_vec[0] {
|
|
|
+ a if a == P2WPriceStatus::Unknown as u8 => P2WPriceStatus::Unknown,
|
|
|
+ a if a == Trading as u8 => Trading,
|
|
|
+ a if a == Halted as u8 => Halted,
|
|
|
+ a if a == Auction as u8 => Auction,
|
|
|
+ other => {
|
|
|
+ return Err(format!("Invalid status value {}", other).into());
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ let mut corp_act_vec = vec![0u8; mem::size_of::<P2WPriceType>()];
|
|
|
+ bytes.read_exact(corp_act_vec.as_mut_slice())?;
|
|
|
+ let corp_act = match corp_act_vec[0] {
|
|
|
+ a if a == NoCorpAct as u8 => NoCorpAct,
|
|
|
+ other => {
|
|
|
+ return Err(format!("Invalid corp_act value {}", other).into());
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ let mut timestamp_vec = vec![0u8; mem::size_of::<UnixTimestamp>()];
|
|
|
+ bytes.read_exact(timestamp_vec.as_mut_slice())?;
|
|
|
+ let timestamp = UnixTimestamp::from_be_bytes(timestamp_vec.as_slice().try_into()?);
|
|
|
+
|
|
|
+ Ok( Self {
|
|
|
+ product_id,
|
|
|
+ price_id,
|
|
|
+ price_type,
|
|
|
+ price,
|
|
|
+ expo,
|
|
|
+ twap,
|
|
|
+ twac,
|
|
|
+ confidence_interval,
|
|
|
+ status,
|
|
|
+ corp_act,
|
|
|
+ timestamp
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// Deserializes Price from raw bytes, sanity-check.
|
|
|
@@ -296,7 +431,7 @@ mod tests {
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
- fn test_serialize() -> SoliResult<()> {
|
|
|
+ fn test_serialize_deserialize() -> Result<(), ErrBox> {
|
|
|
let product_id_bytes = [21u8; 32];
|
|
|
let price_id_bytes = [222u8; 32];
|
|
|
println!("Hex product_id: {:02X?}", &product_id_bytes);
|
|
|
@@ -305,7 +440,7 @@ mod tests {
|
|
|
product_id: Pubkey::new_from_array(product_id_bytes),
|
|
|
price_id: Pubkey::new_from_array(price_id_bytes),
|
|
|
price: (0xdeadbeefdeadbabe as u64) as i64,
|
|
|
- price_type: P2WPriceType::Price,
|
|
|
+ price_type: P2WPriceType::Price,
|
|
|
twap: P2WEma {
|
|
|
val: -42,
|
|
|
numer: 15,
|
|
|
@@ -317,15 +452,18 @@ mod tests {
|
|
|
denom: 2222,
|
|
|
},
|
|
|
expo: -3,
|
|
|
- status: P2WPriceStatus::Trading,
|
|
|
+ status: P2WPriceStatus::Trading,
|
|
|
confidence_interval: 101,
|
|
|
- corp_act: P2WCorpAction::NoCorpAct,
|
|
|
- timestamp: 123456789i64,
|
|
|
+ corp_act: P2WCorpAction::NoCorpAct,
|
|
|
+ timestamp: 123456789i64,
|
|
|
};
|
|
|
|
|
|
println!("Regular: {:#?}", &attestation);
|
|
|
println!("Hex: {:#02X?}", &attestation);
|
|
|
- println!("Hex Bytes: {:02X?}", attestation.serialize());
|
|
|
+ let bytes = attestation.serialize();
|
|
|
+ println!("Hex Bytes: {:02X?}", bytes);
|
|
|
+
|
|
|
+ assert_eq!(PriceAttestation::deserialize(bytes.as_slice())?, attestation);
|
|
|
Ok(())
|
|
|
}
|
|
|
}
|