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:
- Typescript
- Javascript
- Rescript
import { <CONTRACT_NAME> } from "generated";
<CONTRACT_NAME>.<EVENT_NAME>.handler(async ({ event, context }) => {
// Your logic
});
const { <CONTRACT_NAME> } = require("generated");
<CONTRACT_NAME>.<EVENT_NAME>.handler(async ({ event, context }) => {
// Your logic
});
Handlers.<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 ofentityName
when accessing it via context (i.e.,context.user
instead ofcontext.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
- Javascript
- Typescript
- Rescript
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],
});
}
});
import { Greeter, User } from "generated";
Greeter.NewGreeting.handler(async ({ event, context }) => {
let currentUser = await context.User.get(event.params.user.toString());
if (currentUser !== undefined) {
let userObject: User = {
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: currentUser.numberOfGreetings + 1,
greetings: [...currentUser.greetings, event.params.greeting],
};
context.User.set(userObject);
} else {
let userObject: User = {
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
};
context.User.set(userObject);
}
});
open Types
Handlers.Greeter.NewGreeting.handler(async ({ event, context }) => {
let currentUserOpt = context.user.get(event.params.user->Ethers.ethAddressToString);
switch currentUserOpt {
| Some(existingUser) => {
let userObject: user = {
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: user = {
id: event.params.user->Ethers.ethAddressToString,
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
};
context.user.set(userObject);
}
}
});
- 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:
- Javascript
- Typescript
- Rescript
let { getConfigByChainId } = require("../generated/src/ConfigYAML.bs.js");
Greeter.NewGreeting.handler(async ({ event, context }) => {
let config = getConfigByChainId(event.chainId);
});
import { getConfigByChainId } from "../generated/src/ConfigYAML.gen";
Greeter.NewGreeting.handler(async ({ event, context }) => {
let config = getConfigByChainId(event.chainId);
});
open Types
Handlers.Greeter.NewGreeting.handler(async ({ 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.<CONTRACT_NAME>.abi
- ABI of the contractconfig.contracts.<CONTRACT_NAME>.addresses
- An array of addresses of the contractconfig.contracts.<CONTRACT_NAME>.events
- An array of event names emitted by the contract