@subsquid/evm-typegen 4.6.0 → 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/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 +36 -35
- 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 +9 -9
- package/src/abi.support.ts +2 -0
- package/src/description.ts +221 -0
- package/src/main.ts +195 -194
- 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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {keccak256} from '@subsquid/evm-abi'
|
|
2
|
+
import type {Abi, AbiEvent, AbiFunction, AbiParameter} from 'abitype'
|
|
3
|
+
|
|
4
|
+
export interface ContractDef {
|
|
5
|
+
events: EventDef[]
|
|
6
|
+
functions: FunctionDef[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DocDef {
|
|
10
|
+
notice?: string
|
|
11
|
+
dev?: string
|
|
12
|
+
params?: Record<string, string>
|
|
13
|
+
returns?: Record<string, string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** NatSpec documentation extracted from a compilation artifact's userdoc/devdoc fields. */
|
|
17
|
+
export interface NatSpec {
|
|
18
|
+
userdoc?: {
|
|
19
|
+
methods?: Record<string, {notice?: string}>
|
|
20
|
+
events?: Record<string, {notice?: string}>
|
|
21
|
+
}
|
|
22
|
+
devdoc?: {
|
|
23
|
+
methods?: Record<string, {details?: string; params?: Record<string, string>; returns?: Record<string, string>}>
|
|
24
|
+
events?: Record<string, {details?: string; params?: Record<string, string>}>
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EventDef {
|
|
29
|
+
name: string
|
|
30
|
+
signature: string
|
|
31
|
+
topic: string
|
|
32
|
+
inputs: FieldDef[]
|
|
33
|
+
key: string
|
|
34
|
+
typeName: string
|
|
35
|
+
docs?: DocDef
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FunctionDef {
|
|
39
|
+
name: string
|
|
40
|
+
signature: string
|
|
41
|
+
selector: string
|
|
42
|
+
inputs: FieldDef[]
|
|
43
|
+
outputs: FieldDef[]
|
|
44
|
+
key: string
|
|
45
|
+
paramsTypeName: string
|
|
46
|
+
returnTypeName: string
|
|
47
|
+
docs?: DocDef
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FieldDef {
|
|
51
|
+
name: string
|
|
52
|
+
type: TypeDef
|
|
53
|
+
indexed?: boolean
|
|
54
|
+
doc?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type TypeDef =
|
|
58
|
+
| {kind: 'primitive'; name: string}
|
|
59
|
+
| {kind: 'array'; item: TypeDef}
|
|
60
|
+
| {kind: 'fixedArray'; item: TypeDef; size: number}
|
|
61
|
+
| {kind: 'tuple'; fields: FieldDef[]}
|
|
62
|
+
|
|
63
|
+
export function describe(abi: Abi, natspec?: NatSpec): ContractDef {
|
|
64
|
+
const rawEvents = abi.filter((x) => x.type === 'event') as AbiEvent[]
|
|
65
|
+
const rawFunctions = abi.filter((x) => x.type === 'function') as AbiFunction[]
|
|
66
|
+
|
|
67
|
+
const eventSuffix = overloadSuffixer(rawEvents)
|
|
68
|
+
const functionSuffix = overloadSuffixer(rawFunctions)
|
|
69
|
+
|
|
70
|
+
const events: EventDef[] = rawEvents.map((e) => {
|
|
71
|
+
const signature = eventSignature(e)
|
|
72
|
+
const docs = buildEventDocs(signature, natspec)
|
|
73
|
+
const inputs = e.inputs.map((p, i) => toFieldDef(p, i, true))
|
|
74
|
+
annotateParamDocs(inputs, docs?.params)
|
|
75
|
+
return {
|
|
76
|
+
name: e.name,
|
|
77
|
+
signature,
|
|
78
|
+
topic: `0x${keccak256(signature).toString('hex')}`,
|
|
79
|
+
inputs,
|
|
80
|
+
key: eventSuffix(e, e.name),
|
|
81
|
+
typeName: eventSuffix(e, `${capitalize(e.name)}EventArgs`),
|
|
82
|
+
docs,
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const functions: FunctionDef[] = rawFunctions.map((f) => {
|
|
87
|
+
const signature = fnSignature(f)
|
|
88
|
+
const docs = buildFunctionDocs(signature, natspec)
|
|
89
|
+
const inputs = f.inputs.map((p, i) => toFieldDef(p, i, false))
|
|
90
|
+
const outputs = (f.outputs ?? []).map((p, i) => toFieldDef(p, i, false))
|
|
91
|
+
annotateParamDocs(inputs, docs?.params)
|
|
92
|
+
annotateReturnDocs(outputs, docs?.returns)
|
|
93
|
+
return {
|
|
94
|
+
name: f.name,
|
|
95
|
+
signature,
|
|
96
|
+
selector: `0x${keccak256(signature).slice(0, 4).toString('hex')}`,
|
|
97
|
+
inputs,
|
|
98
|
+
outputs,
|
|
99
|
+
key: functionSuffix(f, f.name),
|
|
100
|
+
paramsTypeName: functionSuffix(f, `${capitalize(f.name)}Params`),
|
|
101
|
+
returnTypeName: functionSuffix(f, `${capitalize(f.name)}Return`),
|
|
102
|
+
docs,
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return {events, functions}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildEventDocs(signature: string, natspec: NatSpec | undefined): DocDef | undefined {
|
|
110
|
+
const notice = natspec?.userdoc?.events?.[signature]?.notice
|
|
111
|
+
const devEntry = natspec?.devdoc?.events?.[signature]
|
|
112
|
+
if (!notice && !devEntry) return undefined
|
|
113
|
+
return filterEmptyDoc({
|
|
114
|
+
notice,
|
|
115
|
+
dev: devEntry?.details,
|
|
116
|
+
params: devEntry?.params,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildFunctionDocs(signature: string, natspec: NatSpec | undefined): DocDef | undefined {
|
|
121
|
+
const notice = natspec?.userdoc?.methods?.[signature]?.notice
|
|
122
|
+
const devEntry = natspec?.devdoc?.methods?.[signature]
|
|
123
|
+
if (!notice && !devEntry) return undefined
|
|
124
|
+
return filterEmptyDoc({
|
|
125
|
+
notice,
|
|
126
|
+
dev: devEntry?.details,
|
|
127
|
+
params: devEntry?.params,
|
|
128
|
+
returns: devEntry?.returns,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function filterEmptyDoc(doc: DocDef): DocDef | undefined {
|
|
133
|
+
const hasContent =
|
|
134
|
+
doc.notice != null ||
|
|
135
|
+
doc.dev != null ||
|
|
136
|
+
(doc.params != null && Object.keys(doc.params).length > 0) ||
|
|
137
|
+
(doc.returns != null && Object.keys(doc.returns).length > 0)
|
|
138
|
+
return hasContent ? doc : undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function annotateParamDocs(fields: FieldDef[], params: Record<string, string> | undefined): void {
|
|
142
|
+
if (!params) return
|
|
143
|
+
for (const field of fields) {
|
|
144
|
+
const doc = params[field.name]
|
|
145
|
+
if (doc) field.doc = doc
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function annotateReturnDocs(fields: FieldDef[], returns: Record<string, string> | undefined): void {
|
|
150
|
+
if (!returns) return
|
|
151
|
+
for (let i = 0; i < fields.length; i++) {
|
|
152
|
+
const field = fields[i]
|
|
153
|
+
const doc = returns[field.name] ?? returns[`_${i}`]
|
|
154
|
+
if (doc) field.doc = doc
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function overloadSuffixer(items: readonly (AbiEvent | AbiFunction)[]) {
|
|
159
|
+
const counts = new Map<string, number>()
|
|
160
|
+
const indices = new Map<AbiEvent | AbiFunction, number>()
|
|
161
|
+
for (const item of items) {
|
|
162
|
+
const seen = counts.get(item.name) ?? 0
|
|
163
|
+
indices.set(item, seen)
|
|
164
|
+
counts.set(item.name, seen + 1)
|
|
165
|
+
}
|
|
166
|
+
return (item: AbiEvent | AbiFunction, base: string): string => {
|
|
167
|
+
if ((counts.get(item.name) ?? 1) <= 1) return base
|
|
168
|
+
const idx = indices.get(item)!
|
|
169
|
+
return idx === 0 ? base : `${base}_${idx}`
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toFieldDef(p: AbiParameter, index: number, isEventInput: boolean): FieldDef {
|
|
174
|
+
const field: FieldDef = {
|
|
175
|
+
name: p.name || `_${index}`,
|
|
176
|
+
type: toTypeDef(p),
|
|
177
|
+
}
|
|
178
|
+
if (isEventInput && (p as any).indexed) field.indexed = true
|
|
179
|
+
return field
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function toTypeDef(p: AbiParameter): TypeDef {
|
|
183
|
+
const fixed = p.type.match(/\[(\d+)\]$/)
|
|
184
|
+
if (fixed) {
|
|
185
|
+
return {kind: 'fixedArray', size: Number(fixed[1]), item: toTypeDef(stripOuterArray(p))}
|
|
186
|
+
}
|
|
187
|
+
if (p.type.endsWith('[]')) {
|
|
188
|
+
return {kind: 'array', item: toTypeDef(stripOuterArray(p))}
|
|
189
|
+
}
|
|
190
|
+
if (p.type.startsWith('tuple')) {
|
|
191
|
+
const components = ((p as any).components || []) as AbiParameter[]
|
|
192
|
+
return {
|
|
193
|
+
kind: 'tuple',
|
|
194
|
+
fields: components.map((c, i) => toFieldDef(c, i, false)),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {kind: 'primitive', name: p.type}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stripOuterArray(p: AbiParameter): AbiParameter {
|
|
201
|
+
return {...(p as any), type: p.type.replace(/\[\d*\]$/, '')} as AbiParameter
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function canonicalType(p: AbiParameter): string {
|
|
205
|
+
if (!p.type.startsWith('tuple')) return p.type
|
|
206
|
+
const arrayBrackets = p.type.slice(5)
|
|
207
|
+
const components = (p as any).components as AbiParameter[]
|
|
208
|
+
return `(${components.map(canonicalType).join(',')})${arrayBrackets}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function eventSignature(e: AbiEvent): string {
|
|
212
|
+
return `${e.name}(${e.inputs.map(canonicalType).join(',')})`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function fnSignature(f: AbiFunction): string {
|
|
216
|
+
return `${f.name}(${f.inputs.map(canonicalType).join(',')})`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function capitalize(s: string): string {
|
|
220
|
+
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
221
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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'
|
|
10
11
|
import {chainIdOption} from './chainIds'
|
|
11
12
|
|
|
12
13
|
const LOG = createLogger('sqd:evm-typegen')
|
|
@@ -14,40 +15,40 @@ const PROXY_ETHERSCAN = 'https://cloud.sqd.dev/chains/api/v1/evm/abi'
|
|
|
14
15
|
const ORIGIN_ETHERSCAN = 'https://api.etherscan.io/v2/api'
|
|
15
16
|
|
|
16
17
|
runProgram(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
async function () {
|
|
19
|
+
program
|
|
20
|
+
.description(
|
|
21
|
+
`
|
|
21
22
|
Generates TypeScript facades for EVM transactions, logs and eth_call queries.
|
|
22
23
|
|
|
23
24
|
The generated facades are assumed to be used by "squids" indexing EVM data.
|
|
24
25
|
`.trim(),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
`
|
|
51
52
|
ABI file can be specified in three ways:
|
|
52
53
|
|
|
53
54
|
1. as a plain JSON file:
|
|
@@ -67,186 +68,186 @@ You can overwrite basename of generated files using fragment (#) suffix.
|
|
|
67
68
|
|
|
68
69
|
squid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413#contract
|
|
69
70
|
`,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
program.parse()
|
|
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[]
|
|
85
|
+
|
|
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
|
+
}
|
|
96
|
+
|
|
97
|
+
if (opts.clean && dest.exists()) {
|
|
98
|
+
LOG.info(`deleting ${dest.path()}`)
|
|
99
|
+
dest.del()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (specs.length == 0 && !opts.multicall) {
|
|
103
|
+
LOG.warn('no ABI files given, nothing to generate')
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
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),
|
|
124
|
+
)
|
|
73
125
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
126
|
+
async function read(
|
|
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))}
|
|
81
136
|
}
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (opts.chainId) {
|
|
88
|
-
throw new InvalidOptionArgumentError('Option --chain-id and --etherscan-chain-id cannot be used together')
|
|
89
|
-
}
|
|
90
|
-
opts.chainId = opts.etherscanChainId
|
|
91
|
-
delete opts.etherscanChainId
|
|
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'))
|
|
92
142
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
LOG.info(`deleting ${dest.path()}`)
|
|
96
|
-
dest.del()
|
|
143
|
+
if (Array.isArray(raw)) {
|
|
144
|
+
return {abi: raw}
|
|
97
145
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
LOG.warn('no ABI files given, nothing to generate')
|
|
101
|
-
return
|
|
146
|
+
if (Array.isArray(raw?.abi)) {
|
|
147
|
+
return {abi: raw.abi, natspec: extractNatSpec(raw)}
|
|
102
148
|
}
|
|
149
|
+
throw new Error('Unrecognized ABI format')
|
|
150
|
+
}
|
|
103
151
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
158
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
let abi_json = await read(spec, opts)
|
|
112
|
-
await new Typegen(dest, abi_json, spec.name, LOG).generate()
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
(err) => LOG.fatal(err),
|
|
116
|
-
)
|
|
159
|
+
async function fetchFromEtherscan(address: string, config: EtherscanAPIConfig): Promise<any> {
|
|
160
|
+
let url = new URL(config.api)
|
|
117
161
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
},
|
|
125
|
-
): Promise<any> {
|
|
126
|
-
if (spec.kind == 'address') {
|
|
127
|
-
return fetchFromEtherscan(spec.src, getEtherscanAPIConfig(options))
|
|
128
|
-
}
|
|
129
|
-
let abi: any
|
|
130
|
-
if (spec.kind == 'url') {
|
|
131
|
-
abi = await GET(spec.src)
|
|
132
|
-
} else {
|
|
133
|
-
abi = JSON.parse(fs.readFileSync(spec.src, 'utf-8'))
|
|
134
|
-
}
|
|
135
|
-
if (Array.isArray(abi)) {
|
|
136
|
-
return abi
|
|
137
|
-
} else if (Array.isArray(abi?.abi)) {
|
|
138
|
-
return abi.abi
|
|
139
|
-
} else {
|
|
140
|
-
throw new Error('Unrecognized ABI format')
|
|
141
|
-
}
|
|
142
|
-
}
|
|
162
|
+
let params = new URLSearchParams({
|
|
163
|
+
module: 'contract',
|
|
164
|
+
action: 'getabi',
|
|
165
|
+
address,
|
|
166
|
+
chainid: config.chainId.toString(),
|
|
167
|
+
})
|
|
143
168
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
config: EtherscanAPIConfig,
|
|
147
|
-
): Promise<any> {
|
|
148
|
-
let url = new URL(config.api)
|
|
149
|
-
|
|
150
|
-
let params = new URLSearchParams({
|
|
151
|
-
module: 'contract',
|
|
152
|
-
action: 'getabi',
|
|
153
|
-
address,
|
|
154
|
-
chainid: config.chainId.toString(),
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
if (config.apiKey) {
|
|
158
|
-
params.set('apiKey', config.apiKey);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
url.search = params.toString()
|
|
162
|
-
|
|
163
|
-
let response: { status: string; result: string }
|
|
164
|
-
let attempts = 0
|
|
165
|
-
while (true) {
|
|
166
|
-
response = await GET(url.toString())
|
|
167
|
-
if (
|
|
168
|
-
response.status == '0' &&
|
|
169
|
-
response.result.includes('rate limit') &&
|
|
170
|
-
attempts < 4
|
|
171
|
-
) {
|
|
172
|
-
attempts += 1
|
|
173
|
-
let timeout = attempts * 2
|
|
174
|
-
LOG.warn(
|
|
175
|
-
`faced rate limit error while trying to fetch contract ABI. Trying again in ${timeout} seconds.`,
|
|
176
|
-
)
|
|
177
|
-
await wait(timeout * 1000)
|
|
178
|
-
} else {
|
|
179
|
-
break
|
|
169
|
+
if (config.apiKey) {
|
|
170
|
+
params.set('apiKey', config.apiKey)
|
|
180
171
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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}`)
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
interface Spec {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
kind: 'address' | 'url' | 'file'
|
|
196
|
+
src: string
|
|
197
|
+
name: string
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
function specArgument(value: string, prev?: Spec[]): Spec[] {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
let spec = parseSpec(value)
|
|
202
|
+
prev = prev || []
|
|
203
|
+
prev.push(spec)
|
|
204
|
+
return prev
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
function isAddress(spec: string): boolean {
|
|
205
|
-
|
|
208
|
+
return spec.match(/^0x[0-9a-fA-F]{40}$/) !== null
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
function parseSpec(spec: string): Spec {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
}
|
|
217
220
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|
|
224
228
|
}
|
|
225
|
-
} else {
|
|
226
229
|
return {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
kind: 'file',
|
|
231
|
+
src,
|
|
232
|
+
name: fragment || basename(src),
|
|
230
233
|
}
|
|
231
|
-
}
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
function splitFragment(spec: string): [string, string] {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
let parts = spec.split('#')
|
|
238
|
+
if (parts.length > 1) {
|
|
239
|
+
let fragment = parts.pop()!
|
|
240
|
+
return [parts.join('#'), fragment]
|
|
241
|
+
}
|
|
240
242
|
return [spec, '']
|
|
241
|
-
}
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
function basename(file: string): string {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
)
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
interface EtherscanAPIConfig {
|
|
@@ -262,11 +263,11 @@ function getEtherscanAPIConfig(options: {
|
|
|
262
263
|
}): EtherscanAPIConfig {
|
|
263
264
|
let api: string
|
|
264
265
|
if (options.etherscanApi != null) {
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
266
|
+
api = normalizeEtherscanAPIUrl(options.etherscanApi)
|
|
267
|
+
} else if (options.etherscanApiKey != null) {
|
|
268
|
+
api = ORIGIN_ETHERSCAN
|
|
268
269
|
} else {
|
|
269
|
-
|
|
270
|
+
api = PROXY_ETHERSCAN
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
return {
|
|
@@ -277,9 +278,9 @@ function getEtherscanAPIConfig(options: {
|
|
|
277
278
|
}
|
|
278
279
|
|
|
279
280
|
function normalizeEtherscanAPIUrl(url: string) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
if (url.endsWith('/api')) {
|
|
282
|
+
return url
|
|
283
|
+
}
|
|
283
284
|
|
|
284
|
-
|
|
285
|
-
}
|
|
285
|
+
return url.endsWith('/') ? `${url}api` : `${url}/api`
|
|
286
|
+
}
|