@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/src/main.ts CHANGED
@@ -1,45 +1,54 @@
1
- import * as fs from 'fs'
2
- import path from 'path'
3
- import { InvalidArgumentError, program } from 'commander'
4
- import { createLogger } from '@subsquid/logger'
5
- import { runProgram, wait } from '@subsquid/util-internal'
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 { Typegen } from './typegen'
8
- import { GET } from './util/fetch'
9
- import { OutDir } from '@subsquid/util-internal-code-printer'
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
- async function () {
15
- program
16
- .description(
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
- .name('squid-evm-typegen')
24
- .argument('<output-dir>', 'output directory for generated definitions')
25
- .argument('[abi...]', 'ABI file', specArgument)
26
- .option('--multicall', 'generate facade for MakerDAO multicall contract')
27
- .option(
28
- '--etherscan-api <url>',
29
- 'etherscan API to fetch contract ABI by a known address',
30
- validator.Url(['http:', 'https:']),
31
- 'https://api.etherscan.io/v2'
32
- )
33
- .option('--etherscan-api-key <key>', 'etherscan API key')
34
- .option(
35
- '--etherscan-chain-id <id>',
36
- 'the id of the chain to fetch the contract from',
37
- validator.positiveInt,
38
- )
39
- .option('--clean', 'delete output directory before run')
40
- .addHelpText(
41
- 'afterAll',
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
- program.parse()
73
+ program.parse()
65
74
 
66
- let opts = program.opts() as {
67
- clean?: boolean
68
- multicall?: boolean
69
- etherscanApi: string
70
- etherscanApiKey?: string
71
- etherscanChainId?: string
72
- }
73
- let dest = new OutDir(program.processedArgs[0])
74
- let specs = program.processedArgs[1] as Spec[]
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
- if (opts.clean && dest.exists()) {
77
- LOG.info(`deleting ${dest.path()}`)
78
- dest.del()
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
- if (specs.length == 0 && !opts.multicall) {
82
- LOG.warn('no ABI files given, nothing to generate')
83
- return
84
- }
97
+ if (opts.clean && dest.exists()) {
98
+ LOG.info(`deleting ${dest.path()}`)
99
+ dest.del()
100
+ }
85
101
 
86
- if (opts.multicall) {
87
- dest.add('multicall.ts', [__dirname, '../src/multicall.ts'])
88
- LOG.info(`saved ${dest.path('multicall.ts')}`)
89
- }
102
+ if (specs.length == 0 && !opts.multicall) {
103
+ LOG.warn('no ABI files given, nothing to generate')
104
+ return
105
+ }
90
106
 
91
- for (let spec of specs) {
92
- LOG.info(`processing ${spec.src}`)
93
- let abi_json = await read(spec, opts)
94
- await new Typegen(dest, abi_json, spec.name, LOG).generate()
95
- }
96
- },
97
- (err) => LOG.fatal(err),
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
- spec: Spec,
102
- options: {
103
- etherscanApi: string;
104
- etherscanChainId?: string
105
- etherscanApiKey?: string,
106
- },
107
- ): Promise<any> {
108
- if (spec.kind == 'address') {
109
- return fetchFromEtherscan(spec.src, getEtherscanAPIConfig(options))
110
- }
111
- let abi: any
112
- if (spec.kind == 'url') {
113
- abi = await GET(spec.src)
114
- } else {
115
- abi = JSON.parse(fs.readFileSync(spec.src, 'utf-8'))
116
- }
117
- if (Array.isArray(abi)) {
118
- return abi
119
- } else if (Array.isArray(abi?.abi)) {
120
- return abi.abi
121
- } else {
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
- async function fetchFromEtherscan(
127
- address: string,
128
- config: EtherscanAPIConfig,
129
- ): Promise<any> {
130
- let api = config.api + (config.api.endsWith('/') ? '' : '/') + 'api'
131
- let url = new URL(api)
132
-
133
- let params = new URLSearchParams({
134
- module: 'contract',
135
- action: 'getabi',
136
- address,
137
- })
138
- if (config.chainId) {
139
- params.set('chainid', config.chainId);
140
- }
141
- if (config.apiKey) {
142
- params.set('apiKey', config.apiKey);
143
- }
144
- url.search = params.toString()
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
- if (response.status == '1') {
166
- return JSON.parse(response.result)
167
- } else {
168
- throw new Error(
169
- `Failed to fetch contract ABI from ${config.api}: ${response.result}`,
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
- kind: 'address' | 'url' | 'file'
176
- src: string
177
- name: string
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
- let spec = parseSpec(value)
182
- prev = prev || []
183
- prev.push(spec)
184
- return prev
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
- return spec.match(/^0x[0-9a-fA-F]{40}$/) !== null
208
+ return spec.match(/^0x[0-9a-fA-F]{40}$/) !== null
189
209
  }
190
210
 
191
211
  function parseSpec(spec: string): Spec {
192
- let [src, fragment] = splitFragment(spec)
193
- if (src.startsWith('0x')) {
194
- if (!isAddress(src))
195
- throw new InvalidArgumentError('Invalid contract address')
196
- return {
197
- kind: 'address',
198
- src,
199
- name: fragment || src,
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
- } else if (src.includes('://')) {
202
- let u = new URL(validator.Url(['http:', 'https:'])(src))
203
- return {
204
- kind: 'url',
205
- src,
206
- name: fragment || basename(u.pathname),
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
- kind: 'file',
211
- src,
212
- name: fragment || basename(src),
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
- let parts = spec.split('#')
219
- if (parts.length > 1) {
220
- let fragment = parts.pop()!
221
- return [parts.join('#'), fragment]
222
- } else {
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
- let name = path.parse(file).name
229
- if (name) return name
230
- throw new InvalidArgumentError(
231
- `Can't derive target basename for output files. Use url fragment to specify it, e.g. #erc20`,
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: string
260
+ etherscanApi?: string
243
261
  etherscanApiKey?: string
244
- etherscanChainId?: string
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: options.etherscanApi || 'https://api.etherscan.io/v2',
274
+ api,
248
275
  apiKey: options.etherscanApiKey || undefined,
249
- chainId: options.etherscanChainId || (options.etherscanApi ? undefined : '1'),
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
+ }