Event Handlers
Registration
A handler is a function that receives blockchain data, processes it, and inserts it into the database. You can register handlers in the file defined in the handler
field in your config.yaml
file. By default this is src/EventHandlers.*
file.
- TypeScript
- JavaScript
- ReScript
import { <CONTRACT_NAME> } from "generated";
<CONTRACT_NAME>.<EVENT_NAME>.handler(async ({ event, context }) => {
// Your logic here
});
const { <CONTRACT_NAME> } = require("generated");
<CONTRACT_NAME>.<EVENT_NAME>.handler(async ({ event, context }) => {
// Your logic here
});
Handlers.<CONTRACT_NAME>.<EVENT_NAME>.handler(async ({ event, context }) => {
// Your logic here
});
The generated
module contains code and types based on config.yaml
and schema.graphql
files. Update it by running pnpm codegen
command whenever you change these files.
Basic Example
Here's a handler example for the NewGreeting
event. It belongs to the Greeter
contract from our beginners Greeter Tutorial:
- TypeScript
- JavaScript
- ReScript
import { Greeter, User } from "generated";
// Handler for the NewGreeting event
Greeter.NewGreeting.handler(async ({ event, context }) => {
const userId = event.params.user; // The id for the User entity
const latestGreeting = event.params.greeting; // The greeting string that was added
const currentUserEntity = await context.User.get(userId); // Optional user entity that may already exist
// Update or create a new User entity
const userEntity: User = currentUserEntity
? {
id: userId,
latestGreeting,
numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
greetings: [...currentUserEntity.greetings, latestGreeting],
}
: {
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
};
context.User.set(userEntity); // Set the User entity in the DB
});
const { Greeter } = require("generated");
// Handler for the NewGreeting event
Greeter.NewGreeting.handler(async ({ event, context }) => {
const userId = event.params.user; // The id for the User entity
const latestGreeting = event.params.greeting; // The greeting string that was added
const currentUserEntity = await context.User.get(userId); // Optional user entity that may already exist
// Update or create a new User entity
const userEntity = currentUserEntity
? {
id: userId,
latestGreeting,
numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
greetings: [...currentUserEntity.greetings, latestGreeting],
}
: {
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
};
context.User.set(userEntity); // Set the User entity in the DB
});
open Types
// Handler for the NewGreeting event
Handlers.Greeter.NewGreeting.handler(async ({event, context}) => {
let userId = event.params.user->Address.toString // The id for the User entity
let latestGreeting = event.params.greeting // The greeting string that was added
let maybeCurrentUserEntity = await context.user.get(userId) // Optional User entity that may already exist
// Update or create a new User entity
let userEntity: Entities.User.t = switch maybeCurrentUserEntity {
| Some(existingUserEntity) => {
id: userId,
latestGreeting,
numberOfGreetings: existingUserEntity.numberOfGreetings + 1,
greetings: existingUserEntity.greetings->Belt.Array.concat([latestGreeting]),
}
| None => {
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
}
}
context.user.set(userEntity) // Set the User entity in the DB
})
Advanced Use Cases
HyperIndex provides many features to help you build more powerful and efficient indexers. Read more about these on separate pages:
- Handle Factory Contracts with Dynamic Contract Registration (with nested factories support)
- Perform external calls to decide which contract address to register using Async Contract Register
- Index all ERC20 token transfers with Wildcard Indexing
- Use Topic Filtering to ignore irrelevant events
- With multiple filters for single event
- With different filters per network
- With filter by dynamicly registered contract addresses (eg Index all ERC20 transfers to/from your Contract)
- Access Contract State directly from handlers
- Perform external calls from handlers by following the IPFS Integration guide
- Optimise database access with Loaders
Event Object
Each handler receives an event
object containing details about the emitted event, including parameters and blockchain metadata.
Accessing Event Parameters
Event parameters are accessed via:
event.params.<PARAMETER_NAME>
Example usage:
const sender = event.params.sender;
const amount = event.params.amount;
Additional Event Information
The event object also contains additional metadata:
event.chainId
– Chain ID of the network emitting the event.event.srcAddress
– Contract address emitting the event.event.logIndex
– Index of the log within the block.event.block
– Block fields (By default:number
,timestamp
,hash
).event.transaction
– Transaction fields (eghash
,gasUsed
, etc. Empty by default).
All addresses returned in the event
object, such as event.transaction.from
,
event.transaction.to
, and event.srcAddress
, are EIP-55 checksummed.
Use .toLowerCase()
if you specifically need lowercase values.
Configure block and transaction fields with field_selection
in your config.yaml
file.
Example event type definition:
type Event<Params, TransactionFields, BlockFields> = {
params: Params;
chainId: 1 | 137;
srcAddress: string;
logIndex: number;
transaction: TransactionFields;
block: BlockFields;
};
Context Object
The handler context
provides methods to interact with entities stored in the database.
Retrieving Entities
Retrieve entities from the database using context.Entity.get
where Entity
is the name of the entity you want to retrieve, which is defined in your schema.graphql file.
await context.Entity.get(entityId);
It'll return Entity
object or undefined
if the entity doesn't exist.
Starting from envio@2.22.0
you can use context.Entity.getOrThrow
to conveniently throw an error if the entity doesn't exist:
const pool = await context.Pool.getOrThrow(poolId);
// Will throw: Entity 'Pool' with ID '...' is expected to exist.
// Or you can pass a custom message as a second argument:
const pool = await context.Pool.getOrThrow(
poolId,
`Pool with ID ${poolId} is expected.`
);
Or use context.Entity.getOrCreate
to automatically create an entity with default values if it doesn't exist:
const pool = await context.Pool.getOrCreate({
id: poolId,
totalValueLockedETH: 0n,
});
// Which is equivalent to:
let pool = await context.Pool.get(poolId);
if (!pool) {
pool = {
id: poolId,
totalValueLockedETH: 0n,
};
context.Pool.set(pool);
}
Modifying Entities
Use context.Entity.set
to create or update an entity:
context.Entity.set({
id: entityId,
...otherEntityFields,
});
Both context.Entity.set
and context.Entity.deleteUnsafe
methods use the In-Memory Storage under the hood and don't require await
in front of them.
Deleting Entities (Unsafe)
To delete an entity:
context.Entity.deleteUnsafe(entityId);
The deleteUnsafe
method is experimental and unsafe. You need to manually handle all entity references after deletion to maintain database consistency.
Updating Specific Entity Fields
Use the following approach to update specific fields in an existing entity:
- TypeScript
- JavaScript
- ReScript
const pool = await context.Pool.get(poolId);
if (pool) {
context.Pool.set({
...pool,
totalValueLockedETH: pool.totalValueLockedETH.plus(newDeposit),
});
}
const pool = await context.Pool.get(poolId);
if (pool) {
context.Pool.set({
...pool,
totalValueLockedETH: pool.totalValueLockedETH.plus(newDeposit),
});
}
let pool = await context.pool.get(poolId);
pool->Option.forEach(pool => {
context.pool.set({
...pool,
totalValueLockedETH: pool.totalValueLockedETH.plus(newDeposit),
});
});
context.log
The context object also provides a logger that you can use to log messages to the console. Compared to console.log
calls, these logs will be displayed on our Hosted Service runtime logs page.
Read more in the Logging Guide.
External Calls
Envio indexer runs using Node.js runtime. This means that you can use fetch
or any other library like viem
to perform external calls from your handlers.
Check out our IPFS Integration and Accessing Contract State guides for more information.
context.effect
(Experimental)
To ensure consistent and reliable data, all handlers are executed synchronously in the on-chain order. This means that external calls might easily blow up the processing time.
To avoid this, you can use Loaders together with Effect API to parallelize external calls and make the indexing process more efficient.
import { experimental_createEffect, S } from "envio";
import { ERC20 } from "generated";
// Define an effect that will be called from the handler.
const getMetadata = experimental_createEffect(
{
name: "getMetadata",
input: S.string,
output: {
description: S.string,
value: S.bigint,
},
},
({ input }) => {
const response = await fetch(`https://api.example.com/metadata/${input}`);
const data = await response.json();
return {
description: data.description,
value: data.value,
};
}
);
ERC20.Transfer.handlerWithLoader({
loader: async ({ event, context }) => {
// Load metadata for the token.
// This will be executed in parallel for all events in the batch.
// The call is automatically memoized, so you don't need to worry about duplicate requests.
const sender = await context.effect(getMetadata, event.params.from);
// Return the loaded data to the handler
return {
sender,
};
},
handler: async ({ event, context, loaderReturn }) => {
const { sender } = loaderReturn;
// Process the transfer with the pre-loaded data
},
});
Read more about the Effect API and Loaders in the dedicated guides.
Accessing config.yaml
Data in Handlers
You can access your indexer configuration within handlers using getConfigByChainId
:
- TypeScript
- JavaScript
- ReScript
import { getConfigByChainId } from "../generated/src/ConfigYAML.gen";
Greeter.NewGreeting.handler(async ({ event, context }) => {
const config = getConfigByChainId(event.chainId);
});
const { getConfigByChainId } = require("../generated/src/ConfigYAML.bs.js");
Greeter.NewGreeting.handler(async ({ event, context }) => {
const config = getConfigByChainId(event.chainId);
});
open Types;
Handlers.Greeter.NewGreeting.handler(async ({ event, context }) => {
let config = ConfigYAML.getConfigByChainId(event.chainId);
});
This exposes configuration data such as:
syncSource
,startBlock
,confirmedBlockThreshold
- Contract-specific data (
abi
,addresses
,events
)
Performance Considerations
For performance optimization and best practices, refer to:
These guides offer detailed recommendations on optimizing entity loading and indexing performance.