Skip to main content
To index contracts on a Virtual Environment, run your own Graph Node and point it at the Virtual Environment’s Public RPC URL. Graph Node reads the Virtual Environment over standard JSON-RPC. Blocks before the fork come from the parent network, and later blocks contain your Virtual Environment transactions. New transactions are indexed within a few seconds, because Graph Node polls the RPC for new blocks every second (ETHEREUM_POLLING_INTERVAL, default 1000 ms). Common setups: This guide was tested with Graph CLI 0.98.1 and Graph Node 0.45.0. The examples index DemoToken, a minimal ERC-20 that emits Transfer(address indexed from, address indexed to, uint256 value), deployed on a Virtual Environment forked from Ethereum mainnet.

Before you begin

  • Node.js 20.18.1 or later (required by Graph CLI) and npm
  • Docker with Docker Compose
  • A Virtual Environment and its Public RPC URL, listed in the Integrate Virtual RPC card on the Virtual Environment’s Home tab (see the dashboard reference)
  • A contract deployed on the Virtual Environment (see Deploy and verify contracts), with:
    • its address
    • its ABI as a JSON file
    • the block number of its deployment transaction:
The subgraph indexes the logs emitted at the contract address on the Virtual Environment, so the contract must exist there: deployed on the Virtual Environment, or deployed on the parent network before the fork. Adding the contract in the Tenderly dashboard doesn’t change what the subgraph indexes.

Set up Graph Node and the subgraph

1

Install the Graph CLI

2

Scaffold the subgraph from your contract

graph init creates the demo-token directory with subgraph.yaml, schema.graphql, an event mapping in src/demo-token.ts, and a docker-compose.yml that runs Graph Node, IPFS, and Postgres.In subgraph.yaml, change indexerHints.prune from auto to never. Graph Node needs the full entity history to roll back reverted blocks.
subgraph.yaml
3

Point Graph Node at the Virtual Environment

In the generated docker-compose.yml, edit the environment of the graph-node service:
  • Set ethereum to <network>:<Public RPC URL>. The network name before the first colon must match network in subgraph.yaml.
  • Add ETHEREUM_REORG_THRESHOLD: "1" so Graph Node can roll back reverted blocks.
  • Add GRAPH_NODE_GENESIS_VALIDATION_ENABLED: "true" so Graph Node refuses an RPC URL from a different Virtual Environment instead of stalling.
docker-compose.yml (graph-node service)
Leave the ipfs and postgres services as generated. Graph Node only reads chain data, so use the Public RPC rather than the Admin RPC.
4

Start Graph Node

Graph Node is ready when the log shows Starting GraphQL HTTP server at: http://localhost:8000.
5

Create and deploy the subgraph

graph deploy builds the subgraph, uploads it to IPFS, and prints the query URL:
The generated package.json runs the same commands as npm run create-local and npm run deploy-local. Run graph codegen again after you edit schema.graphql or the ABI.
6

Query the subgraph

Check indexing status

Query the indexing status API on port 8030:
latestBlock is the last block the subgraph processed and chainHeadBlock is the Virtual Environment’s latest block. Compare the two instead of relying on health. In every failure case on this page, the subgraph keeps reporting healthy while latestBlock stops advancing. To see the requests Graph Node sends, open the JSON-RPC Calls tab of the Virtual Environment. Set the Status filter to Failed to find methods the Virtual Environment rejects, such as trace_filter from call handlers. Expand a row to see the request parameters and the error response.

Run the subgraph with gnd instead of Docker

gnd (Graph Node Dev) runs Graph Node as a native binary, needs no local IPFS node, and deploys the subgraph from its build directory without graph create or graph deploy. It stores entities in Postgres; the postgres service from the generated docker-compose.yml works.
graph node install installs gnd to ~/.local/bin; add that directory to your PATH. gnd dev serves the subgraph at http://localhost:8000/subgraphs/name/subgraph-0. Add --watch to redeploy whenever the build directory changes.

How does Graph Node handle a Virtual Environment revert?

Graph Node treats a revert as a chain reorganization. Once the Virtual Environment’s latest block number passes the last block the subgraph indexed, Graph Node rolls back the entities from the removed blocks and indexes the replacement blocks. Until then, Graph Node ignores the shorter chain (the log shows Provider went backwards - ignoring this latest block) and keeps serving entities from the reverted blocks. After a revert from the dashboard or with evm_revert (see Revert Virtual Environment state), mine past the old latest block: send transactions, click Mine in the dashboard, or call evm_mine over the Admin RPC. The rollback depends on the two settings from the setup:

Index a contract that already exists on the parent network

To include a contract’s history from before the fork, set startBlock to a block before the fork block. Graph Node reads those blocks from the parent network through the Virtual Environment RPC, then continues with the Virtual Environment’s blocks, so the subgraph contains both the parent network’s events and the events from your Virtual Environment transactions. Graph Node scans every block from startBlock onward through the RPC and sizes each eth_getLogs range to the contract’s event density, so a busy contract is scanned a few blocks per request. Backfilling 1,000 blocks of USDC history (about 97,000 Transfer events) took about 7 minutes with the Docker setup on this page. Keep startBlock close to the fork block unless you need the full history. Contract calls in mappings (ethereum.call, for example Token.bind(address).balanceOf(holder)) work on both sides of the fork. Graph Node sends eth_call at the event’s block hash, and the Virtual Environment answers calls for pre-fork blocks with the parent network’s state. Admin RPC cheatcodes that write state directly, such as tenderly_setBalance and tenderly_setErc20Balance, emit no events, so event handlers don’t record them. To fund wallets with ERC-20 tokens in a way the subgraph records, use tenderly_addErc20Balance, which emits a synthetic Transfer event for each funded wallet. Graph Node indexes it like any other Transfer.

Which Virtual Environment modes index parent-network events?

Only Network Mirror Mode adds the parent network’s new blocks, with their logs, to a Virtual Environment. In the other modes, a subgraph sees only the events from your own transactions after the fork.

Switch to a different Virtual Environment

Reset Graph Node’s data before you point it at another Virtual Environment. Graph Node stores indexed blocks per network name and, by default, doesn’t check whether the RPC behind that name changed. With the old database, the subgraph points at blocks that the new Virtual Environment doesn’t have. Graph Node retries Ethereum node did not find block <hash> and keeps serving the old entities while reporting synced: true.
Then update ethereum in docker-compose.yml and the contract address and startBlock in subgraph.yaml, and run docker compose up -d, graph create, and graph deploy again. Keep network set to the parent network’s name (mainnet, not a custom name like virtual_mainnet), so the same subgraph.yaml works against any Virtual Environment forked from that network and against the network itself. With GRAPH_NODE_GENESIS_VALIDATION_ENABLED: "true", Graph Node detects the switch when the two Virtual Environments have different chain IDs and refuses the new RPC with Genesis hash validation failed on provider 'mainnet-rpc-0': net version mismatch on chain 'mainnet'; expected '73574', found '73575'. Virtual Environments forked from the same network share its genesis hash, so a switch between two with the same chain ID passes validation.

Index with event handlers

Index the events your contract emits with eventHandlers. Call handlers (callHandlers) and block handlers with a call filter read traces through the trace_filter RPC method, which Virtual Environment RPCs don’t serve. A subgraph that uses them stays at startBlock - 1, and Graph Node logs Trying again after trace_filter RPC call ... failed with Access forbidden by access rules, try Admin RPC URL from the Public RPC or not supported from the Admin RPC. On public networks, call handlers work with Graph Node on Node RPC.

Troubleshooting