Writing Event Handlers
Once the configuration and schema files are in place, run
envio codegen
in the project directory to generate the functions you will use in your handlers.
Each event requires two functions to be registered:
loader
functionhandler
function
Loader function
Loader functions are called via
- Javascript
- Typescript
- Rescript
<ContractName>Contract.<EventName>.loader
<ContractName>Contract.< EventName >.loader;
<ContractName>Contract.<EventName>.loader
Loader functions are used to load the specific entities (defined in the schema.graphql
) that should be modified by the event.
Entities with specific IDs can be loaded via context.<EntityName>.<label>Load(<id>)
.
If no labels has been defined in the config.yaml
file, then entities can be loaded via context.<EntityName>.load(<id>)
. You can call this .load
function to load as many entities as you want to be available in your handler.
Dev note: 📢 For indexers built using
ReScript
, use a lower case for first letter of entityName when accessing it via context (i.e.context.user
instead ofcontext.User
).
A single event can be used to load single or multiple entities in the loader (and modify/create as many entities as you want in the handler).
Note: that if no required_entities
are set for the event, then all entities can be used in the loader for that event. However if it is set, only entities in the required_entities
can be loaded for that event.
labels
and arrayLabels
To make your code more robust and explicit you can use the advanced features here.
Handler function
Handler functions are called via
- Javascript
- Typescript
- Rescript
<ContractName>Contract.<EventName>.handler
<ContractName>Contract_ < EventName > _handler;
<ContractName>Contract.<EventName>.handler
Event
Event Params
Handler functions are used to modify the entities which have been loaded by the loader function, and thus should contain all the required logic for updating entities with the raw data emitted by the event.
All of the parameters emitted in each event are accessible via event.params.<parameterName>
.
Raw Event Information
Additional raw event information can also be accessed via event.<rawInfo>
.
Below is a list of raw event information that can be accessed:
blockHash
blockNumber
blockTimestamp
chainId
eventId
eventType
logIndex
srcAddress
transactionHash
transactionIndex
txOrigin
(this is an optional parameter that is only present for events fetched via HyperSync, not via RPC Sync)
Context
Handler functions can access the loaded entities that were loaded in the handler via
context.<EntityName>.load(<id>)
they can access these loaded entities via
context.<EntityName>.get(<id>)
Dev note: 📢 For indexers built using
ReScript
, use a lower case for first letter of entityName when accessing it via context (i.e.context.user
instead ofcontext.User
).
Handler functions also provide the following functions per entity, that can be used to interact with an instance of that entity:
- set
- deleteUnsafe
Used as follows
context.<entityName>.set(<entityObject>)
and
context.<EntityName>.deleteUnsafe(<entityObject>.id)
The set function is used to either create an entity or to update an existing entity with the values defined in the entityObject
.
The deleteUnsafe function is an experimental feature that allows you to remove an entity with a particular ID from the database and indexer memory. This is un unsafe
function,
since there may be unexpected repercussions when a deleted entity is linked to another entity. See here for more information regarding linked entities. It is your responsibility to delete/fix these references manually since a 'safe' delete function hasn't been created yet.
Example of a Loader function for the NewGreeting
event:
- Javascript
- Typescript
- Rescript
let { GreeterContract } = require("generated/src/Handlers.bs.js");
GreeterContract.NewGreeting.loader(({ event, context }) => {
context.User.load(event.params.user.toString());
});
import { UserEntity, GreeterContract } from "generated";
GreeterContract.NewGreeting.loader(({ event, context }) => {
context.User.load(event.params.user.toString());
});
open Types
Handlers.GreeterContract.NewGreeting.loader(({ event, context }) => {
context.user.load(event.params.user->Ethers.ethAddressToString)
})
- Within the function that is being registered, the user must define the criteria for loading the greeting entity.
- This is made available to the user through the load entity context defined as
contextUpdator
. - In the case of the above example, we load a
User
entity that corresponds to the id passed from the event.
Example of registering a Handler function for the NewGreeting
event:
- Javascript
- Typescript
- Rescript
let { GreeterContract } = require("../generated/src/Handlers.bs.js");
GreeterContract.NewGreeting.handler(({ event, context }) => {
let existingUser = context.User.get(event.params.user.toString());
if (existingUser !== undefined) {
context.User.set({
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: existingUser.numberOfGreetings + 1,
greetings: [...existingUser.numberOfGreetings, event.params.greeting],
});
} else {
context.User.set({
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
});
}
});
import { GreeterContract } from "../generated/src/Handlers.gen";
import { userEntity } from "../generated/src/Types.gen";
GreeterContract.NewGreeting.handler(({ event, context }) => {
(({ event, context }) => {
let currentUser = context.User.get(event.params.user.toString());
if (currentUser !== undefined) {
let userObject: userEntity = {
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: currentUser.numberOfGreetings + 1,
greetings: [...existingUser.numberOfGreetings, event.params.greeting],
};
context.User.set(userObject);
} else {
let userObject: userEntity = {
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
};
context.User.set(userObject);
}
});
open Types
Handlers.GreeterContract.NewGreeting.handler(({ event, context }) => {
let currentUserOpt = context.user.get(event.params.user->Ethers.ethAddressToString)
switch currentUserOpt {
| Some(existingUser) => {
let userObject: userEntity = {
id: event.params.user->Ethers.ethAddressToString,
latestGreeting: event.params.greeting,
numberOfGreetings: existingUser.numberOfGreetings + 1,
greetings: existingUser.greetings->Belt.Array.concat([event.params.greeting]),
}
context.user.set(userObject)
}
| None =>
let userObject: userEntity = {
id: event.params.user->Ethers.ethAddressToString,
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
}
context.user.set(userObject)
}
})
- Once the user has defined their
loader
function, they are then able to retrieve the loaded entity information. - In the above example, if a
User
entity is found matching the load criteria in theloader
function, it will be available viagreetingWithChanges
. - This is made available to the user through the handler context defined simply as
context
. - This
context
is the gateway by which the user can interact with the indexer and the underlying database. - The user can then modify this retrieved entity and subsequently update the
User
entity in the database. - This is done via the
context
using the function (context.User.set(userObject)
). - The user has access to a
userEntity
type that has all the fields defined in the schema.
Config data in the handler
We expose the config.yaml
data in the handler via getConfigByChainId
. The below code snippets shows how to access the config.yaml
data in the handler and the available data.
- Javascript
- Typescript
- Rescript
let { getConfigByChainId } = require("../generated/src/ConfigYAML.bs.js");
GreeterContract.NewGreeting.handler(({ event, context }) => {
let config = getConfigByChainId(event.chainId);
});
import { getConfigByChainId } from "../generated/src/ConfigYAML.gen";
GreeterContract_NewGreeting_handler(({ event, context }) => {
let config = getConfigByChainId(event.chainId);
});
open Types
Handlers.GreeterContract.NewGreeting.handler(({ event, context }) => {
let config = ConfigYAML.getConfigByChainId(event.chainId)
})
configYaml
syncSource
- Source where the indexer is syncing fromstartBlock
- Block number from which the indexer is syncingconfirmedBlockThreshold
- Number of blocks to wait before a block is considered confirmed (relevant to reorgs)contracts
- An object of contract data where the key is the contract name
contracts
config.contracts.<ContractName>.abi
- ABI of the contractconfig.contracts.<ContractName>.addresses
- An array of addresses of the contractconfig.contracts.<ContractName>.events
- An array of event names emitted by the contract