Typegen
Squid Typegen is a code generation tool for creating Typescript types for substrate Events, Extrinsics, Storage Items (for Substrate) and EVM logs.

Overview

Substrate entities

Event, call, and Storage data are ingested as raw untyped JSON by the Processor. Not only is it unclear what the exact structure of a particular event or call is but, rather frequently, it can change over time.
Runtime upgrades may change the event data and even the event logic altogether. Fortunately, Squid has got you covered with first-class support for runtime upgrades. This comes in very handy when expressing business logic, mapping Events, Extrinsics, and Storage items with database Entities defined in the GraphQL schema.
Having Class wrappers around them makes it much easier to develop Event or Extrinsic Handlers, as well as pre- or post-block "hooks" and manage multiple metadata versions of a blockchain.
Subsquid SDK comes with a CLI tool called substrate metadata explorer which makes it easy to keep track of all runtime upgrades within a certain blockchain. This can then be provided to a different CLI tool called typegen, to generate type-safe, spec version-aware wrappers around events and calls.
In the next section we'll be taking the squid template as an example.

EVM logs

The Ethereum Virtual Machine smart contract is 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.

Blockchain metadata

The template was designed to explore the Kusama blockchain, specifically processing the 'balance.Transfer' event.
In order to generate wrapper classes, the first thing to do is to explore the entire history of the blockchain and extract its metadata. The squid-substrate-metadata-explorer command (for more information on how to run it, head over to this Guide) will do the chain exploration and write it to a file. It will look like this:
1
[
2
{
3
"blockNumber": 0,
4
"blockHash": "0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe",
5
"specVersion": 1020,
6
"metadata": "0x6d65746109701853797374656d011853797374656d34304163..."
7
},
8
// ...
9
]
Copied!
Where the metadata field is cut here, and the rest of the file is omitted for brevity, but there are multiple objects such as this one in this relatively large file. The point is that for every available Runtime version of the blockchain, some metadata is available to be decoded and explored, and this metadata contains the necessary information to process its Events, Extrinsics, and Storage items.

TypeScript class wrappers

This file is then used by the typegen command (again, look at the Guide for how to configure and run it) to decode and interpret the metadata, and then uses that to generate this TypeScript class:
1
export class BalancesTransferEvent {
2
constructor(private ctx: EventContext) {
3
assert(this.ctx.event.name === 'balances.Transfer')
4
}
5
6
/**
7
* Transfer succeeded (from, to, value, fees).
8
*/
9
get isV1020(): boolean {
10
return this.ctx._chain.getEventHash('balances.Transfer') === 'e1ceec345fa4674275d2608b64d810ecec8e9c26719985db4998568cfcafa72b'
11
}
12
13
/**
14
* Transfer succeeded (from, to, value, fees).
15
*/
16
get asV1020(): [Uint8Array, Uint8Array, bigint, bigint] {
17
assert(this.isV1020)
18
return this.ctx._chain.decodeEvent(this.ctx.event)
19
}
20
21
/**
22
* Transfer succeeded (from, to, value).
23
*/
24
get isV1050(): boolean {
25
return this.ctx._chain.getEventHash('balances.Transfer') === '2082574713e816229f596f97b58d3debbdea4b002607df469a619e037cc11120'
26
}
27
28
/**
29
* Transfer succeeded (from, to, value).
30
*/
31
get asV1050(): [Uint8Array, Uint8Array, bigint] {
32
assert(this.isV1050)
33
return this.ctx._chain.decodeEvent(this.ctx.event)
34
}
35
36
/**
37
* Transfer succeeded.
38
*/
39
get isLatest(): boolean {
40
return this.ctx._chain.getEventHash('balances.Transfer') === '68dcb27fbf3d9279c1115ef6dd9d30a3852b23d8e91c1881acd12563a212512d'
41
}
42
43
/**
44
* Transfer succeeded.
45
*/
46
get asLatest(): {from: v9130.AccountId32, to: v9130.AccountId32, amount: bigint} {
47
assert(this.isLatest)
48
return this.ctx._chain.decodeEvent(this.ctx.event)
49
}
50
}
Copied!
This manages different runtime versions, including the starting hash for each and instructions for how to process (decode) the event itself.
All of this is better explained in the section dedicated to the Processor and Event mapping, but, given the class definition for a BalanceTransferEvent, such a class can be used to handle events such as this:
1
processor.addEventHandler('balances.Transfer', async ctx => {
2
let transfer = getTransferEvent(ctx)
3
// ...
4
})
5
6
// ...
7
8
function getTransferEvent(ctx: EventHandlerContext): TransferEvent {
9
let event = new BalancesTransferEvent(ctx)
10
if (event.isV1020) {
11
let [from, to, amount] = event.asV1020
12
return {from, to, amount}
13
} else if (event.isV1050) {
14
let [from, to, amount] = event.asV1050
15
return {from, to, amount}
16
} else {
17
return event.asLatest
18
}
19
}
Copied!
Where, upon processing an event, its metadata version is checked, and the metadata is extracted accordingly, making things a lot easier.

EVM Typegen

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:
erc721.ts
1
import * as ethers from "ethers";
2
3
export const abi = new ethers.utils.Interface(getJsonAbi());
Copied!
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.
erc721.ts
1
export interface ApprovalAddressAddressUint256Event {
2
owner: string;
3
approved: string;
4
tokenId: ethers.BigNumber;
5
}
6
7
export interface ApprovalForAllAddressAddressBoolEvent {
8
owner: string;
9
operator: string;
10
approved: boolean;
11
}
12
13
export interface TransferAddressAddressUint256Event {
14
from: string;
15
to: string;
16
tokenId: ethers.BigNumber;
17
}
18
19
export interface EvmEvent {
20
data: string;
21
topics: string[];
22
}
Copied!
Below them, you'll find a dictionary that maps the signature of a function to its topic and a method to decode it.
erc721.ts
1
export const events = {
2
"Approval(address,address,uint256)": {
3
topic: abi.getEventTopic("Approval(address,address,uint256)"),
4
decode(data: EvmEvent): ApprovalAddressAddressUint256Event {
5
const result = abi.decodeEventLog(
6
abi.getEvent("Approval(address,address,uint256)"),
7
data.data || "",
8
data.topics
9
);
10
return {
11
owner: result[0],
12
approved: result[1],
13
tokenId: result[2],
14
}
15
}
16
}
17
,
18
"ApprovalForAll(address,address,bool)": {
19
topic: abi.getEventTopic("ApprovalForAll(address,address,bool)"),
20
decode(data: EvmEvent): ApprovalForAllAddressAddressBoolEvent {
21
const result = abi.decodeEventLog(
22
abi.getEvent("ApprovalForAll(address,address,bool)"),
23
data.data || "",
24
data.topics
25
);
26
return {
27
owner: result[0],
28
operator: result[1],
29
approved: result[2],
30
}
31
}
32
}
33
,
34
"Transfer(address,address,uint256)": {
35
topic: abi.getEventTopic("Transfer(address,address,uint256)"),
36
decode(data: EvmEvent): TransferAddressAddressUint256Event {
37
const result = abi.decodeEventLog(
38
abi.getEvent("Transfer(address,address,uint256)"),
39
data.data || "",
40
data.topics
41
);
42
return {
43
from: result[0],
44
to: result[1],
45
tokenId: result[2],
46
}
47
}
48
}
49
,
50
}
Copied!
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).

What's next?

Head over to the Processor page, for more information on how processing Events impacts database Entities.
Last modified 20d ago