Skip to content
Overview

Deterministic Ethereum integration tests—without the setup soup.

Statecraft is a composable scenario runtime for Vitest and viem: pin forks, fund wallets, inject bytecode or deploy for real, and wire clients in explicit, ordered steps—so your test bodies stay about behavior, not bootstrapping Anvil again and again.

Before: implicit globals & copy-paste chain code

  • One-off local chain / fork wiring in every test file.
  • Ad hoc wallet funding and client factories.
  • Flaky suites that depend on hidden global setup.

After: named fixtures & typed context

  • Compose withChain, withFork, withFundedWallet, and more as middleware-style steps.

  • Accumulate typed publicClient, walletClient, testClient, wallets, and deployments on the scenario context.

  • Keep fork tests honest: rpcUrl + a required blockNumber for reproducibility.

Compose scenarios, not config files

Each step receives context, extends it, and calls next(updatedCtx) once—clear ordering, predictable lifecycle, and tests that read like the product behavior you care about.

Diagram: scenario composes withFork, withFundedWallet, then test

Four ways to set chain state—pick the right speed

withChain

Spin up a fresh local Anvil-style runtime when you need isolation and a blank slate.

withFork

Fork mainnet (or any JSON-RPC) with a pinned block height for deterministic integration tests.

withContracts

Inject runtime bytecode at known addresses—fast setup without running constructors.

withDeployments

Deploy with real creation bytecode and constructor semantics when you need the full path.

Proof in a handful of lines

The same pattern you will find in packages/examples/examples/scenarios.test.ts and the Quickstart: fork, fund, assert.

import { test, expect } from "vitest";
import { parseEther } from "viem";
import { scenario, withFork, withFundedWallet } from "@statecraft/vitest";
 
test(
  "funded wallet on mainnet fork",
  scenario(
    withFork({
      rpcUrl: process.env.MAINNET_RPC_URL!,
      blockNumber: 22_000_000n,
    }),
    withFundedWallet({ balance: parseEther("1") }),
    async ({ wallet, publicClient }) => {
      const balance = await publicClient!.getBalance({ address: wallet! });
      expect(balance).toBe(parseEther("1"));
    },
  ),
);

Built for reliability, not magic

  • Pinned forks — tests target a specific blockNumber, not “whatever mainnet looks like today.”

  • Explicit runtimes — chain and fork fixtures start runtimes and clean up in finally so failures do not leak processes.

  • Optional isolation — snapshot/revert style helpers when you want tighter boundaries between assertions.

Ship scenarios your team can reuse

Install from the monorepo workspace, run bun test, and promote the same fixtures from examples into your product suite—bounded packages, clear dependency direction, Vitest-native ergonomics.