UNPKG

@nomiclabs/buidler-truffle4

Version:
310 lines (250 loc) 9.42 kB
import { wrapWithSolidityErrorsCorrection } from "@nomiclabs/buidler/internal/buidler-evm/stack-traces/solidity-errors"; import { BuidlerPluginError } from "@nomiclabs/buidler/plugins"; import { NetworkConfig } from "@nomiclabs/buidler/types"; import util from "util"; import { Linker, TruffleContract, TruffleContractInstance } from "./types"; export class LazyTruffleContractProvisioner { private readonly _web3: any; private _defaultAccount?: string; private readonly _deploymentAddresses: { [contractName: string]: string; } = {}; constructor( web3: any, private readonly _networkConfig: NetworkConfig, defaultAccount?: string ) { this._defaultAccount = defaultAccount; this._web3 = web3; } public provision(Contract: TruffleContract, linker: Linker) { Contract.setProvider(this._web3.currentProvider); this._hookCloneCalls(Contract, linker); this._setDefaultValues(Contract); this._addDefaultParamsHooks(Contract); this._hookLink(Contract, linker); this._hookDeployed(Contract); return new Proxy(Contract, { construct(target, argumentsList, newTarget) { if (argumentsList.length > 0 && typeof argumentsList[0] === "string") { return target.at(argumentsList[0]); } return Reflect.construct(target, argumentsList, newTarget); }, }); } private _setDefaultValues(Contract: TruffleContract) { const defaults: any = {}; let hasDefaults = false; if (typeof this._networkConfig.gas === "number") { defaults.gas = this._networkConfig.gas; hasDefaults = true; } if (typeof this._networkConfig.gasPrice === "number") { defaults.gasPrice = this._networkConfig.gasPrice; hasDefaults = true; } if (this._defaultAccount !== undefined) { defaults.from = this._defaultAccount; hasDefaults = true; } if (hasDefaults) { Contract.defaults(defaults); } } private _addDefaultParamsHooks(Contract: TruffleContract) { const originalNew = Contract.new; const originalAt = Contract.at; Contract.new = async (...args: any[]) => { return wrapWithSolidityErrorsCorrection(async () => { args = await this._ensureTxParamsWithDefaults(args); const contractInstance = await originalNew.apply(Contract, args); this._addDefaultParamsToAllInstanceMethods(Contract, contractInstance); return contractInstance; }, 3); }; Contract.at = (...args: any[]) => { const contractInstance = originalAt.apply(Contract, args); contractInstance.then = (resolve: any, reject: any) => { delete contractInstance.then; Promise.resolve(contractInstance).then(resolve, reject); }; this._addDefaultParamsToAllInstanceMethods(Contract, contractInstance); return contractInstance; }; } private _hookLink(Contract: TruffleContract, linker: Linker) { const originalLink = Contract.link; const alreadyLinkedLibs: { [libName: string]: boolean } = {}; let linkingByInstance = false; Contract.link = (...args: any[]) => { // This is a simple way to detect if it is being called with a contract as first argument. if (args[0].constructor.name === "TruffleContract") { const libName = args[0].constructor.contractName; if (alreadyLinkedLibs[libName]) { throw new BuidlerPluginError( "@nomiclabs/buidler-truffle4", `Contract ${Contract.contractName} has already been linked to ${libName}.` ); } linkingByInstance = true; const ret = linker.link(Contract, args[0]); alreadyLinkedLibs[libName] = true; linkingByInstance = false; return ret; } if (!linkingByInstance) { if (typeof args[0] === "string") { throw new BuidlerPluginError( "@nomiclabs/buidler-truffle4", `Linking contracts by name is not supported by Buidler. Please use ${Contract.contractName}.link(libraryInstance) instead.` ); } throw new BuidlerPluginError( "@nomiclabs/buidler-truffle4", `Linking contracts with a map of addresses is not supported by Buidler. Please use ${Contract.contractName}.link(libraryInstance) instead.` ); } originalLink.apply(Contract, args); }; } private _addDefaultParamsToAllInstanceMethods( Contract: TruffleContract, contractInstance: TruffleContractInstance ) { this._getContractInstanceMethodsToOverride(Contract).forEach((name) => this._addDefaultParamsToInstanceMethod(contractInstance, name) ); } private _getContractInstanceMethodsToOverride(Contract: TruffleContract) { const DEFAULT_INSTANCE_METHODS_TO_OVERRIDE = ["sendTransaction"]; const abiFunctions = Contract.abi .filter((item: any) => item.type === "function") .map((item: any) => item.name); return [...DEFAULT_INSTANCE_METHODS_TO_OVERRIDE, ...abiFunctions]; } private _addDefaultParamsToInstanceMethod( instance: TruffleContractInstance, methodName: string ) { const abi = instance.contract.abi.filter( (abiElement: any) => abiElement.name === methodName )[0]; const isConstant = abi !== undefined && (abi.constant === true || abi.stateMutability === "view" || abi.stateMutability === "pure"); const original = instance[methodName]; const originalCall = original.call; const originalEstimateGas = original.estimateGas; const originalSendTransaction = original.sendTransaction; const originalRequest = original.request; instance[methodName] = async (...args: any[]) => { return wrapWithSolidityErrorsCorrection(async () => { args = await this._ensureTxParamsWithDefaults(args, !isConstant); return original.apply(instance, args); }, 3); }; instance[methodName].call = async (...args: any[]) => { return wrapWithSolidityErrorsCorrection(async () => { args = await this._ensureTxParamsWithDefaults(args, !isConstant); return originalCall.apply(original, args); }, 3); }; instance[methodName].estimateGas = async (...args: any[]) => { return wrapWithSolidityErrorsCorrection(async () => { args = await this._ensureTxParamsWithDefaults(args, !isConstant); return originalEstimateGas.apply(original, args); }, 3); }; instance[methodName].sendTransaction = async (...args: any[]) => { return wrapWithSolidityErrorsCorrection(async () => { args = await this._ensureTxParamsWithDefaults(args, !isConstant); return originalSendTransaction.apply(original, args); }, 3); }; instance[methodName].request = (...args: any[]) => { return originalRequest.apply(original, args); }; } private async _ensureTxParamsWithDefaults( args: any[], isDefaultAccountRequired = true ) { args = this._ensureTxParamsIsPresent(args); const txParams = args[args.length - 1]; args[args.length - 1] = await this._addDefaultTxParams( txParams, isDefaultAccountRequired ); return args; } private _ensureTxParamsIsPresent(args: any[]) { if (this._isLastArgumentTxParams(args)) { return args; } return [...args, {}]; } private _isLastArgumentTxParams(args: any[]) { const lastArg = args[args.length - 1]; return lastArg && Object.getPrototypeOf(lastArg) === Object.prototype; } private async _addDefaultTxParams( txParams: any, isDefaultAccountRequired = true ) { return { ...txParams, from: await this._getDefaultAccount(txParams, isDefaultAccountRequired), }; } private async _getDefaultAccount( txParams: any, isDefaultAccountRequired = true ) { if (txParams.from !== undefined) { return txParams.from; } if (this._defaultAccount === undefined) { const getAccounts = this._web3.eth.getAccounts.bind(this._web3.eth); const accounts = await util.promisify(getAccounts)(); if (accounts.length === 0) { if (isDefaultAccountRequired) { throw new BuidlerPluginError( "There's no account available in the selected network." ); } return undefined; } this._defaultAccount = accounts[0]; } return this._defaultAccount; } private _hookCloneCalls(Contract: TruffleContract, linker: Linker) { const originalClone = Contract.clone; Contract.clone = (...args: any[]) => { const cloned = originalClone.apply(Contract, args); return this.provision(cloned, linker); }; } private _hookDeployed(Contract: TruffleContract) { Contract.deployed = async () => { const address = this._deploymentAddresses[Contract.contractName]; if (address === undefined) { throw new BuidlerPluginError( "@nomiclabs/buidler-truffle5", `Trying to get deployed instance of ${Contract.contractName}, but none was set.` ); } return Contract.at(address); }; Contract.setAsDeployed = (instance?: any) => { if (instance === undefined) { delete this._deploymentAddresses[Contract.contractName]; } else { this._deploymentAddresses[Contract.contractName] = instance.address; } }; } }