Skip to main content

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:

  1. loader function
  2. handler function

Loader function

Loader functions are called via

<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 of context.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

<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 of context.User).

Handler functions can also provides the following functions per loaded entity, that can be used to interact with that entity:

  • set
  • delete

which can be used as follows

context.<entityName>.set(<entityObject>)

and

context.<EntityName>.delete()

Example of a Loader function for the NewGreeting event:

let { GreeterContract } = require("../generated/src/Handlers.bs.js");

GreeterContract.NewGreeting.loader((event, context) => {
context.User.load(event.params.user.toString());
});
  • 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:

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],
});
}
});
  • 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 the loader function, it will be available via greetingWithChanges.
  • 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.

let { getConfigByChainId } = require("../generated/src/ConfigYAML.bs.js");

GreeterContract.NewGreeting.handler((event, context) => {
let config = getConfigByChainId(event.chainId);
});

configYaml

  • syncSource - Source where the indexer is syncing from
  • startBlock - Block number from which the indexer is syncing
  • confirmedBlockThreshold - 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 contract
  • config.contracts.<ContractName>.addresses - An array of addresses of the contract
  • config.contracts.<ContractName>.events - An array of event names emitted by the contract