index.js
  1  const Web3 = require('web3');
  2  const async = require('async');
  3  const Provider = require('./provider.js');
  4  const utils = require('../../utils/utils');
  5  const constants = require('../../constants');
  6  const embarkJsUtils = require('embarkjs').Utils;
  7  
  8  const WEB3_READY = 'blockchain:ready';
  9  
 10  const BLOCK_LIMIT = 100;
 11  
 12  // TODO: consider another name, this is the blockchain connector
 13  class BlockchainConnector {
 14    constructor(embark, options) {
 15      const self = this;
 16      this.plugins = options.plugins;
 17      this.logger = embark.logger;
 18      this.events = embark.events;
 19      this.config = embark.config;
 20      this.web3 = options.web3;
 21      this.isDev = options.isDev;
 22      this.web3Endpoint = '';
 23      this.isWeb3Ready = false;
 24      this.wait = options.wait;
 25      this.contractsSubscriptions = [];
 26      this.contractsEvents = [];
 27  
 28      self.events.setCommandHandler("blockchain:web3:isReady", (cb) => {
 29        cb(self.isWeb3Ready);
 30      });
 31  
 32      self.events.setCommandHandler("blockchain:object", (cb) => {
 33        cb(self);
 34      });
 35  
 36      self.events.setCommandHandler("blockchain:getTransaction", (txHash, cb) => {
 37        self.getTransactionByHash(txHash, cb);
 38      });
 39  
 40      embark.registerActionForEvent("contracts:deploy:afterAll", this.subscribeToContractEvents.bind(this));
 41  
 42      if (!this.web3) {
 43        this.initWeb3();
 44      } else {
 45        this.isWeb3Ready = true;
 46      }
 47  
 48      this.registerServiceCheck();
 49      this.registerRequests();
 50      this.registerAPIRequests();
 51      this.registerWeb3Object();
 52      this.registerEvents();
 53      this.subscribeToPendingTransactions();
 54    }
 55  
 56    initWeb3(cb) {
 57      if (!cb) {
 58        cb = function(){};
 59      }
 60      if (this.isWeb3Ready) {
 61        this.events.emit(WEB3_READY);
 62        return cb();
 63      }
 64  
 65      const self = this;
 66      this.web3 = new Web3();
 67  
 68      if (self.wait) {
 69        self.wait = false;
 70        return cb();
 71      }
 72  
 73      let {type, host, port, accounts, protocol, coverage} = this.config.contractsConfig.deployment;
 74      if (!protocol) {
 75        protocol = (type === "rpc") ? 'http' : 'ws';
 76      }
 77  
 78      if (!BlockchainConnector.ACCEPTED_TYPES.includes(type)) {
 79        this.logger.error(__("contracts config error: unknown deployment type %s", type));
 80        this.logger.error(__("Accepted types are: %s", BlockchainConnector.ACCEPTED_TYPES.join(', ')));
 81      }
 82  
 83      if (type === 'vm') {
 84        const sim = self._getSimulator();
 85        self.provider = sim.provider(self.config.contractsConfig.deployment);
 86  
 87        if (coverage) {
 88          // Here we patch the sendAsync method on the provider. The goal behind this is to force pure/constant/view calls to become
 89          // transactions, so that we can pull in execution traces and account for those executions in code coverage.
 90          //
 91          // Instead of a simple call, here's what happens:
 92          //
 93          // 1) A transaction is sent with the same payload, and a pre-defined gas price;
 94          // 2) We wait for the transaction to be mined by asking for the receipt;
 95          // 3) Once we get the receipt back, we dispatch the real call and pass the original callback;
 96          //
 97          // This will still allow tests to get the return value from the call and run contracts unmodified.
 98          self.provider.realSendAsync = self.provider.sendAsync.bind(self.provider);
 99          self.provider.sendAsync = function(payload, cb) {
100            if(payload.method !== 'eth_call') {
101              return self.provider.realSendAsync(payload, cb);
102            }
103            self.events.request('reporter:toggleGasListener');
104            let newParams = Object.assign({}, payload.params[0], {gasPrice: '0x77359400'});
105            let newPayload = {
106              id: payload.id + 1,
107              method: 'eth_sendTransaction',
108              params: [newParams],
109              jsonrpc: payload.jsonrpc
110            };
111  
112            self.provider.realSendAsync(newPayload, (_err, response) => {
113              let txHash = response.result;
114              self.web3.eth.getTransactionReceipt(txHash, (_err, _res) => {
115                self.events.request('reporter:toggleGasListener');
116                self.provider.realSendAsync(payload, cb);
117              });
118            });
119          };
120        }
121  
122        self.web3.setProvider(self.provider);
123        self._emitWeb3Ready();
124        return cb();
125      }
126  
127      protocol = (type === "rpc") ? protocol : 'ws';
128  
129      this.web3Endpoint = utils.buildUrl(protocol, host, port);
130  
131      const providerOptions = {
132        web3: this.web3,
133        accountsConfig: accounts,
134        blockchainConfig: this.config.blockchainConfig,
135        logger: this.logger,
136        isDev: this.isDev,
137        type: type,
138        web3Endpoint: self.web3Endpoint
139      };
140      this.provider = new Provider(providerOptions);
141  
142      self.events.request("processes:launch", "blockchain", () => {
143        self.provider.startWeb3Provider(() => {
144          this.getNetworkId()
145            .then(id => {
146              let networkId = self.config.blockchainConfig.networkId;
147              if (!networkId && constants.blockchain.networkIds[self.config.blockchainConfig.networkType]) {
148                networkId = constants.blockchain.networkIds[self.config.blockchainConfig.networkType];
149              }
150              if (networkId && id.toString() !== networkId.toString()) {
151                self.logger.warn(__('Connected to a blockchain node on network {{realId}} while your config specifies {{configId}}', {realId: id, configId: networkId}));
152                self.logger.warn(__('Make sure you started the right blockchain node'));
153              }
154            })
155            .catch(console.error);
156          self.provider.fundAccounts(() => {
157            self._emitWeb3Ready();
158            cb();
159          });
160        });
161      });
162    }
163  
164    _emitWeb3Ready() {
165      this.isWeb3Ready = true;
166      this.events.emit(WEB3_READY);
167      this.registerWeb3Object();
168      this.subscribeToPendingTransactions();
169    }
170  
171    _getSimulator() {
172      try {
173        return require('ganache-cli');
174      } catch (e) {
175        const moreInfo = 'For more information see https://github.com/trufflesuite/ganache-cli';
176        if (e.code === 'MODULE_NOT_FOUND') {
177          this.logger.error(__('Simulator not found; Please install it with "%s"', 'npm install ganache-cli --save'));
178          this.logger.error(moreInfo);
179          throw e;
180        }
181        this.logger.error("==============");
182        this.logger.error(__("Tried to load Ganache CLI (testrpc), but an error occurred. This is a problem with Ganache CLI"));
183        this.logger.error(moreInfo);
184        this.logger.error("==============");
185        throw e;
186      }
187    }
188  
189    registerEvents() {
190      const self = this;
191      self.events.on('check:wentOffline:Ethereum', () => {
192        self.logger.warn('Ethereum went offline: stopping web3 provider...');
193        self.provider.stop();
194  
195        // once the node goes back online, we can restart the provider
196        self.events.once('check:backOnline:Ethereum', () => {
197          self.logger.warn('Ethereum back online: starting web3 provider...');
198          self.provider.startWeb3Provider(() => {
199            self.logger.warn('web3 provider restarted after ethereum node came back online');
200          });
201        });
202      });
203    }
204  
205    onReady(callback) {
206      if (this.isWeb3Ready) {
207        return callback();
208      }
209  
210      this.events.once(WEB3_READY, () => {
211        callback();
212      });
213    }
214  
215    registerServiceCheck() {
216      const self = this;
217      const NO_NODE = 'noNode';
218  
219      this.events.request("services:register", 'Ethereum', function(cb) {
220        async.waterfall([
221          function checkNodeConnection(next) {
222            if (!self.provider || !self.provider.connected()) {
223              return next(NO_NODE, {name: "No Blockchain node found", status: 'off'});
224            }
225            next();
226          },
227          function checkVersion(next) {
228            // TODO: web3_clientVersion method is currently not implemented in web3.js 1.0
229            self.web3._requestManager.send({method: 'web3_clientVersion', params: []}, (err, version) => {
230              if (err) {
231                self.isWeb3Ready = false;
232                return next(null, {name: "Ethereum node not found", status: 'off'});
233              }
234              if (version.indexOf("/") < 0) {
235                self.events.emit(WEB3_READY);
236                self.isWeb3Ready = true;
237                return next(null, {name: version, status: 'on'});
238              }
239              let nodeName = version.split("/")[0];
240              let versionNumber = version.split("/")[1].split("-")[0];
241              let name = nodeName + " " + versionNumber + " (Ethereum)";
242  
243              self.events.emit(WEB3_READY);
244              self.isWeb3Ready = true;
245              return next(null, {name: name, status: 'on'});
246            });
247          }
248        ], (err, statusObj) => {
249          if (err && err !== NO_NODE) {
250            return cb(err);
251          }
252          cb(statusObj);
253        });
254      }, 5000, 'off');
255    }
256  
257    registerRequests() {
258      const self = this;
259  
260      this.events.setCommandHandler("blockchain:reset", function(cb) {
261        self.isWeb3Ready = false;
262        self.initWeb3((err) => {
263          if (err) {
264            return cb(err);
265          }
266          self.events.emit('blockchain:reseted');
267          cb();
268        });
269      });
270  
271      this.events.setCommandHandler("blockchain:get", function(cb) {
272        cb(self.web3);
273      });
274  
275      this.events.setCommandHandler("blockchain:defaultAccount:get", function(cb) {
276        cb(self.defaultAccount());
277      });
278  
279      this.events.setCommandHandler("blockchain:defaultAccount:set", function(account, cb) {
280        self.setDefaultAccount(account);
281        cb();
282      });
283  
284      this.events.setCommandHandler("blockchain:getAccounts", function(cb) {
285        self.getAccounts(cb);
286      });
287  
288      this.events.setCommandHandler("blockchain:getBalance", function(address, cb) {
289        self.getBalance(address, cb);
290      });
291  
292      this.events.setCommandHandler("blockchain:block:byNumber", function(blockNumber, cb) {
293        self.getBlock(blockNumber, cb);
294      });
295  
296      this.events.setCommandHandler("blockchain:block:byHash", function(blockHash, cb) {
297        self.getBlock(blockHash, cb);
298      });
299  
300      this.events.setCommandHandler("blockchain:gasPrice", function(cb) {
301        self.getGasPrice(cb);
302      });
303  
304      this.events.setCommandHandler("blockchain:networkId", function(cb) {
305        self.getNetworkId().then(cb);
306      });
307  
308      this.events.setCommandHandler("blockchain:contract:create", function(params, cb) {
309        cb(self.ContractObject(params));
310      });
311    }
312  
313    registerAPIRequests() {
314      const self = this;
315  
316      let plugin = self.plugins.createPlugin('blockchain', {});
317      plugin.registerAPICall(
318        'get',
319        '/embark-api/blockchain/accounts',
320        (req, res) => {
321          self.getAccountsWithTransactionCount(res.send.bind(res));
322        }
323      );
324  
325      plugin.registerAPICall(
326        'get',
327        '/embark-api/blockchain/accounts/:address',
328        (req, res) => {
329          self.getAccount(req.params.address, res.send.bind(res));
330        }
331      );
332  
333      plugin.registerAPICall(
334        'get',
335        '/embark-api/blockchain/blocks',
336        (req, res) => {
337          let from = parseInt(req.query.from, 10);
338          let limit = req.query.limit || 10;
339          self.getBlocks(from, limit, false, res.send.bind(res));
340        }
341      );
342  
343      plugin.registerAPICall(
344        'get',
345        '/embark-api/blockchain/blocks/:blockNumber',
346        (req, res) => {
347          self.getBlock(req.params.blockNumber, (err, block) => {
348            if (err) {
349              self.logger.error(err);
350            }
351            res.send(block);
352          });
353        }
354      );
355  
356      plugin.registerAPICall(
357        'get',
358        '/embark-api/blockchain/transactions',
359        (req, res) => {
360          let blockFrom = parseInt(req.query.blockFrom, 10);
361          let blockLimit = req.query.blockLimit || 10;
362          self.getTransactions(blockFrom, blockLimit, res.send.bind(res));
363        }
364      );
365  
366      plugin.registerAPICall(
367        'get',
368        '/embark-api/blockchain/transactions/:hash',
369        (req, res) => {
370          self.getTransaction(req.params.hash, (err, transaction) => {
371            if (err) {
372              self.logger.error(err);
373            }
374            res.send(transaction);
375          });
376        }
377      );
378  
379      plugin.registerAPICall(
380        'ws',
381        '/embark-api/blockchain/blockHeader',
382        (ws) => {
383          self.events.on('block:header', (block) => {
384            ws.send(JSON.stringify({block: block}), () => {});
385          });
386        }
387      );
388  
389      plugin.registerAPICall(
390        'ws',
391        '/embark-api/blockchain/contracts/event',
392        (ws) => {
393          this.events.on('blockchain:contracts:event', (data) => {
394            ws.send(JSON.stringify(data), () => {});
395          });
396        }
397      );
398  
399      plugin.registerAPICall(
400        'get',
401        '/embark-api/blockchain/contracts/events',
402        (_req, res) => {
403          res.send(JSON.stringify(this.contractsEvents));
404        }
405      );
406  
407      plugin.registerAPICall(
408        'post',
409        '/embark-api/messages/sign',
410        (req, res) => {
411          const signer = req.body.address;
412          const message = req.body.message;
413          this.web3.eth.personal.sign(message, signer).then(signature => {
414            res.send({signer, signature, message})
415          }).catch(e => res.send({ error: e.message }))
416        }
417      );
418  
419      plugin.registerAPICall(
420        'post',
421        '/embark-api/messages/verify',
422        (req, res) => {
423          let signature;
424          try {
425            signature = JSON.parse(req.body.message);
426          } catch(e) {
427            return res.send({ error: e.message });
428          }
429  
430          this.web3.eth.personal.ecRecover(signature.message, signature.signature)
431            .then(address => res.send({address}))
432            .catch(e => res.send({ error: e.message }));
433        }
434      );
435    }
436  
437    getAccountsWithTransactionCount(callback) {
438      let self = this;
439      self.getAccounts((err, addresses) => {
440        let accounts = [];
441        async.eachOf(addresses, (address, index, eachCb) => {
442          let account = {address, index};
443          async.waterfall([
444            function(callback) {
445              self.getTransactionCount(address, (err, count) => {
446                if (err) {
447                  self.logger.error(err);
448                  account.transactionCount = 0;
449                } else {
450                  account.transactionCount = count;
451                }
452                callback(null, account);
453              });
454            },
455            function(account, callback) {
456              self.getBalance(address, (err, balance) => {
457                if (err) {
458                  self.logger.error(err);
459                  account.balance = 0;
460                } else {
461                  account.balance = self.web3.utils.fromWei(balance);
462                }
463                callback(null, account);
464              });
465            }
466          ], function(_err, account) {
467            accounts.push(account);
468            eachCb();
469          });
470        }, function() {
471          callback(accounts);
472        });
473      });
474    }
475  
476    getAccount(address, callback) {
477      let self = this;
478      async.waterfall([
479        function(next) {
480          self.getAccountsWithTransactionCount((accounts) => {
481            let account = accounts.find((a) => a.address === address);
482            if (!account) {
483              return next("No account found with this address");
484            }
485            next(null, account);
486          });
487        },
488        function(account, next) {
489          self.getBlockNumber((err, blockNumber) => {
490            if (err) {
491              self.logger.error(err);
492              next(err);
493            } else {
494              next(null, blockNumber, account);
495            }
496          });
497        },
498        function(blockNumber, account, next) {
499          self.getTransactions(blockNumber - BLOCK_LIMIT, BLOCK_LIMIT, (transactions) => {
500            account.transactions = transactions.filter((transaction) => transaction.from === address);
501            next(null, account);
502          });
503        }
504      ], function(err, result) {
505        if (err) {
506          callback();
507        }
508        callback(result);
509      });
510    }
511  
512    getTransactions(blockFrom, blockLimit, callback) {
513      this.getBlocks(blockFrom, blockLimit, true, (blocks) => {
514        let transactions = blocks.reduce((acc, block) => {
515          if (!block || !block.transactions) {
516            return acc;
517          }
518          return acc.concat(block.transactions);
519        }, []);
520        callback(transactions);
521      });
522    }
523  
524    getBlocks(from, limit, returnTransactionObjects, callback) {
525      let self = this;
526      let blocks = [];
527      async.waterfall([
528        function(next) {
529          if (!isNaN(from)) {
530            return next();
531          }
532          self.getBlockNumber((err, blockNumber) => {
533            if (err) {
534              self.logger.error(err);
535              from = 0;
536            } else {
537              from = blockNumber;
538            }
539            next();
540          });
541        },
542        function(next) {
543          async.times(limit, function(n, eachCb) {
544            self.web3.eth.getBlock(from - n, returnTransactionObjects, function(err, block) {
545              if (err && err.message) {
546                // FIXME Returns an error because we are too low
547                return eachCb();
548              }
549              blocks.push(block);
550              eachCb();
551            });
552          }, next);
553        }
554      ], function() {
555        callback(blocks);
556      });
557    }
558  
559    defaultAccount() {
560      return this.web3.eth.defaultAccount;
561    }
562  
563    getBlockNumber(cb) {
564      return this.web3.eth.getBlockNumber(cb);
565    }
566  
567    setDefaultAccount(account) {
568      this.web3.eth.defaultAccount = account;
569    }
570  
571    getAccounts(cb) {
572      this.web3.eth.getAccounts(cb);
573    }
574  
575    getTransactionCount(address, cb) {
576      this.web3.eth.getTransactionCount(address, cb);
577    }
578  
579    getBalance(address, cb) {
580      this.web3.eth.getBalance(address, cb);
581    }
582  
583    getCode(address, cb) {
584      this.web3.eth.getCode(address, cb);
585    }
586  
587    getBlock(blockNumber, cb) {
588      this.web3.eth.getBlock(blockNumber, true, cb);
589    }
590  
591    getTransactionByHash(hash, cb) {
592      this.web3.eth.getTransaction(hash, cb);
593    }
594  
595    getGasPrice(cb) {
596      const self = this;
597      this.onReady(() => {
598        self.web3.eth.getGasPrice(cb);
599      });
600    }
601  
602    getNetworkId() {
603      return this.web3.eth.net.getId();
604    }
605  
606    //TODO: fix me, why is this gasPrice??
607    getTransaction(hash, cb) {
608      const self = this;
609      this.onReady(() => {
610        self.web3.eth.getGasPrice(cb);
611      });
612    }
613  
614    ContractObject(params) {
615      return new this.web3.eth.Contract(params.abi, params.address);
616    }
617  
618    deployContractObject(contractObject, params) {
619      return contractObject.deploy({arguments: params.arguments, data: params.data});
620    }
621  
622    estimateDeployContractGas(deployObject, cb) {
623      return deployObject.estimateGas().then((gasValue) => {
624        cb(null, gasValue);
625      }).catch(cb);
626    }
627  
628    deployContractFromObject(deployContractObject, params, cb) {
629      embarkJsUtils.secureSend(this.web3, deployContractObject, {
630        from: params.from, gas: params.gas, gasPrice: params.gasPrice
631      }, true, cb);
632    }
633  
634    determineDefaultAccount(cb) {
635      const self = this;
636      self.getAccounts(function(err, accounts) {
637        if (err) {
638          self.logger.error(err);
639          return cb(new Error(err));
640        }
641        let accountConfig = self.config.blockchainConfig.account;
642        let selectedAccount = accountConfig && accountConfig.address;
643        const defaultAccount = selectedAccount || accounts[0];
644        self.setDefaultAccount(defaultAccount);
645        cb(null, defaultAccount);
646      });
647    }
648  
649    registerWeb3Object() {
650      // doesn't feel quite right, should be a cmd or plugin method
651      // can just be a command without a callback
652      this.events.emit("runcode:register", "web3", this.web3, false);
653    }
654  
655    subscribeToPendingTransactions() {
656      const self = this;
657      this.onReady(() => {
658        if (self.logsSubscription) {
659          self.logsSubscription.unsubscribe();
660        }
661        self.logsSubscription = self.web3.eth
662          .subscribe('newBlockHeaders', () => {})
663          .on("data", function (blockHeader) {
664            self.events.emit('block:header', blockHeader);
665          });
666  
667        if (self.pendingSubscription) {
668          self.pendingSubscription.unsubscribe();
669        }
670        self.pendingSubscription = self.web3.eth
671          .subscribe('pendingTransactions', function(error, transaction){
672            if (!error) {
673              self.events.emit('block:pending:transaction', transaction);
674            }
675          });
676      });
677    }
678  
679    subscribeToContractEvents(callback) {
680      this.contractsSubscriptions.forEach((eventEmitter) => {
681        eventEmitter.unsubscribe();
682      });
683      this.contractsSubscriptions = [];
684      this.events.request("contracts:list", (_err, contractsList) => {
685        contractsList.forEach(contractObject => {
686          if (!contractObject.address){
687            return;
688          }
689  
690          const contract = this.ContractObject({abi: contractObject.abiDefinition, address: contractObject.address});
691          const eventEmitter = contract.events.allEvents();
692          this.contractsSubscriptions.push(eventEmitter);
693          eventEmitter.on('data', (data) => {
694            const dataWithName = Object.assign(data, {name: contractObject.className});
695            this.contractsEvents.push(dataWithName);
696            this.events.emit('blockchain:contracts:event', dataWithName);
697          });
698        });
699        callback();
700      });
701    }
702  }
703  
704  BlockchainConnector.ACCEPTED_TYPES = ['rpc', 'ws', 'vm'];
705  module.exports = BlockchainConnector;