Documentation
Write indexing functions
Read contract data

Read contract data

Ideally, smart contracts emit event logs containing all the data you need to build your application. In practice, developers often forget to include certain event logs, or omit them as a gas optimization. In some cases, you can address these gaps by reading data directly from a contract.

Ponder natively supports this pattern by injecting a custom Viem Client into the indexing function context. This modified client automatically caches RPC requests and supports several RPC methods including eth_call, multicall, eth_getStorageAt, and eth_getCode.

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/core";
import { http } from "viem";
 
import { BlitmapAbi } from "./abis/Blitmap";
 
export default createConfig({
  networks: {
    mainnet: {
      chainId: 1,
      transport: http(process.env.PONDER_RPC_URL_1),
    },
  },
  contracts: {
    Blitmap: {
      network: "mainnet",
      abi: BlitmapAbi,
      address: "0x8d04...D3Ff63",
      startBlock: 12439123,
    },
  },
});
src/index.ts
import { ponder } from "@/generated";
 
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const { client } = context;
  //      ^? ReadonlyClient<"mainnet">
  const { Blitmap } = context.contracts;
  //      ^? {
  //           abi: [...]
  //           address: "0x8d04...D3Ff63",
  //         }
 
  // Fetch the URI for the newly minted token.
  const tokenUri = await client.readContract({
    abi: Blitmap.abi,
    address: Blitmap.address,
    functionName: "tokenURI",
    args: [event.args.tokenId],
  });
 
  // Insert a Token record, including the URI.
  const token = await context.db.Token.create({
    id: event.args.tokenId,
    data: { uri: tokenUri },
  });
});

Client

The context.client object is a custom Viem Client that caches requests by block number.

⚠️

Do not manually set up a viem Client. If context.client is not working for you, please open a GitHub issue or send a message to the chat. We'd like to understand and accommodate your workflow.

src/index.ts
// Don't do this! ❌ ❌ ❌
import { createPublicClient, getContract, http } from "viem";
 
const publicClient = createPublicClient({
  transport: http("https://eth-mainnet.g.alchemy.com/v2/..."),
});
 
const Blitmap = getContract({
  address: "0x8d04...D3Ff63",
  abi: blitmapAbi,
  publicClient,
});
 
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const tokenUri = await Blitmap.read.tokenURI(event.args.tokenId);
});
src/index.ts
// Do this instead. ✅ ✅ ✅
 
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],
  });
});

Transport

The custom client uses the transport you specify in ponder.config.ts.

Supported actions

namedescriptionViem docs
readContractReturns the result of a read-only function on a contract.readContract
multicallSimilar to readContract, but batches requests.multicall
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

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 "@/generated";
 
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 youself.
    // 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 "@/generated";
 
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,
  });
});
💡

The blockTag option is not supported by custom client actions.

Caching

To speed up indexing and avoid unnecessary RPC requests, all method calls are cached. 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.

Contract addresses & ABIs

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

Multiple networks

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

It's not currently possible to call a contract that's on a different network than the current event. If you need this feature, please open an issue or send a message to the chat.

ponder.config.ts
import { createConfig } from "@ponder/core";
import { http } from "viem";
 
import { UniswapV3FactoryAbi } from "./abis/UniswapV3Factory";
 
export default createConfig({
  networks: {
    mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
    base: { chainId: 8453, transport: http(process.env.PONDER_RPC_URL_8453) },
  },
  contracts: {
    UniswapV3Factory: {
      abi: UniswapV3FactoryAbi,
      network: {
        mainnet: {
          address: "0x1F98431c8aD98523631AE4a59f267346ea31F984",
          startBlock: 12369621,
        },
        base: {
          address: "0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
          startBlock: 1371680,
        },
      },
    },
  },
});
src/index.ts
import { ponder } from "@/generated";
 
ponder.on("UniswapV3Factory:FeeAmountEnabled", async ({ event, context }) => {
  const tickSpacing = await context.client.readContract({
    abi: context.contracts.UniswapV3Factory.abi,
    address: context.contracts.UniswapV3Factory.address,
    functionName: "feeAmountTickSpacing",
    args: [event.args.fee],
  });
});

Factory contracts

Contracts that are created by a factory have a dynamic address, so the context.contracts object does not have an address property. To read data from the contract that emitted the current event, use event.log.address.

src/index.ts
import { ponder } from "@/generated";
 
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 to enter. For example, the address might come from event.args.

src/index.ts
import { ponder } from "@/generated";
 
ponder.on("FancyLendingProtocol: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 from a contract without syncing & indexing event logs from it, import the ABI object directly into an indexing function file and include the address manually. One-off requests like this are still cached and the blockNumber is set automatically.

ponder.config.ts
import { createConfig } from "@ponder/core";
 
import { AaveTokenAbi } from "./abis/AaveToken";
 
export default createConfig({
  contracts: {
    AaveToken: {
      network: "mainnet",
      abi: AaveTokenAbi,
      address: "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9",
      startBlock: 10926829,
    },
  },
});
src/index.ts
import { ponder } from "@/generated";
 
import { ChainlinkPriceFeedAbi } from "../abis/ChainlinkPriceFeed";
 
ponder.on("AaveToken:Mint", async ({ event, context }) => {
  const priceData = await context.client.readContract({
    abi: ChainlinkPriceFeedAbi,
    address: "0x547a514d5e3769680Ce22B2361c10Ea13619e8a9",
    functionName: "latestRoundData",
  });
 
  const usdValue = priceData.answer * event.args.amount;
  // ...
});

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 (it only emits events required by ERC721). The gradient data for a given Zorb can, however, be accessed using the gradientForAddress function.

src/index.ts
import { ponder } from "@/generated";
 
ponder.on("ZorbNft:Transfer", async ({ event, context }) => {
  // If this is a mint, read gradient metadata from the contract.
  if (event.args.from === ZERO_ADDRESS) {
    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.Zorb.create({
      id: event.args.tokenId,
      data: {
        gradient: gradientData,
        ownerId: event.args.to,
      },
    });
 
    return;
  }
 
  // If not a mint, just update ownership information.
  await context.db.Zorb.update({
    id: event.args.tokenId,
    data: {
      ownerId: event.args.to,
    },
  });
});