@nomiclabs/buidler-truffle4
Version:
Truffle 4 Buidler compatibility plugin
310 lines (250 loc) • 9.42 kB
text/typescript
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;
}
};
}
}