Переглянути джерело

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
Stan Drozd 4 роки тому
батько
коміт
5dbd3ea722

+ 2 - 2
solana/bridge/program/src/types.rs

@@ -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,

+ 2 - 2
solana/pyth2wormhole/Cargo.lock

@@ -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"

+ 3 - 1
solana/pyth2wormhole/program/Cargo.toml

@@ -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"}

+ 172 - 0
solana/pyth2wormhole/program/src/attest.rs

@@ -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(())
+}

+ 2 - 6
solana/pyth2wormhole/program/src/config.rs

@@ -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,
 }

+ 0 - 73
solana/pyth2wormhole/program/src/forward.rs

@@ -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(())
-}

+ 3 - 10
solana/pyth2wormhole/program/src/initialize.rs

@@ -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![]
     }

+ 16 - 8
solana/pyth2wormhole/program/src/lib.rs

@@ -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 forward::{forward_price, Forward, ForwardData};
-pub use initialize::{initialize, Initialize};
-pub use set_config::{set_config, SetConfig};
+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 - 1
solana/pyth2wormhole/program/src/set_config.rs

@@ -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,
 };
 

+ 0 - 165
solana/pyth2wormhole/program/src/types.rs

@@ -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(())
-    }
-}

+ 331 - 0
solana/pyth2wormhole/program/src/types/mod.rs

@@ -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(())
+    }
+}

+ 139 - 0
solana/pyth2wormhole/program/src/types/pyth_extensions.rs

@@ -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
+    }
+}

+ 2 - 0
solana/solitaire/program/src/lib.rs

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

+ 1 - 1
solana/solitaire/rocksalt/src/lib.rs

@@ -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)
             }