From d578e4755b3db809b8bfab1641a75b5ab80c264e Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 26 Mar 2024 12:41:54 +0100 Subject: [PATCH] Migrate EVM typegen to new codec --- evm/evm-codec/package.json | 1 + evm/evm-codec/src/index.ts | 2 + evm/evm-typegen/package.json | 8 +- evm/evm-typegen/src/abi.support.ts | 135 ------------ evm/evm-typegen/src/main.ts | 305 ++++++++++++++------------ evm/evm-typegen/src/multicall.ts | 3 + evm/evm-typegen/src/pretty-out-dir.ts | 24 ++ evm/evm-typegen/src/typegen.ts | 294 ++++++++++++++----------- evm/evm-typegen/src/util/types.ts | 102 ++++----- 9 files changed, 399 insertions(+), 475 deletions(-) delete mode 100644 evm/evm-typegen/src/abi.support.ts create mode 100644 evm/evm-typegen/src/pretty-out-dir.ts diff --git a/evm/evm-codec/package.json b/evm/evm-codec/package.json index 40eb6bd40..deef50656 100644 --- a/evm/evm-codec/package.json +++ b/evm/evm-codec/package.json @@ -17,6 +17,7 @@ "test": "vitest run" }, "dependencies": { + "keccak256": "^1.0.6" }, "devDependencies": { "@types/node": "^18.18.14", diff --git a/evm/evm-codec/src/index.ts b/evm/evm-codec/src/index.ts index 608718159..6579d9a43 100644 --- a/evm/evm-codec/src/index.ts +++ b/evm/evm-codec/src/index.ts @@ -3,3 +3,5 @@ export { Sink } from "./sink"; export type { Codec } from "./codec"; export * from "./codecs/primitives"; export * from "./contract-base"; +import keccak256 from "keccak256"; +export { keccak256 }; diff --git a/evm/evm-typegen/package.json b/evm/evm-typegen/package.json index 41c8d738b..b3cb40752 100644 --- a/evm/evm-typegen/package.json +++ b/evm/evm-typegen/package.json @@ -24,12 +24,12 @@ "@subsquid/util-internal": "^3.1.0", "@subsquid/util-internal-code-printer": "^1.2.2", "@subsquid/util-internal-commander": "^1.3.2", - "commander": "^11.1.0" - }, - "peerDependencies": { - "ethers": "^6.9.0" + "@subsquid/evm-codec": "file:../evm-codec", + "commander": "^11.1.0", + "prettier": "^3.2.5" }, "devDependencies": { + "abitype": "^1.0.0", "@types/node": "^18.18.14", "typescript": "~5.3.2" } diff --git a/evm/evm-typegen/src/abi.support.ts b/evm/evm-typegen/src/abi.support.ts deleted file mode 100644 index deac8281b..000000000 --- a/evm/evm-typegen/src/abi.support.ts +++ /dev/null @@ -1,135 +0,0 @@ -import assert from 'assert' -import * as ethers from 'ethers' - -export interface EventRecord { - topics: string[] - data: string -} -export type LogRecord = EventRecord - -export class LogEvent { - private fragment: ethers.EventFragment - - constructor(private abi: ethers.Interface, public readonly topic: string) { - let fragment = abi.getEvent(topic) - assert(fragment != null, 'Missing fragment') - this.fragment = fragment - } - - is(rec: EventRecord): boolean { - return rec.topics[0] === this.topic - } - - decode(rec: EventRecord): Args { - return this.abi.decodeEventLog(this.fragment, rec.data, rec.topics) as any as Args - } -} - -export interface FuncRecord { - sighash?: string - input: string -} - -export class Func { - private fragment: ethers.FunctionFragment - - constructor(private abi: ethers.Interface, public readonly sighash: string) { - let fragment = abi.getFunction(sighash) - assert(fragment != null, 'Missing fragment') - this.fragment = fragment - } - - is(rec: FuncRecord): boolean { - let sighash = rec.sighash ? rec.sighash : rec.input.slice(0, 10) - return sighash === this.sighash - } - - decode(input: ethers.BytesLike): Args & FieldArgs - decode(rec: FuncRecord): Args & FieldArgs - decode(inputOrRec: ethers.BytesLike | FuncRecord): Args & FieldArgs { - const input = ethers.isBytesLike(inputOrRec) ? inputOrRec : inputOrRec.input - return this.abi.decodeFunctionData(this.fragment, input) as any as Args & FieldArgs - } - - encode(args: Args): string { - return this.abi.encodeFunctionData(this.fragment, args) - } - - decodeResult(output: ethers.BytesLike): Result { - const decoded = this.abi.decodeFunctionResult(this.fragment, output) - return decoded.length > 1 ? decoded : decoded[0] - } - - tryDecodeResult(output: ethers.BytesLike): Result | undefined { - try { - return this.decodeResult(output) - } catch(err: any) { - return undefined - } - } -} - - -export function isFunctionResultDecodingError(val: unknown): val is Error & {data: string} { - if (!(val instanceof Error)) return false - let err = val as any - return err.code == 'CALL_EXCEPTION' - && typeof err.data == 'string' - && !err.errorArgs - && !err.errorName -} - - -export interface ChainContext { - _chain: Chain -} - - -export interface BlockContext { - _chain: Chain - block: Block -} - - -export interface Block { - height: number -} - - -export interface Chain { - client: { - call: (method: string, params?: unknown[]) => Promise - } -} - - -export class ContractBase { - private readonly _chain: Chain - private readonly blockHeight: number - readonly address: string - - constructor(ctx: BlockContext, address: string) - constructor(ctx: ChainContext, block: Block, address: string) - constructor(ctx: BlockContext, blockOrAddress: Block | string, address?: string) { - this._chain = ctx._chain - if (typeof blockOrAddress === 'string') { - this.blockHeight = ctx.block.height - this.address = ethers.getAddress(blockOrAddress) - } else { - if (address == null) { - throw new Error('missing contract address') - } - this.blockHeight = blockOrAddress.height - this.address = ethers.getAddress(address) - } - } - - async eth_call(func: Func, args: Args): Promise { - let data = func.encode(args) - let result = await this._chain.client.call('eth_call', [ - {to: this.address, data}, - '0x'+this.blockHeight.toString(16) - ]) - return func.decodeResult(result) - } -} diff --git a/evm/evm-typegen/src/main.ts b/evm/evm-typegen/src/main.ts index e8cdb78f5..c57941f7f 100644 --- a/evm/evm-typegen/src/main.ts +++ b/evm/evm-typegen/src/main.ts @@ -1,40 +1,39 @@ -import * as fs from 'fs' -import * as ethers from 'ethers' -import path from 'path' -import {InvalidArgumentError, program} from 'commander' -import {createLogger} from '@subsquid/logger' -import {runProgram, wait} from '@subsquid/util-internal' -import {OutDir} from '@subsquid/util-internal-code-printer' -import * as validator from '@subsquid/util-internal-commander' -import {Typegen} from './typegen' -import {GET} from './util/fetch' - - -const LOG = createLogger('sqd:evm-typegen') - - -runProgram(async function() { +import * as fs from "fs"; +import path from "path"; +import { InvalidArgumentError, program } from "commander"; +import { createLogger } from "@subsquid/logger"; +import { runProgram, wait } from "@subsquid/util-internal"; +import * as validator from "@subsquid/util-internal-commander"; +import { Typegen } from "./typegen"; +import { GET } from "./util/fetch"; +import { PrettyOutDir } from "./pretty-out-dir"; + +const LOG = createLogger("sqd:evm-typegen"); + +runProgram( + async function () { program - .description(` + .description( + ` Generates TypeScript facades for EVM transactions, logs and eth_call queries. The generated facades are assumed to be used by "squids" indexing EVM data. - `.trim()) - .name('squid-evm-typegen') - .argument('', 'output directory for generated definitions') - .argument('[abi...]', 'ABI file', specArgument) - .option('--multicall', 'generate facade for MakerDAO multicall contract') - .option( - '--etherscan-api ', - 'etherscan API to fetch contract ABI by a known address', - validator.Url(['http:', 'https:']) - ) - .option( - '--etherscan-api-key ', - 'etherscan API key' - ) - .option('--clean', 'delete output directory before run') - .addHelpText('afterAll', ` + `.trim(), + ) + .name("squid-evm-typegen") + .argument("", "output directory for generated definitions") + .argument("[abi...]", "ABI file", specArgument) + .option("--multicall", "generate facade for MakerDAO multicall contract") + .option( + "--etherscan-api ", + "etherscan API to fetch contract ABI by a known address", + validator.Url(["http:", "https:"]), + ) + .option("--etherscan-api-key ", "etherscan API key") + .option("--clean", "delete output directory before run") + .addHelpText( + "afterAll", + ` ABI file can be specified in three ways: 1. as a plain JSON file: @@ -53,152 +52,166 @@ In all cases typegen will use ABI's basename as a basename of generated files. You can overwrite basename of generated files using fragment (#) suffix. squid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413#contract - `) + `, + ); - program.parse() + program.parse(); let opts = program.opts() as { - clean?: boolean, - multicall?: boolean, - etherscanApi?: string - etherscanApiKey?: string - } - let dest = new OutDir(program.processedArgs[0]) - let specs = program.processedArgs[1] as Spec[] + clean?: boolean; + multicall?: boolean; + etherscanApi?: string; + etherscanApiKey?: string; + }; + let dest = new PrettyOutDir(program.processedArgs[0]); + let specs = program.processedArgs[1] as Spec[]; if (opts.clean && dest.exists()) { - LOG.info(`deleting ${dest.path()}`) - dest.del() + LOG.info(`deleting ${dest.path()}`); + dest.del(); } if (specs.length == 0 && !opts.multicall) { - LOG.warn('no ABI files given, nothing to generate') - return + LOG.warn("no ABI files given, nothing to generate"); + return; } - dest.add('abi.support.ts', [__dirname, '../src/abi.support.ts']) - LOG.info(`saved ${dest.path('abi.support.ts')}`) - if (opts.multicall) { - dest.add('multicall.ts', [__dirname, '../src/multicall.ts']) - LOG.info(`saved ${dest.path('multicall.ts')}`) + dest.add("multicall.ts", [__dirname, "../src/multicall.ts"]); + LOG.info(`saved ${dest.path("multicall.ts")}`); } for (let spec of specs) { - LOG.info(`processing ${spec.src}`) - let abi_json = await read(spec, opts) - let abi = new ethers.Interface(abi_json) - new Typegen(dest, abi, spec.name, LOG).generate() - } -}, err => LOG.fatal(err)) - - -async function read(spec: Spec, options?: {etherscanApi?: string, etherscanApiKey?: string}): Promise { - if (spec.kind == 'address') { - return fetchFromEtherscan(spec.src, options?.etherscanApi, options?.etherscanApiKey) - } - let abi: any - if (spec.kind == 'url') { - abi = await GET(spec.src) - } else { - abi = JSON.parse(fs.readFileSync(spec.src, 'utf-8')) - } - if (Array.isArray(abi)) { - return abi - } else if (Array.isArray(abi?.abi)) { - return abi.abi - } else { - throw new Error('Unrecognized ABI format') + LOG.info(`processing ${spec.src}`); + let abi_json = await read(spec, opts); + await new Typegen(dest, abi_json, spec.name, LOG).generate(); } + }, + (err) => LOG.fatal(err), +); + +async function read( + spec: Spec, + options?: { etherscanApi?: string; etherscanApiKey?: string }, +): Promise { + if (spec.kind == "address") { + return fetchFromEtherscan( + spec.src, + options?.etherscanApi, + options?.etherscanApiKey, + ); + } + let abi: any; + if (spec.kind == "url") { + abi = await GET(spec.src); + } else { + abi = JSON.parse(fs.readFileSync(spec.src, "utf-8")); + } + if (Array.isArray(abi)) { + return abi; + } else if (Array.isArray(abi?.abi)) { + return abi.abi; + } else { + throw new Error("Unrecognized ABI format"); + } } - -async function fetchFromEtherscan(address: string, api?: string, apiKey?: string): Promise { - api = api || 'https://api.etherscan.io/' - let url = new URL('api?module=contract&action=getabi', api) - url.searchParams.set('address', address) - if (apiKey) { - url.searchParams.set('apiKey', apiKey) - } - let response: {status: string, result: string} - let attempts = 0 - while (true) { - response = await GET(url.toString()) - if (response.status == '0' && response.result.includes('rate limit') && attempts < 4) { - attempts += 1 - let timeout = attempts * 2 - LOG.warn(`faced rate limit error while trying to fetch contract ABI. Trying again in ${timeout} seconds.`) - await wait(timeout * 1000) - } else { - break - } - } - if (response.status == '1') { - return JSON.parse(response.result) +async function fetchFromEtherscan( + address: string, + api?: string, + apiKey?: string, +): Promise { + api = api || "https://api.etherscan.io/"; + let url = new URL("api?module=contract&action=getabi", api); + url.searchParams.set("address", address); + if (apiKey) { + url.searchParams.set("apiKey", apiKey); + } + let response: { status: string; result: string }; + let attempts = 0; + while (true) { + response = await GET(url.toString()); + if ( + response.status == "0" && + response.result.includes("rate limit") && + attempts < 4 + ) { + attempts += 1; + let timeout = attempts * 2; + LOG.warn( + `faced rate limit error while trying to fetch contract ABI. Trying again in ${timeout} seconds.`, + ); + await wait(timeout * 1000); } else { - throw new Error(`Failed to fetch contract ABI from ${api}: ${response.result}`) + break; } + } + if (response.status == "1") { + return JSON.parse(response.result); + } else { + throw new Error( + `Failed to fetch contract ABI from ${api}: ${response.result}`, + ); + } } - interface Spec { - kind: 'address' | 'url' | 'file' - src: string - name: string + kind: "address" | "url" | "file"; + src: string; + name: string; } - function specArgument(value: string, prev?: Spec[]): Spec[] { - let spec = parseSpec(value) - prev = prev || [] - prev.push(spec) - return prev + let spec = parseSpec(value); + prev = prev || []; + prev.push(spec); + return prev; } +function isAddress(spec: string): boolean { + return spec.match(/^0x[0-9a-fA-F]{40}$/) !== null; +} function parseSpec(spec: string): Spec { - let [src, fragment] = splitFragment(spec) - if (src.startsWith('0x')) { - if (!ethers.isAddress(src)) throw new InvalidArgumentError('Invalid contract address') - return { - kind: 'address', - src, - name: fragment || src - } - } else if (src.includes('://')) { - let u = new URL( - validator.Url(['http:', 'https:'])(src) - ) - return { - kind: 'url', - src, - name: fragment || basename(u.pathname) - } - } else { - return { - kind: 'file', - src, - name: fragment || basename(src) - } - } + let [src, fragment] = splitFragment(spec); + if (src.startsWith("0x")) { + if (!isAddress(src)) + throw new InvalidArgumentError("Invalid contract address"); + return { + kind: "address", + src, + name: fragment || src, + }; + } else if (src.includes("://")) { + let u = new URL(validator.Url(["http:", "https:"])(src)); + return { + kind: "url", + src, + name: fragment || basename(u.pathname), + }; + } else { + return { + kind: "file", + src, + name: fragment || basename(src), + }; + } } - function splitFragment(spec: string): [string, string] { - let parts = spec.split('#') - if (parts.length > 1) { - let fragment = parts.pop()! - return [parts.join('#'), fragment] - } else { - return [spec, ''] - } + let parts = spec.split("#"); + if (parts.length > 1) { + let fragment = parts.pop()!; + return [parts.join("#"), fragment]; + } else { + return [spec, ""]; + } } - function basename(file: string): string { - let name = path.parse(file).name - if (name) return name - throw new InvalidArgumentError( - `Can't derive target basename for output files. Use url fragment to specify it, e.g. #erc20` - ) + let name = path.parse(file).name; + if (name) return name; + throw new InvalidArgumentError( + `Can't derive target basename for output files. Use url fragment to specify it, e.g. #erc20`, + ); } diff --git a/evm/evm-typegen/src/multicall.ts b/evm/evm-typegen/src/multicall.ts index 4b20dca4f..66efcd1e2 100644 --- a/evm/evm-typegen/src/multicall.ts +++ b/evm/evm-typegen/src/multicall.ts @@ -1,3 +1,5 @@ +/** + * TODO migrate multicall import * as ethers from 'ethers' import {ContractBase, Func} from './abi.support' @@ -210,3 +212,4 @@ function* splitIntoPages(size: number, page: number): Iterable<[from: number, to from = to } } +*/ diff --git a/evm/evm-typegen/src/pretty-out-dir.ts b/evm/evm-typegen/src/pretty-out-dir.ts new file mode 100644 index 000000000..dc2d7d6ae --- /dev/null +++ b/evm/evm-typegen/src/pretty-out-dir.ts @@ -0,0 +1,24 @@ +import { FileOutput, OutDir } from "@subsquid/util-internal-code-printer"; +import * as prettier from "prettier"; +import fs from "fs"; +import path from "path"; + +export class PrettyOutDir extends OutDir { + file(name: string): PrettyFileOutput { + return new PrettyFileOutput(this.path(name)); + } +} + +export class PrettyFileOutput extends FileOutput { + constructor(public readonly file: string) { + super(file); + } + + async write() { + fs.mkdirSync(path.dirname(this.file), { recursive: true }); + fs.writeFileSync( + this.file, + await prettier.format(this.toString(), { parser: "typescript" }), + ); + } +} diff --git a/evm/evm-typegen/src/typegen.ts b/evm/evm-typegen/src/typegen.ts index ec0a4ed54..e475bd5ed 100644 --- a/evm/evm-typegen/src/typegen.ts +++ b/evm/evm-typegen/src/typegen.ts @@ -1,143 +1,181 @@ -import * as ethers from 'ethers' -import {Logger} from '@subsquid/logger' -import {def} from '@subsquid/util-internal' -import {FileOutput, OutDir} from '@subsquid/util-internal-code-printer' -import {getFullTupleType, getReturnType, getStructType, getTupleType, getType} from './util/types' - +import { Logger } from "@subsquid/logger"; +import { def } from "@subsquid/util-internal"; +import { keccak256 } from "@subsquid/evm-codec"; +import { getType } from "./util/types"; +import type { Abi, AbiEvent, AbiFunction, AbiParameter } from "abitype"; +import { PrettyFileOutput, PrettyOutDir } from "./pretty-out-dir"; export class Typegen { - private out: FileOutput - - constructor(private dest: OutDir, private abi: ethers.Interface, private basename: string, private log: Logger) { - this.out = dest.file(basename + '.ts') - } - - generate(): void { - this.out.line("import * as ethers from 'ethers'") - this.out.line("import {LogEvent, Func, ContractBase} from './abi.support'") - this.out.line(`import {ABI_JSON} from './${this.basename}.abi'`) - this.out.line() - this.out.line("export const abi = new ethers.Interface(ABI_JSON);") - - this.generateEvents() - this.generateFunctions() - this.generateContract() - - this.writeAbi() - this.out.write() - this.log.info(`saved ${this.out.file}`) - } - - private writeAbi() { - let out = this.dest.file(this.basename + '.abi.ts') - let json = this.abi.formatJson() - json = JSON.stringify(JSON.parse(json), null, 4) - out.line(`export const ABI_JSON = ${json}`) - out.write() - this.log.info(`saved ${out.file}`) - } - - private generateEvents() { - let events = this.getEvents() - if (events.length == 0) { - return - } - this.out.line() - this.out.block(`export const events =`, () => { - for (let e of events) { - this.out.line(`${this.getPropName(e)}: new LogEvent<${getFullTupleType(e.inputs)}>(`) - this.out.indentation(() => this.out.line(`abi, '${e.topicHash}'`)) - this.out.line('),') - } - }) - } - - private generateFunctions() { - let functions = this.getFunctions() - if (functions.length == 0) { - return - } - this.out.line() - this.out.block(`export const functions =`, () => { - for (let f of functions) { - let sighash = f.selector - let pArgs = getTupleType(f.inputs) - let pArgStruct = getStructType(f.inputs) - let pResult = getReturnType(f.outputs) - this.out.line(`${this.getPropName(f)}: new Func<${pArgs}, ${pArgStruct}, ${pResult}>(`) - this.out.indentation(() => this.out.line(`abi, '${sighash}'`)) - this.out.line('),') - } - }) - } - - private generateContract() { - this.out.line() - this.out.block(`export class Contract extends ContractBase`, () => { - let functions = this.getFunctions() - for (let f of functions) { - if (f.constant && f.outputs?.length) { - this.out.line() - let argNames = f.inputs.map((a, idx) => a.name || `arg${idx}`) - let args = f.inputs.map((a, idx) => `${argNames[idx]}: ${getType(a)}`).join(', ') - this.out.block(`${this.getPropName(f)}(${args}): Promise<${getReturnType(f.outputs)}>`, () => { - this.out.line(`return this.eth_call(functions${this.getRef(f)}, [${argNames.join(', ')}])`) - }) - } - } - }) + private out: PrettyFileOutput; + + constructor( + dest: PrettyOutDir, + private abi: Abi, + basename: string, + private log: Logger, + ) { + this.out = dest.file(basename + ".ts"); + } + + async generate() { + this.out.line(`import * as p from "@subsquid/evm-codec";`); + this.out.line( + "const { event, fun, indexed, arg, array, fixedArray, tuple, ContractBase } = p;", + ); + this.out.line(); + + this.generateEvents(); + this.generateFunctions(); + this.generateContract(); + + await this.out.write(); + this.log.info(`saved ${this.out.file}`); + } + + private generateEvents() { + let events = this.getEvents(); + if (events.length == 0) { + return; } - - private getRef(item: ethers.EventFragment | ethers.FunctionFragment): string { - let key = this.getPropName(item) - if (key[0] == "'") { - return `[${key}]` - } else { - return '.' + key - } + this.out.line(); + this.out.block(`export const events =`, () => { + for (let e of events) { + this.out.line( + `${this.getPropName(e)}: event("${this.topic0(e)}", ${this.toTypes( + e.inputs, + )}),`, + ); + } + }); + } + + private topic0(e: AbiEvent): string { + return `0x${keccak256(this.sighash(e)).toString("hex")}`; + } + + private toTypes(inputs: readonly AbiParameter[]): string { + return inputs.map(getType).join(", "); + } + + private generateFunctions() { + let functions = this.getFunctions(); + if (functions.length == 0) { + return; } - - private getPropName(item: ethers.EventFragment | ethers.FunctionFragment): string { - if (this.getOverloads(item) == 1) { - return item.name - } else { - return `'${item.format('sighash')}'` + this.out.line(); + this.out.block(`export const functions =`, () => { + for (let f of functions) { + const returnType = + f.outputs.length > 0 ? `, ${this.toTypes(f.outputs)}` : ""; + + this.out.line( + `${this.getPropName(f)}: fun("${this.functionSelector( + f, + )}", [${this.toTypes(f.inputs)}]${returnType}),`, + ); + } + }); + } + + private functionSelector(f: AbiFunction): string { + const sighash = this.sighash(f); + return `0x${keccak256(sighash).slice(0, 4).toString("hex")}`; + } + + private generateContract() { + this.out.line(); + this.out.block(`export class Contract extends ContractBase`, () => { + let functions = this.getFunctions(); + for (let f of functions) { + if ( + (f.stateMutability === "pure" || f.stateMutability === "view") && + f.outputs?.length + ) { + this.out.line(); + let argNames = f.inputs.map((a, idx) => a.name || `arg${idx}`); + const ref = this.getRef(f); + let args = f.inputs + .map( + (a, idx) => + `${argNames[idx]}: Parameters[${idx}]`, + ) + .join(", "); + this.out.block(`${this.getPropName(f)}(${args})`, () => { + this.out.line( + `return this.eth_call(functions${ref}, [${argNames.join(", ")}])`, + ); + }); } + } + }); + } + + private getRef(item: AbiEvent | AbiFunction): string { + let key = this.getPropName(item); + if (key[0] == "'") { + return `[${key}]`; + } else { + return "." + key; } + } - private getOverloads(item: ethers.EventFragment | ethers.FunctionFragment): number { - if (item instanceof ethers.EventFragment) { - return this.eventOverloads()[item.name] - } else { - return this.functionOverloads()[item.name] - } + private cannonicalType(param: AbiParameter): string { + if (!param.type.startsWith("tuple")) { + return param.type; } - - @def - private functionOverloads(): Record { - let overloads: Record = {} - for (let item of this.getFunctions()) { - overloads[item.name] = (overloads[item.name] || 0) + 1 - } - return overloads + const arrayBrackets = param.type.slice(5); + return `(${(param as any).components.map((param: AbiParameter) => + this.cannonicalType(param), + )})${arrayBrackets}`; + } + + private sighash(item: AbiEvent | AbiFunction): string { + return `${item.name}(${item.inputs + .map((param) => this.cannonicalType(param)) + .join(",")})`; + } + + private getPropName(item: AbiEvent | AbiFunction): string { + if (this.getOverloads(item) == 1) { + return item.name; + } else { + return `"${this.sighash(item)}"`; } + } - @def - private eventOverloads(): Record { - let overloads: Record = {} - for (let item of this.getEvents()) { - overloads[item.name] = (overloads[item.name] || 0) + 1 - } - return overloads + private getOverloads(item: AbiEvent | AbiFunction): number { + if (item.type === "event") { + return this.eventOverloads()[item.name]; + } else { + return this.functionOverloads()[item.name]; } + } - @def - private getFunctions(): ethers.FunctionFragment[] { - return this.abi.fragments.filter(f => f.type === 'function') as ethers.FunctionFragment[] + @def + private functionOverloads(): Record { + let overloads: Record = {}; + for (let item of this.getFunctions()) { + overloads[item.name] = (overloads[item.name] || 0) + 1; } - - @def - private getEvents(): ethers.EventFragment[] { - return this.abi.fragments.filter(f => f.type === 'event') as ethers.EventFragment[] + return overloads; + } + + @def + private eventOverloads(): Record { + let overloads: Record = {}; + for (let item of this.getEvents()) { + overloads[item.name] = (overloads[item.name] || 0) + 1; } + return overloads; + } + + @def + private getFunctions(): AbiFunction[] { + return this.abi.filter((f) => f.type === "function") as AbiFunction[]; + } + + @def + private getEvents(): AbiEvent[] { + return this.abi.filter((f) => f.type === "event") as AbiEvent[]; + } } diff --git a/evm/evm-typegen/src/util/types.ts b/evm/evm-typegen/src/util/types.ts index 0c0d82212..2897dff4f 100644 --- a/evm/evm-typegen/src/util/types.ts +++ b/evm/evm-typegen/src/util/types.ts @@ -1,72 +1,50 @@ -import assert from 'assert' -import type {ParamType} from 'ethers' +import type { AbiEventParameter } from "abitype"; - -// taken from: https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/cli/src.ts/typescript.ts#L10 -export function getType(param: ParamType): string { - if (param.baseType === 'array') { - assert(param.arrayChildren != null, 'Missing children for array type') - return 'Array<' + getType(param.arrayChildren) + '>' - } - - if (param.baseType === 'tuple') { - assert(param.components != null, 'Missing components for tuple type') - return getFullTupleType(param.components) - } - - if (param.type === 'address' || param.type === 'string') { - return 'string' - } - - if (param.type === 'bool') { - return 'boolean' - } - - let match = param.type.match(/^(u?int)([0-9]+)$/) - if (match) { - return parseInt(match[2]) < 53 ? 'number' : 'bigint' - } - - if (param.type.substring(0, 5) === 'bytes') { - return 'string' - } - - throw new Error('unknown type') +function isStaticArray(param: AbiEventParameter) { + return param.type.match(/\[\d+]$/); } - -export function getFullTupleType(params: ReadonlyArray): string { - let tuple = getTupleType(params) - let struct = getStructType(params) - if (struct == '{}') { - return tuple - } else { - return `(${tuple} & ${struct})` - } +function isDynamicArray(param: AbiEventParameter) { + return param.type.endsWith("[]"); } - -export function getTupleType(params: ReadonlyArray): string { - return '[' + params.map(p => { - return p.name ? `${p.name}: ${getType(p)}` : `_: ${getType(p)}` - }).join(', ') + ']' +function elementsCount(param: AbiEventParameter) { + return Number(param.type.match(/\[(\d+)]$/)?.[1] ?? 0); } - -// https://github.com/ethers-io/ethers.js/blob/278f84174409b470fa7992e1f8b5693e6e5d2dac/src.ts/abi/coders/tuple.ts#L36 -export function getStructType(params: ReadonlyArray): string { - let array: any = [] - let counts: Record = {} - for (let p of params) { - if (p.name && array[p.name] == null) { - counts[p.name] = (counts[p.name] || 0) + 1 - } - } - let fields = params.filter(p => counts[p.name] == 1) - return '{' + fields.map(f => `${f.name}: ${getType(f)}`).join(', ') + '}' +function arrayChildType(param: AbiEventParameter) { + return param.type.replace(/\[\d*]$/, ""); } - -export function getReturnType(outputs: ReadonlyArray) { - return outputs.length == 1 ? getType(outputs[0]) : getFullTupleType(outputs) +export function getType(param: AbiEventParameter): string { + const { indexed, ...indexlessParam } = param; + if (indexed) { + return `indexed(${getType(indexlessParam as any)})`; + } + const { name, ...namelessParam } = indexlessParam; + + if (name) { + return `arg("${name}", ${getType(namelessParam as any)})`; + } + + if (isStaticArray(param)) { + const elements = elementsCount(param); + return `fixedArray(${getType({ + ...param, + type: arrayChildType(param), + })}, ${elements})`; + } + + if (isDynamicArray(param)) { + return `array(${getType({ + ...param, + type: arrayChildType(param), + })})`; + } + + if (param.type === "tuple") { + return `tuple(${(param as any).components.map(getType).join(", ")})`; + } + + return `p.${param.type}`; }