@subsquid/evm-typegen 4.5.1 → 5.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/lib/abi.support.d.ts +3 -0
- package/lib/abi.support.d.ts.map +1 -0
- package/lib/abi.support.js +9 -0
- package/lib/abi.support.js.map +1 -0
- package/lib/chainIds.d.ts +3 -0
- package/lib/chainIds.d.ts.map +1 -0
- package/lib/chainIds.js +268 -0
- package/lib/chainIds.js.map +1 -0
- package/lib/description.d.ts +76 -0
- package/lib/description.d.ts.map +1 -0
- package/lib/description.js +156 -0
- package/lib/description.js.map +1 -0
- package/lib/main.js +68 -42
- package/lib/main.js.map +1 -1
- package/lib/multicall.d.ts +17 -13
- package/lib/multicall.d.ts.map +1 -1
- package/lib/multicall.js +9 -7
- package/lib/multicall.js.map +1 -1
- package/lib/typegen.d.ts +8 -23
- package/lib/typegen.d.ts.map +1 -1
- package/lib/typegen.js +267 -187
- package/lib/typegen.js.map +1 -1
- package/package.json +11 -10
- package/src/abi.support.ts +2 -0
- package/src/chainIds.ts +315 -0
- package/src/description.ts +221 -0
- package/src/main.ts +209 -174
- package/src/multicall.ts +171 -159
- package/src/typegen.ts +276 -227
- package/lib/util/types.d.ts +0 -3
- package/lib/util/types.d.ts.map +0 -1
- package/lib/util/types.js +0 -46
- package/lib/util/types.js.map +0 -1
- package/src/util/types.ts +0 -54
package/src/main.ts
CHANGED
|
@@ -1,45 +1,54 @@
|
|
|
1
|
-
import * as fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import {InvalidArgumentError, InvalidOptionArgumentError, program} from 'commander'
|
|
4
|
+
import {createLogger} from '@subsquid/logger'
|
|
5
|
+
import {runProgram, wait} from '@subsquid/util-internal'
|
|
6
6
|
import * as validator from '@subsquid/util-internal-commander'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import {Typegen} from './typegen'
|
|
8
|
+
import type {NatSpec} from './description'
|
|
9
|
+
import {GET} from './util/fetch'
|
|
10
|
+
import {OutDir} from '@subsquid/util-internal-code-printer'
|
|
11
|
+
import {chainIdOption} from './chainIds'
|
|
10
12
|
|
|
11
13
|
const LOG = createLogger('sqd:evm-typegen')
|
|
14
|
+
const PROXY_ETHERSCAN = 'https://cloud.sqd.dev/chains/api/v1/evm/abi'
|
|
15
|
+
const ORIGIN_ETHERSCAN = 'https://api.etherscan.io/v2/api'
|
|
12
16
|
|
|
13
17
|
runProgram(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
async function () {
|
|
19
|
+
program
|
|
20
|
+
.description(
|
|
21
|
+
`
|
|
18
22
|
Generates TypeScript facades for EVM transactions, logs and eth_call queries.
|
|
19
23
|
|
|
20
24
|
The generated facades are assumed to be used by "squids" indexing EVM data.
|
|
21
25
|
`.trim(),
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
)
|
|
27
|
+
.name('squid-evm-typegen')
|
|
28
|
+
.argument('<output-dir>', 'output directory for generated definitions')
|
|
29
|
+
.argument('[abi...]', 'ABI file', specArgument)
|
|
30
|
+
.option('--multicall', 'generate facade for MakerDAO multicall contract')
|
|
31
|
+
.option(
|
|
32
|
+
'--etherscan-api <url>',
|
|
33
|
+
'etherscan API to fetch contract ABI by a known address\n(if no API token is provided, the default value equals to SQD Proxy service, otherwise equals to Etherscan API)',
|
|
34
|
+
validator.Url(['http:', 'https:']),
|
|
35
|
+
)
|
|
36
|
+
.option('--etherscan-api-key <key>', 'etherscan API key')
|
|
37
|
+
.option(
|
|
38
|
+
'--chain-id <id>',
|
|
39
|
+
'chain ID (numeric or named, e.g., "1" or "ethereum") to fetch the contract from',
|
|
40
|
+
chainIdOption,
|
|
41
|
+
1,
|
|
42
|
+
)
|
|
43
|
+
.option(
|
|
44
|
+
'--etherscan-chain-id <id>',
|
|
45
|
+
'DEPRECATED: use --chain-id instead. Chain ID (numeric or named, e.g., "1" or "ethereum") to fetch the contract from',
|
|
46
|
+
chainIdOption,
|
|
47
|
+
)
|
|
48
|
+
.option('--clean', 'delete output directory before run')
|
|
49
|
+
.addHelpText(
|
|
50
|
+
'afterAll',
|
|
51
|
+
`
|
|
43
52
|
ABI file can be specified in three ways:
|
|
44
53
|
|
|
45
54
|
1. as a plain JSON file:
|
|
@@ -59,193 +68,219 @@ You can overwrite basename of generated files using fragment (#) suffix.
|
|
|
59
68
|
|
|
60
69
|
squid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413#contract
|
|
61
70
|
`,
|
|
62
|
-
|
|
71
|
+
)
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
program.parse()
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
let opts = program.opts() as {
|
|
76
|
+
clean?: boolean
|
|
77
|
+
multicall?: boolean
|
|
78
|
+
etherscanApi?: string
|
|
79
|
+
etherscanApiKey?: string
|
|
80
|
+
chainId?: number
|
|
81
|
+
etherscanChainId?: number
|
|
82
|
+
}
|
|
83
|
+
let dest = new OutDir(program.processedArgs[0])
|
|
84
|
+
let specs = program.processedArgs[1] as Spec[]
|
|
75
85
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
if (opts.etherscanChainId) {
|
|
87
|
+
LOG.warn('Option --etherscan-chain-id is deprecated. Please use --chain-id instead')
|
|
88
|
+
if (opts.chainId) {
|
|
89
|
+
throw new InvalidOptionArgumentError(
|
|
90
|
+
'Option --chain-id and --etherscan-chain-id cannot be used together',
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
opts.chainId = opts.etherscanChainId
|
|
94
|
+
opts.etherscanChainId = undefined
|
|
95
|
+
}
|
|
80
96
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
if (opts.clean && dest.exists()) {
|
|
98
|
+
LOG.info(`deleting ${dest.path()}`)
|
|
99
|
+
dest.del()
|
|
100
|
+
}
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
if (specs.length == 0 && !opts.multicall) {
|
|
103
|
+
LOG.warn('no ABI files given, nothing to generate')
|
|
104
|
+
return
|
|
105
|
+
}
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
if (opts.multicall) {
|
|
108
|
+
dest.add('multicall.ts', [__dirname, '../src/multicall.ts'])
|
|
109
|
+
LOG.info(`saved ${dest.path('multicall.ts')}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (specs.length > 0) {
|
|
113
|
+
dest.add('abi.support.ts', [__dirname, '../src/abi.support.ts'])
|
|
114
|
+
LOG.info(`saved ${dest.path('abi.support.ts')}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (let spec of specs) {
|
|
118
|
+
LOG.info(`processing ${spec.src}`)
|
|
119
|
+
let {abi, natspec} = await read(spec, opts)
|
|
120
|
+
await new Typegen(dest, abi, spec.name, LOG, natspec).generate()
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
(err) => LOG.fatal(err),
|
|
98
124
|
)
|
|
99
125
|
|
|
100
126
|
async function read(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
): Promise<any> {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
spec: Spec,
|
|
128
|
+
options: {
|
|
129
|
+
etherscanApi?: string
|
|
130
|
+
chainId?: number
|
|
131
|
+
etherscanApiKey?: string
|
|
132
|
+
},
|
|
133
|
+
): Promise<{abi: any; natspec?: NatSpec}> {
|
|
134
|
+
if (spec.kind == 'address') {
|
|
135
|
+
return {abi: await fetchFromEtherscan(spec.src, getEtherscanAPIConfig(options))}
|
|
136
|
+
}
|
|
137
|
+
let raw: any
|
|
138
|
+
if (spec.kind == 'url') {
|
|
139
|
+
raw = await GET(spec.src)
|
|
140
|
+
} else {
|
|
141
|
+
raw = JSON.parse(fs.readFileSync(spec.src, 'utf-8'))
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(raw)) {
|
|
144
|
+
return {abi: raw}
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(raw?.abi)) {
|
|
147
|
+
return {abi: raw.abi, natspec: extractNatSpec(raw)}
|
|
148
|
+
}
|
|
122
149
|
throw new Error('Unrecognized ABI format')
|
|
123
|
-
}
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let response: { status: string; result: string }
|
|
147
|
-
let attempts = 0
|
|
148
|
-
while (true) {
|
|
149
|
-
response = await GET(url.toString())
|
|
150
|
-
if (
|
|
151
|
-
response.status == '0' &&
|
|
152
|
-
response.result.includes('rate limit') &&
|
|
153
|
-
attempts < 4
|
|
154
|
-
) {
|
|
155
|
-
attempts += 1
|
|
156
|
-
let timeout = attempts * 2
|
|
157
|
-
LOG.warn(
|
|
158
|
-
`faced rate limit error while trying to fetch contract ABI. Trying again in ${timeout} seconds.`,
|
|
159
|
-
)
|
|
160
|
-
await wait(timeout * 1000)
|
|
161
|
-
} else {
|
|
162
|
-
break
|
|
152
|
+
function extractNatSpec(artifact: any): NatSpec | undefined {
|
|
153
|
+
const userdoc = artifact.userdoc
|
|
154
|
+
const devdoc = artifact.devdoc
|
|
155
|
+
if (!userdoc && !devdoc) return undefined
|
|
156
|
+
return {userdoc, devdoc}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function fetchFromEtherscan(address: string, config: EtherscanAPIConfig): Promise<any> {
|
|
160
|
+
let url = new URL(config.api)
|
|
161
|
+
|
|
162
|
+
let params = new URLSearchParams({
|
|
163
|
+
module: 'contract',
|
|
164
|
+
action: 'getabi',
|
|
165
|
+
address,
|
|
166
|
+
chainid: config.chainId.toString(),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
if (config.apiKey) {
|
|
170
|
+
params.set('apiKey', config.apiKey)
|
|
163
171
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
|
|
173
|
+
url.search = params.toString()
|
|
174
|
+
|
|
175
|
+
let response: {status: string; result: string}
|
|
176
|
+
let attempts = 0
|
|
177
|
+
while (true) {
|
|
178
|
+
response = await GET(url.toString())
|
|
179
|
+
if (response.status == '0' && response.result.includes('rate limit') && attempts < 4) {
|
|
180
|
+
attempts += 1
|
|
181
|
+
let timeout = attempts * 2
|
|
182
|
+
LOG.warn(`faced rate limit error while trying to fetch contract ABI. Trying again in ${timeout} seconds.`)
|
|
183
|
+
await wait(timeout * 1000)
|
|
184
|
+
} else {
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (response.status == '1') {
|
|
189
|
+
return JSON.parse(response.result)
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`Failed to fetch contract ABI from ${config.api}: ${response.result}`)
|
|
172
192
|
}
|
|
173
193
|
|
|
174
194
|
interface Spec {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
kind: 'address' | 'url' | 'file'
|
|
196
|
+
src: string
|
|
197
|
+
name: string
|
|
178
198
|
}
|
|
179
199
|
|
|
180
200
|
function specArgument(value: string, prev?: Spec[]): Spec[] {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
let spec = parseSpec(value)
|
|
202
|
+
prev = prev || []
|
|
203
|
+
prev.push(spec)
|
|
204
|
+
return prev
|
|
185
205
|
}
|
|
186
206
|
|
|
187
207
|
function isAddress(spec: string): boolean {
|
|
188
|
-
|
|
208
|
+
return spec.match(/^0x[0-9a-fA-F]{40}$/) !== null
|
|
189
209
|
}
|
|
190
210
|
|
|
191
211
|
function parseSpec(spec: string): Spec {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
let [src, fragment] = splitFragment(spec)
|
|
213
|
+
if (src.startsWith('0x')) {
|
|
214
|
+
if (!isAddress(src)) throw new InvalidArgumentError('Invalid contract address')
|
|
215
|
+
return {
|
|
216
|
+
kind: 'address',
|
|
217
|
+
src,
|
|
218
|
+
name: fragment || src,
|
|
219
|
+
}
|
|
200
220
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
221
|
+
if (src.includes('://')) {
|
|
222
|
+
let u = new URL(validator.Url(['http:', 'https:'])(src))
|
|
223
|
+
return {
|
|
224
|
+
kind: 'url',
|
|
225
|
+
src,
|
|
226
|
+
name: fragment || basename(u.pathname),
|
|
227
|
+
}
|
|
207
228
|
}
|
|
208
|
-
} else {
|
|
209
229
|
return {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
kind: 'file',
|
|
231
|
+
src,
|
|
232
|
+
name: fragment || basename(src),
|
|
213
233
|
}
|
|
214
|
-
}
|
|
215
234
|
}
|
|
216
235
|
|
|
217
236
|
function splitFragment(spec: string): [string, string] {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
237
|
+
let parts = spec.split('#')
|
|
238
|
+
if (parts.length > 1) {
|
|
239
|
+
let fragment = parts.pop()!
|
|
240
|
+
return [parts.join('#'), fragment]
|
|
241
|
+
}
|
|
223
242
|
return [spec, '']
|
|
224
|
-
}
|
|
225
243
|
}
|
|
226
244
|
|
|
227
245
|
function basename(file: string): string {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
246
|
+
let name = path.parse(file).name
|
|
247
|
+
if (name) return name
|
|
248
|
+
throw new InvalidArgumentError(
|
|
249
|
+
`Can't derive target basename for output files. Use url fragment to specify it, e.g. #erc20`,
|
|
250
|
+
)
|
|
233
251
|
}
|
|
234
252
|
|
|
235
253
|
interface EtherscanAPIConfig {
|
|
236
254
|
api: string
|
|
255
|
+
chainId: number
|
|
237
256
|
apiKey?: string
|
|
238
|
-
chainId?: string
|
|
239
257
|
}
|
|
240
258
|
|
|
241
259
|
function getEtherscanAPIConfig(options: {
|
|
242
|
-
etherscanApi
|
|
260
|
+
etherscanApi?: string
|
|
243
261
|
etherscanApiKey?: string
|
|
244
|
-
|
|
262
|
+
chainId?: number
|
|
245
263
|
}): EtherscanAPIConfig {
|
|
264
|
+
let api: string
|
|
265
|
+
if (options.etherscanApi != null) {
|
|
266
|
+
api = normalizeEtherscanAPIUrl(options.etherscanApi)
|
|
267
|
+
} else if (options.etherscanApiKey != null) {
|
|
268
|
+
api = ORIGIN_ETHERSCAN
|
|
269
|
+
} else {
|
|
270
|
+
api = PROXY_ETHERSCAN
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
return {
|
|
247
|
-
api
|
|
274
|
+
api,
|
|
248
275
|
apiKey: options.etherscanApiKey || undefined,
|
|
249
|
-
chainId: options.
|
|
276
|
+
chainId: options.chainId ?? 1,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeEtherscanAPIUrl(url: string) {
|
|
281
|
+
if (url.endsWith('/api')) {
|
|
282
|
+
return url
|
|
250
283
|
}
|
|
251
|
-
|
|
284
|
+
|
|
285
|
+
return url.endsWith('/') ? `${url}api` : `${url}/api`
|
|
286
|
+
}
|