/ lib / modules / blockchain_process / gethClient.js
gethClient.js
  1  const async = require('async');
  2  const GethMiner = require('./miner');
  3  const os = require('os');
  4  
  5  const DEFAULTS = {
  6    "BIN": "geth",
  7    "NETWORK_TYPE": "custom",
  8    "NETWORK_ID": 1337,
  9    "RPC_API": ['eth', 'web3', 'net', 'debug'],
 10    "WS_API": ['eth', 'web3', 'net', 'shh', 'debug', 'pubsub'],
 11    "DEV_WS_API": ['eth', 'web3', 'net', 'shh', 'debug', 'pubsub', 'personal'],
 12    "TARGET_GAS_LIMIT": 8000000
 13  };
 14  
 15  // TODO: make all of this async
 16  class GethClient {
 17  
 18    static get DEFAULTS() {
 19      return DEFAULTS;
 20    }
 21  
 22    constructor(options) {
 23      this.config = options && options.hasOwnProperty('config') ? options.config : {};
 24      this.env = options && options.hasOwnProperty('env') ? options.env : 'development';
 25      this.isDev = options && options.hasOwnProperty('isDev') ? options.isDev : (this.env === 'development');
 26      this.name = "geth";
 27      this.prettyName = "Go-Ethereum (https://github.com/ethereum/go-ethereum)";
 28      this.bin = this.config.ethereumClientBin || DEFAULTS.BIN;
 29      this.httpReady = false;
 30      this.wsReady = !this.config.wsRPC;
 31    }
 32  
 33    isReady(data) {
 34      if (data.indexOf('HTTP endpoint opened') > -1) {
 35        this.httpReady = true;
 36      }
 37      if (data.indexOf('WebSocket endpoint opened') > -1) {
 38        this.wsReady = true;
 39      }
 40      return this.httpReady && this.wsReady;
 41    }
 42  
 43    /**
 44     * Check if the client needs some sort of 'keep alive' transactions to avoid freezing by inactivity
 45     * @returns {boolean} if keep alive is needed
 46     */
 47    needKeepAlive() {
 48      // TODO: check version also (geth version < 1.8.15)
 49      if (this.isDev) {
 50        // Trigger regular txs due to a bug in geth (< 1.8.15) and stuck transactions in --dev mode.   
 51        return true;
 52      }
 53      return false;
 54    }
 55  
 56    commonOptions(firstAccount = false) {
 57      let config = this.config;
 58      let cmd = [];
 59  
 60      cmd.push(this.determineNetworkType(config));
 61  
 62      if (config.datadir) {
 63        cmd.push(`--datadir=${config.datadir}`);
 64      }
 65  
 66      if (config.syncMode) {
 67        cmd.push("--syncmode=" + config.syncMode);
 68      }
 69  
 70      // geth in dev mode needs the first account to have a blank password, so we use for convenience the same Parity's devpassword
 71      if (config.account && config.account.password) {
 72        if (firstAccount) cmd.push(`--password=${config.account.devPassword}`);
 73        else cmd.push(`--password=${config.account.password}`);
 74      }
 75  
 76      if (Number.isInteger(config.verbosity) && config.verbosity >= 0 && config.verbosity <= 5) {
 77        cmd.push("--verbosity=" + config.verbosity);
 78      }
 79  
 80      return cmd;
 81    }
 82  
 83    getMiner() {
 84      return new GethMiner({datadir: this.config.datadir});
 85    }
 86  
 87    getBinaryPath() {
 88      return this.bin;
 89    }
 90  
 91    determineVersionCommand() {
 92      return this.bin + " version";
 93    }
 94  
 95    determineNetworkType(config) {
 96      let cmd;
 97      if (config.networkType === 'testnet') {
 98        cmd = "--testnet";
 99      } else if (config.networkType === 'rinkeby') {
100        cmd = "--rinkeby";
101      } else if (config.networkType === 'custom') {
102        cmd = "--networkid=" + config.networkId;
103      }
104      return cmd;
105    }
106  
107    initGenesisCommmand() {
108      let config = this.config;
109      let cmd = this.bin + " " + this.commonOptions().join(' ');
110      if (config.genesisBlock) {
111        cmd += " init \"" + config.genesisBlock + "\" ";
112      }
113      return cmd;
114    }
115  
116    newAccountCommand(firstAccount = false) {
117      if (!(this.config.account && this.config.account.password)) {
118        console.warn(__('Your blockchain is missing a password and creating an account may fail. Please consider updating ').yellow + __('config/blockchain > account > password').cyan + __(' then re-run the command').yellow);
119      }
120      return this.bin + " " + this.commonOptions(firstAccount).join(' ') + " account new ";
121    }
122  
123    parseNewAccountCommandResultToAddress(data = "") {
124      if (data.match(/{(\w+)}/)) return "0x" + data.match(/{(\w+)}/)[1];
125      return "";
126    }
127  
128    listAccountsCommand() {
129      return this.bin + " " + this.commonOptions().join(' ') + " account list ";
130    }
131  
132    parseListAccountsCommandResultToAddress(data = "") {
133      if (data.match(/{(\w+)}/)) return "0x" + data.match(/{(\w+)}/)[1];
134      return "";
135    }
136  
137    parseListAccountsCommandResultToAddressList(data = "") {
138      let list = data.split(os.EOL);
139      list.pop(); // Remove empty value
140      return list.map(el => "0x" + el.match(/{(\w+)}/)[1]);
141    }
142  
143    parseListAccountsCommandResultToAddressCount(data = "") {
144      const count = this.parseListAccountsCommandResultToAddressList(data).length;
145      return (count > 0 ? count : 0);
146    }
147  
148    determineRpcOptions(config) {
149      let cmd = [];
150      cmd.push("--port=" + config.port);
151      cmd.push("--rpc");
152      cmd.push("--rpcport=" + config.rpcPort);
153      cmd.push("--rpcaddr=" + config.rpcHost);
154      if (config.rpcCorsDomain) {
155        if (config.rpcCorsDomain === '*') {
156          console.warn('==================================');
157          console.warn(__('rpcCorsDomain set to *'));
158          console.warn(__('make sure you know what you are doing'));
159          console.warn('==================================');
160        }
161        cmd.push("--rpccorsdomain=" + config.rpcCorsDomain);
162      } else {
163        console.warn('==================================');
164        console.warn(__('warning: cors is not set'));
165        console.warn('==================================');
166      }
167      return cmd;
168    }
169  
170    determineWsOptions(config) {
171      let cmd = [];
172      if (config.wsRPC) {
173        cmd.push("--ws");
174        cmd.push("--wsport=" + config.wsPort);
175        cmd.push("--wsaddr=" + config.wsHost);
176        if (config.wsOrigins) {
177          if (config.wsOrigins === '*') {
178            console.warn('==================================');
179            console.warn(__('wsOrigins set to *'));
180            console.warn(__('make sure you know what you are doing'));
181            console.warn('==================================');
182          }
183          cmd.push("--wsorigins=" + config.wsOrigins);
184        } else {
185          console.warn('==================================');
186          console.warn(__('warning: wsOrigins is not set'));
187          console.warn('==================================');
188        }
189      }
190      return cmd;
191    }
192  
193    initDevChain(datadir, callback) {
194      // No specific configuration needed for the dev chain
195      return callback();
196    }
197  
198    mainCommand(address, done) {
199      let self = this;
200      let config = this.config;
201      let rpc_api = this.config.rpcApi;
202      let ws_api = this.config.wsApi;
203      let args = [];
204      async.series([
205        function commonOptions(callback) {
206          let cmd = self.commonOptions();
207          args = args.concat(cmd);
208          callback(null, cmd);
209        },
210        function rpcOptions(callback) {
211          let cmd = self.determineRpcOptions(self.config);
212          args = args.concat(cmd);
213          callback(null, cmd);
214        },
215        function wsOptions(callback) {
216          let cmd = self.determineWsOptions(self.config);
217          args = args.concat(cmd);
218          callback(null, cmd);
219        },
220        function dontGetPeers(callback) {
221          if (config.nodiscover) {
222            args.push("--nodiscover");
223            return callback(null, "--nodiscover");
224          }
225          callback(null, "");
226        },
227        function vmDebug(callback) {
228          if (config.vmdebug) {
229            args.push("--vmdebug");
230            return callback(null, "--vmdebug");
231          }
232          callback(null, "");
233        },
234        function maxPeers(callback) {
235          let cmd = "--maxpeers=" + config.maxpeers;
236          args.push(cmd);
237          callback(null, cmd);
238        },
239        function mining(callback) {
240          if (config.mineWhenNeeded || config.mine) {
241            args.push("--mine");
242            return callback(null, "--mine");
243          }
244          callback("");
245        },
246        function bootnodes(callback) {
247          if (config.bootnodes && config.bootnodes !== "" && config.bootnodes !== []) {
248            args.push("--bootnodes=" + config.bootnodes);
249            return callback(null, "--bootnodes=" + config.bootnodes);
250          }
251          callback("");
252        },
253        function whisper(callback) {
254          if (config.whisper) {
255            rpc_api.push('shh');
256            if (ws_api.indexOf('shh') === -1) {
257              ws_api.push('shh');
258            }
259            args.push("--shh");
260            return callback(null, "--shh ");
261          }
262          callback("");
263        },
264        function rpcApi(callback) {
265          args.push('--rpcapi=' + rpc_api.join(','));
266          callback(null, '--rpcapi=' + rpc_api.join(','));
267        },
268        function wsApi(callback) {
269          args.push('--wsapi=' + ws_api.join(','));
270          callback(null, '--wsapi=' + ws_api.join(','));
271        },
272        function accountToUnlock(callback) {
273          if (self.isDev && self.config.unlockAddressList) {
274            // The first address is the dev account, that is automatically unlocked by the client using blank password
275            args.push("--unlock=" + self.config.unlockAddressList.slice(1));
276            return callback(null, "--unlock=" + self.config.unlockAddressList.slice(1));
277          }
278          let accountAddress = "";
279          if (config.account && config.account.address) {
280            accountAddress = config.account.address;
281          } else {
282            accountAddress = address;
283          }
284          if (accountAddress) {
285            if(!(self.config && self.config.account && self.config.account.password)){
286              console.warn(__("\n===== Password needed =====\nPassword for account {{account}} not found. Unlocking this account may fail. Please ensure a password is specified in config/blockchain.js > {{env}} > account > password.\n", {account: address, env: self.env}));
287            }
288            args.push("--unlock=" + accountAddress);
289            return callback(null, "--unlock=" + accountAddress);
290          }
291          callback(null, "");
292        },
293        function gasLimit(callback) {
294          if (config.targetGasLimit) {
295            args.push("--miner.gastarget=" + config.targetGasLimit);
296            return callback(null, "--miner.gastarget=" + config.targetGasLimit);
297          }
298          callback(null, "");
299        },
300        function isDev(callback) {
301          if (self.isDev) {
302            args.push('--dev');
303            return callback(null, '--dev');
304          }
305          callback(null, '');
306        }
307      ], function(err) {
308        if (err) {
309          throw new Error(err.message);
310        }
311        return done(self.bin, args);
312      });
313    }
314  }
315  
316  module.exports = GethClient;