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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@polareth/evmstate

v0.1.4

Published

A TypeScript library for tracing, and visualizing EVM state changes with detailed human-readable labeling.

Readme

@polareth/evmstate

A TypeScript library for tracing, and visualizing EVM state changes with detailed human-readable labeling.

Overview

The library traces all state changes after a transaction has been executed in a local VM, or by watching transactions in incoming blocks. It then labels them with semantic insights and a detailed diff of all the changes.

It can be seen as an alternative to using event logs for evm interfaces, as it captures and labels every state change with precise semantic information, including variable names, mapping keys, array indices, decoded values and path tracing.

Powered by Tevm and whatsabi.

Features

  • Complete state change tracing: Track the state of every account touched during the transaction
  • Human-readable labeling: Retrieve the storage layout of each account if it's available for contracts, to label storage slots with variable names, decode values and provide a detailed path of access from the base slot to the final value
  • Intelligent key detection: Extract and match mapping keys from transaction data
  • Type-aware decoding: Convert raw storage values to appropriate JavaScript types; the state trace is fully typed if a storage layout is provided

Installation

npm install @polareth/evmstate
# or
pnpm add @polareth/evmstate
# or
yarn add @polareth/evmstate

Quickstart

import { traceState } from "@polareth/evmstate";

// Trace a transaction
const trace = await traceState({
  rpcUrl: "https://1.rpc.thirdweb.com",
  from: "0xYourAddress",
  to: "0xContractAddress",
  data: "0xEncodedCalldata",
  value: 0n,
});

// Watch an account's state
const unsubscribe = await watchState({
  rpcUrl: "https://1.rpc.thirdweb.com",
  address: "0xContractAddress",
  storageLayout: contractStorageLayout as const,
  abi: contractAbi,
  onStateChange: (stateChange) => {
    console.log(stateChange);
  },
  onError: (error) => {
    console.error(error);
  },
});

Core functionality

1. traceState - Analyze transaction state

The traceState function is the primary way to analyze how a transaction affects state. It can be used in several ways:

Basic usage with RPC URL and transaction parameters

import { traceState } from "@polareth/evmstate";

// Trace a simulated transaction
const trace = await traceState({
  rpcUrl: "https://1.rpc.thirdweb.com",
  from: "0xYourAddress",
  to: "0xContractAddress",
  data: "0xEncodedCalldata",
  value: 0n,
});

Using contract ABI for better readability

import { traceState } from "@polareth/evmstate";

// Trace with typed contract call (similar to viem)
const trace = await traceState({
  rpcUrl: "https://1.rpc.thirdweb.com",
  from: "0xYourAddress",
  to: "0xContractAddress",
  abi: contractAbi,
  functionName: "transfer",
  args: ["0xRecipient", "1000000000000000000"], // address, amount
});

Tracing an existing transaction

import { traceState } from "@polareth/evmstate";

// Trace an existing transaction by hash
const trace = await traceState({
  rpcUrl: "https://1.rpc.thirdweb.com",
  txHash: "0xTransactionHash",
});

Using a custom Tevm client for more control

import { createMemoryClient, http } from "tevm";
import { mainnet } from "tevm/common";
import { traceState } from "@polareth/evmstate";

// Initialize client
const client = createMemoryClient({
  common: mainnet,
  fork: {
    transport: http("https://1.rpc.thirdweb.com"),
    blockTag: "latest",
  },
});

// Trace with custom client
const trace = await traceState({
  client,
  from: "0xYourAddress",
  to: "0xContractAddress",
  data: "0xEncodedCalldata",
});

2. Tracer - Create reusable tracing instances

The Tracer class provides an object-oriented interface for reusing client instances and configuration:

import { createMemoryClient, http } from "tevm";
import { mainnet } from "tevm/common";
import { Tracer } from "@polareth/evmstate";

// Initialize client
const client = createMemoryClient({
  common: mainnet,
  fork: {
    transport: http("https://1.rpc.thirdweb.com"),
    blockTag: "latest",
  },
});

// Create a reusable tracer
const tracer = new Tracer({ client });

// Trace multiple transactions with the same client
const trace1 = await tracer.traceState({
  from: "0xYourAddress",
  to: "0xContractAddress",
  data: "0xEncodedCalldata1",
});

const trace2 = await tracer.traceState({
  from: "0xYourAddress",
  to: "0xContractAddress",
  data: "0xEncodedCalldata2",
});

3. watchState - Monitor account state

The watchState function allows continuous monitoring of state access for a specific contract or EOA:

import { watchState } from "@polareth/evmstate";

// Start watching state
const unsubscribe = await watchState({
  rpcUrl: "https://1.rpc.thirdweb.com",
  address: "0xContractAddress",
  // Optional storage layout (improves labeling) - needs to be imported 'as const' similar to the ABI
  storageLayout: contractStorageLayout,
  // Optional ABI (improves decoding)
  abi: contractAbi,
  // Callback for state change/access
  onStateChange: (stateChange) => {
    console.log("State change detected:", stateChange);
    // Use the state
  },
  // Callback on error
  onError: (error) => {
    console.error("Watch error:", error);
  },
  // Optional polling interval (default: 1000ms)
  pollingInterval: 2000,
});

// Later, stop watching
unsubscribe();

Understanding the output

The traceState and watchState functions return detailed information about state changes. The output follows this structure (watchState directly emits the object for the account address):

{
  "0xContractAddress": {
    // Intrinsic state (balance, nonce, code)
    "balance": {
      "current": 1000000000000000000n, // Current value (bigint)
      "modified": true, // Whether it was modified
      "next": 2000000000000000000n // New value after the transaction
    },
    "nonce": {
      "current": 5,
      "modified": true,
      "next": 6
    },
    "code": { "current": "0x...", "modified": false },

    // Storage changes, labeled by variable name
    "storage": {
      // Primitive types
      "counter": {
        "kind": "primitive",
        "name": "counter",
        "type": "uint256",
        "trace": [
          {
            "current": { "hex": "0x05", "decoded": 5n },
            "modified": true,
            "next": { "hex": "0x06", "decoded": 6n },
            "path": [],
            "fullExpression": "counter",
            "slots": ["0x0000000000000000000000000000000000000000000000000000000000000000"]
          }
        ]
      },

      // Mappings with keys
      "balances": {
        "kind": "mapping",
        "name": "balances",
        "type": "mapping(address => uint256)",
        "trace": [
          {
            "current": { "hex": "0x2386f26fc10000", "decoded": 10000000000000000n },
            "modified": true,
            "next": { "hex": "0x2386f26fc10001", "decoded": 20000000000000000n },
            "path": [
              {
                "kind": "mapping_key",
                "key": "0x1234567890123456789012345678901234567890",
                "keyType": "address"
              }
            ],
            "fullExpression": "balances[0x1234567890123456789012345678901234567890]",
            "slots": ["0x8e9c0c9f9fb928592f2fb0a9314450706c27839d034893b88d8ed2f54cf1bd5e"]
          }
        ]
      },

      // Arrays with indices
      "numbers": {
        "kind": "dynamic_array",
        "name": "numbers",
        "type": "uint256[]",
        "trace": [
          {
            "current": { "hex": "0x03", "decoded": 3n },
            "modified": false,
            "path": [
              { "kind": "array_length", "name": "_length" }
            ],
            "fullExpression": "numbers._length",
            "slots": ["0x0000000000000000000000000000000000000000000000000000000000000003"]
          },
          {
            "current": { "hex": "0x64", "decoded": 100n },
            "modified": true,
            "next": { "hex": "0xc8", "decoded": 200n },
            "path": [
              { "kind": "array_index", "index": 2n }
            ],
            "fullExpression": "numbers[2]",
            "slots": ["0x5de13444fe158c7b5525d0d208535a5f84ca2f75ce5219b9c55fb55643beb57c"]
          }
        ]
      },

      // Structs with fields
      "user": {
        "kind": "struct",
        "name": "user",
        "type": "struct Contract.User",
        "trace": [
          {
            "current": { "hex": "0x00", "decoded": 0n },
            "modified": true,
            "next": { "hex": "0x01", "decoded": 1n },
            "path": [
              { "kind": "struct_field", "name": "id" }
            ],
            "fullExpression": "user.id",
            "slots": ["0x0000000000000000000000000000000000000000000000000000000000000004"]
          }
        ]
      }
    }
  }
}

Key properties in the output

For each storage variable, the output includes:

  • name: The human-readable variable name from the contract
  • type?: The Solidity type of the variable
  • kind?: The kind of storage variable ("primitive", "mapping", "dynamic_array", "static_array", "struct", "bytes", "string")
  • trace: An array of trace entries for this variable

Each trace entry contains:

  • current?: The current value before the transaction (both hex and decoded)
  • next?: The new value after the transaction (if modified)
  • modified: Boolean indicating if the value was changed
  • path: Array of path components (mapping keys, array indices, struct fields, length fields for bytes or arrays)
  • fullExpression: A human-readable representation of the full variable access (e.g., balances[0x1234][5])
  • slots: The actual storage slots accessed

Advanced usage

Fully typed state changes

When providing a storage layout with as const, TypeScript will infer the correct types for all state changes:

import { watchState } from "@polareth/evmstate";

import { erc20Layout } from "./layouts";

// Get fully typed state changes
const unsubscribe = await watchState({
  rpcUrl: "https://1.rpc.thirdweb.com",
  address: "0xContractAddress",
  storageLayout: erc20Layout as const,
  onStateChange: (stateChange) => {
    if (stateChange.storage.balances) {
      const balances = stateChange.storage.balances;
      // balances[`0x${string}`]
      const userBalance = balances.trace[0].fullExpression;
      // bigint | undefined
      const amount = balances.trace[0].next.decoded;
    }
  },
});

Using a custom Tevm client

For more control over the environment, you can provide your own Tevm client:

import { createMemoryClient, http } from "tevm";
import { mainnet } from "tevm/common";
import { watchState } from "@polareth/evmstate";

// Create custom client with specific configuration
const client = createMemoryClient({
  common: mainnet,
  fork: {
    transport: http("https://1.rpc.thirdweb.com"),
    blockTag: "latest",
  },
  // Add custom tevm options here
});

// Use the custom client
const unsubscribe = await watchState({
  client,
  address: "0xContractAddress",
  onStateChange: (stateChange) => {
    // Process state changes...
  },
});

Supported contract patterns

The library has been extensively tested with diverse contract patterns:

  • Basic value types: Integers, booleans, addresses, bytes
  • Storage packing: Multiple variables packed in a single slot
  • Arrays: Fixed and dynamic arrays with index access
  • Mappings: Simple and nested mappings with various key types
  • Structs: Simple and nested struct types
  • Dynamic types: Bytes and string types
  • Proxies: Transparent proxy patterns with implementation analysis
  • Native transfers: ETH transfers between accounts
  • Contract creation: Tracking new contract deployments

How it works

The library combines several techniques to provide comprehensive state analysis:

  1. Transaction simulation: Uses TEVM to simulate transactions in a local EVM environment
  2. Debug tracing: Leverages debug_traceTransaction and debug_traceBlock for detailed state access
  3. Storage layout analysis: Parses contract storage layouts to map slots to variable names
  4. Key detection: Analyzes transaction input and execution traces to identify mapping keys and array indices
  5. Type-aware decoding: Converts raw storage values to appropriate JavaScript types based on variable definitions

License

MIT