Skip to main content

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_keypair::Keypair;
9use solana_program::instruction::Instruction;
10use solana_signer::Signer;
11use solana_transaction::Transaction;
12use std::fmt;
13use thiserror::Error;
14
15#[derive(Error, Debug)]
16pub enum TransactionError {
17    #[error("Transaction execution failed: {0}")]
18    ExecutionFailed(String),
19
20    #[error("Transaction build error: {0}")]
21    BuildError(String),
22
23    #[error("Assertion failed: {0}")]
24    AssertionFailed(String),
25}
26
27/// Wrapper around LiteSVM's TransactionMetadata with helper methods for testing
28///
29/// This struct provides convenient methods for analyzing transaction results,
30/// including log inspection, compute unit tracking, and success assertions.
31///
32/// # Example
33///
34/// ```ignore
35/// let result = svm.send_instruction(ix, &[&signer])?;
36/// result.assert_success();
37/// assert!(result.has_log("Transfer complete"));
38/// println!("Used {} compute units", result.compute_units());
39/// ```
40pub struct TransactionResult {
41    inner: TransactionMetadata,
42    instruction_name: Option<String>,
43    error: Option<String>,
44}
45
46impl TransactionResult {
47    /// Create a new TransactionResult wrapper for successful transaction
48    ///
49    /// # Arguments
50    ///
51    /// * `result` - The transaction metadata from LiteSVM
52    /// * `instruction_name` - Optional name of the instruction for debugging
53    pub fn new(result: TransactionMetadata, instruction_name: Option<String>) -> Self {
54        Self {
55            inner: result,
56            instruction_name,
57            error: None,
58        }
59    }
60
61    /// Create a new TransactionResult wrapper for failed transaction
62    ///
63    /// # Arguments
64    ///
65    /// * `error` - The error message
66    /// * `result` - The transaction metadata from LiteSVM
67    /// * `instruction_name` - Optional name of the instruction for debugging
68    pub fn new_failed(
69        error: String,
70        result: TransactionMetadata,
71        instruction_name: Option<String>,
72    ) -> Self {
73        Self {
74            inner: result,
75            instruction_name,
76            error: Some(error),
77        }
78    }
79
80    /// Assert that the transaction succeeded, panic with logs if it failed
81    ///
82    /// # Returns
83    ///
84    /// Returns self for chaining
85    ///
86    /// # Example
87    ///
88    /// ```ignore
89    /// result.assert_success();
90    /// ```
91    pub fn assert_success(&self) -> &Self {
92        assert!(
93            self.error.is_none(),
94            "Transaction failed: {}\nLogs:\n{}",
95            self.error.as_ref().unwrap_or(&"Unknown error".to_string()),
96            self.logs().join("\n")
97        );
98        self
99    }
100
101    /// Check if the transaction succeeded
102    ///
103    /// # Returns
104    ///
105    /// true if the transaction succeeded, false otherwise
106    pub fn is_success(&self) -> bool {
107        self.error.is_none()
108    }
109
110    /// Get the error message if the transaction failed
111    ///
112    /// # Returns
113    ///
114    /// The error message if the transaction failed, None otherwise
115    pub fn error(&self) -> Option<&String> {
116        self.error.as_ref()
117    }
118
119    /// Get the transaction logs
120    ///
121    /// # Returns
122    ///
123    /// A slice of log messages
124    pub fn logs(&self) -> &[String] {
125        &self.inner.logs
126    }
127
128    /// Check if the logs contain a specific message
129    ///
130    /// # Arguments
131    ///
132    /// * `message` - The message to search for
133    ///
134    /// # Returns
135    ///
136    /// true if the message is found in the logs, false otherwise
137    pub fn has_log(&self, message: &str) -> bool {
138        self.inner.logs.iter().any(|log| log.contains(message))
139    }
140
141    /// Find a log entry containing the specified text
142    ///
143    /// # Arguments
144    ///
145    /// * `pattern` - The pattern to search for
146    ///
147    /// # Returns
148    ///
149    /// The first matching log entry, or None
150    pub fn find_log(&self, pattern: &str) -> Option<&String> {
151        self.inner.logs.iter().find(|log| log.contains(pattern))
152    }
153
154    /// Get the compute units consumed
155    ///
156    /// # Returns
157    ///
158    /// The number of compute units consumed
159    pub fn compute_units(&self) -> u64 {
160        self.inner.compute_units_consumed
161    }
162
163    /// Print the transaction logs
164    pub fn print_logs(&self) {
165        println!("=== Transaction Logs ===");
166        if let Some(name) = &self.instruction_name {
167            println!("Instruction: {}", name);
168        }
169        for log in &self.inner.logs {
170            println!("{}", log);
171        }
172        if let Some(err) = &self.error {
173            println!("Error: {}", err);
174        }
175        println!("Compute Units: {}", self.compute_units());
176        println!("========================");
177    }
178
179    /// Get the inner TransactionMetadata for direct access
180    pub fn inner(&self) -> &TransactionMetadata {
181        &self.inner
182    }
183
184    /// Assert that the transaction failed
185    ///
186    /// # Panics
187    ///
188    /// Panics if the transaction succeeded
189    ///
190    /// # Returns
191    ///
192    /// Returns self for chaining
193    ///
194    /// # Example
195    ///
196    /// ```ignore
197    /// result.assert_failure();
198    /// ```
199    pub fn assert_failure(&self) -> &Self {
200        assert!(
201            self.error.is_some(),
202            "Expected transaction to fail, but it succeeded.\nLogs:\n{}",
203            self.logs().join("\n")
204        );
205        self
206    }
207
208    /// Assert that the transaction failed with a specific error message
209    ///
210    /// # Arguments
211    ///
212    /// * `expected_error` - The expected error message (substring match)
213    ///
214    /// # Panics
215    ///
216    /// Panics if the transaction succeeded or failed with a different error
217    ///
218    /// # Returns
219    ///
220    /// Returns self for chaining
221    ///
222    /// # Example
223    ///
224    /// ```ignore
225    /// result.assert_error("insufficient funds");
226    /// ```
227    pub fn assert_error(&self, expected_error: &str) -> &Self {
228        match &self.error {
229            Some(error) => {
230                assert!(
231                    error.contains(expected_error),
232                    "Transaction failed with unexpected error.\nExpected substring: {}\nActual error: {}\nLogs:\n{}",
233                    expected_error,
234                    error,
235                    self.logs().join("\n")
236                );
237            }
238            None => {
239                panic!(
240                    "Expected transaction to fail with error containing '{}', but it succeeded.\nLogs:\n{}",
241                    expected_error,
242                    self.logs().join("\n")
243                );
244            }
245        }
246        self
247    }
248
249    /// Assert that the transaction failed with a specific error code
250    ///
251    /// This is useful for asserting Anchor custom errors.
252    ///
253    /// # Arguments
254    ///
255    /// * `error_code` - The expected error code number
256    ///
257    /// # Panics
258    ///
259    /// Panics if the transaction succeeded or failed with a different error code
260    ///
261    /// # Returns
262    ///
263    /// Returns self for chaining
264    ///
265    /// # Example
266    ///
267    /// ```ignore
268    /// // Assert that transaction failed with custom error code 6000
269    /// result.assert_error_code(6000);
270    /// ```
271    pub fn assert_error_code(&self, error_code: u32) -> &Self {
272        let error_code_str = format!("custom program error: 0x{:x}", error_code);
273        self.assert_error(&error_code_str)
274    }
275
276    /// Assert that the transaction failed with a specific Anchor error
277    ///
278    /// This checks for Anchor's error code format in the logs.
279    ///
280    /// # Arguments
281    ///
282    /// * `error_name` - The name of the Anchor error
283    ///
284    /// # Panics
285    ///
286    /// Panics if the transaction succeeded or the error wasn't found in logs
287    ///
288    /// # Returns
289    ///
290    /// Returns self for chaining
291    ///
292    /// # Example
293    ///
294    /// ```ignore
295    /// // Assert that transaction failed with Anchor error
296    /// result.assert_anchor_error("InsufficientFunds");
297    /// ```
298    pub fn assert_anchor_error(&self, error_name: &str) -> &Self {
299        self.assert_failure();
300
301        // Check if error name appears in logs
302        let found_in_logs = self.logs().iter().any(|log| log.contains(error_name));
303
304        // Also check the error message
305        let found_in_error = self
306            .error
307            .as_ref()
308            .map(|e| e.contains(error_name))
309            .unwrap_or(false);
310
311        assert!(
312            found_in_logs || found_in_error,
313            "Expected Anchor error '{}' not found in transaction logs or error message.\nError: {:?}\nLogs:\n{}",
314            error_name,
315            self.error,
316            self.logs().join("\n")
317        );
318        self
319    }
320
321    /// Assert that the logs contain a specific error message
322    ///
323    /// Unlike `assert_error`, this only checks the logs, not the error field.
324    ///
325    /// # Arguments
326    ///
327    /// * `error_message` - The expected error message in logs
328    ///
329    /// # Panics
330    ///
331    /// Panics if the error message is not found in logs
332    ///
333    /// # Returns
334    ///
335    /// Returns self for chaining
336    ///
337    /// # Example
338    ///
339    /// ```ignore
340    /// result.assert_log_error("Transfer amount exceeds balance");
341    /// ```
342    pub fn assert_log_error(&self, error_message: &str) -> &Self {
343        assert!(
344            self.has_log(error_message),
345            "Expected error message '{}' not found in logs.\nLogs:\n{}",
346            error_message,
347            self.logs().join("\n")
348        );
349        self
350    }
351}
352
353impl fmt::Debug for TransactionResult {
354    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355        f.debug_struct("TransactionResult")
356            .field("instruction", &self.instruction_name)
357            .field("success", &self.is_success())
358            .field("error", &self.error())
359            .field("compute_units", &self.compute_units())
360            .field("log_count", &self.logs().len())
361            .finish()
362    }
363}
364
365/// Transaction helper methods for LiteSVM
366pub trait TransactionHelpers {
367    /// Send a single instruction and return a wrapped result
368    ///
369    /// # Example
370    /// ```no_run
371    /// # use litesvm_utils::TransactionHelpers;
372    /// # use litesvm::LiteSVM;
373    /// # use solana_program::instruction::Instruction;
374    /// # use solana_keypair::Keypair;
375    /// # let mut svm = LiteSVM::new();
376    /// # let ix = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
377    /// # let signer = Keypair::new();
378    /// let result = svm.send_instruction(ix, &[&signer]).unwrap();
379    /// result.assert_success();
380    /// ```
381    fn send_instruction(
382        &mut self,
383        instruction: Instruction,
384        signers: &[&Keypair],
385    ) -> Result<TransactionResult, TransactionError>;
386
387    /// Send multiple instructions in a single transaction
388    ///
389    /// # Example
390    /// ```no_run
391    /// # use litesvm_utils::TransactionHelpers;
392    /// # use litesvm::LiteSVM;
393    /// # use solana_program::instruction::Instruction;
394    /// # use solana_keypair::Keypair;
395    /// # let mut svm = LiteSVM::new();
396    /// # let ix1 = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
397    /// # let ix2 = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
398    /// # let signer = Keypair::new();
399    /// let result = svm.send_instructions(&[ix1, ix2], &[&signer]).unwrap();
400    /// result.assert_success();
401    /// ```
402    fn send_instructions(
403        &mut self,
404        instructions: &[Instruction],
405        signers: &[&Keypair],
406    ) -> Result<TransactionResult, TransactionError>;
407
408    /// Send a transaction and return a wrapped result
409    ///
410    /// # Example
411    /// ```no_run
412    /// # use litesvm_utils::TransactionHelpers;
413    /// # use litesvm::LiteSVM;
414    /// # use solana_program::instruction::Instruction;
415    /// # use solana_keypair::Keypair;
416    /// # use solana_signer::Signer;
417    /// # use solana_transaction::Transaction;
418    /// # let mut svm = LiteSVM::new();
419    /// # let ix = Instruction::new_with_bytes(solana_program::pubkey::Pubkey::new_unique(), &[], vec![]);
420    /// # let signer = Keypair::new();
421    /// let tx = Transaction::new_signed_with_payer(
422    ///     &[ix],
423    ///     Some(&signer.pubkey()),
424    ///     &[&signer],
425    ///     svm.latest_blockhash(),
426    /// );
427    /// let result = svm.send_transaction_result(tx).unwrap();
428    /// result.assert_success();
429    /// ```
430    fn send_transaction_result(
431        &mut self,
432        transaction: Transaction,
433    ) -> Result<TransactionResult, TransactionError>;
434}
435
436impl TransactionHelpers for LiteSVM {
437    fn send_instruction(
438        &mut self,
439        instruction: Instruction,
440        signers: &[&Keypair],
441    ) -> Result<TransactionResult, TransactionError> {
442        if signers.is_empty() {
443            return Err(TransactionError::BuildError(
444                "No signers provided".to_string(),
445            ));
446        }
447
448        let tx = Transaction::new_signed_with_payer(
449            &[instruction],
450            Some(&signers[0].pubkey()),
451            signers,
452            self.latest_blockhash(),
453        );
454
455        self.send_transaction_result(tx)
456    }
457
458    fn send_instructions(
459        &mut self,
460        instructions: &[Instruction],
461        signers: &[&Keypair],
462    ) -> Result<TransactionResult, TransactionError> {
463        if signers.is_empty() {
464            return Err(TransactionError::BuildError(
465                "No signers provided".to_string(),
466            ));
467        }
468
469        let tx = Transaction::new_signed_with_payer(
470            instructions,
471            Some(&signers[0].pubkey()),
472            signers,
473            self.latest_blockhash(),
474        );
475
476        self.send_transaction_result(tx)
477    }
478
479    fn send_transaction_result(
480        &mut self,
481        transaction: Transaction,
482    ) -> Result<TransactionResult, TransactionError> {
483        match self.send_transaction(transaction) {
484            Ok(result) => Ok(TransactionResult::new(result, None)),
485            Err(failed) => {
486                // Return a failed transaction result with metadata
487                Ok(TransactionResult::new_failed(
488                    format!("{:?}", failed.err),
489                    failed.meta,
490                    None,
491                ))
492            }
493        }
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::test_helpers::TestHelpers;
501    use solana_system_interface::instruction as system_instruction;
502
503    #[test]
504    fn test_transaction_result_success() {
505        let mut svm = LiteSVM::new();
506        let payer = svm.create_funded_account(10_000_000_000).unwrap();
507        let recipient = Keypair::new();
508
509        // Create a simple transfer instruction
510        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
511
512        let result = svm.send_instruction(ix, &[&payer]).unwrap();
513
514        assert!(result.is_success());
515        assert_eq!(result.error(), None);
516        result.assert_success();
517    }
518
519    #[test]
520    fn test_transaction_result_has_log() {
521        let mut svm = LiteSVM::new();
522        let payer = svm.create_funded_account(10_000_000_000).unwrap();
523        let recipient = Keypair::new();
524
525        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
526        let result = svm.send_instruction(ix, &[&payer]).unwrap();
527
528        // System program logs typically contain "invoke" messages
529        assert!(result.has_log("invoke"));
530    }
531
532    #[test]
533    fn test_transaction_result_find_log() {
534        let mut svm = LiteSVM::new();
535        let payer = svm.create_funded_account(10_000_000_000).unwrap();
536        let recipient = Keypair::new();
537
538        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
539        let result = svm.send_instruction(ix, &[&payer]).unwrap();
540
541        // Should find a log containing "invoke"
542        let log = result.find_log("invoke");
543        assert!(log.is_some());
544    }
545
546    #[test]
547    fn test_transaction_result_compute_units() {
548        let mut svm = LiteSVM::new();
549        let payer = svm.create_funded_account(10_000_000_000).unwrap();
550        let recipient = Keypair::new();
551
552        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
553        let result = svm.send_instruction(ix, &[&payer]).unwrap();
554
555        // Simple transfer should consume some compute units
556        let cu = result.compute_units();
557        assert!(cu > 0);
558        assert!(cu < 1_000_000); // Should be reasonable
559    }
560
561    #[test]
562    fn test_transaction_result_logs() {
563        let mut svm = LiteSVM::new();
564        let payer = svm.create_funded_account(10_000_000_000).unwrap();
565        let recipient = Keypair::new();
566
567        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
568        let result = svm.send_instruction(ix, &[&payer]).unwrap();
569
570        let logs = result.logs();
571        assert!(!logs.is_empty());
572    }
573
574    #[test]
575    fn test_transaction_result_inner() {
576        let mut svm = LiteSVM::new();
577        let payer = svm.create_funded_account(10_000_000_000).unwrap();
578        let recipient = Keypair::new();
579
580        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
581        let result = svm.send_instruction(ix, &[&payer]).unwrap();
582
583        // Should be able to access inner metadata
584        let _inner = result.inner();
585        assert!(_inner.compute_units_consumed > 0);
586    }
587
588    #[test]
589    fn test_transaction_result_failure() {
590        let mut svm = LiteSVM::new();
591        let payer = Keypair::new(); // Unfunded account
592
593        // This should fail due to insufficient funds
594        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
595        let result = svm.send_instruction(ix, &[&payer]).unwrap();
596
597        assert!(!result.is_success());
598        assert!(result.error().is_some());
599    }
600
601    #[test]
602    fn test_transaction_result_assert_failure() {
603        let mut svm = LiteSVM::new();
604        let payer = Keypair::new(); // Unfunded account
605
606        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
607        let result = svm.send_instruction(ix, &[&payer]).unwrap();
608
609        // Should not panic when asserting failure on a failed transaction
610        result.assert_failure();
611    }
612
613    #[test]
614    #[should_panic(expected = "Expected transaction to fail")]
615    fn test_transaction_result_assert_failure_on_success() {
616        let mut svm = LiteSVM::new();
617        let payer = svm.create_funded_account(10_000_000_000).unwrap();
618
619        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
620        let result = svm.send_instruction(ix, &[&payer]).unwrap();
621
622        // Should panic when asserting failure on a successful transaction
623        result.assert_failure();
624    }
625
626    #[test]
627    fn test_transaction_result_assert_error() {
628        let mut svm = LiteSVM::new();
629        let payer = Keypair::new(); // Unfunded account
630
631        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
632        let result = svm.send_instruction(ix, &[&payer]).unwrap();
633
634        // Should contain "AccountNotFound" in the error (account doesn't exist)
635        result.assert_error("AccountNotFound");
636    }
637
638    #[test]
639    #[should_panic(expected = "Transaction failed with unexpected error")]
640    fn test_transaction_result_assert_error_wrong_message() {
641        let mut svm = LiteSVM::new();
642        let payer = Keypair::new(); // Unfunded account
643
644        let ix = system_instruction::transfer(&payer.pubkey(), &Keypair::new().pubkey(), 1_000_000);
645        let result = svm.send_instruction(ix, &[&payer]).unwrap();
646
647        // Should panic when expecting wrong error message
648        result.assert_error("this error does not exist");
649    }
650
651    #[test]
652    fn test_send_multiple_instructions() {
653        let mut svm = LiteSVM::new();
654        let payer = svm.create_funded_account(10_000_000_000).unwrap();
655        let recipient1 = Keypair::new();
656        let recipient2 = Keypair::new();
657
658        // Send two transfers in one transaction
659        let ix1 = system_instruction::transfer(&payer.pubkey(), &recipient1.pubkey(), 1_000_000);
660        let ix2 = system_instruction::transfer(&payer.pubkey(), &recipient2.pubkey(), 2_000_000);
661
662        let result = svm.send_instructions(&[ix1, ix2], &[&payer]).unwrap();
663        result.assert_success();
664
665        // Verify both transfers succeeded
666        let balance1 = svm.get_balance(&recipient1.pubkey()).unwrap();
667        let balance2 = svm.get_balance(&recipient2.pubkey()).unwrap();
668        assert_eq!(balance1, 1_000_000);
669        assert_eq!(balance2, 2_000_000);
670    }
671
672    #[test]
673    fn test_send_instruction_no_signers() {
674        let mut svm = LiteSVM::new();
675        let payer = Keypair::new();
676        let recipient = Keypair::new();
677
678        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
679
680        // Should error when no signers provided
681        let result = svm.send_instruction(ix, &[]);
682        assert!(result.is_err());
683        match result {
684            Err(TransactionError::BuildError(msg)) => {
685                assert!(msg.contains("No signers"));
686            }
687            _ => panic!("Expected BuildError"),
688        }
689    }
690
691    #[test]
692    fn test_send_instructions_no_signers() {
693        let mut svm = LiteSVM::new();
694        let payer = Keypair::new();
695        let recipient = Keypair::new();
696
697        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
698
699        // Should error when no signers provided
700        let result = svm.send_instructions(&[ix], &[]);
701        assert!(result.is_err());
702    }
703
704    #[test]
705    fn test_transaction_result_debug() {
706        let mut svm = LiteSVM::new();
707        let payer = svm.create_funded_account(10_000_000_000).unwrap();
708        let recipient = Keypair::new();
709
710        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
711        let result = svm.send_instruction(ix, &[&payer]).unwrap();
712
713        // Should be able to format as debug
714        let debug_str = format!("{:?}", result);
715        assert!(debug_str.contains("TransactionResult"));
716    }
717
718    #[test]
719    fn test_transaction_result_print_logs() {
720        let mut svm = LiteSVM::new();
721        let payer = svm.create_funded_account(10_000_000_000).unwrap();
722        let recipient = Keypair::new();
723
724        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
725        let result = svm.send_instruction(ix, &[&payer]).unwrap();
726
727        // Should not panic when printing logs
728        result.print_logs();
729    }
730
731    #[test]
732    fn test_send_transaction_result() {
733        let mut svm = LiteSVM::new();
734        let payer = svm.create_funded_account(10_000_000_000).unwrap();
735        let recipient = Keypair::new();
736
737        let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);
738        let tx = Transaction::new_signed_with_payer(
739            &[ix],
740            Some(&payer.pubkey()),
741            &[&payer],
742            svm.latest_blockhash(),
743        );
744
745        let result = svm.send_transaction_result(tx).unwrap();
746        result.assert_success();
747    }
748}