diff --git a/Cargo.lock b/Cargo.lock index a70e10d9031b55..1c361111cea12e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,7 @@ dependencies = [ "bincode", "criterion", "solana-sdk", + "solana-svm-transaction", ] [[package]] diff --git a/transaction-view/Cargo.toml b/transaction-view/Cargo.toml index 0b3f4e828c969d..25fef46304e595 100644 --- a/transaction-view/Cargo.toml +++ b/transaction-view/Cargo.toml @@ -11,6 +11,7 @@ edition = { workspace = true } [dependencies] solana-sdk = { workspace = true } +solana-svm-transaction = { workspace = true } [dev-dependencies] # See order-crates-for-publishing.py for using this unusual `path = "."` diff --git a/transaction-view/src/instructions_meta.rs b/transaction-view/src/instructions_meta.rs index ad380f1724548a..9a6d5e3dd72c0a 100644 --- a/transaction-view/src/instructions_meta.rs +++ b/transaction-view/src/instructions_meta.rs @@ -1,6 +1,11 @@ -use crate::{ - bytes::{advance_offset_for_array, check_remaining, optimized_read_compressed_u16, read_byte}, - result::Result, +use { + crate::{ + bytes::{ + advance_offset_for_array, check_remaining, optimized_read_compressed_u16, read_byte, + }, + result::Result, + }, + solana_svm_transaction::instruction::SVMInstruction, }; /// Contains metadata about the instructions in a transaction packet. @@ -64,6 +69,68 @@ impl InstructionsMeta { } } +pub struct InstructionsIterator<'a> { + pub(crate) bytes: &'a [u8], + pub(crate) offset: usize, + pub(crate) num_instructions: u16, + pub(crate) index: u16, +} + +impl<'a> Iterator for InstructionsIterator<'a> { + type Item = SVMInstruction<'a>; + + fn next(&mut self) -> Option { + if self.index < self.num_instructions { + // Each instruction has 3 pieces: + // 1. Program ID index (u8) + // 2. Accounts indexes ([u8]) + // 3. Data ([u8]) + + // Read the program ID index. + let program_id_index = read_byte(self.bytes, &mut self.offset).ok()?; + + // Read the number of account indexes, and then update the offset + // to skip over the account indexes. + let num_accounts = optimized_read_compressed_u16(self.bytes, &mut self.offset).ok()?; + // SAFETY: Only returned after we check that there are enough bytes. + let accounts = unsafe { + core::slice::from_raw_parts( + self.bytes.as_ptr().add(self.offset), + usize::from(num_accounts), + ) + }; + advance_offset_for_array::(self.bytes, &mut self.offset, num_accounts).ok()?; + + // Read the length of the data, and then update the offset to skip + // over the data. + let data_len = optimized_read_compressed_u16(self.bytes, &mut self.offset).ok()?; + // SAFETY: Only returned after we check that there are enough bytes. + let data = unsafe { + core::slice::from_raw_parts( + self.bytes.as_ptr().add(self.offset), + usize::from(data_len), + ) + }; + advance_offset_for_array::(self.bytes, &mut self.offset, data_len).ok()?; + self.index = self.index.wrapping_add(1); + + Some(SVMInstruction { + program_id_index, + accounts, + data, + }) + } else { + None + } + } +} + +impl ExactSizeIterator for InstructionsIterator<'_> { + fn len(&self) -> usize { + usize::from(self.num_instructions.wrapping_sub(self.index)) + } +} + #[cfg(test)] mod tests { use { diff --git a/transaction-view/src/transaction_meta.rs b/transaction-view/src/transaction_meta.rs index 6547823a2c35ea..d467448c2ff500 100644 --- a/transaction-view/src/transaction_meta.rs +++ b/transaction-view/src/transaction_meta.rs @@ -2,7 +2,7 @@ use { crate::{ address_table_lookup_meta::AddressTableLookupMeta, bytes::advance_offset_for_type, - instructions_meta::InstructionsMeta, + instructions_meta::{InstructionsIterator, InstructionsMeta}, message_header_meta::{MessageHeaderMeta, TransactionVersion}, result::{Result, TransactionParsingError}, signature_meta::SignatureMeta, @@ -142,6 +142,19 @@ impl TransactionMeta { .as_ptr() .add(usize::from(self.recent_blockhash_offset)) as *const Hash) } + + /// Return an iterator over the instructions in the transaction. + /// # Safety + /// - This function must be called with the same `bytes` slice that was + /// used to create the `TransactionMeta` instance. + pub unsafe fn instructions_iter<'a>(&self, bytes: &'a [u8]) -> InstructionsIterator<'a> { + InstructionsIterator { + bytes, + offset: usize::from(self.instructions.offset), + num_instructions: self.instructions.num_instructions, + index: 0, + } + } } #[cfg(test)] @@ -153,7 +166,7 @@ mod tests { message::{v0, Message, MessageHeader, VersionedMessage}, pubkey::Pubkey, signature::Signature, - system_instruction, + system_instruction::{self, SystemInstruction}, transaction::VersionedTransaction, }, }; @@ -391,4 +404,24 @@ mod tests { assert_eq!(recent_blockhash, tx.message.recent_blockhash()); } } + + #[test] + fn test_instructions_iter() { + let tx = simple_transfer(); + let bytes = bincode::serialize(&tx).unwrap(); + let meta = TransactionMeta::try_new(&bytes).unwrap(); + + // SAFETY: `bytes` is the same slice used to create `meta`. + unsafe { + let mut iter = meta.instructions_iter(&bytes); + let ix = iter.next().unwrap(); + assert_eq!(ix.program_id_index, 2); + assert_eq!(ix.accounts, &[0, 1]); + assert_eq!( + ix.data, + &bincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() + ); + assert!(iter.next().is_none()); + } + } }