feat(@embark/nethermind): add Nethermind blockchain client plugin

This commit is contained in:
Jonathan Rainville 2020-01-08 16:17:13 -05:00 committed by Iuri Matias
parent 4190d5ec65
commit 6db8d8750a
23 changed files with 1022 additions and 14 deletions

View File

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

View File

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

View File

@ -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);

View File

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

View File

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

View File

@ -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);

View File

@ -167,7 +167,7 @@ describe('embark.Config', function () {
},
"datadir": ".embark/extNetwork/datadir",
"rpcHost": "mynetwork.com",
"rpcPort": undefined,
"rpcPort": false,
"rpcCorsDomain": {
"auto": true,
"additionalCors": []

View File

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

View File

@ -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,

View File

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

View File

@ -0,0 +1,4 @@
engine-strict = true
package-lock = false
save-exact = true
scripts-prepend-node-path = true

View File

@ -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).

View File

@ -0,0 +1,67 @@
{
"name": "embark-nethermind",
"version": "5.0.0-alpha.9",
"author": "Iuri Matias <iuri.matias@gmail.com>",
"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"
}
}

View File

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

View File

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

View File

@ -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');
}
}
}

View File

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

View File

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

View File

@ -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"
}
]
}

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -85,6 +85,9 @@
{
"path": "packages/plugins/mocha-tests"
},
{
"path": "packages/plugins/nethermind"
},
{
"path": "packages/plugins/parity"
},