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.

Read from

Suppose we're building an application that stores the gradient metadata of each Zorb NFT.

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

The gradient data is not available in any of the events emitted by the ZorbNft contract, but we can work around this by calling the gradientForAddress function.

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

Supported actions

The context.client viem Client offers the following actions, all of which support caching.

When you use these actions from within indexing function code, the blockNumber option is automatically set to the block number of the event being processed (event.block.number). It's not currently possible to override this behavior.

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)

Read from a contract that's not indexed

To read data from a contract without indexing it, import the ABI and address

To read from a contract that is not present in the context.contracts object

The context.contracts object only contains contracts that you have added in ponder.config.ts. Sometimes, it's useful for read from a contract without indexing it's event logs. To

The context object passed to every indexing function exposes a client property, which is a read-only viem Public Client that has been modified to cache all RPC method calls.

You should always use context.client rather than a manually constructed client. If you feel tempted to create your own client, please open a GitHub issue or send a message to the chat. We'd like to understand and accommodate your workflow.

The context object also has a contracts property, which is an object containing contract addresses and ABIs from ponder.config.ts.

The context.contracts object contains a read-only viem contract instance (opens in a new tab) for each contract you define in ponder.config.ts. These contract instances expose each read-only function (state mutability "pure" or "view") present in the contract's ABI. They also cache contract read results, which speeds up indexing and avoids unnecessary RPC requests.

Example

In this example, the Blitmap:Mint event does not include the token URI of the newly minted NFT. To add the token URI to the indexed data, we can read data directly from the contract using the Blitmap.tokenURI view method.

ponder.config.ts
export const config = {
  /* ... */
  contracts: [
    {
      name: "Blitmap",
      network: "mainnet",
      abi: "./abis/Blitmap.json",
      address: "0x8d04...D3Ff63",
      startBlock: 12439123,
    },
  ],
};
src/index.ts
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const { Blitmap } = context.contracts;
 
  const tokenUri = await Blitmap.read.tokenURI(event.params.tokenId);
 
  const token = await context.models.Token.create({
    id: event.params.tokenId,
    data: { uri: tokenUri },
  });
  // { id: 7777, uri: "https://api.blitmap.com/v1/metadata/7777" }
});

Read contract data without syncing all events

When you add a contract in ponder.config.ts, Ponder fetches all event logs emitted by that contract. Sometimes, you only want to read data from a contract (you don't need its event logs).

To tell Ponder not to fetch event logs for a contract, set isLogEventSource: false in your config.

ponder.config.ts
export const config = {
  /* ... */
  contracts: [
    {
      name: "AaveToken",
      network: "mainnet",
      abi: "./abis/AaveToken.json",
      address: "0x7Fc6...2DDaE9",
      startBlock: 10926829,
    },
    {
      name: "AaveUsdPriceFeed",
      network: "mainnet",
      abi: "./abis/ChainlinkPriceFeed.json",
      address: "0x547a...19e8a9",
      isLogEventSource: false,
    },
  ],
};
src/index.ts
ponder.on("AaveToken:Mint", async ({ event, context }) => {
  const { AaveUsdPriceFeed } = context.contracts;
 
  const priceData = await AaveUsdPriceFeed.read.latestRoundData();
  const usdValue = priceData.answer * event.params.amount;
 
  // ...
});

Caching

To avoid unnecessary RPC requests and speed up indexing, Ponder caches all contract read results. When an indexing function that reads a contract runs for the first time, it will make an RPC request. But on subsequent hot reloads or redeployments, this data will be served from the cache.

To take advantage of caching, you must use context.contracts. Do not manually set up a viem Client.

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.params.tokenId);
  // ...
});
src/index.ts
// Do this instead. ✅ ✅ ✅
 
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const { Blitmap } = context.contracts;
 
  const tokenUri = await Blitmap.read.tokenURI(event.params.tokenId);
  // ...
});

Specify a block number

By default, contract reads use the eth_call RPC method with blockNumber set to the block number of the event being processed (event.block.number). You can read the contract at a different block number (e.g. the contract deployment block number or "latest") by passing the blockNumber or blockTag option, but this will disable caching.

src/index.ts
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const { Blitmap } = context.contracts;
 
  const { tokenId } = event.params;
 
  // Read at event.block.number, caching enabled ✅
  const latestTokenUri = await Blitmap.read.tokenURI(tokenId);
 
  // Read at 17226745, caching disabled ❌
  const historicalTokenUri = await Blitmap.read.tokenURI(tokenId, {
    blockNumber: 17226745,
  });
 
  // Read at "latest", caching disabled ❌
  const latestTokenUri = await Blitmap.read.tokenURI(tokenId, {
    blockTag: "latest",
  });
});