LiteSVM with TypeScript
The litesvm
package provides the core testing infrastructure for creating a lightweight Solana environment where you can directly manipulate account state and execute transactions against your programs.
First Steps
Add LiteSVM to your project:
npm i --save-dev litesvm
LiteSVM Basics
Start by declaring your program ID and creating a LiteSVM instance.
Use the exact same program ID that you defined in your program to ensure transactions execute correctly and don't throw ProgramMismatch
errors during testing:
import { LiteSVM } from "litesvm";
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("22222222222222222222222222222222222222222222");
describe("test", () => {
// Create a new instance of LiteSVM
const svm = new LiteSVM();
// Load the program with the right public key
svm.addProgramFromFile(programId, "target/deploy/program.so");
})
To execute tests, create a transaction object and use the .sendTransaction(tx)
function:
import { LiteSVM } from "litesvm";
import { Transaction } from "@solana/web3.js";
describe("test", () => {
// Create a new instance of LiteSVM
const svm = new LiteSVM();
// Create a new Transaction
const tx = new Transaction();
// Add the latest blockhash
tx.recentBlockhash = svm.latestBlockhash();
// Add the instructions and the signers
// tx.add(...ixs);
// tx.sign(...signersKeypair);
// Send the transaction
svm.sendTransaction(tx);
})
Accounts
When testing Solana programs with LiteSVM, you'll work with several types of accounts that mirror real-world program execution scenarios.
Understanding how to construct these accounts properly is essential for effective testing.
System Accounts
The most fundamental account type is the system account, which comes in two primary variants:
- Payer accounts: Accounts with lamports that fund program account creation or lamport transfers
- Uninitialized accounts: Empty accounts with no lamports, typically used to represent program accounts awaiting initialization
System accounts contain no data and are owned by the System Program. The key difference between payers and uninitialized accounts is their lamport balance: payers have funds, while uninitialized accounts start empty.
Here's how to create a payer
account in LiteSVM:
import { LiteSVM } from "litesvm";
import { Keypair, SystemProgram } from "@solana/web3.js";
describe("test", () => {
// Create a new instance of LiteSVM
const svm = new LiteSVM();
// Create a new Account
const account = Keypair.generate();
// Add the Account with the modified data
svm.setAccount(account.publicKey, {
lamports: 100_000_000,
data: Buffer.alloc(0),
owner: SystemProgram.programId,
executable: false,
});
})
Program Accounts
For program accounts that contain custom data structures, you can use a similar approach.
You will also need to serialize the account data into a buffer, which can be done either manually or using a library such as @coral-xyz/borsh
(see example here).
import { LiteSVM } from "litesvm";
import { Keypair } from "@solana/web3.js";
describe("test", () => {
// Create a new instance of LiteSVM
const svm = new LiteSVM();
// Create a new Account
const account = Keypair.generate();
// Populate the data of the Account
const accountData = Buffer.alloc(SIZE_OF_THE_ACCOUNT);
// Serialize the account data into the byte buffer defined above
// ...
// Grab the minimum amount of lamports to make it rent exempt
const lamports = svm.minimumBalanceForRentExemption(SIZE_OF_THE_ACCOUNT);
// Add the Account with the modified data
svm.setAccount(account.publicKey, {
lamports,
data: accountData,
owner: PROGRAM_ID,
executable: false,
});
})
Token Accounts
To serialize data for SPL Token accounts, you can use AccountLayout
and MintLayout
from @solana/spl-token
.
import { LiteSVM } from "litesvm";
import { Keypair } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID, AccountLayout, MintLayout, ACCOUNT_SIZE, MINT_SIZE } from "@solana/spl-token"
describe("test", () => {
// Create a new instance of LiteSVM
const svm = new LiteSVM();
const owner = Keypair.generate();
// Create a new Mint Account
const mint = Keypair.generate();
// Populate the data of the Mint Account
let mintData = Buffer.alloc(MINT_SIZE);
MintLayout.encode(
{
mintAuthorityOption: 1,
mintAuthority: owner.publicKey,
supply: BigInt(0),
decimals: 0,
isInitialized: true,
freezeAuthorityOption: 0,
freezeAuthority: PublicKey.default,
},
mintData
)
// Grab the minimum amount of lamports to make it rent exempt
const lamports = svm.minimumBalanceForRentExemption(MINT_SIZE);
// Add the Account with the modified data
svm.setAccount(mint.publicKey, {
lamports,
data: mintData,
owner: TOKEN_PROGRAM_ID,
executable: false,
});
// Create a new Token Account
const tokenAccount = Keypair.generate();
// Populate the data of the Token Account
const tokenAccountData = Buffer.alloc(ACCOUNT_SIZE);
AccountLayout.encode(
{
mint: mint.publicKey,
owner: owner.publicKey,
amount: BigInt(100),
delegateOption: 0,
delegate: PublicKey.default,
delegatedAmount: BigInt(0),
state: 1,
isNativeOption: 0,
isNative: BigInt(0),
closeAuthorityOption: 0,
closeAuthority: PublicKey.default,
},
tokenAccountData,
);
// Grab the minimum amount of lamports to make it rent exempt
const lamports = svm.minimumBalanceForRentExemption(ACCOUNT_SIZE);
// Add the Account with the modified data
svm.setAccount(tokenAccount.publicKey, {
lamports,
data: tokenAccountData,
owner: TOKEN_PROGRAM_ID,
executable: false,
});
})
Execution
With accounts created and added to your LiteSVM instance, you can now send transactions and validate your program logic.
Before sending a transaction, you can simulate the result:
// Simulate before executing
const simulatedResult = svm.simulateTransaction(tx);
Then send the transaction and inspect its logs:
// Execute and inspect logs
const result = svm.sendTransaction(tx);
console.log(result.logs);
Advanced Features
Before and after execution, the entire ledger contained in your LiteSVM instance is readable and customizable.
You can manipulate sysvar values like the clock:
// Change the Clock
const newClock = svm.getClock();
newClock.unixTimestamp = 50n;
svm.setClock(newClock);
// Jump to a certain Slot
svm.warpToSlot(500);
// Expire the current blockhash
svm.expireBlockhash();
You can also read account and protocol data:
// Get all the information about an account (data, lamports, owner, ...)
svm.getAccount(account.publicKey);
// Get the lamport balance of an account
svm.getBalance(account.publicKey);
Or configure how the runtime behaves:
// Sets the compute budget
const computeBudget = new ComputeBudget();
computeBudget.computeUnitLimit = 2_000_000n;
svm.withComputeBudget(computeBudget);
// Sets Sigverify as active
svm.withSigverify(true);
// Sets the Blockhash check as active
svm.withBlockhashCheck(true);
// Sets the default Sysvars
svm.withSysvars();
// Set the FeatureSet to use
svm.withFeatureSet(new FeatureSet(...))