Skip to main content
@eco-incorp/sauce/testing runs your programs from a test, as cooks on a chain only the test can see. local() starts an empty chain; fork() starts a copy of a real chain at a pinned block, recorded to a file once and replayed from it with no network. Each chain has the V12 release deployed and ten test accounts, each with a Pot, and the kit gives you calls to fund accounts, run or simulate a cook, read balances and check what moved. The kit runs in Node only. It reads no environment variable: the endpoint, the block and the state file are arguments. Its functions are plain async functions, so any test runner works; the examples use node:test.

Install

  • EVM chains run on Foundry’s anvil, which must be on PATH or named by the anvil option. Install Foundry as the Foundry installation guide describes:
  • Solana runs in LiteSVM, inside the test’s process. Install litesvm in your project at the version the package pins. Starting a Solana chain without it throws an error that names the install command:

Test a program that calls nothing

A program that calls no protocol and moves no token needs no copy of a real chain. local() starts an empty one with no network.
double.sauce.ts
double.test.ts
chain.run sent a cook from test account 0’s Pot and returned what main returned. chain.simulate ran the same cook without sending it; the require failed, so it threw CookReverted with the require’s message as reason. Starting a chain takes seconds, so give each test and hook a timeout: node:test has no default limit.

Test against a copy of a chain

A program that reads or moves a token needs a chain where the token exists. This test forks Base at a pinned block and runs a payment program on it:
pay.sauce.ts
pay.test.ts
The first run records. The state file does not exist yet, so the fork reads Base through rpcUrl, here viem’s default Base endpoint, and stop() writes every account and storage slot the tests read to test/state/pay-base.json.gz:
A public endpoint serves the state of recent blocks only, so record soon after you choose the block; an older block needs an archive node. Every later run replays the file and reads no endpoint:
The replayed cook used the same gas as the recorded one. Commit the state file beside the test; CI then runs it with anvil installed and no endpoint or secret.
  • base.fund(pot, { USDC: "5" }) gave account 0’s Pot 5 USDC. A string is whole tokens; a bigint is base units.
  • result.delta(holder, "USDC") is how much a holder’s balance changed during the cook, in base units: 1,000,000 is 1 USDC.
  • snapshot() before each test and revert() after it return the chain to the same state, so the second test’s Pot holds no USDC.

fork

Starts a copy of a chain at a pinned point: on an EVM chain, anvil at options.block; on Solana, LiteSVM. The release’s engine, Router and Kitchen are ready, ten test accounts exist, and account 0’s Pot is deployed.

Import

Signature

Parameters

"auto" picks the first row that applies. Passing a mode forces it: "record" records the file again, and "replay" refuses to start without it:

Returns

A TestChain, or a SolanaTestChain when chain is Solana.

local

Starts an empty chain: the release deployed at its addresses, ten test accounts, account 0’s Pot, no Kitchen fee, and no protocols or tokens. It needs no network.

Import

Signature

Parameters

local({ chain: "solana" }) starts an empty Solana in LiteSVM with the engine, the Kitchen, the runtime’s own programs and wrapped SOL.

Returns

A TestChain, or a SolanaTestChain for Solana.

TestChain

What fork and local return on an EVM chain. fund writes a token balance into the token’s own storage, in the slot its balanceOf reads; the total supply does not change. It refuses a token whose balance is computed rather than stored, such as a rebasing token, and a Base B20 token, a node precompile anvil cannot run. Send such a token from a holder with impersonate.

CookOptions

A program is a ProgramInput: a path relative to the working directory, a file: URL, or { source, path? }. It compiles with the chain’s names, and its relative imports resolve beside the file.

CookResult

run reports the transaction it mined: returned and a revert’s reason come from the node’s trace of that transaction. It sends with the pending block’s gas limit as its budget, with no gas estimate and no simulation first, so a cook that is valid only in the block it is mined in is still sent. gasUsed is the gas the cook consumed, not that budget; unused gas is not charged. A reverted cook takes a block and a nonce like any transaction. Call simulate for a result without sending.

CookReverted

The error a cook that reverts throws, on EVM chains and on Solana.

Recordings

A replay boots from the state file at the pinned block’s number and timestamp. The file holds every account and storage slot the recording run read, at their values in that block, plus the block’s gas limit, the hardfork, the next block’s base fee and the test accounts’ nonces, so a replayed cook uses the same gas and is the same transaction. Each block after the pinned one is blockInterval seconds later, so no result depends on the wall clock. A replay knows only what the recording read: an account or slot read for the first time reads as zero. After a test starts reading something new, such as another token or pool, or after you move the block, record again: delete the file or pass mode: "record". The state file never contains the endpoint’s URL; the kit refuses to write one that does.

Solana

On Solana, fork and local return a SolanaTestChain. Its calls match TestChain’s, with these differences:
  • fork("solana", { state, rpcUrl, mode, zeroFee }) takes no block: a Solana node serves accounts only as they are now, so a recording is of the moment you record. The file keeps that slot, its time, and the cluster’s rent and epochs, and holds each account the test’s transactions named.
  • Test accounts are keypairs: account(i) has address, secretKey and pot, and each starts with 1,000 SOL. The keys are the same on every run, so a recording made with them replays with them.
  • run and simulate take accounts, main’s Account parameters by name, in the words sauce run --account takes (pot, pot:USDC, @to:USDC, mint:USDC, program:USDC, an address). The accounts the SDK adds for token methods are derived.
  • The result is a SolanaCookResult: returned, returnData, changes, delta(holder, token), logs, computeUnits, signature after run, and staged and lookupTable for a program too large for one transaction.
  • warp(slots) and slot() move and read the slot; getAccount(key) and setAccount(key, account) read and write any account.
solana-pay.test.ts
The test reads the Solana endpoint from an environment variable your command sets, so the URL stays out of the repository; the kit itself reads none.

Next steps