pyth-crosschain/solana/pyth2wormhole/client/src/lib.rs

528 lines
15 KiB
Rust

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<dyn std::error::Error + Send + Sync>;
pub fn gen_init_tx(
payer: Keypair,
p2w_addr: Pubkey,
config: Pyth2WormholeConfig,
latest_blockhash: Hash,
) -> Result<Transaction, ErrBox> {
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::<Vec<&Keypair>>(
&[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<Instruction, ErrBox> {
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<Transaction, ErrBox> {
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::<Vec<&Keypair>>(
&[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<Instruction, ErrBox> {
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<Transaction, ErrBox> {
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::<Vec<&Keypair>>(
&[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<Transaction, ErrBox> {
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::<Vec<&Keypair>>(
&[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<Pyth2WormholeConfig, ErrBox> {
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<Transaction, ErrBoxSend> {
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::<Vec<&Keypair>>(
&[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<Vec<P2WProductAccount>, ErrBox> {
let mut ret: Vec<P2WProductAccount> = 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<Pubkey> = 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<String>,
pub price_account_keys: Vec<Pubkey>,
}