pyth2wormhole: on-chain attestation call, update Pyth-facing types

This commit takes the selected Pyth Price struct account and after
serialization places them inside a PostMessage cross-program
call to Wormhole.

Change-Id: If04123705290f4749de318c0dfaa8f1d840ed349
This commit is contained in:
Stan Drozd 2021-08-02 13:20:06 +02:00
parent 3eae629a1b
commit 5dbd3ea722
14 changed files with 675 additions and 270 deletions

View File

@ -71,7 +71,7 @@ impl Owned for GuardianSetData {
}
}
#[derive(Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
#[derive(Clone, Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct BridgeConfig {
/// Period for how long a guardian set is valid after it has been replaced by a new one. This
/// guarantees that VAAs issued by that set can still be submitted for a certain period. In
@ -82,7 +82,7 @@ pub struct BridgeConfig {
pub fee: u64,
}
#[derive(Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
#[derive(Clone, Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct BridgeData {
/// The current guardian set index, used to decide which signature sets to accept.
pub guardian_set_index: u32,

View File

@ -2057,8 +2057,8 @@ dependencies = [
[[package]]
name = "pyth-client"
version = "0.2.0"
source = "git+https://github.com/drozdziak1/pyth-client-rs?branch=v2-clone-and-debug#0b073bcdad1312051b086334e00f7925cff137e2"
version = "0.2.2"
source = "git+https://github.com/pyth-network/pyth-client-rs?branch=v2#0d2689fdd4ffba09d7d77f5a52b09e16912983ec"
[[package]]
name = "pyth2wormhole"

View File

@ -10,13 +10,15 @@ name = "pyth2wormhole"
[features]
client = ["solitaire/client", "solitaire-client", "no-entrypoint"]
trace = ["solitaire/trace", "bridge/trace"]
no-entrypoint = []
[dependencies]
bridge = {path = "../../bridge/program"}
solitaire = { path = "../../solitaire/program" }
solitaire-client = { path = "../../solitaire/client", optional = true }
pyth-client = {git = "https://github.com/drozdziak1/pyth-client-rs", branch = "v2-clone-and-debug"}
rocksalt = { path = "../../solitaire/rocksalt" }
solana-program = "=1.7.0"
borsh = "0.8.1"
# NOTE: We're following bleeding edge to encounter format changes more easily
pyth-client = {git = "https://github.com/pyth-network/pyth-client-rs", branch = "v2"}

View File

@ -0,0 +1,172 @@
use crate::{
config::P2WConfigAccount,
types::PriceAttestation,
};
use borsh::{
BorshDeserialize,
BorshSerialize,
};
use solana_program::{
clock::Clock,
instruction::{
AccountMeta,
Instruction,
},
program::{
invoke,
invoke_signed,
},
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
};
use bridge::{
types::{
BridgeData,
ConsistencyLevel,
},
PostMessageData,
};
use solitaire::{
trace,
AccountState,
Derive,
ExecutionContext,
FromAccounts,
Info,
InstructionContext,
Keyed,
Mut,
Peel,
Result as SoliResult,
Seeded,
Signer,
SolitaireError,
Sysvar,
ToInstruction,
};
#[derive(FromAccounts, ToInstruction)]
pub struct Attest<'b> {
// Payer also used for wormhole
pub payer: Mut<Signer<Info<'b>>>,
pub system_program: Info<'b>,
pub config: P2WConfigAccount<'b, { AccountState::Initialized }>,
pub wormhole_program: Info<'b>,
pub pyth_product: Info<'b>,
pub pyth_price: Info<'b>,
pub clock: Sysvar<'b, Clock>,
// post_message accounts
/// Wormhole program address
pub wh_prog: Info<'b>,
/// Bridge config needed for fee calculation
pub wh_bridge: Mut<Info<'b>>,
/// Account to store the posted message
pub wh_message: Signer<Mut<Info<'b>>>,
/// Emitter of the VAA
pub wh_emitter: Info<'b>,
/// Tracker for the emitter sequence
pub wh_sequence: Mut<Info<'b>>,
// We reuse our payer
// pub wh_payer: Mut<Signer<Info<'b>>>,
/// Account to collect tx fee
pub wh_fee_collector: Mut<Info<'b>>,
pub wh_rent: Sysvar<'b, Rent>,
}
#[derive(BorshDeserialize, BorshSerialize)]
pub struct AttestData {
pub nonce: u32,
pub consistency_level: ConsistencyLevel,
}
impl<'b> InstructionContext<'b> for Attest<'b> {
fn deps(&self) -> Vec<Pubkey> {
vec![solana_program::system_program::id()]
}
}
pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> SoliResult<()> {
accs.config.verify_derivation(ctx.program_id, None)?;
if accs.config.pyth_owner != *accs.pyth_price.owner
|| accs.config.pyth_owner != *accs.pyth_product.owner
{
trace!(&format!(
"pyth_owner pubkey mismatch (expected {:?}, got price owner {:?} and product owner {:?}",
accs.config.pyth_owner, accs.pyth_price.owner, accs.pyth_product.owner
));
return Err(SolitaireError::InvalidOwner(accs.pyth_price.owner.clone()).into());
}
if accs.config.wh_prog != *accs.wh_prog.key {
trace!(&format!(
"Wormhole program account mismatch (expected {:?}, got {:?})",
accs.config.wh_prog, accs.wh_prog.key
));
}
let price_attestation = PriceAttestation::from_pyth_price_bytes(
accs.pyth_price.key.clone(),
accs.clock.unix_timestamp,
&*accs.pyth_price.try_borrow_data()?,
)?;
if &price_attestation.product_id != accs.pyth_product.key {
trace!(&format!(
"Price's product_id does not match the pased account (points at {:?} instead)",
price_attestation.product_id
));
return Err(ProgramError::InvalidAccountData.into());
}
let bridge_config = BridgeData::try_from_slice(&accs.wh_bridge.try_borrow_mut_data()?)?.config;
// Pay wormhole fee
let transfer_ix = solana_program::system_instruction::transfer(
accs.payer.key,
accs.wh_fee_collector.info().key,
bridge_config.fee,
);
solana_program::program::invoke(&transfer_ix, ctx.accounts)?;
// Send payload
let post_message_data = (
bridge::instruction::Instruction::PostMessage,
PostMessageData {
nonce: data.nonce,
payload: price_attestation.serialize(),
consistency_level: data.consistency_level,
},
);
let ix = Instruction::new_with_bytes(
accs.config.wh_prog,
post_message_data.try_to_vec()?.as_slice(),
vec![
AccountMeta::new(*accs.wh_bridge.key, false),
AccountMeta::new(*accs.wh_message.key, true),
AccountMeta::new_readonly(*accs.wh_emitter.key, true),
AccountMeta::new(*accs.wh_sequence.key, false),
AccountMeta::new(*accs.payer.key, true),
AccountMeta::new(*accs.wh_fee_collector.key, false),
AccountMeta::new_readonly(*accs.clock.info().key, false),
AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
],
);
trace!("Before cross-call");
invoke(&ix, ctx.accounts)?;
Ok(())
}

View File

@ -1,17 +1,13 @@
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::pubkey::Pubkey;
use solitaire::{
data_wrapper,
processors::seeded::{AccountOwner, Seeded},
AccountState, Data, Derive, Owned,
};
use solitaire::{processors::seeded::AccountOwner, AccountState, Data, Derive, Owned};
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Pyth2WormholeConfig {
/// Authority owning this contract
pub owner: Pubkey,
/// Wormhole bridge program
pub wormhole_program_addr: Pubkey,
pub wh_prog: Pubkey,
/// Authority owning Pyth price data
pub pyth_owner: Pubkey,
}

View File

@ -1,73 +0,0 @@
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{msg, program_error::ProgramError, pubkey::Pubkey};
use solitaire::{
processors::seeded::AccountOwner, AccountState, Context, Data, ExecutionContext, FromAccounts,
Info, InstructionContext, Keyed, Owned, Peel, Result as SoliResult, Signer,
ToInstruction,
};
use crate::{config::P2WConfigAccount, types::PriceAttestation};
use solana_program::{
clock::Clock, instruction::Instruction, msg, program::invoke_signed,
program_error::ProgramError, pubkey::Pubkey,
};
use solitaire::{
processors::seeded::AccountOwner, trace, AccountState, CPICall, Context, Data,
ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Owned, Peel,
Result as SoliResult, Signer, SolitaireError, Sysvar, ToInstruction,
};
#[derive(FromAccounts, ToInstruction)]
pub struct Forward<'b> {
pub payer: Signer<Info<'b>>,
pub system_program: Signer<Info<'b>>,
pub config: P2WConfigAccount<'b, {AccountState::Initialized}>,
pub wormhole_program: Info<'b>,
pub pyth_product: Info<'b>,
pub pyth_price: Info<'b>,
pub post_message_call: CPICall<PostMessage<'b>>,
}
#[derive(BorshDeserialize, BorshSerialize)]
pub struct ForwardData {
pub target_chain: u32,
}
impl<'b> InstructionContext<'b> for Forward<'b> {
fn verify(&self, _program_id: &Pubkey) -> SoliResult<()> {
if self.config.wormhole_program_addr != *self.wormhole_program.key {
trace!(&format!(
"wormhole_program pubkey mismatch (expected {:?}",
self.config.wormhole_program_addr
));
return Err(ProgramError::InvalidAccountData.into());
}
if self.config.pyth_owner != *self.pyth_price.owner
|| self.config.pyth_owner != *self.pyth_product.owner
{
trace!(&format!(
"pyth_owner pubkey mismatch (expected {:?}",
self.config.pyth_owner
));
return Err(SolitaireError::InvalidOwner(self.pyth_price.owner.clone()).into());
}
Ok(())
}
fn deps(&self) -> Vec<Pubkey> {
vec![solana_program::system_program::id()]
}
}
pub fn forward_price(
_ctx: &ExecutionContext,
accs: &mut Forward,
_data: ForwardData,
) -> SoliResult<()> {
let _price_attestation = PriceAttestation::from_bytes(&*accs.pyth_price.0.try_borrow_data()?)?;
Ok(())
}

View File

@ -1,22 +1,15 @@
use solana_program::pubkey::Pubkey;
use solitaire::{
AccountState, Context, Creatable, CreationLamports, ExecutionContext, FromAccounts, Info,
InstructionContext, Keyed, Peel, Result as SoliResult, Signer, ToInstruction,
};
use solitaire::{AccountState, CreationLamports, ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Mut, Peel, Result as SoliResult, Signer, ToInstruction};
use crate::config::{P2WConfigAccount, Pyth2WormholeConfig};
#[derive(FromAccounts, ToInstruction)]
pub struct Initialize<'b> {
pub new_config: P2WConfigAccount<'b, {AccountState::Uninitialized}>,
pub payer: Signer<Info<'b>>,
pub new_config: Mut<P2WConfigAccount<'b, {AccountState::Uninitialized}>>,
pub payer: Mut<Signer<Info<'b>>>,
}
impl<'b> InstructionContext<'b> for Initialize<'b> {
fn verify(&self, _program_id: &Pubkey) -> SoliResult<()> {
Ok(())
}
fn deps(&self) -> Vec<Pubkey> {
vec![]
}

View File

@ -1,21 +1,29 @@
#![feature(const_generics)]
pub mod attest;
pub mod config;
pub mod forward;
pub mod initialize;
pub mod set_config;
pub mod types;
use solitaire::{
solitaire
use solitaire::solitaire;
pub use attest::{
attest,
Attest,
AttestData,
};
pub use config::Pyth2WormholeConfig;
pub use initialize::{
initialize,
Initialize,
};
pub use set_config::{
set_config,
SetConfig,
};
pub use config::Pyth2WormholeConfig;
pub use forward::{forward_price, Forward, ForwardData};
pub use initialize::{initialize, Initialize};
pub use set_config::{set_config, SetConfig};
solitaire! {
Forward(ForwardData) => forward_price,
Attest(AttestData) => attest,
Initialize(Pyth2WormholeConfig) => initialize,
SetConfig(Pyth2WormholeConfig) => set_config,
}

View File

@ -1,6 +1,6 @@
use solana_program::{msg, pubkey::Pubkey};
use solitaire::{
AccountState, Context, ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Peel,
AccountState, ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Peel,
Result as SoliResult, Signer, SolitaireError, ToInstruction,
};

View File

@ -1,165 +0,0 @@
use std::{mem};
use pyth_client::{CorpAction, Price, PriceStatus, PriceType};
use solana_program::{program_error::ProgramError, pubkey::Pubkey};
use solitaire::{Result as SoliResult, SolitaireError};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PriceAttestation {
pub product: Pubkey,
pub price_type: PriceType,
pub price: i64,
pub expo: i32,
pub confidence_interval: u64,
pub status: PriceStatus,
pub corp_act: CorpAction,
}
impl PriceAttestation {
pub fn from_bytes(value: &[u8]) -> Result<Self, SolitaireError> {
let price = parse_pyth_price(value)?;
Ok(PriceAttestation {
product: Pubkey::new(&price.prod.val[..]),
price_type: price.ptype,
price: price.agg.price,
expo: price.expo,
confidence_interval: price.agg.conf,
status: price.agg.status,
corp_act: price.agg.corp_act,
})
}
}
/// Deserializes Price from raw bytes
fn parse_pyth_price(price_data: &[u8]) -> SoliResult<Price> {
if price_data.len() != mem::size_of::<Price>() {
return Err(ProgramError::InvalidAccountData.into());
}
let price_account = pyth_client::cast::<Price>(price_data);
Ok(price_account.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use pyth_client::{AccKey, AccountType, PriceComp, PriceInfo};
macro_rules! empty_acckey {
() => {
AccKey { val: [0u8; 32] }
};
}
macro_rules! empty_priceinfo {
() => {
PriceInfo {
price: 0,
conf: 0,
status: PriceStatus::Unknown,
corp_act: CorpAction::NoCorpAct,
pub_slot: 0,
}
};
}
macro_rules! empty_pricecomp {
() => {
PriceComp {
publisher: empty_acckey!(),
agg: empty_priceinfo!(),
latest: empty_priceinfo!(),
}
};
}
macro_rules! empty_price {
() => {
Price {
magic: pyth_client::MAGIC,
ver: pyth_client::VERSION,
atype: AccountType::Price as u32,
size: 0,
ptype: PriceType::Price,
expo: 0,
num: 0,
unused: 0,
curr_slot: 0,
valid_slot: 0,
twap: 0,
avol: 0,
drv0: 0,
drv1: 0,
drv2: 0,
drv3: 0,
drv4: 0,
drv5: 0,
prod: empty_acckey!(),
next: empty_acckey!(),
agg_pub: empty_acckey!(),
agg: empty_priceinfo!(),
// A nice macro might fix come handy if this gets annoying
comp: [
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
],
}
};
}
#[test]
fn test_parse_pyth_price_wrong_size_slices() {
assert!(parse_pyth_price(&[]).is_err());
assert!(parse_pyth_price(vec![0u8; 1].as_slice()).is_err());
}
#[test]
fn test_normal_values() -> SoliResult<()> {
let price = Price {
expo: 5,
agg: PriceInfo {
price: 42,
..empty_priceinfo!()
},
..empty_price!()
};
let price_vec = vec![price.clone()];
// use the C repr to mock pyth's format
let (_, bytes, _) = unsafe { price_vec.as_slice().align_to::<u8>() };
assert_eq!(parse_pyth_price(bytes)?, price);
Ok(())
}
}

View File

@ -0,0 +1,331 @@
pub mod pyth_extensions;
use std::mem;
use borsh::BorshSerialize;
use pyth_client::{
AccountType,
CorpAction,
Ema,
Price,
PriceStatus,
PriceType,
};
use solana_program::{clock::UnixTimestamp, program_error::ProgramError, pubkey::Pubkey};
use solitaire::{
trace,
Result as SoliResult,
SolitaireError,
};
use self::pyth_extensions::{
P2WCorpAction,
P2WEma,
P2WPriceStatus,
P2WPriceType,
};
// Constants and values common to every p2w custom-serialized message
/// Precedes every message implementing the p2w serialization format
pub const P2W_MAGIC: &'static [u8] = b"P2WH";
/// Format version used and understood by this codebase
pub const P2W_FORMAT_VERSION: u16 = 1;
/// Decides the format of following bytes
#[repr(u8)]
pub enum PayloadId {
PriceAttestation = 1,
}
// On-chain data types
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct PriceAttestation {
pub product_id: Pubkey,
pub price_id: Pubkey,
pub price_type: P2WPriceType,
pub price: i64,
pub expo: i32,
pub twap: P2WEma,
pub twac: P2WEma,
pub confidence_interval: u64,
pub status: P2WPriceStatus,
pub corp_act: P2WCorpAction,
pub timestamp: UnixTimestamp,
}
impl PriceAttestation {
pub fn from_pyth_price_bytes(price_id: Pubkey, timestamp: UnixTimestamp, value: &[u8]) -> Result<Self, SolitaireError> {
let price = parse_pyth_price(value)?;
Ok(PriceAttestation {
product_id: Pubkey::new(&price.prod.val[..]),
price_id,
price_type: (&price.ptype).into(),
price: price.agg.price,
twap: (&price.twap).into(),
twac: (&price.twac).into(),
expo: price.expo,
confidence_interval: price.agg.conf,
status: (&price.agg.status).into(),
corp_act: (&price.agg.corp_act).into(),
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)]
let PriceAttestation {
product_id,
price_id,
price_type,
price,
expo,
twap,
twac,
confidence_interval,
status,
corp_act,
timestamp
} = self;
// magic
let mut buf = P2W_MAGIC.to_vec();
// version
buf.extend_from_slice(&P2W_FORMAT_VERSION.to_be_bytes()[..]);
// payload_id
buf.push(PayloadId::PriceAttestation as u8);
// product_id
buf.extend_from_slice(&product_id.to_bytes()[..]);
// price_id
buf.extend_from_slice(&price_id.to_bytes()[..]);
// price_type
buf.push(price_type.clone() as u8);
// price
buf.extend_from_slice(&price.to_be_bytes()[..]);
// exponent
buf.extend_from_slice(&expo.to_be_bytes()[..]);
// twap
buf.append(&mut twap.serialize());
// twac
buf.append(&mut twac.serialize());
// confidence_interval
buf.extend_from_slice(&confidence_interval.to_be_bytes()[..]);
// status
buf.push(status.clone() as u8);
// corp_act
buf.push(corp_act.clone() as u8);
// timestamp
buf.extend_from_slice(&timestamp.to_be_bytes()[..]);
buf
}
}
/// Deserializes Price from raw bytes, sanity-check.
fn parse_pyth_price(price_data: &[u8]) -> SoliResult<&Price> {
if price_data.len() != mem::size_of::<Price>() {
trace!(&format!(
"parse_pyth_price: buffer length mismatch ({} expected, got {})",
mem::size_of::<Price>(),
price_data.len()
));
return Err(ProgramError::InvalidAccountData.into());
}
let price_account = pyth_client::cast::<Price>(price_data);
if price_account.atype != AccountType::Price as u32 {
trace!(&format!(
"parse_pyth_price: AccountType mismatch ({} expected, got {})",
mem::size_of::<Price>(),
price_data.len()
));
return Err(ProgramError::InvalidAccountData.into());
}
Ok(price_account)
}
#[cfg(test)]
mod tests {
use super::*;
use pyth_client::{
AccKey,
AccountType,
PriceComp,
PriceInfo,
};
macro_rules! empty_acckey {
() => {
AccKey { val: [0u8; 32] }
};
}
macro_rules! empty_priceinfo {
() => {
PriceInfo {
price: 0,
conf: 0,
status: PriceStatus::Unknown,
corp_act: CorpAction::NoCorpAct,
pub_slot: 0,
}
};
}
macro_rules! empty_pricecomp {
() => {
PriceComp {
publisher: empty_acckey!(),
agg: empty_priceinfo!(),
latest: empty_priceinfo!(),
}
};
}
macro_rules! empty_ema {
() => {
(&P2WEma::default()).into()
};
}
macro_rules! empty_price {
() => {
Price {
magic: pyth_client::MAGIC,
ver: pyth_client::VERSION,
atype: AccountType::Price as u32,
size: 0,
ptype: PriceType::Price,
expo: 0,
num: 0,
num_qt: 0,
last_slot: 0,
valid_slot: 0,
drv1: 0,
drv2: 0,
drv3: 0,
twap: empty_ema!(),
twac: empty_ema!(),
prod: empty_acckey!(),
next: empty_acckey!(),
prev_slot: 0, // valid slot of previous update
prev_price: 0, // aggregate price of previous update
prev_conf: 0, // confidence interval of previous update
agg: empty_priceinfo!(),
// A nice macro might come in handy if this gets annoying
comp: [
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
empty_pricecomp!(),
],
}
};
}
#[test]
fn test_parse_pyth_price_wrong_size_slices() {
assert!(parse_pyth_price(&[]).is_err());
assert!(parse_pyth_price(vec![0u8; 1].as_slice()).is_err());
}
#[test]
fn test_normal_values() -> SoliResult<()> {
let price = Price {
expo: 5,
agg: PriceInfo {
price: 42,
..empty_priceinfo!()
},
..empty_price!()
};
let price_vec = vec![price];
// use the C repr to mock pyth's format
let (_, bytes, _) = unsafe { price_vec.as_slice().align_to::<u8>() };
parse_pyth_price(bytes)?;
Ok(())
}
#[test]
fn test_serialize() -> SoliResult<()> {
let product_id_bytes = [21u8; 32];
let price_id_bytes = [222u8; 32];
println!("Hex product_id: {:02X?}", &product_id_bytes);
println!("Hex price_id: {:02X?}", &price_id_bytes);
let attestation: PriceAttestation = PriceAttestation {
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,
twap: P2WEma {
val: -42,
numer: 15,
denom: 37,
},
twac: P2WEma {
val: 42,
numer: 1111,
denom: 2222,
},
expo: -3,
status: P2WPriceStatus::Trading,
confidence_interval: 101,
corp_act: P2WCorpAction::NoCorpAct,
timestamp: 123456789i64,
};
println!("Regular: {:#?}", &attestation);
println!("Hex: {:#02X?}", &attestation);
println!("Hex Bytes: {:02X?}", attestation.serialize());
Ok(())
}
}

View File

@ -0,0 +1,139 @@
//! This module contains 1:1 (or close) copies of selected Pyth types
//! with quick and dirty enhancements.
use std::mem;
use pyth_client::{
CorpAction,
Ema,
PriceStatus,
PriceType,
};
/// 1:1 Copy of pyth_client::PriceType with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum P2WPriceType {
Unknown,
Price,
}
impl From<&PriceType> for P2WPriceType {
fn from(pt: &PriceType) -> Self {
match pt {
PriceType::Unknown => Self::Unknown,
PriceType::Price => Self::Price,
}
}
}
impl Default for P2WPriceType {
fn default() -> Self {
Self::Price
}
}
/// 1:1 Copy of pyth_client::PriceStatus with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum P2WPriceStatus {
Unknown,
Trading,
Halted,
Auction,
}
impl From<&PriceStatus> for P2WPriceStatus {
fn from(ps: &PriceStatus) -> Self {
match ps {
PriceStatus::Unknown => Self::Unknown,
PriceStatus::Trading => Self::Trading,
PriceStatus::Halted => Self::Halted,
PriceStatus::Auction => Self::Auction,
}
}
}
impl Default for P2WPriceStatus {
fn default() -> Self {
Self::Trading
}
}
/// 1:1 Copy of pyth_client::CorpAction with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum P2WCorpAction {
NoCorpAct,
}
impl Default for P2WCorpAction {
fn default() -> Self {
Self::NoCorpAct
}
}
impl From<&CorpAction> for P2WCorpAction {
fn from(ca: &CorpAction) -> Self {
match ca {
CorpAction::NoCorpAct => P2WCorpAction::NoCorpAct,
}
}
}
/// 1:1 Copy of pyth_client::Ema with all-pub fields.
#[derive(Clone, Default, Debug, Eq, PartialEq)]
#[repr(C)]
pub struct P2WEma {
pub val: i64,
pub numer: i64,
pub denom: i64,
}
/// CAUTION: This impl may panic and requires an unsafe cast
impl From<&Ema> for P2WEma {
fn from(ema: &Ema) -> Self {
let our_size = mem::size_of::<P2WEma>();
let upstream_size = mem::size_of::<Ema>();
if our_size == upstream_size {
unsafe { std::mem::transmute_copy(ema) }
} else {
dbg!(our_size);
dbg!(upstream_size);
// Because of private upstream fields it's impossible to
// complain about type-level changes at compile-time
panic!("P2WEma sizeof mismatch")
}
}
}
/// CAUTION: This impl may panic and requires an unsafe cast
impl Into<Ema> for &P2WEma {
fn into(self) -> Ema {
let our_size = mem::size_of::<P2WEma>();
let upstream_size = mem::size_of::<Ema>();
if our_size == upstream_size {
unsafe { std::mem::transmute_copy(self) }
} else {
dbg!(our_size);
dbg!(upstream_size);
// Because of private upstream fields it's impossible to
// complain about type-level changes at compile-time
panic!("P2WEma sizeof mismatch")
}
}
}
impl P2WEma {
pub fn serialize(&self) -> Vec<u8> {
let mut v = vec![];
// val
v.extend(&self.val.to_be_bytes()[..]);
// numer
v.extend(&self.numer.to_be_bytes()[..]);
// denom
v.extend(&self.denom.to_be_bytes()[..]);
v
}
}

View File

@ -72,10 +72,12 @@ pub use crate::{
peel::Peel,
persist::Persist,
seeded::{
invoke_seeded,
AccountOwner,
AccountSize,
Creatable,
Owned,
Seeded,
},
},
types::*,

View File

@ -118,7 +118,7 @@ pub fn derive_from_accounts(input: TokenStream) -> TokenStream {
}
impl #combined_impl_g solitaire::Peel<'a, 'b, 'c> for #name #type_g {
fn peel<I>(ctx: &'c mut Context<'a, 'b, 'c, I>) -> solitaire::Result<Self> where Self: Sized {
fn peel<I>(ctx: &'c mut solitaire::Context<'a, 'b, 'c, I>) -> solitaire::Result<Self> where Self: Sized {
let v: #name #type_g = FromAccounts::from(ctx.this, ctx.iter, ctx.data)?;
Ok(v)
}