diff --git a/solana/pyth2wormhole/Cargo.lock b/solana/pyth2wormhole/Cargo.lock index 67482c5f..8a53dca7 100644 --- a/solana/pyth2wormhole/Cargo.lock +++ b/solana/pyth2wormhole/Cargo.lock @@ -255,6 +255,7 @@ dependencies = [ "sha3", "solana-program", "solitaire", + "solitaire-client", "wasm-bindgen", ] @@ -2078,6 +2079,7 @@ name = "pyth2wormhole-client" version = "0.1.0" dependencies = [ "borsh", + "bridge", "clap 3.0.0-beta.2", "env_logger", "log", diff --git a/solana/pyth2wormhole/client/Cargo.toml b/solana/pyth2wormhole/client/Cargo.toml index 906b0c11..004a57de 100644 --- a/solana/pyth2wormhole/client/Cargo.toml +++ b/solana/pyth2wormhole/client/Cargo.toml @@ -6,13 +6,14 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["pyth2wormhole/client"] +default = ["pyth2wormhole/client", "bridge/client"] [dependencies] borsh = "0.8.1" clap = "3.0.0-beta.2" # This beta assimilates structopt into clap env_logger = "0.8.4" log = "0.4.14" +bridge = {path = "../../bridge/program"} pyth2wormhole = {path = "../program"} shellexpand = "2.1.0" solana-client = "=1.7.0" diff --git a/solana/pyth2wormhole/client/src/cli.rs b/solana/pyth2wormhole/client/src/cli.rs index 4f755110..efcce7f3 100644 --- a/solana/pyth2wormhole/client/src/cli.rs +++ b/solana/pyth2wormhole/client/src/cli.rs @@ -24,7 +24,11 @@ pub struct Cli { pub payer: String, #[clap(long, default_value = "http://localhost:8899")] pub rpc_url: String, + #[clap(long)] pub p2w_addr: Pubkey, + /// The bridge program account + #[clap(long = "wh-prog")] + pub wh_prog: Pubkey, #[clap(subcommand)] pub action: Action, } @@ -35,16 +39,18 @@ pub enum Action { Init { #[clap(long = "owner")] new_owner_addr: Pubkey, - #[clap(long = "wormhole")] - wormhole_addr: Pubkey, - #[clap(long = "pyth")] + #[clap(long = "pyth-owner")] pyth_owner_addr: Pubkey, }, #[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")] product_addr: Pubkey, + #[clap(long = "price")] + price_addr: Pubkey, + #[clap(long)] + nonce: u32, }, } diff --git a/solana/pyth2wormhole/client/src/main.rs b/solana/pyth2wormhole/client/src/main.rs index f169e258..b0455295 100644 --- a/solana/pyth2wormhole/client/src/main.rs +++ b/solana/pyth2wormhole/client/src/main.rs @@ -4,16 +4,60 @@ use borsh::BorshSerialize; use clap::Clap; use log::LevelFilter; use solana_client::rpc_client::RpcClient; -use solana_program::{hash::Hash, pubkey::Pubkey}; -use solana_sdk::{ - commitment_config::CommitmentConfig, signature::read_keypair_file, transaction::Transaction, +use solana_program::{ + hash::Hash, + 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; @@ -22,7 +66,7 @@ fn main() -> Result<(), ErrBox> { init_logging(cli.log_level); 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; @@ -31,19 +75,28 @@ fn main() -> Result<(), ErrBox> { let tx = match cli.action { Action::Init { new_owner_addr, - wormhole_addr, pyth_owner_addr, } => handle_init( payer, p2w_addr, new_owner_addr, - wormhole_addr, + cli.wh_prog, pyth_owner_addr, recent_blockhash, )?, - Action::Forward { product_addr: _ } => { - todo!() - } + Action::Attest { + 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)?; @@ -55,7 +108,7 @@ fn handle_init( payer: Keypair, p2w_addr: Pubkey, new_owner_addr: Pubkey, - wormhole_addr: Pubkey, + wh_prog: Pubkey, pyth_owner_addr: Pubkey, recent_blockhash: Hash, ) -> Result { @@ -65,17 +118,17 @@ fn handle_init( let accs = InitializeAccounts { payer: Signer(payer), - new_config: Unprivileged(>::key(None, &p2w_addr)), + new_config: Derived(p2w_addr), }; let config = Pyth2WormholeConfig { owner: new_owner_addr, - wormhole_program_addr: wormhole_addr, + wh_prog: wh_prog, 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::>( &[ix], @@ -86,6 +139,86 @@ fn handle_init( 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 { + + 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::>( + &[ix], + Some(&payer.pubkey()), + &signer_keypairs, + recent_blockhash, + ); + Ok(tx_signed) +} + fn init_logging(verbosity: u32) { use LevelFilter::*; let filter = match verbosity { diff --git a/solana/pyth2wormhole/program/src/attest.rs b/solana/pyth2wormhole/program/src/attest.rs index e27296bc..bed644e8 100644 --- a/solana/pyth2wormhole/program/src/attest.rs +++ b/solana/pyth2wormhole/program/src/attest.rs @@ -54,7 +54,6 @@ pub struct Attest<'b> { pub payer: Mut>>, 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>, diff --git a/solana/pyth2wormhole/program/src/set_config.rs b/solana/pyth2wormhole/program/src/set_config.rs index 32642de7..cf903fa4 100644 --- a/solana/pyth2wormhole/program/src/set_config.rs +++ b/solana/pyth2wormhole/program/src/set_config.rs @@ -8,10 +8,11 @@ use crate::config::{P2WConfigAccount, Pyth2WormholeConfig}; #[derive(FromAccounts, ToInstruction)] pub struct SetConfig<'b> { - /// New config to apply to the program + /// Current config used by the program pub config: P2WConfigAccount<'b, { AccountState::Initialized }>, /// Current owner authority of the program pub current_owner: Signer>, + /// Payer account for updating the account data pub payer: Signer>, } diff --git a/solana/solitaire/client/src/lib.rs b/solana/solitaire/client/src/lib.rs index 140e1c30..341fb039 100644 --- a/solana/solitaire/client/src/lib.rs +++ b/solana/solitaire/client/src/lib.rs @@ -21,18 +21,20 @@ pub use solana_sdk::{ use borsh::BorshSerialize; -use solitaire::{ - AccountState, - Info, - Sysvar, -}; pub use solitaire::{ + processors::seeded::Seeded, Data, Derive, Keyed, Owned, Signer, }; +use solitaire::{ + AccountState, + Info, + Mut, + Sysvar, +}; type StdResult = std::result::Result; @@ -104,12 +106,12 @@ impl<'a, 'b: 'a, T, const Seed: &'static str> Wrap for Derive { fn wrap(a: &AccEntry) -> StdResult, ErrBox> { match a { 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)]) } 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)]) } @@ -187,6 +189,21 @@ impl<'b> Wrap for Info<'b> { } } +impl Wrap for Mut { + fn wrap(a: &AccEntry) -> StdResult, 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::() + ) + .into()), + } + } +} + /// Trait used on client side to easily validate a program accounts + ix_data for a bare Solana call pub trait ToInstruction { fn to_ix(