/ lib / modules / blockchain_process / blockchain.js
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;