From c87cfbd51298eba41da1e57ca3a8c9f6e1d4edc3 Mon Sep 17 00:00:00 2001 From: Danil Nemirovsky Date: Wed, 30 Oct 2024 17:57:47 +0000 Subject: [PATCH] feat: Scrape Sealevel dispatched messages (#4776) ### Description Scraper is able to index dispatch messages: 1. Blocks are stored into database 2. Transactions are stored into database (need population of all fields) 3. Dispatched messages are stored into database ### Drive-by changes Initial indexing of delivered messages (so that Scraper does not crush) ### Related issues - Contributes into https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4272 ### Backward compatibility Yes (Solana-like chains should not be enabled for Scraper) ### Testing Manual run of Scraper E2E Tests --------- Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com> --- .../chains/hyperlane-sealevel/src/account.rs | 68 ++++ .../chains/hyperlane-sealevel/src/error.rs | 18 +- .../hyperlane-sealevel/src/interchain_gas.rs | 114 +++--- .../main/chains/hyperlane-sealevel/src/lib.rs | 3 + .../chains/hyperlane-sealevel/src/mailbox.rs | 231 +++++++----- .../chains/hyperlane-sealevel/src/provider.rs | 73 +++- .../hyperlane-sealevel/src/rpc/client.rs | 39 ++- .../hyperlane-sealevel/src/transaction.rs | 188 ++++++++++ .../src/transaction/tests.rs | 329 ++++++++++++++++++ .../chains/hyperlane-sealevel/src/utils.rs | 33 ++ 10 files changed, 916 insertions(+), 180 deletions(-) create mode 100644 rust/main/chains/hyperlane-sealevel/src/account.rs create mode 100644 rust/main/chains/hyperlane-sealevel/src/transaction.rs create mode 100644 rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs create mode 100644 rust/main/chains/hyperlane-sealevel/src/utils.rs diff --git a/rust/main/chains/hyperlane-sealevel/src/account.rs b/rust/main/chains/hyperlane-sealevel/src/account.rs new file mode 100644 index 0000000000..f671c432e8 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/account.rs @@ -0,0 +1,68 @@ +use base64::{engine::general_purpose::STANDARD as Base64, Engine}; +use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}; +use solana_client::{ + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, +}; +use solana_sdk::{account::Account, commitment_config::CommitmentConfig, pubkey::Pubkey}; + +use hyperlane_core::{ChainCommunicationError, ChainResult}; + +use crate::rpc::SealevelRpcClient; + +pub async fn search_accounts_by_discriminator( + client: &SealevelRpcClient, + program_id: &Pubkey, + discriminator: &[u8; 8], + nonce_bytes: &[u8], + offset: usize, + length: usize, +) -> ChainResult> { + let target_message_account_bytes = &[discriminator, nonce_bytes].concat(); + let target_message_account_bytes = Base64.encode(target_message_account_bytes); + + // First, find all accounts with the matching account data. + // To keep responses small in case there is ever more than 1 + // match, we don't request the full account data, and just request + // the field which was used to generate account id + #[allow(deprecated)] + let memcmp = RpcFilterType::Memcmp(Memcmp { + // Ignore the first byte, which is the `initialized` bool flag. + offset: 1, + bytes: MemcmpEncodedBytes::Base64(target_message_account_bytes), + encoding: None, + }); + let config = RpcProgramAccountsConfig { + filters: Some(vec![memcmp]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: Some(UiDataSliceConfig { offset, length }), + commitment: Some(CommitmentConfig::finalized()), + min_context_slot: None, + }, + with_context: Some(false), + }; + let accounts = client + .get_program_accounts_with_config(program_id, config) + .await?; + Ok(accounts) +} + +pub fn search_and_validate_account( + accounts: Vec<(Pubkey, Account)>, + message_account: F, +) -> ChainResult +where + F: Fn(&Account) -> ChainResult, +{ + for (pubkey, account) in accounts { + let expected_pubkey = message_account(&account)?; + if expected_pubkey == pubkey { + return Ok(pubkey); + } + } + + Err(ChainCommunicationError::from_other_str( + "Could not find valid storage PDA pubkey", + )) +} diff --git a/rust/main/chains/hyperlane-sealevel/src/error.rs b/rust/main/chains/hyperlane-sealevel/src/error.rs index ff0142c393..4ba2171191 100644 --- a/rust/main/chains/hyperlane-sealevel/src/error.rs +++ b/rust/main/chains/hyperlane-sealevel/src/error.rs @@ -1,6 +1,7 @@ -use hyperlane_core::ChainCommunicationError; +use hyperlane_core::{ChainCommunicationError, H512}; use solana_client::client_error::ClientError; use solana_sdk::pubkey::ParsePubkeyError; +use solana_transaction_status::EncodedTransaction; /// Errors from the crates specific to the hyperlane-sealevel /// implementation. @@ -17,6 +18,21 @@ pub enum HyperlaneSealevelError { /// Decoding error #[error("{0}")] Decoding(#[from] solana_sdk::bs58::decode::Error), + /// No transaction in block error + #[error("{0}")] + NoTransactions(String), + /// Too many transactions of particular content in block + #[error("{0}")] + TooManyTransactions(String), + /// Unsupported transaction encoding + #[error("{0:?}")] + UnsupportedTransactionEncoding(EncodedTransaction), + /// Unsigned transaction + #[error("{0}")] + UnsignedTransaction(H512), + /// Incorrect transaction + #[error("received incorrect transaction, expected hash: {0:?}, received hash: {1:?}")] + IncorrectTransaction(Box, Box), } impl From for ChainCommunicationError { diff --git a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs index d2f78eb4b4..3c2cbd1c16 100644 --- a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs +++ b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs @@ -1,25 +1,22 @@ +use std::ops::RangeInclusive; + use async_trait::async_trait; -use hyperlane_core::{ - config::StrOrIntParseError, ChainCommunicationError, ChainResult, ContractLocator, - HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, - InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, H512, -}; +use derive_new::new; use hyperlane_sealevel_igp::{ accounts::{GasPaymentAccount, ProgramDataAccount}, igp_gas_payment_pda_seeds, igp_program_data_pda_seeds, }; -use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}; -use solana_client::{ - rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, - rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, -}; -use std::ops::RangeInclusive; +use solana_sdk::{account::Account, pubkey::Pubkey}; use tracing::{info, instrument}; -use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; -use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; +use hyperlane_core::{ + config::StrOrIntParseError, ChainCommunicationError, ChainResult, ContractLocator, + HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, + InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, H512, +}; -use derive_new::new; +use crate::account::{search_accounts_by_discriminator, search_and_validate_account}; +use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; /// The offset to get the `unique_gas_payment_pubkey` field from the serialized GasPaymentData. /// The account data includes prefixes that are accounted for here: a 1 byte initialized flag @@ -121,70 +118,23 @@ impl SealevelInterchainGasPaymasterIndexer { &self, sequence_number: u64, ) -> ChainResult { - let payment_bytes = &[ - &hyperlane_sealevel_igp::accounts::GAS_PAYMENT_DISCRIMINATOR[..], - &sequence_number.to_le_bytes()[..], - ] - .concat(); - #[allow(deprecated)] - let payment_bytes: String = base64::encode(payment_bytes); - - // First, find all accounts with the matching gas payment data. - // To keep responses small in case there is ever more than 1 - // match, we don't request the full account data, and just request - // the `unique_gas_payment_pubkey` field. - #[allow(deprecated)] - let memcmp = RpcFilterType::Memcmp(Memcmp { - // Ignore the first byte, which is the `initialized` bool flag. - offset: 1, - bytes: MemcmpEncodedBytes::Base64(payment_bytes), - encoding: None, - }); - let config = RpcProgramAccountsConfig { - filters: Some(vec![memcmp]), - account_config: RpcAccountInfoConfig { - encoding: Some(UiAccountEncoding::Base64), - // Don't return any data - data_slice: Some(UiDataSliceConfig { - offset: UNIQUE_GAS_PAYMENT_PUBKEY_OFFSET, - length: 32, // the length of the `unique_gas_payment_pubkey` field - }), - commitment: Some(CommitmentConfig::finalized()), - min_context_slot: None, - }, - with_context: Some(false), - }; - tracing::debug!(config=?config, "Fetching program accounts"); - let accounts = self - .rpc_client - .get_program_accounts_with_config(&self.igp.program_id, config) - .await?; + let discriminator = hyperlane_sealevel_igp::accounts::GAS_PAYMENT_DISCRIMINATOR; + let sequence_number_bytes = sequence_number.to_le_bytes(); + let unique_gas_payment_pubkey_length = 32; // the length of the `unique_gas_payment_pubkey` field + let accounts = search_accounts_by_discriminator( + &self.rpc_client, + &self.igp.program_id, + discriminator, + &sequence_number_bytes, + UNIQUE_GAS_PAYMENT_PUBKEY_OFFSET, + unique_gas_payment_pubkey_length, + ) + .await?; tracing::debug!(accounts=?accounts, "Fetched program accounts"); - // Now loop through matching accounts and find the one with a valid account pubkey - // that proves it's an actual gas payment PDA. - let mut valid_payment_pda_pubkey = Option::::None; - - for (pubkey, account) in accounts { - let unique_gas_payment_pubkey = Pubkey::new(&account.data); - let (expected_pubkey, _bump) = Pubkey::try_find_program_address( - igp_gas_payment_pda_seeds!(unique_gas_payment_pubkey), - &self.igp.program_id, - ) - .ok_or_else(|| { - ChainCommunicationError::from_other_str( - "Could not find program address for unique_gas_payment_pubkey", - ) - })?; - if expected_pubkey == pubkey { - valid_payment_pda_pubkey = Some(pubkey); - break; - } - } - - let valid_payment_pda_pubkey = valid_payment_pda_pubkey.ok_or_else(|| { - ChainCommunicationError::from_other_str("Could not find valid gas payment PDA pubkey") + let valid_payment_pda_pubkey = search_and_validate_account(accounts, |account| { + self.interchain_payment_account(account) })?; // Now that we have the valid gas payment PDA pubkey, we can get the full account data. @@ -224,6 +174,20 @@ impl SealevelInterchainGasPaymasterIndexer { H256::from(gas_payment_account.igp.to_bytes()), )) } + + 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( + igp_gas_payment_pda_seeds!(unique_gas_payment_pubkey), + &self.igp.program_id, + ) + .ok_or_else(|| { + ChainCommunicationError::from_other_str( + "Could not find program address for unique_gas_payment_pubkey", + ) + })?; + Ok(expected_pubkey) + } } #[async_trait] diff --git a/rust/main/chains/hyperlane-sealevel/src/lib.rs b/rust/main/chains/hyperlane-sealevel/src/lib.rs index 04e2218c6b..941c64a7bd 100644 --- a/rust/main/chains/hyperlane-sealevel/src/lib.rs +++ b/rust/main/chains/hyperlane-sealevel/src/lib.rs @@ -15,6 +15,7 @@ pub use solana_sdk::signer::keypair::Keypair; pub use trait_builder::*; pub use validator_announce::*; +mod account; mod error; mod interchain_gas; mod interchain_security_module; @@ -24,4 +25,6 @@ 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/mailbox.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs index 5e348d9995..45e4461279 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs @@ -4,22 +4,15 @@ use std::{collections::HashMap, num::NonZeroU64, ops::RangeInclusive, str::FromS use async_trait::async_trait; use borsh::{BorshDeserialize, BorshSerialize}; -use jsonrpc_core::futures_util::TryFutureExt; -use tracing::{debug, info, instrument, warn}; - -use hyperlane_core::{ - accumulator::incremental::IncrementalMerkle, BatchItem, ChainCommunicationError, - ChainCommunicationError::ContractError, ChainResult, Checkpoint, ContractLocator, Decode as _, - Encode as _, FixedPointNumber, HyperlaneAbi, HyperlaneChain, HyperlaneContract, - HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, KnownHyperlaneDomain, - LogMeta, Mailbox, MerkleTreeHook, ReorgPeriod, SequenceAwareIndexer, TxCostEstimate, TxOutcome, - H256, H512, U256, -}; use hyperlane_sealevel_interchain_security_module_interface::{ InterchainSecurityModuleInstruction, VerifyInstruction, }; use hyperlane_sealevel_mailbox::{ - accounts::{DispatchedMessageAccount, InboxAccount, OutboxAccount}, + accounts::{ + DispatchedMessageAccount, InboxAccount, OutboxAccount, ProcessedMessage, + ProcessedMessageAccount, DISPATCHED_MESSAGE_DISCRIMINATOR, PROCESSED_MESSAGE_DISCRIMINATOR, + }, + instruction, instruction::InboxProcess, mailbox_dispatched_message_pda_seeds, mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds, mailbox_process_authority_pda_seeds, mailbox_processed_message_pda_seeds, @@ -27,6 +20,7 @@ use hyperlane_sealevel_mailbox::{ use hyperlane_sealevel_message_recipient_interface::{ HandleInstruction, MessageRecipientInstruction, }; +use jsonrpc_core::futures_util::TryFutureExt; use serializable_account_meta::SimulationReturnData; use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}; use solana_client::{ @@ -51,10 +45,24 @@ use solana_sdk::{ }; use solana_transaction_status::{ EncodedConfirmedBlock, EncodedTransaction, EncodedTransactionWithStatusMeta, TransactionStatus, - UiInnerInstructions, UiInstruction, UiMessage, UiParsedInstruction, UiReturnDataEncoding, - UiTransaction, UiTransactionReturnData, UiTransactionStatusMeta, + UiCompiledInstruction, UiInnerInstructions, UiInstruction, UiMessage, UiParsedInstruction, + UiReturnDataEncoding, UiTransaction, UiTransactionReturnData, UiTransactionStatusMeta, +}; +use tracing::{debug, info, instrument, warn}; + +use hyperlane_core::{ + accumulator::incremental::IncrementalMerkle, BatchItem, ChainCommunicationError, + ChainCommunicationError::ContractError, ChainResult, Checkpoint, ContractLocator, Decode as _, + Encode as _, FixedPointNumber, HyperlaneAbi, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, KnownHyperlaneDomain, + LogMeta, Mailbox, MerkleTreeHook, ReorgPeriod, SequenceAwareIndexer, TxCostEstimate, TxOutcome, + H256, H512, U256, }; +use crate::account::{search_accounts_by_discriminator, search_and_validate_account}; +use crate::error::HyperlaneSealevelError; +use crate::transaction::search_dispatched_message_transactions; +use crate::utils::{decode_h256, decode_h512, from_base58}; use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; const SYSTEM_PROGRAM: &str = "11111111111111111111111111111111"; @@ -653,73 +661,26 @@ impl SealevelMailboxIndexer { self.rpc().get_block_height().await } - async fn get_message_with_nonce( + async fn get_dispatched_message_with_nonce( &self, nonce: u32, ) -> ChainResult<(Indexed, LogMeta)> { - let target_message_account_bytes = &[ - &hyperlane_sealevel_mailbox::accounts::DISPATCHED_MESSAGE_DISCRIMINATOR[..], - &nonce.to_le_bytes()[..], - ] - .concat(); - let target_message_account_bytes = base64::encode(target_message_account_bytes); - - // First, find all accounts with the matching account data. - // To keep responses small in case there is ever more than 1 - // match, we don't request the full account data, and just request - // the `unique_message_pubkey` field. - let memcmp = RpcFilterType::Memcmp(Memcmp { - // Ignore the first byte, which is the `initialized` bool flag. - offset: 1, - bytes: MemcmpEncodedBytes::Base64(target_message_account_bytes), - encoding: None, - }); - let config = RpcProgramAccountsConfig { - filters: Some(vec![memcmp]), - account_config: RpcAccountInfoConfig { - encoding: Some(UiAccountEncoding::Base64), - // Don't return any data - data_slice: Some(UiDataSliceConfig { - offset: 1 + 8 + 4 + 8, // the offset to get the `unique_message_pubkey` field - length: 32, // the length of the `unique_message_pubkey` field - }), - commitment: Some(CommitmentConfig::finalized()), - min_context_slot: None, - }, - with_context: Some(false), - }; - let accounts = self - .rpc() - .get_program_accounts_with_config(&self.mailbox.program_id, config) - .await?; - - // Now loop through matching accounts and find the one with a valid account pubkey - // that proves it's an actual message storage PDA. - let mut valid_message_storage_pda_pubkey = Option::::None; - - for (pubkey, account) in accounts { - let unique_message_pubkey = Pubkey::new(&account.data); - let (expected_pubkey, _bump) = Pubkey::try_find_program_address( - mailbox_dispatched_message_pda_seeds!(unique_message_pubkey), - &self.mailbox.program_id, - ) - .ok_or_else(|| { - ChainCommunicationError::from_other_str( - "Could not find program address for unique_message_pubkey", - ) - })?; - if expected_pubkey == pubkey { - valid_message_storage_pda_pubkey = Some(pubkey); - break; - } - } + let nonce_bytes = nonce.to_le_bytes(); + let unique_dispatched_message_pubkey_offset = 1 + 8 + 4 + 8; // the offset to get the `unique_message_pubkey` field + let unique_dispatch_message_pubkey_length = 32; // the length of the `unique_message_pubkey` field + let accounts = search_accounts_by_discriminator( + self.rpc(), + &self.program_id, + &DISPATCHED_MESSAGE_DISCRIMINATOR, + &nonce_bytes, + unique_dispatched_message_pubkey_offset, + unique_dispatch_message_pubkey_length, + ) + .await?; - let valid_message_storage_pda_pubkey = - valid_message_storage_pda_pubkey.ok_or_else(|| { - ChainCommunicationError::from_other_str( - "Could not find valid message storage PDA pubkey", - ) - })?; + let valid_message_storage_pda_pubkey = search_and_validate_account(accounts, |account| { + self.dispatched_message_account(&account) + })?; // Now that we have the valid message storage PDA pubkey, we can get the full account data. let account = self @@ -733,11 +694,99 @@ impl SealevelMailboxIndexer { let hyperlane_message = HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?; + let block = self + .mailbox + .provider + .rpc() + .get_block(dispatched_message_account.slot) + .await?; + let block_hash = decode_h256(&block.blockhash)?; + + let transactions = + block.transactions.ok_or(HyperlaneSealevelError::NoTransactions("block which should contain message dispatch transaction does not contain any transaction".to_owned()))?; + + let transaction_hashes = search_dispatched_message_transactions( + &self.mailbox.program_id, + &valid_message_storage_pda_pubkey, + transactions, + ); + + // We expect to see that there is only one message dispatch transaction + if transaction_hashes.len() > 1 { + Err(HyperlaneSealevelError::TooManyTransactions("Block contains more than one dispatch message transaction operating on the same dispatch message store PDA".to_owned()))? + } + + let (transaction_index, transaction_hash) = transaction_hashes + .into_iter() + .next() + .ok_or(HyperlaneSealevelError::NoTransactions("block which should contain message dispatch transaction does not contain any after filtering".to_owned()))?; + Ok(( hyperlane_message.into(), LogMeta { address: self.mailbox.program_id.to_bytes().into(), block_number: dispatched_message_account.slot, + block_hash, + transaction_id: transaction_hash, + transaction_index: transaction_index as u64, + log_index: U256::from(nonce), + }, + )) + } + + fn dispatched_message_account(&self, account: &Account) -> ChainResult { + let unique_message_pubkey = Pubkey::new(&account.data); + let (expected_pubkey, _bump) = Pubkey::try_find_program_address( + mailbox_dispatched_message_pda_seeds!(unique_message_pubkey), + &self.mailbox.program_id, + ) + .ok_or_else(|| { + ChainCommunicationError::from_other_str( + "Could not find program address for unique message pubkey", + ) + })?; + Ok(expected_pubkey) + } + + async fn get_delivered_message_with_nonce( + &self, + nonce: u32, + ) -> ChainResult<(Indexed, LogMeta)> { + let nonce_bytes = nonce.to_le_bytes(); + let delivered_message_id_offset = 1 + 8 + 8; // the offset to get the `message_id` field + let delivered_message_id_length = 32; + let accounts = search_accounts_by_discriminator( + self.rpc(), + &self.program_id, + &PROCESSED_MESSAGE_DISCRIMINATOR, + &nonce_bytes, + delivered_message_id_offset, + delivered_message_id_length, + ) + .await?; + + debug!(account_len = ?accounts.len(), "Found accounts with processed message discriminator"); + + let valid_message_storage_pda_pubkey = search_and_validate_account(accounts, |account| { + self.delivered_message_account(&account) + })?; + + // Now that we have the valid delivered message storage PDA pubkey, + // we can get the full account data. + let account = self + .rpc() + .get_account_with_finalized_commitment(&valid_message_storage_pda_pubkey) + .await?; + let delivered_message_account = ProcessedMessageAccount::fetch(&mut account.data.as_ref()) + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + let message_id = delivered_message_account.message_id; + + Ok(( + message_id.into(), + LogMeta { + address: self.mailbox.program_id.to_bytes().into(), + block_number: delivered_message_account.slot, // TODO: get these when building out scraper support. // It's inconvenient to get these :| block_hash: H256::zero(), @@ -747,6 +796,18 @@ impl SealevelMailboxIndexer { }, )) } + + fn delivered_message_account(&self, account: &Account) -> ChainResult { + let message_id = H256::from_slice(&account.data); + let (expected_pubkey, _bump) = Pubkey::try_find_program_address( + mailbox_processed_message_pda_seeds!(message_id), + &self.mailbox.program_id, + ) + .ok_or_else(|| { + ChainCommunicationError::from_other_str("Could not find program address for message id") + })?; + Ok(expected_pubkey) + } } #[async_trait] @@ -774,7 +835,7 @@ impl Indexer for SealevelMailboxIndexer { let message_capacity = range.end().saturating_sub(*range.start()); let mut messages = Vec::with_capacity(message_capacity as usize); for nonce in range { - messages.push(self.get_message_with_nonce(nonce).await?); + messages.push(self.get_dispatched_message_with_nonce(nonce).await?); } Ok(messages) } @@ -788,9 +849,19 @@ impl Indexer for SealevelMailboxIndexer { impl Indexer for SealevelMailboxIndexer { async fn fetch_logs_in_range( &self, - _range: RangeInclusive, + range: RangeInclusive, ) -> ChainResult, LogMeta)>> { - todo!() + info!( + ?range, + "Fetching SealevelMailboxIndexer HyperlaneMessage Delivery logs" + ); + + let message_capacity = range.end().saturating_sub(*range.start()); + let mut message_ids = Vec::with_capacity(message_capacity as usize); + for nonce in range { + message_ids.push(self.get_delivered_message_with_nonce(nonce).await?); + } + Ok(message_ids) } async fn get_finalized_block_number(&self) -> ChainResult { diff --git a/rust/main/chains/hyperlane-sealevel/src/provider.rs b/rust/main/chains/hyperlane-sealevel/src/provider.rs index 932a7191ea..a0c5a41ead 100644 --- a/rust/main/chains/hyperlane-sealevel/src/provider.rs +++ b/rust/main/chains/hyperlane-sealevel/src/provider.rs @@ -1,14 +1,17 @@ -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; use async_trait::async_trait; +use solana_sdk::signature::Signature; +use solana_transaction_status::EncodedTransaction; + use hyperlane_core::{ - BlockInfo, ChainInfo, ChainResult, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, - HyperlaneProviderError, TxnInfo, H256, H512, U256, + BlockInfo, ChainCommunicationError, ChainInfo, ChainResult, HyperlaneChain, HyperlaneDomain, + HyperlaneProvider, HyperlaneProviderError, TxnInfo, TxnReceiptInfo, H256, H512, U256, }; -use solana_sdk::bs58; -use solana_sdk::pubkey::Pubkey; -use crate::{error::HyperlaneSealevelError, ConnectionConf, SealevelRpcClient}; +use crate::error::HyperlaneSealevelError; +use crate::utils::{decode_h256, decode_h512, decode_pubkey}; +use crate::{ConnectionConf, SealevelRpcClient}; /// A wrapper around a Sealevel provider to get generic blockchain information. #[derive(Debug)] @@ -50,10 +53,7 @@ impl HyperlaneProvider for SealevelProvider { async fn get_block_by_height(&self, slot: u64) -> ChainResult { let confirmed_block = self.rpc_client.get_block(slot).await?; - let hash_binary = bs58::decode(confirmed_block.blockhash) - .into_vec() - .map_err(HyperlaneSealevelError::Decoding)?; - let block_hash = H256::from_slice(&hash_binary); + let block_hash = decode_h256(&confirmed_block.blockhash)?; let block_time = confirmed_block .block_time @@ -68,8 +68,55 @@ impl HyperlaneProvider for SealevelProvider { Ok(block_info) } - async fn get_txn_by_hash(&self, _hash: &H512) -> ChainResult { - todo!() // FIXME + /// TODO This method is superfluous for Solana. + /// Since we have to request full block to find transaction hash and transaction index + /// for Solana, we have all the data about transaction mach earlier before this + /// method is invoked. + /// We can refactor abstractions so that our chain-agnostic code is more suitable + /// for all chains, not only Ethereum-like chains. + async fn get_txn_by_hash(&self, hash: &H512) -> ChainResult { + let signature = Signature::new(hash.as_bytes()); + let transaction = self.rpc_client.get_transaction(&signature).await?; + + let ui_transaction = match transaction.transaction.transaction { + EncodedTransaction::Json(t) => t, + t => Err(Into::::into( + HyperlaneSealevelError::UnsupportedTransactionEncoding(t), + ))?, + }; + + let received_signature = ui_transaction + .signatures + .first() + .ok_or(HyperlaneSealevelError::UnsignedTransaction(*hash))?; + let received_hash = decode_h512(received_signature)?; + + if &received_hash != hash { + Err(Into::::into( + HyperlaneSealevelError::IncorrectTransaction( + Box::new(*hash), + Box::new(received_hash), + ), + ))?; + } + + let receipt = TxnReceiptInfo { + gas_used: Default::default(), + cumulative_gas_used: Default::default(), + effective_gas_price: None, + }; + + Ok(TxnInfo { + hash: *hash, + gas_limit: Default::default(), + max_priority_fee_per_gas: None, + max_fee_per_gas: None, + gas_price: None, + nonce: 0, + sender: Default::default(), + recipient: None, + receipt: Some(receipt), + }) } async fn is_contract(&self, _address: &H256) -> ChainResult { @@ -78,7 +125,7 @@ impl HyperlaneProvider for SealevelProvider { } async fn get_balance(&self, address: String) -> ChainResult { - let pubkey = Pubkey::from_str(&address).map_err(Into::::into)?; + let pubkey = decode_pubkey(&address)?; self.rpc_client.get_balance(&pubkey).await } diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs index 44ba2c8d59..88a474cbbf 100644 --- a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs @@ -1,10 +1,9 @@ use base64::Engine; use borsh::{BorshDeserialize, BorshSerialize}; -use hyperlane_core::{ChainCommunicationError, ChainResult, U256}; use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; use solana_client::{ nonblocking::rpc_client::RpcClient, rpc_config::RpcBlockConfig, - rpc_config::RpcProgramAccountsConfig, rpc_response::Response, + rpc_config::RpcProgramAccountsConfig, rpc_config::RpcTransactionConfig, rpc_response::Response, }; use solana_sdk::{ account::Account, @@ -17,9 +16,12 @@ use solana_sdk::{ transaction::Transaction, }; use solana_transaction_status::{ - TransactionStatus, UiConfirmedBlock, UiReturnDataEncoding, UiTransactionReturnData, + EncodedConfirmedTransactionWithStatusMeta, TransactionStatus, UiConfirmedBlock, + UiReturnDataEncoding, UiTransactionReturnData, }; +use hyperlane_core::{ChainCommunicationError, ChainResult, U256}; + use crate::error::HyperlaneSealevelError; pub struct SealevelRpcClient(RpcClient); @@ -99,6 +101,17 @@ impl SealevelRpcClient { Ok(account) } + pub async fn get_balance(&self, pubkey: &Pubkey) -> ChainResult { + let balance = self + .0 + .get_balance(pubkey) + .await + .map_err(Into::::into) + .map_err(ChainCommunicationError::from)?; + + Ok(balance.into()) + } + pub async fn get_block(&self, height: u64) -> ChainResult { let config = RpcBlockConfig { commitment: Some(CommitmentConfig::finalized()), @@ -170,15 +183,19 @@ impl SealevelRpcClient { .map_err(ChainCommunicationError::from_other) } - pub async fn get_balance(&self, pubkey: &Pubkey) -> ChainResult { - let balance = self - .0 - .get_balance(pubkey) + pub async fn get_transaction( + &self, + signature: &Signature, + ) -> ChainResult { + let config = RpcTransactionConfig { + commitment: Some(CommitmentConfig::finalized()), + ..Default::default() + }; + self.0 + .get_transaction_with_config(signature, config) .await - .map_err(Into::::into) - .map_err(ChainCommunicationError::from)?; - - Ok(balance.into()) + .map_err(HyperlaneSealevelError::ClientError) + .map_err(Into::into) } pub async fn is_blockhash_valid(&self, hash: &Hash) -> ChainResult { diff --git a/rust/main/chains/hyperlane-sealevel/src/transaction.rs b/rust/main/chains/hyperlane-sealevel/src/transaction.rs new file mode 100644 index 0000000000..26a0722cf2 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/transaction.rs @@ -0,0 +1,188 @@ +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 dispatches 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 message and searches a message dispatch transaction in a list of transaction. +/// The list of transaction is usually comes from a block. The function returns list of hashes +/// of such transactions. +/// +/// The transaction will be searched with the following criteria: +/// 1. Transaction contains Mailbox program id in the list of accounts. +/// 2. Transaction contains dispatched message PDA in the list of accounts. +/// 3. Transaction is performing message dispatch (OutboxDispatch). +/// +/// * `mailbox_program_id` - Identifier of Mailbox program +/// * `message_storage_pda_pubkey` - Identifier for dispatch message store PDA +/// * `transactions` - List of transactions +pub fn search_dispatched_message_transactions( + mailbox_program_id: &Pubkey, + message_storage_pda_pubkey: &Pubkey, + transactions: Vec, +) -> 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_not_relevant( + mailbox_program_id, + message_storage_pda_pubkey, + hash, + account_keys, + instructions, + ) + .map(|hash| (index, hash)) + }) + .collect::>() +} + +fn filter_not_relevant( + mailbox_program_id: &Pubkey, + message_storage_pda_pubkey: &Pubkey, + hash: H512, + account_keys: Vec, + instructions: Vec, +) -> Option { + 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. + }; + + let message_storage_pda_pubkey_str = message_storage_pda_pubkey.to_string(); + let dispatch_message_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 message store PDA account, transaction is not message dispatch. + }; + + 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. + }; + + // If Mailbox program does not operate on dispatch message store PDA account, transaction is not message dispatch. + if !mailbox_program + .accounts + .contains(&dispatch_message_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. + }; + + 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. + }; + + // If the call into Mailbox program is not OutboxDispatch, transaction is not message dispatch. + if !matches!(instruction, Instruction::OutboxDispatch(_)) { + 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/transaction/tests.rs b/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs new file mode 100644 index 0000000000..759f15de97 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs @@ -0,0 +1,329 @@ +use solana_transaction_status::EncodedTransactionWithStatusMeta; + +use crate::transaction::search_dispatched_message_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 transaction = serde_json::from_str::(JSON).unwrap(); + let transactions = vec![transaction]; + + // when + let transaction_hashes = search_dispatched_message_transactions( + &mailbox_program_id, + &dispatched_message_pda_account, + transactions, + ); + + // then + assert!(!transaction_hashes.is_empty()); +} + +const 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" + ] + } +} +"#; diff --git a/rust/main/chains/hyperlane-sealevel/src/utils.rs b/rust/main/chains/hyperlane-sealevel/src/utils.rs new file mode 100644 index 0000000000..56b8202c61 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/utils.rs @@ -0,0 +1,33 @@ +use std::str::FromStr; + +use solana_sdk::bs58; +use solana_sdk::pubkey::Pubkey; + +use hyperlane_core::{H256, H512}; + +use crate::error::HyperlaneSealevelError; + +pub fn from_base58(base58: &str) -> Result, HyperlaneSealevelError> { + let binary = bs58::decode(base58) + .into_vec() + .map_err(HyperlaneSealevelError::Decoding)?; + Ok(binary) +} + +pub fn decode_h256(base58: &str) -> Result { + let binary = from_base58(base58)?; + let hash = H256::from_slice(&binary); + + Ok(hash) +} + +pub fn decode_h512(base58: &str) -> Result { + let binary = from_base58(base58)?; + let hash = H512::from_slice(&binary); + + Ok(hash) +} + +pub fn decode_pubkey(address: &str) -> Result { + Pubkey::from_str(address).map_err(Into::::into) +}