Squid EVM typegen
EVM logs
The Ethereum Virtual Machine smart contract is a bytecode deployed on an EVM-capable blockchain. There could be several functions in a contract. An Application Binary Interface is the interface between two program modules, one of which is often at the level of machine code. The interface is the de facto method for encoding/decoding data into/out of the machine code.
An ABI is necessary so that you can specify which function in the contract to invoke, as well as get a guarantee that the function will return data in the format you are expecting.
Subsquid has developed a CLI tool that is able to inspect the ABI in JSON format, parse it and create TypeScript interfaces and mappings to decode functions and data, as specified in the ABI itself.
Similarly to Substrate entities, having Interfaces for data and mappings for function decoding, speeds up the development of EVM log handler functions, creating standards for passing data around.
ABI interface and decoding
Solidity developers, and more generally, those who have dealt with EVM contracts should already be familiar with the concept of ABI, but as a refresher, and specifically for Substrate specialist who are taking a look at this for the first time, ABI stands for Application Binary Interface and it is described as:
The Contract Application Binary Interface (ABI) is the standard way to interact with contracts in the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction. Data is encoded according to its type, as described in this specification. The encoding is not self describing and thus requires a schema in order to decode.
In many ways, it is a very similar concept to the Types Bundle of Substrate. It collects types, and names, inputs output and properties of functions expressed in contracts. And the resulting interface is noted in a JSON file.
As an example, here is the ABI for the ERC-721 standard
ERC-721 ABI
Defining an ABI (or more) is crucial for being able to process EVM logs, but there a few more steps to take in order to do so.
Installation
The Squid evm typegen tool is part of Subsquid SDK and is used for generating TypeScript interface classes for EVM Application Binary Interfaces.
info
Note: in the context of this guide, we assume the Development Environment has been already set up and that npm
is used, although other options are available.
To install EVM typegen tool, simply run this in a console.
npm install @subsquid/substrate-evm-typegen
Once installed, check available commands by running
npx squid-evm-typegen --help
Which will print out a help.
Options for squid-substrate-typegen
command
Argument | Description | Required |
---|---|---|
-h or --help | display help for command | |
--abi | path to a JSON abi file | yes |
--output | path for output typescript file | yes |
EVM Typegen Example
npx squid-evm-typegen --abi=src/abi/ERC721.json --output=src/abi/erc721.ts
Subsquid provides a tool called squid-evm-typegen
that accepts a JSON file, with an ABI definition as an input, and will generate a TypeScript file, containing Interfaces and decoding mappings as an output.
In the squid-evm-template repository you'll find a JSON file containing the ERC721 ABI and right next to it, the TypeScript file generated by such tool. Let's dissect and explain what it contains:
import * as ethers from "ethers";
export const abi = new ethers.utils.Interface(getJsonAbi());
These first two lines import and instantiate a programmatic interface for the ABI.
Then, a series of data interfaces are declared. These are the inputs and outputs of the functions declared in the ABI.
export interface ApprovalAddressAddressUint256Event {
owner: string;
approved: string;
tokenId: ethers.BigNumber;
}
export interface ApprovalForAllAddressAddressBoolEvent {
owner: string;
operator: string;
approved: boolean;
}
export interface TransferAddressAddressUint256Event {
from: string;
to: string;
tokenId: ethers.BigNumber;
}
export interface EvmEvent {
data: string;
topics: string[];
}
Below them, you'll find a dictionary that maps the signature of a function to its topic
and a method to decode it.
export const events = {
"Approval(address,address,uint256)": {
topic: abi.getEventTopic("Approval(address,address,uint256)"),
decode(data: EvmEvent): ApprovalAddressAddressUint256Event {
const result = abi.decodeEventLog(
abi.getEvent("Approval(address,address,uint256)"),
data.data || "",
data.topics
);
return {
owner: result[0],
approved: result[1],
tokenId: result[2],
}
}
}
,
"ApprovalForAll(address,address,bool)": {
topic: abi.getEventTopic("ApprovalForAll(address,address,bool)"),
decode(data: EvmEvent): ApprovalForAllAddressAddressBoolEvent {
const result = abi.decodeEventLog(
abi.getEvent("ApprovalForAll(address,address,bool)"),
data.data || "",
data.topics
);
return {
owner: result[0],
operator: result[1],
approved: result[2],
}
}
}
,
"Transfer(address,address,uint256)": {
topic: abi.getEventTopic("Transfer(address,address,uint256)"),
decode(data: EvmEvent): TransferAddressAddressUint256Event {
const result = abi.decodeEventLog(
abi.getEvent("Transfer(address,address,uint256)"),
data.data || "",
data.topics
);
return {
from: result[0],
to: result[1],
tokenId: result[2],
}
}
}
,
}
At the bottom of the file, there will always be an auxiliary function that returns the ABI in a raw JSON format (not reported here, for brevity).