Statecraft is a TypeScript-first testing primitive for Ethereum integration tests that replaces ad hoc beforeEach setup and brittle helper stacks with one explicit scenario pipeline. You compose deterministic fixtures like withChain, withFork, and withFundedWallet into a single test function, so setup order is visible, repeatable, and easy to reason about. If your Vitest integration tests are getting flaky or hard to maintain, Statecraft gives you a cleaner path without replacing viem, Anvil, or your test runner.
Statecraft relies on Anvil from Foundry for local and forked runtimes.
For local development:
curl -L https://foundry.paradigm.xyz | bashEnsure anvil is available on your PATH.
For GitHub Actions, set up Foundry before running tests:
- name: Setup Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run SDK Tests
run: bun run test:cibun add -D @st8craft/core @pimlico/alto viemimport { test, expect } from "vitest";
import { scenario, withChain, withFundedWallet } from "@st8craft/core";
test(
"runs a funded wallet scenario in one test",
scenario(
withChain(),
withFundedWallet({
balance: 1_000_000_000_000_000_000n, // 1 ETH in wei
}),
async ({ wallet, publicClient }) => {
const balance = await publicClient.getBalance({ address: wallet });
expect(balance).toBe(1_000_000_000_000_000_000n);
},
),
);Most suites start clean, then degrade into hidden setup, flaky fork state, and copy-pasted helper stacks. Statecraft keeps setup as explicit middleware in one place:
- Deterministic by default, pin fork blocks and keep state setup explicit.
- Composable setup, add only the
withXfixtures a test needs. - Honest context flow, each fixture extends context and passes it forward in order.
- Runner-friendly design,
scenario(...)returns an async function for Vitest, Jest, ornode:test.
Without Statecraft, setup usually lives in custom helpers plus beforeEach, and ordering guarantees are easy to break.
With Statecraft, setup is explicit middleware:
import { test, expect } from "vitest";
import { erc20Abi, parseEther } from "viem";
import {
scenario,
withFork,
withFundedWallet,
withErc20Balance,
} from "@st8craft/core";
const USDC_MAINNET = "0xA0b86991c6218b36c1d19D4a2e9Eb0ce3606eB48" as const;
test(
"fork + funded wallet + USDC balance",
scenario(
withFork({
rpcUrl: process.env.VITE_RPC_URL!,
blockNumber: 22_000_000n,
}),
withFundedWallet({
balance: parseEther("1"),
}),
withErc20Balance({
token: USDC_MAINNET,
amount: 1_000_000n, // 1 USDC (6 decimals)
}),
async ({ walletClient, publicClient }) => {
const usdc = await publicClient.readContract({
address: USDC_MAINNET,
abi: erc20Abi,
functionName: "balanceOf",
args: [walletClient.account.address],
});
expect(usdc).toBe(1_000_000n);
},
),
);scenario(...steps, testFn): composes setup steps into one async test function (examples wrap it with Vitesttest).withChain(): starts a fresh local Anvil runtime.withFork({ rpcUrl, blockNumber }): starts a pinned local fork for deterministic mainnet state.withFundedWallet({ balance, erc20? }): creates and funds a test wallet.withErc20Balance({ token, amount }): seeds ERC-20 balance on compatible local or forked nodes.withSnapshot(): snapshots before inner steps and reverts infinally.withContracts(...): injects runtime bytecode at known addresses.withDeployments(...): performs real deployments with constructor semantics.
Use Statecraft when:
- your tests need repeatable fork state
- setup is spread across helper files and hooks
- fixture ordering bugs are common
Skip Statecraft when:
- plain unit tests already cover your behavior
- your integration setup is trivial and stable
- you need a full Ethereum framework (Statecraft is not that)
withErc20Balanceis test-only balance mutation, not a mint path.- Forks should use pinned
blockNumbervalues for reproducibility. - Some tokens and node configurations may require alternative setup strategies.
Run examples:
bun install
bun run testIncluded examples in packages/examples/examples/scenarios.test.ts:
- fresh local chain plus funded wallet
- forked mainnet plus funded wallet plus real contract call
- forked mainnet plus funded wallet plus USDC via
withFundedWallet.erc20orwithErc20Balance - runtime bytecode injection with
withContracts - real deployment flow with
withDeployments
packages/core: published SDK (@st8craft/core): runtime, viem clients, scenario engine, andwithXfixturespackages/examples: runnable scenario examples (private workspace package)
This repo uses Vocs for docs.
Suggested reading order:
- Overview
- Quickstart
- Core Concepts
- Migration (only when upgrading)
For contributor and agent-assisted workflows, see AGENTS.md.
Run docs locally:
bun run docs:devStatecraft is an early-stage, 0.x SDK. Public APIs and behaviors may change between minor versions, so review the changelog before upgrading.
- Security reporting: see SECURITY.md.
- Support and questions: open an issue on GitHub: https://github.com/joepegler/statecraft/issues.
- Community expectations: see CODE_OF_CONDUCT.md.
Releases to npm are published via Changesets and include CI build and test verification.
Additionally, the publish step validates the packed tarball to ensure it contains the expected dist/ output.
Key checks:
bun run buildandvitest runas part of CI (seesdk-tests.yml).scripts/validate-publish-manifests.mjsruns during the publish workflow to validate the final npm package contents (seepublish-npm.yml).