Skip to main content
Version: v2

NOTE: v2 is currently in rc phase (release candidate) and is not yet stable. Please refer to the v1 documentation for the stable version.

Introduction

V2 of HyperIndex is about streamlining the process of starting an indexer and optimizing it as you go. There are two big changes:

  • Handlers are now asynchronous, and loaders became an optional tool for additional optimizations.
  • It made async-mode not needed, hence it's removed in v2.
  • Loaders (when used) are more expressive and connected via the return type to the context of the handler.
    • In v1, you needed to use linked entities to load entity fields of other entities. This was unintuitive.
      • In v2, you can directly access the fields of the loader the exact same way as you do in the handler, with an async 'get' function.
    • In v1, you needed to call 'load' in the loader, and 'get' in the handler separately (or use labelled fields).
      • In v2, you can use the return type of the loader to directly access the fields in the handler via the context, or you can call 'get' again.
  • Fixed indexing params with names that are reserved words in ReScript.
  • Validation and autocompletion for config.yaml. You can enable it by adding # yaml-language-server: $schema=./node_modules/envio/evm.schema.json on top of your config.yaml file.

Changes to Make

Handlers

  • Handlers are now asynchronous - add the async keyword and rename handlerAsync to handler.
  • You can use handlerWithLoader if you need a loader, otherwise use handler directly.
  • The 'get' function is now asyncronous, so add an await before those functions.
  • No labelled entities.

Loaders

  • Loaders are merged into the handlers using handlerWithLoader.
  • Loading linked entities is done directly with promises in the loader.
  • Loaders are completely optional - only use the if you care about high throughput indexing.
  • Loaders return the required entities which are then used in the handler.
  • The dynamic contract registration moved from loaders to its own <ContractName>.<EventName>.contractRegister handler.
  • The return type of the loader is used directly in the handler to access the loaded data. No need to re-'get' it again in the handler.

Configuration

  • There is no async-mode anymore, so you can remove isAsync: true from each of the events in your config.yaml.
- isAsync: true
  • Removed entity labels and required entities.
- required_entities:
- - name: User

Miscellaneous breaking changes and deprecations

  • The context.Entity.load function is deprecated and should be replaced with direct calls to context.Entity.get in the loader.
  • The context.ParentEntity.loadField functions are deprecated and should be replaced with direct calls to context.ChildEntity.get.
  • Remove the Contract and Entity suffixes from generated code.
  • For JavaScript/TypeScript users:
    • The event param names are not uncapitalized anymore. So you might need to change event.params.capitalizedParamName to event.params.CapitalizedParamName.
  • For ReScript users:
    • We moved to the built-in bigint type instead of the Ethers.BigInt.t.
    • We migrated to ReScript 11 uncurried mode. Curried mode is not supported anymore. So you need to remove uncurried: false from your rescript.json file. Also, we vendored RescriptMocha bindings to support uncurried mode. Please use it instead of rescript-mocha.

Migration Steps

1. Update Imports

Replace the old import statements with the new ones.

Before:

import {
GreeterContract_NewGreeting_handler,
// or you aren't using these `_` versions of the imports
GreeterContract,
// ...
} from "../generated/src/Handlers.gen"; // Not all imports still look like this, but on old indexers they do.

import {
GreetingEntity,
UserEntity,
// ... other entities
} from "../generated/src/Types.gen";

After:

import {
Greeter, // the Greeter Contract
// ...
Greeting, // the Greeting Entity
User, // The User Entity
// ... other entities
} from "generated"; // Note this requires adding the 'generated' folder to your 'optionalDependencies' in your package.json

2. Update Handler Definitions

Before:

/// or if your indexer is very old: GreeterContract_Event1_loader
GreeterContract.Event1.loader(({ event, context }) => {
// Loader code
});
GreeterContract.Event1.handler(({ event, context }) => {
// Handler code
});

After:

Greeter.Event1.handlerWithLoader({
loader: async ({ event, context }) => {
// Loader code
return {
/* loaded data, this data is available in the "handler" via the `loaderReturn` parameter */
};
},
handler: async ({ event, context, loaderReturn }) => {
// Handler code using loaderReturn
},
});

Or without a loader:

Before:

GreeterContract.Event1.handler(({ event, context }) => {
// Handler code
});

After:

Greeter.Event1.handler(async ({ event, context }) => {
// Handler code
});

3. Dynamic Contract Registration

Use contractRegister for dynamic contract registration. Assuming there is an event called NewGreeterCreated that creates a contract called Greeter that has the address of the newGreeter as a field.

Before:

GreeterContract.NewGreeterCreated.loader(({ event, context }) => {
context.contractRegistration.addGreeter(event.params.newGreeter);
});

After:

Greeter.NewGreeterCreated.contractRegister(({ event, context }) => {
context.addGreeter(event.params.newGreeter);
});

4. Handling Entities

Before

const greetingInstance: GreetingEntity = {
...currentGreeting,
// ...
};
context.Greeting.set(greetingInstance);

After

const greetingInstance: Greeting = {
...currentGreeting,
// ...
};
context.Greeting.set(greetingInstance);

Only change is in the TypeScript/ReScript type for the entity 💪

5. Accessing Loaded Data

Access data via asyncronous get functions:

Before:

let currentEntity = context.Entity.get(event.srcAddress.toString());

After:

let currentEntity = await context.Entity.get(event.srcAddress.toString());

Access loaded data through the loaderReturn if you are using loaders:

Before:

let currentEntity = context.Entity.get(event.srcAddress.toString());

After:

const { currentEntity } = loaderReturn;

6. Loading Linked Entities

Before

GreeterContract.Event1.loader(({ event, context }) => {
context.Entity.load(event.srcAddress.toString(), {
loadField1: true,
loadField2: true,
});
});

After:

Greeter.Event1.handlerWithLoader({
loader: async ({ event, context }) => {
const currentEntity = await context.Entity.get(event.srcAddress.toString());
if (currentEntity == undefined) return null;

const field1Instance = await context.Entity.getField1(
currentEntity.field1_id
);
const field2Instance = await context.Entity.getField2(
currentEntity.field2_id
);

return { currentEntity, field1Instance, field2Instance };
},
});

Examples

As we upgrade public repos on GitHub, we'll add the commits of the upgrade to this page for reference:

Additional Tips

  • Make sure to thoroughly test your migrated code to catch any issues that might arise from the asynchronous nature of the new handlers.
  • If performance isn't a massive concern, you can simply use the handler function without a loader.