diff --git a/lib/cmds/blockchain/blockchain.js b/lib/cmds/blockchain/blockchain.js index 9be5a974c..c556ac407 100644 --- a/lib/cmds/blockchain/blockchain.js +++ b/lib/cmds/blockchain/blockchain.js @@ -170,9 +170,8 @@ Blockchain.prototype.run = function() { if (self.onReadyCallback && !self.readyCalled && data.indexOf('WebSocket endpoint opened') > -1) { if (self.isDev) { self.createFundAndUnlockAccounts((err) => { + // TODO: this is never called! if(err) console.error('Error creating, unlocking, and funding accounts', err); - //self.readyCalled = true; - //self.onReadyCallback(); }); } self.readyCalled = true; @@ -189,9 +188,10 @@ Blockchain.prototype.run = function() { }; Blockchain.prototype.createFundAndUnlockAccounts = function(cb) { - let devFunds = new DevFunds(this.config); - devFunds.createFundAndUnlockAccounts((err) => { - cb(err); + DevFunds.new({blockchainConfig: this.config}).then(devFunds => { + devFunds.createFundAndUnlockAccounts((err) => { + cb(err); + }); }); }; diff --git a/lib/cmds/blockchain/dev_funds.js b/lib/cmds/blockchain/dev_funds.js index e26c97e7d..13ab4348c 100644 --- a/lib/cmds/blockchain/dev_funds.js +++ b/lib/cmds/blockchain/dev_funds.js @@ -4,22 +4,32 @@ const {getWeiBalanceFromString, buildUrl} = require('../../utils/utils.js'); const {readFileSync, dappPath} = require('../../core/fs'); class DevFunds { - constructor(blockchainConfig) { - // TODO: temporary to avoid nasty suprises, should be removed - try { - this.web3 = null; - this.blockchainConfig = blockchainConfig; - this.accounts = []; - this.numAccounts = this.blockchainConfig.account.numAccounts || 0; - this.password = readFileSync(dappPath(blockchainConfig.account.password), 'utf8').replace('\n', ''); - this.web3 = new Web3(); - this.networkId = null; - this.balance = Web3.utils.toWei("1", "ether"); - if (this.blockchainConfig.account.balance) { - this.balance = getWeiBalanceFromString(this.blockchainConfig.account.balance, this.web3); - } - } catch(_err) { - // empty for now + constructor(options) { + this.blockchainConfig = options.blockchainConfig; + this.accounts = []; + this.numAccounts = this.blockchainConfig.account.numAccounts || 0; + this.password = this.blockchainConfig.account.password ? readFileSync(dappPath(this.blockchainConfig.account.password), 'utf8').replace('\n', '') : 'dev_password'; + this.networkId = null; + this.balance = Web3.utils.toWei("1", "ether"); + this.provider = options.provider || new Web3.providers.WebsocketProvider(buildUrl('ws', this.blockchainConfig.wsHost, this.blockchainConfig.wsPort), {headers: {Origin: "http://localhost:8000"}}); + this.web3 = new Web3(this.provider); + if (this.blockchainConfig.account.balance) { + this.balance = getWeiBalanceFromString(this.blockchainConfig.account.balance, this.web3); + } + this.logger = options.logger || console; + } + + static async new(options){ + const df = new DevFunds(options); + await df._init(); + return df; + } + + async _init () { + const accounts = await this.web3.eth.getAccounts(); + this.web3.eth.defaultAccount = accounts[0]; + if (accounts.length > 1) { + this.accounts = accounts.slice(1); } } @@ -31,7 +41,7 @@ class DevFunds { } // trigger regular txs due to a bug in geth and stuck transactions in --dev mode - regularTxs(cb) { + _regularTxs(cb) { const self = this; self.web3.eth.net.getId().then((networkId) => { self.networkId = networkId; @@ -39,29 +49,16 @@ class DevFunds { return; } - setInterval(function() { self._sendTx(); }, 1500); + setInterval(function () { self._sendTx(); }, 1500); if (cb) { cb(); } }); } - regularUnlocks() { + _regularUnlocks() { const self = this; - setInterval(function() { self.unlockAccounts(self.password, () => {}); }, 20000); - } - - connectToNode(cb) { - - this.web3.setProvider(new Web3.providers.WebsocketProvider(buildUrl('ws', this.blockchainConfig.wsHost, this.blockchainConfig.wsPort), {headers: {Origin: "http://localhost:8000"}})); - - this.web3.eth.getAccounts().then((accounts) => { - this.web3.eth.defaultAccount = accounts[0]; - if (accounts.length > 1) { - this.accounts = accounts.slice(1); - } - cb(); - }); + setInterval(function () { self.unlockAccounts(self.password, () => { }); }, 20000); } createAccounts(numAccounts, password, cb) { @@ -86,17 +83,15 @@ class DevFunds { } fundAccounts(balance, cb) { - async.each(this.accounts, (account, next) => { this.web3.eth.getBalance(account).then(currBalance => { const remainingBalance = balance - currBalance; if (remainingBalance <= 0) return next(); - this.web3.eth.sendTransaction({to: account, value: remainingBalance}).then((_result) => { - next(); - }).catch(next); - }, cb); - }); + this.web3.eth.sendTransaction({to: account, value: remainingBalance}).catch(next); + next(); // don't wait for the tx receipt as it never comes! + }).catch(cb); + }, cb); } createFundAndUnlockAccounts(cb) { @@ -104,9 +99,6 @@ class DevFunds { return cb(); } async.waterfall([ - (next) => { - this.connectToNode(next); - }, (next) => { this.createAccounts(this.numAccounts, this.password, next); }, @@ -114,8 +106,8 @@ class DevFunds { this.unlockAccounts(this.password, next); }, (next) => { - this.regularTxs(); - this.regularUnlocks(); + this._regularTxs(); + this._regularUnlocks(); this.fundAccounts(this.balance, next); } ], cb); diff --git a/test/devFunds.js b/test/devFunds.js new file mode 100644 index 000000000..68b089eb9 --- /dev/null +++ b/test/devFunds.js @@ -0,0 +1,150 @@ +/*global describe, it, before*/ +const assert = require('assert'); +let TestLogger = require('../lib/tests/test_logger.js'); +const Web3 = require('web3'); +const i18n = require('../lib/i18n/i18n.js'); +const constants = require('../lib/constants.json'); +const DevFunds = require('../lib/cmds/blockchain/dev_funds'); +const async = require('async'); +const FakeIpcProvider = require('./helpers/fakeIpcProvider'); +const utils = require('../lib/utils/utils'); +i18n.setOrDetectLocale('en'); + +describe('embark.DevFunds', function () { + let config = { + networkType: 'livenet', + genesisBlock: 'foo/bar/genesis.json', + geth_bin: 'geth', + datadir: '/foo/datadir/', + mineWhenNeeded: true, + rpcHost: 'someserver', + rpcPort: 12345, + rpcApi: ['eth', 'web3', 'net', 'debug'], + rpcCorsDomain: true, + networkId: 1, + port: 123456, + nodiscover: true, + maxpeers: 25, + mine: true, + vmdebug: false, + whisper: false, + account: { + password: './test/test1/password', + numAccounts: 3, + balance: "5 ether" + }, + bootnodes: "", + wsApi: ["eth", "web3", "net", "shh", "debug"], + wsHost: "localhost", + wsOrigins: false, + wsPort: 8546, + wsRPC: true, + targetGasLimit: false, + syncMode: undefined, + syncmode: undefined, + verbosity: undefined, + proxy: true + }; + + if (config.proxy) { + config.wsPort += constants.blockchain.servicePortOnProxy; + config.rpcPort += constants.blockchain.servicePortOnProxy; + } + + describe('#create, fund, and unlock accounts', function () { + let provider = new FakeIpcProvider(); + const web3 = new Web3(provider); + let devFunds; + + before(async () => { + provider.injectResult(['0x47d33b27bb249a2dbab4c0612bf9caf4c1950855']); // getAccounts: return --dev account + devFunds = await DevFunds.new({blockchainConfig: config, provider: provider, logger: new TestLogger({})}); + }); + + it('should create correct number of accounts', function (done) { + provider.injectResult('0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae'); // createAccount #1 + provider.injectResult('0x22f4d0a3c12e86b4b5f39b213f7e19d048276dab'); // createAccount #2 + + devFunds.createAccounts(config.account.numAccounts, 'test_password', (err) => { + assert.equal(err, null); + + // TODO: make FakeIpcProvider smart enough to keep track of created accounts + provider.injectResult(['0x47d33b27bb249a2dbab4c0612bf9caf4c1950855', '0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae', '0x22f4d0a3c12e86b4b5f39b213f7e19d048276dab']); + + web3.eth.getAccounts().then((accts) => { + assert.equal(accts.length, config.account.numAccounts); + assert.strictEqual(accts[0], '0x47D33b27Bb249a2DBab4C0612BF9CaF4C1950855'); // --dev acct + assert.strictEqual(accts[1], '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe'); // created acct #1 + assert.strictEqual(accts[2], '0x22F4d0A3C12E86b4b5F39B213f7e19D048276DAb'); // created acct #2 + done(); + }); + }); + }); + + it('should unlock accounts', function (done) { + if (devFunds.accounts.length === 0) { + assert.equal(true, true, "no accounts to unlock"); + return done(); + } + + devFunds.accounts.forEach(_acct => { + provider.injectResult(true); // account unlock result + }); + + devFunds.unlockAccounts(devFunds.password, (errUnlock) => { + assert.equal(errUnlock, null); + done(); + }); + }); + + it('should fund accounts', function (done) { + + if (devFunds.accounts.length === 0) { + assert.equal(true, true, "no accounts to fund"); + return done(); + } + devFunds.accounts.forEach(_acct => { + provider.injectResult('1234567890'); // account balance + // provider.injectResult('0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe'); // send tx response + }); + + devFunds.fundAccounts(devFunds.balance, (errFundAccounts) => { + + assert.equal(errFundAccounts, null); + + // inject response for web3.eth.getAccounts + // TODO: make FakeIpcProvider smart enough to keep track of created accounts + provider.injectResult(['0x47d33b27bb249a2dbab4c0612bf9caf4c1950855', '0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae', '0x22f4d0a3c12e86b4b5f39b213f7e19d048276dab']); + + web3.eth.getAccounts().then((accts) => { + + const weiFromConfig = utils.getWeiBalanceFromString(config.account.balance, web3); + + async.each(accts, (acct, cb) => { + + // inject response for web3.eth.getBalance. + // essentially, this will always return the amount we specified + // in the config. + // this is dodgy. really, we should be letting the FakeIpcProvider + // at this point tell us how many wei we have per account (as it would + // in a real node), but the FakeIpcProvider is not smart enough... yet. + // TODO: make FakeIpcProvider smart enough to keep track of balances + provider.injectResult(web3.utils.numberToHex(weiFromConfig)); + + web3.eth.getBalance(acct).then((wei) => { + assert.equal(wei, weiFromConfig); + cb(); + }).catch(cb); + + }, function (errAcctsBalance) { + if (errAcctsBalance) throw errAcctsBalance; + done(); + }); + }).catch((errGetAccts) => { + if (errGetAccts) throw errGetAccts; + done(); + }); + }); + }); + }); +}); diff --git a/test/helpers/fakeIpcProvider.js b/test/helpers/fakeIpcProvider.js new file mode 100644 index 000000000..20ce2fbd2 --- /dev/null +++ b/test/helpers/fakeIpcProvider.js @@ -0,0 +1,137 @@ +const assert = require('assert'); +const _ = require('lodash'); + +const FakeIpcProvider = function IpcProvider() { + var _this = this; + this.countId = 1; + this.notificationCount = 1; + this.getResponseStub = function () { + return { + jsonrpc: '2.0', + id: _this.countId, + result: null + }; + }; + this.getErrorStub = function () { + return { + jsonrpc: '2.0', + id: _this.countId, + error: { + code: 1234, + message: 'Stub error' + } + }; + }; + + this.response = []; + this.error = []; + this.validation = []; + this.notificationCallbacks = []; +}; + +FakeIpcProvider.prototype.send = function (payload, callback) { + var _this = this; + + // set id + if (payload.id) + this.countId = payload.id; + // else + // this.countId++; + + assert.equal(_.isArray(payload) || _.isObject(payload), true); + assert.equal(_.isFunction(callback), true); + + var validation = this.validation.shift(); + + if (validation) { + // imitate plain json object + validation(JSON.parse(JSON.stringify(payload)), callback); + } + + var response = this.getResponseOrError('response', payload); + var error = this.getResponseOrError('error', payload); + + setTimeout(function () { + callback(error, response); + }, 1); +}; + +FakeIpcProvider.prototype.on = function (type, callback) { + if (type === 'data') { + this.notificationCallbacks.push(callback); + } +}; + +FakeIpcProvider.prototype.getResponseOrError = function (type, payload) { + var _this = this; + var response; + + if (type === 'error') { + response = this.error.shift(); + } else { + response = this.response.shift() || this.getResponseStub(); + } + + + if (response) { + if (_.isArray(response)) { + response = response.map(function (resp, index) { + resp.id = payload[index] ? payload[index].id : _this.countId++; + return resp; + }); + } else + response.id = payload.id; + } + + return response; +}; + +FakeIpcProvider.prototype.injectNotification = function (notification) { + var _this = this; + setTimeout(function () { + _this.notificationCallbacks.forEach(function (cb) { + if (notification && cb) + cb(null, notification); + }); + }, 100 + this.notificationCount); + + this.notificationCount += 10; +}; + +// FakeHttpProvider.prototype.injectResponse = function (response) { +// this.response = response; +// }; + +FakeIpcProvider.prototype.injectBatchResults = function (results, error) { + var _this = this; + this.response.push(results.map(function (r) { + if (error) { + var response = _this.getErrorStub(); + response.error.message = r; + } else { + var response = _this.getResponseStub(); + response.result = r; + } + return response; + })); +}; + +FakeIpcProvider.prototype.injectResult = function (result) { + var response = this.getResponseStub(); + response.result = result; + + this.response.push(response); +}; + +FakeIpcProvider.prototype.injectError = function (error) { + var errorStub = this.getErrorStub(); + errorStub.error = error; // message, code + + this.error.push(errorStub); +}; + +FakeIpcProvider.prototype.injectValidation = function (callback) { + this.validation.push(callback); +}; + +module.exports = FakeIpcProvider; diff --git a/test/test1/password b/test/test1/password new file mode 100644 index 000000000..fca906b15 --- /dev/null +++ b/test/test1/password @@ -0,0 +1 @@ +dev_password \ No newline at end of file