# 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. solana-program-test is a bit fussy about how this is done.

Firstly, the program's .so file must be present in one of the following directories:

  • ./tests/fixtures (just create this directory if it doesn't exist)
  • The current working directory
  • A directory you define in the BPF_OUT_DIR or SBF_OUT_DIR environment variables.

(If you're not aware, the .so file is created when you run anchor build or cargo build-sbf and can be found in target/deploy).

TIP

If you want to pull a Solana program from mainnet or devnet, use the solana program dump command from the Solana CLI.

Now to add the program to our tests we use the programs parameter in the start function. The program name used in this parameter must match the filename without the .so extension.

Here's an example using a simple program (opens new window) from the Solana Program Library that just does some logging:

import { start } from "solana-bankrun";
import {
	PublicKey,
	Transaction,
	TransactionInstruction,
} from "@solana/web3.js";

test("spl logging", async () => {
	const programId = PublicKey.unique();
	const context = await start([{ name: "spl_example_logging", programId }], []);
	const client = context.banksClient;
	const payer = context.payer;
	const blockhash = context.lastBlockhash;
	const ixs = [
		new TransactionInstruction({
			programId,
			keys: [
				{ pubkey: PublicKey.unique(), isSigner: false, isWritable: false },
			],
		}),
	];
	const tx = new Transaction();
	tx.recentBlockhash = blockhash;
	tx.add(...ixs);
	tx.sign(payer);
	// let's sim it first
	const simRes = await client.simulateTransaction(tx);
	const meta = await client.processTransaction(tx);
	expect(simRes.meta?.logMessages).toEqual(meta?.logMessages);
	expect(meta.logMessages[1]).toBe("Program log: static string");
});

The .so file must be named spl_example_logging.so, since spl_example_logging is the name we used in the programs parameter.

# Anchor integration

# Basic

If you have an Anchor workspace, bankrun can make some extra assumptions that make it more convenient to get started. Just use start_anchor and give it the path to the project root (the folder containing the Anchor.toml file). The programs in the workspace will be automatically deployed to the test environment.

Example:

import { startAnchor } from "solana-bankrun";
import { PublicKey } from "@solana/web3.js";

test("anchor", async () => {
	const context = await startAnchor("tests/anchor-example", [], []);
	const programId = new PublicKey(
		"Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
	);
	const executableAccount = await context.banksClient.getAccount(programId);
	expect(executableAccount).not.toBeNull();
	expect(executableAccount?.executable).toBe(true);
});

# anchor-bankrun

If you want deeper Anchor integration, you can install the anchor-bankrun (opens new window) package. This allows you to write typical Anchor tests with minimal changes using the BankrunProvider class. Here's an example that tests a program from the Anchor repository:

import { startAnchor } from "solana-bankrun";
import { BankrunProvider } from "anchor-bankrun";
import { Keypair, PublicKey } from "@solana/web3.js";
import { BN, Program } from "@coral-xyz/anchor";
import { IDL as PuppetIDL, Puppet } from "./anchor-example/puppet";

const PUPPET_PROGRAM_ID = new PublicKey(
	"Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
);

test("anchor", async () => {
	const context = await startAnchor("tests/anchor-example", [], []);

	const provider = new BankrunProvider(context);

	const puppetProgram = new Program<Puppet>(
		PuppetIDL,
		PUPPET_PROGRAM_ID,
		provider,
	);

	const puppetKeypair = Keypair.generate();
	await puppetProgram.methods
		.initialize()
		.accounts({
			puppet: puppetKeypair.publicKey,
		})
		.signers([puppetKeypair])
		.rpc();

	const data = new BN(123456);
	await puppetProgram.methods
		.setData(data)
		.accounts({
			puppet: puppetKeypair.publicKey,
		})
		.rpc();

	const dataAccount = await puppetProgram.account.data.fetch(
		puppetKeypair.publicKey,
	);
	expect(dataAccount.data.eq(new BN(123456)));
});

# Time travel

Many programs rely on the Clock sysvar: for example, a mint that doesn't become available until after a certain time. With bankrun you can dynamically overwrite the Clock sysvar using context.set_clock(). Here's an example using a program that panics if clock.unix_timestamp is greater than 100 (which is on January 1st 1970):

import { Clock, start } from "solana-bankrun";
import {
	PublicKey,
	Transaction,
	TransactionInstruction,
} from "@solana/web3.js";

test("clock", async () => {
	const programId = PublicKey.unique();
	const context = await start(
		[{ name: "bankrun_clock_example", programId }],
		[],
	);
	const client = context.banksClient;
	const payer = context.payer;
	const blockhash = context.lastBlockhash;
	const ixs = [
		new TransactionInstruction({ keys: [], programId, data: Buffer.from("") }),
	];
	const tx = new Transaction();
	tx.recentBlockhash = blockhash;
	tx.add(...ixs);
	tx.sign(payer);
	// this will fail because it's not January 1970 anymore
	await expect(client.processTransaction(tx)).rejects.toThrow(
		"Program failed to complete",
	);
	// so let's turn back time
	const currentClock = await client.getClock();
	context.setClock(
		new Clock(
			currentClock.slot,
			currentClock.epochStartTimestamp,
			currentClock.epoch,
			currentClock.leaderScheduleEpoch,
			50n,
		),
	);
	const ixs2 = [
		new TransactionInstruction({
			keys: [],
			programId,
			data: Buffer.from("foobar"), // unused, just here to dedup the tx
		}),
	];
	const tx2 = new Transaction();
	tx2.recentBlockhash = blockhash;
	tx2.add(...ixs2);
	tx2.sign(payer);
	// now the transaction goes through
	await client.processTransaction(tx2);
});

See also: context.warp_to_slot(), which lets you jump to a future slot.

# Writing arbitrary accounts

Bankrun 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 { start } from "solana-bankrun";
import { PublicKey } from "@solana/web3.js";
import {
	getAssociatedTokenAddressSync,
	AccountLayout,
	ACCOUNT_SIZE,
	TOKEN_PROGRAM_ID,
} from "@solana/spl-token";

test("infinite usdc mint", async () => {
	const owner = PublicKey.unique();
	const usdcMint = new PublicKey(
		"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
	);
	const ata = getAssociatedTokenAddressSync(usdcMint, owner, true);
	const usdcToOwn = 1_000_000_000_000n;
	const tokenAccData = Buffer.alloc(ACCOUNT_SIZE);
	AccountLayout.encode(
		{
			mint: usdcMint,
			owner,
			amount: usdcToOwn,
			delegateOption: 0,
			delegate: PublicKey.default,
			delegatedAmount: 0n,
			state: 1,
			isNativeOption: 0,
			isNative: 0n,
			closeAuthorityOption: 0,
			closeAuthority: PublicKey.default,
		},
		tokenAccData,
	);
	const context = await start(
		[],
		[
			{
				address: ata,
				info: {
					lamports: 1_000_000_000,
					data: tokenAccData,
					owner: TOKEN_PROGRAM_ID,
					executable: false,
				},
			},
		],
	);
	const client = context.banksClient;
	const rawAccount = await client.getAccount(ata);
	expect(rawAccount).not.toBeNull();
	const rawAccountData = rawAccount?.data;
	const decoded = AccountLayout.decode(rawAccountData);
	expect(decoded.amount).toBe(usdcToOwn);
});

TIP

If you want to set account data after calling start(), you can use context.set_account().

# 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 bankrun:

import { start } from "solana-bankrun";
import { PublicKey, Connection } from "@solana/web3.js";

test("copy accounts from devnet", async () => {
	const owner = PublicKey.unique();
	const usdcMint = new PublicKey(
		"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
	);
	const connection = new Connection("https://api.devnet.solana.com");
	const accountInfo = await connection.getAccountInfo(usdcMint);

	const context = await start(
		[],
		[
			{
				address: usdcMint,
				info: accountInfo,
			},
		],
	);

	const client = context.banksClient;
	const rawAccount = await client.getAccount(usdcMint);
	expect(rawAccount).not.toBeNull();
});

# Other features

Other things you can do with bankrun include:

  • Changing the max compute units with the compute_max_units parameter.
  • Changing the transaction account lock limit with the transaction_account_lock_limit parameter.

# When should I use solana-test-validator?

While bankrun 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 BanksServer 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 bankrun wherever possible, as it will make your life much easier.

# Supported platforms

bankrun is supported on Linux x64 and MacOS targets, because this is what solana-program-test runs on. If you find a platform that is not supported but which can run solana-program-test, please open an issue.