Bankrun
The bankrun
module offers a quick and powerful yet lightweight solution for testing Solana programs.
While people often use solana-test-validator
for this,
bankrun
is orders of magnitude faster and far more convenient.
You don’t have to
take care of an external process and you can start as many bankrun
instances as you like without worrying about ports in use or hogging your machines resources.
You can also do things that are not possible with solana-test-validator
,
such as jumping back and forth in time or dynamically setting account data.
If you’ve used solana-program-test
you’ll be familiar with bankrun
, since that’s what it uses under the hood.
For those unfamiliar, bankrun
and solana-program-test
work by spinning up a lightweight
BanksServer
that’s like an RPC but much faster, and creating a BanksClient
to talk to the
server. This author thought solana-program-test
was a boring name, so he chose bankrun
instead
(you’re running Solana Banks).
Minimal example
This example just transfers lamports from Alice to Bob without loading
any programs of our own. Note: you’ll need pytest-asyncio
installed in
the environment, since the test function is async.
from pytest import mark
from solders.bankrun import start
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import transfer
from solders.transaction import VersionedTransaction
@mark.asyncio
async def test_transfer() -> None:
context = await start()
receiver = Pubkey.new_unique()
client = context.banks_client
payer = context.payer
blockhash = context.last_blockhash
transfer_lamports = 1_000_000
ixs = [
transfer(
{
"from_pubkey": context.payer.pubkey(),
"to_pubkey": receiver,
"lamports": transfer_lamports,
}
)
]
msg = Message.new_with_blockhash(ixs, payer.pubkey(), blockhash)
tx = VersionedTransaction(msg, [payer])
await client.process_transaction(tx)
balance_after = await client.get_balance(receiver)
assert balance_after == transfer_lamports
Some things to note here:
The
context
object contains abanks_client
to talk to theBanksServer
, apayer
keypair that has been funded with a bunch of SOL, and alast_blockhash
that we can use in our transactions.We haven’t loaded any specific programs, but by default we have access to the System Program, the SPL token programs and the SPL memo program.
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
orSBF_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
).
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 from the Solana monorepo that just does some logging:
from pytest import mark
from solders.bankrun import start
from solders.instruction import AccountMeta, Instruction
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
@mark.asyncio
async def test_logging() -> None:
program_id = Pubkey.from_string("Logging111111111111111111111111111111111111")
ix = Instruction(
program_id,
bytes([5, 10, 11, 12, 13, 14]),
[AccountMeta(Pubkey.new_unique(), is_signer=False, is_writable=True)],
)
context = await start(programs=[("spl_example_logging", program_id)])
payer = context.payer
blockhash = context.last_blockhash
client = context.banks_client
msg = Message.new_with_blockhash([ix], payer.pubkey(), blockhash)
tx = VersionedTransaction(msg, [payer])
# let's sim it first
sim_res = await client.simulate_transaction(tx)
meta = await client.process_transaction(tx)
assert sim_res.meta == meta
assert meta is not None
assert meta.log_messages[1] == "Program log: static string"
assert (
meta.compute_units_consumed < 10_000
) # not being precise here in case it changes
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
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:
from pathlib import Path
from pytest import mark
from solders.bankrun import start_anchor
from solders.pubkey import Pubkey
@mark.asyncio
async def test_anchor() -> None:
ctx = await start_anchor(Path("tests/bankrun/anchor-example"))
program_id = Pubkey.from_string("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS")
executable_account = await ctx.banks_client.get_account(program_id)
assert executable_account is not None
assert executable_account.executable
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):
from pytest import mark, raises
from solders.bankrun import start
from solders.clock import Clock
from solders.instruction import Instruction
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import TransactionError, VersionedTransaction
@mark.asyncio
async def test_set_clock() -> None:
program_id = Pubkey.new_unique()
context = await start(programs=[("solders_clock_example", program_id)])
client = context.banks_client
payer = context.payer
blockhash = context.last_blockhash
ixs = [Instruction(program_id=program_id, data=b"", accounts=[])]
msg = Message.new_with_blockhash(ixs, payer.pubkey(), blockhash)
tx = VersionedTransaction(msg, [payer])
# this will fail because it's not January 1970 anymore
with raises(TransactionError):
await client.process_transaction(tx)
# so let's turn back time
current_clock = await client.get_clock()
context.set_clock(
Clock(
slot=current_clock.slot,
epoch_start_timestamp=current_clock.epoch_start_timestamp,
epoch=current_clock.epoch,
leader_schedule_epoch=current_clock.leader_schedule_epoch,
unix_timestamp=50,
)
)
ixs2 = [
Instruction(
program_id=program_id,
data=b"foobar", # unused, this is just to dedup the transaction
accounts=[],
)
]
msg2 = Message.new_with_blockhash(ixs2, payer.pubkey(), blockhash)
tx2 = VersionedTransaction(msg2, [payer])
# now the transaction goes through
await client.process_transaction(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:
from pytest import mark
from solders.account import Account
from solders.bankrun import start
from solders.pubkey import Pubkey
from solders.token import ID as TOKEN_PROGRAM_ID
from solders.token.associated import get_associated_token_address
from solders.token.state import TokenAccount, TokenAccountState
@mark.asyncio
async def test_infinite_usdc_mint() -> None:
owner = Pubkey.new_unique()
usdc_mint = Pubkey.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
ata = get_associated_token_address(owner, usdc_mint)
usdc_to_own = 1_000_000_000_000
token_acc = TokenAccount(
mint=usdc_mint,
owner=owner,
amount=usdc_to_own,
delegate=None,
state=TokenAccountState.Initialized,
is_native=None,
delegated_amount=0,
close_authority=None,
)
context = await start(
accounts=[
(
ata,
Account(
lamports=1_000_000_000,
data=bytes(token_acc),
owner=TOKEN_PROGRAM_ID,
executable=False,
),
)
]
)
client = context.banks_client
raw_account = await client.get_account(ata)
assert raw_account is not None
raw_account_data = raw_account.data
assert TokenAccount.from_bytes(raw_account_data).amount == usdc_to_own
Tip
If you want to set account data after calling bankrun.start()
,
you can use context.set_account()
.
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 not included on windows
and musllinux-i686
targets, but otherwise
should run everywhere that solders
runs.