blockchain.js
1 const async = require('async'); 2 const {spawn, exec} = require('child_process'); 3 const fs = require('../../core/fs.js'); 4 const constants = require('../../constants.json'); 5 const utils = require('../../utils/utils.js'); 6 const GethClient = require('./gethClient.js'); 7 const ParityClient = require('./parityClient.js'); 8 const DevFunds = require('./dev_funds.js'); 9 const proxy = require('./proxy'); 10 const Ipc = require('../../core/ipc'); 11 12 const {defaultHost, dockerHostSwap} = require('../../utils/host'); 13 const Logger = require('../../core/logger'); 14 15 // time between IPC connection attmpts (in ms) 16 const IPC_CONNECT_INTERVAL = 2000; 17 18 /*eslint complexity: ["error", 38]*/ 19 var Blockchain = function(userConfig, clientClass) { 20 this.userConfig = userConfig; 21 this.env = userConfig.env || 'development'; 22 this.isDev = userConfig.isDev; 23 this.onReadyCallback = userConfig.onReadyCallback || (() => {}); 24 this.onExitCallback = userConfig.onExitCallback; 25 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 26 this.events = userConfig.events; 27 this.proxyIpc = null; 28 this.isStandalone = userConfig.isStandalone; 29 30 let defaultWsApi = clientClass.DEFAULTS.WS_API; 31 if (this.isDev) defaultWsApi = clientClass.DEFAULTS.DEV_WS_API; 32 33 this.config = { 34 silent: this.userConfig.silent, 35 ethereumClientName: this.userConfig.ethereumClientName, 36 ethereumClientBin: this.userConfig.ethereumClientBin || this.userConfig.ethereumClientName || 'geth', 37 networkType: this.userConfig.networkType || clientClass.DEFAULTS.NETWORK_TYPE, 38 networkId: this.userConfig.networkId || clientClass.DEFAULTS.NETWORK_ID, 39 genesisBlock: this.userConfig.genesisBlock || false, 40 datadir: this.userConfig.datadir || false, 41 mineWhenNeeded: this.userConfig.mineWhenNeeded || false, 42 rpcHost: dockerHostSwap(this.userConfig.rpcHost) || defaultHost, 43 rpcPort: this.userConfig.rpcPort || 8545, 44 rpcCorsDomain: this.userConfig.rpcCorsDomain || false, 45 rpcApi: this.userConfig.rpcApi || clientClass.DEFAULTS.RPC_API, 46 port: this.userConfig.port || 30303, 47 nodiscover: this.userConfig.nodiscover || false, 48 mine: this.userConfig.mine || false, 49 account: this.userConfig.account || {}, 50 devPassword: this.userConfig.devPassword || "", 51 whisper: (this.userConfig.whisper != false), 52 maxpeers: ((this.userConfig.maxpeers === 0) ? 0 : (this.userConfig.maxpeers || 25)), 53 bootnodes: this.userConfig.bootnodes || "", 54 wsRPC: (this.userConfig.wsRPC != false), 55 wsHost: dockerHostSwap(this.userConfig.wsHost) || defaultHost, 56 wsPort: this.userConfig.wsPort || 8546, 57 wsOrigins: this.userConfig.wsOrigins || false, 58 wsApi: this.userConfig.wsApi || defaultWsApi, 59 vmdebug: this.userConfig.vmdebug || false, 60 targetGasLimit: this.userConfig.targetGasLimit || false, 61 syncMode: this.userConfig.syncMode || this.userConfig.syncmode, 62 verbosity: this.userConfig.verbosity, 63 proxy: this.userConfig.proxy || true 64 }; 65 66 if (this.userConfig === {} || this.userConfig.default || JSON.stringify(this.userConfig) === '{"enabled":true}') { 67 this.config.account = {}; 68 if (this.env === 'development') { 69 this.isDev = true; 70 } else { 71 this.config.account.password = fs.embarkPath("templates/boilerplate/config/privatenet/password"); 72 this.config.genesisBlock = fs.embarkPath("templates/boilerplate/config/privatenet/genesis.json"); 73 } 74 this.config.datadir = fs.dappPath(".embark/development/datadir"); 75 this.config.wsOrigins = "http://localhost:8000"; 76 this.config.rpcCorsDomain = "http://localhost:8000"; 77 this.config.targetGasLimit = 8000000; 78 } 79 80 const spaceMessage = 'The path for %s in blockchain config contains spaces, please remove them'; 81 if (this.config.datadir && this.config.datadir.indexOf(' ') > 0) { 82 this.logger.error(__(spaceMessage, 'datadir')); 83 process.exit(); 84 } 85 if (this.config.account.password && this.config.account.password.indexOf(' ') > 0) { 86 this.logger.error(__(spaceMessage, 'account.password')); 87 process.exit(); 88 } 89 if (this.config.genesisBlock && this.config.genesisBlock.indexOf(' ') > 0) { 90 this.logger.error(__(spaceMessage, 'genesisBlock')); 91 process.exit(); 92 } 93 this.initProxy(); 94 this.client = new clientClass({config: this.config, env: this.env, isDev: this.isDev}); 95 96 this.initStandaloneProcess(); 97 }; 98 99 /** 100 * Polls for a connection to an IPC server (generally this is set up 101 * in the Embark process). Once connected, any logs logged to the 102 * Logger will be shipped off to the IPC server. In the case of `embark 103 * run`, the BlockchainListener module is listening for these logs. 104 * 105 * @returns {void} 106 */ 107 Blockchain.prototype.initStandaloneProcess = function () { 108 if (this.isStandalone) { 109 // on every log logged in logger (say that 3x fast), send the log 110 // to the IPC serve listening (only if we're connected of course) 111 this.events.on('log', (logLevel, message) => { 112 if (this.ipc.connected) { 113 this.ipc.request('blockchain:log', {logLevel, message}); 114 } 115 }); 116 117 this.ipc = new Ipc({ipcRole: 'client'}); 118 119 // Wait for an IPC server to start (ie `embark run`) by polling `.connect()`. 120 // Do not kill this interval as the IPC server may restart (ie restart 121 // `embark run` without restarting `embark blockchain`) 122 setInterval(() => { 123 if (!this.ipc.connected) { 124 this.ipc.connect(() => {}); 125 } 126 }, IPC_CONNECT_INTERVAL); 127 } 128 }; 129 130 Blockchain.prototype.initProxy = function () { 131 if (this.config.proxy) { 132 this.config.rpcPort += constants.blockchain.servicePortOnProxy; 133 this.config.wsPort += constants.blockchain.servicePortOnProxy; 134 } 135 }; 136 137 Blockchain.prototype.setupProxy = async function () { 138 if (!this.proxyIpc) this.proxyIpc = new Ipc({ipcRole: 'client'}); 139 140 let wsProxy; 141 if (this.config.wsRPC) { 142 wsProxy = proxy.serve(this.proxyIpc, this.config.wsHost, this.config.wsPort, true, this.config.wsOrigins); 143 } 144 145 [this.rpcProxy, this.wsProxy] = await Promise.all([proxy.serve(this.proxyIpc, this.config.rpcHost, this.config.rpcPort, false), wsProxy]); 146 }; 147 148 Blockchain.prototype.shutdownProxy = function () { 149 if (!this.config.proxy) { 150 return; 151 } 152 153 if (this.rpcProxy) this.rpcProxy.close(); 154 if (this.wsProxy) this.wsProxy.close(); 155 }; 156 157 Blockchain.prototype.runCommand = function (cmd, options, callback) { 158 this.logger.info(__("running: %s", cmd.underline).green); 159 if (this.config.silent) { 160 options.silent = true; 161 } 162 return exec(cmd, options, callback); 163 }; 164 165 Blockchain.prototype.run = function () { 166 var self = this; 167 this.logger.info("===============================================================================".magenta); 168 this.logger.info("===============================================================================".magenta); 169 this.logger.info(__("Embark Blockchain using %s", self.client.prettyName.underline).magenta); 170 this.logger.info("===============================================================================".magenta); 171 this.logger.info("===============================================================================".magenta); 172 173 if (self.client.name === 'geth') this.checkPathLength(); 174 175 let address = ''; 176 async.waterfall([ 177 function checkInstallation(next) { 178 self.isClientInstalled((err) => { 179 if (err) { 180 return next({message: err}); 181 } 182 next(); 183 }); 184 }, 185 function init(next) { 186 if (self.isDev) { 187 return self.initDevChain((err) => { 188 next(err); 189 }); 190 } 191 return self.initChainAndGetAddress((err, addr) => { 192 address = addr; 193 next(err); 194 }); 195 }, 196 function getMainCommand(next) { 197 self.client.mainCommand(address, function (cmd, args) { 198 next(null, cmd, args); 199 }, true); 200 } 201 ], function(err, cmd, args) { 202 if (err) { 203 self.logger.error(err.message); 204 return; 205 } 206 args = utils.compact(args); 207 208 let full_cmd = cmd + " " + args.join(' '); 209 self.logger.info(__("running: %s", full_cmd.underline).green); 210 self.child = spawn(cmd, args, {cwd: process.cwd()}); 211 212 self.child.on('error', (err) => { 213 err = err.toString(); 214 self.logger.error('Blockchain error: ', err); 215 if (self.env === 'development' && err.indexOf('Failed to unlock') > 0) { 216 self.logger.error('\n' + __('Development blockchain has changed to use the --dev option.').yellow); 217 self.logger.error(__('You can reset your workspace to fix the problem with').yellow + ' embark reset'.cyan); 218 self.logger.error(__('Otherwise, you can change your data directory in blockchain.json (datadir)').yellow); 219 } 220 }); 221 222 // TOCHECK I don't understand why stderr and stdout are reverted. 223 // This happens with Geth and Parity, so it does not seems a client problem 224 self.child.stdout.on('data', (data) => { 225 self.logger.info(`${self.client.name} error: ${data}`); 226 }); 227 228 self.child.stderr.on('data', async (data) => { 229 data = data.toString(); 230 if (!self.readyCalled && self.client.isReady(data)) { 231 self.readyCalled = true; 232 if (self.isDev) { 233 self.fundAccounts((err) => { 234 if (err) this.logger.error('Error funding accounts', err); 235 }); 236 } 237 if (self.config.proxy) { 238 await self.setupProxy(); 239 } 240 self.readyCallback(); 241 } 242 self.logger.info(`${self.client.name}: ${data}`); 243 }); 244 245 self.child.on('exit', (code) => { 246 let strCode; 247 if (code) { 248 strCode = 'with error code ' + code; 249 } else { 250 strCode = 'with no error code (manually killed?)'; 251 } 252 self.logger.error(self.client.name + ' exited ' + strCode); 253 if (self.onExitCallback) { 254 self.onExitCallback(); 255 } 256 }); 257 258 self.child.on('uncaughtException', (err) => { 259 self.logger.error('Uncaught ' + self.client.name + ' exception', err); 260 if (self.onExitCallback) { 261 self.onExitCallback(); 262 } 263 }); 264 }); 265 }; 266 267 Blockchain.prototype.fundAccounts = function(cb) { 268 DevFunds.new({blockchainConfig: this.config}).then(devFunds => { 269 devFunds.fundAccounts(this.client.needKeepAlive(), (err) => { 270 cb(err); 271 }); 272 }); 273 }; 274 275 Blockchain.prototype.readyCallback = function () { 276 if (this.onReadyCallback) { 277 this.onReadyCallback(); 278 } 279 if (this.config.mineWhenNeeded && !this.isDev) { 280 this.miner = this.client.getMiner(); 281 } 282 }; 283 284 Blockchain.prototype.kill = function () { 285 this.shutdownProxy(); 286 if (this.child) { 287 this.child.kill(); 288 } 289 }; 290 291 Blockchain.prototype.checkPathLength = function () { 292 let dappPath = fs.dappPath(''); 293 if (dappPath.length > 66) { 294 // this.logger.error is captured and sent to the console output regardless of silent setting 295 this.logger.error("===============================================================================".yellow); 296 this.logger.error("===========> ".yellow + __('WARNING! ÐApp path length is too long: ').yellow + dappPath.yellow); 297 this.logger.error("===========> ".yellow + __('This is known to cause issues with starting geth, please consider reducing your ÐApp path\'s length to 66 characters or less.').yellow); 298 this.logger.error("===============================================================================".yellow); 299 } 300 }; 301 302 Blockchain.prototype.isClientInstalled = function (callback) { 303 let versionCmd = this.client.determineVersionCommand(); 304 this.runCommand(versionCmd, {}, (err, stdout, stderr) => { 305 if (err || !stdout || stderr.indexOf("not found") >= 0 || stdout.indexOf("not found") >= 0) { 306 return callback(__('Ethereum client bin not found:') + ' ' + this.client.getBinaryPath()); 307 } 308 callback(); 309 }); 310 }; 311 312 Blockchain.prototype.initDevChain = function(callback) { 313 const self = this; 314 const ACCOUNTS_ALREADY_PRESENT = 'accounts_already_present'; 315 // Init the dev chain 316 self.client.initDevChain('.embark/development/datadir', (err) => { 317 if (err) { 318 console.log(err); 319 return callback(err); 320 } 321 322 let needToCreateOtherAccounts = self.config.account && self.config.account.numAccounts; 323 if (!needToCreateOtherAccounts) return callback(); 324 325 // Create other accounts 326 async.waterfall([ 327 function listAccounts(next) { 328 self.runCommand(self.client.listAccountsCommand(), {}, (err, stdout, _stderr) => { 329 if (err || stdout === undefined || stdout.indexOf("Fatal") >= 0) { 330 console.log(__("no accounts found").green); 331 return next(); 332 } 333 // List current addresses 334 self.config.unlockAddressList = self.client.parseListAccountsCommandResultToAddressList(stdout); 335 // Count current addresses and remove the default account from the count (because password can be different) 336 let addressCount = self.client.parseListAccountsCommandResultToAddressCount(stdout); 337 let utilityAddressCount = self.client.parseListAccountsCommandResultToAddressCount(stdout) - 1; 338 utilityAddressCount = (utilityAddressCount > 0 ? utilityAddressCount : 0); 339 let accountsToCreate = self.config.account.numAccounts - utilityAddressCount; 340 if (accountsToCreate > 0) { 341 next(null, accountsToCreate, addressCount === 0); 342 } else { 343 next(ACCOUNTS_ALREADY_PRESENT); 344 } 345 }); 346 }, 347 function newAccounts(accountsToCreate, firstAccount, next) { 348 // At first launch, Geth --dev does not create its own dev account if any other account are present. Parity --dev always does. Make this choerent between the two 349 if (firstAccount && self.client.name === 'geth') accountsToCreate++; 350 var accountNumber = 0; 351 async.whilst( 352 function() { 353 return accountNumber < accountsToCreate; 354 }, 355 function(callback) { 356 accountNumber++; 357 self.runCommand(self.client.newAccountCommand(firstAccount), {}, (err, stdout, _stderr) => { 358 if (err) { 359 return callback(err, accountNumber); 360 } 361 self.config.unlockAddressList.push(self.client.parseNewAccountCommandResultToAddress(stdout)); 362 firstAccount = false; 363 callback(null, accountNumber); 364 }); 365 }, 366 function(err) { 367 next(err); 368 } 369 ); 370 } 371 ], (err) => { 372 if (err && err !== ACCOUNTS_ALREADY_PRESENT) { 373 console.log(err); 374 return callback(err); 375 } 376 callback(); 377 }); 378 }); 379 }; 380 381 Blockchain.prototype.initChainAndGetAddress = function (callback) { 382 const self = this; 383 let address = null; 384 const ALREADY_INITIALIZED = 'already'; 385 386 // ensure datadir exists, bypassing the interactive liabilities prompt. 387 self.datadir = '.embark/' + self.env + '/datadir'; 388 389 async.waterfall([ 390 function makeDir(next) { 391 fs.mkdirp(self.datadir, (err, _result) => { 392 next(err); 393 }); 394 }, 395 function listAccounts(next) { 396 self.runCommand(self.client.listAccountsCommand(), {}, (err, stdout, _stderr) => { 397 if (err || stdout === undefined || stdout.indexOf("Fatal") >= 0) { 398 this.logger.info(__("no accounts found").green); 399 return next(); 400 } 401 let firstAccountFound = self.client.parseListAccountsCommandResultToAddress(stdout); 402 if (firstAccountFound === undefined || firstAccountFound === "") { 403 console.log(__("no accounts found").green); 404 return next(); 405 } 406 this.logger.info(__("already initialized").green); 407 address = firstAccountFound; 408 next(ALREADY_INITIALIZED); 409 }); 410 }, 411 function genesisBlock(next) { 412 //There's no genesis init with Parity. Custom network are set in the chain property at startup 413 if (!self.config.genesisBlock || self.client.name === 'parity') { 414 return next(); 415 } 416 this.logger.info(__("initializing genesis block").green); 417 self.runCommand(self.client.initGenesisCommmand(), {}, (err, _stdout, _stderr) => { 418 next(err); 419 }); 420 }, 421 function newAccount(next) { 422 self.runCommand(self.client.newAccountCommand(), {}, (err, stdout, _stderr) => { 423 if (err) { 424 return next(err); 425 } 426 address = self.client.parseNewAccountCommandResultToAddress(stdout); 427 next(); 428 }); 429 } 430 ], (err) => { 431 if (err === ALREADY_INITIALIZED) { 432 err = null; 433 } 434 callback(err, address); 435 }); 436 }; 437 438 var BlockchainClient = function(userConfig, clientName, env, onReadyCallback, onExitCallback, logger, events, isStandalone) { 439 if ((userConfig === {} || JSON.stringify(userConfig) === '{"enabled":true}') && env !== 'development') { 440 logger.info("===> " + __("warning: running default config on a non-development environment")); 441 } 442 // if client is not set in preferences, default is geth 443 if (!userConfig.ethereumClientName) userConfig.ethereumClientName = 'geth'; 444 // if clientName is set, it overrides preferences 445 if (clientName) userConfig.ethereumClientName = clientName; 446 // Choose correct client instance based on clientName 447 let clientClass; 448 switch (userConfig.ethereumClientName) { 449 case 'geth': 450 clientClass = GethClient; 451 break; 452 453 case 'parity': 454 clientClass = ParityClient; 455 break; 456 return new Blockchain({blockchainConfig, client: GethCommands, env, isDev, onReadyCallback, onExitCallback, logger, events, isStandalone}); 457 default: 458 console.error(__('Unknow client "%s". Please use one of the following: %s', userConfig.ethereumClientName, 'geth, parity')); 459 process.exit(); 460 } 461 userConfig.isDev = (userConfig.isDev || userConfig.default); 462 userConfig.env = env; 463 userConfig.onReadyCallback = onReadyCallback; 464 userConfig.onExitCallback = onExitCallback; 465 return new Blockchain(userConfig, clientClass); 466 }; 467 468 module.exports = BlockchainClient;