Indexing function API
Indexing functions are user-defined functions that process blockchain data. They can be registered within any .ts
file inside the src/
directory. There are two kinds of events: log events and the "setup"
event.
Registration
To register an indexing function, use the .on()
method of the ponder
object exported from "@/generated"
.
Ponder uses
Values returned by indexing functions are ignored.
import { ponder } from "@/generated";
ponder.on("ContractName:EventName", async ({ event, context }) => {
const { args, log, block, transaction } = event;
const { db, network, client, contracts } = context;
// ...
});
Event
The event
argument passed to each indexing function contains raw event data.
type Event = {
name: string;
args: Args;
log: Log;
block: Block;
transaction: Transaction;
};
Event arguments
The event.args
object contains event log arguments decoded using Viem's decodeEventLog.
/** Sample `args` type for an ERC20 Transfer event. */
type Args = {
from: `0x${string}`;
to: `0x${string}`;
value: bigint;
};
Block, transaction, and log
The event.block
, event.transaction
, and event.log
objects contain raw blockchain data.
/** The block containing the transaction that emitted the log being processed. */
type Block = {
/** Base fee per gas */
baseFeePerGas: bigint | null;
/** "Extra data" field of this block */
extraData: `0x${string}`;
/** Maximum gas allowed in this block */
gasLimit: bigint;
/** Total used gas by all transactions in this block */
gasUsed: bigint;
/** Block hash */
hash: `0x${string}`;
/** Logs bloom filter */
logsBloom: `0x${string}`;
/** Address that received this block’s mining rewards */
miner: `0x${string}`;
/** Block number */
number: bigint;
/** Parent block hash */
parentHash: `0x${string}`;
/** Root of the this block’s receipts trie */
receiptsRoot: `0x${string}`;
/** Size of this block in bytes */
size: bigint;
/** Root of this block’s final state trie */
stateRoot: `0x${string}`;
/** Unix timestamp of when this block was collated */
timestamp: bigint;
/** Total difficulty of the chain until this block */
totalDifficulty: bigint | null;
/** Root of this block’s transaction trie */
transactionsRoot: `0x${string}`;
};
/** The transaction that emitted the log being processed. */
type Transaction = {
/** Hash of block containing this transaction */
blockHash: `0x${string}`;
/** Number of block containing this transaction */
blockNumber: bigint;
/** Transaction sender */
from: `0x${string}`;
/** Gas provided for transaction execution */
gas: bigint;
/** Base fee per gas. */
gasPrice?: bigint | undefined;
/** Hash of this transaction */
hash: `0x${string}`;
/** Contract code or a hashed method call */
input: `0x${string}`;
/** Total fee per gas in wei (gasPrice/baseFeePerGas + maxPriorityFeePerGas). */
maxFeePerGas?: bigint | undefined;
/** Max priority fee per gas (in wei). */
maxPriorityFeePerGas?: bigint | undefined;
/** Unique number identifying this transaction */
nonce: number;
/** Transaction recipient or `null` if deploying a contract */
to: `0x${string}` | null;
/** Index of this transaction in the block */
transactionIndex: number;
/** Value in wei sent with this transaction */
value: bigint;
};
/** The log being processed. */
type Log = {
/** Globally unique identifier for this log (`${blockHash}-${logIndex}`). */
id: string;
/** The address from which this log originated */
address: `0x${string}`;
/** Hash of block containing this log */
blockHash: `0x${string}`;
/** Number of block containing this log */
blockNumber: bigint;
/** Contains the non-indexed arguments of the log */
data: `0x${string}`;
/** Index of this log within its block */
logIndex: number;
/** `true` if this log has been removed in a chain reorganization */
removed: boolean;
/** List of order-dependent topics */
topics: [`0x${string}`, ...`0x${string}`[]] | [];
/** Hash of the transaction that created this log */
transactionHash: `0x${string}`;
/** Index of the transaction that created this log */
transactionIndex: number;
};
Context
The context
argument passed to each indexing function contains database model objects and helper objects based on your config.
At runtime, the indexing engine uses a different context
object depending on the network the current event was emitted on. The TypeScript types for the context
object reflect this by creating a union of possible types for context.network
and context.contracts
.
type Context = {
db: Record<string, DatabaseModel>;
network: { name: string; chainId: number };
client: ReadOnlyClient;
contracts: Record<
string,
{
abi: Abi;
address?: `0x${string}`;
startBlock?: number;
endBlock?: number;
}
>;
};
Database
The context.db
object contains ORM-style models that can be used to create, read, update, and delete database records. See Create & update records for a detailed overview.
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Person: p.createTable({
id: p.string(),
age: p.int().optional(),
}),
Dog: p.createTable({
id: p.bigint(),
ownerId: p.string().references("Person.id"),
}),
}));
import { ponder } from "@/generated";
ponder.on("UniswapV3Factory:Ownership", async ({ event, context }) => {
const { Person } = context.db;
// ^? Model<{ id: string; age?: number }>;
const { Dog } = context.db;
// ^? Model<{ id: bigint; ownerId: string; }>;
});
Network
The context.network
object contains information about the network that the current event was emitted on. The name and chain ID properties are strictly typed as a union of possible values based on the networks that the contract is configured to run on.
ponder.on("UniswapV3Factory:Ownership", async ({ event, context }) => {
context.network;
// ^? { name: "mainnet", chainId 1 } | { name: "base", chainId 8453 }
if (context.network.name === "mainnet") {
// Do mainnet-specific stuff!
}
});
Client
See the Read contract data guide for more details.
Contracts
See the Read contract data guide for more details.
"setup"
event
You can also define a setup function for each contract that runs before indexing begins.
- The indexing function does not receive an
event
argument, onlycontext
. - If you read from contracts in a
"setup"
indexing function, theblockNumber
for the request is set to the contract'sstartBlock
.
For example, you might have a singleton World
record that gets updated in indexing functions. Without the "setup"
event, you would need to upsert the record in each indexing function that attempts to use it, which is clunky and inefficient.
import { ponder } from "@/generated";
ponder.on("FunGame:NewPlayer", async ({ context }) => {
await context.db.World.upsert({
id: 1,
create: { playerCount: 0 },
update: ({ current }) => ({
...current,
playerCount: current.playerCount + 1,
}),
});
});
import { ponder } from "@/generated";
ponder.on("FunGame:setup", async ({ context }) => {
await context.db.World.create({
id: 1,
data: { playerCount: 0 },
});
});
ponder.on("FunGame:NewPlayer", async ({ context }) => {
await context.db.World.update({
id: 1,
data: ({ current }) => ({
...current,
playerCount: current.playerCount + 1,
}),
});
});