Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BuidlerEVM JSON-RPC Server #438

Merged
merged 19 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
135cad9
Added simple JSON-RPC server task.
BencicAndrej Feb 6, 2020
85e9cbd
Updated body parsing and error handling logic.
BencicAndrej Feb 6, 2020
a9ab1af
Refactored JSON-RPC server into manageable components.
BencicAndrej Feb 7, 2020
66cfdc5
Addressed PR comments.
BencicAndrej Feb 7, 2020
457d18f
Addressed PR comments.
BencicAndrej Feb 7, 2020
3ddad21
Included reading the buffer inside of try/catch.
BencicAndrej Feb 7, 2020
31090f3
Refactored tests to support multiple providers under test.
BencicAndrej Feb 11, 2020
7708848
Fix json-rpc response and request's id types
alcuadrado Feb 11, 2020
fd539bf
Added check that --network parameter is not set when running `buidler…
BencicAndrej Feb 11, 2020
fb2c905
Merge remote-tracking branch 'origin/buidlerevm-rpc' into buidlerevm-rpc
BencicAndrej Feb 11, 2020
41404fe
Updated server shutdown method to include error handling.
BencicAndrej Feb 11, 2020
3b3e33a
Added explanation for empty throw.
BencicAndrej Feb 11, 2020
5e2bd43
Added specialized transaction error, and check for error codes.
BencicAndrej Feb 11, 2020
358cfa2
Added websocket support for JSON-RPC server.
BencicAndrej Feb 12, 2020
df1b070
Added eth_subscribe support for WebSocket server.
BencicAndrej Feb 12, 2020
057b4e0
Updated wording.
BencicAndrej Feb 14, 2020
e2d56f3
Changed how listeners work for eth_subscribe.
BencicAndrej Feb 14, 2020
2071817
Updated TransactionExecutionError to include parent and keep the stack.
BencicAndrej Feb 14, 2020
6355d03
Merge branch 'master' into buidlerevm-rpc
nebojsa94 Feb 14, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/buidler-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"time-require": "^0.1.2"
},
"dependencies": {
"@nomiclabs/ethereumjs-vm": "^4.1.1",
"@types/bn.js": "^4.11.5",
"@types/lru-cache": "^5.1.0",
"abort-controller": "^3.0.0",
Expand All @@ -86,7 +87,6 @@
"ethereumjs-common": "^1.3.2",
"ethereumjs-tx": "^2.1.1",
"ethereumjs-util": "^6.1.0",
"@nomiclabs/ethereumjs-vm": "^4.1.1",
"find-up": "^2.1.0",
"fp-ts": "1.19.3",
"fs-extra": "^7.0.1",
Expand All @@ -98,6 +98,7 @@
"mocha": "^5.2.0",
"node-fetch": "^2.6.0",
"qs": "^6.7.0",
"raw-body": "^2.4.1",
"semver": "^6.3.0",
"solc": "0.5.15",
"solidity-parser-antlr": "^0.4.2",
Expand Down
77 changes: 77 additions & 0 deletions packages/buidler-core/src/builtin-tasks/jsonrpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import debug from "debug";

import {
JsonRpcServer,
JsonRpcServerConfig
} from "../internal/buidler-evm/jsonrpc/server";
import { BUIDLEREVM_NETWORK_NAME } from "../internal/constants";
import { task, types } from "../internal/core/config/config-env";
import { BuidlerError } from "../internal/core/errors";
import { ERRORS } from "../internal/core/errors-list";
import { createProvider } from "../internal/core/providers/construction";
import { lazyObject } from "../internal/util/lazy";
import { EthereumProvider, ResolvedBuidlerConfig } from "../types";

import { TASK_JSONRPC } from "./task-names";

const log = debug("buidler:core:tasks:jsonrpc");

function _createBuidlerEVMProvider(
config: ResolvedBuidlerConfig
): EthereumProvider {
log("Creating BuidlerEVM Provider");

const networkName = BUIDLEREVM_NETWORK_NAME;
const networkConfig = config.networks[networkName];

return lazyObject(() => {
log(`Creating buidlerevm provider for JSON-RPC sever`);
return createProvider(
networkName,
networkConfig,
config.solc.version,
config.paths
);
});
}

export default function() {
task(TASK_JSONRPC, "Starts a buidler JSON-RPC server")
.addOptionalParam(
"hostname",
"The host to which to bind to for new connections",
"localhost",
types.string
)
.addOptionalParam(
"port",
"The port on which to listen for new connections",
8545,
types.int
)
.setAction(async ({ hostname, port }, { config }) => {
try {
BencicAndrej marked this conversation as resolved.
Show resolved Hide resolved
const serverConfig: JsonRpcServerConfig = {
hostname,
port,
provider: _createBuidlerEVMProvider(config)
};

const server = new JsonRpcServer(serverConfig);

process.exitCode = await server.listen();
} catch (error) {
if (BuidlerError.isBuidlerError(error)) {
BencicAndrej marked this conversation as resolved.
Show resolved Hide resolved
throw error;
}

throw new BuidlerError(
ERRORS.BUILTIN_TASKS.JSONRPC_SERVER_ERROR,
{
error: error.message
},
error
);
}
});
}
2 changes: 2 additions & 0 deletions packages/buidler-core/src/builtin-tasks/task-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const TASK_HELP = "help";

export const TASK_RUN = "run";

export const TASK_JSONRPC = "jsonrpc";

export const TASK_TEST = "test";

export const TASK_TEST_RUN_MOCHA_TESTS = "test:run-mocha-tests";
Expand Down
140 changes: 140 additions & 0 deletions packages/buidler-core/src/internal/buidler-evm/jsonrpc/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import chalk from "chalk";
import debug from "debug";
import { IncomingMessage, ServerResponse } from "http";
import getRawBody from "raw-body";

import { EthereumProvider } from "../../../types";
import { BuidlerError } from "../../core/errors";
import { ERRORS } from "../../core/errors-list";
import {
isValidJsonRequest,
isValidJsonResponse,
JsonRpcRequest,
JsonRpcResponse
} from "../../util/jsonrpc";
import {
BuidlerEVMProviderError,
InternalError,
InvalidJsonInputError,
InvalidRequestError
} from "../provider/errors";

const log = debug("buidler:core:buidler-evm:jsonrpc");

export default class JsonRpcHandler {
private _provider: EthereumProvider;

constructor(provider: EthereumProvider) {
this._provider = provider;
}

public requestListener = async (
req: IncomingMessage,
res: ServerResponse
) => {
let rpcReq: JsonRpcRequest | undefined;
let rpcResp: JsonRpcResponse | undefined;

try {
rpcReq = await this._readRequest(req);

rpcResp = await this._handleRequest(rpcReq);
} catch (error) {
rpcResp = await this._handleError(error);
}

// Validate the RPC response.
if (!isValidJsonResponse(rpcResp)) {
// Malformed response coming from the provider, report to user as an internal error.
rpcResp = await this._handleError(new InternalError("Internal error"));
}

if (rpcReq !== undefined) {
rpcResp.id = rpcReq.id;
}

res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(rpcResp));
};

private _readRequest = async (
req: IncomingMessage
): Promise<JsonRpcRequest> => {
let json;

try {
const buf = await getRawBody(req);
const text = buf.toString();

json = JSON.parse(text);
} catch (error) {
throw new InvalidJsonInputError(`Parse error: ${error.message}`);
}

if (!isValidJsonRequest(json)) {
throw new InvalidRequestError("Invalid request");
}

return json;
};

private _handleRequest = async (
req: JsonRpcRequest
): Promise<JsonRpcResponse> => {
console.log(req.method);

const result = await this._provider.send(req.method, req.params);

return {
jsonrpc: "2.0",
id: req.id,
result
};
};

private _handleError = async (error: any): Promise<JsonRpcResponse> => {
this._printError(error);

// In case of non-buidler error, treat it as internal and associate the appropriate error code.
if (!BuidlerEVMProviderError.isBuidlerEVMProviderError(error)) {
error = new InternalError(error.message);
}

return {
jsonrpc: "2.0",
error: {
code: error.code,
message: error.message
}
};
};

private _printError = (error: any) => {
if (BuidlerEVMProviderError.isBuidlerEVMProviderError(error)) {
// Report the error to console in the format of other BuidlerErrors (wrappedError.message),
// while preserving the stack from the originating error (error.stack).
const wrappedError = new BuidlerError(
ERRORS.BUILTIN_TASKS.JSONRPC_HANDLER_ERROR,
{
error: error.message
},
error
);

console.error(chalk.red(`Error ${wrappedError.message}`));
} else if (BuidlerError.isBuidlerError(error)) {
console.error(chalk.red(`Error ${error.message}`));
} else if (error instanceof Error) {
console.error(
chalk.red(`An unexpected error occurred: ${error.message}`)
);
} else {
console.error(chalk.red("An unexpected error occurred."));
}

console.log("");

console.error(error.stack);
};
}
74 changes: 74 additions & 0 deletions packages/buidler-core/src/internal/buidler-evm/jsonrpc/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import debug from "debug";
import http, { Server } from "http";

import { EthereumProvider } from "../../../types";
import { HttpProvider } from "../../core/providers/http";

import JsonRpcHandler from "./handler";

const log = debug("buidler:core:buidler-evm:jsonrpc");

export interface JsonRpcServerConfig {
hostname: string;
port: number;

provider: EthereumProvider;
}

export class JsonRpcServer {
private _config: JsonRpcServerConfig;
private _server: Server;

constructor(config: JsonRpcServerConfig) {
this._config = config;

const handler = new JsonRpcHandler(config.provider);

this._server = http.createServer(handler.requestListener);
}

public getProvider = (name = "json-rpc"): EthereumProvider => {
const { address, port } = this._server.address();

return new HttpProvider(`http://${address}:${port}/`, name);
};

public listen = (): Promise<number> => {
return new Promise<number>(async resolve => {
process.once("SIGINT", async () => {
await this.close();

resolve(0);
});

await this.start();
});
};

public start = async () => {
return new Promise(resolve => {
log(`Starting JSON-RPC server on port ${this._config.port}`);
this._server.listen(this._config.port, this._config.hostname, () => {
// We get the address and port directly from the server in order to handle random port allocation with `0`.
const { address, port } = this._server.address();

console.log(`Started JSON-RPC server at http://${address}:${port}/`);
BencicAndrej marked this conversation as resolved.
Show resolved Hide resolved

resolve();
});
});
};

public close = async () => {
BencicAndrej marked this conversation as resolved.
Show resolved Hide resolved
return new Promise(resolve => {
this._server.on("close", () => {
log("JSON-RPC server closed");

resolve();
});

log("Closing JSON-RPC server");
this._server.close();
});
};
}
20 changes: 20 additions & 0 deletions packages/buidler-core/src/internal/buidler-evm/provider/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,22 @@
// -32003 Transaction rejected Transaction creation failed non-standard

export class BuidlerEVMProviderError extends Error {
public static isBuidlerEVMProviderError(
other: any
): other is BuidlerEVMProviderError {
return (
other !== undefined &&
other !== null &&
other._isBuidlerEVMProviderError === true
);
}

private readonly _isBuidlerEVMProviderError: boolean;

constructor(message: string, public readonly code: number) {
super(message);

this._isBuidlerEVMProviderError = true;
}
}

Expand Down Expand Up @@ -57,6 +71,12 @@ export class InvalidInputError extends BuidlerEVMProviderError {
}
}

export class TransactionExecutionError extends BuidlerEVMProviderError {
constructor(message: string) {
super(message, -32003);
}
}

export class MethodNotSupportedError extends BuidlerEVMProviderError {
constructor(message: string) {
super(message, -32004);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ import { SolidityTracer } from "../stack-traces/solidityTracer";
import { VMTracer } from "../stack-traces/vm-tracer";

import { Blockchain } from "./blockchain";
import { InternalError, InvalidInputError } from "./errors";
import {
InternalError,
InvalidInputError,
TransactionExecutionError
} from "./errors";
import { getCurrentTimestamp } from "./utils";

const log = debug("buidler:core:buidler-evm:node");
Expand Down Expand Up @@ -77,8 +81,6 @@ export interface TransactionParams {
nonce: BN;
}

export class TransactionExecutionError extends Error {}

export interface TxBlockResult {
receipt: TxReceipt;
createAddresses: Buffer | undefined;
Expand Down
Loading