Skip to content

Commit

Permalink
[ethers-v4] Support for overloaded functions
Browse files Browse the repository at this point in the history
  • Loading branch information
krzkaczor committed Apr 27, 2020
1 parent 7f4980e commit b5aac41
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 132 deletions.
27 changes: 27 additions & 0 deletions packages/target-ethers-v4-test/test/Overload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { typedAssert } from 'test-utils'

import { createNewBlockchain, deployContract } from './common'
import { Overloads } from '../types/Overloads'
import { BigNumber } from 'ethers/utils'

describe('Overloads', () => {
let contract: Overloads
let ganache: any
beforeEach(async () => {
const { ganache: _ganache, signer } = await createNewBlockchain()
ganache = _ganache
contract = await deployContract<Overloads>(signer, 'Overloads')
})

afterEach(() => ganache.close())

it('works with 1st overload', async () => {
const result = await contract.functions['overload1(int256)'](1)
typedAssert(result, new BigNumber(1))
})

it('works with 2n overload', async () => {
const result = await contract.functions['overload1(uint256,uint256)'](1, 2)
typedAssert(result, new BigNumber(3))
})
})
14 changes: 12 additions & 2 deletions packages/target-ethers-v4-test/types/Overloads.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,20 @@ export class Overloads extends Contract {
interface: OverloadsInterface;

functions: {
overload1(input1: BigNumberish): Promise<BigNumber>;
"overload1(int256)"(input1: BigNumberish): Promise<BigNumber>;

"overload1(uint256,uint256)"(
input1: BigNumberish,
input2: BigNumberish
): Promise<BigNumber>;
};

overload1(input1: BigNumberish): Promise<BigNumber>;
"overload1(int256)"(input1: BigNumberish): Promise<BigNumber>;

"overload1(uint256,uint256)"(
input1: BigNumberish,
input2: BigNumberish
): Promise<BigNumber>;

filters: {};

Expand Down
48 changes: 48 additions & 0 deletions packages/target-ethers-v4/src/codegen/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { FunctionDeclaration, isConstant, isConstantFn, FunctionDocumentation, getSignatureForFn } from 'typechain'
import { generateInputTypes, generateOutputTypes } from './types'

export function codegenFunctions(fns: FunctionDeclaration[]): string {
if (fns.length === 1) {
return generateFunction(fns[0])
}

return codegenForOverloadedFunctions(fns)
}

export function codegenForOverloadedFunctions(fns: FunctionDeclaration[]): string {
return fns.map((fn) => generateFunction(fn, `"${getSignatureForFn(fn)}"`)).join('\n')
}

function generateFunction(fn: FunctionDeclaration, overloadedName?: string): string {
return `
${generateFunctionDocumentation(fn.documentation)}
${overloadedName ?? fn.name}(${generateInputTypes(fn.inputs)}${
!isConstant(fn) && !isConstantFn(fn) ? 'overrides?: TransactionOverrides' : ''
}): Promise<${
fn.stateMutability === 'pure' || fn.stateMutability === 'view'
? generateOutputTypes(fn.outputs)
: 'ContractTransaction'
}>;
`
}

function generateFunctionDocumentation(doc?: FunctionDocumentation): string {
if (!doc) return ''

let docString = '/**'
if (doc.details) docString += `\n * ${doc.details}`
if (doc.notice) docString += `\n * ${doc.notice}`

const params = Object.entries(doc.params || {})
if (params.length) {
params.forEach(([key, value]) => {
docString += `\n * @param ${key} ${value}`
})
}

if (doc.return) docString += `\n * @returns ${doc.return}`

docString += '\n */'

return docString
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { values } from 'lodash'
import {
AbiOutputParameter,
AbiParameter,
BytecodeWithLinkReferences,
Contract,
FunctionDocumentation,
EventArgDeclaration,
EventDeclaration,
EvmOutputType,
EvmType,
FunctionDeclaration,
isConstant,
isConstantFn,
TupleType,
} from 'typechain'
import { generateInputType, generateInputTypes } from './types'
import { codegenFunctions } from './functions'

export function codegenContractTypings(contract: Contract) {
const template = `
Expand Down Expand Up @@ -52,16 +47,10 @@ export function codegenContractTypings(contract: Contract) {
interface: ${contract.name}Interface;
functions: {
${values(contract.functions)
.map((v) => v[0])
.map(generateFunction)
.join('\n')}
${values(contract.functions).map(codegenFunctions).join('\n')}
};
${values(contract.functions)
.map((v) => v[0])
.map(generateFunction)
.join('\n')}
${values(contract.functions).map(codegenFunctions).join('\n')}
filters: {
${values(contract.events)
Expand Down Expand Up @@ -199,19 +188,6 @@ function generateLibraryAddressesInterface(contract: Contract, bytecode: Bytecod
};`
}

function generateFunction(fn: FunctionDeclaration): string {
return `
${generateFunctionDocumentation(fn.documentation)}
${fn.name}(${generateInputTypes(fn.inputs)}${
!isConstant(fn) && !isConstantFn(fn) ? 'overrides?: TransactionOverrides' : ''
}): Promise<${
fn.stateMutability === 'pure' || fn.stateMutability === 'view'
? generateOutputTypes(fn.outputs)
: 'ContractTransaction'
}>;
`
}

function generateEstimateFunction(fn: FunctionDeclaration): string {
return `
${fn.name}(${generateInputTypes(fn.inputs)}): Promise<BigNumber>;
Expand All @@ -226,47 +202,6 @@ function generateInterfaceFunctionDescription(fn: FunctionDeclaration): string {
`
}

function generateFunctionDocumentation(doc?: FunctionDocumentation): string {
if (!doc) return ''

let docString = '/**'
if (doc.details) docString += `\n * ${doc.details}`
if (doc.notice) docString += `\n * ${doc.notice}`

const params = Object.entries(doc.params || {})
if (params.length) {
params.forEach(([key, value]) => {
docString += `\n * @param ${key} ${value}`
})
}

if (doc.return) docString += `\n * @returns ${doc.return}`

docString += '\n */'

return docString
}

function generateInputTypes(input: Array<AbiParameter>): string {
if (input.length === 0) {
return ''
}
return (
input.map((input, index) => `${input.name || `arg${index}`}: ${generateInputType(input.type)}`).join(', ') + ', '
)
}

function generateOutputTypes(outputs: Array<AbiOutputParameter>): string {
if (outputs.length === 1) {
return generateOutputType(outputs[0].type)
} else {
return `{
${outputs.map((t) => t.name && `${t.name}: ${generateOutputType(t.type)}, `).join('')}
${outputs.map((t, i) => `${i}: ${generateOutputType(t.type)}`).join(', ')}
}`
}
}

function generateParamArrayTypes(params: Array<AbiParameter>): string {
return `[${params.map((param) => generateInputType(param.type)).join(', ')}]`
}
Expand Down Expand Up @@ -313,62 +248,3 @@ function generateEventTypes(eventArgs: EventArgDeclaration[]) {
function generateEventArgType(eventArg: EventArgDeclaration): string {
return eventArg.isIndexed ? `${generateInputType(eventArg.type)} | null` : 'null'
}

// https://docs.ethers.io/ethers.js/html/api-contract.html#types
function generateInputType(evmType: EvmType): string {
switch (evmType.type) {
case 'integer':
return 'BigNumberish'
case 'uinteger':
return 'BigNumberish'
case 'address':
return 'string'
case 'bytes':
case 'dynamic-bytes':
return 'Arrayish'
case 'array':
return `(${generateInputType(evmType.itemType)})[]`
case 'boolean':
return 'boolean'
case 'string':
return 'string'
case 'tuple':
return generateTupleType(evmType, generateInputType)
}
}

function generateOutputType(evmType: EvmOutputType): string {
switch (evmType.type) {
case 'integer':
case 'uinteger':
return evmType.bits <= 48 ? 'number' : 'BigNumber'
case 'address':
return 'string'
case 'void':
return 'void'
case 'bytes':
case 'dynamic-bytes':
return 'string'
case 'array':
return `(${generateOutputType(evmType.itemType)})[]`
case 'boolean':
return 'boolean'
case 'string':
return 'string'
case 'tuple':
return generateOutputTupleType(evmType)
}
}

function generateTupleType(tuple: TupleType, generator: (evmType: EvmType) => string) {
return '{' + tuple.components.map((component) => `${component.name}: ${generator(component.type)}`).join(',') + '}'
}

function generateOutputTupleType(tuple: TupleType) {
return (
'{' +
tuple.components.map((component) => `${component.name}: ${generateOutputType(component.type)} ,`).join('\n') +
tuple.components.map((component, index) => `${index}: ${generateOutputType(component.type)}`).join(', ') +
'}'
)
}
80 changes: 80 additions & 0 deletions packages/target-ethers-v4/src/codegen/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { EvmType, EvmOutputType, TupleType, AbiParameter, AbiOutputParameter } from 'typechain'

export function generateInputTypes(input: Array<AbiParameter>): string {
if (input.length === 0) {
return ''
}
return (
input.map((input, index) => `${input.name || `arg${index}`}: ${generateInputType(input.type)}`).join(', ') + ', '
)
}

export function generateOutputTypes(outputs: Array<AbiOutputParameter>): string {
if (outputs.length === 1) {
return generateOutputType(outputs[0].type)
} else {
return `{
${outputs.map((t) => t.name && `${t.name}: ${generateOutputType(t.type)}, `).join('')}
${outputs.map((t, i) => `${i}: ${generateOutputType(t.type)}`).join(', ')}
}`
}
}

// https://docs.ethers.io/ethers.js/html/api-contract.html#types
export function generateInputType(evmType: EvmType): string {
switch (evmType.type) {
case 'integer':
return 'BigNumberish'
case 'uinteger':
return 'BigNumberish'
case 'address':
return 'string'
case 'bytes':
case 'dynamic-bytes':
return 'Arrayish'
case 'array':
return `(${generateInputType(evmType.itemType)})[]`
case 'boolean':
return 'boolean'
case 'string':
return 'string'
case 'tuple':
return generateTupleType(evmType, generateInputType)
}
}

export function generateOutputType(evmType: EvmOutputType): string {
switch (evmType.type) {
case 'integer':
case 'uinteger':
return evmType.bits <= 48 ? 'number' : 'BigNumber'
case 'address':
return 'string'
case 'void':
return 'void'
case 'bytes':
case 'dynamic-bytes':
return 'string'
case 'array':
return `(${generateOutputType(evmType.itemType)})[]`
case 'boolean':
return 'boolean'
case 'string':
return 'string'
case 'tuple':
return generateOutputTupleType(evmType)
}
}

export function generateTupleType(tuple: TupleType, generator: (evmType: EvmType) => string) {
return '{' + tuple.components.map((component) => `${component.name}: ${generator(component.type)}`).join(',') + '}'
}

export function generateOutputTupleType(tuple: TupleType) {
return (
'{' +
tuple.components.map((component) => `${component.name}: ${generateOutputType(component.type)} ,`).join('\n') +
tuple.components.map((component, index) => `${index}: ${generateOutputType(component.type)}`).join(', ') +
'}'
)
}
2 changes: 1 addition & 1 deletion packages/target-ethers-v4/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
parse,
} from 'typechain'

import { codegenAbstractContractFactory, codegenContractFactory, codegenContractTypings } from './generation'
import { codegenAbstractContractFactory, codegenContractFactory, codegenContractTypings } from './codegen'

export interface IEthersCfg {
outDir?: string
Expand Down
2 changes: 1 addition & 1 deletion packages/target-ethers-v4/test/generation.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from 'chai'
import { Contract } from 'typechain'

import { codegenContractFactory } from '../src/generation'
import { codegenContractFactory } from '../src/codegen'

describe('Ethers generation edge cases', () => {
const emptyContract: Contract = {
Expand Down

0 comments on commit b5aac41

Please sign in to comment.