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>
pull/4792/head
Danil Nemirovsky 3 weeks ago committed by GitHub
parent 7e9e248bef
commit c87cfbd512
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 68
      rust/main/chains/hyperlane-sealevel/src/account.rs
  2. 18
      rust/main/chains/hyperlane-sealevel/src/error.rs
  3. 114
      rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs
  4. 3
      rust/main/chains/hyperlane-sealevel/src/lib.rs
  5. 231
      rust/main/chains/hyperlane-sealevel/src/mailbox.rs
  6. 73
      rust/main/chains/hyperlane-sealevel/src/provider.rs
  7. 39
      rust/main/chains/hyperlane-sealevel/src/rpc/client.rs
  8. 188
      rust/main/chains/hyperlane-sealevel/src/transaction.rs
  9. 329
      rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs
  10. 33
      rust/main/chains/hyperlane-sealevel/src/utils.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<Vec<(Pubkey, Account)>> {
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<F>(
accounts: Vec<(Pubkey, Account)>,
message_account: F,
) -> ChainResult<Pubkey>
where
F: Fn(&Account) -> ChainResult<Pubkey>,
{
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",
))
}

@ -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<H512>, Box<H512>),
}
impl From<HyperlaneSealevelError> for ChainCommunicationError {

@ -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<SealevelGasPayment> {
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::<Pubkey>::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<Pubkey> {
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]

@ -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;

@ -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<HyperlaneMessage>, 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::<Pubkey>::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<Pubkey> {
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<H256>, 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<Pubkey> {
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<HyperlaneMessage> 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<HyperlaneMessage> for SealevelMailboxIndexer {
impl Indexer<H256> for SealevelMailboxIndexer {
async fn fetch_logs_in_range(
&self,
_range: RangeInclusive<u32>,
range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<H256>, 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<u32> {

@ -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<BlockInfo> {
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<TxnInfo> {
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<TxnInfo> {
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::<ChainCommunicationError>::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::<ChainCommunicationError>::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<bool> {
@ -78,7 +125,7 @@ impl HyperlaneProvider for SealevelProvider {
}
async fn get_balance(&self, address: String) -> ChainResult<U256> {
let pubkey = Pubkey::from_str(&address).map_err(Into::<HyperlaneSealevelError>::into)?;
let pubkey = decode_pubkey(&address)?;
self.rpc_client.get_balance(&pubkey).await
}

@ -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<U256> {
let balance = self
.0
.get_balance(pubkey)
.await
.map_err(Into::<HyperlaneSealevelError>::into)
.map_err(ChainCommunicationError::from)?;
Ok(balance.into())
}
pub async fn get_block(&self, height: u64) -> ChainResult<UiConfirmedBlock> {
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<U256> {
let balance = self
.0
.get_balance(pubkey)
pub async fn get_transaction(
&self,
signature: &Signature,
) -> ChainResult<EncodedConfirmedTransactionWithStatusMeta> {
let config = RpcTransactionConfig {
commitment: Some(CommitmentConfig::finalized()),
..Default::default()
};
self.0
.get_transaction_with_config(signature, config)
.await
.map_err(Into::<HyperlaneSealevelError>::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<bool> {

@ -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<EncodedTransactionWithStatusMeta>,
) -> 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::<Vec<(usize, H512)>>()
}
fn filter_not_relevant(
mailbox_program_id: &Pubkey,
message_storage_pda_pubkey: &Pubkey,
hash: H512,
account_keys: Vec<String>,
instructions: Vec<UiCompiledInstruction>,
) -> Option<H512> {
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<String>, Vec<UiCompiledInstruction>)> {
let Some(transaction_hash) = tx
.signatures
.first()
.map(|signature| decode_h512(signature))
.and_then(|r| r.ok())
else {
warn!(
transaction = ?tx,
"transaction does not have any signatures or signatures cannot be decoded",
);
return None;
};
let UiMessage::Raw(message) = tx.message else {
warn!(message = ?tx.message, "we expect messages in Raw format");
return None;
};
let instructions = instructions(message.instructions, meta);
Some((transaction_hash, message.account_keys, instructions))
}
fn filter_by_encoding(
tx: EncodedTransactionWithStatusMeta,
) -> Option<(UiTransaction, UiTransactionStatusMeta)> {
match (tx.transaction, tx.meta) {
// We support only transactions encoded as JSON
// We need none-empty metadata as well
(EncodedTransaction::Json(t), Some(m)) => Some((t, m)),
t => {
warn!(
?t,
"transaction is not encoded as json or metadata is empty"
);
None
}
}
}
fn account_index_map(account_keys: Vec<String>) -> HashMap<String, usize> {
account_keys
.into_iter()
.enumerate()
.map(|(index, key)| (key, index))
.collect::<HashMap<String, usize>>()
}
/// Extract all instructions from transaction
fn instructions(
instruction: Vec<UiCompiledInstruction>,
meta: UiTransactionStatusMeta,
) -> Vec<UiCompiledInstruction> {
let inner_instructions = match meta.inner_instructions {
OptionSerializer::Some(ii) => ii
.into_iter()
.flat_map(|ii| ii.instructions)
.flat_map(|ii| match ii {
UiInstruction::Compiled(ci) => Some(ci),
_ => None,
})
.collect::<Vec<UiCompiledInstruction>>(),
OptionSerializer::None | OptionSerializer::Skip => vec![],
};
[instruction, inner_instructions].concat()
}
#[cfg(test)]
mod tests;

@ -0,0 +1,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::<EncodedTransactionWithStatusMeta>(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"
]
}
}
"#;

@ -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<Vec<u8>, HyperlaneSealevelError> {
let binary = bs58::decode(base58)
.into_vec()
.map_err(HyperlaneSealevelError::Decoding)?;
Ok(binary)
}
pub fn decode_h256(base58: &str) -> Result<H256, HyperlaneSealevelError> {
let binary = from_base58(base58)?;
let hash = H256::from_slice(&binary);
Ok(hash)
}
pub fn decode_h512(base58: &str) -> Result<H512, HyperlaneSealevelError> {
let binary = from_base58(base58)?;
let hash = H512::from_slice(&binary);
Ok(hash)
}
pub fn decode_pubkey(address: &str) -> Result<Pubkey, HyperlaneSealevelError> {
Pubkey::from_str(address).map_err(Into::<HyperlaneSealevelError>::into)
}
Loading…
Cancel
Save