Skip to content

Lord1Egypt/ethsmith

Repository files navigation

ethsmith

The unified Ethereum development toolkit — Ganache-compatible API, powered by Foundry's Forge + Cast + Anvil + Chisel, with LevelDB persistence.

npm version CI Docker Node.js License: MIT


Demo

ethsmith demo

Fork mainnet in one flag:

$ ethsmith node --fork.network mainnet --block-time 12

2026-06-06 [info] Forking mainnet at block 22009000
2026-06-06 [info] RPC proxy ready {"port":8545}

geth attach works out of the box:

$ geth attach http://127.0.0.1:8545
Welcome to the Geth JavaScript console!

instance: anvil/v1.7.1
at block: 0
modules: eth:1.0 net:1.0 personal:1.0 web3:1.0

> eth.accounts
["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", ...]
> eth.blockNumber
0

Why ethsmith?

Problem Solution
Ganache is dead (archived, no updates) ethsmith speaks Ganache's CLI and API — drop-in replacement
Anvil is fast but uses a different CLI All your old ganache-cli flags work unchanged
Anvil dumps state as giant JSON (100MB+ for forked chains) ethsmith uses LevelDB — compact, indexed, < 10 MB
Four separate Foundry tools (forge, cast, anvil, chisel) One command: ethsmith compile, ethsmith test, ethsmith call...
Ganache's JS EVM was slow Anvil's Rust EVM is 100× faster
No Ganache in CI after v7 npm install -g ethsmith — works everywhere Node 20+ runs

Install

npm install -g ethsmith

Requires Node.js >= 20.

Foundry binary discovery — what happens automatically:

npm install -g ethsmith
    └─ postinstall runs
       ├─ SKIP_ETHSMITH_POSTINSTALL=1 set?  → exit, no download
       ├─ anvil found in system PATH?        → skip, use existing Foundry install
       ├─ ~/.ethsmith/bin/anvil exists?      → skip, already installed
       └─ none of the above                 → download Foundry → ~/.ethsmith/bin/

ethsmith node
    └─ looks for anvil in this order:
       1. ~/.ethsmith/bin/anvil   ← managed copy (installed by postinstall)
       2. System PATH             ← your existing global Foundry install
       3. Downloads on-demand    ← last-resort fallback

If postinstall was skipped (corporate proxy, offline, SKIP_ETHSMITH_POSTINSTALL=1) or you want to upgrade Foundry:

ethsmith install          # download / re-download Foundry to ~/.ethsmith/bin/
ethsmith update           # update Foundry to latest version
ethsmith update --check   # check versions without installing
ethsmith doctor           # verify Node version, all 4 binaries, paths, DB

Quick Start

# Start a local Ethereum node (port 8545, 10 accounts, 1000 ETH each)
ethsmith

# Same but with Ganache options — they all work
ethsmith --port 8545 --accounts 20 --deterministic --chain-id 1337

# Fork mainnet at latest block
ethsmith --fork https://eth.llamarpc.com

# Fork mainnet by name (built-in RPC endpoints)
ethsmith --fork.network mainnet

# Fork at specific block
ethsmith --fork https://eth.llamarpc.com@20000000

# Use LevelDB at custom path (state persists across restarts)
ethsmith --db ./mychain

# Start with block-time 2s (mine every 2 seconds)
ethsmith --block-time 2

All Commands

Node (Ethereum local node)

ethsmith [node] [options]

This is the default command — just run ethsmith with any options.

Node Options

Flag Ganache Alias Description Default
--port <n> -p HTTP + WebSocket RPC port 8545
--accounts <n> -a Number of accounts to generate 10
--mnemonic <phrase> -m BIP39 mnemonic for accounts random
--deterministic -d Use standard test mnemonic (same accounts always) false
--chain-id <id> --networkId Chain ID (Ganache: --networkId) 1337
--gas-limit <n> --gasLimit Block gas limit 30000000
--gas-price <wei> --gasPrice Minimum gas price 1000000000
--balance <eth> --defaultBalanceEther Initial ETH per account 1000
--block-time <s> Auto-mine interval (0 = instamine) 0
--no-mining Disable auto-mining false
--fork <url> Fork from URL (url@blockNumber supported)
--fork.url <url> Fork URL (Ganache compat)
--fork.blockNumber <n> Fork at specific block latest
--fork.network <name> Fork by name: mainnet sepolia arbitrum optimism base polygon
--hardfork <name> EVM hardfork: london paris shanghai cancun prague cancun
--unlock <addr> --impersonate Unlock/impersonate address (repeatable)
--db <path> LevelDB directory (state persists) ~/.ethsmith/db/<chainId>
--state-interval <s> Checkpoint to LevelDB every N seconds 30
--ipc [path] Enable IPC socket disabled
--optimism Enable Optimism L2 mode false
--init <file> Genesis JSON file
--fund-accounts <addr:eth> Fund address on startup (repeatable)
--keep-history / --prune-history Keep full block history (default: pruned) pruned
--order <type> TX ordering: fees or fifo fees
--log-level <level> debug info warn error info
--log-file <path> Log file path ~/.ethsmith/logs/

Compile (Forge Build)

ethsmith compile                    # forge build
ethsmith compile --optimize         # enable optimizer
ethsmith compile --via-ir           # use IR pipeline
ethsmith compile --sizes            # show contract sizes

Test (Forge Test)

ethsmith test                       # forge test
ethsmith test --watch               # watch for file changes
ethsmith test --gas-report          # show gas usage table
ethsmith test --match-test "testFoo" # run specific test
ethsmith test -vvvv                 # max verbosity

Fuzz (Property-Based Testing)

ethsmith fuzz                       # forge test --fuzz-runs 10000
ethsmith fuzz --fuzz-runs 100000    # more runs
ethsmith fuzz --match-test "testFuzz_"

Coverage

ethsmith coverage                   # forge coverage
ethsmith coverage --report lcov     # LCOV format for CI

Deploy

ethsmith deploy src/Token.sol:Token \
  --rpc-url http://localhost:8545 \
  --private-key 0xac0974be...

# With verification
ethsmith deploy src/Token.sol:Token \
  --rpc-url https://mainnet.infura.io/v3/KEY \
  --private-key $PRIVATE_KEY \
  --verify \
  --etherscan-api-key $ETHERSCAN_KEY

Flatten / Inspect / Format

ethsmith flatten src/Token.sol      # forge flatten (for Etherscan)
ethsmith inspect src/Token.sol:Token abi
ethsmith inspect src/Token.sol:Token bytecode
ethsmith fmt                        # format all Solidity files
ethsmith snapshot                   # gas snapshot (forge snapshot)

Maintenance

ethsmith update                     # update Foundry binaries to latest
ethsmith update --check             # check versions without installing
ethsmith install                    # (re-)download Foundry to ~/.ethsmith/bin/
ethsmith doctor                     # verify Node version, binaries, paths, DB

ethsmith clean                      # remove leftover Anvil tmp dirs from crashes
ethsmith clean --dry-run            # preview what would be deleted and disk reclaimed

Cast Commands (On-chain Interaction) — click to expand
# Read calls
ethsmith call 0xTokenAddr "balanceOf(address)" 0xMyAddr
ethsmith balance 0xMyAddr
ethsmith block latest
ethsmith block 19000000 --rpc-url https://eth.llamarpc.com
ethsmith tx 0xTxHash
ethsmith receipt 0xTxHash
ethsmith logs 0xContractAddr
ethsmith storage 0xContractAddr 0x0    # read storage slot
ethsmith code 0xContractAddr           # get bytecode
ethsmith nonce 0xAddr
ethsmith chain-id
ethsmith gas-price

# Write transactions
ethsmith send 0xTokenAddr "transfer(address,uint256)" 0xRecipient 1000000000000000000 \
  --private-key 0xac0974be...

# ABI encoding/decoding
ethsmith abi-encode "transfer(address,uint256)" 0xABC 1000
ethsmith abi-decode "transfer(address,uint256)" 0xa9059cbb...
ethsmith decode "transfer(address,uint256)" 0xa9059cbb...  # decode calldata
ethsmith sig "transfer(address,uint256)"     # → 0xa9059cbb
ethsmith keccak "Transfer(address,address,uint256)"

# Wallet
ethsmith wallet new                  # generate new wallet
ethsmith wallet import               # import from private key

# ERC20 helpers
ethsmith erc20 transfer 0xToken 0xRecipient 1ether

# CREATE2
ethsmith create2 --init-code 0x60... --salt 0x1234...

# Transaction tracing (replay + full EVM trace)
ethsmith trace 0xTxHash --rpc-url https://eth.llamarpc.com

# Utilities
ethsmith to-hex 255                  # → 0xff
ethsmith to-dec 0xff                 # → 255
ethsmith from-utf8 "hello"
ethsmith estimate 0xAddr "foo()"     # gas estimate
ethsmith rpc eth_blockNumber         # raw JSON-RPC

Chisel — Solidity REPL

ethsmith repl               # launch interactive Solidity REPL
# or
ethsmith chisel

Inside the REPL:

> uint256 x = 42;
> x * 2
Type: uint256
└ Data: 84

> address(0).balance
Type: uint256
└ Data: 0

> !inspect x        // show type + value
> !save mysession   // save session to disk
> !load mysession   // load session
> !quit             // exit

State Persistence (LevelDB)

ethsmith automatically saves your blockchain state to LevelDB every 30 seconds and on graceful shutdown. On restart, state is restored exactly where you left off.

# State is stored at:
~/.ethsmith/db/<chainId>/

# Custom location
ethsmith --db ./my-project-chain

# Adjust checkpoint interval (seconds)
ethsmith --state-interval 10

# Multiple independent chains
ethsmith --chain-id 1337 --db ./chainA &
ethsmith --port 8546 --chain-id 1338 --db ./chainB &

Why LevelDB instead of Anvil's JSON files?

Format Size (forked mainnet) Compression Indexed
Anvil JSON 100 MB+ None (plain text) No
ethsmith LevelDB ~8 MB gzip Yes

Fork Mode

# Fork mainnet at latest block
ethsmith --fork https://eth.llamarpc.com

# Fork at specific block (reproducible)
ethsmith --fork https://eth.llamarpc.com@20000000

# Fork by network name (built-in endpoints)
ethsmith --fork.network mainnet
ethsmith --fork.network sepolia
ethsmith --fork.network arbitrum
ethsmith --fork.network optimism
ethsmith --fork.network base
ethsmith --fork.network polygon

# Fork + state persistence (fork cache saved in LevelDB)
ethsmith --fork.network mainnet --db ./mainnet-fork

# Reset fork to original state (RPC)
curl -X POST http://localhost:8545 -d '{"jsonrpc":"2.0","method":"anvil_reset","params":[]}'

Anvil Special RPC Methods (32 methods) — click to expand

All 32 Anvil special methods work out of the box:

# Storage manipulation
cast rpc anvil_setStorageAt 0xAddr 0x0 0xValue
cast rpc anvil_setCode 0xAddr 0x60...         # replace bytecode
cast rpc anvil_setBalance 0xAddr 0xDE0B...    # set ETH balance
cast rpc anvil_setNonce 0xAddr 0x5             # override nonce
cast rpc anvil_setChainId 31337

# Account impersonation
cast rpc anvil_impersonateAccount 0xWhale
cast rpc anvil_stopImpersonatingAccount 0xWhale
cast rpc anvil_autoImpersonateAccount true    # impersonate any sender

# Block control
cast rpc anvil_mine 100                        # mine 100 blocks instantly
cast rpc anvil_setBlockTimestamp 1700000000
cast rpc anvil_setNextBlockBaseFeePerGas 0

# Snapshot / Revert
cast rpc evm_snapshot                          # → "0x1"
cast rpc evm_revert '["0x1"]'                  # revert to snapshot
cast rpc anvil_reorg '{"depth":2}'             # simulate reorg
cast rpc anvil_rollback '{"blocks":5}'         # roll back 5 blocks

# State dump/load (used internally by LevelDB layer)
cast rpc anvil_dumpState                       # → hex state
cast rpc anvil_loadState '["0x..."]'           # restore state

# Mempool
cast rpc anvil_dropTransaction 0xTxHash
cast rpc anvil_dropAllTransactions

# DeFi helpers
cast rpc anvil_dealErc20 '["0xToken","0xAddr","1000000000000000000"]'
cast rpc anvil_setErc20Allowance '["0xToken","0xOwner","0xSpender","1000"]'
cast rpc anvil_addBalance '["0xAddr","0xDE0B..."]'

# Info
cast rpc anvil_nodeInfo
cast rpc anvil_metadata

Ganache Compatibility Methods (personal_*, miner_*, EIP-712) — click to expand
# Personal namespace (MetaMask compatibility)
cast rpc personal_listAccounts
cast rpc personal_importRawKey '["0xPrivKey","password"]'
cast rpc personal_unlockAccount '["0xAddr","password",0]'
cast rpc personal_lockAccount '["0xAddr"]'
cast rpc personal_sendTransaction '[{"from":"0x...","to":"0x...","value":"0x1"},"password"]'

# Miner control
cast rpc miner_start              # start auto-mining
cast rpc miner_stop               # stop auto-mining (manual mode)
cast rpc miner_setGasPrice '["0x3B9ACA00"]'

# EIP-712 signing (MetaMask style)
cast rpc eth_signTypedData_v4 '["0xAddr",{"types":{...},"domain":{...},"message":{...}}]'

Debug & Trace (Full EVM Tracing) — click to expand
# Step-by-step EVM trace
cast rpc debug_traceTransaction '["0xTxHash"]'
cast rpc debug_traceCall '[{"to":"0x...","data":"0x..."},"latest"]'
cast rpc debug_traceBlockByNumber '["latest"]'

# Parity-style trace
cast rpc trace_transaction '["0xTxHash"]'
cast rpc trace_block '["latest"]'
cast rpc trace_filter '[{"fromBlock":"0x1","toBlock":"0x10","toAddress":["0x..."]}]'
cast rpc trace_replayBlockTransactions '["latest",["trace"]]'

# Mempool inspection
cast rpc txpool_content
cast rpc txpool_inspect
cast rpc txpool_status

Programmatic API (Node.js)

Drop-in replacement for the old ganache npm package:

const ethsmith = require('ethsmith')

// ── In-memory provider (for unit tests — no ports) ──────────────────────────
const provider = await ethsmith.provider({
  chain: { chainId: 1337 },
  mnemonic: 'test test test test test test test test test test test junk',
  wallet: { totalAccounts: 5 }
})

// Use with ethers.js
const { ethers } = require('ethers')
const ethersProvider = new ethers.BrowserProvider(provider)
const signer = await ethersProvider.getSigner()

// ── Server with port ────────────────────────────────────────────────────────
const server = ethsmith.server({
  port: 8545,
  deterministic: true,
  'chain-id': '1337',
  'block-time': '2'
})
await server.listen(8545)
// server.provider is an EIP-1193 provider

// ── Fork mainnet in tests ───────────────────────────────────────────────────
const forkProvider = await ethsmith.provider({
  fork: 'https://eth.llamarpc.com@20000000',
  'chain-id': '1'
})

// ── Stop ────────────────────────────────────────────────────────────────────
await server.close()
await provider.disconnect()

Hardhat / Truffle / Foundry Integration

Hardhat — use as local node:

# Terminal 1
ethsmith --port 8545 --deterministic

# Terminal 2
npx hardhat --network localhost test

Hardhat config:

module.exports = {
  networks: {
    localhost: {
      url: 'http://127.0.0.1:8545',
      chainId: 1337
    }
  }
}

Truffle — works identically to old Ganache:

module.exports = {
  networks: {
    development: {
      host: '127.0.0.1',
      port: 8545,
      network_id: '*'
    }
  }
}

Foundry — point to ethsmith node:

ethsmith --port 8545 &
forge test --fork-url http://localhost:8545
cast send 0xAddr "mint(address,uint256)" $ADDR 1000 --rpc-url http://localhost:8545 --private-key 0xac0974be...

Logging

All events are logged to ~/.ethsmith/logs/ in JSON format with daily rotation.

# Set log level
ethsmith --log-level debug

# Custom log file
ethsmith --log-file ./ethsmith.log

# Example log output (JSON):
# {"timestamp":"2026-06-06 03:15:27.000","level":"info","message":"Starting ethsmith node","port":8545,"chainId":1337}
# {"timestamp":"...","level":"info","message":"Checkpoint saved","block":150}
# {"timestamp":"...","level":"info","message":"State restored","blockNumber":150}

Log events:

  • Node startup (port, chainId, accounts, mnemonic)
  • Every checkpoint (block number, state size, compression ratio)
  • State restore on startup (block restored to)
  • Fork info (URL, block number, latency)
  • DB operations (open, close, integrity)
  • All errors with full stack traces

Configuration File

Create ~/.ethsmith/config.json for persistent defaults:

{
  "port": 8545,
  "accounts": 10,
  "deterministic": true,
  "chainId": 1337,
  "gasLimit": 30000000,
  "balance": 1000,
  "stateInterval": 30,
  "logLevel": "info"
}

Multi-Instance

Run multiple independent chains simultaneously:

# Chain A — local dev
ethsmith --port 8545 --chain-id 1337 --db ~/.ethsmith/dev &

# Chain B — mainnet fork
ethsmith --port 8546 --chain-id 1 --fork.network mainnet --db ~/.ethsmith/fork-mainnet &

# Chain C — sepolia fork
ethsmith --port 8547 --chain-id 11155111 --fork.network sepolia --db ~/.ethsmith/fork-sepolia &

Each instance has its own LevelDB directory and process — completely independent.


Docker

Basic Usage

# Ephemeral — fresh chain every restart (quickest start)
docker run -p 8545:8545 lord1egypt/ethsmith:latest

# Detached (background)
docker run -d -p 8545:8545 --name ethsmith lord1egypt/ethsmith:latest

# Stop / restart
docker stop ethsmith
docker start ethsmith

Persistent State

# Named volume — state survives container restarts and re-creates
docker run -p 8545:8545 -v ethsmith-data:/root/.ethsmith/db lord1egypt/ethsmith:latest

# Bind-mount — state saved to a local folder you can inspect
docker run -p 8545:8545 -v ./data:/root/.ethsmith/db lord1egypt/ethsmith:latest
docker run -p 8545:8545 -v /absolute/path:/root/.ethsmith/db lord1egypt/ethsmith:latest

Always mount to /root/.ethsmith/db — not /root/.ethsmith.
Foundry binaries are installed to /root/.ethsmith/bin inside the image layer; mounting the parent directory shadows them and breaks startup.

Accounts

# Standard deterministic accounts (same 10 addresses every time)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic

# Custom number of accounts
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --accounts 20

# Custom mnemonic
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --mnemonic "your twelve word mnemonic phrase goes here trust me bro"

# Custom starting balance (ETH per account)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --balance 5000

# Unlock / impersonate a specific address on startup
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --unlock 0xYourAddress

Network & Chain

# Custom chain ID
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --chain-id 31337

# Custom port inside container (still map 8545 outside)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --port 8545

# Custom gas limit and gas price
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --gas-limit 60000000 --gas-price 1000000000

# Specific EVM hardfork
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --hardfork cancun
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --hardfork shanghai
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --hardfork london

# Optimism L2 mode
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --optimism

Block Time (Mining)

# Default: instamine — every transaction mines a block immediately
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic

# Mine a block every 1 second
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --block-time 1

# Mine a block every 5 seconds
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --block-time 5

# Mine a block every 12 seconds (mainnet-like cadence)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --block-time 12

# Disable auto-mining entirely (manual mine via RPC only)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --no-mining

Fork Mode

# Fork mainnet (built-in public RPC)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network mainnet

# Fork sepolia testnet
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network sepolia

# Fork other networks by name
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network arbitrum
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network optimism
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network base
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network polygon

# Fork from custom RPC URL
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork https://eth.llamarpc.com

# Fork at a specific block number (reproducible)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork https://eth.llamarpc.com@20000000

# Fork with your own RPC via -e (quick, but key appears in shell history and ps aux)
docker run -p 8545:8545 \
  -e ETHSMITH_FORK_MAINNET_URL=https://mainnet.infura.io/v3/YOUR_KEY \
  lord1egypt/ethsmith:latest node --fork.network mainnet

# Better for secrets: --env-file keeps the key out of shell history, ps aux, and docker inspect args
# 1. Create .env (add to .gitignore — never commit this file)
#    ETHSMITH_FORK_MAINNET_URL=https://mainnet.infura.io/v3/YOUR_KEY
#    ETHSMITH_FORK_SEPOLIA_URL=https://sepolia.infura.io/v3/YOUR_KEY
# 2. Run with --env-file
docker run -p 8545:8545 \
  --env-file .env \
  lord1egypt/ethsmith:latest node --fork.network mainnet

# Fork + persistent state (fork cache stays across restarts)
docker run -p 8545:8545 \
  -v ethsmith-fork:/root/.ethsmith/db \
  lord1egypt/ethsmith:latest node --fork.network mainnet

Logging

# Set log level (debug / info / warn / error)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --log-level debug
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --log-level warn

# Write logs to a file inside the container (mount volume to retrieve them)
docker run -p 8545:8545 \
  -v ./logs:/logs \
  lord1egypt/ethsmith:latest node --log-file /logs/ethsmith.log

# Log level via environment variable
docker run -p 8545:8545 \
  -e ETHSMITH_LOG_LEVEL=debug \
  lord1egypt/ethsmith:latest

# Quiet (errors only) with persistent state
docker run -p 8545:8545 \
  -v ethsmith-data:/root/.ethsmith/db \
  lord1egypt/ethsmith:latest node --log-level error

State & Performance

# Checkpoint every 10 seconds instead of default 30
docker run -p 8545:8545 \
  -v ethsmith-data:/root/.ethsmith/db \
  lord1egypt/ethsmith:latest node --state-interval 10

# Keep full block history (needed for eth_getLogs across old blocks)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --keep-history

# TX ordering: fifo (first-in-first-out) instead of default fee-priority
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --order fifo

All Options Combined

# Full example — deterministic, persistent, fork mainnet, 1s blocks, debug logs
docker run -d \
  --name ethsmith-dev \
  -p 8545:8545 \
  -v ethsmith-data:/root/.ethsmith/db \
  -e ETHSMITH_FORK_MAINNET_URL=https://eth.llamarpc.com \
  lord1egypt/ethsmith:latest \
  node \
    --deterministic \
    --accounts 20 \
    --balance 10000 \
    --chain-id 1337 \
    --block-time 1 \
    --fork.network mainnet \
    --gas-limit 60000000 \
    --state-interval 10 \
    --log-level debug

docker-compose

# docker-compose.yml
version: '3.8'

services:
  # Local dev node — instamine, deterministic accounts, persistent
  node-dev:
    image: lord1egypt/ethsmith:latest
    ports: ['8545:8545']
    volumes: ['./data/dev:/root/.ethsmith/db']
    command: node --deterministic --chain-id 1337
    restart: unless-stopped
    stop_grace_period: 30s   # gives checkpoint time to finish before SIGKILL

  # Mainnet fork — 12s block time, persistent fork cache
  node-mainnet-fork:
    image: lord1egypt/ethsmith:latest
    ports: ['8546:8545']
    volumes: ['./data/fork-mainnet:/root/.ethsmith/db']
    environment:
      - ETHSMITH_FORK_MAINNET_URL=https://eth.llamarpc.com
    command: node --fork.network mainnet --block-time 12 --chain-id 1
    restart: unless-stopped
    stop_grace_period: 30s

  # Sepolia fork — for testnet simulation
  node-sepolia-fork:
    image: lord1egypt/ethsmith:latest
    ports: ['8547:8545']
    volumes: ['./data/fork-sepolia:/root/.ethsmith/db']
    command: node --fork.network sepolia --deterministic
    restart: unless-stopped
    stop_grace_period: 30s

  # Silent CI node — no auto-mining, errors only
  node-ci:
    image: lord1egypt/ethsmith:latest
    ports: ['8548:8545']
    command: node --deterministic --no-mining --log-level error

Available docker run Flags Reference

Flag Example Description
--deterministic node --deterministic Same 10 accounts every run
--accounts <n> node --accounts 20 Number of accounts (default 10)
--mnemonic <phrase> node --mnemonic "word1 word2..." Custom BIP39 mnemonic
--balance <eth> node --balance 5000 Starting ETH per account (default 1000)
--chain-id <id> node --chain-id 31337 Chain ID (default 1337)
--port <n> node --port 8545 RPC port inside container
--block-time <s> node --block-time 1 Seconds between blocks (default: instamine)
--no-mining node --no-mining Disable auto-mining
--fork <url> node --fork https://... Fork from RPC URL
--fork <url@block> node --fork https://...@20000000 Fork at specific block
--fork.network <name> node --fork.network mainnet Fork by name
--hardfork <name> node --hardfork cancun EVM hardfork
--gas-limit <n> node --gas-limit 60000000 Block gas limit
--gas-price <wei> node --gas-price 1000000000 Minimum gas price
--unlock <addr> node --unlock 0xABC... Impersonate address on startup
--state-interval <s> node --state-interval 10 LevelDB checkpoint interval
--keep-history node --keep-history Keep full block history
--order <type> node --order fifo TX ordering: fees or fifo
--optimism node --optimism Enable Optimism L2 mode
--log-level <level> node --log-level debug Log verbosity
--log-file <path> node --log-file /logs/node.log Write logs to file

Environment variables:

Variable Example Description
ETHSMITH_LOG_LEVEL debug Log level (alternative to --log-level)
ETHSMITH_LOG_FILE /logs/ethsmith.log Log file path
ETHSMITH_FORK_MAINNET_URL https://... Override mainnet RPC endpoint
ETHSMITH_FORK_SEPOLIA_URL https://... Override sepolia RPC endpoint
ETHSMITH_FORK_ARBITRUM_URL https://... Override arbitrum RPC endpoint
ETHSMITH_FORK_OPTIMISM_URL https://... Override optimism RPC endpoint
ETHSMITH_FORK_BASE_URL https://... Override base RPC endpoint
ETHSMITH_FORK_POLYGON_URL https://... Override polygon RPC endpoint

CI/CD Pipeline — click to expand

ethsmith uses a fully automated CI/CD pipeline:

Workflows

Workflow Trigger What it does
CI (ci.yml) push / PR to main Runs all 6 test suites on Node 20 & 22 with Foundry installed
Release (release.yml) git push --tags with v*.*.* Creates a GitHub Release automatically
Publish (publish.yml) GitHub Release created Runs tests → publishes to npm → builds & pushes Docker image

How to release a new version

# 1. Bump version in package.json
npm version patch   # or minor / major

# 2. Push commit + tag
git push && git push --tags
# → GitHub Actions creates the Release automatically
# → Publish workflow fires: npm publish + docker push

Required GitHub Secrets

Go to Settings → Secrets → Actions and add:

Secret Value
NPM_TOKEN npm access token (from npmjs.com → Account → Access Tokens)
DOCKER_USERNAME lord1egypt
DOCKER_PASSWORD Docker Hub password or access token

Architecture — click to expand
ethsmith/
├── bin/
│   └── ethsmith.js          ← CLI entry point (Node >=20 check)
├── src/
│   ├── index.js             ← Public API: provider(), server()
│   ├── core/
│   │   ├── binary.js        ← Foundry binary detection + auto-download
│   │   ├── flags.js         ← Ganache → Anvil flag translation (20+ flags)
│   │   ├── db.js            ← LevelDB: state save/load/restore, integrity
│   │   └── logger.js        ← Winston + daily log rotation
│   └── cli/
│       ├── index.js         ← Commander.js CLI (30+ subcommands)
│       ├── node.js          ← EthsmithNode class (spawns Anvil, manages state)
│       ├── forge.js         ← Forge wrapper (compile/test/deploy/fuzz/coverage...)
│       ├── cast.js          ← Cast wrapper (call/send/balance/trace/abi-*...)
│       ├── chisel.js        ← Chisel REPL wrapper
│       └── install.js       ← Foundry binary installer

State persistence flow:

startup → LevelDB has state? → anvil_loadState RPC → Anvil restores state
running → every 30s → anvil_dumpState RPC → gzip → LevelDB
shutdown → anvil_dumpState RPC → gzip → LevelDB → kill Anvil

Troubleshooting / FAQ

Docker: container exits immediately with "unknown option '--host'"

This was a bug in images older than v1.3.3. The CMD incorrectly passed --host which doesn't exist.

Fix: pull the latest image.

docker pull lord1egypt/ethsmith:latest
Docker: state not persisting between container restarts

You need a volume mount. Without -v, every docker run starts a fresh chain.

# Named volume (recommended)
docker run -p 8545:8545 -v ethsmith-data:/root/.ethsmith/db lord1egypt/ethsmith:latest

# Bind-mount to local folder
docker run -p 8545:8545 -v ./data:/root/.ethsmith/db lord1egypt/ethsmith:latest

Wrong path — mounting to /root/.ethsmith instead of /root/.ethsmith/db shadows the Foundry binaries inside the image. Always use /root/.ethsmith/db.

Docker: "exec: anvil: not found" or Foundry binaries missing

Foundry is installed by postinstall.js when the image is built. This can fail if:

  • The image was pulled but not re-built after a base change
  • The volume is mounted to /root/.ethsmith (shadows binaries)
# Always mount to /root/.ethsmith/db — not the parent
docker run -p 8545:8545 -v ethsmith-data:/root/.ethsmith/db lord1egypt/ethsmith:latest
geth attach: "Method not found" for net_peerCount

This was fixed in v1.3.3. The proxy now returns "0x0" for net_peerCount directly instead of forwarding to Anvil.

Upgrade (recommended):

npm install -g ethsmith          # npm
docker pull lord1egypt/ethsmith:latest  # Docker

Workaround without upgrading — add a small HTTP proxy in front that intercepts net_peerCount and returns {"jsonrpc":"2.0","id":1,"result":"0x0"}, or just upgrade — it's a one-liner.

Port 8545 already in use
# Use a different port
ethsmith --port 8546
docker run -p 8546:8545 lord1egypt/ethsmith:latest node --port 8545
Foundry not found / "anvil: command not found"
ethsmith install    # downloads Foundry to ~/.ethsmith/bin/
ethsmith doctor     # verify all binaries and paths

If you're on Alpine Linux or Termux (Android), see the musl/Termux limitation in AUDIT.md.

State not restoring on restart

ethsmith restores state automatically if the --db path is the same between runs. Default is ~/.ethsmith/db/<chainId>/.

# Explicit path — safest for scripts
ethsmith --db ./mychain --chain-id 1337

# Make sure chain ID matches — state is keyed by chain ID
ethsmith --chain-id 1337 --db ./mychain    # start
ethsmith --chain-id 1337 --db ./mychain    # restart — restores correctly
ethsmith --chain-id 9999 --db ./mychain    # WRONG chain ID — starts fresh
--log-file option ignored

Fixed in v1.3.2. If you're on an older version:

npm install -g ethsmith   # upgrade to latest
~/.foundry/anvil/tmp/ growing to 50 GB+

Fixed in v1.3.1. ethsmith now cleans Anvil's internal tmp dirs on startup and shutdown.

To clean manually:

ethsmith clean          # remove leftover dirs
ethsmith clean --dry-run  # preview what would be deleted

Support


Comparison

Feature Ganache (archived) plain Anvil ethsmith
Active development
EVM speed JS (slow) Rust (fast) Rust (fast)
Old Ganache CLI flags
personal_* RPC
miner_start/stop
LevelDB persistence
Compact state storage ❌ (big JSON)
Anvil special methods
Modern hardforks
debug_trace*
trace_* (Parity)
Forge (compile/test)
Cast (call/send/abi)
Chisel (Solidity REPL)
Fork by network name
fork.provider (in-memory)
Optimism L2 support
Node.js >= 20
Programmatic API

Tech Stack

Package Purpose
commander v12 CLI framework
level v8 LevelDB bindings (Node.js)
winston v3 Structured logging
winston-daily-rotate-file v5 Log rotation
axios v1 HTTP (RPC calls + binary download)
tar v7 Extract Foundry binaries
ethers v6 Programmatic provider
Foundry (system) forge, cast, anvil, chisel binaries

License

MIT — Lord1Egypt


Related Projects

Other open-source projects by the same author (Lord1Egypt):

Project Description
skillforge-agent 539 ready-to-use AI agent skills for Claude, Gemini, GPT — npm + PyPI
awesome-prompt-forge 2,592 curated AI system prompts across 14 categories — npm + PyPI
awesome-ai-system-prompts GitHub collection of AI system prompts for developers

About

Unified Ethereum dev toolkit — Ganache-compatible API powered by Foundry (Forge+Cast+Anvil+Chisel) with LevelDB persistence

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors