pyth2wormhole: implement deserialization, print sequence

Change-Id: I39308ad6431df52f35a0496e71836497561640c5
This commit is contained in:
Stan Drozd 2021-10-04 15:49:43 +02:00 committed by Stanislaw Drozd
parent 92b75555b1
commit a8465ef791
8 changed files with 257 additions and 39 deletions

View File

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

View File

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

View File

@ -1,8 +1,14 @@
pub mod cli; pub mod cli;
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{
BorshDeserialize,
BorshSerialize,
};
use clap::Clap; use clap::Clap;
use log::{LevelFilter, error}; use log::{
warn,
LevelFilter,
};
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_program::{ use solana_program::{
hash::Hash, hash::Hash,
@ -22,6 +28,7 @@ use solana_sdk::{
signature::read_keypair_file, signature::read_keypair_file,
transaction::Transaction, transaction::Transaction,
}; };
use solana_transaction_status::UiTransactionEncoding;
use solitaire::{ use solitaire::{
processors::seeded::Seeded, processors::seeded::Seeded,
AccountState, AccountState,
@ -52,6 +59,7 @@ use bridge::{
}; };
use pyth2wormhole::{ use pyth2wormhole::{
attest::P2WEmitter,
config::P2WConfigAccount, config::P2WConfigAccount,
initialize::InitializeAccounts, initialize::InitializeAccounts,
set_config::SetConfigAccounts, set_config::SetConfigAccounts,
@ -62,6 +70,8 @@ use pyth2wormhole::{
pub type ErrBox = Box<dyn std::error::Error>; pub type ErrBox = Box<dyn std::error::Error>;
pub const SEQNO_PREFIX: &'static str = "Program log: Sequence: ";
fn main() -> Result<(), ErrBox> { fn main() -> Result<(), ErrBox> {
let cli = Cli::parse(); let cli = Cli::parse();
init_logging(cli.log_level); init_logging(cli.log_level);
@ -87,7 +97,7 @@ fn main() -> Result<(), ErrBox> {
recent_blockhash, recent_blockhash,
)?, )?,
Action::SetConfig { Action::SetConfig {
owner, ref owner,
new_owner_addr, new_owner_addr,
new_wh_prog, new_wh_prog,
new_pyth_owner_addr, 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(()) Ok(())
} }
@ -203,17 +229,19 @@ fn handle_attest(
nonce: u32, nonce: u32,
recent_blockhash: Hash, recent_blockhash: Hash,
) -> Result<Transaction, ErrBox> { ) -> Result<Transaction, ErrBox> {
let emitter_keypair = Keypair::new();
let message_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 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 // Derive dynamic seeded accounts
let seq_addr = Sequence::key( let seq_addr = Sequence::key(
&SequenceDerivationData { &SequenceDerivationData {
emitter_key: &emitter_keypair.pubkey(), emitter_key: &emitter_addr,
}, },
&config.wh_prog, &config.wh_prog,
); );
@ -225,10 +253,7 @@ fn handle_attest(
// system_program // system_program
AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(system_program::id(), false),
// config // config
AccountMeta::new_readonly( AccountMeta::new_readonly(p2w_config_addr, false),
p2w_config_addr,
false,
),
// pyth_product // pyth_product
AccountMeta::new_readonly(product_addr, false), AccountMeta::new_readonly(product_addr, false),
// pyth_price // pyth_price
@ -245,7 +270,7 @@ fn handle_attest(
// wh_message // wh_message
AccountMeta::new(message_keypair.pubkey(), true), AccountMeta::new(message_keypair.pubkey(), true),
// wh_emitter // wh_emitter
AccountMeta::new_readonly(emitter_keypair.pubkey(), true), AccountMeta::new_readonly(emitter_addr, false),
// wh_sequence // wh_sequence
AccountMeta::new(seq_addr, false), AccountMeta::new(seq_addr, false),
// wh_fee_collector // 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); let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
// Signers that use off-chain keypairs // 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>>( let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
&[ix], &[ix],

View File

@ -13,7 +13,7 @@ default = ["bridge/no-entrypoint"]
client = ["solitaire/client", "solitaire-client", "no-entrypoint"] client = ["solitaire/client", "solitaire-client", "no-entrypoint"]
trace = ["solitaire/trace", "bridge/trace"] trace = ["solitaire/trace", "bridge/trace"]
no-entrypoint = [] no-entrypoint = []
wasm = ["no-entrypoint", "wasm-bindgen"] wasm = ["no-entrypoint", "wasm-bindgen", "serde", "serde_derive", "serde_json"]
[dependencies] [dependencies]
bridge = {path = "../../bridge/program"} bridge = {path = "../../bridge/program"}
@ -24,4 +24,8 @@ solana-program = "=1.7.0"
borsh = "0.8.1" borsh = "0.8.1"
# NOTE: We're following bleeding edge to encounter format changes more easily # 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"} pyth-client = {git = "https://github.com/pyth-network/pyth-client-rs", branch = "v2"}
# Crates needed for easier wasm data passing
wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true} 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}

View File

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

View File

@ -1,6 +1,13 @@
pub mod pyth_extensions; pub mod pyth_extensions;
use std::mem; use std::{
convert::{
TryFrom,
TryInto,
},
io::Read,
mem,
};
use borsh::BorshSerialize; use borsh::BorshSerialize;
use pyth_client::{ use pyth_client::{
@ -11,9 +18,14 @@ use pyth_client::{
PriceStatus, PriceStatus,
PriceType, PriceType,
}; };
use solana_program::{clock::UnixTimestamp, program_error::ProgramError, pubkey::Pubkey}; use solana_program::{
clock::UnixTimestamp,
program_error::ProgramError,
pubkey::Pubkey,
};
use solitaire::{ use solitaire::{
trace, trace,
ErrBox,
Result as SoliResult, Result as SoliResult,
SolitaireError, SolitaireError,
}; };
@ -33,6 +45,8 @@ pub const P2W_MAGIC: &'static [u8] = b"P2WH";
/// Format version used and understood by this codebase /// Format version used and understood by this codebase
pub const P2W_FORMAT_VERSION: u16 = 1; pub const P2W_FORMAT_VERSION: u16 = 1;
pub const PUBKEY_LEN: usize = 32;
/// Decides the format of following bytes /// Decides the format of following bytes
#[repr(u8)] #[repr(u8)]
pub enum PayloadId { pub enum PayloadId {
@ -42,6 +56,7 @@ pub enum PayloadId {
// On-chain data types // On-chain data types
#[derive(Clone, Default, Debug, Eq, PartialEq)] #[derive(Clone, Default, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
pub struct PriceAttestation { pub struct PriceAttestation {
pub product_id: Pubkey, pub product_id: Pubkey,
pub price_id: Pubkey, pub price_id: Pubkey,
@ -57,7 +72,11 @@ pub struct PriceAttestation {
} }
impl 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)?; let price = parse_pyth_price(value)?;
Ok(PriceAttestation { Ok(PriceAttestation {
@ -90,7 +109,7 @@ impl PriceAttestation {
confidence_interval, confidence_interval,
status, status,
corp_act, corp_act,
timestamp timestamp,
} = self; } = self;
// magic // magic
@ -137,6 +156,122 @@ impl PriceAttestation {
buf 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. /// Deserializes Price from raw bytes, sanity-check.
@ -296,7 +431,7 @@ mod tests {
} }
#[test] #[test]
fn test_serialize() -> SoliResult<()> { fn test_serialize_deserialize() -> Result<(), ErrBox> {
let product_id_bytes = [21u8; 32]; let product_id_bytes = [21u8; 32];
let price_id_bytes = [222u8; 32]; let price_id_bytes = [222u8; 32];
println!("Hex product_id: {:02X?}", &product_id_bytes); println!("Hex product_id: {:02X?}", &product_id_bytes);
@ -325,7 +460,10 @@ mod tests {
println!("Regular: {:#?}", &attestation); println!("Regular: {:#?}", &attestation);
println!("Hex: {:#02X?}", &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(()) Ok(())
} }
} }

View File

@ -1,7 +1,7 @@
//! This module contains 1:1 (or close) copies of selected Pyth types //! This module contains 1:1 (or close) copies of selected Pyth types
//! with quick and dirty enhancements. //! with quick and dirty enhancements.
use std::mem; use std::{convert::TryInto, io::Read, mem};
use pyth_client::{ use pyth_client::{
CorpAction, CorpAction,
@ -9,9 +9,11 @@ use pyth_client::{
PriceStatus, PriceStatus,
PriceType, PriceType,
}; };
use solitaire::ErrBox;
/// 1:1 Copy of pyth_client::PriceType with derived additional traits. /// 1:1 Copy of pyth_client::PriceType with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
#[repr(u8)] #[repr(u8)]
pub enum P2WPriceType { pub enum P2WPriceType {
Unknown, Unknown,
@ -35,6 +37,7 @@ impl Default for P2WPriceType {
/// 1:1 Copy of pyth_client::PriceStatus with derived additional traits. /// 1:1 Copy of pyth_client::PriceStatus with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
pub enum P2WPriceStatus { pub enum P2WPriceStatus {
Unknown, Unknown,
Trading, Trading,
@ -61,6 +64,7 @@ impl Default for P2WPriceStatus {
/// 1:1 Copy of pyth_client::CorpAction with derived additional traits. /// 1:1 Copy of pyth_client::CorpAction with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
pub enum P2WCorpAction { pub enum P2WCorpAction {
NoCorpAct, NoCorpAct,
} }
@ -81,6 +85,7 @@ impl From<&CorpAction> for P2WCorpAction {
/// 1:1 Copy of pyth_client::Ema with all-pub fields. /// 1:1 Copy of pyth_client::Ema with all-pub fields.
#[derive(Clone, Default, Debug, Eq, PartialEq)] #[derive(Clone, Default, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
#[repr(C)] #[repr(C)]
pub struct P2WEma { pub struct P2WEma {
pub val: i64, pub val: i64,
@ -136,4 +141,20 @@ impl P2WEma {
v 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 })
}
} }

View File

@ -1,8 +1,29 @@
use solitaire::Seeded;
use solana_program::pubkey::Pubkey;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use std::str::FromStr;
use crate::{attest::P2WEmitter, types::PriceAttestation};
/// sanity check for wasm compilation, TODO(sdrozd): remove after /// sanity check for wasm compilation, TODO(sdrozd): remove after
/// meaningful endpoints are added /// meaningful endpoints are added
#[wasm_bindgen] #[wasm_bindgen]
pub fn hello_p2w() -> String { pub fn hello_p2w() -> String {
"Ciao mondo!".to_owned() "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()
}