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 (opens in a new tab) 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_getBytecode.

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 },
  });
});

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:Ownership", async ({ event, context }) => {
  const gradientData = await context.client.readContract({
    abi: context.contracts.ZorbNft.abi,
    address: context.contracts.ZorbNft.address,
    functionName: "gradientForAddress",
    args: [event.args.to],
  });
});

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;
  // ...
});

Client

The context.client object is a custom Viem Client (opens in a new tab).

Supported actions

namedescriptionViem docs
readContractReturns the result of a read-only function on a contract.readContract (opens in a new tab)
multicallSimilar to readContract, but batches requests.multicall (opens in a new tab)
getBalanceReturns the balance of an address in wei.getBalance (opens in a new tab)
getBytecodeReturns the bytecode at an address.getBytecode (opens in a new tab)
getStorageAtReturns the value from a storage slot at a given address.getStorageAt (opens in a new tab)

Block number

The blockNumber/blockTag RPC request parameter is automatically set to the block number of the current event (event.block.number). It's not possible to override this behavior.

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.

⚠️

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],
  });
});

More examples

Zorbs gradient data

Suppose we're building an application that stores the gradient metadata of each Zorb NFT (opens in a new tab). 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.from.to === 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,
    },
  });
});