@subsquid/evm-typegen 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -3
- package/bin/run.js +1 -1
- package/lib/abi.support.d.ts +49 -0
- package/lib/abi.support.d.ts.map +1 -0
- package/lib/abi.support.js +100 -0
- package/lib/abi.support.js.map +1 -0
- package/lib/main.d.ts +1 -1
- package/lib/main.d.ts.map +1 -1
- package/lib/main.js +175 -21
- package/lib/main.js.map +1 -1
- package/lib/multicall.d.ts +31 -0
- package/lib/multicall.d.ts.map +1 -0
- package/lib/multicall.js +175 -0
- package/lib/multicall.js.map +1 -0
- package/lib/typegen.d.ts +13 -5
- package/lib/typegen.d.ts.map +1 -1
- package/lib/typegen.js +86 -254
- package/lib/typegen.js.map +1 -1
- package/lib/util/fetch.d.ts +7 -0
- package/lib/util/fetch.d.ts.map +1 -0
- package/lib/util/fetch.js +106 -0
- package/lib/util/fetch.js.map +1 -0
- package/lib/util/types.d.ts +7 -0
- package/lib/util/types.d.ts.map +1 -0
- package/lib/util/types.js +62 -0
- package/lib/util/types.js.map +1 -0
- package/package.json +11 -5
- package/src/abi.support.ts +115 -0
- package/src/main.ts +184 -24
- package/src/multicall.ts +212 -0
- package/src/typegen.ts +82 -290
- package/src/util/fetch.ts +82 -0
- package/src/util/types.ts +69 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getReturnType = exports.getStructType = exports.getTupleType = exports.getFullTupleType = exports.getType = void 0;
|
|
4
|
+
// taken from: https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/cli/src.ts/typescript.ts#L10
|
|
5
|
+
function getType(param) {
|
|
6
|
+
if (param.baseType === 'array') {
|
|
7
|
+
return 'Array<' + getType(param.arrayChildren) + '>';
|
|
8
|
+
}
|
|
9
|
+
if (param.baseType === 'tuple') {
|
|
10
|
+
return getFullTupleType(param.components);
|
|
11
|
+
}
|
|
12
|
+
if (param.type === 'address' || param.type === 'string') {
|
|
13
|
+
return 'string';
|
|
14
|
+
}
|
|
15
|
+
if (param.type === 'bool') {
|
|
16
|
+
return 'boolean';
|
|
17
|
+
}
|
|
18
|
+
let match = param.type.match(/^(u?int)([0-9]+)$/);
|
|
19
|
+
if (match) {
|
|
20
|
+
return parseInt(match[2]) < 53 ? 'number' : 'ethers.BigNumber';
|
|
21
|
+
}
|
|
22
|
+
if (param.type.substring(0, 5) === 'bytes') {
|
|
23
|
+
return 'string';
|
|
24
|
+
}
|
|
25
|
+
throw new Error('unknown type');
|
|
26
|
+
}
|
|
27
|
+
exports.getType = getType;
|
|
28
|
+
function getFullTupleType(params) {
|
|
29
|
+
let tuple = getTupleType(params);
|
|
30
|
+
let struct = getStructType(params);
|
|
31
|
+
if (struct == '{}') {
|
|
32
|
+
return tuple;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return `(${tuple} & ${struct})`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.getFullTupleType = getFullTupleType;
|
|
39
|
+
function getTupleType(params) {
|
|
40
|
+
return '[' + params.map(p => {
|
|
41
|
+
return p.name ? `${p.name}: ${getType(p)}` : getType(p);
|
|
42
|
+
}).join(', ') + ']';
|
|
43
|
+
}
|
|
44
|
+
exports.getTupleType = getTupleType;
|
|
45
|
+
// https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/abi/src.ts/coders/tuple.ts#L29
|
|
46
|
+
function getStructType(params) {
|
|
47
|
+
let array = [];
|
|
48
|
+
let counts = {};
|
|
49
|
+
for (let p of params) {
|
|
50
|
+
if (p.name && array[p.name] == null) {
|
|
51
|
+
counts[p.name] = (counts[p.name] || 0) + 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let fields = params.filter(p => counts[p.name] == 1);
|
|
55
|
+
return '{' + fields.map(f => `${f.name}: ${getType(f)}`).join(', ') + '}';
|
|
56
|
+
}
|
|
57
|
+
exports.getStructType = getStructType;
|
|
58
|
+
function getReturnType(outputs) {
|
|
59
|
+
return outputs.length == 1 ? getType(outputs[0]) : getFullTupleType(outputs);
|
|
60
|
+
}
|
|
61
|
+
exports.getReturnType = getReturnType;
|
|
62
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/util/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@subsquid/evm-typegen",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "CLI for generating typescript types and decode implementations for evm logs",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"repository": "git@github.com:subsquid/squid.git",
|
|
@@ -17,16 +17,22 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"commander": "^9.3.0",
|
|
20
|
-
"@ethersproject/abi": "^5.
|
|
21
|
-
"@
|
|
22
|
-
"@subsquid/
|
|
20
|
+
"@ethersproject/abi": "^5.7.0",
|
|
21
|
+
"@ethersproject/address": "^5.7.0",
|
|
22
|
+
"@subsquid/logger": "^0.3.0",
|
|
23
|
+
"@subsquid/util-internal-code-printer": "^0.1.0",
|
|
24
|
+
"@subsquid/util-internal-commander": "^0.0.1",
|
|
25
|
+
"@subsquid/util-internal": "^1.0.0",
|
|
26
|
+
"node-fetch": "^2.6.7"
|
|
23
27
|
},
|
|
24
28
|
"devDependencies": {
|
|
25
29
|
"@types/node": "^16.11.41",
|
|
30
|
+
"@types/node-fetch": "^2.6.2",
|
|
31
|
+
"ethers": "^5.7.2",
|
|
26
32
|
"typescript": "~4.7.4"
|
|
27
33
|
},
|
|
28
34
|
"scripts": {
|
|
29
35
|
"build": "rm -rf lib && tsc"
|
|
30
36
|
},
|
|
31
|
-
"readme": "# @subsquid/evm-typegen\n\
|
|
37
|
+
"readme": "# @subsquid/evm-typegen\n\nGenerates TypeScript facades for EVM transactions, logs and `eth_call` queries. \n\nThe generated facade classes are assumed to be used by [squids](https://docs.Subsquid.io/overview) indexing EVM data. \nThe generated classes depend on [ethers](https://www.npmjs.com/package/ethers).\n\n## Usage\n\n```\nnpm i -g @subsquid/evm-typegen\n```\n\n```\nArguments:\n output-dir output directory for generated definitions\n abi ABI file\n\nOptions:\n --multicall generate facade for MakerDAO multicall contract\n --etherscan-api <url> etherscan API to fetch contract ABI by a known address\n --clean delete output directory before run\n -h, --help display help for command\n\nABI file can be specified in three ways:\n\n1. as a plain JSON file:\n\nsquid-evm-typegen src/abi erc20.json\n\n2. as a contract address (to fetch ABI from etherscan)\n\nsquid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413\n\n3. as an arbitrary http url\n\nsquid-evm-typegen src/abi https://example.com/erc721.json\n\nIn all cases typegen will use ABI's basename as a basename of generated files.\nYou can overwrite basename of generated files using fragment (#) suffix.\n\nsquid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413#contract \n```\n"
|
|
32
38
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as ethers from 'ethers'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export interface LogRecord {
|
|
5
|
+
topics: string[]
|
|
6
|
+
data: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export class LogEvent<Args> {
|
|
11
|
+
private fragment: ethers.utils.EventFragment
|
|
12
|
+
|
|
13
|
+
constructor(private abi: ethers.utils.Interface, public readonly topic: string) {
|
|
14
|
+
this.fragment = abi.getEvent(topic)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
decode(rec: LogRecord): Args {
|
|
18
|
+
return this.abi.decodeEventLog(this.fragment, rec.data, rec.topics) as any as Args
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export class Func<Args extends any[], FieldArgs, Result> {
|
|
24
|
+
private fragment: ethers.utils.FunctionFragment
|
|
25
|
+
|
|
26
|
+
constructor(private abi: ethers.utils.Interface, public readonly sighash: string) {
|
|
27
|
+
this.fragment = abi.getFunction(sighash)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
decode(input: ethers.utils.BytesLike): Args & FieldArgs {
|
|
31
|
+
return this.abi.decodeFunctionData(this.fragment, input) as any as Args & FieldArgs
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
encode(args: Args): string {
|
|
35
|
+
return this.abi.encodeFunctionData(this.fragment, args)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
decodeResult(output: ethers.utils.BytesLike): Result {
|
|
39
|
+
const decoded = this.abi.decodeFunctionResult(this.fragment, output)
|
|
40
|
+
return decoded.length > 1 ? decoded : decoded[0]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
tryDecodeResult(output: ethers.utils.BytesLike): Result | undefined {
|
|
44
|
+
try {
|
|
45
|
+
return this.decodeResult(output)
|
|
46
|
+
} catch(err: any) {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
export function isFunctionResultDecodingError(val: unknown): val is Error & {data: string} {
|
|
54
|
+
if (!(val instanceof Error)) return false
|
|
55
|
+
let err = val as any
|
|
56
|
+
return err.code == 'CALL_EXCEPTION'
|
|
57
|
+
&& typeof err.data == 'string'
|
|
58
|
+
&& !err.errorArgs
|
|
59
|
+
&& !err.errorName
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
export interface ChainContext {
|
|
64
|
+
_chain: Chain
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
export interface BlockContext {
|
|
69
|
+
_chain: Chain
|
|
70
|
+
block: Block
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
export interface Block {
|
|
75
|
+
height: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
export interface Chain {
|
|
80
|
+
client: {
|
|
81
|
+
call: <T=any>(method: string, params?: unknown[]) => Promise<T>
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
export class ContractBase {
|
|
87
|
+
private readonly _chain: Chain
|
|
88
|
+
private readonly blockHeight: number
|
|
89
|
+
readonly address: string
|
|
90
|
+
|
|
91
|
+
constructor(ctx: BlockContext, address: string)
|
|
92
|
+
constructor(ctx: ChainContext, block: Block, address: string)
|
|
93
|
+
constructor(ctx: BlockContext, blockOrAddress: Block | string, address?: string) {
|
|
94
|
+
this._chain = ctx._chain
|
|
95
|
+
if (typeof blockOrAddress === 'string') {
|
|
96
|
+
this.blockHeight = ctx.block.height
|
|
97
|
+
this.address = ethers.utils.getAddress(blockOrAddress)
|
|
98
|
+
} else {
|
|
99
|
+
if (address == null) {
|
|
100
|
+
throw new Error('missing contract address')
|
|
101
|
+
}
|
|
102
|
+
this.blockHeight = blockOrAddress.height
|
|
103
|
+
this.address = ethers.utils.getAddress(address)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async eth_call<Args extends any[], FieldArgs, Result>(func: Func<Args, FieldArgs, Result>, args: Args): Promise<Result> {
|
|
108
|
+
let data = func.encode(args)
|
|
109
|
+
let result = await this._chain.client.call('eth_call', [
|
|
110
|
+
{to: this.address, data},
|
|
111
|
+
'0x'+this.blockHeight.toString(16)
|
|
112
|
+
])
|
|
113
|
+
return func.decodeResult(result)
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -1,34 +1,194 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import {Interface} from '@ethersproject/abi'
|
|
2
|
+
import {isAddress} from '@ethersproject/address'
|
|
3
|
+
import {createLogger} from '@subsquid/logger'
|
|
4
|
+
import {runProgram, wait} from '@subsquid/util-internal'
|
|
5
|
+
import {OutDir} from '@subsquid/util-internal-code-printer'
|
|
6
|
+
import * as validator from '@subsquid/util-internal-commander'
|
|
7
|
+
import {InvalidArgumentError, program} from 'commander'
|
|
8
|
+
import * as fs from 'fs'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
import {Typegen} from './typegen'
|
|
11
|
+
import {GET} from './util/fetch'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const LOG = createLogger('sqd:evm-typegen')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
runProgram(async function() {
|
|
18
|
+
program
|
|
19
|
+
.description(`
|
|
20
|
+
Generates TypeScript facades for EVM transactions, logs and eth_call queries.
|
|
21
|
+
|
|
22
|
+
The generated facades are assumed to be used by "squids" indexing EVM data.
|
|
10
23
|
`.trim())
|
|
11
|
-
.
|
|
12
|
-
.
|
|
24
|
+
.name('squid-evm-typegen')
|
|
25
|
+
.argument('<output-dir>', 'output directory for generated definitions')
|
|
26
|
+
.argument('[abi...]', 'ABI file', specArgument)
|
|
27
|
+
.option('--multicall', 'generate facade for MakerDAO multicall contract')
|
|
28
|
+
.option(
|
|
29
|
+
'--etherscan-api <url>',
|
|
30
|
+
'etherscan API to fetch contract ABI by a known address',
|
|
31
|
+
validator.Url(['http:', 'https:'])
|
|
32
|
+
)
|
|
33
|
+
.option('--clean', 'delete output directory before run')
|
|
34
|
+
.addHelpText('afterAll', `
|
|
35
|
+
ABI file can be specified in three ways:
|
|
36
|
+
|
|
37
|
+
1. as a plain JSON file:
|
|
38
|
+
|
|
39
|
+
squid-evm-typegen src/abi erc20.json
|
|
40
|
+
|
|
41
|
+
2. as a contract address (to fetch ABI from etherscan)
|
|
42
|
+
|
|
43
|
+
squid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413
|
|
44
|
+
|
|
45
|
+
3. as an arbitrary http url
|
|
46
|
+
|
|
47
|
+
squid-evm-typegen src/abi https://example.com/erc721.json
|
|
48
|
+
|
|
49
|
+
In all cases typegen will use ABI's basename as a basename of generated files.
|
|
50
|
+
You can overwrite basename of generated files using fragment (#) suffix.
|
|
51
|
+
|
|
52
|
+
squid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413#contract
|
|
53
|
+
`)
|
|
54
|
+
|
|
55
|
+
program.parse()
|
|
56
|
+
|
|
57
|
+
let opts = program.opts() as {
|
|
58
|
+
clean?: boolean,
|
|
59
|
+
multicall?: boolean,
|
|
60
|
+
etherscanApi?: string
|
|
61
|
+
}
|
|
62
|
+
let dest = new OutDir(program.processedArgs[0])
|
|
63
|
+
let specs = program.processedArgs[1] as Spec[]
|
|
64
|
+
|
|
65
|
+
if (opts.clean && dest.exists()) {
|
|
66
|
+
LOG.info(`deleting ${dest.path()}`)
|
|
67
|
+
dest.del()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (specs.length == 0 && !opts.multicall) {
|
|
71
|
+
LOG.warn('no ABI files given, nothing to generate')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
dest.add('abi.support.ts', [__dirname, '../src/abi.support.ts'])
|
|
76
|
+
LOG.info(`saved ${dest.path('abi.support.ts')}`)
|
|
77
|
+
|
|
78
|
+
if (opts.multicall) {
|
|
79
|
+
dest.add('multicall.ts', [__dirname, '../src/multicall.ts'])
|
|
80
|
+
LOG.info(`saved ${dest.path('multicall.ts')}`)
|
|
81
|
+
}
|
|
13
82
|
|
|
14
|
-
|
|
83
|
+
for (let spec of specs) {
|
|
84
|
+
LOG.info(`processing ${spec.src}`)
|
|
85
|
+
let abi_json = await read(spec, opts)
|
|
86
|
+
let abi = new Interface(abi_json)
|
|
87
|
+
new Typegen(dest, abi, spec.name, LOG).generate()
|
|
88
|
+
}
|
|
89
|
+
}, err => LOG.fatal(err))
|
|
15
90
|
|
|
16
|
-
const options = program.opts();
|
|
17
|
-
const inputPath = options.abi;
|
|
18
|
-
const outputPath = options.output;
|
|
19
91
|
|
|
20
|
-
|
|
21
|
-
|
|
92
|
+
async function read(spec: Spec, options?: {etherscanApi?: string}): Promise<any> {
|
|
93
|
+
if (spec.kind == 'address') {
|
|
94
|
+
return fetchFromEtherscan(spec.src, options?.etherscanApi)
|
|
95
|
+
}
|
|
96
|
+
let abi: any
|
|
97
|
+
if (spec.kind == 'url') {
|
|
98
|
+
abi = await GET(spec.src)
|
|
99
|
+
} else {
|
|
100
|
+
abi = JSON.parse(fs.readFileSync(spec.src, 'utf-8'))
|
|
22
101
|
}
|
|
102
|
+
if (Array.isArray(abi)) {
|
|
103
|
+
return abi
|
|
104
|
+
} else if (Array.isArray(abi?.abi)) {
|
|
105
|
+
return abi.abi
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error('Unrecognized ABI format')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async function fetchFromEtherscan(address: string, api?: string): Promise<any> {
|
|
113
|
+
api = api || 'https://api.etherscan.io/'
|
|
114
|
+
let url = new URL('api?module=contract&action=getabi', api)
|
|
115
|
+
url.searchParams.set('address', address)
|
|
116
|
+
let response: {status: string, result: string}
|
|
117
|
+
let attempts = 2
|
|
118
|
+
while (true) {
|
|
119
|
+
response = await GET(url.toString())
|
|
120
|
+
if (response.status == '0' && response.result.includes('rate limit') && --attempts) {
|
|
121
|
+
LOG.warn('faced rate limit error while trying to fetch contract ABI. Trying again in 2 seconds.')
|
|
122
|
+
await wait(2000)
|
|
123
|
+
} else {
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (response.status == '1') {
|
|
128
|
+
return JSON.parse(response.result)
|
|
129
|
+
} else {
|
|
130
|
+
throw new Error(`Failed to fetch contract ABI from ${api}: ${response.result}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
23
134
|
|
|
24
|
-
|
|
25
|
-
|
|
135
|
+
interface Spec {
|
|
136
|
+
kind: 'address' | 'url' | 'file'
|
|
137
|
+
src: string
|
|
138
|
+
name: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
function specArgument(value: string, prev: Spec[]): Spec[] {
|
|
143
|
+
let spec = parseSpec(value)
|
|
144
|
+
prev.push(spec)
|
|
145
|
+
return prev
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
function parseSpec(spec: string): Spec {
|
|
150
|
+
let [src, fragment] = splitFragment(spec)
|
|
151
|
+
if (src.startsWith('0x')) {
|
|
152
|
+
if (!isAddress(src)) throw new InvalidArgumentError('Invalid contract address')
|
|
153
|
+
return {
|
|
154
|
+
kind: 'address',
|
|
155
|
+
src,
|
|
156
|
+
name: fragment || src
|
|
157
|
+
}
|
|
158
|
+
} else if (src.includes('://')) {
|
|
159
|
+
let u = new URL(
|
|
160
|
+
validator.Url(['http:', 'https:'])(src)
|
|
161
|
+
)
|
|
162
|
+
return {
|
|
163
|
+
kind: 'url',
|
|
164
|
+
src,
|
|
165
|
+
name: fragment || basename(u.pathname)
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
return {
|
|
169
|
+
kind: 'file',
|
|
170
|
+
src,
|
|
171
|
+
name: fragment || basename(src)
|
|
172
|
+
}
|
|
26
173
|
}
|
|
174
|
+
}
|
|
175
|
+
|
|
27
176
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
177
|
+
function splitFragment(spec: string): [string, string] {
|
|
178
|
+
let parts = spec.split('#')
|
|
179
|
+
if (parts.length > 1) {
|
|
180
|
+
let fragment = parts.pop()!
|
|
181
|
+
return [parts.join('#'), fragment]
|
|
182
|
+
} else {
|
|
183
|
+
return [spec, '']
|
|
33
184
|
}
|
|
34
185
|
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
function basename(file: string): string {
|
|
189
|
+
let name = path.parse(file).name
|
|
190
|
+
if (name) return name
|
|
191
|
+
throw new InvalidArgumentError(
|
|
192
|
+
`Can't derive target basename for output files. Use url fragment to specify it, e.g. #erc20`
|
|
193
|
+
)
|
|
194
|
+
}
|
package/src/multicall.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import * as ethers from 'ethers'
|
|
2
|
+
import {ContractBase, Func} from './abi.support'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const abi = new ethers.utils.Interface([
|
|
6
|
+
{
|
|
7
|
+
type: 'function',
|
|
8
|
+
name: 'aggregate',
|
|
9
|
+
stateMutability: 'nonpayable',
|
|
10
|
+
inputs: [
|
|
11
|
+
{
|
|
12
|
+
name: 'calls',
|
|
13
|
+
type: 'tuple[]',
|
|
14
|
+
components: [
|
|
15
|
+
{name: 'target', type: 'address'},
|
|
16
|
+
{name: 'callData', type: 'bytes'},
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
outputs: [
|
|
21
|
+
{name: 'blockNumber', type: 'uint256'},
|
|
22
|
+
{name: 'returnData', type: 'bytes[]'},
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'tryAggregate',
|
|
27
|
+
type: 'function',
|
|
28
|
+
stateMutability: 'nonpayable',
|
|
29
|
+
inputs: [
|
|
30
|
+
{name: 'requireSuccess', type: 'bool'},
|
|
31
|
+
{
|
|
32
|
+
name: 'calls',
|
|
33
|
+
type: 'tuple[]',
|
|
34
|
+
components: [
|
|
35
|
+
{name: 'target', type: 'address'},
|
|
36
|
+
{name: 'callData', type: 'bytes'},
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
outputs: [
|
|
41
|
+
{
|
|
42
|
+
name: 'returnData',
|
|
43
|
+
type: 'tuple[]',
|
|
44
|
+
components: [
|
|
45
|
+
{name: 'success', type: 'bool'},
|
|
46
|
+
{name: 'returnData', type: 'bytes'},
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
type AnyFunc = Func<any, {}, any>
|
|
55
|
+
type Call = [address: string, bytes: string]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
const aggregate = new Func<[calls: Call[]], {}, {blockNumber: ethers.BigNumber, returnData: string[]}>(
|
|
59
|
+
abi, abi.getSighash('aggregate')
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const try_aggregate = new Func<[requireSuccess: boolean, calls: Array<[target: string, callData: string]>], {}, Array<{success: boolean, returnData: string}>>(
|
|
64
|
+
abi, abi.getSighash('tryAggregate')
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
export type MulticallResult<T> = {
|
|
69
|
+
success: true
|
|
70
|
+
value: T
|
|
71
|
+
} | {
|
|
72
|
+
success: false
|
|
73
|
+
returnData?: string
|
|
74
|
+
value?: undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export class Multicall extends ContractBase {
|
|
79
|
+
static aggregate = aggregate
|
|
80
|
+
static try_aggregate = try_aggregate
|
|
81
|
+
|
|
82
|
+
aggregate<Args extends any[], R>(
|
|
83
|
+
func: Func<Args, {}, R>,
|
|
84
|
+
address: string,
|
|
85
|
+
calls: Args[],
|
|
86
|
+
paging?: number
|
|
87
|
+
): Promise<R[]>
|
|
88
|
+
|
|
89
|
+
aggregate<Args extends any[], R>(
|
|
90
|
+
func: Func<Args, {}, R>,
|
|
91
|
+
calls: [address: string, args: Args][],
|
|
92
|
+
paging?: number
|
|
93
|
+
): Promise<R[]>
|
|
94
|
+
|
|
95
|
+
aggregate(
|
|
96
|
+
calls: [func: AnyFunc, address: string, args: any[]][],
|
|
97
|
+
paging?: number
|
|
98
|
+
): Promise<any[]>
|
|
99
|
+
|
|
100
|
+
async aggregate(...args: any[]): Promise<any[]> {
|
|
101
|
+
let [calls, funcs, page] = this.makeCalls(args)
|
|
102
|
+
let size = calls.length
|
|
103
|
+
let results = new Array(size)
|
|
104
|
+
for (let [from, to] of splitIntoPages(size, page)) {
|
|
105
|
+
let {returnData} = await this.eth_call(aggregate, [calls.slice(from, to)])
|
|
106
|
+
for (let i = from; i < to; i++) {
|
|
107
|
+
let data = returnData[i - from]
|
|
108
|
+
results[i] = funcs[i].decodeResult(data)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
tryAggregate<Args extends any[], R>(
|
|
115
|
+
func: Func<Args, {}, R>,
|
|
116
|
+
address: string,
|
|
117
|
+
calls: Args[],
|
|
118
|
+
paging?: number
|
|
119
|
+
): Promise<MulticallResult<R>[]>
|
|
120
|
+
|
|
121
|
+
tryAggregate<Args extends any[], R>(
|
|
122
|
+
func: Func<Args, {}, R>,
|
|
123
|
+
calls: [address: string, args: Args][],
|
|
124
|
+
paging?: number
|
|
125
|
+
): Promise<MulticallResult<R>[]>
|
|
126
|
+
|
|
127
|
+
tryAggregate(
|
|
128
|
+
calls: [func: AnyFunc, address: string, args: any[]][],
|
|
129
|
+
paging?: number
|
|
130
|
+
): Promise<MulticallResult<any>[]>
|
|
131
|
+
|
|
132
|
+
async tryAggregate(...args: any[]): Promise<any[]> {
|
|
133
|
+
let [calls, funcs, page] = this.makeCalls(args)
|
|
134
|
+
let size = calls.length
|
|
135
|
+
let results = new Array(size)
|
|
136
|
+
for (let [from, to] of splitIntoPages(size, page)) {
|
|
137
|
+
let response = await this.eth_call(try_aggregate, [false, calls.slice(from, to)])
|
|
138
|
+
for (let i = from; i < to; i++) {
|
|
139
|
+
let res = response[i - from]
|
|
140
|
+
if (res.success) {
|
|
141
|
+
try {
|
|
142
|
+
results[i] = {
|
|
143
|
+
success: true,
|
|
144
|
+
value: funcs[i].decodeResult(res.returnData)
|
|
145
|
+
}
|
|
146
|
+
} catch(err: any) {
|
|
147
|
+
results[i] = {success: false, returnData: res.returnData}
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
results[i] = {success: false}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return results
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private makeCalls(args: any[]): [calls: Call[], funcs: AnyFunc[], page: number] {
|
|
158
|
+
let page = typeof args[args.length-1] == 'number' ? args.pop()! : Number.MAX_SAFE_INTEGER
|
|
159
|
+
switch(args.length) {
|
|
160
|
+
case 1: {
|
|
161
|
+
let list: [func: AnyFunc, address: string, args: any[]][] = args[0]
|
|
162
|
+
let calls = new Array(list.length)
|
|
163
|
+
let funcs = new Array(list.length)
|
|
164
|
+
for (let i = 0; i < list.length; i++) {
|
|
165
|
+
let [func, address, args] = list[i]
|
|
166
|
+
calls[i] = [address, func.encode(args)]
|
|
167
|
+
funcs[i] = func
|
|
168
|
+
}
|
|
169
|
+
return [calls, funcs, page]
|
|
170
|
+
}
|
|
171
|
+
case 2: {
|
|
172
|
+
let func: AnyFunc = args[0]
|
|
173
|
+
let list: [address: string, args: any[]][] = args[1]
|
|
174
|
+
let calls = new Array(list.length)
|
|
175
|
+
let funcs = new Array(list.length)
|
|
176
|
+
for (let i = 0; i < list.length; i++) {
|
|
177
|
+
let [address, args] = list[i]
|
|
178
|
+
calls[i] = [address, func.encode(args)]
|
|
179
|
+
funcs[i] = func
|
|
180
|
+
}
|
|
181
|
+
return [calls, funcs, page]
|
|
182
|
+
}
|
|
183
|
+
case 3: {
|
|
184
|
+
let func: AnyFunc = args[0]
|
|
185
|
+
let address: string = args[1]
|
|
186
|
+
let list: any[][] = args[2]
|
|
187
|
+
let calls = new Array(list.length)
|
|
188
|
+
let funcs = new Array(list.length)
|
|
189
|
+
for (let i = 0; i < list.length; i++) {
|
|
190
|
+
let args = list[i]
|
|
191
|
+
calls[i] = [address, func.encode(args)]
|
|
192
|
+
funcs[i] = func
|
|
193
|
+
}
|
|
194
|
+
return [calls, funcs, page]
|
|
195
|
+
}
|
|
196
|
+
default:
|
|
197
|
+
throw new Error('unexpected number of arguments')
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
function* splitIntoPages(size: number, page: number): Iterable<[from: number, to: number]> {
|
|
204
|
+
let from = 0
|
|
205
|
+
while (size) {
|
|
206
|
+
let step = Math.min(page, size)
|
|
207
|
+
let to = from + step
|
|
208
|
+
yield [from, to]
|
|
209
|
+
size -= step
|
|
210
|
+
from = to
|
|
211
|
+
}
|
|
212
|
+
}
|