npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

viem-tx-sim

v0.3.0

Published

Preview transaction and batch balance changes with viem—no local fork or simulation API.

Readme

viem-tx-sim

Preview transaction and batch balance changes with viem over JSON-RPC, without a local fork or simulation API.

npm version CI MIT license

Pass your viem PublicClient, sender, and calldata to preview native and token balance changes before signing. Use the same API for one transaction or an ordered batch.

[!IMPORTANT] A simulation previews one RPC state snapshot. The signed transaction may execute against different state. Contracts can observe the injected code at from, so do not use the result as a security boundary.

Before you install

  • Runtime: Node.js 20 or newer, ESM, and viem 2.8 or newer.
  • RPC: Requires eth_call with state overrides, and eth_createAccessList for discovery. Some RPC providers omit access lists when a probed call reverts.
  • Scope: Caller receives raw balance changes and reverts. Client handles token metadata, prices, gas estimation, transaction assembly, and permit signing.

Install

pnpm add viem-tx-sim viem

Quick start

Use the viem client, sender, and calls from your application. List the balances you want to observe:

import { TxSimulator } from "viem-tx-sim";

const simulator = TxSimulator.create({ client });

const result = await simulator.simulate({
  from,
  calls,
  balanceQueries: [
    { asset: "native", account: from },
    { asset: tokenAddress, account: from },
    { asset: tokenAddress, account: recipient },
  ],
});

if (result.status === "reverted") {
  console.error(result.failingCallIndex, result.revertReason);
} else {
  console.table(result.balanceDeltas);
}

For a transfer of 250 token units, the table would contain:

| asset | account | before | after | delta | byCall | | --- | --- | ---: | ---: | ---: | --- | | 0xToken… | from | 1000n | 750n | -250n | [-250n] | | 0xToken… | recipient | 10n | 260n | 250n | [250n] |

Each balance delta contains before, after, delta, and byCall. delta is after - before; byCall[i] is the change caused by calls[i]. result.unresolved contains unreadable queries.

See examples for discovery, sequential batches, unfunded accounts, and revert handling.

Choose your workflow

simulate() takes calls, balance queries, and optional state assumptions. It sends one eth_call. Call helper methods for discovery.

| Goal | Use | Example | | --- | --- | --- | | Observe known balances | simulate() with balanceQueries | Known balances | | Discover the sender's touched ERC-20 balances | balanceQueries.forUser() | Balance discovery | | Simulate approve-then-use or another ordered batch | simulate() with multiple calls | Sequential batch | | Preview an unfunded or view-only account | tokenOverrides.* or nativeBalanceOverrides | Unfunded account | | Decode reverts or inspect RPC activity | errorAbi and debug | Reverts and debugging |

How it works

flowchart TD
    accTitle: Transaction simulation flow
    accDescr: Optional helpers prepare inputs. One eth_call injects a ghost contract at the sender, executes calls in order, and returns balance deltas or revert details.
    H["Optional helpers"] --> I["Calls, balance queries,<br/>state assumptions"]
    I --> S["simulate()"]
    S --> R["eth_call with<br/>state overrides"]
    R --> G["Ghost contract at from"]
    G --> E["Calls run in order<br/>msg.sender == from"]
    E --> O["Balance deltas<br/>or revert details"]

During eth_call, the package injects the runtime bytecode of a never-deployed ghost contract at from.

Downstream contracts see msg.sender == from. Calls run in order within one EVM context. The ghost contract records balance checkpoints before and after each call, then returns total and per-call changes. eth_call writes no state onchain.

The optional helpers prepare two kinds of explicit input:

  • balanceQueries.* discovers balances to observe.
  • tokenOverrides.* verifies storage slots or estimates required balances and allowances.

Results and errors

Both successful and reverted simulations return balanceDeltas and unresolved. A reverted result includes failingCallIndex and revertData. Successful decoding adds revertSelector, revertReason, and revertError.

Transaction reverts are results. Infrastructure and invalid-input failures throw typed errors:

  • AccessListUnsupportedError: the RPC endpoint cannot create access lists.
  • StateOverrideUnsupportedError: the RPC endpoint rejected state overrides or returned invalid simulator data.
  • InvalidSimulationInputError: the caller supplied invalid input, such as an empty batch.

RPC providers and contracts control parts of error messages and revertReason. Treat that text as untrusted before rendering it in a UI.

Batch gas estimation

gas.estimateBatch() measures per-call execution gas for a sequential batch in one eth_call (zero access lists). Dependent non-atomic legs — the canonical approve-then-swap — cannot be eth_estimateGas-ed standalone, because the second leg reverts without the first leg's state. The ghost runs the batch sequentially in one frame through a probe-free entry point and returns each call's gas.

const estimate = await simulator.gas.estimateBatch({
  from,
  calls: [approveCall, swapCall],
  // Prepare with tokenOverrides.* first — an unfunded account can't measure a swap.
  tokenSlotOverrides,
});
// estimate.byCall[i] = { executionGas, intrinsicAndCalldataGas, suggestedLimit }
// estimate.failingCallIndex is null on success, or the index of the first reverting call.

Each suggestedLimit is executionGas + intrinsicAndCalldataGas and is pre-buffer. Apply your own EIP-150 headroom before using it as a per-leg gas limit — 2× is recommended. EIP-150's 63/64 gas-forwarding rule means a limit that merely equals raw consumption can run out of gas at the innermost frame of a deep call tree, because each CALL forwards only 63/64 of remaining gas. The library returns a pre-buffer value and leaves the multiplier as a product decision: a wallet that re-estimates at broadcast can use a tighter factor.

Two error bars apply, and both should be documented to consumers:

  • State drift between simulation and broadcast. Non-atomic 5792 legs broadcast as separate transactions across potentially several blocks; pools move, storage changes, and the gas a leg actually costs at broadcast can differ from the simulated value. The direction is unpredictable; the buffer absorbs modest drift, large drift needs re-estimation.
  • Warm/cold access divergence (known direction). The sequential simulation runs all calls in one frame, so call 2 sees storage and accounts that call 1 already warmed (EIP-2929: 100 gas warm vs 2100 cold SLOAD, 100 vs 2600 for account access). When the same legs broadcast as separate transactions, each real transaction starts with cold state. The sequential simulation therefore systematically under-measures the gas of later calls that reuse earlier calls' warmed slots — the real cold-start transaction costs more. This compounds with the EIP-150 dilution in the same direction, a second reason the buffer should be generous. (For an atomic batch executed as one transaction the warming is real and the measurement is accurate — the divergence is specifically a non-atomic, separate-broadcast artifact.)

Limitations

  • A simulation reads one RPC state snapshot. Pending transactions, later blocks, and builder behavior can change the outcome before the signed transaction lands.
  • balanceQueries.forUser() derives candidates from eth_createAccessList. If the RPC provider returns a revert without an access list, the helper may miss addresses touched later in the call. Supply balance queries when you know the assets. When the provider rejects the access list because from cannot fund the calls, discovery degrades to the direct call targets rather than throwing (direct transfers still discovered; intermediary tokens may be missed).
  • Permit2 measurement in tokenOverrides.estimateRequirements() depends on the Permit2 singleton being in the discovered candidate set. When discovery degrades to direct call targets (unfunded from, above), a Permit2 singleton reached only through inner calls is not a candidate, so its requirements are silently unmeasured. Fund from, or include a direct Permit2 call in the batch, to restore measurement.
  • Balance queries read native balances or balanceOf(address). The library does not model token-ID ownership or ERC-1155 balances by ID.
  • NFT capture (nftQueries) reports ERC-721/1155 tokens received by from during simulation — via receiver callbacks (safe transfers, _safeMint) and an ERC-721 Enumerable walk (plain _mint on Enumerable collections such as Uniswap V3 positions). It does not detect: NFTs sent by from; counter-based mints that are neither safe nor Enumerable (e.g. Uniswap V4 positions — planned); plain-_mint non-Enumerable ERC-721s; or general ERC-1155 balances. When a batch both receives and sends tokens of the same plain-_mint Enumerable collection, the swap-and-pop reindexing an ERC-721 Enumerable transfer performs can move a newly received token below the walk's index window, under-reporting it (receipts recorded via safe-mint/safe-transfer receiver hooks are unaffected). Captured tokenUri reflects post-simulation state and is best-effort under a gas budget — heavy on-chain renderers may return undefined.
  • Injecting code at from changes code-size and account-type checks. The ghost contract handles ERC-1271 and common NFT receiver flows. Contracts with other EOA-versus-contract branches may choose another path during simulation.
  • Storage overrides require a verified balance or allowance slot. The helper puts non-standard or indirect layouts in unresolved and leaves their state unchanged.
  • Supply signed permit calldata. Your application creates and signs permits.
  • tokenSlotOverrides amounts must be below uint256.max. Use OVERRIDE_TOKEN_AMOUNT when you construct overrides so allowance decrements and incoming transfers remain observable.

To pin a multi-step workflow, pass the same fixed blockNumber to every discovery, preparation, and simulate() call. A moving tag such as latest can resolve to a different block on each RPC request.

Public API

  • TxSimulator.create({ client, gas?, debug?, errorAbi? })
  • simulate({ from, calls, balanceQueries, … })
  • balanceQueries.forUser() and balanceQueries.discoverErc20s()
  • tokenOverrides.forBalances() and tokenOverrides.forAllowances()
  • tokenOverrides.estimateRequirements()
  • gas.estimateBatch()
  • DEFAULT_SIMULATION_GAS_LIMIT and OVERRIDE_TOKEN_AMOUNT

The package exports its public argument/result types and typed error classes. TypeScript declarations are the detailed reference.

Development

Use Node.js 20+, pnpm 10, and Foundry nightly nightly-7debd6d47628c5551837534aee507dbf552d5889.

Foundry v1.7.1 lacks the access-list-on-revert behavior required by the test suite. Replace the nightly pin with the first stable release newer than v1.7.1.

foundryup --install nightly-7debd6d47628c5551837534aee507dbf552d5889
pnpm install
pnpm verify

pnpm verify runs formatting checks, linting, typechecking, the build, and the test suite. The test suite starts isolated local EVM nodes. After changing the contract, run pnpm build:contracts to regenerate src/generated/txSimulatorBytecode.ts. Do not edit generated bytecode by hand.

Run the mainnet tests with an RPC URL:

MAINNET_RPC_URL=https://your-rpc.example pnpm test:mainnet

Support and contributing

Use GitHub Issues for usage questions, reproducible bugs, provider compatibility reports, and focused proposals. Pull requests should include relevant tests and pass pnpm verify.

Report vulnerabilities through GitHub's private vulnerability form. If the form is unavailable, open an issue requesting a private contact channel without including technical details.

See the changelog for released changes.

Credits

Apoorv Lathey's transaction-simulation thread inspired the ghost-contract approach. Read docs/motivation.md for the original design notes and diagrams.

License

MIT © 2026 frontier159