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",
"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",
]

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
@ -90,7 +109,7 @@ impl PriceAttestation {
confidence_interval,
status,
corp_act,
timestamp
timestamp,
} = self;
// magic
@ -137,6 +156,122 @@ impl PriceAttestation {
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);
@ -325,7 +460,10 @@ mod tests {
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(())
}
}

View File

@ -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,
@ -61,6 +64,7 @@ impl Default for P2WPriceStatus {
/// 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 })
}
}

View File

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