pub mod attestation_cfg; pub mod batch_state; pub mod healthcheck; pub mod message; pub mod util; pub use { attestation_cfg::{ AttestationConditions, AttestationConfig, P2WSymbol, }, batch_state::BatchState, healthcheck::{ HealthCheckState, HEALTHCHECK_STATE, }, message::P2WMessageQueue, pyth2wormhole::Pyth2WormholeConfig, util::{ start_metrics_server, RLMutex, RLMutexGuard, }, }; use { borsh::{ BorshDeserialize, BorshSerialize, }, bridge::{ accounts::{ Bridge, FeeCollector, Sequence, SequenceDerivationData, }, types::ConsistencyLevel, }, log::{ debug, trace, warn, }, p2w_sdk::P2WEmitter, pyth2wormhole::{ config::{ OldP2WConfigAccount, P2WConfigAccount, }, message::{ P2WMessage, P2WMessageDrvData, }, AttestData, }, pyth_sdk_solana::state::{ load_mapping_account, load_price_account, load_product_account, }, solana_client::nonblocking::rpc_client::RpcClient, solana_program::{ hash::Hash, instruction::{ AccountMeta, Instruction, }, pubkey::Pubkey, system_program, sysvar::{ clock, rent, }, }, solana_sdk::{ signer::{ keypair::Keypair, Signer, }, transaction::Transaction, }, solitaire::{ processors::seeded::Seeded, AccountState, ErrBox, }, }; /// Future-friendly version of solitaire::ErrBox pub type ErrBoxSend = Box; pub fn gen_init_tx( payer: Keypair, p2w_addr: Pubkey, config: Pyth2WormholeConfig, latest_blockhash: Hash, ) -> Result { let payer_pubkey = payer.pubkey(); let acc_metas = vec![ // new_config AccountMeta::new( P2WConfigAccount::<{ AccountState::Uninitialized }>::key(None, &p2w_addr), false, ), // payer AccountMeta::new(payer.pubkey(), true), // system_program AccountMeta::new(system_program::id(), false), ]; let ix_data = (pyth2wormhole::instruction::Instruction::Initialize, config); let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas); let signers = vec![&payer]; let tx_signed = Transaction::new_signed_with_payer::>( &[ix], Some(&payer_pubkey), &signers, latest_blockhash, ); Ok(tx_signed) } pub fn get_set_config_ix( p2w_addr: &Pubkey, owner_pubkey: &Pubkey, payer_pubkey: &Pubkey, new_config: Pyth2WormholeConfig, ) -> Result { let acc_metas = vec![ // config AccountMeta::new( P2WConfigAccount::<{ AccountState::Initialized }>::key(None, p2w_addr), false, ), // current_owner AccountMeta::new(*owner_pubkey, true), // payer AccountMeta::new(*payer_pubkey, true), // system_program AccountMeta::new(system_program::id(), false), ]; let ix_data = ( pyth2wormhole::instruction::Instruction::SetConfig, new_config, ); Ok(Instruction::new_with_bytes( *p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas, )) } pub fn gen_set_config_tx( payer: Keypair, p2w_addr: Pubkey, owner: Keypair, new_config: Pyth2WormholeConfig, latest_blockhash: Hash, ) -> Result { let ix = get_set_config_ix(&p2w_addr, &owner.pubkey(), &payer.pubkey(), new_config)?; let signers = vec![&owner, &payer]; let tx_signed = Transaction::new_signed_with_payer::>( &[ix], Some(&payer.pubkey()), &signers, latest_blockhash, ); Ok(tx_signed) } pub fn get_set_is_active_ix( p2w_addr: &Pubkey, ops_owner_pubkey: &Pubkey, payer_pubkey: &Pubkey, new_is_active: bool, ) -> Result { let acc_metas = vec![ // config AccountMeta::new( P2WConfigAccount::<{ AccountState::Initialized }>::key(None, p2w_addr), false, ), // ops_owner AccountMeta::new(*ops_owner_pubkey, true), // payer AccountMeta::new(*payer_pubkey, true), ]; let ix_data = ( pyth2wormhole::instruction::Instruction::SetIsActive, new_is_active, ); Ok(Instruction::new_with_bytes( *p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas, )) } pub fn gen_set_is_active_tx( payer: Keypair, p2w_addr: Pubkey, ops_owner: Keypair, new_is_active: bool, latest_blockhash: Hash, ) -> Result { let ix = get_set_is_active_ix( &p2w_addr, &ops_owner.pubkey(), &payer.pubkey(), new_is_active, )?; let signers = vec![&ops_owner, &payer]; let tx_signed = Transaction::new_signed_with_payer::>( &[ix], Some(&payer.pubkey()), &signers, latest_blockhash, ); Ok(tx_signed) } pub fn gen_migrate_tx( payer: Keypair, p2w_addr: Pubkey, owner: Keypair, latest_blockhash: Hash, ) -> Result { let payer_pubkey = payer.pubkey(); let acc_metas = vec![ // new_config AccountMeta::new( P2WConfigAccount::<{ AccountState::Uninitialized }>::key(None, &p2w_addr), false, ), // old_config AccountMeta::new(OldP2WConfigAccount::key(None, &p2w_addr), false), // owner AccountMeta::new(owner.pubkey(), true), // payer AccountMeta::new(payer.pubkey(), true), // system_program AccountMeta::new(system_program::id(), false), ]; let ix_data = (pyth2wormhole::instruction::Instruction::Migrate, ()); let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas); let signers = vec![&owner, &payer]; let tx_signed = Transaction::new_signed_with_payer::>( &[ix], Some(&payer_pubkey), &signers, latest_blockhash, ); Ok(tx_signed) } /// Get the current config account data for given p2w program address pub async fn get_config_account( rpc_client: &RpcClient, p2w_addr: &Pubkey, ) -> Result { let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, p2w_addr); let config = Pyth2WormholeConfig::try_from_slice( rpc_client .get_account_data(&p2w_config_addr) .await? .as_slice(), )?; Ok(config) } /// Generate an Instruction for making the attest() contract /// call. pub fn gen_attest_tx( p2w_addr: Pubkey, p2w_config: &Pyth2WormholeConfig, // Must be fresh, not retrieved inside to keep side effects away payer: &Keypair, wh_msg_id: u64, symbols: &[P2WSymbol], latest_blockhash: Hash, ) -> Result { let emitter_addr = P2WEmitter::key(None, &p2w_addr); let seq_addr = Sequence::key( &SequenceDerivationData { emitter_key: &emitter_addr, }, &p2w_config.wh_prog, ); let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr); if symbols.len() > p2w_config.max_batch_size as usize { return Err((format!( "Expected up to {} symbols for batch, {} were found", p2w_config.max_batch_size, symbols.len() )) .into()); } // Initial attest() accounts let mut acc_metas = vec![ // payer AccountMeta::new(payer.pubkey(), true), // system_program AccountMeta::new_readonly(system_program::id(), false), // config AccountMeta::new_readonly(p2w_config_addr, false), ]; // Batch contents and padding if applicable let mut padded_symbols = { let mut not_padded: Vec<_> = symbols .iter() .flat_map(|s| { vec![ AccountMeta::new_readonly(s.product_addr, false), AccountMeta::new_readonly(s.price_addr, false), ] }) .collect(); // Align to max batch size with null accounts let mut padding_accounts = vec![ AccountMeta::new_readonly(Pubkey::new_from_array([0u8; 32]), false); 2 * (p2w_config.max_batch_size as usize - symbols.len()) ]; not_padded.append(&mut padding_accounts); not_padded }; acc_metas.append(&mut padded_symbols); // Continue with other pyth2wormhole accounts let mut acc_metas_remainder = vec![ // clock AccountMeta::new_readonly(clock::id(), false), // wh_prog AccountMeta::new_readonly(p2w_config.wh_prog, false), // wh_bridge AccountMeta::new( Bridge::<{ AccountState::Initialized }>::key(None, &p2w_config.wh_prog), false, ), // wh_message AccountMeta::new( P2WMessage::key( &P2WMessageDrvData { id: wh_msg_id, batch_size: symbols.len() as u16, message_owner: payer.pubkey(), }, &p2w_addr, ), false, ), // wh_emitter AccountMeta::new_readonly(emitter_addr, false), // wh_sequence AccountMeta::new(seq_addr, false), // wh_fee_collector AccountMeta::new(FeeCollector::<'_>::key(None, &p2w_config.wh_prog), false), AccountMeta::new_readonly(rent::id(), false), ]; acc_metas.append(&mut acc_metas_remainder); let ix_data = ( pyth2wormhole::instruction::Instruction::Attest, AttestData { consistency_level: ConsistencyLevel::Confirmed, message_account_id: wh_msg_id, }, ); let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas); let tx_signed = Transaction::new_signed_with_payer::>( &[ix], Some(&payer.pubkey()), &vec![payer], latest_blockhash, ); Ok(tx_signed) } /// Enumerates all products and their prices in a Pyth mapping. /// Returns map of: product address => [price addresses] pub async fn crawl_pyth_mapping( rpc_client: &RpcClient, first_mapping_addr: &Pubkey, ) -> Result, ErrBox> { let mut ret: Vec = vec![]; let mut n_mappings = 1; // We assume the first one must be valid let mut n_products_total = 0; // Grand total products in all mapping accounts let mut n_prices_total = 0; // Grand total prices in all product accounts in all mapping accounts let mut mapping_addr = *first_mapping_addr; // loop until the last non-zero MappingAccount.next account loop { let mapping_bytes = rpc_client.get_account_data(&mapping_addr).await?; let mapping = match load_mapping_account(&mapping_bytes) { Ok(p) => p, Err(e) => { warn!( "Mapping: Could not parse account {} as a Pyth mapping, crawling terminated. Error: {:?}", mapping_addr, e ); break; } }; // Products in this mapping account let mut n_mapping_products = 0; // loop through all products in this mapping; filter out zeroed-out empty product slots for prod_addr in mapping.products.iter().filter(|p| *p != &Pubkey::default()) { let prod_bytes = rpc_client.get_account_data(prod_addr).await?; let prod = match load_product_account(&prod_bytes) { Ok(p) => p, Err(e) => { warn!("Mapping {}: Could not parse account {} as a Pyth product, skipping to next product. Error: {:?}", mapping_addr, prod_addr, e); continue; } }; let mut prod_name = None; for (key, val) in prod.iter() { if key.eq_ignore_ascii_case("symbol") { prod_name = Some(val.to_owned()); } } let mut price_addr = prod.px_acc; let mut n_prod_prices = 0; // the product might have no price, can happen in tilt due to race-condition, failed tx to add price, ... if price_addr == Pubkey::default() { debug!( "Found product with addr {} that has no prices. \ This should not happen in a production enviornment.", prod_addr ); continue; } // loop until the last non-zero PriceAccount.next account let mut price_accounts: Vec = vec![]; loop { let price_bytes = rpc_client.get_account_data(&price_addr).await?; let price = match load_price_account(&price_bytes) { Ok(p) => p, Err(e) => { warn!("Product {}: Could not parse account {} as a Pyth price, skipping to next product. Error: {:?}", prod_addr, price_addr, e); break; } }; price_accounts.push(price_addr); n_prod_prices += 1; if price.next == Pubkey::default() { trace!( "Product {}: processed {} price(s)", prod_addr, n_prod_prices ); break; } price_addr = price.next; } ret.push(P2WProductAccount { key: *prod_addr, name: prod_name.clone(), price_account_keys: price_accounts, }); n_prices_total += n_prod_prices; } n_mapping_products += 1; n_products_total += n_mapping_products; // Traverse other mapping accounts if applicable if mapping.next == Pubkey::default() { trace!( "Mapping {}: processed {} products", mapping_addr, n_mapping_products ); break; } mapping_addr = mapping.next; n_mappings += 1; } debug!( "Processed {} price(s) in {} product account(s), in {} mapping account(s)", n_prices_total, n_products_total, n_mappings ); Ok(ret) } #[derive(Clone, Debug)] pub struct P2WProductAccount { pub key: Pubkey, pub name: Option, pub price_account_keys: Vec, }