litesvm_utils/transaction.rs
1//! Transaction execution and result handling utilities
2//!
3//! This module provides convenient wrappers for executing transactions
4//! and handling their results in tests.
5
6use litesvm::types::TransactionMetadata;
7use litesvm::LiteSVM;
8use solana_program::instruction::Instruction;
9use solana_sdk::signature::{Keypair, Signer};
10use solana_sdk::transaction::Transaction;
11use std::fmt;
12use thiserror::Error;
13
14#[derive(Error, Debug)]
15pub enum TransactionError {
16 #[error("Transaction execution failed: {0}")]
17 ExecutionFailed(String),
18
19 #[error("Transaction build error: {0}")]
20 BuildError(String),
21
22 #[error("Assertion failed: {0}")]
23 AssertionFailed(String),
24}
25
26/// Wrapper around LiteSVM's TransactionMetadata with helper methods for testing
27///
28/// This struct provides convenient methods for analyzing transaction results,
29/// including log inspection, compute unit tracking, and success assertions.
30///
31/// # Example
32///
33/// ```ignore
34/// let result = svm.send_instruction(ix, &[&signer])?;
35/// result.assert_success();
36/// assert!(result.has_log("Transfer complete"));
37/// println!("Used {} compute units", result.compute_units());
38/// ```
39pub struct TransactionResult {
40 inner: TransactionMetadata,
41 instruction_name: Option<String>,
42 error: Option<String>,
43}
44
45impl TransactionResult {
46 /// Create a new TransactionResult wrapper for successful transaction
47 ///
48 /// # Arguments
49 ///
50 /// * `result` - The transaction metadata from LiteSVM
51 /// * `instruction_name` - Optional name of the instruction for debugging
52 pub fn new(result: TransactionMetadata, instruction_name: Option<String>) -> Self {
53 Self {
54 inner: result,
55 instruction_name,
56 error: None,
57 }
58 }
59
60 /// Create a new TransactionResult wrapper for failed transaction
61 ///
62 /// # Arguments
63 ///
64 /// * `error` - The error message
65 /// * `result` - The transaction metadata from LiteSVM
66 /// * `instruction_name` - Optional name of the instruction for debugging
67 pub fn new_failed(error: String, result: TransactionMetadata, instruction_name: Option<String>) -> Self {
68 Self {
69 inner: result,
70 instruction_name,
71 error: Some(error),
72 }
73 }
74
75 /// Assert that the transaction succeeded, panic with logs if it failed
76 ///
77 /// # Returns
78 ///
79 /// Returns self for chaining
80 ///
81 /// # Example
82 ///
83 /// ```ignore
84 /// result.assert_success();
85 /// ```
86 pub fn assert_success(&self) -> &Self {
87 assert!(
88 self.error.is_none(),
89 "Transaction failed: {}\nLogs:\n{}",
90 self.error.as_ref().unwrap_or(&"Unknown error".to_string()),
91 self.logs().join("\n")
92 );
93 self
94 }
95
96 /// Check if the transaction succeeded
97 ///
98 /// # Returns
99 ///
100 /// true if the transaction succeeded, false otherwise
101 pub fn is_success(&self) -> bool {
102 self.error.is_none()
103 }
104
105 /// Get the error message if the transaction failed
106 ///
107 /// # Returns
108 ///
109 /// The error message if the transaction failed, None otherwise
110 pub fn error(&self) -> Option<&String> {
111 self.error.as_ref()
112 }
113
114 /// Get the transaction logs
115 ///
116 /// # Returns
117 ///
118 /// A slice of log messages
119 pub fn logs(&self) -> &[String] {
120 &self.inner.logs
121 }
122
123 /// Check if the logs contain a specific message
124 ///
125 /// # Arguments
126 ///
127 /// * `message` - The message to search for
128 ///
129 /// # Returns
130 ///
131 /// true if the message is found in the logs, false otherwise
132 pub fn has_log(&self, message: &str) -> bool {
133 self.inner.logs.iter().any(|log| log.contains(message))
134 }
135
136 /// Find a log entry containing the specified text
137 ///
138 /// # Arguments
139 ///
140 /// * `pattern` - The pattern to search for
141 ///
142 /// # Returns
143 ///
144 /// The first matching log entry, or None
145 pub fn find_log(&self, pattern: &str) -> Option<&String> {
146 self.inner.logs.iter().find(|log| log.contains(pattern))
147 }
148
149 /// Get the compute units consumed
150 ///
151 /// # Returns
152 ///
153 /// The number of compute units consumed
154 pub fn compute_units(&self) -> u64 {
155 self.inner.compute_units_consumed
156 }
157
158 /// Print the transaction logs
159 pub fn print_logs(&self) {
160 println!("=== Transaction Logs ===");
161 if let Some(name) = &self.instruction_name {
162 println!("Instruction: {}", name);
163 }
164 for log in &self.inner.logs {
165 println!("{}", log);
166 }
167 if let Some(err) = &self.error {
168 println!("Error: {}", err);
169 }
170 println!("Compute Units: {}", self.compute_units());
171 println!("========================");
172 }
173
174 /// Get the inner TransactionMetadata for direct access
175 pub fn inner(&self) -> &TransactionMetadata {
176 &self.inner
177 }
178}
179
180impl fmt::Debug for TransactionResult {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 f.debug_struct("TransactionResult")
183 .field("instruction", &self.instruction_name)
184 .field("success", &self.is_success())
185 .field("error", &self.error())
186 .field("compute_units", &self.compute_units())
187 .field("log_count", &self.logs().len())
188 .finish()
189 }
190}
191
192/// Transaction helper methods for LiteSVM
193pub trait TransactionHelpers {
194 /// Send a single instruction and return a wrapped result
195 ///
196 /// # Example
197 /// ```no_run
198 /// # use litesvm_utils::TransactionHelpers;
199 /// # use litesvm::LiteSVM;
200 /// # use solana_program::instruction::Instruction;
201 /// # use solana_sdk::signature::Keypair;
202 /// # let mut svm = LiteSVM::new();
203 /// # let ix = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
204 /// # let signer = Keypair::new();
205 /// let result = svm.send_instruction(ix, &[&signer]).unwrap();
206 /// result.assert_success();
207 /// ```
208 fn send_instruction(
209 &mut self,
210 instruction: Instruction,
211 signers: &[&Keypair],
212 ) -> Result<TransactionResult, TransactionError>;
213
214 /// Send multiple instructions in a single transaction
215 ///
216 /// # Example
217 /// ```no_run
218 /// # use litesvm_utils::TransactionHelpers;
219 /// # use litesvm::LiteSVM;
220 /// # use solana_program::instruction::Instruction;
221 /// # use solana_sdk::signature::Keypair;
222 /// # let mut svm = LiteSVM::new();
223 /// # let ix1 = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
224 /// # let ix2 = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
225 /// # let signer = Keypair::new();
226 /// let result = svm.send_instructions(&[ix1, ix2], &[&signer]).unwrap();
227 /// result.assert_success();
228 /// ```
229 fn send_instructions(
230 &mut self,
231 instructions: &[Instruction],
232 signers: &[&Keypair],
233 ) -> Result<TransactionResult, TransactionError>;
234
235 /// Send a transaction and return a wrapped result
236 ///
237 /// # Example
238 /// ```no_run
239 /// # use litesvm_utils::TransactionHelpers;
240 /// # use litesvm::LiteSVM;
241 /// # use solana_sdk::transaction::Transaction;
242 /// # use solana_program::instruction::Instruction;
243 /// # use solana_sdk::signature::{Keypair, Signer};
244 /// # let mut svm = LiteSVM::new();
245 /// # let ix = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
246 /// # let signer = Keypair::new();
247 /// let tx = Transaction::new_signed_with_payer(
248 /// &[ix],
249 /// Some(&signer.pubkey()),
250 /// &[&signer],
251 /// svm.latest_blockhash(),
252 /// );
253 /// let result = svm.send_transaction_result(tx).unwrap();
254 /// result.assert_success();
255 /// ```
256 fn send_transaction_result(
257 &mut self,
258 transaction: Transaction,
259 ) -> Result<TransactionResult, TransactionError>;
260}
261
262impl TransactionHelpers for LiteSVM {
263 fn send_instruction(
264 &mut self,
265 instruction: Instruction,
266 signers: &[&Keypair],
267 ) -> Result<TransactionResult, TransactionError> {
268 if signers.is_empty() {
269 return Err(TransactionError::BuildError("No signers provided".to_string()));
270 }
271
272 let tx = Transaction::new_signed_with_payer(
273 &[instruction],
274 Some(&signers[0].pubkey()),
275 signers,
276 self.latest_blockhash(),
277 );
278
279 self.send_transaction_result(tx)
280 }
281
282 fn send_instructions(
283 &mut self,
284 instructions: &[Instruction],
285 signers: &[&Keypair],
286 ) -> Result<TransactionResult, TransactionError> {
287 if signers.is_empty() {
288 return Err(TransactionError::BuildError("No signers provided".to_string()));
289 }
290
291 let tx = Transaction::new_signed_with_payer(
292 instructions,
293 Some(&signers[0].pubkey()),
294 signers,
295 self.latest_blockhash(),
296 );
297
298 self.send_transaction_result(tx)
299 }
300
301 fn send_transaction_result(
302 &mut self,
303 transaction: Transaction,
304 ) -> Result<TransactionResult, TransactionError> {
305 match self.send_transaction(transaction) {
306 Ok(result) => Ok(TransactionResult::new(result, None)),
307 Err(failed) => {
308 // Return a failed transaction result with metadata
309 Ok(TransactionResult::new_failed(
310 format!("{:?}", failed.err),
311 failed.meta,
312 None,
313 ))
314 }
315 }
316 }
317}