diff --git a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs index 3c2cbd1c1..9a38fb487 100644 --- a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs +++ b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.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 { + 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::::into) + } + fn interchain_payment_account(&self, account: &Account) -> ChainResult { let unique_gas_payment_pubkey = Pubkey::new(&account.data); let (expected_pubkey, _bump) = Pubkey::try_find_program_address( diff --git a/rust/main/chains/hyperlane-sealevel/src/lib.rs b/rust/main/chains/hyperlane-sealevel/src/lib.rs index 941c64a7b..90a2e01b6 100644 --- a/rust/main/chains/hyperlane-sealevel/src/lib.rs +++ b/rust/main/chains/hyperlane-sealevel/src/lib.rs @@ -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; diff --git a/rust/main/chains/hyperlane-sealevel/src/log_meta_composer.rs b/rust/main/chains/hyperlane-sealevel/src/log_meta_composer.rs new file mode 100644 index 000000000..f530b04e0 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/log_meta_composer.rs @@ -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 { + 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, + 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::>() +} + +fn filter_by_relevancy( + program_id: &Pubkey, + message_storage_pda_pubkey: &Pubkey, + hash: H512, + account_keys: Vec, + instructions: Vec, + is_specified_instruction: fn(&[u8]) -> bool, +) -> Option { + 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, Vec)> { + 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) -> HashMap { + account_keys + .into_iter() + .enumerate() + .map(|(index, key)| (key, index)) + .collect::>() +} + +/// Extract all instructions from transaction +fn instructions( + instruction: Vec, + meta: UiTransactionStatusMeta, +) -> Vec { + 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::>(), + OptionSerializer::None | OptionSerializer::Skip => vec![], + }; + + [instruction, inner_instructions].concat() +} + +#[cfg(test)] +mod tests; diff --git a/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/delivery_message_txn.json b/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/delivery_message_txn.json new file mode 100644 index 000000000..a778e6354 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/delivery_message_txn.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/dispatch_message_txn.json b/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/dispatch_message_txn.json new file mode 100644 index 000000000..59b411eb5 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/dispatch_message_txn.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/tests.rs b/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/tests.rs new file mode 100644 index 000000000..d1097b1b0 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/log_meta_composer/tests.rs @@ -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 { + let transaction = serde_json::from_str::(json).unwrap(); + let transactions = vec![transaction]; + transactions +} diff --git a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs index 6e5c21c70..21a9ff28b 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs @@ -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 { 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 { - 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::::into) } async fn get_delivered_message_with_nonce( @@ -807,35 +827,6 @@ impl SealevelMailboxIndexer { message_storage_pda_pubkey: &Pubkey, message_account_slot: &Slot, ) -> ChainResult { - 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( - &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 - 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::::into) } } diff --git a/rust/main/chains/hyperlane-sealevel/src/transaction.rs b/rust/main/chains/hyperlane-sealevel/src/transaction.rs deleted file mode 100644 index bf6dc29f8..000000000 --- a/rust/main/chains/hyperlane-sealevel/src/transaction.rs +++ /dev/null @@ -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( - mailbox_program_id: &Pubkey, - message_storage_pda_pubkey: &Pubkey, - transactions: Vec, - 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::>() -} - -fn filter_by_relevancy( - mailbox_program_id: &Pubkey, - message_storage_pda_pubkey: &Pubkey, - hash: H512, - account_keys: Vec, - instructions: Vec, - is_specified_message_instruction: &F, -) -> Option -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, Vec)> { - 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) -> HashMap { - account_keys - .into_iter() - .enumerate() - .map(|(index, key)| (key, index)) - .collect::>() -} - -/// Extract all instructions from transaction -fn instructions( - instruction: Vec, - meta: UiTransactionStatusMeta, -) -> Vec { - 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::>(), - OptionSerializer::None | OptionSerializer::Skip => vec![], - }; - - [instruction, inner_instructions].concat() -} - -#[cfg(test)] -mod tests; diff --git a/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs b/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs deleted file mode 100644 index 634418c21..000000000 --- a/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs +++ /dev/null @@ -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) { - let mailbox_program_id = decode_pubkey("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi").unwrap(); - let transaction = serde_json::from_str::(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" - ] - } -} -"#; diff --git a/rust/sealevel/programs/hyperlane-sealevel-igp/src/instruction.rs b/rust/sealevel/programs/hyperlane-sealevel-igp/src/instruction.rs index a4fb51984..3dd26948d 100644 --- a/rust/sealevel/programs/hyperlane-sealevel-igp/src/instruction.rs +++ b/rust/sealevel/programs/hyperlane-sealevel-igp/src/instruction.rs @@ -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::try_from_slice(data).map_err(|_| ProgramError::InvalidInstructionData) + } + + /// Serializes an instruction into a vector of bytes. + pub fn into_instruction_data(self) -> Result, ProgramError> { + self.try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string())) + } +} + /// Initializes an IGP. #[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] pub struct InitIgp {