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.
Here's a basic example of reading data from the same contract that emitted the event.
In this example, suppose the creator of an NFT contract forgot to include certain traits of an NFT in the Mint
event data.
ponder.on...
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.
name | description | Viem docs |
---|---|---|
readContract | Calls a read-only function on a contract. | readContract (opens in a new tab) |
multicall | Similar to readContract, but batches requests. | multicall (opens in a new tab) |
getBalance | Returns the balance of an address in wei. | getBalance (opens in a new tab) |
getBytecode | Retrieves the bytecode at an address. | getBytecode (opens in a new tab) |
getStorageAt | Returns 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 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.
export const config = {
/* ... */
contracts: [
{
name: "Blitmap",
network: "mainnet",
abi: "./abis/Blitmap.json",
address: "0x8d04...D3Ff63",
startBlock: 12439123,
},
],
};
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.
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,
},
],
};
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.
// 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);
// ...
});
// 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.
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",
});
});