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 all 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
7 changes: 5 additions & 2 deletions packages/buidler-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@
"@types/qs": "^6.5.3",
"@types/semver": "^6.0.2",
"@types/uuid": "^3.4.5",
"@types/ws": "^7.2.1",
"chai": "^4.2.0",
"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 +88,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,14 +99,16 @@
"mocha": "^5.2.0",
"node-fetch": "^2.6.0",
"qs": "^6.7.0",
"raw-body": "^2.4.1",
"semver": "^6.3.0",
"slash": "^3.0.0",
"solc": "0.5.15",
"solidity-parser-antlr": "^0.4.2",
"source-map-support": "^0.5.13",
"ts-essentials": "^2.0.7",
"tsort": "0.0.1",
"uuid": "^3.3.2"
"uuid": "^3.3.2",
"ws": "^7.2.1"
},
"nyc": {
"extension": [
Expand Down
88 changes: 88 additions & 0 deletions packages/buidler-core/src/builtin-tasks/jsonrpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 }, { network, buidlerArguments, config }) => {
if (
network.name !== BUIDLEREVM_NETWORK_NAME &&
buidlerArguments.network !== undefined
) {
throw new BuidlerError(
ERRORS.BUILTIN_TASKS.JSONRPC_UNSUPPORTED_NETWORK
);
}

try {
const serverConfig: JsonRpcServerConfig = {
hostname,
port,
provider: _createBuidlerEVMProvider(config)
};

const server = new JsonRpcServer(serverConfig);

process.exitCode = await server.listen();
} catch (error) {
if (BuidlerError.isBuidlerError(error)) {
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
226 changes: 226 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,226 @@
import chalk from "chalk";
import debug from "debug";
import { IncomingMessage, ServerResponse } from "http";
import getRawBody from "raw-body";
import WebSocket from "ws";

import { EthereumProvider } from "../../../types";
import { BuidlerError } from "../../core/errors";
import { ERRORS } from "../../core/errors-list";
import {
isSuccessfulJsonResponse,
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 handleHttp = async (req: IncomingMessage, res: ServerResponse) => {
let rpcReq: JsonRpcRequest | undefined;
let rpcResp: JsonRpcResponse | undefined;

try {
rpcReq = await _readHttpRequest(req);

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

// Validate the RPC response.
if (!isValidJsonResponse(rpcResp)) {
// Malformed response coming from the provider, report to user as an internal error.
rpcResp = await _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));
};

public handleWs = async (ws: WebSocket) => {
const subscriptions: string[] = [];
let isClosed = false;

const listener = (payload: { subscription: string; result: any }) => {
// Don't attempt to send a message to the websocket if we already know it is closed,
// or the current websocket connection isn't interested in the particular subscription.
if (isClosed || subscriptions.includes(payload.subscription)) {
return;
}

try {
ws.send(
JSON.stringify({
jsonrpc: "2.0",
method: "eth_subscribe",
params: payload
})
);
} catch (error) {
_handleError(error);
}
};

// Handle eth_subscribe notifications.
this._provider.addListener("notification", listener);

ws.on("message", async msg => {
let rpcReq: JsonRpcRequest | undefined;
let rpcResp: JsonRpcResponse | undefined;

try {
rpcReq = await _readWsRequest(msg as string);

rpcResp = await this._handleRequest(rpcReq);

// If eth_subscribe was successful, keep track of the subscription id,
// so we can cleanup on websocket close.
if (
rpcReq.method === "eth_subscribe" &&
isSuccessfulJsonResponse(rpcResp)
) {
subscriptions.push(rpcResp.result.id);
}
} catch (error) {
rpcResp = await _handleError(error);
}

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

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

ws.send(JSON.stringify(rpcResp));
});

ws.on("close", () => {
// Remove eth_subscribe listener.
this._provider.removeListener("notification", listener);

// Clear any active subscriptions for the closed websocket connection.
isClosed = true;
subscriptions.forEach(async subscriptionId => {
await this._provider.send("eth_unsubscribe", [subscriptionId]);
});
});
};

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
};
};
}

const _readHttpRequest = 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;
};

const _readWsRequest = (msg: string): JsonRpcRequest => {
let json: any;
try {
json = JSON.parse(msg);
} catch (error) {
throw new InvalidJsonInputError(`Parse error: ${error.message}`);
}

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

return json;
};

const _handleError = async (error: any): Promise<JsonRpcResponse> => {
_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",
id: null,
error: {
code: error.code,
message: error.message
}
};
};

const _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);
};
Loading