Bladeren bron

pyth2wormhole: implement deserialization, print sequence

Change-Id: I39308ad6431df52f35a0496e71836497561640c5
Stan Drozd 4 jaren geleden
bovenliggende
commit
a8465ef791

+ 4 - 0
solana/pyth2wormhole/Cargo.lock

@@ -2067,6 +2067,9 @@ dependencies = [
  "bridge",
  "pyth-client",
  "rocksalt",
+ "serde",
+ "serde_derive",
+ "serde_json",
  "solana-program",
  "solitaire",
  "solitaire-client",
@@ -2087,6 +2090,7 @@ dependencies = [
  "solana-client",
  "solana-program",
  "solana-sdk",
+ "solana-transaction-status",
  "solitaire",
  "solitaire-client",
 ]

+ 1 - 0
solana/pyth2wormhole/client/Cargo.toml

@@ -19,5 +19,6 @@ shellexpand = "2.1.0"
 solana-client = "=1.7.0"
 solana-program = "=1.7.0"
 solana-sdk = "=1.7.0"
+solana-transaction-status = "=1.7.0"
 solitaire-client = {path = "../../solitaire/client"}
 solitaire = {path = "../../solitaire/program"}

+ 38 - 13
solana/pyth2wormhole/client/src/main.rs

@@ -1,8 +1,14 @@
 pub mod cli;
 
-use borsh::{BorshDeserialize, BorshSerialize};
+use borsh::{
+    BorshDeserialize,
+    BorshSerialize,
+};
 use clap::Clap;
-use log::{LevelFilter, error};
+use log::{
+    warn,
+    LevelFilter,
+};
 use solana_client::rpc_client::RpcClient;
 use solana_program::{
     hash::Hash,
@@ -22,6 +28,7 @@ use solana_sdk::{
     signature::read_keypair_file,
     transaction::Transaction,
 };
+use solana_transaction_status::UiTransactionEncoding;
 use solitaire::{
     processors::seeded::Seeded,
     AccountState,
@@ -52,6 +59,7 @@ use bridge::{
 };
 
 use pyth2wormhole::{
+    attest::P2WEmitter,
     config::P2WConfigAccount,
     initialize::InitializeAccounts,
     set_config::SetConfigAccounts,
@@ -62,6 +70,8 @@ use pyth2wormhole::{
 
 pub type ErrBox = Box<dyn std::error::Error>;
 
+pub const SEQNO_PREFIX: &'static str = "Program log: Sequence: ";
+
 fn main() -> Result<(), ErrBox> {
     let cli = Cli::parse();
     init_logging(cli.log_level);
@@ -87,7 +97,7 @@ fn main() -> Result<(), ErrBox> {
             recent_blockhash,
         )?,
         Action::SetConfig {
-            owner,
+            ref owner,
             new_owner_addr,
             new_wh_prog,
             new_pyth_owner_addr,
@@ -115,7 +125,23 @@ fn main() -> Result<(), ErrBox> {
         )?,
     };
 
-    rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
+    let sig = rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
+
+    // To complete attestation, retrieve sequence number from transaction logs
+    if let Action::Attest { .. } = cli.action {
+        let this_tx = rpc_client.get_transaction(&sig, UiTransactionEncoding::Json)?;
+
+        if let Some(logs) = this_tx.transaction.meta.and_then(|meta| meta.log_messages) {
+	    for log in logs {
+		if log.starts_with(SEQNO_PREFIX) {
+		    let seqno = log.replace(SEQNO_PREFIX, "");
+		    println!("Sequence number: {}", seqno);
+		}
+	    }
+        } else {
+            warn!("Could not get program logs for attestation");
+        }
+    }
 
     Ok(())
 }
@@ -203,17 +229,19 @@ fn handle_attest(
     nonce: u32,
     recent_blockhash: Hash,
 ) -> Result<Transaction, ErrBox> {
-    let emitter_keypair = Keypair::new();
     let message_keypair = Keypair::new();
 
+    let emitter_addr = P2WEmitter::key(None, &p2w_addr);
+
     let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr);
 
-    let config = Pyth2WormholeConfig::try_from_slice(rpc.get_account_data(&p2w_config_addr)?.as_slice())?;
+    let config =
+        Pyth2WormholeConfig::try_from_slice(rpc.get_account_data(&p2w_config_addr)?.as_slice())?;
 
     // Derive dynamic seeded accounts
     let seq_addr = Sequence::key(
         &SequenceDerivationData {
-            emitter_key: &emitter_keypair.pubkey(),
+            emitter_key: &emitter_addr,
         },
         &config.wh_prog,
     );
@@ -225,10 +253,7 @@ fn handle_attest(
         // system_program
         AccountMeta::new_readonly(system_program::id(), false),
         // config
-        AccountMeta::new_readonly(
-	    p2w_config_addr,
-            false,
-        ),
+        AccountMeta::new_readonly(p2w_config_addr, false),
         // pyth_product
         AccountMeta::new_readonly(product_addr, false),
         // pyth_price
@@ -245,7 +270,7 @@ fn handle_attest(
         // wh_message
         AccountMeta::new(message_keypair.pubkey(), true),
         // wh_emitter
-        AccountMeta::new_readonly(emitter_keypair.pubkey(), true),
+        AccountMeta::new_readonly(emitter_addr, false),
         // wh_sequence
         AccountMeta::new(seq_addr, false),
         // wh_fee_collector
@@ -264,7 +289,7 @@ fn handle_attest(
     let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
 
     // Signers that use off-chain keypairs
-    let signer_keypairs = vec![&payer, &message_keypair, &emitter_keypair];
+    let signer_keypairs = vec![&payer, &message_keypair];
 
     let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
         &[ix],

+ 6 - 2
solana/pyth2wormhole/program/Cargo.toml

@@ -13,7 +13,7 @@ default = ["bridge/no-entrypoint"]
 client = ["solitaire/client", "solitaire-client", "no-entrypoint"]
 trace = ["solitaire/trace", "bridge/trace"]
 no-entrypoint = []
-wasm = ["no-entrypoint", "wasm-bindgen"]
+wasm = ["no-entrypoint", "wasm-bindgen", "serde", "serde_derive", "serde_json"]
 
 [dependencies]
 bridge = {path = "../../bridge/program"}
@@ -24,4 +24,8 @@ 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"}
-wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true}
+# Crates needed for easier wasm data passing
+wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true}
+serde = { version = "1", optional = true}
+serde_derive = { version = "1", optional = true}
+serde_json = { version = "1", optional = true}

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

@@ -40,12 +40,15 @@ use solitaire::{
     Peel,
     Result as SoliResult,
     Seeded,
+    invoke_seeded,
     Signer,
     SolitaireError,
     Sysvar,
     ToInstruction,
 };
 
+pub type P2WEmitter<'b> = Derive<Info<'b>, "p2w-emitter">;
+
 #[derive(FromAccounts, ToInstruction)]
 pub struct Attest<'b> {
     // Payer also used for wormhole
@@ -67,7 +70,7 @@ pub struct Attest<'b> {
     pub wh_message: Signer<Mut<Info<'b>>>,
 
     /// Emitter of the VAA
-    pub wh_emitter: Info<'b>,
+    pub wh_emitter: P2WEmitter<'b>,
 
     /// Tracker for the emitter sequence
     pub wh_sequence: Mut<Info<'b>>,
@@ -163,7 +166,8 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
     );
 
     trace!("Before cross-call");
-    invoke(&ix, ctx.accounts)?;
+
+    invoke_seeded(&ix, ctx, &accs.wh_emitter, None)?;
 
     Ok(())
 }

+ 158 - 20
solana/pyth2wormhole/program/src/types/mod.rs

@@ -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(&timestamp.to_be_bytes()[..]);
+        // timestamp
+        buf.extend_from_slice(&timestamp.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(())
     }
 }

+ 23 - 2
solana/pyth2wormhole/program/src/types/pyth_extensions.rs

@@ -1,7 +1,7 @@
 //! This module contains 1:1 (or close) copies of selected Pyth types
 //! with quick and dirty enhancements.
 
-use std::mem;
+use std::{convert::TryInto, io::Read, mem};
 
 use pyth_client::{
     CorpAction,
@@ -9,9 +9,11 @@ use pyth_client::{
     PriceStatus,
     PriceType,
 };
+use solitaire::ErrBox;
 
 /// 1:1 Copy of pyth_client::PriceType with derived additional traits.
 #[derive(Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
 #[repr(u8)]
 pub enum P2WPriceType {
     Unknown,
@@ -35,6 +37,7 @@ impl Default for P2WPriceType {
 
 /// 1:1 Copy of pyth_client::PriceStatus with derived additional traits.
 #[derive(Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
 pub enum P2WPriceStatus {
     Unknown,
     Trading,
@@ -55,12 +58,13 @@ impl From<&PriceStatus> for P2WPriceStatus {
 
 impl Default for P2WPriceStatus {
     fn default() -> Self {
-	Self::Trading
+        Self::Trading
     }
 }
 
 /// 1:1 Copy of pyth_client::CorpAction with derived additional traits.
 #[derive(Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
 pub enum P2WCorpAction {
     NoCorpAct,
 }
@@ -81,6 +85,7 @@ impl From<&CorpAction> for P2WCorpAction {
 
 /// 1:1 Copy of pyth_client::Ema with all-pub fields.
 #[derive(Clone, Default, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
 #[repr(C)]
 pub struct P2WEma {
     pub val: i64,
@@ -136,4 +141,20 @@ impl P2WEma {
 
         v
     }
+
+    pub fn deserialize(mut bytes: impl Read) -> Result<Self, ErrBox> {
+        let mut val_vec = vec![0u8; mem::size_of::<i64>()];
+        bytes.read_exact(val_vec.as_mut_slice())?;
+        let val = i64::from_be_bytes(val_vec.as_slice().try_into()?);
+	
+        let mut numer_vec = vec![0u8; mem::size_of::<i64>()];
+        bytes.read_exact(numer_vec.as_mut_slice())?;
+        let numer = i64::from_be_bytes(numer_vec.as_slice().try_into()?);
+
+        let mut denom_vec = vec![0u8; mem::size_of::<i64>()];
+        bytes.read_exact(denom_vec.as_mut_slice())?;
+        let denom = i64::from_be_bytes(denom_vec.as_slice().try_into()?);
+
+        Ok(Self { val, numer, denom })
+    }
 }

+ 21 - 0
solana/pyth2wormhole/program/src/wasm.rs

@@ -1,8 +1,29 @@
+use solitaire::Seeded;
+use solana_program::pubkey::Pubkey;
 use wasm_bindgen::prelude::*;
 
+use std::str::FromStr;
+
+use crate::{attest::P2WEmitter, types::PriceAttestation};
+
 /// sanity check for wasm compilation, TODO(sdrozd): remove after
 /// meaningful endpoints are added
 #[wasm_bindgen]
 pub fn hello_p2w() -> String {
     "Ciao mondo!".to_owned()
 }
+
+#[wasm_bindgen]
+pub fn get_emitter_address(program_id: String) -> Vec<u8> {
+    let program_id = Pubkey::from_str(program_id.as_str()).unwrap();
+    let emitter = P2WEmitter::key(None, &program_id);
+
+    emitter.to_bytes().to_vec()
+}
+
+#[wasm_bindgen]
+pub fn parse_attestation(bytes: Vec<u8>) -> JsValue {
+    let a = PriceAttestation::deserialize(bytes.as_slice()).unwrap();
+    
+    JsValue::from_serde(&a).unwrap()
+}