- The Surfpool Kit plugin,
@solana/surfpool/kit, starts a local Surfnet inside your test process and connects a Solana Kit client to it, with a funded payer and typed cheatcodes. - Pass your Chainstack Solana Mainnet endpoint as
remoteRpcUrl, and the Surfnet forks Mainnet: each account is copied from your endpoint the first time something reads it, and every transaction runs locally. WithoutremoteRpcUrl, the Surfnet is offline and Mainnet accounts don’t exist in it. - The test suite in this guide reads the real USDC mint, gives a wallet 1,000 USDC with a cheatcode, sends a transfer, profiles compute units without sending, and moves the clock forward 30 days. A run takes about 2 seconds and sends 12 read requests to your endpoint.
- Set
slotTimeMs: 400if your code depends on slots or epochs. With the default 1 ms slot, moving the clock forward 30 days advances 6,000 epochs instead of 15.
Main article
Surfpool is a local Solana network that forks Mainnet on demand. The Solana: Surfpool quick-start covers thesurfpool CLI. This guide covers the Kit plugin, which runs the same Surfnet inside a Node.js process, so a TypeScript test suite can start a fresh fork for each test file and drive it through a Kit client.
The fork is lazy. Your tests use real Mainnet accounts, such as token mints, programs, and pools, without a clone list or a snapshot, and nothing they do reaches Mainnet.
To follow along, you need a Solana Mainnet node endpoint. See Solana tooling to get set up.
Requirements
- Node.js 20.18 or later, which
@solana/kit8 requires. - macOS (Apple silicon or Intel) or Linux x64 with glibc.
@solana/surfpoolships its native Surfnet build only for these platforms. On other platforms, run thesurfpoolCLI and connect to it with the plugin’s attach mode, described in the Surfpool Kit plugin documentation.
Set up the project
@solana/kit 8.4.0, @solana/surfpool 1.6.1, @solana/kit-plugin-rpc 0.18.0, @solana/kit-plugin-signer 0.18.0, @solana/sysvars 8.4.0, and @solana-program/token 0.17.0.
Two of these packages need care:
@solana/surfpool1.6.1 requires the 0.18 line of@solana/kit-plugin-rpcand@solana/kit-plugin-signer. Installing all the packages in one command lets npm pick 0.18. Requesting 0.19 explicitly fails withERESOLVE.@solana/sysvarsprovidesfetchSysvarClock. Kit 8 doesn’t re-export it from@solana/kit.
How the fork reaches your endpoint
The Surfnet calls your Chainstack endpoint only to read state:- At startup, it reads the genesis hash, epoch info, the epoch schedule, and one sysvar account. The local clock starts at your endpoint’s current slot and epoch.
- The first time your code or a transaction reads an account, the Surfnet fetches it with
getAccountInfoorgetMultipleAccounts. Later reads use the local copy, so the copy doesn’t follow changes on Mainnet. - Transactions run only in the local Surfnet. The Surfnet never sends a transaction to your endpoint.
client.payer.
Write the test suite
The suite uses the Node.js built-in test runner, so it needs no test framework. It reads your endpoint from theCHAINSTACK_ENDPOINT environment variable.
fork.test.mjs
Start and stop the fork
Thebefore hook builds the client. surfpool() starts the Surfnet and installs client.rpc, client.payer, client.cheatcodes, and client.surfnet. The surfnet options go to the native Surfnet: remoteRpcUrl turns on the fork, and slotTimeMs: 400 matches Mainnet’s slot time. tokenProgram() from @solana-program/token adds the Token program’s instructions and accounts as client.token.
The after hook calls client.surfnet.stop(), which closes the Surfnet’s local HTTP and WebSocket servers and frees their ports. Node.js runs each test file in its own process, so each file gets its own Surfnet.
Read Mainnet state
The first test reads the USDC mint twice: through the fork and directly from your endpoint. The forked copy has the same decimals and mint authority as the account on Mainnet.Fund a wallet and send a transfer
ThesetTokenAccount cheatcode writes the payer’s USDC token account, creating it if needed, with a balance of 1,000 USDC. No faucet or swap is involved, and the USDC mint authority doesn’t sign anything.
client.token.instructions.transferToATA creates the recipient’s associated token account and transfers 250 USDC. sendTransaction() plans the transaction, adds a SetComputeUnitLimit instruction with an estimated limit, signs it with the payer, and sends it to the Surfnet. The test then checks both balances, and checks that the recipient’s token account doesn’t exist on Mainnet.
Profile compute units
TheprofileTransaction cheatcode runs a signed transaction against the fork and returns its compute units, logs, and account states, without changing any account. The test confirms that the recipient’s token account still doesn’t exist after profiling.
The test builds this transaction by hand rather than with planTransaction(). A planned transaction carries a SetComputeUnitLimit instruction with a limit of 0, which sendTransaction() replaces with an estimate when it sends. Profiling a planned transaction as it is fails with Computational budget exceeded.
Move the clock
ThetimeTravel cheatcode takes absoluteTimestamp in milliseconds and moves the Clock sysvar to that time. Use it to test vesting schedules, lockups, and other time-based logic.
The Surfnet converts the time difference into slots using slotTimeMs. With slotTimeMs: 400, moving 30 days forward advances about 6.48 million slots and 15 epochs, as on Mainnet. With the default of 1 ms, the same move advances about 2.59 billion slots and 6,000 epochs.
Run the tests
Why a Mainnet account is not found
surfpool() without remoteRpcUrl starts an offline Surfnet. It still has a funded payer, but no Mainnet accounts, so reading the USDC mint fails:
Account not found, check that remoteRpcUrl is set and that CHAINSTACK_ENDPOINT holds your endpoint.
See also
- Solana: Surfpool quick-start — the
surfpoolCLI, Anchor integration, and the full cheatcode list. - Solana: Fast unit testing with LiteSVM — unit tests without RPC or Mainnet state.
- Surfpool Kit plugin documentation — attach mode, the standalone cheatcodes client, and every plugin option.
- solana-foundation/surfpool — Surfpool source; the Node.js package and Kit plugin live in
crates/sdk-node.