From 632b3734a001245dd8c07c8cbf06b9026d2c684f Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Thu, 17 Jan 2019 11:16:54 -0500 Subject: [PATCH] fix: pass specific connection errors instead of leaking --- .gitignore | 1 + package.json | 14 +++-- src/blockchain.js | 68 +++++++++++++++++------- test/blockchain_test.js | 58 +++++++++++++++++++++ test/test.js | 111 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+), 23 deletions(-) create mode 100644 test/blockchain_test.js create mode 100644 test/test.js diff --git a/.gitignore b/.gitignore index ca4e25b..8d34d30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ dist node_modules package +yarn.lock .idea .vscode diff --git a/package.json b/package.json index 4cfb6ed..7afa772 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "clean": "rimraf dist embark.min.js embarkjs-*.tgz package", "http-server": "http-server", "prepare": "npm run build", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "mocha --exit", "webpack": "webpack" }, "repository": { @@ -56,15 +56,19 @@ "async-es": "2.6.1" }, "devDependencies": { - "@babel/cli": "7.0.0-rc.1", - "@babel/core": "7.0.0-rc.1", - "@babel/plugin-transform-runtime": "7.0.0-rc.1", - "@babel/preset-env": "7.0.0-rc.1", + "@babel/cli": "7.2.3", + "@babel/core": "7.2.2", + "@babel/plugin-transform-runtime": "7.2.0", + "@babel/preset-env": "7.2.3", + "@babel/register": "7.0.0", "ajv": "6.5.2", + "chai": "4.2.0", "cross-env": "5.2.0", "http-server": "0.11.1", + "mocha": "5.2.0", "npm-run-all": "4.1.5", "rimraf": "2.6.2", + "web3": "1.0.0-beta.37", "webpack": "4.16.1", "webpack-cli": "3.0.8" }, diff --git a/src/blockchain.js b/src/blockchain.js index 93737d6..e1e4526 100644 --- a/src/blockchain.js +++ b/src/blockchain.js @@ -28,15 +28,23 @@ Blockchain.connect = function(connectionList, opts, doneCb) { const self = this; const checkConnect = (next) => { - this.blockchainConnector.getAccounts((err, _a) => { - if (err) { - this.blockchainConnector.setProvider(null); - } - return next(null, !err); + this.blockchainConnector.getAccounts((error, _a) => { + const provider = self.blockchainConnector.getCurrentProvider(); + const connectionString = provider.host; + + if (error) this.blockchainConnector.setProvider(null); + + return next(null, { + connectionString, + error, + connected: !error + }); }); }; const connectWeb3 = async (next) => { + const connectionString = 'web3://'; + if (window.ethereum) { try { if (Blockchain.autoEnable) { @@ -45,11 +53,19 @@ Blockchain.connect = function(connectionList, opts, doneCb) { } return checkConnect(next); } catch (error) { - return next(null, false); + return next(null, { + connectionString, + error, + connected: false + }); } } - return next(null, false); + return next(null, { + connectionString, + error: new Error("web3 provider not detected"), + connected: false + }); }; const connectWebsocket = (value, next) => { @@ -62,20 +78,24 @@ Blockchain.connect = function(connectionList, opts, doneCb) { checkConnect(next); }; + let connectionErrs = {}; + this.doFirst(function(cb) { - reduce(connectionList, false, function(connected, value, next) { - if (connected) { - return next(null, connected); + reduce(connectionList, false, function(result, connectionString, next) { + if (result.connected) { + return next(null, result); + } else if(result) { + connectionErrs[result.connectionString] = result.error; } - if (value === '$WEB3') { + if (connectionString === '$WEB3') { connectWeb3(next); - } else if (value.indexOf('ws://') >= 0) { - connectWebsocket(value, next); + } else if (connectionString.indexOf('ws://') >= 0) { + connectWebsocket(connectionString, next); } else { - connectHttp(value, next); + connectHttp(connectionString, next); } - }, function(_err, _connected) { + }, function(_err, _connectionErr, _connected) { self.blockchainConnector.getAccounts((err, accounts) => { const currentProv = self.blockchainConnector.getCurrentProvider(); if (opts.warnAboutMetamask && currentProv && currentProv.isMetaMask) { @@ -83,15 +103,18 @@ Blockchain.connect = function(connectionList, opts, doneCb) { // embark will only do this if geth is our client and we are in // dev mode if(opts.blockchainClient === 'geth') { - console.warn("%cNote: There is a known issue with Geth that may cause transactions to get stuck when using Metamask. Please log in to the cockpit (http://localhost:8000/embark?enableRegularTxs=true) to enable a workaround. Once logged in, the workaround will automatically be enabled.", "font-size: 2em"); + console.warn("%cNote: There is a known issue with Geth that may cause transactions to get stuck when using Metamask. Please log in to the cockpit (http://localhost:8000/embark?enableRegularTxs=true) to enable a workaround. Once logged in, the workaround will automatically be enabled.", "font-size: 2em"); } console.warn("%cNote: Embark has detected you are in the development environment and using Metamask, please make sure Metamask is connected to your local node", "font-size: 2em"); } if (accounts) { self.blockchainConnector.setDefaultAccount(accounts[0]); } - cb(err); - doneCb(err); + + const connectionError = new BlockchainConnectionError(connectionErrs); + + cb(connectionErrs); + doneCb(connectionErrs); }); }); }); @@ -263,4 +286,13 @@ Contract.prototype.send = function(value, unit, _options) { Blockchain.Contract = Contract; +class BlockchainConnectionError extends Error { + constructor(connectionErrors) { + super("Could not establish a connection to a node."); + + this.connections = connectionErrors; + this.name = 'BlockchainConnectionError'; + } +} + export default Blockchain; diff --git a/test/blockchain_test.js b/test/blockchain_test.js new file mode 100644 index 0000000..1b58579 --- /dev/null +++ b/test/blockchain_test.js @@ -0,0 +1,58 @@ +/* global before,describe,it */ +const {startRPCMockServer, TestProvider} = require('./test'); + +const async = require('async'); +const {assert} = require('chai'); + +const Blockchain = require('../dist/blockchain'); + +describe('Blockchain', () => { + describe('#connect', () => { + before(() => { + Blockchain.default.registerProvider('web3', TestProvider); + Blockchain.default.setProvider('web3', {}); + }); + + const scenarios = [ + { + description: 'should not keep trying other connections if connected', + servers: [true, true], + visited: [true, false], + error: false + }, + { + description: 'should keep trying other connections if not connected', + servers: [false, true], + visited: [true, true], + error: false + }, + { + description: 'should return error if no connections succeed', + servers: [false, false], + visited: [true, true], + error: true + } + ]; + + scenarios.forEach(scenario => { + it(scenario.description, done => { + async.parallel( + scenario.servers.map(validServer => + (cb) => startRPCMockServer({successful: validServer}, cb) + ), + (_err, servers) => { + const connStrings = servers.map(server => server.connectionString); + Blockchain.default.connect(connStrings, {}, err => { + if(scenario.error) assert(err); + + servers.forEach((server, idx) => { + assert.strictEqual(server.visited, scenario.visited[idx]); + }); + done(); + }); + } + ); + }); + }); + }); +}); diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..462fca4 --- /dev/null +++ b/test/test.js @@ -0,0 +1,111 @@ +const async = require('async'); +const http = require('http'); +const net = require('net'); + +const Web3 = require('web3'); + +const startRPCMockServer = (options = {}, callback) => { + const opts = Object.assign({}, { + successful: true + }, options); + + let port = 0; + let sock = net.createServer(); + let state = { visited: false }; + let server = http.createServer({}, (req, res) => { + state.visited = true; + if(!opts.successful) { + res.statusCode = 500; + return res.end(); + } + + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); + }); + req.on('end', () => { + const request = JSON.parse(body); + const accountsResponse = JSON.stringify({ + "jsonrpc": "2.0", + "id": request.id, + "result": [ + "0x7c67d951b7338a96168f259a16b7ba25e7a30315" + ] + }); + + res.writeHead(200, { + 'Content-Length': Buffer.byteLength(accountsResponse), + 'Content-Type': 'application/json' + }) + res.end(accountsResponse); + }); + }); + + async.waterfall([ + cb => { sock.listen(0, cb) }, + cb => { port = sock.address().port; cb() }, + cb => { sock.close(cb) }, + cb => { server.listen(port, '127.0.0.1', () => cb()); } + ], () => { + state.server = server; + state.connectionString = `http://localhost:${port}`; + callback(null, state); + }); +} + +const TestProvider = {}; + +TestProvider.init = function(_config) { + this.web3 = global.web3 || new Web3(); + global.web3 = global.web3 || this.web3; +}; + +TestProvider.getInstance = function () { + return this.web3; +}; + +TestProvider.getAccounts = function () { + return this.web3.eth.getAccounts(...arguments); +}; + +TestProvider.getNewProvider = function (providerName, ...args) { + return new Web3.providers[providerName](...args); +}; + +TestProvider.setProvider = function (provider) { + return this.web3.setProvider(provider); +}; + +TestProvider.getCurrentProvider = function () { + return this.web3.currentProvider; +}; + +TestProvider.getDefaultAccount = function () { + return this.web3.eth.defaultAccount; +}; + +TestProvider.setDefaultAccount = function (account) { + this.web3.eth.defaultAccount = account; +}; + +TestProvider.newContract = function (options) { + return new this.web3.eth.Contract(options.abi, options.address); +}; + +TestProvider.send = function () { + return this.web3.eth.sendTransaction(...arguments); +}; + +TestProvider.toWei = function () { + return this.web3.toWei(...arguments); +}; + +TestProvider.getNetworkId = function () { + return this.web3.eth.net.getId(); +}; + +module.exports = { + TestProvider, + + startRPCMockServer +}