Skip to content

Commit

Permalink
[ethers] Add concrete contract factories
Browse files Browse the repository at this point in the history
  • Loading branch information
quezak committed Jul 3, 2019
1 parent 11f380f commit 1e11be2
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 54 deletions.
30 changes: 30 additions & 0 deletions lib/parser/abiParser.ts
Expand Up @@ -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;
}
62 changes: 45 additions & 17 deletions lib/targets/ethers/generation.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}";
`;
}

Expand All @@ -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; }>;
`;
}

Expand Down Expand Up @@ -138,12 +162,16 @@ function generateOutputTypes(outputs: Array<AbiParameter>): string {
}
}

function generateParamTypes(params: Array<AbiParameter>): string {
function generateParamArrayTypes(params: Array<AbiParameter>): string {
return `[${params.map(param => generateInputType(param.type)).join(", ")}]`;
}

function generateParamNames(params: Array<AbiParameter | EventArgDeclaration>): string {
return `[${params.map(param => param.name).join(", ")}]`;
return `${params.map(param => param.name).join(", ")}`;
}

function generateParamArrayNames(params: Array<AbiParameter | EventArgDeclaration>): string {
return `[${generateParamNames(params)}]`;
}

function generateEvents(event: EventDeclaration) {
Expand All @@ -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[]; }>;
`;
Expand Down
77 changes: 68 additions & 9 deletions 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;
Expand All @@ -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<string> = {};

constructor(ctx: TContext<IEthersCfg>) {
super(ctx);
Expand All @@ -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),
};
}

Expand Down
4 changes: 4 additions & 0 deletions lib/targets/shared.ts
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion test/integration/before.ts
Expand Up @@ -78,7 +78,7 @@ async function generateEthers(cwd: string, prettierCfg: any) {
removeSync(join(__dirname, outDir));

const rawConfig: TPluginCfg<ITypechainCfg> = {
files: "**/*.abi",
files: "**/*.{abi,bin}",
target: "ethers",
outDir,
};
Expand Down
6 changes: 3 additions & 3 deletions 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"));
});
Expand Down
15 changes: 10 additions & 5 deletions test/integration/targets/ethers/ContractsWithStructs.spec.ts
@@ -1,32 +1,37 @@
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<ContractWithStructs> {
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"));
expect(output._person.name).to.be.eq("fred");
});

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 },
Expand Down
9 changes: 5 additions & 4 deletions 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<DumbContract> {
const factory = getContractFactory("DumbContract") as DumbContractFactory;
const factory = new DumbContractFactory(getTestSigner());
return factory.deploy(0);
}

Expand All @@ -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");
Expand Down
18 changes: 3 additions & 15 deletions 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;
Expand All @@ -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<Contract> {
const factory = getContractFactory(contractName);
return factory.deploy();
export function getTestSigner(): Signer {
return signer;
}

after(async () => {
Expand Down

0 comments on commit 1e11be2

Please sign in to comment.