pyth2wormhole-client attestation call

Change-Id: I407734b5fcf919f437a0e0a32d2b35e7ec50b999
This commit is contained in:
Stan Drozd 2021-08-02 14:52:24 +02:00
parent 5dbd3ea722
commit 7d6db92c35
7 changed files with 192 additions and 33 deletions

View File

@ -255,6 +255,7 @@ dependencies = [
"sha3", "sha3",
"solana-program", "solana-program",
"solitaire", "solitaire",
"solitaire-client",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -2078,6 +2079,7 @@ name = "pyth2wormhole-client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"borsh", "borsh",
"bridge",
"clap 3.0.0-beta.2", "clap 3.0.0-beta.2",
"env_logger", "env_logger",
"log", "log",

View File

@ -6,13 +6,14 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["pyth2wormhole/client"] default = ["pyth2wormhole/client", "bridge/client"]
[dependencies] [dependencies]
borsh = "0.8.1" borsh = "0.8.1"
clap = "3.0.0-beta.2" # This beta assimilates structopt into clap clap = "3.0.0-beta.2" # This beta assimilates structopt into clap
env_logger = "0.8.4" env_logger = "0.8.4"
log = "0.4.14" log = "0.4.14"
bridge = {path = "../../bridge/program"}
pyth2wormhole = {path = "../program"} pyth2wormhole = {path = "../program"}
shellexpand = "2.1.0" shellexpand = "2.1.0"
solana-client = "=1.7.0" solana-client = "=1.7.0"

View File

@ -24,7 +24,11 @@ pub struct Cli {
pub payer: String, pub payer: String,
#[clap(long, default_value = "http://localhost:8899")] #[clap(long, default_value = "http://localhost:8899")]
pub rpc_url: String, pub rpc_url: String,
#[clap(long)]
pub p2w_addr: Pubkey, pub p2w_addr: Pubkey,
/// The bridge program account
#[clap(long = "wh-prog")]
pub wh_prog: Pubkey,
#[clap(subcommand)] #[clap(subcommand)]
pub action: Action, pub action: Action,
} }
@ -35,16 +39,18 @@ pub enum Action {
Init { Init {
#[clap(long = "owner")] #[clap(long = "owner")]
new_owner_addr: Pubkey, new_owner_addr: Pubkey,
#[clap(long = "wormhole")] #[clap(long = "pyth-owner")]
wormhole_addr: Pubkey,
#[clap(long = "pyth")]
pyth_owner_addr: Pubkey, pyth_owner_addr: Pubkey,
}, },
#[clap( #[clap(
about = "Use an existing pyth2wormhole program to forward product price information to another chain" about = "Use an existing pyth2wormhole program to attest product price information to another chain"
)] )]
Forward { Attest {
#[clap(long = "product")] #[clap(long = "product")]
product_addr: Pubkey, product_addr: Pubkey,
#[clap(long = "price")]
price_addr: Pubkey,
#[clap(long)]
nonce: u32,
}, },
} }

View File

@ -4,16 +4,60 @@ use borsh::BorshSerialize;
use clap::Clap; use clap::Clap;
use log::LevelFilter; use log::LevelFilter;
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_program::{hash::Hash, pubkey::Pubkey}; use solana_program::{
use solana_sdk::{ hash::Hash,
commitment_config::CommitmentConfig, signature::read_keypair_file, transaction::Transaction, instruction::{
AccountMeta,
Instruction,
},
pubkey::Pubkey,
system_program,
sysvar::{
clock,
rent,
},
};
use solana_sdk::{
commitment_config::CommitmentConfig,
signature::read_keypair_file,
transaction::Transaction,
};
use solitaire::{
processors::seeded::Seeded,
AccountState,
Derive,
Info,
};
use solitaire_client::{
AccEntry,
Keypair,
SolSigner,
ToInstruction,
}; };
use solitaire::{AccountState, processors::seeded::Seeded};
use solitaire_client::{AccEntry, Keypair, SolSigner, ToInstruction};
use cli::{Action, Cli}; use cli::{
Action,
Cli,
};
use pyth2wormhole::{config::P2WConfigAccount, initialize::InitializeAccounts, Pyth2WormholeConfig}; use bridge::{
accounts::{
Bridge,
FeeCollector,
Sequence,
SequenceDerivationData,
},
types::ConsistencyLevel,
CHAIN_ID_SOLANA,
};
use pyth2wormhole::{
config::P2WConfigAccount,
initialize::InitializeAccounts,
types::PriceAttestation,
AttestData,
Pyth2WormholeConfig,
};
pub type ErrBox = Box<dyn std::error::Error>; pub type ErrBox = Box<dyn std::error::Error>;
@ -22,7 +66,7 @@ fn main() -> Result<(), ErrBox> {
init_logging(cli.log_level); init_logging(cli.log_level);
let payer = read_keypair_file(&*shellexpand::tilde(&cli.payer))?; let payer = read_keypair_file(&*shellexpand::tilde(&cli.payer))?;
let rpc_client = RpcClient::new_with_commitment(cli.rpc_url, CommitmentConfig::processed()); let rpc_client = RpcClient::new_with_commitment(cli.rpc_url, CommitmentConfig::finalized());
let p2w_addr = cli.p2w_addr; let p2w_addr = cli.p2w_addr;
@ -31,19 +75,28 @@ fn main() -> Result<(), ErrBox> {
let tx = match cli.action { let tx = match cli.action {
Action::Init { Action::Init {
new_owner_addr, new_owner_addr,
wormhole_addr,
pyth_owner_addr, pyth_owner_addr,
} => handle_init( } => handle_init(
payer, payer,
p2w_addr, p2w_addr,
new_owner_addr, new_owner_addr,
wormhole_addr, cli.wh_prog,
pyth_owner_addr, pyth_owner_addr,
recent_blockhash, recent_blockhash,
)?, )?,
Action::Forward { product_addr: _ } => { Action::Attest {
todo!() product_addr,
} price_addr,
nonce,
} => handle_attest(
&rpc_client,
payer,
p2w_addr,
product_addr,
price_addr,
nonce,
recent_blockhash,
)?,
}; };
rpc_client.send_and_confirm_transaction_with_spinner(&tx)?; rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
@ -55,7 +108,7 @@ fn handle_init(
payer: Keypair, payer: Keypair,
p2w_addr: Pubkey, p2w_addr: Pubkey,
new_owner_addr: Pubkey, new_owner_addr: Pubkey,
wormhole_addr: Pubkey, wh_prog: Pubkey,
pyth_owner_addr: Pubkey, pyth_owner_addr: Pubkey,
recent_blockhash: Hash, recent_blockhash: Hash,
) -> Result<Transaction, ErrBox> { ) -> Result<Transaction, ErrBox> {
@ -65,17 +118,17 @@ fn handle_init(
let accs = InitializeAccounts { let accs = InitializeAccounts {
payer: Signer(payer), payer: Signer(payer),
new_config: Unprivileged(<P2WConfigAccount<'_, {AccountState::Uninitialized}>>::key(None, &p2w_addr)), new_config: Derived(p2w_addr),
}; };
let config = Pyth2WormholeConfig { let config = Pyth2WormholeConfig {
owner: new_owner_addr, owner: new_owner_addr,
wormhole_program_addr: wormhole_addr, wh_prog: wh_prog,
pyth_owner: pyth_owner_addr, pyth_owner: pyth_owner_addr,
}; };
let ix_data = pyth2wormhole::instruction::Instruction::Initialize(config); let ix_data = (pyth2wormhole::instruction::Instruction::Initialize, config);
let (ix, signers) = accs.gen_client_ix(p2w_addr, ix_data.try_to_vec()?.as_slice())?; let (ix, signers) = accs.to_ix(p2w_addr, ix_data.try_to_vec()?.as_slice())?;
let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>( let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
&[ix], &[ix],
@ -86,6 +139,86 @@ fn handle_init(
Ok(tx_signed) Ok(tx_signed)
} }
fn handle_attest(
rpc: &RpcClient, // Needed for reading Pyth account data
payer: Keypair,
p2w_addr: Pubkey,
product_addr: Pubkey,
price_addr: Pubkey,
nonce: u32,
recent_blockhash: Hash,
) -> Result<Transaction, ErrBox> {
let emitter_keypair = Keypair::new();
let message_keypair = Keypair::new();
// Derive dynamic seeded accounts
let seq_addr = Sequence::key(
&SequenceDerivationData {
emitter_key: &emitter_keypair.pubkey(),
},
&wh_prog,
);
// Arrange Attest accounts
let acc_metas = vec![
// payer
AccountMeta::new(payer.pubkey(), true),
// system_program
AccountMeta::new_readonly(system_program::id(), false),
// config
AccountMeta::new_readonly(
P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr),
false,
),
// pyth_product
AccountMeta::new_readonly(product_addr, false),
// pyth_price
AccountMeta::new_readonly(price_addr, false),
// clock
AccountMeta::new_readonly(clock::id(), false),
// wh_prog
AccountMeta::new_readonly(wh_prog, false),
// wh_bridge
AccountMeta::new(
Bridge::<{ AccountState::Initialized }>::key(None, &wh_prog),
false,
),
// wh_message
AccountMeta::new(message_keypair.pubkey(), true),
// wh_emitter
AccountMeta::new_readonly(emitter_keypair.pubkey(), true),
// wh_sequence
AccountMeta::new(seq_addr, false),
// wh_fee_collector
AccountMeta::new(FeeCollector::<'_>::key(None, &wh_prog), false),
AccountMeta::new_readonly(rent::id(), false),
];
let ix_data = (
pyth2wormhole::instruction::Instruction::Attest,
AttestData {
nonce,
consistency_level: ConsistencyLevel::Finalized,
},
);
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 tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
&[ix],
Some(&payer.pubkey()),
&signer_keypairs,
recent_blockhash,
);
Ok(tx_signed)
}
fn init_logging(verbosity: u32) { fn init_logging(verbosity: u32) {
use LevelFilter::*; use LevelFilter::*;
let filter = match verbosity { let filter = match verbosity {

View File

@ -54,7 +54,6 @@ pub struct Attest<'b> {
pub payer: Mut<Signer<Info<'b>>>, pub payer: Mut<Signer<Info<'b>>>,
pub system_program: Info<'b>, pub system_program: Info<'b>,
pub config: P2WConfigAccount<'b, { AccountState::Initialized }>, pub config: P2WConfigAccount<'b, { AccountState::Initialized }>,
pub wormhole_program: Info<'b>,
pub pyth_product: Info<'b>, pub pyth_product: Info<'b>,
pub pyth_price: Info<'b>, pub pyth_price: Info<'b>,
pub clock: Sysvar<'b, Clock>, pub clock: Sysvar<'b, Clock>,

View File

@ -8,10 +8,11 @@ use crate::config::{P2WConfigAccount, Pyth2WormholeConfig};
#[derive(FromAccounts, ToInstruction)] #[derive(FromAccounts, ToInstruction)]
pub struct SetConfig<'b> { pub struct SetConfig<'b> {
/// New config to apply to the program /// Current config used by the program
pub config: P2WConfigAccount<'b, { AccountState::Initialized }>, pub config: P2WConfigAccount<'b, { AccountState::Initialized }>,
/// Current owner authority of the program /// Current owner authority of the program
pub current_owner: Signer<Info<'b>>, pub current_owner: Signer<Info<'b>>,
/// Payer account for updating the account data
pub payer: Signer<Info<'b>>, pub payer: Signer<Info<'b>>,
} }

View File

@ -21,18 +21,20 @@ pub use solana_sdk::{
use borsh::BorshSerialize; use borsh::BorshSerialize;
use solitaire::{
AccountState,
Info,
Sysvar,
};
pub use solitaire::{ pub use solitaire::{
processors::seeded::Seeded,
Data, Data,
Derive, Derive,
Keyed, Keyed,
Owned, Owned,
Signer, Signer,
}; };
use solitaire::{
AccountState,
Info,
Mut,
Sysvar,
};
type StdResult<T, E> = std::result::Result<T, E>; type StdResult<T, E> = std::result::Result<T, E>;
@ -104,12 +106,12 @@ impl<'a, 'b: 'a, T, const Seed: &'static str> Wrap for Derive<T, Seed> {
fn wrap(a: &AccEntry) -> StdResult<Vec<AccountMeta>, ErrBox> { fn wrap(a: &AccEntry) -> StdResult<Vec<AccountMeta>, ErrBox> {
match a { match a {
AccEntry::Derived(program_id) => { AccEntry::Derived(program_id) => {
let (k, extra_seed) = Pubkey::find_program_address(&[Seed.as_bytes()], &program_id); let k = Self::key(None, program_id);
Ok(vec![AccountMeta::new(k, false)]) Ok(vec![AccountMeta::new(k, false)])
} }
AccEntry::DerivedRO(program_id) => { AccEntry::DerivedRO(program_id) => {
let (k, extra_seed) = Pubkey::find_program_address(&[Seed.as_bytes()], &program_id); let k = Self::key(None, program_id);
Ok(vec![AccountMeta::new_readonly(k, false)]) Ok(vec![AccountMeta::new_readonly(k, false)])
} }
@ -187,6 +189,21 @@ impl<'b> Wrap for Info<'b> {
} }
} }
impl<T: Wrap> Wrap for Mut<T> {
fn wrap(a: &AccEntry) -> StdResult<Vec<AccountMeta>, ErrBox> {
match a {
AccEntry::Unprivileged(_) | AccEntry::Signer(_) | AccEntry::Derived(_) => {
Ok(T::wrap(a)?)
}
_other => Err(format!(
"{} must be passed as Unprivileged, Signer or Derived (Must be mutable on-chain)",
std::any::type_name::<Self>()
)
.into()),
}
}
}
/// Trait used on client side to easily validate a program accounts + ix_data for a bare Solana call /// Trait used on client side to easily validate a program accounts + ix_data for a bare Solana call
pub trait ToInstruction { pub trait ToInstruction {
fn to_ix( fn to_ix(