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
parent
7e9e248bef
commit
c87cfbd512
@ -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", |
||||
)) |
||||
} |
@ -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…
Reference in new issue