Skip to main content
Version: v2

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 that you want to process requires that a handler be registered. These functions are asynchronous.

Note: Two other functions, handlerWithLoader and contractRegister, are also sometimes used but not required for most indexers. Both functions are explored in the Advanced section. handlerWithLoader is used to optimize the loading of entities from the database, and accessing one-to-many relationships on entities, contractRegister is used for registering addresses dynamically from a factory contract.

Handler Function

Handler functions are called via:

import { <CONTRACT_NAME> } from "generated";

<CONTRACT_NAME>.<EVENT_NAME>.handler(async ({ event, context }) => {
// Your logic
});

Event

Event Params

Handler functions are used to modify the entities that have been loaded by the loader function and 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.<PARAMETER_NAME>.

Additional Event Information

Additional event information can also be accessed via event.<EVENT_PROPERTY>.

Below is an example type of an event:

type Event<Params, TransactionFields, BlockFields> = {
params: Params;
chainId: number;
srcAddress: string;
logIndex: number;
transaction: TransactionFields;
block: BlockFields;
};

Note that the fields block and transaction type will depend on configured values in field_selection of your config.yaml.

For instance, if you add the following to your config.yaml:

field_selection:
transaction_fields:
- "hash"
- "transactionIndex"
- "gasUsed"
block_fields:
- "parentHash"

You can expect to get:

type TransactionFields = {
hash: string;
transactionIndex: number;
gasUsed: bigint;
};

type AdditionalBlockFields = {
parentHash: string;
};

type BlockFields = {
// number, timestamp, and hash are always available by default
number: number;
timestamp: number;
hash: string;
} & AdditionalBlockFields;

It is recommended to add # yaml-language-server: $schema=./node_modules/envio/evm.schema.json to the top of your config.yaml to help with editor autocompletion.

Context

In your handler functions, you can access entities in the database via the asynchronous get function as shown below:

await context.<ENTITY_NAME>.get(<ID>);

The context also contains some logging functions that include the context automatically. You can also use console.log.

Dev note: 📢 For indexers built using ReScript, use a lowercase for the first letter of entityName when accessing it via context (i.e., context.user instead of context.User).

In addition to the get function, which is a read-only action, two write actions are provided in the context:

  • set
  • deleteUnsafe

Used as follows:

context.<ENTITY_NAME>.set(<ENTITY_OBJECT>);

and

context.<ENTITY_NAME>.deleteUnsafe(<ENTITY_OBJECT>.id);

The set method is used to either create an entity or to update an existing entity with the values defined in the entityObject. The deleteUnsafe method is an experimental feature that allows you to remove an entity with a particular ID from the database and indexer memory. This is an unsafe function since there may be unexpected repercussions when a deleted entity is linked to another entity. It is your responsibility to delete/fix these references manually.

Example of Registering a Handler Function for the NewGreeting Event

let { Greeter } = require("generated");

Greeter.NewGreeting.handler(async ({ event, context }) => {
let existingUser = await 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.greetings, event.params.greeting],
});
} else {
context.User.set({
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
});
}
});
  • This context is the gateway by which the user can interact with the indexer and the underlying database.
  • The user can then retrieve and modify this entity and subsequently 'set' the User entity in the database.
  • The user has access to a User 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 show how to access the config.yaml data in the handler and the available data:

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

Greeter.NewGreeting.handler(async ({ 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.<CONTRACT_NAME>.abi - ABI of the contract
  • config.contracts.<CONTRACT_NAME>.addresses - An array of addresses of the contract
  • config.contracts.<CONTRACT_NAME>.events - An array of event names emitted by the contract