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:
loaderfunctionhandlerfunction
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.greetinginstead ofcontext.Greeting).
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).
Each entity that is to be loaded by the event must be defined in the requiredEntities field of the event in the config.yaml file.
If you want to load a label with multiple entities grouped together as an array, an arary label is the aproach to go with. This is done by adding arrayLabels with a list of array labels to use with the entity to the label definition in the config.yaml file.
A full example using labels:
# config.yaml
networks:
- id: 12345 #Your chain Id
contracts:
- name: Greeter
# ... other fields
events:
- event: "TestEvent"
requiredEntities:
- name: "A"
arrayLabels:
- "allAs"
label:
- "singleA"
# ... rest of your config file
# schema.graphql
type A {
id: ID!
someField: String!
}
With that config you could use this in your loaders (and handlers):
- Javascript
- Typescript
- Rescript
GreeterContract.TestEvent.loader((~event, ~context) => {
context.A.allALoad(["id1", "id2", "id3"])
context.A.singleALoad("singled")
// ... other loaders
})
GreeterContract.TestEvent.handler((~event, ~context) => {
let arrayOfAs = context.A.allA
let singleA = context.A.singleA
// ... rest of the handler that uses or updates these entities.
})
GreeterContract_TestEvent_loader(({event, context}) => {
context.A.allALoad(["id1", "id2", "id3"])
context.A.singleALoad("singled")
// ... other loaders
})
GreeterContract_TestEvent_handler(({event, context}) => {
let arrayOfAs = context.A.allA
let singleA = context.A.singleA
// ... rest of the handler that uses or updates these entities.
})
GreeterContract.TestEvent.loader((event, context) => {
context.a.allALoad(["id1", "id2", "id3"])
context.a.singleALoad("singled")
// ... other loaders
})
GreeterContract.TestEvent.handler((event, context) => {
let arrayOfAs = context.a.allA
let singleA = context.a.singleA
// ... rest of the handler that uses or updates these entities.
})
Handler function
Handler functions are called via
- Javascript
- Typescript
- Rescript
<ContractName>Contract.<EventName>.handler
<ContractName>Contract_<EventName>_handler
<ContractName>Contract.<EventName>.handler
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>.
Additional raw event information can also be accessed via event.<rawInfo>.
Below is a list of raw event information that can be accessed:
blockHashblockNumberblockTimestampchainIdeventIdeventTypelogIndexsrcAddresstransactionHashtransactionIndex
Handler functions can access the loaded entities information via context.<EntityName>.<label>Load.
If no label name has been defined in the config.yaml file, handler functions can access the loaded entities information 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.greetinginstead ofcontext.Greeting). 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().
Greeter example
Inspecting the config.yaml of the NewGreeting event, it indicates that there is a defined requiredEntities field of the following:
events:
- name: "NewGreeting"
requiredEntities:
- name: "Greeting"
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.Greeting.load(event.params.user.toString());
});
import { GreeterContract_NewGreeting_loader } from "../generated/src/Handlers.gen";
import { greetingEntity } from "../generated/src/Types.gen";
GreeterContract_NewGreeting_loader(({ event, context }) => {
context.Greeting.load(event.params.user.toString());
});
open Types
Handlers.GreeterContract.NewGreeting.loader((~event, ~context) => {
context.greeting.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
Greetingentity 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 existingGreeter = context.Greeting.get(event.params.user.toString());
if (existingGreeter != undefined) {
context.Greeting.set({
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: existingGreeter.numberOfGreetings + 1,
greetings: [...existingGreeter.numberOfGreetings, event.params.greeting],
});
} else {
context.Greeting.set({
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
});
}
});
import { GreeterContract_NewGreeting_handler } from "../generated/src/Handlers.gen";
import { greetingEntity } from "../generated/src/Types.gen";
GreeterContract_NewGreeting_handler(({ event, context }) => {
(({ event, context }) => {
let currentGreeter = context.Greeting.get(event.params.user.toString());
if (currentGreeter != null) {
let greetingObject: greetingEntity = {
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: currentGreeter.numberOfGreetings + 1,
greetings: [...existingGreeter.numberOfGreetings, event.params.greeting],
};
context.Greeting.set(greetingObject);
} else {
let greetingObject: greetingEntity = {
id: event.params.user.toString(),
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
};
context.Greeting.set(greetingObject);
}
});
open Types
Handlers.GreeterContract.NewGreeting.handler((~event, ~context) => {
let currentGreeterOpt = context.greeting.get(event.params.user->Ethers.ethAddressToString)
switch currentGreeterOpt {
| Some(existingGreeter) => {
let greetingObject: greetingEntity = {
id: event.params.user->Ethers.ethAddressToString,
latestGreeting: event.params.greeting,
numberOfGreetings: existingGreeter.numberOfGreetings + 1,
greetings: existingGreeter.greetings->Belt.Array.concat([event.params.greeting]),
}
context.greeting.set(greetingObject)
}
| None =>
let greetingObject: greetingEntity = {
id: event.params.user->Ethers.ethAddressToString,
latestGreeting: event.params.greeting,
numberOfGreetings: 1,
greetings: [event.params.greeting],
}
context.greeting.set(greetingObject)
}
})
- Once the user has defined their
loaderfunction, they are then able to retrieve the loaded entity information. - In the above example, if a
Greetingentity is found matching the load criteria in theloaderfunction, it will be available viagreetingWithChanges. - This is made available to the user through the handler context defined simply as
context. - This
contextis 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
Greetingentity in the database. - This is done via the
contextusing the function (context.Greeting.set(greetingObject)). - The user has access to a
greetingEntitytype that has all the fields defined in the schema.