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}