Tutorial
Deploying programs
Most of the time we want to do more than just mess around with token transfers - we want to test our own programs.
TIP
If you want to pull a Solana program from mainnet or devnet, use the solana program dump command from the Solana CLI.
To add a compiled program to our tests we can use the addProgramFromFile method.
Here's an example using a simple program from the Solana Program Library that just does some logging:
import { AccountRole, generateKeyPairSigner, lamports } from "@solana/kit";
import { LiteSVM, TransactionMetadata } from "litesvm";
import assert from "node:assert/strict";
import { test } from "node:test";
import {
generateAddress,
getSignedTransaction,
LAMPORTS_PER_SOL,
} from "./util";
test("spl logging", async () => {
// Given the following addresses and signers.
const [payer, programAddress, loggedAddress] = await Promise.all([
generateKeyPairSigner(),
generateAddress(),
generateAddress(),
]);
// And a LiteSVM client with a logging program loaded from `spl_example_logging.so`.
const svm = new LiteSVM();
svm.airdrop(payer.address, lamports(LAMPORTS_PER_SOL));
svm.addProgramFromFile(
programAddress,
"program_bytes/spl_example_logging.so",
);
// When we simulate and send a transaction that calls the program.
const transaction = await getSignedTransaction(svm, payer, [
{
accounts: [{ address: loggedAddress, role: AccountRole.READONLY }],
programAddress,
},
]);
const simulationResult = svm.simulateTransaction(transaction);
const result = svm.sendTransaction(transaction);
// Then we expect the logs from simulation and execution to match.
if (result instanceof TransactionMetadata) {
assert.deepStrictEqual(simulationResult.meta().logs(), result.logs());
assert.strictEqual(result.logs()[1], "Program log: static string");
} else {
throw new Error("Unexpected tx failure");
}
});Time travel
Many programs rely on the Clock sysvar: for example, a mint that doesn't become available until after a certain time. With litesvm you can dynamically overwrite the Clock sysvar using svm.setClock(). Here's an example using a program that panics if clock.unix_timestamp is greater than 100 (which is on January 1st 1970):
import { generateKeyPairSigner, lamports } from "@solana/kit";
import {
FailedTransactionMetadata,
LiteSVM,
TransactionMetadata,
} from "litesvm";
import assert from "node:assert/strict";
import { test } from "node:test";
import {
generateAddress,
getSignedTransaction,
LAMPORTS_PER_SOL,
} from "./util";
test("clock", async () => {
// Given the following addresses and signers.
const [payer, programAddress] = await Promise.all([
generateKeyPairSigner(),
generateAddress(),
]);
// And a LiteSVM client with a hello world program loaded from `litesvm_clock_example.so`.
const svm = new LiteSVM();
svm.airdrop(payer.address, lamports(LAMPORTS_PER_SOL));
svm.addProgramFromFile(
programAddress,
"program_bytes/litesvm_clock_example.so",
);
// And given two unique transactions.
const [firstTransaction, secondTransaction] = await Promise.all([
getSignedTransaction(svm, payer, [
{ programAddress, data: new Uint8Array([0]) },
]),
getSignedTransaction(svm, payer, [
{ programAddress, data: new Uint8Array([1]) },
]),
]);
// When we set the time to January 1st 2000 and send the first transaction.
const initialClock = svm.getClock();
initialClock.unixTimestamp = 1735689600n;
svm.setClock(initialClock);
const firstResult = svm.sendTransaction(firstTransaction);
// Then it fails because the contract wants it to be January 1970.
if (firstResult instanceof FailedTransactionMetadata) {
assert.ok(firstResult.err().toString().includes("ProgramFailedToComplete"));
} else {
throw new Error("Expected transaction failure here");
}
// When we set the time to January 1st 1970 and send the second transaction.
const newClock = svm.getClock();
newClock.unixTimestamp = 50n;
svm.setClock(newClock);
const secondResult = svm.sendTransaction(secondTransaction);
// Then it succeeds.
assert.ok(secondResult instanceof TransactionMetadata);
});See also: svm.warpToSlot(), which lets you jump to a future slot.
Writing arbitrary accounts
LiteSVM lets you write any account data you want, regardless of whether the account state would even be possible.
Here's an example where we give an account a bunch of USDC, even though we don't have the USDC mint keypair. This is convenient for testing because it means we don't have to work with fake USDC in our tests:
import {
AccountState,
findAssociatedTokenPda,
getTokenDecoder,
getTokenEncoder,
Token,
TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import {
address,
assertAccountExists,
decodeAccount,
generateKeyPairSigner,
lamports,
none,
} from "@solana/kit";
import { LiteSVM } from "litesvm";
import assert from "node:assert/strict";
import { test } from "node:test";
import { generateAddress, LAMPORTS_PER_SOL } from "./util";
test("infinite usdc mint", async () => {
// Given the following addresses and signers.
const [payer, owner] = await Promise.all([
generateKeyPairSigner(),
generateAddress(),
]);
const usdcMint = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
// And a LiteSVM client such that the payer has some balance.
const svm = new LiteSVM();
svm.airdrop(payer.address, lamports(LAMPORTS_PER_SOL));
// Add the following associated token account for the owner.
const [ata] = await findAssociatedTokenPda({
owner,
mint: usdcMint,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
// And the following token account data.
const tokenAccountData: Token = {
mint: usdcMint,
owner,
amount: 1_000_000_000_000n,
delegate: none(),
state: AccountState.Initialized,
isNative: none(),
delegatedAmount: 0n,
closeAuthority: none(),
};
const encodedTokenAccountData = getTokenEncoder().encode(tokenAccountData);
// When we set that associated token account on the LiteSVM.
svm.setAccount({
address: ata,
lamports: lamports(LAMPORTS_PER_SOL),
programAddress: TOKEN_PROGRAM_ADDRESS,
executable: false,
data: encodedTokenAccountData,
space: BigInt(encodedTokenAccountData.length),
});
// Then we can fetch the account and it matches what we set.
const fetchedAccount = decodeAccount(svm.getAccount(ata), getTokenDecoder());
assertAccountExists(fetchedAccount);
assert.deepStrictEqual(fetchedAccount.data, tokenAccountData);
});Copying Accounts from a live environment
If you want to copy accounts from mainnet or devnet, you can use the solana account command in the Solana CLI to save account data to a file.
Or, if you want to pull live data every time you test, you can do this with a few lines of code. Here's a simple example that pulls account data from devnet and passes it to LiteSVM:
import { address, assertAccountExists, createSolanaRpc, fetchEncodedAccount } from "@solana/kit";
import { LiteSVM } from "litesvm";
import assert from "node:assert/strict";
import { test } from "node:test";
test("copy accounts from devnet", async () => {
const usdcMint = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const account = await fetchEncodedAccount(rpc, usdcMint);
assertAccountExists(account);
const svm = new LiteSVM();
svm.setAccount(account);
const rawAccount = svm.getAccount(usdcMint);
assert.notStrictEqual(rawAccount, null);
});Other features
Other things you can do with litesvm include:
- Changing the max compute units and other compute budget behaviour using the
withComputeBudgetmethod. - Disable transaction signature checking using
svm.withSigverify(false). - Find previous transactions using the
getTransactionmethod.
When should I use solana-test-validator?
While litesvm is faster and more convenient, it is also less like a real RPC node. So solana-test-validator is still useful when you need to call RPC methods that LiteSVM doesn't support, or when you want to test something that depends on real-life validator behaviour rather than just testing your program and client code.
In general though I would recommend using litesvm wherever possible, as it will make your life much easier.
Supported platforms
litesvm is supported on Linux x64 and MacOS targets. If you find a platform that is not supported but which can run the litesvm Rust crate, please open an issue.