Read contract data – Ponder
Skip to content

Read contract data

Call read-only functions directly

Sometimes, indexing function triggers (event logs, traces, etc.) do not contain all of the onchain data you need to build your application. It's often useful to call read-only contract functions, fetch transaction receipts, or simulate contract interactions.

Ponder natively supports this pattern through a custom Viem Client that includes performance & usability improvements specific to indexing.

Basic example

To read data from a contract, use context.client.readContract() and include the contract address and ABI from context.contracts.

ponder.config.ts
import { createConfig } from "ponder";
import { BlitmapAbi } from "./abis/Blitmap";
 
export default createConfig({
  chains: {
    mainnet: { id: 1, rpc: process.env.PONDER_RPC_URL_1 },
  },
  contracts: {
    Blitmap: {
      chain: "mainnet",
      abi: BlitmapAbi,
      address: "0x8d04...D3Ff63",
      startBlock: 12439123,
    },
  },
});

Client

The context.client object is a custom Viem Client that caches RPC responses.

src/index.ts
import { ponder } from "ponder:registry";
 
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const tokenUri = await context.client.readContract({
    abi: context.contracts.Blitmap.abi,
    address: context.contracts.Blitmap.address,
    method: "tokenUri",
    args: [event.args.tokenId],
  });
});

Supported actions

The context.client object supports most Viem actions.

namedescriptionViem docs
readContractReturns the result of a read-only function on a contract.readContract
multicallSimilar to readContract, but batches requests.multicall
simulateContractSimulates & validates a contract interaction.simulateContract
getBalanceReturns the balance of an address in wei.getBalance
getBytecodeReturns the bytecode at an address.getBytecode
getStorageAtReturns the value from a storage slot at a given address.getStorageAt
getBlockReturns information about a block at a block number, hash or tag.getBlock
getTransactionCountReturns the number of transactions an account has broadcast / sent.getTransactionCount
getBlockTransactionCountReturns the number of Transactions at a block number, hash or tag.getBlockTransactionCount
getTransactionReturns information about a transaction given a hash or block identifier.getTransaction
getTransactionReceiptReturns the transaction receipt given a transaction hash.getTransactionReceipt
getTransactionConfirmationsReturns the number of blocks passed (confirmations) since the transaction was processed on a block.getTransactionConfirmations
callAn Action for executing a new message call.call
estimateGasAn Action for estimating gas for a transaction.estimateGas
getFeeHistoryReturns a collection of historical gas information.getFeeHistory
getProofReturns the account and storage values of the specified account including the Merkle-proof.getProof
getEnsAddressGets address for ENS name.getEnsAddress
getEnsAvatarGets the avatar of an ENS name.getEnsAvatar
getEnsNameGets primary name for specified address.getEnsName
getEnsResolverGets resolver for ENS name.getEnsResolver
getEnsTextGets a text record for specified ENS name.getEnsText

Direct RPC requests

Use the context.client.request method to make direct RPC requests. This low-level approach can be useful for advanced RPC request patterns that are not supported by the actions above.

src/index.ts
import { ponder } from "ponder:registry";
 
ponder.on("ENS:NewOwner", async ({ event, context }) => {
  const traces = await context.client.request({ 
    method: 'debug_traceTransaction', 
    params: [event.transaction.hash, { tracer: "callTracer" }] 
  }); 
 
  // ...
});

Block number

By default, the blockNumber option is set to the block number of the current event (event.block.number).

src/index.ts
import { ponder } from "ponder:registry";
 
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const totalSupply = await context.client.readContract({
    abi: context.contracts.Blitmap.abi,
    address: context.contracts.Blitmap.address,
    functionName: "totalSupply",
    // This is set automatically, no need to include it yourself.
    // blockNumber: event.block.number,
  });
});

You can also specify a blockNumber to read data at a specific block height. It will still be cached.

src/index.ts
import { ponder } from "ponder:registry";
 
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const totalSupply = await context.client.readContract({
    abi: context.contracts.Blitmap.abi,
    address: context.contracts.Blitmap.address,
    functionName: "totalSupply",
    blockNumber: 15439123n, 
  });
});

Caching

Most RPC requests made using context.client are cached in the database. When an indexing function calls a method with a specific set of arguments for the first time, it will make an RPC request. Any subsequent calls to the same method with the same arguments will be served from the cache.

See the full list of cache-enabled RPC methods in the source code.

Contract addresses & ABIs

The context.contracts object contains each contract address and ABI you provide in ponder.config.ts.

Multiple chains

If a contract is configured to run on multiple chains, context.contracts contains the contract addresses for whichever chain the current event is from.

ponder.config.ts
import { createConfig } from "ponder";
import { UniswapV3FactoryAbi } from "./abis/UniswapV3Factory";
 
export default createConfig({
  chains: {
    mainnet: { id: 1, rpc: process.env.PONDER_RPC_URL_1 },
    base: { id: 8453, rpc: process.env.PONDER_RPC_URL_8453 },
  },
  contracts: {
    UniswapV3Factory: {
      abi: UniswapV3FactoryAbi,
      chain: { 
        mainnet: { 
          address: "0x1F98431c8aD98523631AE4a59f267346ea31F984", 
          startBlock: 12369621, 
        }, 
        base: { 
          address: "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", 
          startBlock: 1371680, 
        }, 
      }, 
    },
  },
});

Factory contracts

The context.contracts object does not include an address property for contracts that use factory(). To read data from the contract that emitted the current event, use event.log.address.

src/index.ts
import { ponder } from "ponder:registry";
 
ponder.on("SudoswapPool:Transfer", async ({ event, context }) => {
  const { SudoswapPool } = context.contracts;
  //      ^? { abi: [...] }
 
  const totalSupply = await context.client.readContract({
    abi: SudoswapPool.abi,
    address: event.log.address,
    functionName: "totalSupply",
  });
});

To call a factory contract child from an indexing function for a different contract, use your application logic to determine the correct address. For example, the address might come from event.args.

src/index.ts
import { ponder } from "ponder:registry";
 
ponder.on("LendingProtocol:RegisterPool", async ({ event, context }) => {
  const totalSupply = await context.client.readContract({
    abi: context.contracts.SudoswapPool.abi,
    address: event.args.pool,
    functionName: "totalSupply",
  });
});

Read a contract without indexing it

The context.contracts object only contains addresses & ABIs for the contracts in ponder.config.ts.

To read an external contract, import the ABI object directly and include the address manually. Ad-hoc requests like this are still cached and the block number will be set automatically.

ponder.config.ts
import { createConfig } from "ponder";
import { AaveTokenAbi } from "./abis/AaveToken";
 
export default createConfig({
  contracts: {
    AaveToken: {
      chain: "mainnet",
      abi: AaveTokenAbi,
      address: "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9",
      startBlock: 10926829,
    },
  },
});

More examples

Zorbs gradient data

Suppose we're building an application that stores the gradient metadata of each Zorb NFT. Here's a snippet from the contract.

ZorbNft.sol
contract ZorbNft is ERC721 {
 
    function mint() public {
        // ...
    }
 
    function gradientForAddress(address user) public pure returns (bytes[5] memory) { 
        return ColorLib.gradientForAddress(user); 
    } 
}

Every Zorb has a gradient, but the contract doesn't emit gradient data in any event logs. To read the gradient data for each new Zorb, we can call the gradientForAddress function.

import { ponder } from "ponder:registry";
import { zorbs } from "ponder:schema";
 
ponder.on("ZorbNft:Transfer", async ({ event, context }) => {
  if (event.args.from === ZERO_ADDRESS) {
    // If this is a mint, read gradient metadata from the contract.
    const gradientData = await context.client.readContract({
      abi: context.contracts.ZorbNft.abi,
      address: context.contracts.ZorbNft.address,
      functionName: "gradientForAddress",
      args: [event.args.to],
    });
 
    await context.db.insert(zorbs).values({
      id: event.args.tokenId,
      gradient: gradientData,
      ownerId: event.args.to,
    });
  } else {
    // If not a mint, just update ownership information.
    await context.db
      .update(zorbs, { id: event.args.tokenId })
      .set({ ownerId: event.args.to });
  }
});