feat: Scraper populates gas payments for Sealevel (#4843)

### Description

Scraper populates gas payments for Sealevel

### Related issues

- Contributes into
https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4272

### Backward compatibility

Yes

### Testing

Manual run of Scraper

---------

Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
pull/4846/head
Danil Nemirovsky 2 weeks ago committed by GitHub
parent 9fcd592878
commit 408b975900
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 51
      rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs
  2. 2
      rust/main/chains/hyperlane-sealevel/src/lib.rs
  3. 292
      rust/main/chains/hyperlane-sealevel/src/log_meta_composer.rs
  4. 187
      rust/main/chains/hyperlane-sealevel/src/log_meta_composer/delivery_message_txn.json
  5. 302
      rust/main/chains/hyperlane-sealevel/src/log_meta_composer/dispatch_message_txn.json
  6. 83
      rust/main/chains/hyperlane-sealevel/src/log_meta_composer/tests.rs
  7. 131
      rust/main/chains/hyperlane-sealevel/src/mailbox.rs
  8. 209
      rust/main/chains/hyperlane-sealevel/src/transaction.rs
  9. 547
      rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs
  10. 13
      rust/sealevel/programs/hyperlane-sealevel-igp/src/instruction.rs

@ -6,16 +6,17 @@ use hyperlane_sealevel_igp::{
accounts::{GasPaymentAccount, ProgramDataAccount},
igp_gas_payment_pda_seeds, igp_program_data_pda_seeds,
};
use solana_sdk::{account::Account, pubkey::Pubkey};
use solana_sdk::{account::Account, clock::Slot, pubkey::Pubkey};
use tracing::{info, instrument};
use hyperlane_core::{
config::StrOrIntParseError, ChainCommunicationError, ChainResult, ContractLocator,
HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, H512,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, U256,
};
use crate::account::{search_accounts_by_discriminator, search_and_validate_account};
use crate::log_meta_composer::{is_interchain_payment_instruction, LogMetaComposer};
use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient};
/// The offset to get the `unique_gas_payment_pubkey` field from the serialized GasPaymentData.
@ -90,6 +91,7 @@ impl InterchainGasPaymaster for SealevelInterchainGasPaymaster {}
pub struct SealevelInterchainGasPaymasterIndexer {
rpc_client: SealevelRpcClient,
igp: SealevelInterchainGasPaymaster,
log_meta_composer: LogMetaComposer,
}
/// IGP payment data on Sealevel
@ -110,7 +112,18 @@ impl SealevelInterchainGasPaymasterIndexer {
let rpc_client = SealevelRpcClient::new(conf.url.to_string());
let igp = SealevelInterchainGasPaymaster::new(conf, &igp_account_locator).await?;
Ok(Self { rpc_client, igp })
let log_meta_composer = LogMetaComposer::new(
igp.program_id,
"interchain gas payment".to_owned(),
is_interchain_payment_instruction,
);
Ok(Self {
rpc_client,
igp,
log_meta_composer,
})
}
#[instrument(err, skip(self))]
@ -155,26 +168,38 @@ impl SealevelInterchainGasPaymasterIndexer {
gas_amount: gas_payment_account.gas_amount.into(),
};
let log_meta = self
.interchain_payment_log_meta(
U256::from(sequence_number),
&valid_payment_pda_pubkey,
&gas_payment_account.slot,
)
.await?;
Ok(SealevelGasPayment::new(
Indexed::new(igp_payment).with_sequence(
sequence_number
.try_into()
.map_err(StrOrIntParseError::from)?,
),
LogMeta {
address: self.igp.program_id.to_bytes().into(),
block_number: gas_payment_account.slot,
// TODO: get these when building out scraper support.
// It's inconvenient to get these :|
block_hash: H256::zero(),
transaction_id: H512::zero(),
transaction_index: 0,
log_index: sequence_number.into(),
},
log_meta,
H256::from(gas_payment_account.igp.to_bytes()),
))
}
async fn interchain_payment_log_meta(
&self,
log_index: U256,
payment_pda_pubkey: &Pubkey,
payment_pda_slot: &Slot,
) -> ChainResult<LogMeta> {
let block = self.rpc_client.get_block(*payment_pda_slot).await?;
self.log_meta_composer
.log_meta(block, log_index, payment_pda_pubkey, payment_pda_slot)
.map_err(Into::<ChainCommunicationError>::into)
}
fn interchain_payment_account(&self, account: &Account) -> ChainResult<Pubkey> {
let unique_gas_payment_pubkey = Pubkey::new(&account.data);
let (expected_pubkey, _bump) = Pubkey::try_find_program_address(

@ -19,12 +19,12 @@ mod account;
mod error;
mod interchain_gas;
mod interchain_security_module;
mod log_meta_composer;
mod mailbox;
mod merkle_tree_hook;
mod multisig_ism;
mod provider;
mod rpc;
mod trait_builder;
mod transaction;
mod utils;
mod validator_announce;

@ -0,0 +1,292 @@
use std::collections::HashMap;
use solana_sdk::{clock::Slot, pubkey::Pubkey};
use solana_transaction_status::{
option_serializer::OptionSerializer, EncodedTransaction, EncodedTransactionWithStatusMeta,
UiCompiledInstruction, UiConfirmedBlock, UiInstruction, UiMessage, UiTransaction,
UiTransactionStatusMeta,
};
use tracing::warn;
use hyperlane_core::{LogMeta, H512, U256};
use crate::error::HyperlaneSealevelError;
use crate::utils::{decode_h256, decode_h512, from_base58};
#[derive(Debug)]
pub struct LogMetaComposer {
program_id: Pubkey,
transaction_description: String,
is_specified_instruction: fn(&[u8]) -> bool,
}
impl LogMetaComposer {
pub fn new(
program_id: Pubkey,
transaction_description: String,
is_specified_instruction: fn(&[u8]) -> bool,
) -> Self {
Self {
program_id,
transaction_description,
is_specified_instruction,
}
}
pub fn log_meta(
&self,
block: UiConfirmedBlock,
log_index: U256,
pda_pubkey: &Pubkey,
pda_slot: &Slot,
) -> Result<LogMeta, HyperlaneSealevelError> {
let block_hash = decode_h256(&block.blockhash)?;
let transactions = block
.transactions
.ok_or(HyperlaneSealevelError::NoTransactions(format!(
"block which should contain {} transaction does not contain any transaction",
self.transaction_description,
)))?;
let transaction_hashes = search_transactions(
transactions,
&self.program_id,
pda_pubkey,
self.is_specified_instruction,
);
// We expect to see that there is only one transaction
if transaction_hashes.len() > 1 {
Err(HyperlaneSealevelError::TooManyTransactions(format!(
"block contains more than one {} transactions operating on the same PDA",
self.transaction_description,
)))?
}
let (transaction_index, transaction_hash) =
transaction_hashes
.into_iter()
.next()
.ok_or(HyperlaneSealevelError::NoTransactions(format!(
"block which should contain {} transaction does not contain any after filtering",
self.transaction_description,
)))?;
let log_meta = LogMeta {
address: self.program_id.to_bytes().into(),
block_number: *pda_slot,
block_hash,
transaction_id: transaction_hash,
transaction_index: transaction_index as u64,
log_index,
};
Ok(log_meta)
}
}
pub fn is_message_dispatch_instruction(instruction_data: &[u8]) -> bool {
use hyperlane_sealevel_mailbox::instruction::Instruction;
let instruction = match Instruction::from_instruction_data(instruction_data) {
Ok(ii) => ii,
Err(_) => return false,
};
matches!(instruction, Instruction::OutboxDispatch(_))
}
pub fn is_message_delivery_instruction(instruction_data: &[u8]) -> bool {
use hyperlane_sealevel_mailbox::instruction::Instruction;
let instruction = match Instruction::from_instruction_data(instruction_data) {
Ok(ii) => ii,
Err(_) => return false,
};
matches!(instruction, Instruction::InboxProcess(_))
}
pub fn is_interchain_payment_instruction(instruction_data: &[u8]) -> bool {
use hyperlane_sealevel_igp::instruction::Instruction;
let instruction = match Instruction::from_instruction_data(instruction_data) {
Ok(ii) => ii,
Err(_) => return false,
};
matches!(instruction, Instruction::PayForGas(_))
}
/// This function searches for relevant transactions in the vector of provided transactions and
/// returns the relative index and hashes of such transactions.
///
/// This function takes a program identifier and the identifier for PDA and searches transactions
/// which act upon this program and the PDA.
///
/// When the vector of transaction contains all the transactions from a block and in the order
/// in which these transaction appear in the block, the function returns indexes of the relevant
/// transaction in the block.
///
/// The transaction will be searched with the following criteria:
/// 1. Transaction contains program id in the list of accounts.
/// 2. Transaction contains the given PDA in the list of accounts.
/// 3. Transaction is executing the program upon the PDA with the specified instruction.
///
/// * `transactions` - List of transactions
/// * `program_id` - Identifier of program for which we are searching transactions for.
/// * `pda_pubkey` - Identifier for PDA the relevant transaction should operate upon.
/// * `is_specified_instruction` - Function which returns `true` for instruction which should be
/// included into the relevant transaction.
fn search_transactions(
transactions: Vec<EncodedTransactionWithStatusMeta>,
program_id: &Pubkey,
pda_pubkey: &Pubkey,
is_specified_instruction: fn(&[u8]) -> bool,
) -> Vec<(usize, H512)> {
transactions
.into_iter()
.enumerate()
.filter_map(|(index, tx)| filter_by_encoding(tx).map(|(tx, meta)| (index, tx, meta)))
.filter_map(|(index, tx, meta)| {
filter_by_validity(tx, meta)
.map(|(hash, account_keys, instructions)| (index, hash, account_keys, instructions))
})
.filter_map(|(index, hash, account_keys, instructions)| {
filter_by_relevancy(
program_id,
pda_pubkey,
hash,
account_keys,
instructions,
is_specified_instruction,
)
.map(|hash| (index, hash))
})
.collect::<Vec<(usize, H512)>>()
}
fn filter_by_relevancy(
program_id: &Pubkey,
message_storage_pda_pubkey: &Pubkey,
hash: H512,
account_keys: Vec<String>,
instructions: Vec<UiCompiledInstruction>,
is_specified_instruction: fn(&[u8]) -> bool,
) -> Option<H512> {
let account_index_map = account_index_map(account_keys);
let program_id_str = program_id.to_string();
let program_index = match account_index_map.get(&program_id_str) {
Some(i) => *i as u8,
None => return None, // If account keys do not contain program, transaction is not relevant
};
let pda_pubkey_str = message_storage_pda_pubkey.to_string();
let pda_account_index = match account_index_map.get(&pda_pubkey_str) {
Some(i) => *i as u8,
None => return None, // If account keys do not contain the given PDA account, transaction is not relevant
};
let program_maybe = instructions
.into_iter()
.find(|instruction| instruction.program_id_index == program_index);
let program = match program_maybe {
Some(p) => p,
None => return None, // If transaction does not contain call into program, transaction is not relevant
};
// If program does not operate on the given PDA account, transaction is not relevant
if !program.accounts.contains(&pda_account_index) {
return None;
}
let instruction_data = match from_base58(&program.data) {
Ok(d) => d,
Err(_) => return None, // If we cannot decode instruction data, transaction is not relevant
};
// If the call into program is not the specified instruction, transaction is not relevant
if !is_specified_instruction(&instruction_data) {
return None;
}
Some(hash)
}
fn filter_by_validity(
tx: UiTransaction,
meta: UiTransactionStatusMeta,
) -> Option<(H512, Vec<String>, Vec<UiCompiledInstruction>)> {
let Some(transaction_hash) = tx
.signatures
.first()
.map(|signature| decode_h512(signature))
.and_then(|r| r.ok())
else {
warn!(
transaction = ?tx,
"transaction does not have any signatures or signatures cannot be decoded",
);
return None;
};
let UiMessage::Raw(message) = tx.message else {
warn!(message = ?tx.message, "we expect messages in Raw format");
return None;
};
let instructions = instructions(message.instructions, meta);
Some((transaction_hash, message.account_keys, instructions))
}
fn filter_by_encoding(
tx: EncodedTransactionWithStatusMeta,
) -> Option<(UiTransaction, UiTransactionStatusMeta)> {
match (tx.transaction, tx.meta) {
// We support only transactions encoded as JSON
// We need none-empty metadata as well
(EncodedTransaction::Json(t), Some(m)) => Some((t, m)),
t => {
warn!(
?t,
"transaction is not encoded as json or metadata is empty"
);
None
}
}
}
fn account_index_map(account_keys: Vec<String>) -> HashMap<String, usize> {
account_keys
.into_iter()
.enumerate()
.map(|(index, key)| (key, index))
.collect::<HashMap<String, usize>>()
}
/// Extract all instructions from transaction
fn instructions(
instruction: Vec<UiCompiledInstruction>,
meta: UiTransactionStatusMeta,
) -> Vec<UiCompiledInstruction> {
let inner_instructions = match meta.inner_instructions {
OptionSerializer::Some(ii) => ii
.into_iter()
.flat_map(|ii| ii.instructions)
.flat_map(|ii| match ii {
UiInstruction::Compiled(ci) => Some(ci),
_ => None,
})
.collect::<Vec<UiCompiledInstruction>>(),
OptionSerializer::None | OptionSerializer::Skip => vec![],
};
[instruction, inner_instructions].concat()
}
#[cfg(test)]
mod tests;

@ -0,0 +1,187 @@
{
"blockTime": 1726514134,
"meta": {
"computeUnitsConsumed": 200654,
"err": null,
"fee": 5000,
"innerInstructions": [
{
"index": 1,
"instructions": [
{
"accounts": [
10
],
"data": "8YGwT5LUTP4",
"programIdIndex": 9,
"stackHeight": 2
},
{
"accounts": [
12
],
"data": "2848tnNKZjKzgKikguTY4s5nESn7KLYUbLsrp6Z1FYq4BmM31xRwBXnJU5RW9rEvRUjJfJa58kXdgQYEQpg4sDrRfx5HnGsgXfitkxJw5NKVcFAYLSqKvpkYxer2tAn3a8ZzPvuDD9iqyLkvJnRZ3TbcoAHNisFfvBeWK95YL8zxsyzDS9ZBMaoYrLKQx9b915xj9oijw2UNk7FF5qxThZDKwF8rwckb6t2o6ypzFEqYeQCsRW5quayYsLBjHi8RdY18NDkcnPVkQbdR7FmfrncV4H5ZYZaayMtgAs6kHxRgeuuBEtrYG1UbGjWTQAss9zmeXcKipqS3S2bee96U5w9Cd981e8dkakCtKR7KusjE9nhsFTfXoxcwkRhi3TzqDicrqt7Erf78K",
"programIdIndex": 8,
"stackHeight": 2
},
{
"accounts": [
0,
3
],
"data": "11117UpxCJ2YqmddN2ykgdMGRXkyPgnqEtj5XYrnk1iC4P1xrvXq2zvZQkj3uNaitHEw2k",
"programIdIndex": 5,
"stackHeight": 2
},
{
"accounts": [
11,
5,
10,
1,
5,
2
],
"data": "7MHiQP8ahsZcB5cM9ZXGa2foMYQENm7GnrFaV4AmfgKNzSndaXhrcqbVNRgN2kGmrrsfTi8bNEGkAJn6MWjY95PnakaF2HAchXrUUBzQrWKQdRp8VbKjDsnH1tEUiAWm439Y12TpWTW3uSphh1oycpTJP",
"programIdIndex": 9,
"stackHeight": 2
},
{
"accounts": [
2,
1
],
"data": "3Bxs4ThwQbE4vyj5",
"programIdIndex": 5,
"stackHeight": 3
}
]
}
],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi invoke [1]",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg invoke [2]",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg consumed 4402 of 1363482 compute units",
"Program return: 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg AA==",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg success",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC invoke [2]",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC consumed 106563 of 1353660 compute units",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg invoke [2]",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Warp route transfer completed from origin: 1408864445, recipient: 528MctBmY7rXqufM3r8k7t9DTfVNuB4K1rr8xVU4naJM, remote_amount: 100000",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg consumed 28117 of 1240216 compute units",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg success",
"Program log: Hyperlane inbox processed message 0x34ed0705362554568a1a2d24aef6bfde71894dd1bb2f0457fb4bd66016074fcc",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi consumed 200504 of 1399850 compute units",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi success"
],
"postBalances": [
338367600,
199691000,
891880,
1287600,
1211040,
1,
1,
1141440,
1141440,
1141440,
2686560,
0,
8017920,
1141440
],
"postTokenBalances": [],
"preBalances": [
339660200,
199591000,
991880,
0,
1211040,
1,
1,
1141440,
1141440,
1141440,
2686560,
0,
8017920,
1141440
],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 290198208,
"transaction": {
"message": {
"accountKeys": [
"G5FM3UKwcBJ47PwLWLLY1RQpqNtTMgnqnd6nZGcJqaBp",
"528MctBmY7rXqufM3r8k7t9DTfVNuB4K1rr8xVU4naJM",
"5H4cmX5ybSqK6Ro6nvr9eiR8G8ATTYRwVsZ42VRRW3wa",
"Dj7jk47KKXvw4nseNGdyHtNHtjPes2XSfByhF8xymrtS",
"H3EgdESu59M4hn5wrbeyi9VjmFiLYM7iUAbGtrA5uHNE",
"11111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111",
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
"372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC",
"4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg",
"A2nmLy86tmraneRMEZ5yWbDGq6YsPKNcESGaTZKkRWZU",
"DmU32nL975xAshVYgLLdyMoaUzHa2aCzHJyfLyKRdz3M",
"E2jimXLCtTiuZ6jbXP8B7SyZ5vVc1PKYYnMeho9yJ1en",
"E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi"
],
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 9,
"numRequiredSignatures": 1
},
"instructions": [
{
"accounts": [],
"data": "K1FDJ7",
"programIdIndex": 6,
"stackHeight": null
},
{
"accounts": [
0,
5,
4,
11,
3,
10,
7,
8,
12,
9,
5,
10,
1,
5,
2
],
"data": "3RwSrioTudpACxczi2EejzKoZCPVuzq6qWLCQYAWoZoTcRPBobUn7tB5SFvMPNHGJ551rmjXDyKdaQLuzX3d5bjHSrSsquwHqWgM6L2kMEEJZjtygNyx3RhJD9GyZqekDuK19cfYfn1dyLuo7SSqswV3t6yptLhnCv8DhxBLRuXhV2GdNy9PLU3VNc9PvPWxg1Grtr9UZ5GnmdKDeqRvonM9AqmuN6mnv3UaqjjAEX8yDKPhWHm6w1HRzfgbjkXQVL5aSqdgJeF3EVBKJCzvMKbUVjTRgD6iHQyUVrSYvrHpKZxc6EctBHN6tyeZrW5RD1M6giasnm4WqrjDwUyz9xwvk31srJrZp7W7D6i2tTajmBbiKjpNo75iaHj4dycf1H",
"programIdIndex": 13,
"stackHeight": null
}
],
"recentBlockhash": "AzQN8x5uKk7ExXW4eUu2FiqRG1BX73uvfHcQeBDHcu8a"
},
"signatures": [
"5pBEVfDD3siir1CBf9taeWuee44GspA7EixYkKnzN1hkeYXLxtKYrbe3aE6hxswbY3hhDRVPDor1ZsSXUorC7bcR"
]
}
}

@ -0,0 +1,302 @@
{
"blockTime": 1729865514,
"meta": {
"computeUnitsConsumed": 171834,
"err": null,
"fee": 3564950,
"innerInstructions": [
{
"index": 2,
"instructions": [
{
"accounts": [
8,
7,
6,
0
],
"data": "gCzo5F74HA9Pb",
"programIdIndex": 19,
"stackHeight": 2
},
{
"accounts": [
5,
11,
10,
18,
0,
1,
2
],
"data": "2Nsbnwq8JuYnSefHfRznxFtFqdPnbeydtt5kenfF8GR1ZU2XtF8jJDo4SUc2VY52V5C25WsKsQZBLsoCVQNzefgVj2bVznkThjuZuSKXJfZN9ADggiM2soRKVsAjf3xHm3CC3w3iyvK5U9LsjmYtiDNbJCFtEPRTDxsfvMS45Bg3q6EogmBN9JiZNLP",
"programIdIndex": 17,
"stackHeight": 2
},
{
"accounts": [
0,
5
],
"data": "3Bxs3zrfFUZbEPqZ",
"programIdIndex": 10,
"stackHeight": 3
},
{
"accounts": [
0,
2
],
"data": "11114XfZCGKrze4PNou1GXiYCJgiBCGpHks9hxjb8tFwYMjtgVtMzvriDxwYPdRqSoqztL",
"programIdIndex": 10,
"stackHeight": 3
},
{
"accounts": [
10,
0,
3,
1,
4,
9,
14
],
"data": "5MtKiLZhPB3NhS7Gus6CenAEMS2QBtpY9QtuLeVH4CkpUN7599vsYzZXhk8Vu",
"programIdIndex": 15,
"stackHeight": 2
},
{
"accounts": [
0,
9
],
"data": "3Bxs4A3YxXXYy5gj",
"programIdIndex": 10,
"stackHeight": 3
},
{
"accounts": [
0,
4
],
"data": "111158VjdPaAaGVkCbPZoXJqknHXBEqoypfVjf96mwePbKxAkrKfR2gUFyN7wD8ccc9g1z",
"programIdIndex": 10,
"stackHeight": 3
}
]
}
],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program 3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm invoke [1]",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
"Program log: Instruction: TransferChecked",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6200 of 983051 compute units",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi invoke [2]",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Protocol fee of 0 paid from FGyh1FfooV7AtVrYjFGmjMxbELC8RMxNp4xY5WY4L4md to BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Dispatched message to 1408864445, ID 0x09c74f3e10d98c112696b72ba1609aae47616f64f28b4cb1ad8a4a710e93ee89",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi consumed 86420 of 972001 compute units",
"Program return: E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi CcdPPhDZjBEmlrcroWCarkdhb2Tyi0yxrYpKcQ6T7ok=",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi success",
"Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv invoke [2]",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Paid IGP JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M for 431000 gas for message 0x09c7…ee89 to 1408864445",
"Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv consumed 42792 of 882552 compute units",
"Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv success",
"Program log: Warp route transfer completed to destination: 1408864445, recipient: 0xd41b…f050, remote_amount: 2206478600",
"Program 3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm consumed 171534 of 999700 compute units",
"Program 3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm success"
],
"postBalances": [
12374928,
0,
2241120,
1016160,
1872240,
8679120,
2039280,
319231603414,
2039280,
10172586528,
1,
890880,
1141440,
3361680,
1830480,
1141440,
1,
1141440,
1141440,
934087680
],
"postTokenBalances": [
{
"accountIndex": 6,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"owner": "CcquFeCYNZM48kLPyG3HWxdwgigmyxPBi6iHwve9Myhj",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "165697511204",
"decimals": 6,
"uiAmount": 165697.511204,
"uiAmountString": "165697.511204"
}
},
{
"accountIndex": 8,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"owner": "FGyh1FfooV7AtVrYjFGmjMxbELC8RMxNp4xY5WY4L4md",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "94",
"decimals": 6,
"uiAmount": 9.4E-5,
"uiAmountString": "0.000094"
}
}
],
"preBalances": [
22211372,
0,
0,
1016160,
0,
8679120,
2039280,
319231603414,
2039280,
10170428394,
1,
890880,
1141440,
3361680,
1830480,
1141440,
1,
1141440,
1141440,
934087680
],
"preTokenBalances": [
{
"accountIndex": 6,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"owner": "CcquFeCYNZM48kLPyG3HWxdwgigmyxPBi6iHwve9Myhj",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "163491032604",
"decimals": 6,
"uiAmount": 163491.032604,
"uiAmountString": "163491.032604"
}
},
{
"accountIndex": 8,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"owner": "FGyh1FfooV7AtVrYjFGmjMxbELC8RMxNp4xY5WY4L4md",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "2206478694",
"decimals": 6,
"uiAmount": 2206.478694,
"uiAmountString": "2206.478694"
}
}
],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 297626301,
"transaction": {
"message": {
"accountKeys": [
"FGyh1FfooV7AtVrYjFGmjMxbELC8RMxNp4xY5WY4L4md",
"8DqWVhEZcg4rDYwe5UFaopmGuEajiPz9L3A1ZnytMcUm",
"6eG8PheL41qLFFUtPjSYMtsp4aoAQsMgcsYwkGCB8kwT",
"8Cv4PHJ6Cf3xY7dse7wYeZKtuQv9SAN6ujt5w22a2uho",
"9yMwrDqHsbmmvYPS9h4MLPbe2biEykcL51W7qJSDL5hF",
"BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk",
"CcquFeCYNZM48kLPyG3HWxdwgigmyxPBi6iHwve9Myhj",
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"FDDbaNtod9pt7pmR8qtmRZJtEj9NViDA7J6cazqUjXQj",
"JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M",
"11111111111111111111111111111111",
"37N3sbyVAd3KvQsPw42i1LWkLahzL4ninVQ4n1NmnHjS",
"3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm",
"AHX3iiEPFMyygANrp15cyUr63o9qGkwkB6ki1pgpZ7gZ",
"AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF",
"BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv",
"ComputeBudget111111111111111111111111111111",
"E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi",
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
],
"header": {
"numReadonlySignedAccounts": 1,
"numReadonlyUnsignedAccounts": 10,
"numRequiredSignatures": 2
},
"instructions": [
{
"accounts": [],
"data": "FjL4FH",
"programIdIndex": 16,
"stackHeight": null
},
{
"accounts": [],
"data": "3butUEijJrLf",
"programIdIndex": 16,
"stackHeight": null
},
{
"accounts": [
10,
18,
13,
17,
5,
11,
0,
1,
2,
15,
3,
4,
14,
9,
19,
7,
8,
6
],
"data": "RpjV6TtUSvt6UnMXdNo4h1Ze2VGVifo65r2jqRBUq6HJKhskSnwWybXyB4NxgfvedV9vhKdmDPg8sFT64JEZvxF8VfoGdqoAFt4WFLSB",
"programIdIndex": 12,
"stackHeight": null
}
],
"recentBlockhash": "GHQhVUy7Eq3hcps8YoG9DCd1Tb6ccQZ9xhh81ju8ujHJ"
},
"signatures": [
"4nRGgV9tqCuiKUXeBzWdvdk6YC9BsGWUZurAVQLMX1NwNPpysbZNwXu97Sw4aM9REwaRmWS7gaiSKXbwtmw6oLRi",
"hXjvQbAuFH9vAxZMdGqfnSjN7t7Z7NLTzRq1SG8i6fLr9LS6XahTduPWqakiTsLDyWSofvq3MSncUAkbQLEj85f"
]
}
}

@ -0,0 +1,83 @@
use std::fs;
use std::path::PathBuf;
use solana_transaction_status::EncodedTransactionWithStatusMeta;
use crate::log_meta_composer::{
is_interchain_payment_instruction, is_message_delivery_instruction,
is_message_dispatch_instruction, search_transactions,
};
use crate::utils::decode_pubkey;
#[test]
pub fn test_search_dispatched_message_transaction() {
// given
let mailbox_program_id = decode_pubkey("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi").unwrap();
let dispatched_message_pda_account =
decode_pubkey("6eG8PheL41qLFFUtPjSYMtsp4aoAQsMgcsYwkGCB8kwT").unwrap();
let transactions = transactions(&read_json("dispatch_message_txn.json"));
// when
let transaction_hashes = search_transactions(
transactions,
&mailbox_program_id,
&dispatched_message_pda_account,
is_message_dispatch_instruction,
);
// then
assert!(!transaction_hashes.is_empty());
}
#[test]
pub fn test_search_delivered_message_transaction() {
// given
let mailbox_program_id = decode_pubkey("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi").unwrap();
let delivered_message_pda_account =
decode_pubkey("Dj7jk47KKXvw4nseNGdyHtNHtjPes2XSfByhF8xymrtS").unwrap();
let transactions = transactions(&read_json("delivery_message_txn.json"));
// when
let transaction_hashes = search_transactions(
transactions,
&mailbox_program_id,
&delivered_message_pda_account,
is_message_delivery_instruction,
);
// then
assert!(!transaction_hashes.is_empty());
}
#[test]
pub fn test_search_interchain_payment_transaction() {
// given
let interchain_payment_program_id =
decode_pubkey("BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv").unwrap();
let payment_pda_account =
decode_pubkey("9yMwrDqHsbmmvYPS9h4MLPbe2biEykcL51W7qJSDL5hF").unwrap();
let transactions = transactions(&read_json("dispatch_message_txn.json"));
// when
let transaction_hashes = search_transactions(
transactions,
&interchain_payment_program_id,
&payment_pda_account,
is_interchain_payment_instruction,
);
// then
assert!(!transaction_hashes.is_empty());
}
fn read_json(path: &str) -> String {
let relative = PathBuf::new().join("src/log_meta_composer/").join(path);
let absolute = fs::canonicalize(relative).expect("cannot find path");
fs::read_to_string(absolute).expect("should have been able to read the file")
}
fn transactions(json: &str) -> Vec<EncodedTransactionWithStatusMeta> {
let transaction = serde_json::from_str::<EncodedTransactionWithStatusMeta>(json).unwrap();
let transactions = vec![transaction];
transactions
}

@ -46,8 +46,9 @@ use solana_sdk::{
};
use solana_transaction_status::{
EncodedConfirmedBlock, EncodedTransaction, EncodedTransactionWithStatusMeta, TransactionStatus,
UiCompiledInstruction, UiInnerInstructions, UiInstruction, UiMessage, UiParsedInstruction,
UiReturnDataEncoding, UiTransaction, UiTransactionReturnData, UiTransactionStatusMeta,
UiCompiledInstruction, UiConfirmedBlock, UiInnerInstructions, UiInstruction, UiMessage,
UiParsedInstruction, UiReturnDataEncoding, UiTransaction, UiTransactionReturnData,
UiTransactionStatusMeta,
};
use tracing::{debug, info, instrument, warn};
@ -62,8 +63,9 @@ use hyperlane_core::{
use crate::account::{search_accounts_by_discriminator, search_and_validate_account};
use crate::error::HyperlaneSealevelError;
use crate::transaction::{
is_message_delivery_instruction, is_message_dispatch_instruction, search_message_transactions,
use crate::log_meta_composer::{
is_interchain_payment_instruction, is_message_delivery_instruction,
is_message_dispatch_instruction, LogMetaComposer,
};
use crate::utils::{decode_h256, decode_h512, from_base58};
use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient};
@ -644,15 +646,32 @@ impl Mailbox for SealevelMailbox {
pub struct SealevelMailboxIndexer {
mailbox: SealevelMailbox,
program_id: Pubkey,
dispatch_message_log_meta_composer: LogMetaComposer,
delivery_message_log_meta_composer: LogMetaComposer,
}
impl SealevelMailboxIndexer {
pub fn new(conf: &ConnectionConf, locator: ContractLocator) -> ChainResult<Self> {
let program_id = Pubkey::from(<[u8; 32]>::from(locator.address));
let mailbox = SealevelMailbox::new(conf, locator, None)?;
let dispatch_message_log_meta_composer = LogMetaComposer::new(
mailbox.program_id,
"message dispatch".to_owned(),
is_message_dispatch_instruction,
);
let delivery_message_log_meta_composer = LogMetaComposer::new(
mailbox.program_id,
"message delivery".to_owned(),
is_message_delivery_instruction,
);
Ok(Self {
program_id,
mailbox,
dispatch_message_log_meta_composer,
delivery_message_log_meta_composer,
})
}
@ -728,20 +747,21 @@ impl SealevelMailboxIndexer {
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
) -> ChainResult<LogMeta> {
let error_msg_no_txn = "block which should contain message dispatch transaction does not contain any transaction".to_owned();
let error_msg_too_many_txns = "block contains more than one dispatch message transaction operating on the same dispatch message store PDA".to_owned();
let error_msg_no_txn_after_filtering = "block which should contain message dispatch transaction does not contain any after filtering".to_owned();
self.log_meta(
log_index,
message_storage_pda_pubkey,
message_account_slot,
&is_message_dispatch_instruction,
error_msg_no_txn,
error_msg_too_many_txns,
error_msg_no_txn_after_filtering,
)
.await
let block = self
.mailbox
.provider
.rpc()
.get_block(*message_account_slot)
.await?;
self.dispatch_message_log_meta_composer
.log_meta(
block,
log_index,
message_storage_pda_pubkey,
message_account_slot,
)
.map_err(Into::<ChainCommunicationError>::into)
}
async fn get_delivered_message_with_nonce(
@ -807,35 +827,6 @@ impl SealevelMailboxIndexer {
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
) -> ChainResult<LogMeta> {
let error_msg_no_txn = "block which should contain message delivery transaction does not contain any transaction".to_owned();
let error_msg_too_many_txns = "block contains more than one deliver message transaction operating on the same delivery message store PDA".to_owned();
let error_msg_no_txn_after_filtering = "block which should contain message delivery transaction does not contain any after filtering".to_owned();
self.log_meta(
log_index,
message_storage_pda_pubkey,
message_account_slot,
&is_message_delivery_instruction,
error_msg_no_txn,
error_msg_too_many_txns,
error_msg_no_txn_after_filtering,
)
.await
}
async fn log_meta<F>(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
is_message_instruction: &F,
error_msg_no_txn: String,
error_msg_too_many_txns: String,
error_msg_no_txn_after_filtering: String,
) -> ChainResult<LogMeta>
where
F: Fn(instruction::Instruction) -> bool,
{
let block = self
.mailbox
.provider
@ -843,44 +834,14 @@ impl SealevelMailboxIndexer {
.get_block(*message_account_slot)
.await?;
let block_hash = decode_h256(&block.blockhash)?;
let transactions = block
.transactions
.ok_or(HyperlaneSealevelError::NoTransactions(error_msg_no_txn))?;
let transaction_hashes = search_message_transactions(
&self.mailbox.program_id,
&message_storage_pda_pubkey,
transactions,
&is_message_instruction,
);
// We expect to see that there is only one message dispatch transaction
if transaction_hashes.len() > 1 {
Err(HyperlaneSealevelError::TooManyTransactions(
error_msg_too_many_txns,
))?
}
let (transaction_index, transaction_hash) =
transaction_hashes
.into_iter()
.next()
.ok_or(HyperlaneSealevelError::NoTransactions(
error_msg_no_txn_after_filtering,
))?;
let log_meta = LogMeta {
address: self.mailbox.program_id.to_bytes().into(),
block_number: *message_account_slot,
block_hash,
transaction_id: transaction_hash,
transaction_index: transaction_index as u64,
log_index,
};
Ok(log_meta)
self.delivery_message_log_meta_composer
.log_meta(
block,
log_index,
message_storage_pda_pubkey,
message_account_slot,
)
.map_err(Into::<ChainCommunicationError>::into)
}
}

@ -1,209 +0,0 @@
use std::collections::HashMap;
use hyperlane_sealevel_mailbox::instruction::Instruction;
use solana_sdk::pubkey::Pubkey;
use solana_transaction_status::option_serializer::OptionSerializer;
use solana_transaction_status::{
EncodedTransaction, EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiInstruction,
UiMessage, UiTransaction, UiTransactionStatusMeta,
};
use tracing::warn;
use hyperlane_core::H512;
use crate::utils::{decode_h512, from_base58};
/// This function searches for a transaction which specified instruction on Hyperlane message and
/// returns list of hashes of such transactions.
///
/// This function takes the mailbox program identifier and the identifier for PDA for storing
/// a dispatched or delivered message and searches a message dispatch or delivery transaction
/// in a list of transactions.
///
/// The list of transaction is usually comes from a block. The function returns list of hashes
/// of such transactions and their relative index in the block.
///
/// The transaction will be searched with the following criteria:
/// 1. Transaction contains Mailbox program id in the list of accounts.
/// 2. Transaction contains dispatched/delivered message PDA in the list of accounts.
/// 3. Transaction is performing the specified message instruction.
///
/// * `mailbox_program_id` - Identifier of Mailbox program
/// * `message_storage_pda_pubkey` - Identifier for message store PDA
/// * `transactions` - List of transactions
/// * `is_specified_message_instruction` - Function which returns `true` for specified message
/// instruction
pub fn search_message_transactions<F>(
mailbox_program_id: &Pubkey,
message_storage_pda_pubkey: &Pubkey,
transactions: Vec<EncodedTransactionWithStatusMeta>,
is_specified_message_instruction: &F,
) -> Vec<(usize, H512)>
where
F: Fn(Instruction) -> bool,
{
transactions
.into_iter()
.enumerate()
.filter_map(|(index, tx)| filter_by_encoding(tx).map(|(tx, meta)| (index, tx, meta)))
.filter_map(|(index, tx, meta)| {
filter_by_validity(tx, meta)
.map(|(hash, account_keys, instructions)| (index, hash, account_keys, instructions))
})
.filter_map(|(index, hash, account_keys, instructions)| {
filter_by_relevancy(
mailbox_program_id,
message_storage_pda_pubkey,
hash,
account_keys,
instructions,
is_specified_message_instruction,
)
.map(|hash| (index, hash))
})
.collect::<Vec<(usize, H512)>>()
}
fn filter_by_relevancy<F>(
mailbox_program_id: &Pubkey,
message_storage_pda_pubkey: &Pubkey,
hash: H512,
account_keys: Vec<String>,
instructions: Vec<UiCompiledInstruction>,
is_specified_message_instruction: &F,
) -> Option<H512>
where
F: Fn(Instruction) -> bool,
{
let account_index_map = account_index_map(account_keys);
let mailbox_program_id_str = mailbox_program_id.to_string();
let mailbox_program_index = match account_index_map.get(&mailbox_program_id_str) {
Some(i) => *i as u8,
None => return None, // If account keys do not contain Mailbox program, transaction is not message dispatch/delivery.
};
let message_storage_pda_pubkey_str = message_storage_pda_pubkey.to_string();
let message_storage_pda_account_index =
match account_index_map.get(&message_storage_pda_pubkey_str) {
Some(i) => *i as u8,
None => return None, // If account keys do not contain dispatch/delivery message store PDA account, transaction is not message dispatch/delivery.
};
let mailbox_program_maybe = instructions
.into_iter()
.find(|instruction| instruction.program_id_index == mailbox_program_index);
let mailbox_program = match mailbox_program_maybe {
Some(p) => p,
None => return None, // If transaction does not contain call into Mailbox, transaction is not message dispatch/delivery.
};
// If Mailbox program does not operate on dispatch/delivery message store PDA account, transaction is not message dispatch/delivery.
if !mailbox_program
.accounts
.contains(&message_storage_pda_account_index)
{
return None;
}
let instruction_data = match from_base58(&mailbox_program.data) {
Ok(d) => d,
Err(_) => return None, // If we cannot decode instruction data, transaction is not message dispatch/delivery.
};
let instruction = match Instruction::from_instruction_data(&instruction_data) {
Ok(ii) => ii,
Err(_) => return None, // If we cannot parse instruction data, transaction is not message dispatch/delivery.
};
// If the call into Mailbox program is not OutboxDispatch/InboxProcess, transaction is not message dispatch/delivery.
if is_specified_message_instruction(instruction) {
return None;
}
Some(hash)
}
pub fn is_message_dispatch_instruction(instruction: Instruction) -> bool {
!matches!(instruction, Instruction::OutboxDispatch(_))
}
pub fn is_message_delivery_instruction(instruction: Instruction) -> bool {
!matches!(instruction, Instruction::InboxProcess(_))
}
fn filter_by_validity(
tx: UiTransaction,
meta: UiTransactionStatusMeta,
) -> Option<(H512, Vec<String>, Vec<UiCompiledInstruction>)> {
let Some(transaction_hash) = tx
.signatures
.first()
.map(|signature| decode_h512(signature))
.and_then(|r| r.ok())
else {
warn!(
transaction = ?tx,
"transaction does not have any signatures or signatures cannot be decoded",
);
return None;
};
let UiMessage::Raw(message) = tx.message else {
warn!(message = ?tx.message, "we expect messages in Raw format");
return None;
};
let instructions = instructions(message.instructions, meta);
Some((transaction_hash, message.account_keys, instructions))
}
fn filter_by_encoding(
tx: EncodedTransactionWithStatusMeta,
) -> Option<(UiTransaction, UiTransactionStatusMeta)> {
match (tx.transaction, tx.meta) {
// We support only transactions encoded as JSON
// We need none-empty metadata as well
(EncodedTransaction::Json(t), Some(m)) => Some((t, m)),
t => {
warn!(
?t,
"transaction is not encoded as json or metadata is empty"
);
None
}
}
}
fn account_index_map(account_keys: Vec<String>) -> HashMap<String, usize> {
account_keys
.into_iter()
.enumerate()
.map(|(index, key)| (key, index))
.collect::<HashMap<String, usize>>()
}
/// Extract all instructions from transaction
fn instructions(
instruction: Vec<UiCompiledInstruction>,
meta: UiTransactionStatusMeta,
) -> Vec<UiCompiledInstruction> {
let inner_instructions = match meta.inner_instructions {
OptionSerializer::Some(ii) => ii
.into_iter()
.flat_map(|ii| ii.instructions)
.flat_map(|ii| match ii {
UiInstruction::Compiled(ci) => Some(ci),
_ => None,
})
.collect::<Vec<UiCompiledInstruction>>(),
OptionSerializer::None | OptionSerializer::Skip => vec![],
};
[instruction, inner_instructions].concat()
}
#[cfg(test)]
mod tests;

@ -1,547 +0,0 @@
use solana_sdk::pubkey::Pubkey;
use solana_transaction_status::EncodedTransactionWithStatusMeta;
use crate::transaction::{
is_message_delivery_instruction, is_message_dispatch_instruction, search_message_transactions,
};
use crate::utils::decode_pubkey;
#[test]
pub fn test_search_dispatched_message_transaction() {
// given
let dispatched_message_pda_account =
decode_pubkey("6eG8PheL41qLFFUtPjSYMtsp4aoAQsMgcsYwkGCB8kwT").unwrap();
let (mailbox_program_id, transactions) = transactions(DISPATCH_TXN_JSON);
// when
let transaction_hashes = search_message_transactions(
&mailbox_program_id,
&dispatched_message_pda_account,
transactions,
&is_message_dispatch_instruction,
);
// then
assert!(!transaction_hashes.is_empty());
}
#[test]
pub fn test_search_delivered_message_transaction() {
// given
let delivered_message_pda_account =
decode_pubkey("Dj7jk47KKXvw4nseNGdyHtNHtjPes2XSfByhF8xymrtS").unwrap();
let (mailbox_program_id, transactions) = transactions(DELIVERY_TXN_JSON);
// when
let transaction_hashes = search_message_transactions(
&mailbox_program_id,
&delivered_message_pda_account,
transactions,
&is_message_delivery_instruction,
);
// then
assert!(!transaction_hashes.is_empty());
}
fn transactions(json: &str) -> (Pubkey, Vec<EncodedTransactionWithStatusMeta>) {
let mailbox_program_id = decode_pubkey("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi").unwrap();
let transaction = serde_json::from_str::<EncodedTransactionWithStatusMeta>(json).unwrap();
let transactions = vec![transaction];
(mailbox_program_id, transactions)
}
const DISPATCH_TXN_JSON: &str = r#"
{
"blockTime": 1729865514,
"meta": {
"computeUnitsConsumed": 171834,
"err": null,
"fee": 3564950,
"innerInstructions": [
{
"index": 2,
"instructions": [
{
"accounts": [
8,
7,
6,
0
],
"data": "gCzo5F74HA9Pb",
"programIdIndex": 19,
"stackHeight": 2
},
{
"accounts": [
5,
11,
10,
18,
0,
1,
2
],
"data": "2Nsbnwq8JuYnSefHfRznxFtFqdPnbeydtt5kenfF8GR1ZU2XtF8jJDo4SUc2VY52V5C25WsKsQZBLsoCVQNzefgVj2bVznkThjuZuSKXJfZN9ADggiM2soRKVsAjf3xHm3CC3w3iyvK5U9LsjmYtiDNbJCFtEPRTDxsfvMS45Bg3q6EogmBN9JiZNLP",
"programIdIndex": 17,
"stackHeight": 2
},
{
"accounts": [
0,
5
],
"data": "3Bxs3zrfFUZbEPqZ",
"programIdIndex": 10,
"stackHeight": 3
},
{
"accounts": [
0,
2
],
"data": "11114XfZCGKrze4PNou1GXiYCJgiBCGpHks9hxjb8tFwYMjtgVtMzvriDxwYPdRqSoqztL",
"programIdIndex": 10,
"stackHeight": 3
},
{
"accounts": [
10,
0,
3,
1,
4,
9,
14
],
"data": "5MtKiLZhPB3NhS7Gus6CenAEMS2QBtpY9QtuLeVH4CkpUN7599vsYzZXhk8Vu",
"programIdIndex": 15,
"stackHeight": 2
},
{
"accounts": [
0,
9
],
"data": "3Bxs4A3YxXXYy5gj",
"programIdIndex": 10,
"stackHeight": 3
},
{
"accounts": [
0,
4
],
"data": "111158VjdPaAaGVkCbPZoXJqknHXBEqoypfVjf96mwePbKxAkrKfR2gUFyN7wD8ccc9g1z",
"programIdIndex": 10,
"stackHeight": 3
}
]
}
],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program 3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm invoke [1]",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
"Program log: Instruction: TransferChecked",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6200 of 983051 compute units",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi invoke [2]",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Protocol fee of 0 paid from FGyh1FfooV7AtVrYjFGmjMxbELC8RMxNp4xY5WY4L4md to BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Dispatched message to 1408864445, ID 0x09c74f3e10d98c112696b72ba1609aae47616f64f28b4cb1ad8a4a710e93ee89",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi consumed 86420 of 972001 compute units",
"Program return: E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi CcdPPhDZjBEmlrcroWCarkdhb2Tyi0yxrYpKcQ6T7ok=",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi success",
"Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv invoke [2]",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Paid IGP JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M for 431000 gas for message 0x09c7…ee89 to 1408864445",
"Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv consumed 42792 of 882552 compute units",
"Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv success",
"Program log: Warp route transfer completed to destination: 1408864445, recipient: 0xd41b…f050, remote_amount: 2206478600",
"Program 3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm consumed 171534 of 999700 compute units",
"Program 3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm success"
],
"postBalances": [
12374928,
0,
2241120,
1016160,
1872240,
8679120,
2039280,
319231603414,
2039280,
10172586528,
1,
890880,
1141440,
3361680,
1830480,
1141440,
1,
1141440,
1141440,
934087680
],
"postTokenBalances": [
{
"accountIndex": 6,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"owner": "CcquFeCYNZM48kLPyG3HWxdwgigmyxPBi6iHwve9Myhj",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "165697511204",
"decimals": 6,
"uiAmount": 165697.511204,
"uiAmountString": "165697.511204"
}
},
{
"accountIndex": 8,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"owner": "FGyh1FfooV7AtVrYjFGmjMxbELC8RMxNp4xY5WY4L4md",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "94",
"decimals": 6,
"uiAmount": 9.4E-5,
"uiAmountString": "0.000094"
}
}
],
"preBalances": [
22211372,
0,
0,
1016160,
0,
8679120,
2039280,
319231603414,
2039280,
10170428394,
1,
890880,
1141440,
3361680,
1830480,
1141440,
1,
1141440,
1141440,
934087680
],
"preTokenBalances": [
{
"accountIndex": 6,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"owner": "CcquFeCYNZM48kLPyG3HWxdwgigmyxPBi6iHwve9Myhj",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "163491032604",
"decimals": 6,
"uiAmount": 163491.032604,
"uiAmountString": "163491.032604"
}
},
{
"accountIndex": 8,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"owner": "FGyh1FfooV7AtVrYjFGmjMxbELC8RMxNp4xY5WY4L4md",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "2206478694",
"decimals": 6,
"uiAmount": 2206.478694,
"uiAmountString": "2206.478694"
}
}
],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 297626301,
"transaction": {
"message": {
"accountKeys": [
"FGyh1FfooV7AtVrYjFGmjMxbELC8RMxNp4xY5WY4L4md",
"8DqWVhEZcg4rDYwe5UFaopmGuEajiPz9L3A1ZnytMcUm",
"6eG8PheL41qLFFUtPjSYMtsp4aoAQsMgcsYwkGCB8kwT",
"8Cv4PHJ6Cf3xY7dse7wYeZKtuQv9SAN6ujt5w22a2uho",
"9yMwrDqHsbmmvYPS9h4MLPbe2biEykcL51W7qJSDL5hF",
"BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk",
"CcquFeCYNZM48kLPyG3HWxdwgigmyxPBi6iHwve9Myhj",
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"FDDbaNtod9pt7pmR8qtmRZJtEj9NViDA7J6cazqUjXQj",
"JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M",
"11111111111111111111111111111111",
"37N3sbyVAd3KvQsPw42i1LWkLahzL4ninVQ4n1NmnHjS",
"3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm",
"AHX3iiEPFMyygANrp15cyUr63o9qGkwkB6ki1pgpZ7gZ",
"AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF",
"BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv",
"ComputeBudget111111111111111111111111111111",
"E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi",
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
],
"header": {
"numReadonlySignedAccounts": 1,
"numReadonlyUnsignedAccounts": 10,
"numRequiredSignatures": 2
},
"instructions": [
{
"accounts": [],
"data": "FjL4FH",
"programIdIndex": 16,
"stackHeight": null
},
{
"accounts": [],
"data": "3butUEijJrLf",
"programIdIndex": 16,
"stackHeight": null
},
{
"accounts": [
10,
18,
13,
17,
5,
11,
0,
1,
2,
15,
3,
4,
14,
9,
19,
7,
8,
6
],
"data": "RpjV6TtUSvt6UnMXdNo4h1Ze2VGVifo65r2jqRBUq6HJKhskSnwWybXyB4NxgfvedV9vhKdmDPg8sFT64JEZvxF8VfoGdqoAFt4WFLSB",
"programIdIndex": 12,
"stackHeight": null
}
],
"recentBlockhash": "GHQhVUy7Eq3hcps8YoG9DCd1Tb6ccQZ9xhh81ju8ujHJ"
},
"signatures": [
"4nRGgV9tqCuiKUXeBzWdvdk6YC9BsGWUZurAVQLMX1NwNPpysbZNwXu97Sw4aM9REwaRmWS7gaiSKXbwtmw6oLRi",
"hXjvQbAuFH9vAxZMdGqfnSjN7t7Z7NLTzRq1SG8i6fLr9LS6XahTduPWqakiTsLDyWSofvq3MSncUAkbQLEj85f"
]
}
}
"#;
const DELIVERY_TXN_JSON: &str = r#"
{
"blockTime": 1726514134,
"meta": {
"computeUnitsConsumed": 200654,
"err": null,
"fee": 5000,
"innerInstructions": [
{
"index": 1,
"instructions": [
{
"accounts": [
10
],
"data": "8YGwT5LUTP4",
"programIdIndex": 9,
"stackHeight": 2
},
{
"accounts": [
12
],
"data": "2848tnNKZjKzgKikguTY4s5nESn7KLYUbLsrp6Z1FYq4BmM31xRwBXnJU5RW9rEvRUjJfJa58kXdgQYEQpg4sDrRfx5HnGsgXfitkxJw5NKVcFAYLSqKvpkYxer2tAn3a8ZzPvuDD9iqyLkvJnRZ3TbcoAHNisFfvBeWK95YL8zxsyzDS9ZBMaoYrLKQx9b915xj9oijw2UNk7FF5qxThZDKwF8rwckb6t2o6ypzFEqYeQCsRW5quayYsLBjHi8RdY18NDkcnPVkQbdR7FmfrncV4H5ZYZaayMtgAs6kHxRgeuuBEtrYG1UbGjWTQAss9zmeXcKipqS3S2bee96U5w9Cd981e8dkakCtKR7KusjE9nhsFTfXoxcwkRhi3TzqDicrqt7Erf78K",
"programIdIndex": 8,
"stackHeight": 2
},
{
"accounts": [
0,
3
],
"data": "11117UpxCJ2YqmddN2ykgdMGRXkyPgnqEtj5XYrnk1iC4P1xrvXq2zvZQkj3uNaitHEw2k",
"programIdIndex": 5,
"stackHeight": 2
},
{
"accounts": [
11,
5,
10,
1,
5,
2
],
"data": "7MHiQP8ahsZcB5cM9ZXGa2foMYQENm7GnrFaV4AmfgKNzSndaXhrcqbVNRgN2kGmrrsfTi8bNEGkAJn6MWjY95PnakaF2HAchXrUUBzQrWKQdRp8VbKjDsnH1tEUiAWm439Y12TpWTW3uSphh1oycpTJP",
"programIdIndex": 9,
"stackHeight": 2
},
{
"accounts": [
2,
1
],
"data": "3Bxs4ThwQbE4vyj5",
"programIdIndex": 5,
"stackHeight": 3
}
]
}
],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi invoke [1]",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg invoke [2]",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg consumed 4402 of 1363482 compute units",
"Program return: 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg AA==",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg success",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC invoke [2]",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC consumed 106563 of 1353660 compute units",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg invoke [2]",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Warp route transfer completed from origin: 1408864445, recipient: 528MctBmY7rXqufM3r8k7t9DTfVNuB4K1rr8xVU4naJM, remote_amount: 100000",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg consumed 28117 of 1240216 compute units",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg success",
"Program log: Hyperlane inbox processed message 0x34ed0705362554568a1a2d24aef6bfde71894dd1bb2f0457fb4bd66016074fcc",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi consumed 200504 of 1399850 compute units",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi success"
],
"postBalances": [
338367600,
199691000,
891880,
1287600,
1211040,
1,
1,
1141440,
1141440,
1141440,
2686560,
0,
8017920,
1141440
],
"postTokenBalances": [],
"preBalances": [
339660200,
199591000,
991880,
0,
1211040,
1,
1,
1141440,
1141440,
1141440,
2686560,
0,
8017920,
1141440
],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 290198208,
"transaction": {
"message": {
"accountKeys": [
"G5FM3UKwcBJ47PwLWLLY1RQpqNtTMgnqnd6nZGcJqaBp",
"528MctBmY7rXqufM3r8k7t9DTfVNuB4K1rr8xVU4naJM",
"5H4cmX5ybSqK6Ro6nvr9eiR8G8ATTYRwVsZ42VRRW3wa",
"Dj7jk47KKXvw4nseNGdyHtNHtjPes2XSfByhF8xymrtS",
"H3EgdESu59M4hn5wrbeyi9VjmFiLYM7iUAbGtrA5uHNE",
"11111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111",
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
"372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC",
"4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg",
"A2nmLy86tmraneRMEZ5yWbDGq6YsPKNcESGaTZKkRWZU",
"DmU32nL975xAshVYgLLdyMoaUzHa2aCzHJyfLyKRdz3M",
"E2jimXLCtTiuZ6jbXP8B7SyZ5vVc1PKYYnMeho9yJ1en",
"E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi"
],
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 9,
"numRequiredSignatures": 1
},
"instructions": [
{
"accounts": [],
"data": "K1FDJ7",
"programIdIndex": 6,
"stackHeight": null
},
{
"accounts": [
0,
5,
4,
11,
3,
10,
7,
8,
12,
9,
5,
10,
1,
5,
2
],
"data": "3RwSrioTudpACxczi2EejzKoZCPVuzq6qWLCQYAWoZoTcRPBobUn7tB5SFvMPNHGJ551rmjXDyKdaQLuzX3d5bjHSrSsquwHqWgM6L2kMEEJZjtygNyx3RhJD9GyZqekDuK19cfYfn1dyLuo7SSqswV3t6yptLhnCv8DhxBLRuXhV2GdNy9PLU3VNc9PvPWxg1Grtr9UZ5GnmdKDeqRvonM9AqmuN6mnv3UaqjjAEX8yDKPhWHm6w1HRzfgbjkXQVL5aSqdgJeF3EVBKJCzvMKbUVjTRgD6iHQyUVrSYvrHpKZxc6EctBHN6tyeZrW5RD1M6giasnm4WqrjDwUyz9xwvk31srJrZp7W7D6i2tTajmBbiKjpNo75iaHj4dycf1H",
"programIdIndex": 13,
"stackHeight": null
}
],
"recentBlockhash": "AzQN8x5uKk7ExXW4eUu2FiqRG1BX73uvfHcQeBDHcu8a"
},
"signatures": [
"5pBEVfDD3siir1CBf9taeWuee44GspA7EixYkKnzN1hkeYXLxtKYrbe3aE6hxswbY3hhDRVPDor1ZsSXUorC7bcR"
]
}
}
"#;

@ -41,6 +41,19 @@ pub enum Instruction {
Claim,
}
impl Instruction {
/// Deserializes an instruction from a slice.
pub fn from_instruction_data(data: &[u8]) -> Result<Self, ProgramError> {
Self::try_from_slice(data).map_err(|_| ProgramError::InvalidInstructionData)
}
/// Serializes an instruction into a vector of bytes.
pub fn into_instruction_data(self) -> Result<Vec<u8>, ProgramError> {
self.try_to_vec()
.map_err(|err| ProgramError::BorshIoError(err.to_string()))
}
}
/// Initializes an IGP.
#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)]
pub struct InitIgp {

Loading…
Cancel
Save