434 lines
12 KiB
Rust
434 lines
12 KiB
Rust
pub mod attestation_cfg;
|
|
pub mod cli;
|
|
|
|
use std::{
|
|
fs::File,
|
|
path::{
|
|
Path,
|
|
PathBuf,
|
|
},
|
|
};
|
|
|
|
use borsh::{
|
|
BorshDeserialize,
|
|
BorshSerialize,
|
|
};
|
|
use clap::Clap;
|
|
use log::{
|
|
debug,
|
|
error,
|
|
info,
|
|
warn,
|
|
LevelFilter,
|
|
};
|
|
use solana_client::rpc_client::RpcClient;
|
|
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 solana_transaction_status::UiTransactionEncoding;
|
|
use solitaire::{
|
|
processors::seeded::Seeded,
|
|
AccountState,
|
|
Derive,
|
|
Info,
|
|
};
|
|
use solitaire_client::{
|
|
AccEntry,
|
|
Keypair,
|
|
SolSigner,
|
|
ToInstruction,
|
|
};
|
|
|
|
use cli::{
|
|
Action,
|
|
Cli,
|
|
};
|
|
|
|
use bridge::{
|
|
accounts::{
|
|
Bridge,
|
|
FeeCollector,
|
|
Sequence,
|
|
SequenceDerivationData,
|
|
},
|
|
types::ConsistencyLevel,
|
|
CHAIN_ID_SOLANA,
|
|
};
|
|
|
|
use pyth2wormhole::{
|
|
attest::{
|
|
P2WEmitter,
|
|
P2W_MAX_BATCH_SIZE,
|
|
},
|
|
config::P2WConfigAccount,
|
|
initialize::InitializeAccounts,
|
|
set_config::SetConfigAccounts,
|
|
types::PriceAttestation,
|
|
AttestData,
|
|
Pyth2WormholeConfig,
|
|
};
|
|
|
|
use crate::attestation_cfg::AttestationConfig;
|
|
|
|
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);
|
|
|
|
let payer = read_keypair_file(&*shellexpand::tilde(&cli.payer))?;
|
|
let rpc_client = RpcClient::new_with_commitment(cli.rpc_url, CommitmentConfig::finalized());
|
|
|
|
let p2w_addr = cli.p2w_addr;
|
|
|
|
let latest_blockhash = rpc_client.get_latest_blockhash()?;
|
|
|
|
match cli.action {
|
|
Action::Init {
|
|
owner_addr,
|
|
pyth_owner_addr,
|
|
wh_prog,
|
|
} => {
|
|
let tx = handle_init(
|
|
payer,
|
|
p2w_addr,
|
|
owner_addr,
|
|
wh_prog,
|
|
pyth_owner_addr,
|
|
latest_blockhash,
|
|
)?;
|
|
rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
|
|
}
|
|
Action::SetConfig {
|
|
ref owner,
|
|
new_owner_addr,
|
|
new_wh_prog,
|
|
new_pyth_owner_addr,
|
|
} => {
|
|
let tx = handle_set_config(
|
|
payer,
|
|
p2w_addr,
|
|
read_keypair_file(&*shellexpand::tilde(&owner))?,
|
|
new_owner_addr,
|
|
new_wh_prog,
|
|
new_pyth_owner_addr,
|
|
latest_blockhash,
|
|
)?;
|
|
rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
|
|
}
|
|
Action::Attest {
|
|
ref attestation_cfg,
|
|
} => {
|
|
// Load the attestation config yaml
|
|
let attestation_cfg: AttestationConfig =
|
|
serde_yaml::from_reader(File::open(attestation_cfg)?)?;
|
|
|
|
handle_attest(&rpc_client, payer, p2w_addr, &attestation_cfg)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_init(
|
|
payer: Keypair,
|
|
p2w_addr: Pubkey,
|
|
new_owner_addr: Pubkey,
|
|
wh_prog: Pubkey,
|
|
pyth_owner_addr: Pubkey,
|
|
latest_blockhash: Hash,
|
|
) -> Result<Transaction, ErrBox> {
|
|
use AccEntry::*;
|
|
|
|
let payer_pubkey = payer.pubkey();
|
|
|
|
let accs = InitializeAccounts {
|
|
payer: Signer(payer),
|
|
new_config: Derived(p2w_addr),
|
|
};
|
|
|
|
let config = Pyth2WormholeConfig {
|
|
max_batch_size: P2W_MAX_BATCH_SIZE,
|
|
owner: new_owner_addr,
|
|
wh_prog: wh_prog,
|
|
pyth_owner: pyth_owner_addr,
|
|
};
|
|
let ix_data = (pyth2wormhole::instruction::Instruction::Initialize, config);
|
|
|
|
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>>(
|
|
&[ix],
|
|
Some(&payer_pubkey),
|
|
signers.iter().collect::<Vec<_>>().as_ref(),
|
|
latest_blockhash,
|
|
);
|
|
Ok(tx_signed)
|
|
}
|
|
|
|
fn handle_set_config(
|
|
payer: Keypair,
|
|
p2w_addr: Pubkey,
|
|
owner: Keypair,
|
|
new_owner_addr: Pubkey,
|
|
new_wh_prog: Pubkey,
|
|
new_pyth_owner_addr: Pubkey,
|
|
latest_blockhash: Hash,
|
|
) -> Result<Transaction, ErrBox> {
|
|
use AccEntry::*;
|
|
|
|
let payer_pubkey = payer.pubkey();
|
|
|
|
let accs = SetConfigAccounts {
|
|
payer: Signer(payer),
|
|
current_owner: Signer(owner),
|
|
config: Derived(p2w_addr),
|
|
};
|
|
|
|
let config = Pyth2WormholeConfig {
|
|
max_batch_size: P2W_MAX_BATCH_SIZE,
|
|
owner: new_owner_addr,
|
|
wh_prog: new_wh_prog,
|
|
pyth_owner: new_pyth_owner_addr,
|
|
};
|
|
let ix_data = (pyth2wormhole::instruction::Instruction::SetConfig, config);
|
|
|
|
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>>(
|
|
&[ix],
|
|
Some(&payer_pubkey),
|
|
signers.iter().collect::<Vec<_>>().as_ref(),
|
|
latest_blockhash,
|
|
);
|
|
Ok(tx_signed)
|
|
}
|
|
|
|
fn handle_attest(
|
|
rpc_client: &RpcClient, // Needed for reading Pyth account data
|
|
payer: Keypair,
|
|
p2w_addr: Pubkey,
|
|
attestation_cfg: &AttestationConfig,
|
|
) -> Result<(), ErrBox> {
|
|
// Derive seeded accounts
|
|
let emitter_addr = P2WEmitter::key(None, &p2w_addr);
|
|
|
|
info!("Using emitter addr {}", emitter_addr);
|
|
|
|
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)?.as_slice(),
|
|
)?;
|
|
|
|
let seq_addr = Sequence::key(
|
|
&SequenceDerivationData {
|
|
emitter_key: &emitter_addr,
|
|
},
|
|
&config.wh_prog,
|
|
);
|
|
|
|
// Read the current max batch size from the contract's settings
|
|
let max_batch_size = config.max_batch_size;
|
|
|
|
let batch_count = {
|
|
let whole_batches = attestation_cfg.symbols.len() / config.max_batch_size as usize;
|
|
|
|
// Include partial batch if there is a remainder
|
|
if attestation_cfg.symbols.len() % config.max_batch_size as usize > 0 {
|
|
whole_batches + 1
|
|
} else {
|
|
whole_batches
|
|
}
|
|
};
|
|
|
|
debug!("Symbol config:\n{:#?}", attestation_cfg);
|
|
|
|
info!(
|
|
"{} symbols read, max batch size {}, dividing into {} batches",
|
|
attestation_cfg.symbols.len(),
|
|
max_batch_size,
|
|
batch_count
|
|
);
|
|
|
|
let mut errors = Vec::new();
|
|
|
|
for (idx, symbols) in attestation_cfg
|
|
.symbols
|
|
.as_slice()
|
|
.chunks(max_batch_size as usize)
|
|
.enumerate()
|
|
{
|
|
let batch_no = idx + 1;
|
|
let sym_msg_keypair = Keypair::new();
|
|
info!(
|
|
"Batch {}/{} contents: {:?}",
|
|
batch_no,
|
|
batch_count,
|
|
symbols
|
|
.iter()
|
|
.map(|s| s
|
|
.name
|
|
.clone()
|
|
.unwrap_or(format!("unnamed product {:?}", s.product_addr)))
|
|
.collect::<Vec<_>>()
|
|
);
|
|
|
|
let mut sym_metas_vec: Vec<_> = symbols
|
|
.iter()
|
|
.map(|s| {
|
|
vec![
|
|
AccountMeta::new_readonly(s.product_addr, false),
|
|
AccountMeta::new_readonly(s.price_addr, false),
|
|
]
|
|
})
|
|
.flatten()
|
|
.collect();
|
|
|
|
// Align to max batch size with null accounts
|
|
let mut blank_accounts =
|
|
vec![
|
|
AccountMeta::new_readonly(Pubkey::new_from_array([0u8; 32]), false);
|
|
2 * (max_batch_size as usize - symbols.len())
|
|
];
|
|
sym_metas_vec.append(&mut blank_accounts);
|
|
|
|
// Arrange 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),
|
|
];
|
|
|
|
// Insert max_batch_size metas
|
|
acc_metas.append(&mut sym_metas_vec);
|
|
|
|
// Continue with other pyth2wormhole accounts
|
|
let mut acc_metas_remainder = vec![
|
|
// clock
|
|
AccountMeta::new_readonly(clock::id(), false),
|
|
// wh_prog
|
|
AccountMeta::new_readonly(config.wh_prog, false),
|
|
// wh_bridge
|
|
AccountMeta::new(
|
|
Bridge::<{ AccountState::Initialized }>::key(None, &config.wh_prog),
|
|
false,
|
|
),
|
|
// wh_message
|
|
AccountMeta::new(sym_msg_keypair.pubkey(), true),
|
|
// wh_emitter
|
|
AccountMeta::new_readonly(emitter_addr, false),
|
|
// wh_sequence
|
|
AccountMeta::new(seq_addr, false),
|
|
// wh_fee_collector
|
|
AccountMeta::new(FeeCollector::<'_>::key(None, &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::Finalized,
|
|
},
|
|
);
|
|
|
|
let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
|
|
|
|
// Execute the transaction, obtain the resulting sequence
|
|
// number. The and_then() calls enforce error handling
|
|
// location near loop end.
|
|
let res = rpc_client
|
|
.get_latest_blockhash()
|
|
.and_then(|latest_blockhash| {
|
|
let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
|
|
&[ix],
|
|
Some(&payer.pubkey()),
|
|
&vec![&payer, &sym_msg_keypair],
|
|
latest_blockhash,
|
|
);
|
|
rpc_client.send_and_confirm_transaction_with_spinner(&tx_signed)
|
|
})
|
|
.and_then(|sig| rpc_client.get_transaction(&sig, UiTransactionEncoding::Json))
|
|
.map_err(|e| -> ErrBox { e.into() })
|
|
.and_then(|this_tx| {
|
|
this_tx
|
|
.transaction
|
|
.meta
|
|
.and_then(|meta| meta.log_messages)
|
|
.and_then(|logs| {
|
|
let mut seqno = None;
|
|
for log in logs {
|
|
if log.starts_with(SEQNO_PREFIX) {
|
|
seqno = Some(log.replace(SEQNO_PREFIX, ""));
|
|
break;
|
|
}
|
|
}
|
|
seqno
|
|
})
|
|
.ok_or_else(|| format!("No seqno in program logs").into())
|
|
});
|
|
|
|
// Individual batch errors mustn't prevent other batches from being sent.
|
|
match res {
|
|
Ok(seqno) => {
|
|
println!("Sequence number: {}", seqno);
|
|
info!("Batch {}/{}: OK, seqno {}", batch_no, batch_count, seqno);
|
|
}
|
|
Err(e) => {
|
|
let msg = format!(
|
|
"Batch {}/{} tx error: {}",
|
|
batch_no,
|
|
batch_count,
|
|
e.to_string()
|
|
);
|
|
error!("{}", &msg);
|
|
|
|
errors.push(msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
if errors.len() > 0 {
|
|
let err_list = errors.join("\n");
|
|
|
|
Err(format!("{} of {} batches failed:\n{}", errors.len(), batch_count, err_list).into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn init_logging(verbosity: u32) {
|
|
use LevelFilter::*;
|
|
let filter = match verbosity {
|
|
0..=1 => Error,
|
|
2 => Warn,
|
|
3 => Info,
|
|
4 => Debug,
|
|
_other => Trace,
|
|
};
|
|
|
|
env_logger::builder().filter_level(filter).init();
|
|
}
|