From 6db8d8750a258e20dc1ca9e670ea3b1b8153d111 Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Wed, 8 Jan 2020 16:17:13 -0500 Subject: [PATCH] feat(@embark/nethermind): add Nethermind blockchain client plugin --- packages/core/core/src/config.ts | 3 + packages/core/core/src/plugin.ts | 7 - packages/core/engine/src/index.ts | 11 +- packages/core/utils/src/check.js | 62 ++++ packages/core/utils/src/index.ts | 1 + packages/embark/src/cmd/cmd_controller.js | 11 +- packages/embark/src/test/config.js | 2 +- .../ethereum-blockchain-client/src/index.js | 2 +- packages/plugins/geth/src/blockchain.js | 2 +- packages/plugins/geth/src/index.js | 1 + packages/plugins/nethermind/.npmrc | 4 + packages/plugins/nethermind/README.md | 20 ++ packages/plugins/nethermind/package.json | 67 ++++ packages/plugins/nethermind/src/blockchain.js | 301 ++++++++++++++++++ .../nethermind/src/blockchainProcess.js | 57 ++++ .../src/blockchainProcessLauncher.js | 84 +++++ packages/plugins/nethermind/src/index.js | 121 +++++++ .../nethermind/src/nethermindClient.js | 249 +++++++++++++++ packages/plugins/nethermind/tsconfig.json | 23 ++ packages/plugins/parity/src/blockchain.js | 2 +- packages/plugins/parity/src/index.js | 1 + .../plugins/whisper-geth/src/blockchain.js | 2 +- tsconfig.json | 3 + 23 files changed, 1022 insertions(+), 14 deletions(-) create mode 100644 packages/core/utils/src/check.js create mode 100644 packages/plugins/nethermind/.npmrc create mode 100644 packages/plugins/nethermind/README.md create mode 100644 packages/plugins/nethermind/package.json create mode 100644 packages/plugins/nethermind/src/blockchain.js create mode 100644 packages/plugins/nethermind/src/blockchainProcess.js create mode 100644 packages/plugins/nethermind/src/blockchainProcessLauncher.js create mode 100644 packages/plugins/nethermind/src/index.js create mode 100644 packages/plugins/nethermind/src/nethermindClient.js create mode 100644 packages/plugins/nethermind/tsconfig.json diff --git a/packages/core/core/src/config.ts b/packages/core/core/src/config.ts index 4e8683395..8ae4f06db 100644 --- a/packages/core/core/src/config.ts +++ b/packages/core/core/src/config.ts @@ -77,6 +77,8 @@ export class Config { version: string; + locale: string; + shownNoAccountConfigMsg = false; // flag to ensure "no account config" message is only displayed once to the user corsParts: string[] = []; @@ -95,6 +97,7 @@ export class Config { this.configDir = options.configDir || DEFAULT_CONFIG_PATH; this.chainsFile = options.chainsFile; this.plugins = options.plugins; + this.locale = options.locale || 'en'; this.logger = options.logger; this.package = options.package; this.events = options.events; diff --git a/packages/core/core/src/plugin.ts b/packages/core/core/src/plugin.ts index 9455b39dc..6234c1be7 100644 --- a/packages/core/core/src/plugin.ts +++ b/packages/core/core/src/plugin.ts @@ -138,10 +138,6 @@ export class Plugin { this._loggerObject[type](this.name + ':', ...[].slice.call(arguments, 1)); } - setUpLogger() { - this.logger = new Logger({}); - } - isContextValid() { if (this.currentContext.includes(constants.contexts.any) || this.acceptedContext.includes(constants.contexts.any)) { return true; @@ -161,9 +157,6 @@ export class Plugin { return false; } this.loaded = true; - if (this.shouldInterceptLogs) { - this.setUpLogger(); - } if (isEs6Module(this.pluginModule)) { if (this.pluginModule.default) { this.pluginModule = this.pluginModule.default; diff --git a/packages/core/engine/src/index.ts b/packages/core/engine/src/index.ts index 42492bbb8..11b1eee41 100644 --- a/packages/core/engine/src/index.ts +++ b/packages/core/engine/src/index.ts @@ -85,7 +85,16 @@ export class Engine { const options = _options || {}; this.events = options.events || this.events || new Events(); this.logger = this.logger || new Logger({context: this.context, logLevel: options.logLevel || this.logLevel || 'info', events: this.events, logFile: this.logFile}); - this.config = new Config({env: this.env, logger: this.logger, events: this.events, context: this.context, webServerConfig: this.webServerConfig, version: this.version, package: this.package}); + this.config = new Config({ + env: this.env, + logger: this.logger, + events: this.events, + context: this.context, + webServerConfig: this.webServerConfig, + version: this.version, + package: this.package, + locale: this.locale + }); this.config.loadConfigFiles({embarkConfig: this.embarkConfig, interceptLogs: this.interceptLogs}); this.plugins = this.config.plugins; this.isDev = this.config && this.config.blockchainConfig && (this.config.blockchainConfig.isDev || this.config.blockchainConfig.default); diff --git a/packages/core/utils/src/check.js b/packages/core/utils/src/check.js new file mode 100644 index 000000000..6c62babfd --- /dev/null +++ b/packages/core/utils/src/check.js @@ -0,0 +1,62 @@ +const WebSocket = require("ws"); +const http = require("http"); +const https = require("https"); + +const LIVENESS_CHECK=`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":42}`; + +const parseAndRespond = (data, cb) => { + let resp; + try { + resp = JSON.parse(data); + if (resp.error) { + return cb(resp.error); + } + } catch (e) { + return cb('Version data is not valid JSON'); + } + if (!resp || !resp.result) { + return cb('No version returned'); + } + const [_, version, __] = resp.result.split('/'); + cb(null, version); +}; + +const testRpcWithEndpoint = (endpoint, cb) => { + const options = { + method: "POST", + timeout: 1000, + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(LIVENESS_CHECK) + } + }; + + let obj = http; + if (endpoint.startsWith('https')) { + obj = https; + } + + const req = obj.request(endpoint, options, (res) => { + let data = ""; + res.on("data", chunk => { data += chunk; }); + res.on("end", () => parseAndRespond(data, cb)); + }); + req.on("error", (e) => cb(e)); + req.write(LIVENESS_CHECK); + req.end(); +}; + +const testWsEndpoint = (endpoint, cb) => { + const conn = new WebSocket(endpoint); + conn.on("message", (data) => { + parseAndRespond(data, cb); + conn.close(); + }); + conn.on("open", () => conn.send(LIVENESS_CHECK)); + conn.on("error", (e) => cb(e)); +}; + +module.exports = { + testWsEndpoint, + testRpcWithEndpoint +}; diff --git a/packages/core/utils/src/index.ts b/packages/core/utils/src/index.ts index f2ca2ec9c..c7d6a0837 100644 --- a/packages/core/utils/src/index.ts +++ b/packages/core/utils/src/index.ts @@ -6,6 +6,7 @@ const clipboardy = require('clipboardy'); import { canonicalHost } from './host'; export { canonicalHost, defaultCorsHost, defaultHost, dockerHostSwap, isDocker } from './host'; export { downloadFile, findNextPort, getJson, httpGet, httpsGet, httpGetJson, httpsGetJson, pingEndpoint } from './network'; +export { testRpcWithEndpoint, testWsEndpoint } from './check'; const logUtils = require('./log-utils'); export const escapeHtml = logUtils.escapeHtml; export const normalizeInput = logUtils.normalizeInput; diff --git a/packages/embark/src/cmd/cmd_controller.js b/packages/embark/src/cmd/cmd_controller.js index dbe56b656..6b68c54b0 100644 --- a/packages/embark/src/cmd/cmd_controller.js +++ b/packages/embark/src/cmd/cmd_controller.js @@ -58,13 +58,22 @@ class EmbarkController { }); engine.init({}, () => { + Object.assign(engine.config.blockchainConfig, { isStandalone: true }); + engine.registerModuleGroup("coreComponents"); engine.registerModuleGroup("blockchainStackComponents"); engine.registerModuleGroup("blockchain"); + // load custom plugins + engine.loadDappPlugins(); + let pluginList = engine.plugins.listPlugins(); + if (pluginList.length > 0) { + engine.logger.info(__("loaded plugins") + ": " + pluginList.join(", ")); + } + engine.startEngine(async () => { try { - const alreadyStarted = await engine.events.request2("blockchain:node:start", Object.assign(engine.config.blockchainConfig, { isStandalone: true })); + const alreadyStarted = await engine.events.request2("blockchain:node:start", engine.config.blockchainConfig); if (alreadyStarted) { engine.logger.warn(__('Blockchain process already started. No need to run `embark blockchain`')); process.exit(0); diff --git a/packages/embark/src/test/config.js b/packages/embark/src/test/config.js index 1f8167b33..27bda74f8 100644 --- a/packages/embark/src/test/config.js +++ b/packages/embark/src/test/config.js @@ -167,7 +167,7 @@ describe('embark.Config', function () { }, "datadir": ".embark/extNetwork/datadir", "rpcHost": "mynetwork.com", - "rpcPort": undefined, + "rpcPort": false, "rpcCorsDomain": { "auto": true, "additionalCors": [] diff --git a/packages/plugins/ethereum-blockchain-client/src/index.js b/packages/plugins/ethereum-blockchain-client/src/index.js index fd1b58fda..2837d50ee 100644 --- a/packages/plugins/ethereum-blockchain-client/src/index.js +++ b/packages/plugins/ethereum-blockchain-client/src/index.js @@ -64,7 +64,7 @@ class EthereumBlockchainClient { const code = contract.code.substring(0, 2) === '0x' ? contract.code : "0x" + contract.code; const contractObject = contractObj.deploy({arguments: (contract.args || []), data: code}); if (contract.gas === 'auto' || !contract.gas) { - const gasValue = await contractObject.estimateGas(); + const gasValue = await contractObject.estimateGas({value: 0, from: account}); const increase_per = 1 + (Math.random() / 10.0); contract.gas = Math.floor(gasValue * increase_per); } diff --git a/packages/plugins/geth/src/blockchain.js b/packages/plugins/geth/src/blockchain.js index c185b7c03..521f075eb 100644 --- a/packages/plugins/geth/src/blockchain.js +++ b/packages/plugins/geth/src/blockchain.js @@ -35,7 +35,7 @@ class Blockchain { this.config = { silent: this.userConfig.silent, client: this.userConfig.client, - ethereumClientBin: this.userConfig.ethereumClientBin || this.userConfig.client, + ethereumClientBin: this.userConfig.ethereumClientBin, networkType: this.userConfig.networkType || clientClass.DEFAULTS.NETWORK_TYPE, networkId: this.userConfig.networkId || clientClass.DEFAULTS.NETWORK_ID, genesisBlock: this.userConfig.genesisBlock || false, diff --git a/packages/plugins/geth/src/index.js b/packages/plugins/geth/src/index.js index 1a0de10bc..67e158b9a 100644 --- a/packages/plugins/geth/src/index.js +++ b/packages/plugins/geth/src/index.js @@ -12,6 +12,7 @@ class Geth { this.embarkConfig = embark.config.embarkConfig; this.blockchainConfig = embark.config.blockchainConfig; this.communicationConfig = embark.config.communicationConfig; + // TODO get options from config instead of options this.locale = options.locale; this.logger = embark.logger; this.client = options.client; diff --git a/packages/plugins/nethermind/.npmrc b/packages/plugins/nethermind/.npmrc new file mode 100644 index 000000000..e031d3432 --- /dev/null +++ b/packages/plugins/nethermind/.npmrc @@ -0,0 +1,4 @@ +engine-strict = true +package-lock = false +save-exact = true +scripts-prepend-node-path = true diff --git a/packages/plugins/nethermind/README.md b/packages/plugins/nethermind/README.md new file mode 100644 index 000000000..bf508ebe6 --- /dev/null +++ b/packages/plugins/nethermind/README.md @@ -0,0 +1,20 @@ +# `embark-nethermind` + +> Nethermind blockchain client plugin for Embark + + +## Quick docs + +To configure the Netherminds client, you can use the Embark configs as always, or for more control, use the Nethermind config files. +To change them, go in your Netherminds directory, then in `configs/`. There, you will see all the configuration files for the different networks. +If you ever need to run a different network than dev, testnet or mainnet, you can change it in the Embark blockchain configuration by changing the `networkType` to the name of the config file, without the `.cfg`. +Eg: For the Goerli network, just put `networkType: 'goerli` +Note: The dev mode of Netherminds is called `ndm` and the config file is `ndm_consumer_local.cfg`. Using `miningMode: 'dev'` automatically translates to using that config file. + +## Websocket support + +Even though Nethermind supports Websocket connections, it does not support `eth_subscribe`, so you will not be able to use contract events. +Also, please note that you will need to change the `endpoint` in the blockchain configuration to `ws://localhost:8545/ws/json-rpc` when working in local. Do change the port or the host to whatever you need. + +Visit [embark.status.im](https://embark.status.im/) to get started with +[Embark](https://github.com/embark-framework/embark). diff --git a/packages/plugins/nethermind/package.json b/packages/plugins/nethermind/package.json new file mode 100644 index 000000000..0d7ef94d6 --- /dev/null +++ b/packages/plugins/nethermind/package.json @@ -0,0 +1,67 @@ +{ + "name": "embark-nethermind", + "version": "5.0.0-alpha.9", + "author": "Iuri Matias ", + "contributors": [], + "description": "Nethermind blockchain client plugin for Embark", + "homepage": "https://github.com/embark-framework/embark/tree/master/packages/plugins/nethermind#readme", + "bugs": "https://github.com/embark-framework/embark/issues", + "keywords": [ + "blockchain", + "dapps", + "ethereum", + "serverless", + "nethermind" + ], + "files": [ + "dist" + ], + "license": "MIT", + "repository": { + "directory": "packages/plugins/nethermind", + "type": "git", + "url": "https://github.com/embark-framework/embark.git" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "embark-collective": { + "build:node": true, + "typecheck": true + }, + "scripts": { + "_build": "npm run solo -- build", + "_typecheck": "npm run solo -- typecheck", + "ci": "npm run qa", + "clean": "npm run reset", + "lint": "eslint src/", + "qa": "npm-run-all lint _typecheck _build", + "reset": "npx rimraf dist embark-*.tgz package", + "solo": "embark-solo" + }, + "eslintConfig": { + "extends": "../../../.eslintrc.json" + }, + "dependencies": { + "@babel/runtime-corejs3": "7.7.4", + "async": "2.6.1", + "core-js": "3.4.3", + "embark-core": "^5.0.0-alpha.9", + "embark-i18n": "^5.0.0-alpha.5", + "embark-utils": "^5.0.0-alpha.9", + "fs-extra": "8.1.0", + "netcat": "1.3.5", + "semver": "5.6.0", + "ws": "7.1.2" + }, + "devDependencies": { + "embark-solo": "^5.0.0-alpha.5", + "eslint": "5.7.0", + "npm-run-all": "4.1.5", + "rimraf": "3.0.0" + }, + "engines": { + "node": ">=10.17.0 <12.0.0", + "npm": ">=6.11.3", + "yarn": ">=1.19.1" + } +} diff --git a/packages/plugins/nethermind/src/blockchain.js b/packages/plugins/nethermind/src/blockchain.js new file mode 100644 index 000000000..5da236e4c --- /dev/null +++ b/packages/plugins/nethermind/src/blockchain.js @@ -0,0 +1,301 @@ +import {__} from 'embark-i18n'; +const async = require('async'); +const {spawn, exec} = require('child_process'); +const path = require('path'); +const constants = require('embark-core/constants'); +const NethermindClient = require('./nethermindClient.js'); +import {IPC} from 'embark-core'; + +import {compact, dappPath, defaultHost, dockerHostSwap, embarkPath} from 'embark-utils'; +import { Logger } from 'embark-logger'; + +// time between IPC connection attempts (in ms) +const IPC_CONNECT_INTERVAL = 2000; + +class Blockchain { + /*eslint complexity: ["error", 50]*/ + constructor(userConfig, clientClass) { + this.userConfig = userConfig; + this.env = userConfig.env || 'development'; + this.isDev = userConfig.isDev; + this.onReadyCallback = userConfig.onReadyCallback || (() => {}); + this.onExitCallback = userConfig.onExitCallback; + this.logger = userConfig.logger || new Logger({logLevel: 'debug', context: constants.contexts.blockchain}); // do not pass in events as we don't want any log events emitted + this.events = userConfig.events; + this.isStandalone = userConfig.isStandalone; + this.certOptions = userConfig.certOptions; + + let defaultWsApi = clientClass.DEFAULTS.WS_API; + if (this.isDev) defaultWsApi = clientClass.DEFAULTS.DEV_WS_API; + + this.config = { + silent: this.userConfig.silent, + client: this.userConfig.client, + ethereumClientBin: this.userConfig.ethereumClientBin, + networkType: this.userConfig.networkType || clientClass.DEFAULTS.NETWORK_TYPE, + networkId: this.userConfig.networkId || clientClass.DEFAULTS.NETWORK_ID, + genesisBlock: this.userConfig.genesisBlock || false, + datadir: this.userConfig.datadir, + mineWhenNeeded: this.userConfig.mineWhenNeeded || false, + rpcHost: dockerHostSwap(this.userConfig.rpcHost) || defaultHost, + rpcPort: this.userConfig.rpcPort || 8545, + rpcCorsDomain: this.userConfig.rpcCorsDomain || false, + rpcApi: this.userConfig.rpcApi || clientClass.DEFAULTS.RPC_API, + port: this.userConfig.port || 30303, + nodiscover: this.userConfig.nodiscover || false, + mine: this.userConfig.mine || false, + account: {}, + whisper: (this.userConfig.whisper !== false), + maxpeers: ((this.userConfig.maxpeers === 0) ? 0 : (this.userConfig.maxpeers || 25)), + bootnodes: this.userConfig.bootnodes || "", + wsRPC: (this.userConfig.wsRPC !== false), + wsHost: dockerHostSwap(this.userConfig.wsHost) || defaultHost, + wsPort: this.userConfig.wsPort || 8546, + wsOrigins: this.userConfig.wsOrigins || false, + wsApi: this.userConfig.wsApi || defaultWsApi, + vmdebug: this.userConfig.vmdebug || false, + targetGasLimit: this.userConfig.targetGasLimit || false, + syncMode: this.userConfig.syncMode || this.userConfig.syncmode, + verbosity: this.userConfig.verbosity, + proxy: this.userConfig.proxy, + customOptions: this.userConfig.customOptions + }; + + if (this.userConfig.accounts) { + const nodeAccounts = this.userConfig.accounts.find(account => account.nodeAccounts); + if (nodeAccounts) { + this.config.account = { + numAccounts: nodeAccounts.numAddresses || 1, + password: nodeAccounts.password, + balance: nodeAccounts.balance + }; + } + } + + // TODO I think we all do this in config.ts now + if (this.userConfig.default || JSON.stringify(this.userConfig) === '{"client":"nethermind"}') { + if (this.env === 'development') { + this.isDev = true; + } else { + this.config.genesisBlock = embarkPath("templates/boilerplate/config/privatenet/genesis.json"); + } + this.config.datadir = dappPath(".embark/development/datadir"); + this.config.wsOrigins = this.config.wsOrigins || "http://localhost:8000"; + this.config.rpcCorsDomain = this.config.rpcCorsDomain || "http://localhost:8000"; + this.config.targetGasLimit = 8000000; + } + this.config.account.devPassword = path.join(this.config.datadir, "devPassword"); + + const spaceMessage = 'The path for %s in blockchain config contains spaces, please remove them'; + if (this.config.datadir && this.config.datadir.indexOf(' ') > 0) { + this.logger.error(__(spaceMessage, 'datadir')); + process.exit(1); + } + if (this.config.account.password && this.config.account.password.indexOf(' ') > 0) { + this.logger.error(__(spaceMessage, 'accounts.password')); + process.exit(1); + } + if (this.config.genesisBlock && this.config.genesisBlock.indexOf(' ') > 0) { + this.logger.error(__(spaceMessage, 'genesisBlock')); + process.exit(1); + } + this.client = new clientClass({config: this.config, env: this.env, isDev: this.isDev, logger: this.logger}); + + this.initStandaloneProcess(); + } + + /** + * Polls for a connection to an IPC server (generally this is set up + * in the Embark process). Once connected, any logs logged to the + * Logger will be shipped off to the IPC server. In the case of `embark + * run`, the BlockchainListener module is listening for these logs. + * + * @returns {void} + */ + initStandaloneProcess() { + if (!this.isStandalone) { + return; + } + let logQueue = []; + + // on every log logged in logger (say that 3x fast), send the log + // to the IPC serve listening (only if we're connected of course) + this.logger.events.on('log', (logLevel, message) => { + if (this.ipc.connected) { + this.ipc.request('blockchain:log', {logLevel, message}); + } else { + logQueue.push({logLevel, message}); + } + }); + + this.ipc = new IPC({ipcRole: 'client'}); + + // Wait for an IPC server to start (ie `embark run`) by polling `.connect()`. + // Do not kill this interval as the IPC server may restart (ie restart + // `embark run` without restarting `embark blockchain`) + setInterval(() => { + if (!this.ipc.connected) { + this.ipc.connect(() => { + if (this.ipc.connected) { + logQueue.forEach(message => { + this.ipc.request('blockchain:log', message); + }); + logQueue = []; + this.ipc.client.on('process:blockchain:stop', () => { + this.kill(); + process.exit(0); + }); + } + }); + } + }, IPC_CONNECT_INTERVAL); + } + + runCommand(cmd, options, callback) { + this.logger.info(__("running: %s", cmd.underline).green); + if (this.config.silent) { + options.silent = true; + } + return exec(cmd, options, callback); + } + + run() { + var self = this; + this.logger.info("===============================================================================".magenta); + this.logger.info("===============================================================================".magenta); + this.logger.info(__("Embark Blockchain using %s", self.client.prettyName.underline).magenta); + this.logger.info("===============================================================================".magenta); + this.logger.info("===============================================================================".magenta); + + let address = ''; + async.waterfall([ + function checkInstallation(next) { + self.isClientInstalled((err) => { + if (err) { + return next({message: err}); + } + next(); + }); + }, + function getMainCommand(next) { + self.client.mainCommand(address, (cmd, args) => { + next(null, cmd, args); + }, true); + } + ], function (err, cmd, args) { + if (err) { + self.logger.error(err.message); + return; + } + args = compact(args); + + let full_cmd = cmd + " " + args.join(' '); + self.logger.info(__("running: %s", full_cmd.underline).green); + self.child = spawn(cmd, args, {cwd: process.cwd()}); + + self.child.on('error', (err) => { + err = err.toString(); + self.logger.error('Blockchain error: ', err); + if (self.env === 'development' && err.indexOf('Failed to unlock') > 0) { + self.logger.error('\n' + __('Development blockchain has changed to use the --dev option.').yellow); + self.logger.error(__('You can reset your workspace to fix the problem with').yellow + ' embark reset'.cyan); + self.logger.error(__('Otherwise, you can change your data directory in blockchain.json (datadir)').yellow); + } + }); + + self.child.stderr.on('data', (data) => { + self.logger.info(`${self.client.name} error: ${data}`); + }); + + self.child.stdout.on('data', async (data) => { + data = data.toString(); + if (!self.readyCalled && self.client.isReady(data)) { + self.readyCalled = true; + self.readyCallback(); + } + self.logger.info(`${self.client.name}: ${data}`); + }); + + self.child.on('exit', (code) => { + let strCode; + if (code) { + strCode = 'with error code ' + code; + } else { + strCode = 'with no error code (manually killed?)'; + } + self.logger.error(self.client.name + ' exited ' + strCode); + if (self.onExitCallback) { + self.onExitCallback(); + } + }); + + self.child.on('uncaughtException', (err) => { + self.logger.error('Uncaught ' + self.client.name + ' exception', err); + if (self.onExitCallback) { + self.onExitCallback(); + } + }); + }); + } + + readyCallback() { + if (this.onReadyCallback) { + this.onReadyCallback(); + } + } + + kill() { + this.shutdownProxy(); + if (this.child) { + this.child.kill(); + } + } + + isClientInstalled(callback) { + let versionCmd = this.client.determineVersionCommand(); + this.runCommand(versionCmd, {}, (err, stdout, stderr) => { + if (err || !stdout || stderr.indexOf("not found") >= 0 || stdout.indexOf("not found") >= 0) { + return callback(__('Ethereum client bin not found:') + ' ' + this.client.getBinaryPath()); + } + const parsedVersion = this.client.parseVersion(stdout); + const supported = this.client.isSupportedVersion(parsedVersion); + if (supported === undefined) { + this.logger.warn((__('WARNING! Ethereum client version could not be determined or compared with version range') + ' ' + this.client.versSupported + __(', for best results please use a supported version'))); + } else if (!supported) { + this.logger.warn((__('WARNING! Ethereum client version unsupported, for best results please use a version in range') + ' ' + this.client.versSupported)); + } + callback(); + }); + } +} + +export class BlockchainClient extends Blockchain { + constructor(userConfig, options) { + if (JSON.stringify(userConfig) === '{"enabled":true}' && options.env !== 'development') { + options.logger.info("===> " + __("warning: running default config on a non-development environment")); + } + // if client is not set in preferences, default is parity + if (!userConfig.client) userConfig.client = constants.blockchain.clients.parity; + // if clientName is set, it overrides preferences + if (options.clientName) userConfig.client = options.clientName; + // Choose correct client instance based on clientName + let clientClass; + switch (userConfig.client) { + case 'nethermind': + clientClass = NethermindClient; + break; + default: + console.error(__('Unknown client "%s". Please use one of the following: %s', userConfig.client, Object.keys(constants.blockchain.clients).join(', '))); + process.exit(1); + } + userConfig.isDev = (userConfig.isDev || userConfig.default); + userConfig.env = options.env; + userConfig.onReadyCallback = options.onReadyCallback; + userConfig.onExitCallback = options.onExitCallback; + userConfig.logger = options.logger; + userConfig.certOptions = options.certOptions; + userConfig.isStandalone = options.isStandalone; + + super(userConfig, clientClass); + } +} diff --git a/packages/plugins/nethermind/src/blockchainProcess.js b/packages/plugins/nethermind/src/blockchainProcess.js new file mode 100644 index 000000000..897f37342 --- /dev/null +++ b/packages/plugins/nethermind/src/blockchainProcess.js @@ -0,0 +1,57 @@ +import * as i18n from 'embark-i18n'; +import { ProcessWrapper } from 'embark-core'; +const constants = require('embark-core/constants'); +import { BlockchainClient } from './blockchain'; + +let blockchainProcess; + +class BlockchainProcess extends ProcessWrapper { + constructor(options) { + super(); + this.blockchainConfig = options.blockchainConfig; + this.client = options.client; + this.env = options.env; + this.isDev = options.isDev; + this.certOptions = options.certOptions; + + i18n.setOrDetectLocale(options.locale); + + this.blockchainConfig.silent = true; + this.blockchain = new BlockchainClient( + this.blockchainConfig, + { + clientName: this.client, + env: this.env, + certOptions: this.certOptions, + onReadyCallback: this.blockchainReady.bind(this), + onExitCallback: this.blockchainExit.bind(this), + logger: console + } + ); + + this.blockchain.run(); + } + + blockchainReady() { + blockchainProcess.send({result: constants.blockchain.blockchainReady}); + } + + blockchainExit() { + // tell our parent process that ethereum client has exited + blockchainProcess.send({result: constants.blockchain.blockchainExit}); + } + + kill() { + this.blockchain.kill(); + } +} + +process.on('message', (msg) => { + if (msg === 'exit') { + return blockchainProcess.kill(); + } + if (msg.action === constants.blockchain.init) { + blockchainProcess = new BlockchainProcess(msg.options); + return blockchainProcess.send({result: constants.blockchain.initiated}); + } +}); diff --git a/packages/plugins/nethermind/src/blockchainProcessLauncher.js b/packages/plugins/nethermind/src/blockchainProcessLauncher.js new file mode 100644 index 000000000..e27d9f2ff --- /dev/null +++ b/packages/plugins/nethermind/src/blockchainProcessLauncher.js @@ -0,0 +1,84 @@ +import { __ } from 'embark-i18n'; +import { ProcessLauncher } from 'embark-core'; +import { joinPath } from 'embark-utils'; +const constants = require('embark-core/constants'); + +export class BlockchainProcessLauncher { + + constructor (options) { + this.events = options.events; + this.env = options.env; + this.logger = options.logger; + this.normalizeInput = options.normalizeInput; + this.blockchainConfig = options.blockchainConfig; + this.locale = options.locale; + this.isDev = options.isDev; + this.client = options.client; + this.embark = options.embark; + } + + processEnded(code) { + this.logger.error(__('Blockchain process ended before the end of this process. Try running blockchain in a separate process using `$ embark blockchain`. Code: %s', code)); + } + + startBlockchainNode(readyCb) { + this.logger.info(__('Starting Blockchain node in another process').cyan); + + this.blockchainProcess = new ProcessLauncher({ + name: 'blockchain', + modulePath: joinPath(__dirname, './blockchainProcess.js'), + logger: this.logger, + events: this.events, + silent: this.logger.logLevel !== 'trace', + exitCallback: this.processEnded.bind(this), + embark: this.embark + }); + this.blockchainProcess.send({ + action: constants.blockchain.init, options: { + blockchainConfig: this.blockchainConfig, + client: this.client, + env: this.env, + isDev: this.isDev, + locale: this.locale, + certOptions: this.embark.config.webServerConfig.certOptions, + events: this.events + } + }); + + this.blockchainProcess.once('result', constants.blockchain.blockchainReady, () => { + this.logger.info(__('Blockchain node is ready').cyan); + readyCb(); + }); + + this.blockchainProcess.once('result', constants.blockchain.blockchainExit, () => { + // tell everyone that our blockchain process (ie geth) died + this.events.emit(constants.blockchain.blockchainExit); + + // then kill off the blockchain process + this.blockchainProcess.kill(); + }); + + + this.events.setCommandHandler('logs:ethereum:enable', () => { + this.blockchainProcess.setSilent(false); + }); + + this.events.setCommandHandler('logs:ethereum:disable', () => { + this.blockchainProcess.setSilent(true); + }); + + this.events.on('exit', () => { + this.blockchainProcess.send('exit'); + }); + } + + stopBlockchainNode(cb) { + if (this.blockchainProcess) { + this.events.once(constants.blockchain.blockchainExit, cb); + this.blockchainProcess.exitCallback = () => {}; // don't show error message as the process was killed on purpose + this.blockchainProcess.send('exit'); + } + } + +} + diff --git a/packages/plugins/nethermind/src/index.js b/packages/plugins/nethermind/src/index.js new file mode 100644 index 000000000..a4130564f --- /dev/null +++ b/packages/plugins/nethermind/src/index.js @@ -0,0 +1,121 @@ +import { __ } from 'embark-i18n'; +import {BlockchainClient} from "./blockchain"; +const {normalizeInput, testRpcWithEndpoint, testWsEndpoint} = require('embark-utils'); +import {BlockchainProcessLauncher} from './blockchainProcessLauncher'; + +export const NETHERMIND_NAME = 'nethermind'; + +class Nethermind { + constructor(embark) { + this.embark = embark; + this.embarkConfig = embark.config.embarkConfig; + this.blockchainConfig = embark.config.blockchainConfig; + this.locale = embark.config.locale; + this.logger = embark.logger; + this.client = embark.config.blockchainConfig.client; + this.isDev = embark.config.blockchainConfig.isDev; + this.events = embark.events; + + if (!this.shouldInit()) { + return; + } + + this.events.request("blockchain:node:register", NETHERMIND_NAME, { + isStartedFn: (isStartedCb) => { + this._doCheck((state) => { + console.log('Started?', JSON.stringify(state)); + return isStartedCb(null, state.status === "on"); + }); + }, + launchFn: (readyCb) => { + this.events.request('processes:register', 'blockchain', { + launchFn: (cb) => { + this.startBlockchainNode(cb); + }, + stopFn: (cb) => { + this.stopBlockchainNode(cb); + } + }); + this.events.request("processes:launch", "blockchain", (err) => { + if (err) { + this.logger.error(`Error launching blockchain process: ${err.message || err}`); + } + readyCb(); + }); + this.registerServiceCheck(); + }, + stopFn: async (cb) => { + await this.events.request("processes:stop", "blockchain"); + cb(); + } + }); + } + + shouldInit() { + return ( + this.blockchainConfig.client === NETHERMIND_NAME && + this.blockchainConfig.enabled + ); + } + + _getNodeState(err, version, cb) { + if (err) return cb({ name: "Ethereum node not found", status: 'off' }); + + return cb({ name: `${NETHERMIND_NAME} (Ethereum)`, status: 'on' }); + } + + _doCheck(cb) { + if (this.blockchainConfig.endpoint.startsWith('ws')) { + return testWsEndpoint(this.blockchainConfig.endpoint, (err, version) => this._getNodeState(err, version, cb)); + } + testRpcWithEndpoint(this.blockchainConfig.endpoint, (err, version) => this._getNodeState(err, version, cb)); + } + + registerServiceCheck() { + this.events.request("services:register", 'Ethereum', this._doCheck.bind(this), 5000, 'off'); + } + + startBlockchainNode(callback) { + if (this.blockchainConfig.isStandalone) { + return new BlockchainClient(this.blockchainConfig, { + clientName: NETHERMIND_NAME, + env: this.embark.config.env, + certOptions: this.embark.config.webServerConfig.certOptions, + logger: this.logger, + events: this.events, + isStandalone: true, + fs: this.embark.fs + }).run(); + } + + this.blockchainProcess = new BlockchainProcessLauncher({ + events: this.events, + env: this.embark.config.env, + logger: this.logger, + normalizeInput, + blockchainConfig: this.blockchainConfig, + locale: this.locale, + client: this.client, + isDev: this.isDev, + embark: this.embark + }); + + this.blockchainProcess.startBlockchainNode(callback); + } + + stopBlockchainNode(cb) { + const message = __(`The blockchain process has been stopped. It can be restarted by running ${"service blockchain on".bold} in the Embark console.`); + + if (!this.blockchainProcess) { + return cb(); + } + + this.blockchainProcess.stopBlockchainNode(() => { + this.logger.info(message); + cb(); + }); + } + +} + +module.exports = Nethermind; diff --git a/packages/plugins/nethermind/src/nethermindClient.js b/packages/plugins/nethermind/src/nethermindClient.js new file mode 100644 index 000000000..804933101 --- /dev/null +++ b/packages/plugins/nethermind/src/nethermindClient.js @@ -0,0 +1,249 @@ +import {__} from 'embark-i18n'; +const path = require('path'); +const async = require('async'); +const semver = require('semver'); + +const DEFAULTS = { + "BIN": "Nethermind.Runner", + "VERSIONS_SUPPORTED": ">=1.4.0", + "NETWORK_TYPE": "custom", + "NETWORK_ID": 1337, + "RPC_API": ['eth', 'web3', 'net', 'debug', 'personal'], + "WS_API": ['eth', 'web3', 'net', 'debug', 'pubsub', 'personal'], + "DEV_WS_API": ['eth', 'web3', 'net', 'debug', 'pubsub', 'personal'], + "TARGET_GAS_LIMIT": 8000000 +}; + +const NETHERMIND_NAME = 'nethermind'; + +class NethermindClient { + + static get DEFAULTS() { + return DEFAULTS; + } + + constructor(options) { + this.logger = options.logger; + this.config = options.hasOwnProperty('config') ? options.config : {}; + this.env = options.hasOwnProperty('env') ? options.env : 'development'; + this.isDev = options.hasOwnProperty('isDev') ? options.isDev : (this.env === 'development'); + this.name = NETHERMIND_NAME; + this.prettyName = "Nethermind (https://github.com/NethermindEth/nethermind)"; + this.bin = this.config.ethereumClientBin || DEFAULTS.BIN; + this.versSupported = DEFAULTS.VERSIONS_SUPPORTED; + } + + isReady(data) { + return data.indexOf('Running server, url:') > -1; + } + + commonOptions() { + const config = this.config; + const cmd = []; + + cmd.push(this.determineNetworkType(config)); + + if (config.datadir) { + // There isn't a real data dir, so at least we put the keys there + cmd.push(`--KeyStore.KeyStoreDirectory=${config.datadir}`); + } + + if (config.syncMode === 'light') { + this.logger.warn('Light sync mode does not exist in Nethermind. Switching to fast'); + cmd.push("--Sync.FastSync=true"); + } else if (config.syncMode === 'fast') { + cmd.push("--Sync.FastSync=true"); + } + + // In dev mode we store all users passwords in the devPassword file, so Parity can unlock all users from the start + if (this.isDev && config.account && config.account.numAccounts) { + cmd.push(`--Wallet.DevAccounts=${config.account.numAccounts}`); + } else if (config.account && config.account.password) { + // TODO find a way to see if we can set a password + // cmd.push(`--password=${config.account.password}`); + } + + // TODO reanable this when the log level is usable + // currently, you have to restart the client for the log level to apply and even then, it looks bugged + // if (Number.isInteger(config.verbosity) && config.verbosity >= 0 && config.verbosity <= 5) { + // switch (config.verbosity) { + // case 0: + // cmd.push("--log=OFF"); + // break; + // case 1: + // cmd.push("--log=ERROR"); + // break; + // case 2: + // cmd.push("--log=WARN"); + // break; + // case 3: + // cmd.push("--log=INFO"); + // break; + // case 4: + // cmd.push("--log=DEBUG"); + // break; + // case 5: + // cmd.push("--log=TRACE"); + // break; + // default: + // cmd.push("--log=INFO"); + // break; + // } + // } + + return cmd; + } + + getMiner() { + console.warn(__("Miner requested, but Nethermind does not embed a miner! Use Geth or install ethminer (https://github.com/ethereum-mining/ethminer)").yellow); + return; + } + + getBinaryPath() { + return this.bin; + } + + determineVersionCommand() { + let launcher = 'Nethermind.Launcher'; + if (this.config.ethereumClientBin) { + // Replace the Runner by the Launcher in the path + // This is done because the Runner does not have a version command, but the Launcher has one ¯\_(ツ)_/¯ + launcher = this.config.ethereumClientBin.replace(path.basename(this.config.ethereumClientBin), launcher); + } + return `${launcher} --version`; + } + + parseVersion(rawVersionOutput) { + let parsed; + const match = rawVersionOutput.match(/v([0-9.]+)/); + if (match) { + parsed = match[1].trim(); + } + return parsed; + } + + isSupportedVersion(parsedVersion) { + let test; + try { + let v = semver(parsedVersion); + v = `${v.major}.${v.minor}.${v.patch}`; + test = semver.Range(this.versSupported).test(semver(v)); + if (typeof test !== 'boolean') { + test = undefined; + } + } finally { + // eslint-disable-next-line no-unsafe-finally + return test; + } + } + + determineNetworkType(config) { + if (this.isDev) { + return "--config=ndm_consumer_local"; + } + if (config.networkType === 'testnet') { + config.networkType = "ropsten"; + } + return "--config=" + config.networkType; + } + + determineRpcOptions(config) { + let cmd = []; + cmd.push("--Network.DiscoveryPort=" + config.port); + cmd.push("--JsonRpc.Port=" + config.rpcPort); + cmd.push("--JsonRpc.Host=" + config.rpcHost); + // Doesn't seem to support changing CORS + // if (config.rpcCorsDomain) { + // if (config.rpcCorsDomain === '*') { + // console.warn('=================================='); + // console.warn(__('rpcCorsDomain set to "all"')); + // console.warn(__('make sure you know what you are doing')); + // console.warn('=================================='); + // } + // cmd.push("--jsonrpc-cors=" + (config.rpcCorsDomain === '*' ? 'all' : config.rpcCorsDomain)); + // } else { + // console.warn('=================================='); + // console.warn(__('warning: cors is not set')); + // console.warn('=================================='); + // } + return cmd; + } + + determineWsOptions(config) { + let cmd = []; + if (config.wsRPC) { + cmd.push("--Init.WebSocketsEnabled=true"); + } + return cmd; + } + + mainCommand(address, done) { + let self = this; + let config = this.config; + let rpc_api = this.config.rpcApi; + let args = []; + async.waterfall([ + function commonOptions(callback) { + let cmd = self.commonOptions(); + args = args.concat(cmd); + callback(); + }, + function rpcOptions(callback) { + let cmd = self.determineRpcOptions(self.config); + args = args.concat(cmd); + callback(); + }, + function wsOptions(callback) { + let cmd = self.determineWsOptions(self.config); + args = args.concat(cmd); + callback(); + }, + function dontGetPeers(callback) { + if (config.nodiscover) { + args.push("--Init.DiscoveryEnabled=false"); + return callback(); + } + callback(); + }, + function vmDebug(callback) { + if (config.vmdebug) { + args.push("----Init.StoreTraces=true"); + return callback(); + } + callback(); + }, + function maxPeers(callback) { + args.push("--Network.ActivePeersMaxCount=" + config.maxpeers); + callback(); + }, + function bootnodes(callback) { + if (config.bootnodes && config.bootnodes !== "") { + args.push("--Discovery.Bootnodes=" + config.bootnodes); + return callback(); + } + callback(); + }, + function rpcApi(callback) { + args.push('--JsonRpc.EnabledModules=' + rpc_api.join(',')); + callback(); + }, + function customOptions(callback) { + if (config.customOptions) { + if (Array.isArray(config.customOptions)) { + config.customOptions = config.customOptions.join(' '); + } + args.push(config.customOptions); + return callback(); + } + callback(); + } + ], function (err) { + if (err) { + throw new Error(err.message); + } + return done(self.bin, args); + }); + } +} + +module.exports = NethermindClient; diff --git a/packages/plugins/nethermind/tsconfig.json b/packages/plugins/nethermind/tsconfig.json new file mode 100644 index 000000000..9fb3eb55c --- /dev/null +++ b/packages/plugins/nethermind/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "composite": true, + "declarationDir": "./dist", + "rootDir": "./src", + "tsBuildInfoFile": "./node_modules/.cache/tsc/tsconfig.embark-nethermind.tsbuildinfo" + }, + "extends": "../../../tsconfig.base.json", + "include": [ + "src/**/*" + ], + "references": [ + { + "path": "../../core/core" + }, + { + "path": "../../core/i18n" + }, + { + "path": "../../core/utils" + } + ] +} diff --git a/packages/plugins/parity/src/blockchain.js b/packages/plugins/parity/src/blockchain.js index 153af0774..0ef968a31 100644 --- a/packages/plugins/parity/src/blockchain.js +++ b/packages/plugins/parity/src/blockchain.js @@ -36,7 +36,7 @@ class Blockchain { this.config = { silent: this.userConfig.silent, client: this.userConfig.client, - ethereumClientBin: this.userConfig.ethereumClientBin || this.userConfig.client, + ethereumClientBin: this.userConfig.ethereumClientBin, networkType: this.userConfig.networkType || clientClass.DEFAULTS.NETWORK_TYPE, networkId: this.userConfig.networkId || clientClass.DEFAULTS.NETWORK_ID, genesisBlock: this.userConfig.genesisBlock || false, diff --git a/packages/plugins/parity/src/index.js b/packages/plugins/parity/src/index.js index e5a523af8..57913c0b9 100644 --- a/packages/plugins/parity/src/index.js +++ b/packages/plugins/parity/src/index.js @@ -11,6 +11,7 @@ class Parity { this.embark = embark; this.embarkConfig = embark.config.embarkConfig; this.blockchainConfig = embark.config.blockchainConfig; + // TODO get options from config instead of options this.locale = options.locale; this.logger = embark.logger; this.client = options.client; diff --git a/packages/plugins/whisper-geth/src/blockchain.js b/packages/plugins/whisper-geth/src/blockchain.js index fe66cc3ba..2cf4bbb07 100644 --- a/packages/plugins/whisper-geth/src/blockchain.js +++ b/packages/plugins/whisper-geth/src/blockchain.js @@ -33,7 +33,7 @@ class Blockchain { this.config = { silent: this.userConfig.silent, client: this.userConfig.client, - ethereumClientBin: this.userConfig.ethereumClientBin || this.userConfig.client, + ethereumClientBin: this.userConfig.ethereumClientBin, networkType: this.userConfig.networkType || clientClass.DEFAULTS.NETWORK_TYPE, networkId: this.userConfig.networkId || clientClass.DEFAULTS.NETWORK_ID, genesisBlock: this.userConfig.genesisBlock || false, diff --git a/tsconfig.json b/tsconfig.json index d612c1d65..ed48d5b19 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -85,6 +85,9 @@ { "path": "packages/plugins/mocha-tests" }, + { + "path": "packages/plugins/nethermind" + }, { "path": "packages/plugins/parity" },