diff --git a/lib/parser/abiParser.ts b/lib/parser/abiParser.ts index fc17c2793..e5eb46fe0 100644 --- a/lib/parser/abiParser.ts +++ b/lib/parser/abiParser.ts @@ -254,3 +254,33 @@ export function extractAbi(rawJson: string): RawAbiDefinition[] { throw new MalformedAbiError("Not a valid ABI"); } + +export function extractBytecode(rawContents: string): string | null { + const bytecodeRegex = /^[0-9a-fA-F]+$/; + // First try to see if this is a .bin file with just the bytecode, otherwise a json + if (rawContents.match(bytecodeRegex)) return rawContents; + + let json; + try { + json = JSON.parse(rawContents); + } catch { + return null; + } + + if (!json) return null; + + if (json.bytecode && json.bytecode.match(bytecodeRegex)) { + return json.bytecode; + } + + if ( + json.evm && + json.evm.bytecode && + json.evm.bytecode.object && + json.evm.bytecode.object.match(bytecodeRegex) + ) { + return json.evm.bytecode.object; + } + + return null; +} diff --git a/lib/targets/ethers/generation.ts b/lib/targets/ethers/generation.ts index 9ddfdd264..15dc5d606 100644 --- a/lib/targets/ethers/generation.ts +++ b/lib/targets/ethers/generation.ts @@ -21,12 +21,11 @@ import { VoidType, } from "../../parser/typeParser"; -export function codegen(contract: Contract) { +export function codegenContractTypings(contract: Contract) { const template = ` - import { Contract, ContractFactory, ContractTransaction, EventFilter, Signer } from "ethers"; + import { Contract, ContractTransaction, EventFilter, Signer } from "ethers"; import { Listener, Provider } from 'ethers/providers'; import { Arrayish, BigNumber, BigNumberish, Interface } from "ethers/utils"; - import { UnsignedTransaction } from "ethers/utils/transaction"; import { TransactionOverrides, TypedEventDescription, TypedFunctionDescription } from "."; interface ${contract.name}Interface extends Interface { @@ -65,22 +64,47 @@ export function codegen(contract: Contract) { estimate: { ${contract.functions.map(generateEstimateFunction).join("\n")} }; - } - ${generateFactoryIfConstructorPresent(contract)} - `; + }`; return template; } -function generateFactoryIfConstructorPresent(contract: Contract): string { - if (!contract.constructor) return ""; +export function codegenContractFactory(contract: Contract, abi: any, bytecode: string): string { + const constructorArgs = contract.constructor + ? generateInputTypes(contract.constructor.inputs) + : ""; + const constructorArgNames = contract.constructor + ? generateParamNames(contract.constructor.inputs) + : ""; + return ` + import { ContractFactory, Signer } from "ethers"; + import { Arrayish, BigNumberish } from "ethers/utils"; + import { UnsignedTransaction } from "ethers/utils/transaction"; + + import { ${contract.name} } from "./${contract.name}"; + export class ${contract.name}Factory extends ContractFactory { - deploy(${generateInputTypes(contract.constructor.inputs)}): Promise<${contract.name}>; - getDeployTransaction(${generateInputTypes(contract.constructor.inputs)}): UnsignedTransaction; - attach(address: string): ${contract.name}; - connect(signer: Signer): ${contract.name}Factory; + constructor(signer?: Signer) { + super(_abi, _bytecode, signer); + } + deploy(${constructorArgs}): Promise<${contract.name}> { + return super.deploy(${constructorArgNames}) as Promise<${contract.name}>; + } + getDeployTransaction(${constructorArgs}): UnsignedTransaction { + return super.getDeployTransaction(${constructorArgNames}); + }; + attach(address: string): ${contract.name} { + return super.attach(address) as ${contract.name}; + } + connect(signer: Signer): ${contract.name}Factory { + return super.connect(signer) as ${contract.name}Factory; + } } + + const _abi = ${JSON.stringify(abi, null, 2)}; + + const _bytecode = "${bytecode}"; `; } @@ -106,9 +130,9 @@ function generateEstimateFunction(fn: FunctionDeclaration): string { function generateInterfaceFunctionDescription(fn: FunctionDeclaration): string { return ` - ${fn.name}: TypedFunctionDescription<{ encode(${generateParamNames( + ${fn.name}: TypedFunctionDescription<{ encode(${generateParamArrayNames( fn.inputs, - )}: ${generateParamTypes(fn.inputs)}): string; }>; + )}: ${generateParamArrayTypes(fn.inputs)}): string; }>; `; } @@ -138,12 +162,16 @@ function generateOutputTypes(outputs: Array): string { } } -function generateParamTypes(params: Array): string { +function generateParamArrayTypes(params: Array): string { return `[${params.map(param => generateInputType(param.type)).join(", ")}]`; } function generateParamNames(params: Array): string { - return `[${params.map(param => param.name).join(", ")}]`; + return `${params.map(param => param.name).join(", ")}`; +} + +function generateParamArrayNames(params: Array): string { + return `[${generateParamNames(params)}]`; } function generateEvents(event: EventDeclaration) { @@ -154,7 +182,7 @@ function generateEvents(event: EventDeclaration) { function generateInterfaceEventDescription(event: EventDeclaration): string { return ` - ${event.name}: TypedEventDescription<{ encodeTopics(${generateParamNames( + ${event.name}: TypedEventDescription<{ encodeTopics(${generateParamArrayNames( event.inputs, )}: ${generateEventTopicTypes(event.inputs)}): string[]; }>; `; diff --git a/lib/targets/ethers/index.ts b/lib/targets/ethers/index.ts index 99f23e4de..ddd2fb587 100644 --- a/lib/targets/ethers/index.ts +++ b/lib/targets/ethers/index.ts @@ -1,8 +1,9 @@ import { TsGeneratorPlugin, TContext, TFileDesc } from "ts-generator"; import { join } from "path"; -import { extractAbi, parse } from "../../parser/abiParser"; -import { getFilename } from "../shared"; -import { codegen } from "./generation"; +import { extractAbi, parse, Contract, extractBytecode } from "../../parser/abiParser"; +import { getFilename, getFileExtension } from "../shared"; +import { codegenContractTypings, codegenContractFactory } from "./generation"; +import { Dictionary } from "ts-essentials"; export interface IEthersCfg { outDir?: string; @@ -14,6 +15,11 @@ export class Ethers extends TsGeneratorPlugin { name = "Ethers"; private readonly outDirAbs: string; + private readonly contractCache: Dictionary<{ + abi: any; + contract: Contract; + }> = {}; + private readonly bytecodeCache: Dictionary = {}; constructor(ctx: TContext) { super(ctx); @@ -23,20 +29,73 @@ export class Ethers extends TsGeneratorPlugin { this.outDirAbs = join(cwd, rawConfig.outDir || DEFAULT_OUT_PATH); } - transformFile(file: TFileDesc): TFileDesc | void { - const abi = extractAbi(file.contents); - const isEmptyAbi = abi.length === 0; - if (isEmptyAbi) { + transformFile(file: TFileDesc): TFileDesc[] | void { + const fileExt = getFileExtension(file.path); + + // For json files with both ABI and bytecode, both the contract typing and factory can be + // generated at once. For split files (.abi and .bin) we don't know in which order they will + // be transformed -- so we temporarily store whichever comes first, and generate the factory + // only when both ABI and bytecode are present. + + // TODO we might want to add a configuration switch to control whether we want to generate the + // factories, or just contract type declarations. + + if (fileExt === ".bin") { + return this.transformBinFile(file); + } + + return this.transformAbiOrFullJsonFile(file); + } + + transformBinFile(file: TFileDesc): TFileDesc[] | void { + const name = getFilename(file.path); + const bytecode = extractBytecode(file.contents); + + if (!bytecode) { return; } + if (this.contractCache[name]) { + const { contract, abi } = this.contractCache[name]; + return [this.genContractFactoryFile(contract, abi, bytecode)]; + } else { + this.bytecodeCache[name] = bytecode; + } + } + + transformAbiOrFullJsonFile(file: TFileDesc): TFileDesc[] | void { const name = getFilename(file.path); + const abi = extractAbi(file.contents); + + if (abi.length === 0) { + return; + } const contract = parse(abi, name); + const bytecode = extractBytecode(file.contents) || this.bytecodeCache[name]; + + if (bytecode) { + return [ + this.genContractTypingsFile(contract), + this.genContractFactoryFile(contract, abi, bytecode), + ]; + } else { + this.contractCache[name] = { abi, contract }; + return [this.genContractTypingsFile(contract)]; + } + } + + genContractTypingsFile(contract: Contract): TFileDesc { + return { + path: join(this.outDirAbs, `${contract.name}.d.ts`), + contents: codegenContractTypings(contract), + }; + } + genContractFactoryFile(contract: Contract, abi: any, bytecode: string) { return { - path: join(this.outDirAbs, `${name}.d.ts`), - contents: codegen(contract), + path: join(this.outDirAbs, `${contract.name}Factory.ts`), + contents: codegenContractFactory(contract, abi, bytecode), }; } diff --git a/lib/targets/shared.ts b/lib/targets/shared.ts index 187904bec..0000f806a 100644 --- a/lib/targets/shared.ts +++ b/lib/targets/shared.ts @@ -8,3 +8,7 @@ export interface IContext { export function getFilename(path: string) { return parse(path).name; } + +export function getFileExtension(path: string) { + return parse(path).ext; +} diff --git a/test/integration/before.ts b/test/integration/before.ts index 05e2c3742..d756f5c91 100644 --- a/test/integration/before.ts +++ b/test/integration/before.ts @@ -78,7 +78,7 @@ async function generateEthers(cwd: string, prettierCfg: any) { removeSync(join(__dirname, outDir)); const rawConfig: TPluginCfg = { - files: "**/*.abi", + files: "**/*.{abi,bin}", target: "ethers", outDir, }; diff --git a/test/integration/targets/ethers/ContractWithOverloads.spec.ts b/test/integration/targets/ethers/ContractWithOverloads.spec.ts index c9c3ce7dd..8e3b493c4 100644 --- a/test/integration/targets/ethers/ContractWithOverloads.spec.ts +++ b/test/integration/targets/ethers/ContractWithOverloads.spec.ts @@ -1,12 +1,12 @@ -import { deployContract } from "./ethers"; -import { ContractWithOverloads } from "./types/ethers-contracts/ContractWithOverloads"; +import { ContractWithOverloadsFactory } from "./types/ethers-contracts/ContractWithOverloadsFactory"; import { BigNumber } from "ethers/utils"; import { expect } from "chai"; +import { getTestSigner } from "./ethers"; describe("ContractWithOverloads", () => { it("should work", async () => { - const contract = (await deployContract("ContractWithOverloads")) as ContractWithOverloads; + const contract = await new ContractWithOverloadsFactory(getTestSigner()).deploy(); expect(await contract.functions.counter()).to.be.deep.eq(new BigNumber("0")); }); diff --git a/test/integration/targets/ethers/ContractsWithStructs.spec.ts b/test/integration/targets/ethers/ContractsWithStructs.spec.ts index d22959b64..360f2159f 100644 --- a/test/integration/targets/ethers/ContractsWithStructs.spec.ts +++ b/test/integration/targets/ethers/ContractsWithStructs.spec.ts @@ -1,24 +1,29 @@ -import { deployContract } from "./ethers"; import { ContractWithStructs } from "./types/ethers-contracts/ContractWithStructs"; +import { ContractWithStructsFactory } from "./types/ethers-contracts/ContractWithStructsFactory"; import { BigNumber } from "ethers/utils"; import { expect } from "chai"; +import { getTestSigner } from "./ethers"; describe("ContractWithStructs", () => { + function deployContractWithStructs(): Promise { + return new ContractWithStructsFactory(getTestSigner()).deploy(); + } + it("should work", async () => { - const contract = (await deployContract("ContractWithStructs")) as ContractWithStructs; + const contract = await deployContractWithStructs(); const res = await contract.functions.getCounter(1); expect(res).to.be.deep.eq(new BigNumber("1")); }); it("should have an address", async () => { - const contract = (await deployContract("ContractWithStructs")) as ContractWithStructs; + const contract = await deployContractWithStructs(); expect(contract.address).to.be.string; }); it("should return structs in output", async () => { - const contract = (await deployContract("ContractWithStructs")) as ContractWithStructs; + const contract = await deployContractWithStructs(); const output = await contract.functions.getStuff(); expect(output._person.height).to.be.deep.eq(new BigNumber("12")); @@ -26,7 +31,7 @@ describe("ContractWithStructs", () => { }); it("should accepts structs in input", async () => { - const contract = (await deployContract("ContractWithStructs")) as ContractWithStructs; + const contract = await deployContractWithStructs(); await contract.functions.setStuff( { height: 10, name: "bob", account: contract.address }, diff --git a/test/integration/targets/ethers/DumbContract.spec.ts b/test/integration/targets/ethers/DumbContract.spec.ts index bae475c15..d59b57bf8 100644 --- a/test/integration/targets/ethers/DumbContract.spec.ts +++ b/test/integration/targets/ethers/DumbContract.spec.ts @@ -1,14 +1,15 @@ -import { getContractFactory } from "./ethers"; -import { DumbContract, DumbContractFactory } from "./types/ethers-contracts/DumbContract"; +import { DumbContract } from "./types/ethers-contracts/DumbContract"; +import { DumbContractFactory } from "./types/ethers-contracts/DumbContractFactory"; import { BigNumber } from "ethers/utils"; import { expect } from "chai"; import { Event } from "ethers"; import { arrayify } from "ethers/utils/bytes"; +import { getTestSigner } from "./ethers"; describe("DumbContract", () => { function deployDumbContract(): Promise { - const factory = getContractFactory("DumbContract") as DumbContractFactory; + const factory = new DumbContractFactory(getTestSigner()); return factory.deploy(0); } @@ -26,7 +27,7 @@ describe("DumbContract", () => { }); it("should allow passing a contructor argument in multiple ways", async () => { - const factory = getContractFactory("DumbContract") as DumbContractFactory; + const factory = new DumbContractFactory(getTestSigner()); const contract1 = await factory.deploy(42); expect(await contract1.functions.counter()).to.be.deep.eq(new BigNumber("42")); const contract2 = await factory.deploy("1234123412341234123"); diff --git a/test/integration/targets/ethers/ethers.ts b/test/integration/targets/ethers/ethers.ts index b7ef07561..9b94f094d 100644 --- a/test/integration/targets/ethers/ethers.ts +++ b/test/integration/targets/ethers/ethers.ts @@ -1,9 +1,7 @@ const ganache = require("ganache-cli"); -import { Contract, ContractFactory, Signer } from "ethers"; +import { Signer } from "ethers"; import { JsonRpcProvider } from "ethers/providers"; -import { join } from "path"; -import { readFileSync } from "fs"; let signer: Signer; let server: any; @@ -20,18 +18,8 @@ before(async () => { ({ server, signer } = await createNewBlockchain()); }); -export function getContractFactory(contractName: string): ContractFactory { - const abiDirPath = join(__dirname, "../../abis"); - - const abi = JSON.parse(readFileSync(join(abiDirPath, contractName + ".abi"), "utf-8")); - const bin = readFileSync(join(abiDirPath, contractName + ".bin"), "utf-8"); - const code = "0x" + bin; - return new ContractFactory(abi, code, signer); -} - -export async function deployContract(contractName: string): Promise { - const factory = getContractFactory(contractName); - return factory.deploy(); +export function getTestSigner(): Signer { + return signer; } after(async () => {