mirror of https://github.com/embarklabs/embark.git
302 lines
12 KiB
JavaScript
302 lines
12 KiB
JavaScript
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);
|
|
}
|
|
}
|