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:
parent
3eae629a1b
commit
5dbd3ea722
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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![]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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(×tamp.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(())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -72,10 +72,12 @@ pub use crate::{
|
|||
peel::Peel,
|
||||
persist::Persist,
|
||||
seeded::{
|
||||
invoke_seeded,
|
||||
AccountOwner,
|
||||
AccountSize,
|
||||
Creatable,
|
||||
Owned,
|
||||
Seeded,
|
||||
},
|
||||
},
|
||||
types::*,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue