diff --git a/Cargo.lock b/Cargo.lock index 90d37815cebba9..e59302475c85d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,10 @@ dependencies = [ "solana-version", ] +[[package]] +name = "agave-transaction-ffi" +version = "2.1.0" + [[package]] name = "agave-transaction-view" version = "2.1.0" @@ -6149,6 +6153,7 @@ dependencies = [ name = "solana-core" version = "2.1.0" dependencies = [ + "agave-transaction-ffi", "ahash 0.8.10", "anyhow", "arrayvec", diff --git a/Cargo.toml b/Cargo.toml index 2a772d3ee85d3d..d185a033ad6c8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,6 +128,7 @@ members = [ "tps-client", "tpu-client", "transaction-dos", + "transaction-ffi", "transaction-metrics-tracker", "transaction-status", "transaction-view", @@ -173,6 +174,7 @@ Inflector = "0.11.4" agave-transaction-view = { path = "transaction-view", version = "=2.1.0" } aquamarine = "0.3.3" aes-gcm-siv = "0.11.1" +agave-transaction-ffi = { path = "transaction-ffi", version = "=2.1.0" } ahash = "0.8.10" anyhow = "1.0.82" arbitrary = "1.3.2" diff --git a/core/Cargo.toml b/core/Cargo.toml index 4d3c59a8ada4f8..8e23c3b4670a1f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,6 +14,7 @@ edition = { workspace = true } codecov = { repository = "solana-labs/solana", branch = "master", service = "github" } [dependencies] +agave-transaction-ffi = { workspace = true } ahash = { workspace = true } anyhow = { workspace = true } arrayvec = { workspace = true } diff --git a/core/src/lib.rs b/core/src/lib.rs index da9d69ed508875..b1f9dbc4d8a4da 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -41,6 +41,7 @@ pub mod system_monitor_service; pub mod tpu; mod tpu_entry_notifier; pub mod tracer_packet_stats; +mod transaction_ffi; pub mod tvu; pub mod unfrozen_gossip_verified_vote_hashes; pub mod validator; diff --git a/core/src/transaction_ffi.rs b/core/src/transaction_ffi.rs new file mode 100644 index 00000000000000..ec10f014c0f8bc --- /dev/null +++ b/core/src/transaction_ffi.rs @@ -0,0 +1,324 @@ +//! Module to provide easy construction of C-compatible interfaces for +//! interacting with transactions from valid transaction references. +//! + +use { + agave_transaction_ffi::{ + AccountCallback, Instruction, InstructionCallback, TransactionInterface, TransactionPtr, + }, + solana_svm_transaction::svm_transaction::SVMTransaction, +}; + +/// Given a reference to any type that implements `SVMTransaction`, create a +/// `TransactionInterface` that can be used to interact with the transaction in +/// C-compatible code. This interface is only valid for the lifetime of the +/// transaction reference, which cannot be guaranteed by this function interface. +#[allow(dead_code)] +pub unsafe fn create_transaction_interface( + transaction: &Tx, +) -> TransactionInterface { + extern "C" fn num_signatures(transaction_ptr: TransactionPtr) -> usize { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.signatures().len() + } + + extern "C" fn signatures(transaction_ptr: TransactionPtr) -> *const u8 { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.signatures().as_ptr() as *const u8 + } + + extern "C" fn num_total_signatures(transaction_ptr: TransactionPtr) -> u64 { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.num_total_signatures() + } + + extern "C" fn num_write_locks(transaction_ptr: TransactionPtr) -> u64 { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.num_write_locks() + } + + extern "C" fn recent_blockhash( + transaction_ptr: TransactionPtr, + ) -> *const u8 { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.recent_blockhash().as_ref().as_ptr() + } + + extern "C" fn num_instructions(transaction_ptr: TransactionPtr) -> usize { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.num_instructions() + } + + extern "C" fn iter_instructions( + transaction_ptr: TransactionPtr, + callback: InstructionCallback, + ) { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + for instruction in transaction.instructions_iter() { + let instruction = Instruction { + program_index: instruction.program_id_index, + num_accounts: instruction.accounts.len() as u16, + accounts: instruction.accounts.as_ptr(), + data_len: instruction.data.len() as u16, + data: instruction.data.as_ptr(), + }; + + if !unsafe { (callback.callback)(callback.state, instruction) } { + break; + } + } + } + + extern "C" fn num_accounts(transaction_ptr: TransactionPtr) -> usize { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.account_keys().len() + } + + extern "C" fn get_account( + transaction_ptr: TransactionPtr, + index: usize, + ) -> *const u8 { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction + .account_keys() + .get(index) + .map_or(core::ptr::null(), |key| key.as_ref().as_ptr()) + } + + extern "C" fn is_writable( + transaction_ptr: TransactionPtr, + index: usize, + ) -> bool { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.is_writable(index) + } + + extern "C" fn is_signer( + transaction_ptr: TransactionPtr, + index: usize, + ) -> bool { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.is_signer(index) + } + + extern "C" fn is_invoked( + transaction_ptr: TransactionPtr, + index: usize, + ) -> bool { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + transaction.is_invoked(index) + } + + extern "C" fn iter_accounts( + transaction_ptr: TransactionPtr, + callback: AccountCallback, + ) { + let transaction = unsafe { &*(transaction_ptr as *const Tx) }; + for account_key in transaction.account_keys().iter() { + let account_key = account_key.as_ref().as_ptr(); + if !unsafe { (callback.callback)(callback.state, account_key) } { + break; + } + } + } + + TransactionInterface { + transaction_ptr: transaction as *const Tx as *const core::ffi::c_void, + num_signatures_fn: num_signatures::, + signatures_fn: signatures::, + num_total_signatures_fn: num_total_signatures::, + num_write_locks_fn: num_write_locks::, + recent_blockhash_fn: recent_blockhash::, + num_instructions_fn: num_instructions::, + iter_instructions_fn: iter_instructions::, + num_accounts_fn: num_accounts::, + get_account_fn: get_account::, + is_writable_fn: is_writable::, + is_signer_fn: is_signer::, + is_invoked_fn: is_invoked::, + iter_accounts_fn: iter_accounts::, + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_sdk::{ + hash::Hash, pubkey::Pubkey, signature::Keypair, system_transaction, + transaction::SanitizedTransaction, + }, + }; + + #[test] + fn test_create_transaction_interface_with_sanitized_transaction() { + let simple_transfer = + SanitizedTransaction::from_transaction_for_tests(system_transaction::transfer( + &Keypair::new(), + &Pubkey::new_unique(), + 1, + Hash::default(), + )); + + // SAFETY: The interface is valid for as long as the transaction reference is valid. + let interface = unsafe { create_transaction_interface(&simple_transfer) }; + + // Verify signature len and address are the same. + let signatures = simple_transfer.signatures(); + assert_eq!(signatures.len(), unsafe { + (interface.num_signatures_fn)(interface.transaction_ptr) + }); + assert_eq!(signatures.as_ptr(), unsafe { + (interface.signatures_fn)(interface.transaction_ptr) as *const _ + },); + + // Verify requested write locks are the same. + assert_eq!(simple_transfer.message().num_write_locks(), unsafe { + (interface.num_write_locks_fn)(interface.transaction_ptr) + }); + + // Verify recent blockhash is the same. + assert_eq!( + simple_transfer + .message() + .recent_blockhash() + .as_ref() + .as_ptr(), + unsafe { (interface.recent_blockhash_fn)(interface.transaction_ptr) } + ); + + // Verify number of instructions is the same. + assert_eq!(simple_transfer.message().instructions().len(), unsafe { + (interface.num_instructions_fn)(interface.transaction_ptr) + }); + + struct CallbackState<'a> { + count: usize, + transaction: &'a SanitizedTransaction, + } + let mut state = CallbackState { + count: 0, + transaction: &simple_transfer, + }; + + extern "C" fn instruction_callback( + state: *mut core::ffi::c_void, + instruction: Instruction, + ) -> bool { + let state = unsafe { &mut *(state as *mut CallbackState) }; + + // Verify the instruction data is the same. + let actual_instruction = state + .transaction + .message() + .instructions() + .get(state.count) + .unwrap(); + assert_eq!( + actual_instruction.program_id_index, + instruction.program_index + ); + assert_eq!( + actual_instruction.accounts.len(), + instruction.num_accounts as usize + ); + assert_eq!(actual_instruction.accounts.as_ptr(), instruction.accounts); + assert_eq!(actual_instruction.data.len(), instruction.data_len as usize); + assert_eq!(actual_instruction.data.as_ptr(), instruction.data); + + // Update count + state.count += 1; + true + } + + unsafe { + (interface.iter_instructions_fn)( + interface.transaction_ptr, + InstructionCallback { + state: &mut state as *mut _ as *mut core::ffi::c_void, + callback: instruction_callback, + }, + ); + } + assert_eq!(state.count, simple_transfer.message().instructions().len()); + + // Verify number of accounts is the same. + assert_eq!(simple_transfer.message().account_keys().len(), unsafe { + (interface.num_accounts_fn)(interface.transaction_ptr) + }); + + // Verify account and properties are the same. + for (i, account_key) in simple_transfer.message().account_keys().iter().enumerate() { + assert_eq!(account_key.as_ref().as_ptr(), unsafe { + (interface.get_account_fn)(interface.transaction_ptr, i) + }); + + assert_eq!(simple_transfer.message().is_writable(i), unsafe { + (interface.is_writable_fn)(interface.transaction_ptr, i) + }); + + assert_eq!(simple_transfer.message().is_signer(i), unsafe { + (interface.is_signer_fn)(interface.transaction_ptr, i) + }); + + assert_eq!(simple_transfer.message().is_invoked(i), unsafe { + (interface.is_invoked_fn)(interface.transaction_ptr, i) + }); + } + // Verify return out of bounds is null or false + let out_of_bounds_index = simple_transfer.message().account_keys().len(); + assert_eq!(core::ptr::null(), unsafe { + (interface.get_account_fn)(interface.transaction_ptr, out_of_bounds_index) + }); + assert!(!unsafe { + (interface.is_writable_fn)(interface.transaction_ptr, out_of_bounds_index) + }); + assert!(!unsafe { + (interface.is_signer_fn)(interface.transaction_ptr, out_of_bounds_index) + }); + assert!(!unsafe { + (interface.is_invoked_fn)(interface.transaction_ptr, out_of_bounds_index) + }); + + struct AccountCallbackState<'a> { + count: usize, + transaction: &'a SanitizedTransaction, + } + + extern "C" fn account_callback( + state: *mut core::ffi::c_void, + account_key: *const u8, + ) -> bool { + let state = unsafe { &mut *(state as *mut AccountCallbackState) }; + + // Verify the account key is the same. + let actual_account_key = state + .transaction + .message() + .account_keys() + .get(state.count) + .unwrap(); + assert_eq!(actual_account_key.as_ref().as_ptr(), account_key); + + // Update count + state.count += 1; + true + } + + let mut state = AccountCallbackState { + count: 0, + transaction: &simple_transfer, + }; + + unsafe { + (interface.iter_accounts_fn)( + interface.transaction_ptr, + AccountCallback { + state: &mut state as *mut _ as *mut core::ffi::c_void, + callback: account_callback, + }, + ); + } + assert_eq!(state.count, simple_transfer.message().account_keys().len()); + } +} diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index f78add5c5688cb..b6a261c3be2a2d 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -73,6 +73,10 @@ dependencies = [ "thiserror", ] +[[package]] +name = "agave-transaction-ffi" +version = "2.1.0" + [[package]] name = "agave-transaction-view" version = "2.1.0" @@ -4876,6 +4880,7 @@ dependencies = [ name = "solana-core" version = "2.1.0" dependencies = [ + "agave-transaction-ffi", "ahash 0.8.10", "anyhow", "arrayvec", diff --git a/transaction-ffi/Cargo.toml b/transaction-ffi/Cargo.toml new file mode 100644 index 00000000000000..9cd93646e426af --- /dev/null +++ b/transaction-ffi/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "agave-transaction-ffi" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] diff --git a/transaction-ffi/src/lib.rs b/transaction-ffi/src/lib.rs new file mode 100644 index 00000000000000..92573919a7b991 --- /dev/null +++ b/transaction-ffi/src/lib.rs @@ -0,0 +1,286 @@ +/// The account key is a 32-byte array that represents a public key. +pub type AccountKey = *const u8; + +/// An opaque pointer to a transaction. This will be provided to any plugin and +/// should always be non-null. The plugin should not pass any pointers to any +/// functions on the [`TransactionInterface`] that are not provided. +pub type TransactionPtr = *const core::ffi::c_void; + +/// Returns the number of signatures in this transaction. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionNumSignaturesFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr) -> usize; + +/// Returns pointer to the first signature in the transaction. +/// The returned pointer is valid for the lifetime of the transaction, and is +/// guaranteed to be non-null and 64 bytes long. +/// The number of signatures in the transaction can be obtained by calling +/// [`TransactionNumSignaturesFn`]. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionSignaturesFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr) -> *const u8; + +/// Returns the total number of signatures in the transaction, including any +/// pre-compile signatures. +/// WARNING: This function should not be used to determine the number of +/// signatures returned by `TransactionSignaturesFn`. Instead, use +/// `TransactionNumSignaturesFn`. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionNumTotalSignatures = + unsafe extern "C" fn(transaction_ptr: TransactionPtr) -> u64; + +/// Returns the number of requested write-locks in this transaction. +/// This does not consider if write-locks are demoted. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionNumWriteLocksFn = unsafe extern "C" fn(transaction_ptr: TransactionPtr) -> u64; + +/// Returns a reference to the transaction's recent blockhash. +/// The returned pointer is valid for the lifetime of the transaction, and is +/// guaranteed to be non-null and 32 bytes long. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionRecentBlockhashFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr) -> *const u8; + +/// Return the number of instructions in the transaction. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionNumInstructionsFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr) -> usize; + +/// A C-compatible non-owning reference to an instruction in a transaction. +#[repr(C)] +pub struct Instruction { + /// The program index of the instruction. + pub program_index: u8, + /// The number of accounts used by the instruction. + pub num_accounts: u16, + /// Pointer to the first account index used by the instruction. + /// Guaranteed to be non-null and valid for the lifetime of a transaction. + pub accounts: *const u8, + /// The number of data bytes used by the instruction. + pub data_len: u16, + /// Pointer to the first data byte used by the instruction. + /// Guaranteed to be non-null and valid for the lifetime of a transaction. + pub data: *const u8, +} + +/// A C-compatible interface that can be used to interact with instructons in +/// transactions. This callback interface is used to inspect instructions in a +/// loop and optionally break early. +#[repr(C)] +pub struct InstructionCallback { + /// An opaque pointer to arbitrary state that will be passed to the + /// callback. If the callback requires no state, this can be null. + pub state: *mut core::ffi::c_void, + /// A callback that will be called for each instruction in the transaction. + /// The callback should return `true` to continue processing instructions, + /// or `false` to break early. + pub callback: + unsafe extern "C" fn(state: *mut core::ffi::c_void, instruction: Instruction) -> bool, +} + +/// Iterate over the instructions in the transaction calling the provided +/// callback for each instruction until the callback returns `false` or there +/// are no more instructions. +/// # Safety +/// - The transaction pointer must be valid. +/// - If the callback expects a state, the state must be valid. +/// - The callback must be a valid function pointer. +pub type TransactionIterInstructionsFn = unsafe extern "C" fn( + transaction_ptr: TransactionPtr, + instruction_callback: InstructionCallback, +); + +/// Return the number of accounts in the transaction. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionNumAccountsFn = unsafe extern "C" fn(transaction_ptr: TransactionPtr) -> usize; + +/// Get the account key at the specified index. +/// The returned pointer will be null if the index is out of bounds. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionGetAccountFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr, index: usize) -> AccountKey; + +/// Returns `true` if the account at index is writable. +/// If the index is out of bounds, this function will return `false`. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionIsWritableFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr, index: usize) -> bool; + +/// Returns `true` if the account at index is a signer. +/// If the index is out of bounds, this function will return `false`. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionIsSignerFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr, index: usize) -> bool; + +/// Returns `true` if the account at the specified index is invoked as a +/// program in top-level instructions of this transaction. +/// If the index is out of bounds, this function will return `false`. +/// # Safety +/// - The transaction pointer must be valid. +pub type TransactionIsInvokedFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr, index: usize) -> bool; + +/// A C-compatible interface that can be used to interact with account keys in +/// transactions. This callback interface is used to inspect account keys in a +/// loop and optionally break early. +#[repr(C)] +pub struct AccountCallback { + /// An opaque pointer to arbitrary state that will be passed to the + /// callback. If the callback requires no state, this can be null. + pub state: *mut core::ffi::c_void, + /// A callback that will be called for each account key in the transaction. + /// The callback should return `true` to continue processing account keys, + /// or `false` to break early. + pub callback: + unsafe extern "C" fn(state: *mut core::ffi::c_void, account_key: AccountKey) -> bool, +} + +/// Iterate over the account keys in the transaction calling the provided +/// callback for each account key until the callback returns `false` or there +/// are no more account keys. +/// # Safety +/// - The transaction pointer must be valid. +/// - If the callback expects a state, the state must be valid. +/// - The callback must be a valid function pointer. +pub type TransactionIterAccountsFn = + unsafe extern "C" fn(transaction_ptr: TransactionPtr, account_callback: AccountCallback); + +/// A C-compatible interface that can be used to interact with transactions in +/// agave plugins. This interface is used to inspect transactions, not to +/// modify or create them. +/// The actual transaction type is opaque to the plugin and this interface. +#[repr(C)] +pub struct TransactionInterface { + /// A pointer to the transaction. + pub transaction_ptr: TransactionPtr, + /// Returns the number of signatures in this transaction. + /// See [`TransactionNumSignaturesFn`]. + pub num_signatures_fn: TransactionNumSignaturesFn, + /// Returns pointer to the first signature in the transaction. + /// See [`TransactionSignaturesFn`]. + pub signatures_fn: TransactionSignaturesFn, + /// Returns the total number of signatures in the transaction, including + /// any pre-compile signatures. + /// See [`TransactionNumTotalSignatures`]. + pub num_total_signatures_fn: TransactionNumTotalSignatures, + /// Returns the number of requested write-locks in this transaction. + /// See [`TransactionNumWriteLocksFn`]. + pub num_write_locks_fn: TransactionNumWriteLocksFn, + /// Returns a reference to the transaction's recent blockhash. + /// See [`TransactionRecentBlockhashFn`]. + pub recent_blockhash_fn: TransactionRecentBlockhashFn, + /// Return the number of instructions in the transaction. + /// See [`TransactionNumInstructionsFn`]. + pub num_instructions_fn: TransactionNumInstructionsFn, + /// Iterate over the instructions in the transaction calling the provided + /// callback for each instruction until the callback returns `false` or there + /// are no more instructions. + /// See [`TransactionIterInstructionsFn`]. + pub iter_instructions_fn: TransactionIterInstructionsFn, + /// Return the number of accounts in the transaction. + /// See [`TransactionNumAccountsFn`]. + pub num_accounts_fn: TransactionNumAccountsFn, + /// Get the account key at the specified index. + /// See [`TransactionGetAccountFn`]. + pub get_account_fn: TransactionGetAccountFn, + /// Returns `true` if the account at index is writable. + /// See [`TransactionIsWritableFn`]. + pub is_writable_fn: TransactionIsWritableFn, + /// Returns `true` if the account at index is a signer. + /// See [`TransactionIsSignerFn`]. + pub is_signer_fn: TransactionIsSignerFn, + /// Returns `true` if the account at the specified index is invoked as a + /// program in top-level instructions of this transaction. + /// See [`TransactionIsInvokedFn`]. + pub is_invoked_fn: TransactionIsInvokedFn, + /// Iterate over the account keys in the transaction calling the provided + /// callback for each account key until the callback returns `false` or there + /// are no more account keys. + /// See [`TransactionIterAccountsFn`]. + pub iter_accounts_fn: TransactionIterAccountsFn, +} + +// Rust functions to call functions on the `TransactionInterface` struct. +// To avoid comments on unsafe code in each function this top-level comment +// should suffice: +// +// - SAFETY: `TransactionInterface` provided to the plugin has valid +// transaction pointer and fn pointers. +// Unless user modified it - these functions are safe. +impl TransactionInterface { + pub fn num_signatures(&self) -> usize { + unsafe { (self.num_signatures_fn)(self.transaction_ptr) } + } + + pub fn signatures(&self) -> &[[u8; 64]] { + let num_signatures = self.num_signatures(); + unsafe { + let signatures_ptr = (self.signatures_fn)(self.transaction_ptr); + core::slice::from_raw_parts(signatures_ptr as *const [u8; 64], num_signatures) + } + } + + pub fn num_total_signatures(&self) -> u64 { + unsafe { (self.num_total_signatures_fn)(self.transaction_ptr) } + } + + pub fn num_write_locks(&self) -> u64 { + unsafe { (self.num_write_locks_fn)(self.transaction_ptr) } + } + + pub fn recent_blockhash(&self) -> &[u8; 32] { + unsafe { + let recent_blockhas_ptr = (self.recent_blockhash_fn)(self.transaction_ptr); + &*(recent_blockhas_ptr as *const [u8; 32]) + } + } + + pub fn num_instructions(&self) -> usize { + unsafe { (self.num_instructions_fn)(self.transaction_ptr) } + } + + pub fn instructions_iter(&self, callback: InstructionCallback) { + unsafe { (self.iter_instructions_fn)(self.transaction_ptr, callback) } + } + + pub fn num_accounts(&self) -> usize { + unsafe { (self.num_accounts_fn)(self.transaction_ptr) } + } + + pub fn get_account(&self, index: usize) -> Option<&[u8; 32]> { + unsafe { + let account_key = (self.get_account_fn)(self.transaction_ptr, index); + if account_key.is_null() { + None + } else { + Some(&*(account_key as *const [u8; 32])) + } + } + } + + pub fn is_writable(&self, index: usize) -> bool { + unsafe { (self.is_writable_fn)(self.transaction_ptr, index) } + } + + pub fn is_signer(&self, index: usize) -> bool { + unsafe { (self.is_signer_fn)(self.transaction_ptr, index) } + } + + pub fn is_invoked(&self, index: usize) -> bool { + unsafe { (self.is_invoked_fn)(self.transaction_ptr, index) } + } + + pub fn iter_accounts(&self, callback: AccountCallback) { + unsafe { (self.iter_accounts_fn)(self.transaction_ptr, callback) } + } +}