testsvm_core/
lib.rs

1//! # TestSVM Core
2//!
3//! Core implementation of the TestSVM testing framework for Solana programs.
4//!
5//! This crate provides the fundamental `TestSVM` struct that wraps LiteSVM
6//! with additional functionality for transaction management, account creation,
7//! and enhanced debugging capabilities.
8
9use std::{
10    env,
11    path::{Path, PathBuf},
12};
13
14use anyhow::*;
15use litesvm::LiteSVM;
16use solana_sdk::{
17    clock::Clock,
18    pubkey::Pubkey,
19    signature::{Keypair, Signer},
20    transaction::Transaction,
21};
22
23pub use solana_address_book::AddressBook;
24
25mod tx_result;
26pub use tx_result::{TXError, TXResult};
27
28mod account_ref;
29pub use account_ref::AccountRef;
30
31mod litesvm_helpers;
32use litesvm_helpers::new_funded_account;
33
34pub mod prelude;
35
36/// Test SVM wrapper for LiteSVM with payer management and Anchor helpers
37pub struct TestSVM {
38    /// Underlying LiteSVM instance
39    pub svm: LiteSVM,
40    /// Default fee payer for transactions.
41    pub default_fee_payer: Keypair,
42    /// Address book for labeling addresses
43    pub address_book: AddressBook,
44}
45
46impl TestSVM {
47    /// Create a new test SVM with a payer and address book
48    pub fn init() -> Result<Self> {
49        let mut svm = LiteSVM::new();
50        let default_fee_payer = new_funded_account(&mut svm, 1000 * 1_000_000_000)?;
51        let mut address_book = AddressBook::new();
52        address_book.add_default_accounts()?;
53
54        // Add the default fee payer to the address book
55        address_book.add_wallet(default_fee_payer.pubkey(), "default_fee_payer".to_string())?;
56
57        Ok(Self {
58            svm,
59            default_fee_payer,
60            address_book,
61        })
62    }
63
64    /// Execute a transaction with the test SVM's payer
65    pub fn execute_transaction(&mut self, transaction: Transaction) -> TXResult {
66        match self.svm.send_transaction(transaction.clone()) {
67            Result::Ok(tx_result) => Result::Ok(tx_result),
68            Err(e) => Err(Box::new(TXError {
69                transaction,
70                metadata: e.clone(),
71                address_book: self.address_book.clone(),
72            })),
73        }
74    }
75
76    /// Execute instructions with the test SVM's payer
77    pub fn execute_ixs(
78        &mut self,
79        instructions: &[solana_sdk::instruction::Instruction],
80    ) -> TXResult {
81        self.execute_ixs_with_signers(instructions, &[])
82    }
83
84    /// Execute instructions with additional signers
85    pub fn execute_ixs_with_signers(
86        &mut self,
87        instructions: &[solana_sdk::instruction::Instruction],
88        signers: &[&Keypair],
89    ) -> TXResult {
90        let mut all_signers = vec![&self.default_fee_payer];
91        all_signers.extend_from_slice(signers);
92
93        let transaction = Transaction::new_signed_with_payer(
94            instructions,
95            Some(&self.default_fee_payer.pubkey()),
96            &all_signers,
97            self.svm.latest_blockhash(),
98        );
99
100        self.execute_transaction(transaction)
101    }
102
103    /// Create a new funded wallet and add to address book
104    pub fn new_wallet(&mut self, name: &str) -> Result<Keypair> {
105        let keypair = new_funded_account(&mut self.svm, 10 * 1_000_000_000)?; // 10 SOL
106        let label = format!("wallet:{name}");
107        self.address_book.add_wallet(keypair.pubkey(), label)?;
108        Ok(keypair)
109    }
110
111    /// Get the default fee payer's public key
112    pub fn default_fee_payer(&self) -> Pubkey {
113        self.default_fee_payer.pubkey()
114    }
115
116    /// Add a program to the address book
117    pub fn add_program_from_path(
118        &mut self,
119        label: &str,
120        pubkey: Pubkey,
121        path: impl AsRef<Path>,
122    ) -> Result<()> {
123        self.svm.add_program_from_file(pubkey, path)?;
124        self.address_book.add_program(pubkey, label)
125    }
126
127    /// Add a program fixture from the fixtures directory.
128    ///
129    /// This method loads a program binary from the fixtures directory. The fixture file
130    /// should be located at `fixtures/programs/{fixture_name}.so` relative to your project root.
131    pub fn add_program_fixture(&mut self, fixture_name: &str, pubkey: Pubkey) -> Result<()> {
132        let path = env::var("CARGO_MANIFEST_DIR")
133            .map(PathBuf::from)
134            .map_err(|e| anyhow!("Failed to get environment variable `CARGO_MANIFEST_DIR`: {e}"))?
135            .ancestors()
136            .find_map(|ancestor| {
137                let fixtures_dir = ancestor.join("fixtures");
138                fixtures_dir.exists().then_some(fixtures_dir)
139            })
140            .ok_or_else(|| anyhow!("`fixtures` directory not found"))
141            .map(|fixtures_dir| {
142                fixtures_dir
143                    .join("programs")
144                    .join(fixture_name)
145                    .with_extension("so")
146            })?;
147
148        self.add_program_from_path(fixture_name, pubkey, &path)?;
149        Ok(())
150    }
151
152    /// Finds a program derived address and return an [AccountRef] with proper type information.
153    pub fn get_pda<T: anchor_lang::AccountDeserialize>(
154        &mut self,
155        label: &str,
156        seeds: &[&[u8]],
157        program_id: Pubkey,
158    ) -> Result<AccountRef<T>> {
159        let (pda, _) = self.get_pda_with_bump(label, seeds, program_id)?;
160        Ok(pda)
161    }
162
163    /// Finds a program derived address and return an [AccountRef] with proper type information and bump seed.
164    pub fn get_pda_with_bump<T: anchor_lang::AccountDeserialize>(
165        &mut self,
166        label: &str,
167        seeds: &[&[u8]],
168        program_id: Pubkey,
169    ) -> Result<(AccountRef<T>, u8)> {
170        let (pubkey, bump) = self
171            .address_book
172            .find_pda_with_bump(label, seeds, program_id)?;
173        Ok((AccountRef::new(pubkey), bump))
174    }
175
176    /// Advance the time by the specified number of seconds
177    /// Assumes 450ms per slot, in practice this is not always the case.
178    pub fn advance_time(&mut self, seconds: u64) {
179        let mut clock = self.svm.get_sysvar::<Clock>();
180        clock.unix_timestamp += seconds as i64;
181        // assume 450ms per slot.
182        let num_slots = seconds / 450;
183        clock.slot += num_slots;
184        self.svm.set_sysvar(&clock);
185    }
186
187    /// Advance slots using LiteSVM's warp_to_slot feature
188    /// This is useful for simulating time passing in tests
189    pub fn advance_slots(&mut self, num_slots: u32) {
190        let current_slot = self.svm.get_sysvar::<solana_sdk::clock::Clock>().slot;
191        let target_slot = current_slot + num_slots as u64;
192
193        self.svm.warp_to_slot(target_slot);
194    }
195}