Skip to main content

Substrate EVM Support

SubQuery TeamAbout 5 min

Substrate EVM Support

We provide a custom data source processor for Parity's Frontier EVMopen in new window and Acala's EVM+open in new window. This offers a simple way to filter and index both EVM and Substrate activity on many Polkadot networks within a single SubQuery project.

Tested and Supported networks

Theoretically all the following networks (and more) should also be supported since they implement Parity's Frontier EVM. Please let us know if you verify this and we can add them to the known support:

  • Moonbeam
  • Moonriver
  • Moonbase Alpha
  • Acala
  • Karura
  • Astar
  • Shiden
  • Automata
  • Bitcountry
  • Clover
  • Darwinia
  • Edgeware
  • Gamepower
  • Human
  • InVarch
  • T3rn
  • PAID
  • Manta
  • Parastate
  • Polkafoundry
  • ChainX
  • Gaia
  • Thales
  • Unique

You can also refer to the basic Moonriver EVMopen in new window or Acala EVM+open in new window example projects with an event and call handler. This project is also hosted live in the SubQuery Explorer hereopen in new window.

Getting started

  1. Add the custom datasource as a dependency:
  • Create a new project from an EVM template though subql init OR
  • For existing projects, yarn add -D @subql/frontier-evm-processor or npm i @subql/acala-evm-processor --save-dev.
  1. Add exports to your package.json like below in order for IPFS deployments to work
  ...
  "exports": {
    "frontierEvm": "./node_modules/@subql/frontier-evm-processor/dist/index.js"
    //"acalaEvm": "./node_modules/@subql/acala-evm-processor/dist/index.js",
    "chaintypes": "./src/chaintypes.ts" // chain types if required
  }
  1. Import processor file to your project.ts like below
import { FrontierEvmDatasource } from "@subql/frontier-evm-processor";

const project: SubstrateProject<FrontierEvmDatasource> = {
  ...
  dataSources: [
    {
      // This is the datasource for Moonbeam's Native Substrate processor
      kind: "substrate/FrontierEvm",
      startBlock: 752073,
      processor: {
        file: "./node_modules/@subql/frontier-evm-processor/dist/bundle.js",
        options: {
          abi: "erc20",
          address: "0xe3e43888fa7803cdc7bea478ab327cf1a0dc11a7", // FLARE token https://moonscan.io/token/0xe3e43888fa7803cdc7bea478ab327cf1a0dc11a7
        },
      },
      assets: new Map([["erc20", { file: "./abis/erc20.abi.json" }]]),
      mapping: {...},
    },
  ],
}
  1. Add a custom data source as described below.
  2. Add handlers for the custom data source to your code.

Data Source Spec

FieldTypeRequiredDescription
kindsubstrate/FrontierEvm of substrate/AcalaEVMYesType of the datasource
processor.file"./node_modules/@subql/frontier-evm-processor/dist/bundle.js" or "./node_modules/@subql/acala-evm-processor/dist/bundle.js"YesFile reference to the data processor code
processor.optionsProcessorOptionsNoOptions specific to the Frontier Processor
assets{ [key: String]: { file: String }}NoAn object of external asset files

Processor Options

FieldTypeRequiredDescription
abiStringNoThe ABI that is used by the processor to parse arguments. MUST be a key of assets
addressString or nullNoA contract address where the event is from or call is made to. null will capture contract creation calls

Call Handlers

Works in the same way as substrate/CallHandler except with a different handler argument and minor filtering changes.

FieldTypeRequiredDescription
kindsubstrate/FrontierEvmCall or substrate/AcalaEvmCallYesSpecifies that this is an Call type handler
filterCall FilterNoFilter the data source to execute

Call Filters

FieldTypeExample(s)Description
functionString0x095ea7b3, approve(address to,uint256 value)Either Function Signatureopen in new window strings or the function sighash to filter the function called on the contract
fromString0x6bd193ee6d2104f14f94e2ca6efefae561a4334bAn Ethereum address that sent the transaction

Handler Functions

Unlike a normal handler you will not get a SubstrateExtrinsic as the parameter, instead you will get a FrontierEvmCall or AcalaEvmCall which is based on Ethers TransactionResponseopen in new window type.

Changes from the TransactionResponse type:

  • It doesn't have wait and confirmations properties.
  • A success property is added to know if the transaction was a success.
  • args is added if the abi field is provided and the arguments can be successfully parsed. You can add a generic parameter like so to type args: FrontierEvmCall<{ from: string, to: string, value: BigNumber }>.

Event Handlers

Works in the same way as substrate/EventHandler except with a different handler argument and minor filtering changes.

FieldTypeRequiredDescription
kindsubstrate/FrontierEvmEvent or substrate/AcalaEvmEventYesSpecifies that this is an Event type handler
filterEvent FilterNoFilter the data source to execute

Event Filters

FieldTypeExample(s)Description
topicsString arrayTransfer(address indexed from,address indexed to,uint256 value)The topics filter follows the Ethereum JSON-PRC log filters, more documentation can be found hereopen in new window.

Note on topics:

There are a couple of improvements from basic log filters:

Codegen

If you're creating a new Substrate Frontier EVM or Acala EVM+ based project, the normal codegen command will also generate ABI types and save them into src/types using the npx typechain --target=ethers-v5 command, allowing you to bind these contracts to specific addresses in the mappings and call read-only contract methods against the block being processed. It will also generate a class for every contract event to provide easy access to event parameters, as well as the block and transaction the event originated from. All of these types are written to src/types/**.ts. In the example Moonriver EVM Starter SubQuery projectopen in new window, you would import these types like so.

import { GraphQLEntity1, GraphQLEntity2 } from "../types";

Handler Functions

Unlike a normal handler you will not get a SubstrateEvent as the parameter, instead you will get a FrontierEvmEvent or AcalaEvmEvent which is based on Ethers Logopen in new window type.

Changes from the Log type:

  • args is added if the abi field is provided and the arguments can be successfully parsed. You can add a generic parameter like so to type args: FrontierEvmEvent<{ from: string, to: string, value: BigNumber }>.
Frontier EVM
import { Approval, Transaction } from "../types";
import {
  FrontierEvmEvent,
  FrontierEvmCall,
} from "@subql/frontier-evm-processor";
import { BigNumber } from "ethers";

// Setup types from ABI
type TransferEventArgs = [string, string, BigNumber] & {
  from: string;
  to: string;
  value: BigNumber;
};
type ApproveCallArgs = [string, BigNumber] & {
  _spender: string;
  _value: BigNumber;
};

export async function handleFrontierEvmEvent(
  event: FrontierEvmEvent<TransferEventArgs>
): Promise<void> {
  const transaction = new Transaction(event.transactionHash);

  transaction.value = event.args.value.toBigInt();
  transaction.from = event.args.from;
  transaction.to = event.args.to;
  transaction.contractAddress = event.address;

  await transaction.save();
}

export async function handleFrontierEvmCall(
  event: FrontierEvmCall<ApproveCallArgs>
): Promise<void> {
  const approval = new Approval(event.hash);

  approval.owner = event.from;
  approval.value = event.args._value.toBigInt();
  approval.spender = event.args._spender;
  approval.contractAddress = event.to;

  await approval.save();
}

Data Source Example

This is an extract from the project.ts manifest file.

Frontier EVM
{
  dataSources: [
    {
      // This is the datasource for Moonbeam's Native Substrate processor
      kind: "substrate/FrontierEvm",
      startBlock: 752073,
      processor: {
        file: "./node_modules/@subql/frontier-evm-processor/dist/bundle.js",
        options: {
          abi: "erc20",
          address: "0xe3e43888fa7803cdc7bea478ab327cf1a0dc11a7", // FLARE token https://moonscan.io/token/0xe3e43888fa7803cdc7bea478ab327cf1a0dc11a7
        },
      },
      assets: new Map([["erc20", { file: "./erc20.abi.json" }]]),
      mapping: {
        file: "./dist/index.js",
        handlers: [
          {
            handler: "handleEvmEvent",
            kind: "substrate/FrontierEvmEvent",
            filter: {
              topics: [
                "Transfer(address indexed from,address indexed to,uint256 value)",
              ],
            },
          },
          {
            handler: "handleEvmCall",
            kind: "substrate/FrontierEvmCall",
            filter: {
              function: "approve(address to,uint256 value)",
            },
          },
        ],
      },
    },
  ],
}

Querying contracts

@subql/frontier-evm-processor is the only package that currently allows this. It provides FrontierEthProvideropen in new window which implements an Ethers Provideropen in new window, this implementation is restricted to only support methods for the current height. You can pass it to a contract instance in order to query contract state at the hight currently being indexed.

Known Limitations

  • There is no way to get the transaction receipts with call handlers.
  • blockHash properties are currently left undefined, the blockNumber property can be used instead.

Extra info

  • There is also a @subql/moonbeam-evm-processor which is an alias for @subql/frontier-evm-processor.
  • The source code for these processors can be found in our datasource-processors repoopen in new window.