use litesvm::types::TransactionMetadata;
use litesvm::LiteSVM;
use solana_keypair::Keypair;
use solana_program::instruction::Instruction;
use solana_signer::Signer;
use solana_transaction::Transaction;
use std::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TransactionError {
#[error("Transaction execution failed: {0}")]
ExecutionFailed(String),
#[error("Transaction build error: {0}")]
BuildError(String),
#[error("Assertion failed: {0}")]
AssertionFailed(String),
}
pub struct TransactionResult {
inner: TransactionMetadata,
instruction_name: Option<String>,
error: Option<String>,
}
impl TransactionResult {
pub fn new(result: TransactionMetadata, instruction_name: Option<String>) -> Self {
Self {
inner: result,
instruction_name,
error: None,
}
}
pub fn new_failed(
error: String,
result: TransactionMetadata,
instruction_name: Option<String>,
) -> Self {
Self {
inner: result,
instruction_name,
error: Some(error),
}
}
pub fn assert_success(&self) -> &Self {
assert!(
self.error.is_none(),
"Transaction failed: {}\nLogs:\n{}",
self.error.as_ref().unwrap_or(&"Unknown error".to_string()),
self.logs().join("\n")
);
self
}
pub fn is_success(&self) -> bool {
self.error.is_none()
}
pub fn error(&self) -> Option<&String> {
self.error.as_ref()
}
pub fn logs(&self) -> &[String] {
&self.inner.logs
}
pub fn has_log(&self, message: &str) -> bool {
self.inner.logs.iter().any(|log| log.contains(message))
}
pub fn find_log(&self, pattern: &str) -> Option<&String> {
self.inner.logs.iter().find(|log| log.contains(pattern))
}
pub fn compute_units(&self) -> u64 {
self.inner.compute_units_consumed
}
pub fn print_logs(&self) {
println!("=== Transaction Logs ===");
if let Some(name) = &self.instruction_name {
println!("Instruction: {}", name);
}
for log in &self.inner.logs {
println!("{}", log);
}
if let Some(err) = &self.error {
println!("Error: {}", err);
}
println!("Compute Units: {}", self.compute_units());
println!("========================");
}
pub fn inner(&self) -> &TransactionMetadata {
&self.inner
}
pub fn assert_failure(&self) -> &Self {
assert!(
self.error.is_some(),
"Expected transaction to fail, but it succeeded.\nLogs:\n{}",
self.logs().join("\n")
);
self
}
pub fn assert_error(&self, expected_error: &str) -> &Self {
match &self.error {
Some(error) => {
assert!(
error.contains(expected_error),
"Transaction failed with unexpected error.\nExpected substring: {}\nActual error: {}\nLogs:\n{}",
expected_error,
error,
self.logs().join("\n")
);
}
None => {
panic!(
"Expected transaction to fail with error containing '{}', but it succeeded.\nLogs:\n{}",
expected_error,
self.logs().join("\n")
);
}
}
self
}
pub fn assert_error_code(&self, error_code: u32) -> &Self {
let error_code_str = format!("custom program error: 0x{:x}", error_code);
self.assert_error(&error_code_str)
}
pub fn assert_anchor_error(&self, error_name: &str) -> &Self {
self.assert_failure();
let found_in_logs = self.logs().iter().any(|log| log.contains(error_name));
let found_in_error = self
.error
.as_ref()
.map(|e| e.contains(error_name))
.unwrap_or(false);
assert!(
found_in_logs || found_in_error,
"Expected Anchor error '{}' not found in transaction logs or error message.\nError: {:?}\nLogs:\n{}",
error_name,
self.error,
self.logs().join("\n")
);
self
}
pub fn assert_log_error(&self, error_message: &str) -> &Self {
assert!(
self.has_log(error_message),
"Expected error message '{}' not found in logs.\nLogs:\n{}",
error_message,
self.logs().join("\n")
);
self
}
}
impl fmt::Debug for TransactionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TransactionResult")
.field("instruction", &self.instruction_name)
.field("success", &self.is_success())
.field("error", &self.error())
.field("compute_units", &self.compute_units())
.field("log_count", &self.logs().len())
.finish()
}
}
pub trait TransactionHelpers {
fn send_instruction(
&mut self,
instruction: Instruction,
signers: &[&Keypair],
) -> Result<TransactionResult, TransactionError>;
fn send_instructions(
&mut self,
instructions: &[Instruction],
signers: &[&Keypair],
) -> Result<TransactionResult, TransactionError>;
fn send_transaction_result(
&mut self,
transaction: Transaction,
) -> Result<TransactionResult, TransactionError>;
}
impl TransactionHelpers for LiteSVM {
fn send_instruction(
&mut self,
instruction: Instruction,
signers: &[&Keypair],
) -> Result<TransactionResult, TransactionError> {
if signers.is_empty() {
return Err(TransactionError::BuildError(
"No signers provided".to_string(),
));
}
let tx = Transaction::new_signed_with_payer(
&[instruction],
Some(&signers[0].pubkey()),
signers,
self.latest_blockhash(),
);
self.send_transaction_result(tx)
}
fn send_instructions(
&mut self,
instructions: &[Instruction],
signers: &[&Keypair],
) -> Result<TransactionResult, TransactionError> {
if signers.is_empty() {
return Err(TransactionError::BuildError(
"No signers provided".to_string(),
));
}
let tx = Transaction::new_signed_with_payer(
instructions,
Some(&signers[0].pubkey()),
signers,
self.latest_blockhash(),
);
self.send_transaction_result(tx)
}
fn send_transaction_result(
&mut self,
transaction: Transaction,
) -> Result<TransactionResult, TransactionError> {
match self.send_transaction(transaction) {
Ok(result) => Ok(TransactionResult::new(result, None)),
Err(failed) => {
Ok(TransactionResult::new_failed(
format!("{:?}", failed.err),
failed.meta,
None,
))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::TestHelpers;
use solana_system_interface::instruction as system_instruction;
#[test]
fn test_transaction_result_success() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
assert!(result.is_success());
assert_eq!(result.error(), None);
result.assert_success();
}
#[test]
fn test_transaction_result_has_log() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
assert!(result.has_log("invoke"));
}
#[test]
fn test_transaction_result_find_log() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
let log = result.find_log("invoke");
assert!(log.is_some());
}
#[test]
fn test_transaction_result_compute_units() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
let cu = result.compute_units();
assert!(cu > 0);
assert!(cu < 1_000_000); }
#[test]
fn test_transaction_result_logs() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
let logs = result.logs();
assert!(!logs.is_empty());
}
#[test]
fn test_transaction_result_inner() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
let _inner = result.inner();
assert!(_inner.compute_units_consumed > 0);
}
#[test]
fn test_transaction_result_failure() {
let mut svm = LiteSVM::new();
let payer = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
assert!(!result.is_success());
assert!(result.error().is_some());
}
#[test]
fn test_transaction_result_assert_failure() {
let mut svm = LiteSVM::new();
let payer = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
result.assert_failure();
}
#[test]
#[should_panic(expected = "Expected transaction to fail")]
fn test_transaction_result_assert_failure_on_success() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
result.assert_failure();
}
#[test]
fn test_transaction_result_assert_error() {
let mut svm = LiteSVM::new();
let payer = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
result.assert_error("AccountNotFound");
}
#[test]
#[should_panic(expected = "Transaction failed with unexpected error")]
fn test_transaction_result_assert_error_wrong_message() {
let mut svm = LiteSVM::new();
let payer = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
result.assert_error("this error does not exist");
}
#[test]
fn test_send_multiple_instructions() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient1 = Keypair::new();
let recipient2 = Keypair::new();
let ix1 = system_instruction::transfer(&payer.pubkey(), &recipient1.pubkey(), 1_000_000);
let ix2 = system_instruction::transfer(&payer.pubkey(), &recipient2.pubkey(), 2_000_000);
let result = svm.send_instructions(&[ix1, ix2], &[&payer]).unwrap();
result.assert_success();
let balance1 = svm.get_balance(&recipient1.pubkey()).unwrap();
let balance2 = svm.get_balance(&recipient2.pubkey()).unwrap();
assert_eq!(balance1, 1_000_000);
assert_eq!(balance2, 2_000_000);
}
#[test]
fn test_send_instruction_no_signers() {
let mut svm = LiteSVM::new();
let payer = Keypair::new();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[]);
assert!(result.is_err());
match result {
Err(TransactionError::BuildError(msg)) => {
assert!(msg.contains("No signers"));
}
_ => panic!("Expected BuildError"),
}
}
#[test]
fn test_send_instructions_no_signers() {
let mut svm = LiteSVM::new();
let payer = Keypair::new();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instructions(&[ix], &[]);
assert!(result.is_err());
}
#[test]
fn test_transaction_result_debug() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
let debug_str = format!("{:?}", result);
assert!(debug_str.contains("TransactionResult"));
}
#[test]
fn test_transaction_result_print_logs() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let result = svm.send_instruction(ix, &[&payer]).unwrap();
result.print_logs();
}
#[test]
fn test_send_transaction_result() {
let mut svm = LiteSVM::new();
let payer = svm.create_funded_account(10_000_000_000).unwrap();
let recipient = Keypair::new();
let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&payer.pubkey()),
&[&payer],
svm.latest_blockhash(),
);
let result = svm.send_transaction_result(tx).unwrap();
result.assert_success();
}
}