import * as fs from "async-file"; import { Abi, AbiFunction, Primitive } from 'ethereum'; import { ContractCompiler } from "./ContractCompiler"; import { CompilerOutput, CompilerOutputContracts } from "solc"; import { CompilerConfiguration } from './CompilerConfiguration'; export class ContractInterfaceGenerator { private readonly compiler: ContractCompiler; private readonly configuration: CompilerConfiguration; public constructor(configuration: CompilerConfiguration, compiler: ContractCompiler) { this.compiler = compiler; this.configuration = configuration; } public async generateContractInterfaces(): Promise { const contractsOutput: CompilerOutput = await this.compiler.compileContracts(); const fileContents: String = this.contractInterfacesTemplate(contractsOutput.contracts); await fs.writeFile(this.configuration.contractInterfacesOutputPath, fileContents); return fileContents; } private contractInterfacesTemplate(contracts: CompilerOutputContracts) { const contractInterfaces: Array = []; // We add Controlled first to let other contracts inherit from it contractInterfaces.push(this.contractInterfaceTemplate("Controlled", contracts["Controlled.sol"]["Controlled"].abi)); for (let globalName in contracts) { for (let contractName in contracts[globalName]) { const contractAbi: Abi = contracts[globalName][contractName].abi; if (contractAbi.length == 0) continue; if (contractName == "Controlled") continue; contractInterfaces.push(this.contractInterfaceTemplate(contractName, contractAbi)); } } return `// THIS FILE IS AUTOMATICALLY GENERATED BY \`generateContractInterfaces.ts\`. DO NOT EDIT BY HAND' import BN = require('bn.js'); import { encodeMethod, decodeParams } from 'ethjs-abi'; import { AbiFunction } from 'ethereum'; import { AccountManager } from './AccountManager'; import { Connector } from './Connector'; /** * By convention, pure/view methods have a \`_\` suffix on them indicating to the caller that the function will be executed locally and return the function's result. payable/nonpayable functions have both a localy version and a remote version (distinguished by the trailing \`_\`). If the remote method is called, you will only get back a transaction hash which can be used to lookup the transaction receipt for success/failure (due to EVM limitations you will not get the function results back). */ export class Contract { protected readonly connector: Connector; protected readonly accountManager: AccountManager; public readonly address: string; protected readonly defaultGasPrice: BN; protected constructor(connector: Connector, accountManager: AccountManager, address: string, defaultGasPrice: BN) { this.connector = connector; this.accountManager = accountManager; this.address = address; this.defaultGasPrice = defaultGasPrice; } protected async localCall(abi: AbiFunction, parameters: Array, sender?: string, attachedEth?: BN): Promise> { const from = sender || this.accountManager.defaultAddress; const data = encodeMethod(abi, parameters); const transaction = Object.assign({ from: from, to: this.address, data: data }, attachedEth ? { value: attachedEth } : {}); const result = await this.connector.ethjsQuery.call(transaction); return decodeParams(abi.outputs.map(x => x.name), abi.outputs.map(x => x.type), result); } protected async remoteCall(abi: AbiFunction, parameters: Array, txName: String, sender?: string, gasPrice?: BN, attachedEth?: BN): Promise { const from = sender || this.accountManager.defaultAddress; const data = encodeMethod(abi, parameters); let gas = await this.connector.ethjsQuery.estimateGas(Object.assign({ to: this.address, from: from, data: data }, attachedEth ? { value: attachedEth } : {} )); // This is to address an observed bug while running against geth where occasionally gas estimates are lower than required: https://github.com/ethereum/go-ethereum/issues/15896 gas = BN.min(new BN(6950000), gas.add(gas.div(new BN(10)))); gasPrice = gasPrice || this.defaultGasPrice; const transaction = Object.assign({ from: from, to: this.address, data: data, gasPrice: gasPrice, gas: gas }, attachedEth ? { value: attachedEth } : {}); const signedTransaction = await this.accountManager.signTransaction(transaction); const transactionHash = await this.connector.ethjsQuery.sendRawTransaction(signedTransaction); const txReceipt = await this.connector.waitForTransactionReceipt(transactionHash, \`Waiting on receipt for tx: \${txName}\`); if (txReceipt.status != 1) { throw new Error(\`Tx \${txName} failed: \${txReceipt}\`); } } } ${contractInterfaces.join("\n")} export function ContractFactory(connector: Connector, accountManager: AccountManager, address: string, defaultGasPrice: BN): Controlled { return new Controlled(connector, accountManager, address, defaultGasPrice); } `; } private contractInterfaceTemplate(contractName: String, contractAbi: Abi) { const contractMethods: Array = []; // Typescript doesn't allow the same name for a function. We only have one existing case for function overloading in a class and it has the same signature, so this is ok at the moment. const seen: Set = new Set(); const contractFunctions: Array = contractAbi .filter(v => v.type == "function") .map(v => v); for (let abiFunction of contractFunctions) { if (seen.has(abiFunction.name)) continue; if (!abiFunction.constant) { contractMethods.push(this.remoteMethodTemplate(abiFunction)); } contractMethods.push(this.localMethodTemplate(abiFunction)); seen.add(abiFunction.name); } const extendsControlled: boolean = seen.has("getController") && contractName != "Controlled"; return ` export class ${contractName} extends ${extendsControlled ? "Controlled" : "Contract"} { public constructor(connector: Connector, accountManager: AccountManager, address: string, defaultGasPrice: BN) { super(connector, accountManager, address, defaultGasPrice); } ${contractMethods.join("\n\n")} } ` } private remoteMethodTemplate(abiFunction: AbiFunction) { const argNames: String = this.getArgNamesString(abiFunction); const params: String = this.getParamsString(abiFunction); const options: String = `{ sender?: string, gasPrice?: BN${abiFunction.payable ? ", attachedEth?: BN" : ""} }`; return ` public ${abiFunction.name} = async(${params} options?: ${options}): Promise => { options = options || {}; const abi: AbiFunction = ${JSON.stringify(abiFunction)}; await this.remoteCall(abi, [${argNames}], "${abiFunction.name}", options.sender, options.gasPrice${abiFunction.payable ? ", options.attachedEth" : ""}); return; }`; } private localMethodTemplate(abiFunction: AbiFunction) { const argNames: String = this.getArgNamesString(abiFunction); const params: String = this.getParamsString(abiFunction); const options: String = `{ sender?: string${abiFunction.payable ? ", attachedEth?: BN" : ""} }`; const returnType: String = (abiFunction.outputs[0] !== undefined) ? this.getTsTypeFromPrimitive(abiFunction.outputs[0].type) : "void"; const returnPromiseType: String = (abiFunction.outputs.length === 0 || abiFunction.outputs.length === 1) ? returnType : "Array"; const returnValue: String = abiFunction.outputs.length == 1 ? `<${returnType}>result[0]` : ">result"; return ` public ${abiFunction.name}_ = async(${params} options?: ${options}): Promise<${returnPromiseType}> => { options = options || {}; const abi: AbiFunction = ${JSON.stringify(abiFunction)}; ${abiFunction.outputs.length !== 0 ? 'const result = ' : ''}await this.localCall(abi, [${argNames}], options.sender${abiFunction.payable ? ", options.attachedEth" : ""}); ${abiFunction.outputs.length !== 0 ? `return ${returnValue};` : ''} }`; } private getTsTypeFromPrimitive(abiType: Primitive) { switch(abiType) { case 'uint8': case 'uint64': case 'uint256': case 'int256': { return 'BN'; } case 'string': case 'address': case 'bytes20': case 'bytes32': case 'bytes': { return 'string'; } case 'bool': { return 'boolean'; } case 'address[]': { return 'Array' } case 'uint256[]': { return 'Array'; } case 'bytes32[5]': case 'bytes32[10]': case 'bytes32[20]': case 'bytes32[50]': case 'bytes32[100]': case 'bytes32[200]': case 'bytes32[500]': case 'bytes32[1000]': case 'bytes32[]': { return 'Array'; } default: { throw "Unrecognized Value: " + abiType; } } } private getArgNamesString(abiFunction: AbiFunction) { return abiFunction.inputs.map((v, i) => v.name.substr(1) || `arg${i}`).join(", "); } private getParamsString(abiFunction: AbiFunction) { if (abiFunction.inputs.length == 0) return ""; return abiFunction.inputs.map((v, i) => (v.name.substr(1) || `arg${i}`) + ": " + this.getTsTypeFromPrimitive(v.type)).join(", ") + ","; } }