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
.
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,
},
},
});
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.
// 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);
});
// 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
name | description | Viem docs |
---|---|---|
readContract | Returns the result of a read-only function on a contract. | readContract |
multicall | Similar to readContract, but batches requests. | multicall |
getBalance | Returns the balance of an address in wei. | getBalance |
getBytecode | Returns the bytecode at an address. | getBytecode |
getStorageAt | Returns 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
).
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.
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.
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,
},
},
},
},
});
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
.
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
.
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.
import { createConfig } from "@ponder/core";
import { AaveTokenAbi } from "./abis/AaveToken";
export default createConfig({
contracts: {
AaveToken: {
network: "mainnet",
abi: AaveTokenAbi,
address: "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9",
startBlock: 10926829,
},
},
});
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.
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.
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,
},
});
});