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 a banks_client to talk to the BanksServer, a payer keypair that has been funded with a bunch of SOL, and a last_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 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).

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.