Skip to content

Read contract state

Use eth_call to read USDC balanceOf at an explicit Ethereum mainnet block.

Read a token balance, pool reserve, or any other view function without sending a transaction. This guide reads the USDC balance of the Uniswap V2 USDC/WETH pair at block 18000000 with eth_call, then pins several reads to one block so their results describe the same chain state.

Prerequisites

  • A GraphOps RPC API key.
  • Ethereum mainnet endpoint access.
  • A contract address, ABI or encoded call data, and a block reference.

Request

The data field is balanceOf(address) encoded with selector 0x70a08231 and the target address left-padded to 32 bytes.

curl "https://rpc.graphops.xyz/v1/evm/1" \
  -H "Authorization: Bearer <GRAPHOPS_API_KEY>" \
  -H "Content-Type: application/json" \
  --data '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_call",
    "params": [
      {
        "to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "data": "0x70a08231000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc"
      },
      "0x112a880"
    ]
  }'

Response

{  "jsonrpc": "2.0",  "id": 1,Matches the id you sent  "result": "0x0000000000000000000000000000000000000000000000000000197016bc99ff"uint256 balance: 27,969,208,490,495 base units, 27,969,208.490495 USDC}
The USDC balance of the Uniswap V2 USDC/WETH pair at block 18,000,000. eth_call returns ABI-encoded bytes; your client or ABI decodes them.

USDC has 6 decimals, so divide the decoded integer by 1,000,000. Client libraries such as viem, ethers, and web3.py decode the bytes for you and return the integer.

Read several values at one block

Separate latest reads can land on different blocks as the chain advances. When results must agree, such as two pool balances used to compute a price, resolve one block first and pass its number to every read.

Resolve one block, then pass its number to every read. Both balances describe block N even after latest moves on, so the pool price you compute from them is consistent.

This viem example resolves the finalized block, then reads the pair's USDC and WETH balances at that block:

import { createPublicClient, erc20Abi, http } from "viem";
import { mainnet } from "viem/chains";

const client = createPublicClient({
  chain: mainnet,
  transport: http("https://rpc.graphops.xyz/v1/evm/1", {
    fetchOptions: {
      headers: {
        Authorization: "Bearer <GRAPHOPS_API_KEY>",
      },
    },
  }),
});

const pair = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
const block = await client.getBlock({ blockTag: "finalized" });

const [usdc, weth] = await Promise.all(
  (
    [
      "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    ] as const
  ).map((address) =>
    client.readContract({
      address,
      abi: erc20Abi,
      functionName: "balanceOf",
      args: [pair],
      blockNumber: block.number,
    }),
  ),
);

console.log({ block: block.number, usdc, weth });

Use finalized when the result must not change after a reorganization, safe for user-visible reads that can trail the head slightly, or a block number you stored earlier to repeat a read. See Consistency and finality for how the tags differ.

Interpret errors

  • A revert is a contract execution result, not a transport failure. The JSON-RPC error object carries the revert data when the contract provides it, and retrying the same call at the same block reverts again.
  • Retry only when the HTTP status or JSON-RPC error indicates a transport or server failure. See Retries and reorgs.
  • A historical block may need archive access on your network. See Archive reads.

Next steps