To run this code on mainnet:
pnpm run build-envio
pnpm run docker-envio
pnpm run setup-envio
pnpm run start
# in another tab
cd fetcher; pnpm startTo run on testent:
pnpm run build-envio-testnet
pnpm run docker-envio-testnet
pnpm run setup-envio-testnet
pnpm run start-testnet
# in another tab
cd fetcher; RPC_ENDPOINT="https://linea-goerli.infura.io/v3/<your infura key>" DATABASE_URI="postgresql://postgres:testing@localhost:5433/envio-dev" pnpm startThe following files are required to use the Indexer:
- Configuration (defaults to
config.yaml) - GraphQL Schema (defaults to
schema.graphql) - Event Handlers (defaults to
src/EventHandlers.res)
These files are auto-generated according to the ERC-20 template by running envio init command.
Example config file from ERC-20 scenario:
name: erc-20-indexer
version: 1.0.0
description: ERC-20 indexer
networks:
- id: 1337
rpc_config:
url: http://localhost:8545
start_block: 0
contracts:
- name: ERC20
abi_file_path: abis/erc20.json
address: ["0x2B2f78c5BF6D9C12Ee1225D5F374aa91204580c3"]
handler: ./src/EventHandlers.bs.js
events:
- event: "Approval"
requiredEntities:
- name: "Account"
labels:
- "ownerAccountChanges"
- event: "Transfer"
requiredEntities:
- name: "Account"
labels:
- "senderAccountChanges"
- "receiverAccountChanges"Field Descriptions
name- NameOfTheIndexerversion- Version of the config schema used by the indexerdescription- Description of the projectnetworks- Configuration of the blockchain networks that the project is deployed onid- Chain identifier of the network
rpc_config- RPC Config that will be used to subscribe to blockchain data on this networkurl- URL of the RPC endpointstart_block- Initial block from which the indexer will start listening for eventscontracts- Configuration for each contract deployed on the networkname- User-defined contract nameabi_file_path- File location of the contract ABIaddress- An array of addresses that the contract is deployed to on the networkhandler- Location of the file that handles the events emitted by this contractevents- Configuration for each event emitted by this contract that the indexer will listen forname- Name of the event (must match the name in the ABI)required_entities- An array of entities that need to loaded and made accessible within the handler function (an empty array indicates that no entities are required)name- The name of the required entity (must match an entity defined inschema.graphql)label- A user defined label that corresponds to this entity load
The schema.graphql file contains the definitions of all user-defined entities. These entity types are then created/modified within the handler files.
Example schema definition for ERC-20 scenario:
type Account @entity {
id: ID!
approval: BigInt!
balance: BigInt!
}Once the configuration and graphQL schema files are in place, run
envio codegenin the project directory.
The entity and event types will then be available in the handler files.
A user can specify a specific handler file per contract that processes events emitted by that contract. Each event handler requires two functions to be registered in order to enable full functionality within the indexer.
- An
<event>LoadEntitiesfunction - An
<event>Handlerfunction
Example of registering a loadEntities function for the Transfer event from the above example config:
Handlers.ERC20Contract.registerTransferLoadEntities((~event, ~context) => {
// loading the required accountEntity
context.account.senderAccountChangesLoad(event.params.from->Ethers.ethAddressToString)
context.account.receiverAccountChangesLoad(event.params.to->Ethers.ethAddressToString)
})Inspecting the config of the Transfer event from the above example config indicates that there is a defined requiredEntities field of the following:
events:
- name: "Transfer"
requiredEntities:
- name: "Account"
labels:
- "senderAccountChanges"
- "receiverAccountChanges"
- The register function
ERC20Contract_registerTransferLoadEntitiesfollows a naming convention for all events:register<EventName>LoadEntities. - Within the function that is being registered the user must define the criteria for loading the
Accountentity which corresponds to the label defined in the config. - This is made available to the user through the load entity context defined as
contextUpdator. - In the case of the above example the
senderAccountChangesloads aAccountentity that corresponds to thefromreceived from the event andreceiverAccountChangesloads aAccountentity that corresponds to thetoreceived from the event.
Example of registering a Handler function for the Transfer event and using the loaded entity senderAccountChanges and receiverAccountChanges:
Handlers.ERC20Contract.registerTransferHandler((~event, ~context) => {
// getting the sender accountEntity
let senderAccount = context.account.senderAccountChanges()
switch senderAccount {
| Some(existingSenderAccount) => {
// updating accountEntity object
let accountObject: accountEntity = {
id: existingSenderAccount.id,
approval: existingSenderAccount.approval,
balance: existingSenderAccount.balance->Ethers.BigInt.sub(event.params.value),
}
// updating the accountEntity with the new transfer field value
context.account.update(accountObject)
}
| None => {
// updating accountEntity object
let accountObject: accountEntity = {
id: event.params.from->Ethers.ethAddressToString,
approval: Ethers.BigInt.fromInt(0),
balance: Ethers.BigInt.fromInt(0) ->Ethers.BigInt.sub(event.params.value),
}
// inserting the accountEntity with the new transfer field value
context.account.insert(accountObject)
}
}
// getting the sender accountEntity
let receiverAccount = context.account.receiverAccountChanges()
switch receiverAccount {
| Some(existingReceiverAccount) => {
// updating accountEntity object
let accountObject: accountEntity = {
id: existingReceiverAccount.id,
approval: existingReceiverAccount.approval,
balance: existingReceiverAccount.balance->Ethers.BigInt.add(event.params.value),
}
// updating the accountEntity with the new transfer field value
context.account.update(accountObject)
}
| None => {
// updating accountEntity object
let accountObject: accountEntity = {
id: event.params.to->Ethers.ethAddressToString,
approval: Ethers.BigInt.fromInt(0),
balance: event.params.value,
}
// inserting the accountEntity with the new transfer field value
context.account.insert(accountObject)
}
}
})- The handler functions also follow a naming convention for all events in the form of:
register<EventName>Handler. - Once the user has defined their
loadEntitiesfunction, they are then able to retrieve the loaded entity information via the labels Transfer in theconfig.yamlfile. - In the above example, if a
Accountentity is found matching the load criteria in theloadEntitiesfunction, it will be available viasenderAccountChangesandreceiverAccountChanges. - 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
Accountentity in the database. - This is done via the
contextusing the update function (context.account.update(accountObject)). - The user has access to a
accountEntitytype that has all the fields defined in the schema.
This context also provides the following functions per entity that can be used to interact with that entity:
- insert
- update
- delete