@nomiclabs/buidler
Version:
Buidler is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
343 lines (286 loc) • 9.95 kB
text/typescript
import ansiEscapes from "ansi-escapes";
import chalk, { Chalk } from "chalk";
import debug from "debug";
import Common from "ethereumjs-common";
import { BN } from "ethereumjs-util";
import { EventEmitter } from "events";
import fsExtra from "fs-extra";
import path from "path";
import semver from "semver";
import util from "util";
import { EthereumProvider, ProjectPaths } from "../../../types";
import { SOLC_INPUT_FILENAME, SOLC_OUTPUT_FILENAME } from "../../constants";
import { getUserConfigPath } from "../../core/project-structure";
import { CompilerInput, CompilerOutput } from "../stack-traces/compiler-types";
import { SolidityError } from "../stack-traces/solidity-errors";
import { FIRST_SOLC_VERSION_SUPPORTED } from "../stack-traces/solidityTracer";
import { Mutex } from "../vendor/await-semaphore";
import {
BuidlerEVMProviderError,
MethodNotFoundError,
MethodNotSupportedError,
} from "./errors";
import { BuidlerModule } from "./modules/buidler";
import { EthModule } from "./modules/eth";
import { EvmModule } from "./modules/evm";
import { ModulesLogger } from "./modules/logger";
import { NetModule } from "./modules/net";
import { Web3Module } from "./modules/web3";
import { BuidlerNode, GenesisAccount, SolidityTracerOptions } from "./node";
const log = debug("buidler:core:buidler-evm:provider");
// Set of methods that are never logged
const PRIVATE_RPC_METHODS = new Set(["buidler_getStackTraceFailuresCount"]);
// tslint:disable only-buidler-error
export class BuidlerEVMProvider extends EventEmitter
implements EthereumProvider {
private _common?: Common;
private _node?: BuidlerNode;
private _ethModule?: EthModule;
private _netModule?: NetModule;
private _web3Module?: Web3Module;
private _evmModule?: EvmModule;
private _buidlerModule?: BuidlerModule;
private readonly _mutex = new Mutex();
private readonly _logger = new ModulesLogger();
private _methodBeingCollapsed?: string;
private _methodCollapsedCount: number = 0;
constructor(
private readonly _hardfork: string,
private readonly _networkName: string,
private readonly _chainId: number,
private readonly _networkId: number,
private readonly _blockGasLimit: number,
private readonly _throwOnTransactionFailures: boolean,
private readonly _throwOnCallFailures: boolean,
private readonly _genesisAccounts: GenesisAccount[] = [],
private readonly _solcVersion?: string,
private readonly _paths?: ProjectPaths,
private readonly _loggingEnabled = false,
private readonly _allowUnlimitedContractSize = false,
private readonly _initialDate?: Date
) {
super();
const config = getUserConfigPath();
}
public async send(method: string, params: any[] = []): Promise<any> {
const release = await this._mutex.acquire();
try {
if (this._loggingEnabled && !PRIVATE_RPC_METHODS.has(method)) {
return await this._sendWithLogging(method, params);
}
return await this._send(method, params);
} finally {
release();
}
}
private async _sendWithLogging(
method: string,
params: any[] = []
): Promise<any> {
this._logger.clearLogs();
try {
const result = await this._send(method, params);
// We log after running the method, because we want to use different
// colors depending on whether it failed or not
// TODO: If an eth_call, eth_sendTransaction, or eth_sendRawTransaction
// fails without throwing, this will be displayed in green. It's unclear
// if this is correct. See Eth module's TODOs for more info.
if (this._shouldCollapseMethod(method)) {
this._logCollapsedMethod(method);
} else {
this._startCollapsingMethod(method);
this._log(method, false, chalk.green);
}
const loggedSomething = this._logModuleMessages();
if (loggedSomething) {
this._stopCollapsingMethod();
this._log("");
}
return result;
} catch (err) {
this._stopCollapsingMethod();
if (
err instanceof MethodNotFoundError ||
err instanceof MethodNotSupportedError
) {
this._log(`${method} - Method not supported`, false, chalk.red);
throw err;
}
this._log(method, false, chalk.red);
const loggedSomething = this._logModuleMessages();
if (loggedSomething) {
this._log("");
}
if (err instanceof SolidityError) {
this._logError(err);
} else if (err instanceof BuidlerEVMProviderError) {
this._log(err.message, true);
} else {
this._logError(err, true);
this._log("");
this._log(
"If you think this is a bug in Buidler, please report it here: https://buidler.dev/reportbug",
true
);
}
this._log("");
throw err;
}
}
private _logCollapsedMethod(method: string) {
this._methodCollapsedCount += 1;
process.stdout.write(
// tslint:disable-next-line:prefer-template
ansiEscapes.cursorHide +
ansiEscapes.cursorPrevLine +
chalk.green(`${method} (${this._methodCollapsedCount})`) +
"\n" +
ansiEscapes.eraseEndLine +
ansiEscapes.cursorShow
);
}
private _startCollapsingMethod(method: string) {
this._methodBeingCollapsed = method;
this._methodCollapsedCount = 1;
}
private _stopCollapsingMethod() {
this._methodBeingCollapsed = undefined;
this._methodCollapsedCount = 0;
}
private _shouldCollapseMethod(method: string) {
return (
method === this._methodBeingCollapsed &&
!this._logger.hasLogs() &&
this._methodCollapsedCount > 0
);
}
private async _send(method: string, params: any[] = []): Promise<any> {
await this._init();
if (method.startsWith("eth_")) {
return this._ethModule!.processRequest(method, params);
}
if (method.startsWith("net_")) {
return this._netModule!.processRequest(method, params);
}
if (method.startsWith("web3_")) {
return this._web3Module!.processRequest(method, params);
}
if (method.startsWith("evm_")) {
return this._evmModule!.processRequest(method, params);
}
if (method.startsWith("buidler_")) {
return this._buidlerModule!.processRequest(method, params);
}
throw new MethodNotFoundError(`Method ${method} not found`);
}
private async _init() {
if (this._node !== undefined) {
return;
}
let compilerInput: CompilerInput | undefined;
let compilerOutput: CompilerOutput | undefined;
if (this._solcVersion !== undefined && this._paths !== undefined) {
if (semver.lt(this._solcVersion, FIRST_SOLC_VERSION_SUPPORTED)) {
console.warn(
chalk.yellow(
`Solidity stack traces only work with Solidity version ${FIRST_SOLC_VERSION_SUPPORTED} or higher.`
)
);
} else {
let hasCompiledContracts = false;
if (await fsExtra.pathExists(this._paths.artifacts)) {
const artifactsDir = await fsExtra.readdir(this._paths.artifacts);
hasCompiledContracts = artifactsDir.some((f) => f.endsWith(".json"));
}
if (hasCompiledContracts) {
try {
const solcInputPath = path.join(
this._paths.cache,
SOLC_INPUT_FILENAME
);
const solcOutputPath = path.join(
this._paths.cache,
SOLC_OUTPUT_FILENAME
);
compilerInput = await fsExtra.readJSON(solcInputPath, {
encoding: "utf8",
});
compilerOutput = await fsExtra.readJSON(solcOutputPath, {
encoding: "utf8",
});
} catch (error) {
console.warn(
chalk.yellow(
"Stack traces engine could not be initialized. Run Buidler with --verbose to learn more."
)
);
log(
"Solidity stack traces disabled: Failed to read solc's input and output files. Please report this to help us improve Buidler.\n",
error
);
}
}
}
}
const [common, node] = await BuidlerNode.create(
this._hardfork,
this._networkName,
this._chainId,
this._networkId,
this._blockGasLimit,
this._genesisAccounts,
this._solcVersion,
this._allowUnlimitedContractSize,
this._initialDate,
compilerInput,
compilerOutput
);
this._common = common;
this._node = node;
this._ethModule = new EthModule(
common,
node,
this._throwOnTransactionFailures,
this._throwOnCallFailures,
this._loggingEnabled ? this._logger : undefined
);
this._netModule = new NetModule(common);
this._web3Module = new Web3Module();
this._evmModule = new EvmModule(node);
this._buidlerModule = new BuidlerModule(node);
const listener = (payload: { filterId: BN; result: any }) => {
this.emit("notifications", {
subscription: `0x${payload.filterId.toString(16)}`,
result: payload.result,
});
};
// Handle eth_subscribe events and proxy them to handler
this._node.addListener("ethEvent", listener);
}
private _logModuleMessages(): boolean {
const logs = this._logger.getLogs();
if (logs.length === 0) {
return false;
}
for (const msg of logs) {
this._log(msg, true);
}
return true;
}
private _logError(err: Error, logInRed = false) {
this._log(util.inspect(err), true, logInRed ? chalk.red : undefined);
}
private _log(msg: string, indent = false, color?: Chalk) {
if (indent) {
msg = msg
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
}
if (color !== undefined) {
console.log(color(msg));
return;
}
console.log(msg);
}
}