use solana_account::{Account, AccountSharedData};
use solana_address::Address;
use solana_instruction::{Instruction, account_meta::AccountMeta};
use solana_message::inner_instruction::InnerInstructionsList;
use solana_program_error::ProgramError;
use solana_signature::Signature;
use solana_transaction::InstructionError;
use solana_transaction_context::transaction::TransactionReturnData;
use solana_transaction_error::{TransactionError, TransactionResult as Result};
use crate::{error::HPSVMError, format_logs::format_logs};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TransactionMetadata {
pub signature: Signature,
pub logs: Vec<String>,
pub inner_instructions: InnerInstructionsList,
pub compute_units_consumed: u64,
pub return_data: TransactionReturnData,
pub fee: u64,
#[cfg_attr(feature = "serde", serde(default))]
pub diagnostics: ExecutionDiagnostics,
}
impl TransactionMetadata {
pub fn pretty_logs(&self) -> String {
format_logs(&self.logs)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ExecutionDiagnostics {
pub pre_balances: Vec<u64>,
pub post_balances: Vec<u64>,
pub account_diffs: Vec<AccountDiff>,
pub account_source_failures: Vec<AccountSourceFailure>,
pub pre_token_balances: Vec<TokenBalance>,
pub post_token_balances: Vec<TokenBalance>,
pub execution_trace: ExecutionTrace,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct AccountSourceFailure {
pub pubkey: Address,
pub error: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct AccountDiff {
pub address: Address,
pub pre: Option<Account>,
pub post: Option<Account>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct TokenBalance {
pub account_index: usize,
pub address: Address,
pub mint: Address,
pub owner: Address,
pub amount: u64,
pub decimals: Option<u8>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ExecutionTrace {
pub instructions: Vec<ExecutedInstruction>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ExecutedInstruction {
pub stack_height: u8,
pub program_id: Address,
pub accounts: Vec<AccountMeta>,
pub data: Vec<u8>,
}
impl ExecutedInstruction {
#[must_use]
pub fn instruction(&self) -> Instruction {
Instruction {
program_id: self.program_id,
accounts: self.accounts.clone(),
data: self.data.clone(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SimulatedTransactionInfo {
pub meta: TransactionMetadata,
pub post_accounts: Vec<(Address, AccountSharedData)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FailedTransactionMetadata {
pub err: TransactionError,
pub meta: TransactionMetadata,
}
impl From<ProgramError> for FailedTransactionMetadata {
fn from(value: ProgramError) -> Self {
Self {
err: TransactionError::InstructionError(
0,
InstructionError::Custom(u64::from(value) as u32),
),
meta: Default::default(),
}
}
}
pub type TransactionResult = std::result::Result<TransactionMetadata, FailedTransactionMetadata>;
#[derive(Debug)]
pub(crate) struct ExecutionResult {
pub(crate) post_accounts: Vec<(Address, AccountSharedData)>,
pub(crate) tx_result: Result<()>,
pub(crate) signature: Signature,
pub(crate) compute_units_consumed: u64,
pub(crate) inner_instructions: InnerInstructionsList,
pub(crate) return_data: TransactionReturnData,
pub(crate) execution_trace: ExecutionTrace,
pub(crate) included: bool,
pub(crate) fee: u64,
pub(crate) fee_payer: Option<Address>,
pub(crate) account_source_failures: Vec<AccountSourceFailure>,
pub(crate) fatal_error: Option<HPSVMError>,
pub(crate) inspector_live: bool,
}
impl Default for ExecutionResult {
fn default() -> Self {
Self {
post_accounts: Default::default(),
tx_result: Err(TransactionError::UnsupportedVersion),
signature: Default::default(),
compute_units_consumed: Default::default(),
inner_instructions: Default::default(),
return_data: Default::default(),
execution_trace: Default::default(),
included: false,
fee: 0,
fee_payer: None,
account_source_failures: Default::default(),
fatal_error: None,
inspector_live: false,
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use solana_instruction::{Instruction, account_meta::AccountMeta};
use super::*;
fn trace_frame(stack_height: u8, program_id: Address) -> ExecutedInstruction {
ExecutedInstruction {
stack_height,
program_id,
accounts: vec![AccountMeta::new_readonly(program_id, false)],
data: vec![1, 2, 3],
}
}
#[test]
fn pretty_logs_matches_format_logs() {
let meta = TransactionMetadata {
logs: vec![String::from("Program log: hello"), String::new()],
..Default::default()
};
assert_eq!(meta.pretty_logs(), format_logs(&meta.logs));
assert!(meta.pretty_logs().contains("hello"));
}
#[test]
fn pretty_logs_of_metadata_without_logs_is_empty() {
assert_eq!(TransactionMetadata::default().pretty_logs(), "");
}
#[test]
fn executed_instruction_round_trips_back_into_an_instruction() {
let program_id = Address::new_unique();
let other = Address::new_unique();
let frame = ExecutedInstruction {
stack_height: 2,
program_id,
accounts: vec![
AccountMeta::new(program_id, true),
AccountMeta::new_readonly(other, false),
],
data: vec![9, 8, 7],
};
let instruction = frame.instruction();
assert_eq!(instruction.program_id, program_id);
assert_eq!(instruction.accounts, frame.accounts);
assert_eq!(instruction.data, frame.data);
assert_eq!(instruction, frame.instruction());
}
#[test]
fn executed_instruction_of_an_empty_trace_frame_is_empty() {
let frame = ExecutedInstruction {
stack_height: 0,
program_id: Address::default(),
accounts: Vec::new(),
data: Vec::new(),
};
assert_eq!(
frame.instruction(),
Instruction { program_id: Address::default(), accounts: Vec::new(), data: Vec::new() }
);
}
#[test]
fn program_error_conversion_wraps_a_custom_instruction_error() {
let metadata = FailedTransactionMetadata::from(ProgramError::Custom(4242));
match metadata.err {
TransactionError::InstructionError(0, InstructionError::Custom(code)) => {
assert_eq!(code, 4242);
}
other => panic!("expected InstructionError(0, Custom(4242)), got {other:?}"),
}
assert_eq!(metadata.meta, TransactionMetadata::default());
}
#[test]
fn program_error_conversion_preserves_the_widest_custom_code() {
let widest = u32::MAX;
let metadata = FailedTransactionMetadata::from(ProgramError::Custom(widest));
match metadata.err {
TransactionError::InstructionError(0, InstructionError::Custom(code)) => {
assert_eq!(code, widest);
}
other => panic!("expected InstructionError(0, Custom(_)), got {other:?}"),
}
}
#[test]
fn execution_result_default_is_a_non_included_unsupported_version_failure() {
let result = ExecutionResult::default();
assert!(matches!(result.tx_result, Err(TransactionError::UnsupportedVersion)));
assert!(!result.included);
assert_eq!(result.fee, 0);
assert_eq!(result.compute_units_consumed, 0);
assert_eq!(result.signature, Signature::default());
assert_eq!(result.fee_payer, None);
assert!(result.post_accounts.is_empty());
assert!(result.account_source_failures.is_empty());
assert!(result.fatal_error.is_none());
}
proptest! {
#[test]
fn program_error_conversion_is_a_truncating_custom_code(code in any::<u32>()) {
let metadata = FailedTransactionMetadata::from(ProgramError::Custom(code));
match metadata.err {
TransactionError::InstructionError(0, InstructionError::Custom(actual)) => {
prop_assert_eq!(actual, code);
}
other => prop_assert!(false, "unexpected error {other:?}"),
}
prop_assert_eq!(metadata.meta, TransactionMetadata::default());
}
}
proptest! {
#[test]
fn executed_instruction_round_trips_for_any_frame(
stack_height in any::<u8>(),
account_count in 0usize..6,
data in prop::collection::vec(any::<u8>(), 0..32),
) {
let program_id = Address::new_unique();
let accounts: Vec<AccountMeta> = (0..account_count)
.map(|i| {
let pubkey = Address::new_unique();
if i % 2 == 0 {
AccountMeta::new(pubkey, true)
} else {
AccountMeta::new_readonly(pubkey, false)
}
})
.collect();
let expected_accounts = accounts.clone();
let expected_data = data.clone();
let frame = ExecutedInstruction { stack_height, program_id, accounts, data };
let instruction = frame.instruction();
prop_assert_eq!(instruction.program_id, program_id);
prop_assert_eq!(&instruction.accounts, &expected_accounts);
prop_assert_eq!(&instruction.data, &expected_data);
prop_assert_eq!(frame.accounts, expected_accounts);
prop_assert_eq!(frame.data, expected_data);
}
}
proptest! {
#[test]
fn pretty_logs_never_panics(
logs in prop::collection::vec("[^\r\n]*", 0..8),
) {
let meta = TransactionMetadata { logs, ..Default::default() };
let pretty = meta.pretty_logs();
let expected = meta.logs.iter().filter(|line| !line.is_empty()).count();
let reset = "\u{1b}[0m";
prop_assert_eq!(pretty.lines().count(), expected);
prop_assert_eq!(pretty.matches(reset).count(), expected);
}
}
}