diff --git a/dapps/tests/app/config/communication.json b/dapps/tests/app/config/communication.json index a6b7f7617..96a2b498a 100644 --- a/dapps/tests/app/config/communication.json +++ b/dapps/tests/app/config/communication.json @@ -1,11 +1,12 @@ { "default": { - "enabled": true, + "client": "geth", "provider": "whisper", + "enabled": true, "available_providers": ["whisper"], "connection": { "host": "localhost", - "port": 8546, + "port": 3777, "type": "ws" } } diff --git a/packages/embark-blockchain-process/src/blockchain.js b/packages/embark-blockchain-process/src/blockchain.js index 731a6d320..e84dd14b3 100644 --- a/packages/embark-blockchain-process/src/blockchain.js +++ b/packages/embark-blockchain-process/src/blockchain.js @@ -201,7 +201,7 @@ Blockchain.prototype.run = function () { args = compact(args); let full_cmd = cmd + " " + args.join(' '); - self.logger.info(__("running: %s", full_cmd.underline).green); + self.logger.info(__(">>>>>>>>>>>>>>>>> running: %s", full_cmd.underline).green); self.child = spawn(cmd, args, {cwd: process.cwd()}); self.child.on('error', (err) => { diff --git a/packages/embark-whisper/src/api.js b/packages/embark-whisper/src/api.js index 5a74d30cb..d651ed751 100644 --- a/packages/embark-whisper/src/api.js +++ b/packages/embark-whisper/src/api.js @@ -13,7 +13,6 @@ class API { this.embark = embark; this.logger = embark.logger; this.web3 = web3; - this.registerAPICalls(); } registerAPICalls() { diff --git a/packages/embark-whisper/src/index.js b/packages/embark-whisper/src/index.js index f9afad55a..34469f89c 100644 --- a/packages/embark-whisper/src/index.js +++ b/packages/embark-whisper/src/index.js @@ -17,37 +17,44 @@ class Whisper { this.webSocketsChannels = {}; this.modulesPath = dappPath(embark.config.embarkConfig.generationDir, constants.dappArtifacts.symlinkDir); - this.api = new API(embark, this.web3); - this.api.registerAPICalls(); + this.whisperNodes = {}; + + this.events.setCommandHandler("whisper:node:register", (clientName, startCb) => { + this.whisperNodes[clientName] = startCb; + }); + + this.events.request("communication:node:register", "whisper", (readyCb) => { + let clientName = this.communicationConfig.client; + let registerCb = this.whisperNodes[clientName]; + registerCb.apply(registerCb, [readyCb]); + }); this.events.request("runcode:whitelist", 'embarkjs', () => { }); this.events.request("runcode:whitelist", 'embarkjs-whisper', () => { }); - // TODO: should launch its own whisper node - // this.events.on("communication:started", this.connectEmbarkJSProvider.bind(this)); - this.events.on("blockchain:started", this.connectEmbarkJSProvider.bind(this)); - - embark.registerActionForEvent("pipeline:generateAll:before", this.addEmbarkJSWhisperArtifact.bind(this)); - - this.events.request("communication:node:register", "whisper", (readyCb) => { - // TODO: should launch its own whisper node - console.dir("--- whisper readyCb") - console.dir('--- registering whisper node') - // this.events.request('processes:register', 'communication', { - // launchFn: (cb) => { - // this.startProcess(cb); - // }, - // stopFn: (cb) => { this.stopProcess(cb); } - // }); - // this.events.request("processes:launch", "communication", (err) => { - readyCb() - // }); - // this.registerServiceCheck() + this.events.on("communication:started", () => { + this.setWhisperProvider(); + this.api = new API(embark, this.web3); + this.api.registerAPICalls(); + this.connectEmbarkJSProvider.bind(this) }); + let plugin = this.plugins.createPlugin('whisperplugin', {}); + plugin.registerActionForEvent("pipeline:generateAll:before", this.addEmbarkJSWhisperArtifact.bind(this)); this.registerEmbarkJSCommunication() } + setWhisperProvider() { + let {host, port} = this.communicationConfig.connection; + let web3Endpoint = 'ws://' + host + ':' + port; + // Note: dont't pass to the provider things like {headers: {Origin: "embark"}}. Origin header is for browser to fill + // to protect user, it has no meaning if it is used server-side. See here for more details: https://github.com/ethereum/go-ethereum/issues/16608 + // Moreover, Parity reject origins that are not urls so if you try to connect with Origin: "embark" it gives the followin error: + // << Blocked connection to WebSockets server from untrusted origin: Some("embark") >> + // The best choice is to use void origin, BUT Geth rejects void origin, so to keep both clients happy we can use http://embark + this.web3.setProvider(new Web3.providers.WebsocketProvider(web3Endpoint, {headers: {Origin: constants.embarkResourceOrigin}})); + } + async addEmbarkJSWhisperArtifact(params, cb) { let connection = this.communicationConfig.connection || {}; const config = { diff --git a/packages/embark/src/cmd/cmd_controller.js b/packages/embark/src/cmd/cmd_controller.js index 4b1ad3aec..6f79816c0 100644 --- a/packages/embark/src/cmd/cmd_controller.js +++ b/packages/embark/src/cmd/cmd_controller.js @@ -162,6 +162,7 @@ class EmbarkController { engine.registerModuleGroup("stackComponents"); // TODO: replace with individual plugins + engine.registerModuleGroup("communication"); engine.registerModuleGroup("blockchain"); engine.registerModuleGroup("compiler"); engine.registerModuleGroup("contracts"); @@ -169,7 +170,6 @@ class EmbarkController { engine.registerModuleGroup("webserver"); engine.registerModuleGroup("filewatcher"); engine.registerModuleGroup("storage"); - engine.registerModuleGroup("communication"); engine.registerModuleGroup("cockpit"); engine.registerModuleGroup("namesystem"); engine.registerModulePackage('embark-deploy-tracker', {plugins: engine.plugins}); @@ -192,7 +192,6 @@ class EmbarkController { } catch (e) { return cb(e); } - cb(); }); diff --git a/packages/embark/src/lib/modules/geth/blockchain.js b/packages/embark/src/lib/modules/geth/blockchain.js index 7dba2eecd..f1570572f 100644 --- a/packages/embark/src/lib/modules/geth/blockchain.js +++ b/packages/embark/src/lib/modules/geth/blockchain.js @@ -5,6 +5,7 @@ const {spawn, exec} = require('child_process'); const path = require('path'); const constants = require('embark-core/constants'); const GethClient = require('./gethClient.js'); +const WhisperGethClient = require('./whisperClient.js'); // const ParityClient = require('./parityClient.js'); import { IPC } from 'embark-core'; @@ -15,7 +16,7 @@ const Logger = require('embark-logger'); const IPC_CONNECT_INTERVAL = 2000; /*eslint complexity: ["error", 50]*/ -var Blockchain = function(userConfig, clientClass) { +var Blockchain = function(userConfig, clientClass, communicationConfig) { this.userConfig = userConfig; this.env = userConfig.env || 'development'; this.isDev = userConfig.isDev; @@ -100,8 +101,7 @@ var Blockchain = function(userConfig, clientClass) { this.logger.error(__(spaceMessage, 'genesisBlock')); process.exit(1); } - this.client = new clientClass({config: this.config, env: this.env, isDev: this.isDev}); - + this.client = new clientClass({config: this.config, env: this.env, isDev: this.isDev, communicationConfig: communicationConfig}); this.initStandaloneProcess(); }; @@ -201,7 +201,7 @@ Blockchain.prototype.run = function () { args = compact(args); let full_cmd = cmd + " " + args.join(' '); - self.logger.info(__("running: %s", full_cmd.underline).green); + self.logger.info(__(">>>>>>>>>>>>>>>> running: %s", full_cmd.underline).green); self.child = spawn(cmd, args, {cwd: process.cwd()}); self.child.on('error', (err) => { @@ -413,7 +413,7 @@ Blockchain.prototype.initChainAndGetAddress = function (callback) { }); }; -export function BlockchainClient(userConfig, options) { +export function BlockchainClient(userConfig, options, communicationConfig) { if ((userConfig === {} || JSON.stringify(userConfig) === '{"enabled":true}') && options.env !== 'development') { options.logger.info("===> " + __("warning: running default config on a non-development environment")); } @@ -423,18 +423,13 @@ export function BlockchainClient(userConfig, options) { if (options.clientName) userConfig.client = options.clientName; // Choose correct client instance based on clientName let clientClass; - switch (userConfig.client) { - case constants.blockchain.clients.geth: - clientClass = GethClient; - break; - // case constants.blockchain.clients.parity: - // clientClass = ParityClient; - // 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); + if (communicationConfig) { + clientClass = WhisperGethClient + } else { + clientClass = GethClient; } + userConfig.isDev = (userConfig.isDev || userConfig.default); userConfig.env = options.env; userConfig.onReadyCallback = options.onReadyCallback; @@ -442,5 +437,5 @@ export function BlockchainClient(userConfig, options) { userConfig.logger = options.logger; userConfig.certOptions = options.certOptions; userConfig.isStandalone = options.isStandalone; - return new Blockchain(userConfig, clientClass); + return new Blockchain(userConfig, clientClass, communicationConfig); } diff --git a/packages/embark/src/lib/modules/geth/blockchainProcess.js b/packages/embark/src/lib/modules/geth/blockchainProcess.js index 75b82623c..968bb5eb6 100644 --- a/packages/embark/src/lib/modules/geth/blockchainProcess.js +++ b/packages/embark/src/lib/modules/geth/blockchainProcess.js @@ -9,6 +9,7 @@ class BlockchainProcess extends ProcessWrapper { constructor(options) { super(); this.blockchainConfig = options.blockchainConfig; + this.communicationConfig = options.communicationConfig; this.client = options.client; this.env = options.env; this.isDev = options.isDev; @@ -26,7 +27,8 @@ class BlockchainProcess extends ProcessWrapper { onReadyCallback: this.blockchainReady.bind(this), onExitCallback: this.blockchainExit.bind(this), logger: console - } + }, + this.communicationConfig ); this.blockchain.run(); diff --git a/packages/embark/src/lib/modules/geth/blockchainProcessLauncher.js b/packages/embark/src/lib/modules/geth/blockchainProcessLauncher.js index 19ef09afe..8dd631c60 100644 --- a/packages/embark/src/lib/modules/geth/blockchainProcessLauncher.js +++ b/packages/embark/src/lib/modules/geth/blockchainProcessLauncher.js @@ -10,6 +10,7 @@ export class BlockchainProcessLauncher { this.logger = options.logger; this.normalizeInput = options.normalizeInput; this.blockchainConfig = options.blockchainConfig; + this.communicationConfig = options.communicationConfig; this.locale = options.locale; this.isDev = options.isDev; this.client = options.client; @@ -35,6 +36,7 @@ export class BlockchainProcessLauncher { this.blockchainProcess.send({ action: constants.blockchain.init, options: { blockchainConfig: this.blockchainConfig, + communicationConfig: this.communicationConfig, client: this.client, env: this.env, isDev: this.isDev, diff --git a/packages/embark/src/lib/modules/geth/index.js b/packages/embark/src/lib/modules/geth/index.js index 208192fbe..7f0d3ea7e 100644 --- a/packages/embark/src/lib/modules/geth/index.js +++ b/packages/embark/src/lib/modules/geth/index.js @@ -9,6 +9,7 @@ class Geth { this.embark = embark; this.embarkConfig = embark.config.embarkConfig; this.blockchainConfig = embark.config.blockchainConfig; + this.communicationConfig = embark.config.communicationConfig; this.locale = options.locale; this.logger = embark.logger; this.client = options.client; @@ -23,7 +24,6 @@ class Geth { this.events.request("blockchain:node:register", constants.blockchain.clients.geth, (readyCb) => { this.events.request('processes:register', 'blockchain', { launchFn: (cb) => { - // this.startBlockchainNode(readyCb); this.startBlockchainNode(cb); }, stopFn: (cb) => { @@ -38,6 +38,24 @@ class Geth { }); this.registerServiceCheck(); }); + + this.events.request("whisper:node:register", constants.blockchain.clients.geth, readyCb => { + this.events.request('processes:register', 'communication', { + launchFn: cb => { + this.startWhisperNode(cb); + }, + stopFn: cb => { + this.stopWhisperNode(cb); + } + }); + + this.events.request("processes:launch", "communication", (err) => { + if (err) { + this.logger.error(`Error launching whisper process: ${err.message || err}`); + } + readyCb(); + }); + }); } shouldInit() { @@ -82,6 +100,31 @@ class Geth { this.blockchainProcess.startBlockchainNode(callback); } + startWhisperNode(callback) { + this.whisperProcess = new BlockchainProcessLauncher({ + events: this.events, + logger: this.logger, + normalizeInput, + blockchainConfig: this.blockchainConfig, + communicationConfig: this.communicationConfig, + locale: this.locale, + client: this.client, + isDev: this.isDev, + embark: this.embark + }); + this.whisperProcess.startBlockchainNode(callback); + } + + stopWhisperNode(cb) { + if (!this.whisperProcess) { + return cb(); + } + this.whisperProcess.stopBlockchainNode(() => { + this.logger.info(`The whisper process has been stopped.`); + cb(); + }); + } + stopBlockchainNode(cb) { const message = __(`The blockchain process has been stopped. It can be restarted by running ${"service blockchain on".bold} in the Embark console.`); diff --git a/packages/embark/src/lib/modules/geth/whisperClient.js b/packages/embark/src/lib/modules/geth/whisperClient.js new file mode 100644 index 000000000..3ec4f9e58 --- /dev/null +++ b/packages/embark/src/lib/modules/geth/whisperClient.js @@ -0,0 +1,261 @@ +import { __ } from 'embark-i18n'; +import { dappPath, ipcPath } from 'embark-utils'; +const async = require('async'); +const {exec, spawn} = require('child_process'); +const path = require('path'); +const GethMiner = require('./miner'); +const semver = require('semver'); +const constants = require('embark-core/constants'); + +const DEFAULTS = { + "BIN": "geth", + "VERSIONS_SUPPORTED": ">=1.8.14", + "NETWORK_TYPE": "custom", + "NETWORK_ID": 1337, + "RPC_API": ['eth', 'web3', 'net', 'debug', 'personal'], + "WS_API": ['eth', 'web3', 'net', 'shh', 'debug', 'pubsub', 'personal'], + "DEV_WS_API": ['eth', 'web3', 'net', 'shh', 'debug', 'pubsub', 'personal'], + "TARGET_GAS_LIMIT": 8000000 +}; + +class WhisperGethClient { + + static get DEFAULTS() { + return DEFAULTS; + } + + constructor(options) { + this.config = options && options.hasOwnProperty('config') ? options.config : {}; + this.communicationConfig = options.communicationConfig; + this.env = options && options.hasOwnProperty('env') ? options.env : 'development'; + this.isDev = options && options.hasOwnProperty('isDev') ? options.isDev : (this.env === 'development'); + this.name = constants.blockchain.clients.geth; + this.prettyName = "Go-Ethereum (https://github.com/ethereum/go-ethereum)"; + this.bin = this.config.ethereumClientBin || DEFAULTS.BIN; + this.versSupported = DEFAULTS.VERSIONS_SUPPORTED; + this.httpReady = false; + this.wsReady = !this.config.wsRPC; + } + + isReady(data) { + if (data.indexOf('WebSocket endpoint opened') > -1) { + this.wsReady = true; + } + return this.wsReady; + } + + /** + * Check if the client needs some sort of 'keep alive' transactions to avoid freezing by inactivity + * @returns {boolean} if keep alive is needed + */ + needKeepAlive() { + return false; + } + + commonOptions() { + return []; + } + + getMiner() { + return new GethMiner({datadir: this.config.datadir}); + } + + getBinaryPath() { + return this.bin; + } + + determineVersionCommand() { + return this.bin + " version"; + } + + parseVersion(rawVersionOutput) { + let parsed; + const match = rawVersionOutput.match(/Version: (.*)/); + 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) { + let cmd; + if (config.networkType === 'testnet') { + cmd = "--testnet"; + } else if (config.networkType === 'rinkeby') { + cmd = "--rinkeby"; + } else if (config.networkType === 'custom') { + cmd = "--networkid=" + config.networkId; + } + return cmd; + } + + runAsArchival(config) { + return config.networkId === 1337 || config.archivalMode; + } + + initGenesisCommmand() { + return ""; + } + + newAccountCommand() { + return ""; + } + + parseNewAccountCommandResultToAddress(data = "") { + if (data.match(/{(\w+)}/)) return "0x" + data.match(/{(\w+)}/)[1]; + return ""; + } + + listAccountsCommand() { + return ""; + } + + parseListAccountsCommandResultToAddress(data = "") { + if (data.match(/{(\w+)}/)) return "0x" + data.match(/{(\w+)}/)[1]; + return ""; + } + + parseListAccountsCommandResultToAddressList(data = "") { + const regex = RegExp(/{(\w+)}/g); + let match; + const accounts = []; + while ((match = regex.exec(data)) !== null) { + accounts.push('0x' + match[1]); + } + return accounts; + } + + parseListAccountsCommandResultToAddressCount(data = "") { + return 0; + } + + determineRpcOptions(config) { + let cmd = []; + cmd.push("--port=30304"); + cmd.push("--rpc"); + cmd.push("--rpcport=9998"); + cmd.push("--rpcaddr=" + config.rpcHost); + + if (config.rpcCorsDomain) { + if (config.rpcCorsDomain === '*') { + console.warn('=================================='); + console.warn(__('rpcCorsDomain set to *')); + console.warn(__('make sure you know what you are doing')); + console.warn('=================================='); + } + cmd.push("--rpccorsdomain=" + config.rpcCorsDomain); + } else { + console.warn('=================================='); + console.warn(__('warning: cors is not set')); + console.warn('=================================='); + } + return cmd; + } + + determineWsOptions(config, communicationConfig) { + let cmd = []; + if (config.wsRPC) { + cmd.push("--ws"); + + cmd.push(`--wsport=${communicationConfig.connection.port || config.wsPost++}`); + cmd.push(`--wsaddr=${communicationConfig.connection.host || config.wsHost++}`); + + if (config.wsOrigins) { + if (config.wsOrigins === '*') { + console.warn('=================================='); + console.warn(__('wsOrigins set to *')); + console.warn(__('make sure you know what you are doing')); + console.warn('=================================='); + } + cmd.push("--wsorigins=" + config.wsOrigins); + } else { + console.warn('=================================='); + console.warn(__('warning: wsOrigins is not set')); + console.warn('=================================='); + } + } + return cmd; + } + + initDevChain(datadir, callback) { + callback(); + } + + mainCommand(address, done) { + let self = this; + let config = this.config; + let rpc_api = this.config.rpcApi; + let ws_api = this.config.wsApi; + let args = []; + async.series([ + function commonOptions(callback) { + let cmd = self.commonOptions(); + args = args.concat(cmd); + callback(null, cmd); + }, + function wsOptions(callback) { + let cmd = self.determineWsOptions(self.config, self.communicationConfig); + args = args.concat(cmd); + callback(null, cmd); + }, + function dontGetPeers(callback) { + if (config.nodiscover) { + args.push("--nodiscover"); + return callback(null, "--nodiscover"); + } + callback(null, ""); + }, + function maxPeers(callback) { + let cmd = "--maxpeers=" + config.maxpeers; + args.push(cmd); + callback(null, cmd); + }, + function bootnodes(callback) { + if (config.bootnodes && config.bootnodes !== "" && config.bootnodes !== []) { + args.push("--bootnodes=" + config.bootnodes); + return callback(null, "--bootnodes=" + config.bootnodes); + } + callback(""); + }, + function whisper(callback) { + rpc_api.push('shh'); + if (ws_api.indexOf('shh') === -1) { + ws_api.push('shh'); + } + args.push("--shh"); + return callback(null, "--shh "); + }, + function rpcApi(callback) { + args.push('--rpcapi=' + rpc_api.join(',')); + callback(null, '--rpcapi=' + rpc_api.join(',')); + }, + function wsApi(callback) { + args.push('--wsapi=' + ws_api.join(',')); + callback(null, '--wsapi=' + ws_api.join(',')); + } + ], function(err) { + if (err) { + throw new Error(err.message); + } + return done(self.bin, args); + }); + } +} + +module.exports = WhisperGethClient; +