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    /// Assert that the transaction failed
180    ///
181    /// # Panics
182    ///
183    /// Panics if the transaction succeeded
184    ///
185    /// # Returns
186    ///
187    /// Returns self for chaining
188    ///
189    /// # Example
190    ///
191    /// ```ignore
192    /// result.assert_failure();
193    /// ```
194    pub fn assert_failure(&self) -> &Self {
195        assert!(
196            self.error.is_some(),
197            "Expected transaction to fail, but it succeeded.\nLogs:\n{}",
198            self.logs().join("\n")
199        );
200        self
201    }
202
203    /// Assert that the transaction failed with a specific error message
204    ///
205    /// # Arguments
206    ///
207    /// * `expected_error` - The expected error message (substring match)
208    ///
209    /// # Panics
210    ///
211    /// Panics if the transaction succeeded or failed with a different error
212    ///
213    /// # Returns
214    ///
215    /// Returns self for chaining
216    ///
217    /// # Example
218    ///
219    /// ```ignore
220    /// result.assert_error("insufficient funds");
221    /// ```
222    pub fn assert_error(&self, expected_error: &str) -> &Self {
223        match &self.error {
224            Some(error) => {
225                assert!(
226                    error.contains(expected_error),
227                    "Transaction failed with unexpected error.\nExpected substring: {}\nActual error: {}\nLogs:\n{}",
228                    expected_error,
229                    error,
230                    self.logs().join("\n")
231                );
232            }
233            None => {
234                panic!(
235                    "Expected transaction to fail with error containing '{}', but it succeeded.\nLogs:\n{}",
236                    expected_error,
237                    self.logs().join("\n")
238                );
239            }
240        }
241        self
242    }
243
244    /// Assert that the transaction failed with a specific error code
245    ///
246    /// This is useful for asserting Anchor custom errors.
247    ///
248    /// # Arguments
249    ///
250    /// * `error_code` - The expected error code number
251    ///
252    /// # Panics
253    ///
254    /// Panics if the transaction succeeded or failed with a different error code
255    ///
256    /// # Returns
257    ///
258    /// Returns self for chaining
259    ///
260    /// # Example
261    ///
262    /// ```ignore
263    /// // Assert that transaction failed with custom error code 6000
264    /// result.assert_error_code(6000);
265    /// ```
266    pub fn assert_error_code(&self, error_code: u32) -> &Self {
267        let error_code_str = format!("custom program error: 0x{:x}", error_code);
268        self.assert_error(&error_code_str)
269    }
270
271    /// Assert that the transaction failed with a specific Anchor error
272    ///
273    /// This checks for Anchor's error code format in the logs.
274    ///
275    /// # Arguments
276    ///
277    /// * `error_name` - The name of the Anchor error
278    ///
279    /// # Panics
280    ///
281    /// Panics if the transaction succeeded or the error wasn't found in logs
282    ///
283    /// # Returns
284    ///
285    /// Returns self for chaining
286    ///
287    /// # Example
288    ///
289    /// ```ignore
290    /// // Assert that transaction failed with Anchor error
291    /// result.assert_anchor_error("InsufficientFunds");
292    /// ```
293    pub fn assert_anchor_error(&self, error_name: &str) -> &Self {
294        self.assert_failure();
295
296        // Check if error name appears in logs
297        let found_in_logs = self.logs().iter().any(|log| log.contains(error_name));
298
299        // Also check the error message
300        let found_in_error = self.error
301            .as_ref()
302            .map(|e| e.contains(error_name))
303            .unwrap_or(false);
304
305        assert!(
306            found_in_logs || found_in_error,
307            "Expected Anchor error '{}' not found in transaction logs or error message.\nError: {:?}\nLogs:\n{}",
308            error_name,
309            self.error,
310            self.logs().join("\n")
311        );
312        self
313    }
314
315    /// Assert that the logs contain a specific error message
316    ///
317    /// Unlike `assert_error`, this only checks the logs, not the error field.
318    ///
319    /// # Arguments
320    ///
321    /// * `error_message` - The expected error message in logs
322    ///
323    /// # Panics
324    ///
325    /// Panics if the error message is not found in logs
326    ///
327    /// # Returns
328    ///
329    /// Returns self for chaining
330    ///
331    /// # Example
332    ///
333    /// ```ignore
334    /// result.assert_log_error("Transfer amount exceeds balance");
335    /// ```
336    pub fn assert_log_error(&self, error_message: &str) -> &Self {
337        assert!(
338            self.has_log(error_message),
339            "Expected error message '{}' not found in logs.\nLogs:\n{}",
340            error_message,
341            self.logs().join("\n")
342        );
343        self
344    }
345}
346
347impl fmt::Debug for TransactionResult {
348    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349        f.debug_struct("TransactionResult")
350            .field("instruction", &self.instruction_name)
351            .field("success", &self.is_success())
352            .field("error", &self.error())
353            .field("compute_units", &self.compute_units())
354            .field("log_count", &self.logs().len())
355            .finish()
356    }
357}
358
359/// Transaction helper methods for LiteSVM
360pub trait TransactionHelpers {
361    /// Send a single instruction and return a wrapped result
362    ///
363    /// # Example
364    /// ```no_run
365    /// # use litesvm_utils::TransactionHelpers;
366    /// # use litesvm::LiteSVM;
367    /// # use solana_program::instruction::Instruction;
368    /// # use solana_sdk::signature::Keypair;
369    /// # let mut svm = LiteSVM::new();
370    /// # let ix = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
371    /// # let signer = Keypair::new();
372    /// let result = svm.send_instruction(ix, &[&signer]).unwrap();
373    /// result.assert_success();
374    /// ```
375    fn send_instruction(
376        &mut self,
377        instruction: Instruction,
378        signers: &[&Keypair],
379    ) -> Result<TransactionResult, TransactionError>;
380
381    /// Send multiple instructions in a single transaction
382    ///
383    /// # Example
384    /// ```no_run
385    /// # use litesvm_utils::TransactionHelpers;
386    /// # use litesvm::LiteSVM;
387    /// # use solana_program::instruction::Instruction;
388    /// # use solana_sdk::signature::Keypair;
389    /// # let mut svm = LiteSVM::new();
390    /// # let ix1 = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
391    /// # let ix2 = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
392    /// # let signer = Keypair::new();
393    /// let result = svm.send_instructions(&[ix1, ix2], &[&signer]).unwrap();
394    /// result.assert_success();
395    /// ```
396    fn send_instructions(
397        &mut self,
398        instructions: &[Instruction],
399        signers: &[&Keypair],
400    ) -> Result<TransactionResult, TransactionError>;
401
402    /// Send a transaction and return a wrapped result
403    ///
404    /// # Example
405    /// ```no_run
406    /// # use litesvm_utils::TransactionHelpers;
407    /// # use litesvm::LiteSVM;
408    /// # use solana_sdk::transaction::Transaction;
409    /// # use solana_program::instruction::Instruction;
410    /// # use solana_sdk::signature::{Keypair, Signer};
411    /// # let mut svm = LiteSVM::new();
412    /// # let ix = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
413    /// # let signer = Keypair::new();
414    /// let tx = Transaction::new_signed_with_payer(
415    ///     &[ix],
416    ///     Some(&signer.pubkey()),
417    ///     &[&signer],
418    ///     svm.latest_blockhash(),
419    /// );
420    /// let result = svm.send_transaction_result(tx).unwrap();
421    /// result.assert_success();
422    /// ```
423    fn send_transaction_result(
424        &mut self,
425        transaction: Transaction,
426    ) -> Result<TransactionResult, TransactionError>;
427}
428
429impl TransactionHelpers for LiteSVM {
430    fn send_instruction(
431        &mut self,
432        instruction: Instruction,
433        signers: &[&Keypair],
434    ) -> Result<TransactionResult, TransactionError> {
435        if signers.is_empty() {
436            return Err(TransactionError::BuildError("No signers provided".to_string()));
437        }
438
439        let tx = Transaction::new_signed_with_payer(
440            &[instruction],
441            Some(&signers[0].pubkey()),
442            signers,
443            self.latest_blockhash(),
444        );
445
446        self.send_transaction_result(tx)
447    }
448
449    fn send_instructions(
450        &mut self,
451        instructions: &[Instruction],
452        signers: &[&Keypair],
453    ) -> Result<TransactionResult, TransactionError> {
454        if signers.is_empty() {
455            return Err(TransactionError::BuildError("No signers provided".to_string()));
456        }
457
458        let tx = Transaction::new_signed_with_payer(
459            instructions,
460            Some(&signers[0].pubkey()),
461            signers,
462            self.latest_blockhash(),
463        );
464
465        self.send_transaction_result(tx)
466    }
467
468    fn send_transaction_result(
469        &mut self,
470        transaction: Transaction,
471    ) -> Result<TransactionResult, TransactionError> {
472        match self.send_transaction(transaction) {
473            Ok(result) => Ok(TransactionResult::new(result, None)),
474            Err(failed) => {
475                // Return a failed transaction result with metadata
476                Ok(TransactionResult::new_failed(
477                    format!("{:?}", failed.err),
478                    failed.meta,
479                    None,
480                ))
481            }
482        }
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use crate::test_helpers::TestHelpers;
490    use solana_system_interface::instruction as system_instruction;
491
492    #[test]
493    fn test_transaction_result_success() {
494        let mut svm = LiteSVM::new();
495        let payer = svm.create_funded_account(10_000_000_000).unwrap();
496        let recipient = Keypair::new();
497
498        // Create a simple transfer instruction
499        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
500
501        let result = svm.send_instruction(ix, &[&payer]).unwrap();
502
503        assert!(result.is_success());
504        assert_eq!(result.error(), None);
505        result.assert_success();
506    }
507
508    #[test]
509    fn test_transaction_result_has_log() {
510        let mut svm = LiteSVM::new();
511        let payer = svm.create_funded_account(10_000_000_000).unwrap();
512        let recipient = Keypair::new();
513
514        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
515        let result = svm.send_instruction(ix, &[&payer]).unwrap();
516
517        // System program logs typically contain "invoke" messages
518        assert!(result.has_log("invoke"));
519    }
520
521    #[test]
522    fn test_transaction_result_find_log() {
523        let mut svm = LiteSVM::new();
524        let payer = svm.create_funded_account(10_000_000_000).unwrap();
525        let recipient = Keypair::new();
526
527        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
528        let result = svm.send_instruction(ix, &[&payer]).unwrap();
529
530        // Should find a log containing "invoke"
531        let log = result.find_log("invoke");
532        assert!(log.is_some());
533    }
534
535    #[test]
536    fn test_transaction_result_compute_units() {
537        let mut svm = LiteSVM::new();
538        let payer = svm.create_funded_account(10_000_000_000).unwrap();
539        let recipient = Keypair::new();
540
541        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
542        let result = svm.send_instruction(ix, &[&payer]).unwrap();
543
544        // Simple transfer should consume some compute units
545        let cu = result.compute_units();
546        assert!(cu > 0);
547        assert!(cu < 1_000_000); // Should be reasonable
548    }
549
550    #[test]
551    fn test_transaction_result_logs() {
552        let mut svm = LiteSVM::new();
553        let payer = svm.create_funded_account(10_000_000_000).unwrap();
554        let recipient = Keypair::new();
555
556        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
557        let result = svm.send_instruction(ix, &[&payer]).unwrap();
558
559        let logs = result.logs();
560        assert!(!logs.is_empty());
561    }
562
563    #[test]
564    fn test_transaction_result_inner() {
565        let mut svm = LiteSVM::new();
566        let payer = svm.create_funded_account(10_000_000_000).unwrap();
567        let recipient = Keypair::new();
568
569        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
570        let result = svm.send_instruction(ix, &[&payer]).unwrap();
571
572        // Should be able to access inner metadata
573        let _inner = result.inner();
574        assert!(_inner.compute_units_consumed > 0);
575    }
576
577    #[test]
578    fn test_transaction_result_failure() {
579        let mut svm = LiteSVM::new();
580        let payer = Keypair::new(); // Unfunded account
581
582        // This should fail due to insufficient funds
583        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
584        let result = svm.send_instruction(ix, &[&payer]).unwrap();
585
586        assert!(!result.is_success());
587        assert!(result.error().is_some());
588    }
589
590    #[test]
591    fn test_transaction_result_assert_failure() {
592        let mut svm = LiteSVM::new();
593        let payer = Keypair::new(); // Unfunded account
594
595        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
596        let result = svm.send_instruction(ix, &[&payer]).unwrap();
597
598        // Should not panic when asserting failure on a failed transaction
599        result.assert_failure();
600    }
601
602    #[test]
603    #[should_panic(expected = "Expected transaction to fail")]
604    fn test_transaction_result_assert_failure_on_success() {
605        let mut svm = LiteSVM::new();
606        let payer = svm.create_funded_account(10_000_000_000).unwrap();
607
608        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
609        let result = svm.send_instruction(ix, &[&payer]).unwrap();
610
611        // Should panic when asserting failure on a successful transaction
612        result.assert_failure();
613    }
614
615    #[test]
616    fn test_transaction_result_assert_error() {
617        let mut svm = LiteSVM::new();
618        let payer = Keypair::new(); // Unfunded account
619
620        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
621        let result = svm.send_instruction(ix, &[&payer]).unwrap();
622
623        // Should contain "AccountNotFound" in the error (account doesn't exist)
624        result.assert_error("AccountNotFound");
625    }
626
627    #[test]
628    #[should_panic(expected = "Transaction failed with unexpected error")]
629    fn test_transaction_result_assert_error_wrong_message() {
630        let mut svm = LiteSVM::new();
631        let payer = Keypair::new(); // Unfunded account
632
633        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
634        let result = svm.send_instruction(ix, &[&payer]).unwrap();
635
636        // Should panic when expecting wrong error message
637        result.assert_error("this error does not exist");
638    }
639
640    #[test]
641    fn test_send_multiple_instructions() {
642        let mut svm = LiteSVM::new();
643        let payer = svm.create_funded_account(10_000_000_000).unwrap();
644        let recipient1 = Keypair::new();
645        let recipient2 = Keypair::new();
646
647        // Send two transfers in one transaction
648        let ix1 = system_instruction::transfer(&payer.pubkey(), &recipient1.pubkey(), 1_000_000);
649        let ix2 = system_instruction::transfer(&payer.pubkey(), &recipient2.pubkey(), 2_000_000);
650
651        let result = svm.send_instructions(&[ix1, ix2], &[&payer]).unwrap();
652        result.assert_success();
653
654        // Verify both transfers succeeded
655        let balance1 = svm.get_balance(&recipient1.pubkey()).unwrap();
656        let balance2 = svm.get_balance(&recipient2.pubkey()).unwrap();
657        assert_eq!(balance1, 1_000_000);
658        assert_eq!(balance2, 2_000_000);
659    }
660
661    #[test]
662    fn test_send_instruction_no_signers() {
663        let mut svm = LiteSVM::new();
664        let payer = Keypair::new();
665        let recipient = Keypair::new();
666
667        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
668
669        // Should error when no signers provided
670        let result = svm.send_instruction(ix, &[]);
671        assert!(result.is_err());
672        match result {
673            Err(TransactionError::BuildError(msg)) => {
674                assert!(msg.contains("No signers"));
675            }
676            _ => panic!("Expected BuildError"),
677        }
678    }
679
680    #[test]
681    fn test_send_instructions_no_signers() {
682        let mut svm = LiteSVM::new();
683        let payer = Keypair::new();
684        let recipient = Keypair::new();
685
686        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
687
688        // Should error when no signers provided
689        let result = svm.send_instructions(&[ix], &[]);
690        assert!(result.is_err());
691    }
692
693    #[test]
694    fn test_transaction_result_debug() {
695        let mut svm = LiteSVM::new();
696        let payer = svm.create_funded_account(10_000_000_000).unwrap();
697        let recipient = Keypair::new();
698
699        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
700        let result = svm.send_instruction(ix, &[&payer]).unwrap();
701
702        // Should be able to format as debug
703        let debug_str = format!("{:?}", result);
704        assert!(debug_str.contains("TransactionResult"));
705    }
706
707    #[test]
708    fn test_transaction_result_print_logs() {
709        let mut svm = LiteSVM::new();
710        let payer = svm.create_funded_account(10_000_000_000).unwrap();
711        let recipient = Keypair::new();
712
713        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
714        let result = svm.send_instruction(ix, &[&payer]).unwrap();
715
716        // Should not panic when printing logs
717        result.print_logs();
718    }
719
720    #[test]
721    fn test_send_transaction_result() {
722        let mut svm = LiteSVM::new();
723        let payer = svm.create_funded_account(10_000_000_000).unwrap();
724        let recipient = Keypair::new();
725
726        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
727        let tx = Transaction::new_signed_with_payer(
728            &[ix],
729            Some(&payer.pubkey()),
730            &[&payer],
731            svm.latest_blockhash(),
732        );
733
734        let result = svm.send_transaction_result(tx).unwrap();
735        result.assert_success();
736    }
737}