@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/src/typegen.ts CHANGED
@@ -1,342 +1,134 @@
1
- import {Interface, ParamType} from "@ethersproject/abi"
2
- import {FileOutput, Output} from "@subsquid/util-internal-code-printer"
3
- import {def} from "@subsquid/util-internal"
4
- import fs from "fs"
1
+ import {EventFragment, FunctionFragment, Interface} from '@ethersproject/abi'
2
+ import {Logger} from '@subsquid/logger'
3
+ import {def} from '@subsquid/util-internal'
4
+ import {FileOutput, OutDir} from '@subsquid/util-internal-code-printer'
5
+ import {getFullTupleType, getReturnType, getStructType, getTupleType, getType} from './util/types'
6
+
5
7
 
6
8
  export class Typegen {
7
- private rawAbi: any
8
- private abi: Interface
9
9
  private out: FileOutput
10
10
 
11
- constructor(abiFile: string, outPath: string) {
12
- this.rawAbi = JSON.parse(fs.readFileSync(abiFile, {encoding: "utf-8"}))
13
- this.abi = new Interface(this.rawAbi)
14
- this.out = new FileOutput(outPath)
11
+ constructor(private dest: OutDir, private abi: Interface, private basename: string, private log: Logger) {
12
+ this.out = dest.file(basename + '.ts')
15
13
  }
16
14
 
17
15
  generate(): void {
18
- this.out.line("import * as ethers from \"ethers\";")
19
- this.out.line("import assert from \"assert\";")
20
- this.out.line()
21
- this.out.line("export const abi = new ethers.utils.Interface(getJsonAbi());")
16
+ this.out.line("import * as ethers from 'ethers'")
17
+ this.out.line("import {LogEvent, Func, ContractBase} from './abi.support'")
18
+ this.out.line(`import {ABI_JSON} from './${this.basename}.abi'`)
22
19
  this.out.line()
20
+ this.out.line("export const abi = new ethers.utils.Interface(ABI_JSON);")
21
+
23
22
  this.generateEvents()
24
- this.out.line()
25
23
  this.generateFunctions()
26
- this.out.line()
27
24
  this.generateContract()
28
- this.out.line()
29
- this.out.block("function getJsonAbi(): any", () => {
30
- `return ${JSON.stringify(this.rawAbi, null, 2)}`.split('\n').forEach(line => {
31
- this.out.line(line)
32
- })
33
- })
25
+
26
+ this.writeAbi()
34
27
  this.out.write()
28
+ this.log.info(`saved ${this.out.file}`)
29
+ }
30
+
31
+ private writeAbi() {
32
+ let out = this.dest.file(this.basename + '.abi.ts')
33
+ let json = this.abi.format('json') as string
34
+ json = JSON.stringify(JSON.parse(json), null, 4)
35
+ out.line(`export const ABI_JSON = ${json}`)
36
+ out.write()
37
+ this.log.info(`saved ${out.file}`)
35
38
  }
36
39
 
37
40
  private generateEvents() {
38
- for (const decl of this.getEvents()) {
39
- for (let i = 0; i < decl.overloads.length; i++) {
40
- if (decl.overloads[i].inputs.length === 0) continue
41
- this.out.line(`export type ${decl.name}${i}Event = ${getTupleType(decl.overloads[i].inputs)}`)
42
- this.out.line()
43
- }
41
+ let events = Object.values(this.abi.events)
42
+ if (events.length == 0) {
43
+ return
44
44
  }
45
- this.out.block("export interface EvmLog", () => {
46
- this.out.line("data: string;")
47
- this.out.line("topics: string[];")
48
- })
49
- this.out.line()
50
- this.out.block(`function decodeEvent(signature: string, data: EvmLog): any`, () => {
51
- this.out.line(`return abi.decodeEventLog(`)
52
- this.out.indentation(() => {
53
- this.out.line(`abi.getEvent(signature),`)
54
- this.out.line(`data.data || "",`)
55
- this.out.line("data.topics")
56
- })
57
- this.out.line(");")
58
- })
59
45
  this.out.line()
60
46
  this.out.block(`export const events =`, () => {
61
- for (const event of this.getEvents()) {
62
- for (let i = 0; i < event.overloads.length; i++) {
63
- const overload = event.overloads[i]
64
- const signature = createSignature(event.name, overload.inputs)
65
- this.out.block(`"${signature}":`, () => {
66
- this.out.line(`topic: abi.getEventTopic("${signature}"),`)
67
- if (event.overloads[i].inputs.length > 0) {
68
- this.out.block(`decode(data: EvmLog): ${event.name}${i}Event`, () => {
69
- this.out.line(`return decodeEvent("${signature}", data)`)
70
- })
71
- }
72
- })
73
- this.out.line(",")
74
- }
47
+ for (let e of events) {
48
+ let topic = this.abi.getEventTopic(e)
49
+ this.out.line(`${this.getPropName(e)}: new LogEvent<${getFullTupleType(e.inputs)}>(`)
50
+ this.out.indentation(() => this.out.line(`abi, '${topic}'`))
51
+ this.out.line('),')
75
52
  }
76
53
  })
77
54
  }
78
55
 
79
56
  private generateFunctions() {
80
- for (const func of this.getFunctions()) {
81
- for (let i = 0; i < func.overloads.length; i++) {
82
- if (func.overloads[i].inputs.length === 0) continue
83
- this.out.line(`export type ${upperCaseFirst(func.name)}${i}Function = ${getTupleType(func.overloads[i].inputs)}`)
84
- this.out.line()
85
- }
57
+ let functions = Object.values(this.abi.functions)
58
+ if (functions.length == 0) {
59
+ return
86
60
  }
87
61
  this.out.line()
88
- this.out.block(`function decodeFunction(data: string): any`, () => {
89
- this.out.line(`return abi.decodeFunctionData(data.slice(0, 10), data)`)
90
- })
91
- this.out.line()
92
62
  this.out.block(`export const functions =`, () => {
93
- for (const func of this.getFunctions()) {
94
- for (let i = 0; i < func.overloads.length; i++) {
95
- const overload = func.overloads[i]
96
- const signature = createSignature(func.name, overload.inputs)
97
- this.out.block(`"${signature}":`, () => {
98
- this.out.line(`sighash: abi.getSighash("${signature}"),`)
99
- if (func.overloads[i].inputs.length > 0)
100
- this.out.block(`decode(input: string): ${upperCaseFirst(func.name)}${i}Function`, () => {
101
- this.out.line(`return decodeFunction(input)`)
102
- })
103
- })
104
- this.out.line(",")
105
- }
63
+ for (let f of functions) {
64
+ let sighash = this.abi.getSighash(f)
65
+ let pArgs = getTupleType(f.inputs)
66
+ let pArgStruct = getStructType(f.inputs)
67
+ let pResult = getReturnType(f.outputs || [])
68
+ this.out.line(`${this.getPropName(f)}: new Func<${pArgs}, ${pArgStruct}, ${pResult}>(`)
69
+ this.out.indentation(() => this.out.line(`abi, '${sighash}'`))
70
+ this.out.line('),')
106
71
  }
107
72
  })
108
73
  }
109
74
 
110
75
  private generateContract() {
111
- let abiCalls = this.getCalls()
112
-
113
- this.out.block("interface ChainContext ", () => {
114
- this.out.line(`_chain: Chain`)
115
- })
116
- this.out.line()
117
- this.out.block("interface BlockContext ", () => {
118
- this.out.line(`_chain: Chain`)
119
- this.out.line(`block: Block`)
120
- })
121
76
  this.out.line()
122
- this.out.block("interface Block ", () => {
123
- this.out.line(`height: number`)
124
- })
125
- this.out.line()
126
- this.out.block("interface Chain ", () => {
127
- this.out.block("client: ", () => {
128
- this.out.line(`call: <T=any>(method: string, params?: unknown[]) => Promise<T>`)
129
- })
130
- })
131
- this.out.line()
132
- this.out.block("export class Contract ", () => {
133
- this.out.line(`private readonly _chain: Chain`)
134
- this.out.line(`private readonly blockHeight: number`)
135
- this.out.line(`readonly address: string`)
136
- this.out.line()
137
- this.out.line(`constructor(ctx: BlockContext, address: string)`)
138
- this.out.line(`constructor(ctx: ChainContext, block: Block, address: string)`)
139
- this.out.block(`constructor(ctx: BlockContext, blockOrAddress: Block | string, address?: string)`, () => {
140
- this.out.line(`this._chain = ctx._chain`)
141
- this.out.block(`if (typeof blockOrAddress === 'string') `, () => {
142
- this.out.line(`this.blockHeight = ctx.block.height`)
143
- this.out.line(`this.address = ethers.utils.getAddress(blockOrAddress)`)
144
- })
145
- this.out.block(`else `, () => {
146
- this.out.line(`assert(address != null)`)
147
- this.out.line(`this.blockHeight = blockOrAddress.height`)
148
- this.out.line(`this.address = ethers.utils.getAddress(address)`)
149
- })
150
- })
151
- this.out.line()
152
- for (const decl of abiCalls) {
153
- if (decl.overloads.length > 1) {
154
- for (let overload of decl.overloads) {
155
- const args = overload.inputs.map((i, n) => `${i.name || `arg${n}`}: ${getType(i)}`)
156
- const returnType = overload.outputs.length == 1 ? getType(overload.outputs[0]) : getTupleType(overload.outputs)
157
- this.out.line(`async ${decl.name}(${args}): Promise<${returnType}>`)
158
- }
159
- this.out.block(`async ${decl.name}(...args: any[])`, () => {
160
- this.out.line(`return this.call("${decl.name}", args)`)
161
- })
162
- } else {
163
- const overload = decl.overloads[0]
164
- const params = overload.inputs.map((i, n) => `${i.name || `arg${n}`}: ${getType(i)}`)
165
- const returnType = overload.outputs.length == 1 ? getType(overload.outputs[0]) : getTupleType(overload.outputs)
166
- this.out.block(`async ${decl.name}(${params.join(`, `)}): Promise<${returnType}>`, () => {
167
- this.out.line(`return this.call("${decl.name}", [${overload.inputs.map((i, n) => `${i.name || `arg${n}`}`).join(`, `)}])`)
77
+ this.out.block(`export class Contract extends ContractBase`, () => {
78
+ let functions = Object.values(this.abi.functions)
79
+ for (let f of functions) {
80
+ if (f.constant && f.outputs?.length) {
81
+ this.out.line()
82
+ let argNames = f.inputs.map((a, idx) => a.name || `arg${idx}`)
83
+ let args = f.inputs.map((a, idx) => `${argNames[idx]}: ${getType(a)}`).join(', ')
84
+ this.out.block(`${this.getPropName(f)}(${args}): Promise<${getReturnType(f.outputs)}>`, () => {
85
+ this.out.line(`return this.eth_call(functions${this.getRef(f)}, [${argNames.join(', ')}])`)
168
86
  })
169
87
  }
170
- this.out.line()
171
88
  }
172
- this.out.block(`private async call(name: string, args: any[]) : Promise<any>`, () => {
173
- this.out.line(`const fragment = abi.getFunction(name)`)
174
- this.out.line(`const data = abi.encodeFunctionData(fragment, args)`)
175
- this.out.line(`const result = await this._chain.client.call('eth_call', [{to: this.address, data}, this.blockHeight])`)
176
- this.out.line(`const decoded = abi.decodeFunctionResult(fragment, result)`)
177
- this.out.line(`return decoded.length > 1 ? decoded : decoded[0]`)
178
- })
179
89
  })
180
90
  }
181
91
 
182
- @def
183
- private getEvents(): AbiEvent[] {
184
- let res: Map<string, AbiEvent> = new Map()
185
- for (let event of Object.values(this.abi.events)) {
186
- let abiEvent = res.get(event.name)
187
- if (abiEvent == null) {
188
- abiEvent = {
189
- name: event.name,
190
- overloads: []
191
- }
192
- res.set(event.name, abiEvent)
193
- }
194
-
195
- abiEvent.overloads.push({
196
- inputs: event.inputs || [],
197
- })
92
+ private getRef(item: EventFragment | FunctionFragment): string {
93
+ let key = this.getPropName(item)
94
+ if (key[0] == "'") {
95
+ return `[${key}]`
96
+ } else {
97
+ return '.' + key
198
98
  }
199
-
200
- return [...res.values()]
201
99
  }
202
100
 
203
- @def
204
- private getFunctions(): AbiFunction[] {
205
- let res: Map<string, AbiFunction> = new Map()
206
- for (let func of Object.values(this.abi.functions)) {
207
- if (func.constant) continue
208
-
209
- let abiFunc = res.get(func.name)
210
- if (abiFunc == null) {
211
- abiFunc = {
212
- name: func.name,
213
- overloads: []
214
- }
215
- res.set(func.name, abiFunc)
216
- }
217
-
218
- abiFunc.overloads.push({
219
- inputs: func.inputs || [],
220
- })
101
+ private getPropName(item: EventFragment | FunctionFragment): string {
102
+ if (this.getOverloads(item) == 1) {
103
+ return item.name
104
+ } else {
105
+ return `'${item.format('sighash')}'`
221
106
  }
222
-
223
- return [...res.values()]
224
107
  }
225
108
 
226
- @def
227
- private getCalls(): AbiCall[] {
228
- let res: Map<string, AbiCall> = new Map()
229
- for (let func of Object.values(this.abi.functions)) {
230
- if (!func.constant || func.outputs == null) continue
231
-
232
- let abiCall = res.get(func.name)
233
- if (abiCall == null) {
234
- abiCall = {
235
- name: func.name,
236
- overloads: []
237
- }
238
- res.set(func.name, abiCall)
239
- }
240
-
241
- abiCall.overloads.push({
242
- inputs: func.inputs,
243
- outputs: func.outputs || [],
244
- })
109
+ private getOverloads(item: EventFragment | FunctionFragment): number {
110
+ if (item instanceof EventFragment) {
111
+ return this.eventOverloads()[item.name]
112
+ } else {
113
+ return this.functionOverloads()[item.name]
245
114
  }
246
-
247
- return [...res.values()]
248
- }
249
- }
250
-
251
-
252
- // taken from: https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/cli/src.ts/typescript.ts#L10
253
- function getType(param: ParamType): string {
254
- if (param.type === "address" || param.type === "string") {
255
- return "string"
256
- }
257
-
258
- if (param.type === "bool") {
259
- return "boolean"
260
- }
261
-
262
- if (param.type.substring(0, 5) === "bytes") {
263
- return "string"
264
- }
265
-
266
- let match = param.type.match(/^(u?int)([0-9]+)$/)
267
- if (match) {
268
- return parseInt(match[2]) < 53 ? 'number' : 'ethers.BigNumber'
269
115
  }
270
116
 
271
- if (param.baseType === "array") {
272
- return "Array<" + getType(param.arrayChildren) + ">"
273
- }
274
-
275
- if (param.baseType === "tuple") {
276
- return getTupleType(param.components)
117
+ @def
118
+ private functionOverloads(): Record<string, number> {
119
+ let overloads: Record<string, number> = {}
120
+ for (let item of Object.values(this.abi.functions)) {
121
+ overloads[item.name] = (overloads[item.name] || 0) + 1
122
+ }
123
+ return overloads
277
124
  }
278
125
 
279
- throw new Error("unknown type")
280
- }
281
-
282
-
283
- function getTupleType(params: ParamType[]) {
284
- let tuple = '[' + params.map(p => {
285
- return p.name ? `${p.name}: ${getType(p)}` : getType(p)
286
- }).join(', ') + ']'
287
-
288
- let fields = getStructFields(params)
289
- if (fields.length == 0) return tuple
290
-
291
- let struct = '{' + fields.map(f => `${f.name}: ${getType(f)}`).join(', ') + '}'
292
-
293
- return `(${tuple} & ${struct})`
294
- }
295
-
296
-
297
- // https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/abi/src.ts/coders/tuple.ts#L29
298
- function getStructFields(params: ParamType[]): ParamType[] {
299
- let array: any = []
300
- let counts: Record<string, number> = {}
301
- for (let p of params) {
302
- if (p.name && array[p.name] == null) {
303
- counts[p.name] = (counts[p.name] || 0) + 1
126
+ @def
127
+ private eventOverloads(): Record<string, number> {
128
+ let overloads: Record<string, number> = {}
129
+ for (let item of Object.values(this.abi.events)) {
130
+ overloads[item.name] = (overloads[item.name] || 0) + 1
304
131
  }
132
+ return overloads
305
133
  }
306
- return params.filter(p => counts[p.name] == 1)
307
- }
308
-
309
-
310
- function createSignature(name: string, inputs: ParamType[]) {
311
- return `${name}(${inputs.map((i) => i.type).join(`,`)})`
312
- }
313
-
314
-
315
- interface AbiEvent {
316
- name: string
317
- overloads: {
318
- inputs: ParamType[]
319
- }[]
320
- }
321
-
322
-
323
- interface AbiFunction {
324
- name: string
325
- overloads: {
326
- inputs: ParamType[]
327
- }[]
328
- }
329
-
330
-
331
- interface AbiCall {
332
- name: string
333
- overloads: {
334
- inputs: ParamType[]
335
- outputs: ParamType[]
336
- }[]
337
- }
338
-
339
-
340
- function upperCaseFirst(s: string): string {
341
- return s[0].toUpperCase() + s.slice(1)
342
134
  }
@@ -0,0 +1,82 @@
1
+ import {createLogger} from '@subsquid/logger'
2
+ import {wait} from '@subsquid/util-internal'
3
+ import assert from 'assert'
4
+ import fetch, {FetchError, RequestInit} from 'node-fetch'
5
+
6
+
7
+ const LOG = createLogger('sqd:evm-typegen:fetch')
8
+
9
+
10
+ export async function GET<T=any>(url: string): Promise<T> {
11
+ let init: RequestInit = {
12
+ method: 'GET',
13
+ headers: {
14
+ 'accept': 'application/json',
15
+ 'accept-encoding': 'gzip, br'
16
+ },
17
+ timeout: 10_000
18
+ }
19
+ let backoff = [1000, 2000]
20
+ let errors = 0
21
+ while (true) {
22
+ let result = await performFetch(url, init).catch(err => {
23
+ assert(err instanceof Error)
24
+ return err
25
+ })
26
+ if (errors < backoff.length && isRetryableError(result)) {
27
+ let timeout = backoff[errors]
28
+ LOG.warn(`${result.toString()}. Trying again in ${timeout/1000} seconds`)
29
+ errors += 1
30
+ await wait(timeout)
31
+ } else if (result instanceof Error) {
32
+ throw result
33
+ } else {
34
+ return result
35
+ }
36
+ }
37
+ }
38
+
39
+
40
+ async function performFetch(url: string, init: RequestInit): Promise<any> {
41
+ let response = await fetch(url, init)
42
+ if (response.ok) return response.json()
43
+ let body = await response.text()
44
+ throw new HttpError(response.status, body)
45
+ }
46
+
47
+
48
+ function isRetryableError(err: unknown): err is Error {
49
+ if (err instanceof HttpError) {
50
+ switch(err.status) {
51
+ case 429:
52
+ case 502:
53
+ case 503:
54
+ case 504:
55
+ return true
56
+ default:
57
+ return false
58
+ }
59
+ }
60
+ if (err instanceof FetchError) {
61
+ switch(err.type) {
62
+ case 'body-timeout':
63
+ case 'request-timeout':
64
+ return true
65
+ case 'system':
66
+ return err.message.startsWith('request to')
67
+ default:
68
+ return false
69
+ }
70
+ }
71
+ return false
72
+ }
73
+
74
+
75
+ export class HttpError extends Error {
76
+ constructor(
77
+ public readonly status: number,
78
+ public readonly body?: string
79
+ ) {
80
+ super(`Got http ${status}`)
81
+ }
82
+ }
@@ -0,0 +1,69 @@
1
+ import type {ParamType} from '@ethersproject/abi'
2
+
3
+
4
+ // taken from: https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/cli/src.ts/typescript.ts#L10
5
+ export function getType(param: ParamType): string {
6
+ if (param.baseType === 'array') {
7
+ return 'Array<' + getType(param.arrayChildren) + '>'
8
+ }
9
+
10
+ if (param.baseType === 'tuple') {
11
+ return getFullTupleType(param.components)
12
+ }
13
+
14
+ if (param.type === 'address' || param.type === 'string') {
15
+ return 'string'
16
+ }
17
+
18
+ if (param.type === 'bool') {
19
+ return 'boolean'
20
+ }
21
+
22
+ let match = param.type.match(/^(u?int)([0-9]+)$/)
23
+ if (match) {
24
+ return parseInt(match[2]) < 53 ? 'number' : 'ethers.BigNumber'
25
+ }
26
+
27
+ if (param.type.substring(0, 5) === 'bytes') {
28
+ return 'string'
29
+ }
30
+
31
+ throw new Error('unknown type')
32
+ }
33
+
34
+
35
+ export function getFullTupleType(params: ParamType[]): string {
36
+ let tuple = getTupleType(params)
37
+ let struct = getStructType(params)
38
+ if (struct == '{}') {
39
+ return tuple
40
+ } else {
41
+ return `(${tuple} & ${struct})`
42
+ }
43
+ }
44
+
45
+
46
+ export function getTupleType(params: ParamType[]): string {
47
+ return '[' + params.map(p => {
48
+ return p.name ? `${p.name}: ${getType(p)}` : getType(p)
49
+ }).join(', ') + ']'
50
+ }
51
+
52
+
53
+ // https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/abi/src.ts/coders/tuple.ts#L29
54
+ export function getStructType(params: ParamType[]): string {
55
+ let array: any = []
56
+ let counts: Record<string, number> = {}
57
+ for (let p of params) {
58
+ if (p.name && array[p.name] == null) {
59
+ counts[p.name] = (counts[p.name] || 0) + 1
60
+ }
61
+ }
62
+ let fields = params.filter(p => counts[p.name] == 1)
63
+ return '{' + fields.map(f => `${f.name}: ${getType(f)}`).join(', ') + '}'
64
+ }
65
+
66
+
67
+ export function getReturnType(outputs: ParamType[]) {
68
+ return outputs.length == 1 ? getType(outputs[0]) : getFullTupleType(outputs)
69
+ }