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;