diff --git a/lib/cmds/blockchain/blockchain.js b/lib/cmds/blockchain/blockchain.js index 4bbb68eb..5598e91d 100644 --- a/lib/cmds/blockchain/blockchain.js +++ b/lib/cmds/blockchain/blockchain.js @@ -125,8 +125,9 @@ Blockchain.prototype.initChainAndGetAddress = function() { }; var BlockchainClient = function(blockchainConfig, client, env, isDev) { + // TODO add other clients at some point if (client === 'geth') { - return new Blockchain({blockchainConfig: blockchainConfig, client: GethCommands, env: env, isDev}); + return new Blockchain({blockchainConfig, client: GethCommands, env, isDev}); } else { throw new Error('unknown client'); } diff --git a/lib/cmds/blockchain/geth_commands.js b/lib/cmds/blockchain/geth_commands.js index 379bf54e..b05ada7a 100644 --- a/lib/cmds/blockchain/geth_commands.js +++ b/lib/cmds/blockchain/geth_commands.js @@ -1,4 +1,4 @@ -let async = require('async'); +const async = require('async'); // TODO: make all of this async class GethCommands { diff --git a/lib/contracts/accountParser.js b/lib/contracts/accountParser.js new file mode 100644 index 00000000..e2460bd6 --- /dev/null +++ b/lib/contracts/accountParser.js @@ -0,0 +1,73 @@ +const bip39 = require("bip39"); +const hdkey = require('ethereumjs-wallet/hdkey'); +const fs = require('../core/fs'); + +class AccountParser { + static parseAccountsConfig(accountsConfig, web3, logger) { + let accounts = []; + if (accountsConfig && accountsConfig.length) { + accountsConfig.forEach(accountConfig => { + const account = AccountParser.getAccount(accountConfig, web3, logger); + if (!account) { + return; + } + if (Array.isArray(account)) { + accounts = accounts.concat(account); + return; + } + accounts.push(account); + }); + } + return accounts; + } + + static getAccount(accountConfig, web3, logger) { + if (!logger) { + logger = console; + } + if (accountConfig.privateKey) { + if (!accountConfig.privateKey.startsWith('0x')) { + accountConfig.privateKey = '0x' + accountConfig.privateKey; + } + if (!web3.utils.isHexStrict(accountConfig.privateKey)) { + logger.warn(`Private key ending with ${accountConfig.privateKey.substr(accountConfig.privateKey.length - 5)} is not a HEX string`); + return null; + } + return web3.eth.accounts.privateKeyToAccount(accountConfig.privateKey); + } + if (accountConfig.privateKeyFile) { + let fileContent = fs.readFileSync(fs.dappPath(accountConfig.privateKeyFile)).toString(); + fileContent = fileContent.trim().split(/[,;]/); + return fileContent.map((key, index) => { + if (!key.startsWith('0x')) { + key = '0x' + key; + } + if (!web3.utils.isHexStrict(key)) { + logger.warn(`Private key is not a HEX string in file ${accountConfig.privateKeyFile} at index ${index}`); + return null; + } + return web3.eth.accounts.privateKeyToAccount(key); + }); + } + if (accountConfig.mnemonic) { + const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(accountConfig.mnemonic.trim())); + + const addressIndex = accountConfig.addressIndex || 0; + const numAddresses = accountConfig.numAddresses || 1; + const wallet_hdpath = accountConfig.hdpath || "m/44'/60'/0'/0/"; + + const accounts = []; + for (let i = addressIndex; i < addressIndex + numAddresses; i++) { + const wallet = hdwallet.derivePath(wallet_hdpath + i).getWallet(); + accounts.push(web3.eth.accounts.privateKeyToAccount('0x' + wallet.getPrivateKey().toString('hex'))); + } + return accounts; + } + logger.warn('Unsupported account configuration: ' + JSON.stringify(accountConfig)); + logger.warn('Try using one of those: ' + + '{ "privateKey": "your-private-key", "privateKeyFile": "path/to/file/containing/key", "mnemonic": "12 word mnemonic" }'); + return null; + } +} + +module.exports = AccountParser; diff --git a/lib/contracts/fundAccount.js b/lib/contracts/fundAccount.js new file mode 100644 index 00000000..cb9baade --- /dev/null +++ b/lib/contracts/fundAccount.js @@ -0,0 +1,72 @@ +const async = require('async'); +const TARGET = 15000000000000000000; +const ALREADY_FUNDED = 'alreadyFunded'; + +function fundAccount(web3, accountAddress, callback) { + let accountBalance; + let coinbaseAddress; + let lastNonce; + let gasPrice; + + async.waterfall([ + function getAccountBalance(next) { + web3.eth.getBalance(accountAddress, (err, balance) => { + if (err) { + return next(err); + } + if (balance >= TARGET) { + return next(ALREADY_FUNDED); + } + accountBalance = balance; + next(); + }); + }, + function getNeededParams(next) { + async.parallel([ + function getCoinbaseAddress(paraCb) { + web3.eth.getCoinbase() + .then((address) => { + coinbaseAddress = address; + paraCb(); + }).catch(paraCb); + }, + function getGasPrice(paraCb) { + web3.eth.getGasPrice((err, price) => { + if (err) { + return paraCb(err); + } + gasPrice = price; + paraCb(); + }); + } + ], (err, _result) => { + next(err); + }); + }, + function getNonce(next) { + web3.eth.getTransactionCount(coinbaseAddress, (err, nonce) => { + if (err) { + return next(err); + } + lastNonce = nonce; + next(); + }); + }, + function sendTransaction(next) { + web3.eth.sendTransaction({ + from: coinbaseAddress, + to: accountAddress, + value: TARGET - accountBalance, + gasPrice: gasPrice, + nonce: lastNonce + }, next); + } + ], (err) => { + if (err && err !== ALREADY_FUNDED) { + return callback(err); + } + callback(); + }); +} + +module.exports = fundAccount; diff --git a/lib/contracts/provider.js b/lib/contracts/provider.js index a5225f0e..29b5ad2a 100644 --- a/lib/contracts/provider.js +++ b/lib/contracts/provider.js @@ -1,98 +1,67 @@ const ProviderEngine = require('web3-provider-engine'); const RpcSubprovider = require('web3-provider-engine/subproviders/rpc.js'); -const bip39 = require("bip39"); -const hdkey = require('ethereumjs-wallet/hdkey'); -const fs = require('../core/fs'); +const async = require('async'); +const AccountParser = require('./accountParser'); +const fundAccount = require('./fundAccount'); + +const NO_ACCOUNTS = 'noAccounts'; class Provider { constructor(options) { - const self = this; this.web3 = options.web3; this.accountsConfig = options.accountsConfig; + this.web3Endpoint = options.web3Endpoint; this.logger = options.logger; this.isDev = options.isDev; this.engine = new ProviderEngine(); this.asyncMethods = {}; - - this.engine.addProvider(new RpcSubprovider({ - rpcUrl: options.web3Endpoint - })); - - if (this.accountsConfig && this.accountsConfig.length) { - this.accounts = []; - this.addresses = []; - this.accountsConfig.forEach(accountConfig => { - const account = this.getAccount(accountConfig); - if (!account) { - return; - } - if (Array.isArray(account)) { - this.accounts = this.accounts.concat(account); - account.forEach(acc => { - this.addresses.push(acc.address); - }); - return; - } - this.accounts.push(account); - this.addresses.push(account.address); - }); - - if (this.accounts.length) { - this.accounts.forEach(account => { - this.web3.eth.accounts.wallet.add(account); - }); - this.asyncMethods = { - eth_accounts: self.eth_accounts.bind(this) - }; - if (this.isDev) { - this.logger.warn('You are using your own account in the develop environment. It might not be funded.'); - } - } - } - - // network connectivity error - this.engine.on('error', (err) => { - // report connectivity errors - this.logger.error(err.stack); - }); - this.engine.start(); } - getAccount(accountConfig) { - if (accountConfig.privateKey) { - if (!accountConfig.privateKey.startsWith('0x')) { - accountConfig.privateKey = '0x' + accountConfig.privateKey; - } - return this.web3.eth.accounts.privateKeyToAccount(accountConfig.privateKey); - } - if (accountConfig.privateKeyFile) { - let fileContent = fs.readFileSync(fs.dappPath(accountConfig.privateKeyFile)).toString(); - fileContent = fileContent.trim().split(/[,;]/); - return fileContent.map(key => { - if (!key.startsWith('0x')) { - key = '0x' + key; + startWeb3Provider(callback) { + const self = this; + self.engine.addProvider(new RpcSubprovider({ + rpcUrl: self.web3Endpoint + })); + + // network connectivity error + self.engine.on('error', (err) => { + // report connectivity errors + self.logger.error(err.stack); + }); + + self.engine.start(); + self.web3.setProvider(self); + + self.accounts = AccountParser.parseAccountsConfig(self.accountsConfig, self.web3, self.logger); + self.addresses = []; + async.waterfall([ + function fundAccounts(next) { + if (!self.accounts.length) { + return next(NO_ACCOUNTS); } - return this.web3.eth.accounts.privateKeyToAccount(key); - }); - } - if (accountConfig.mnemonic) { - const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(accountConfig.mnemonic.trim())); - - const addressIndex = accountConfig.addressIndex || 0; - const numAddresses = accountConfig.numAddresses || 1; - const wallet_hdpath = accountConfig.hdpath || "m/44'/60'/0'/0/"; - - const accounts = []; - for (let i = addressIndex; i < addressIndex + numAddresses; i++) { - const wallet = hdwallet.derivePath(wallet_hdpath + i).getWallet(); - accounts.push(this.web3.eth.accounts.privateKeyToAccount('0x' + wallet.getPrivateKey().toString('hex'))); + if (!self.isDev) { + return next(); + } + async.each(self.accounts, (account, eachCb) => { + fundAccount(self.web3, account.address, eachCb); + }, next); + }, + function populateWeb3Wallet(next) { + self.accounts.forEach(account => { + self.addresses.push(account.address); + self.web3.eth.accounts.wallet.add(account); + }); + self.asyncMethods = { + eth_accounts: self.eth_accounts.bind(self) + }; + next(); } - return accounts; - } - this.logger.warn('Unsupported account configuration: ' + JSON.stringify(accountConfig)); - this.logger.warn('Try using one of those: ' + - '{ "privateKey": "your-private-key", "privateKeyFile": "path/to/file/containing/key", "mnemonic": "12 word mnemonic" }'); - return null; + ], function (err) { + if (err && err !== NO_ACCOUNTS) { + self.logger.error((err)); + } + callback(); + }); } eth_accounts(payload, cb) { diff --git a/lib/core/engine.js b/lib/core/engine.js index 18bb6682..b41736e8 100644 --- a/lib/core/engine.js +++ b/lib/core/engine.js @@ -40,7 +40,7 @@ class Engine { }, 0); if (this.interceptLogs || this.interceptLogs === undefined) { - this.doInterceptLogs(); + // this.doInterceptLogs(); } } @@ -275,6 +275,7 @@ class Engine { web3Service(options) { let self = this; this.web3 = options.web3; + let provider; if (this.web3 === undefined) { this.web3 = new Web3(); if (this.config.contractsConfig.deployment.type === "rpc") { @@ -286,43 +287,55 @@ class Engine { isDev: this.isDev, web3Endpoint }; - this.web3.setProvider(new Provider(providerOptions)); + provider = new Provider(providerOptions); } else { throw new Error("contracts config error: unknown deployment type " + this.config.contractsConfig.deployment.type); } } - self.servicesMonitor.addCheck('Ethereum', function (cb) { - if (self.web3.currentProvider === undefined) { - return cb({name: __("No Blockchain node found"), status: 'off'}); + async.waterfall([ + function (next) { + if (!provider) { + return next(); + } + provider.startWeb3Provider(next); } - - self.web3.eth.getAccounts(function(err, _accounts) { - if (err) { + ], function (err) { + if (err) { + console.error(err); + } + self.servicesMonitor.addCheck('Ethereum', function (cb) { + if (self.web3.currentProvider === undefined) { return cb({name: __("No Blockchain node found"), status: 'off'}); } - // TODO: web3_clientVersion method is currently not implemented in web3.js 1.0 - self.web3._requestManager.send({method: 'web3_clientVersion', params: []}, (err, version) => { + self.web3.eth.getAccounts(function(err, _accounts) { if (err) { - return cb({name: __("Ethereum node (version unknown)"), status: 'on'}); + return cb({name: __("No Blockchain node found"), status: 'off'}); } - if (version.indexOf("/") < 0) { - return cb({name: version, status: 'on'}); - } - let nodeName = version.split("/")[0]; - let versionNumber = version.split("/")[1].split("-")[0]; - let name = nodeName + " " + versionNumber + " (Ethereum)"; - return cb({name: name, status: 'on'}); + // TODO: web3_clientVersion method is currently not implemented in web3.js 1.0 + self.web3._requestManager.send({method: 'web3_clientVersion', params: []}, (err, version) => { + if (err) { + return cb({name: __("Ethereum node (version unknown)"), status: 'on'}); + } + if (version.indexOf("/") < 0) { + return cb({name: version, status: 'on'}); + } + let nodeName = version.split("/")[0]; + let versionNumber = version.split("/")[1].split("-")[0]; + let name = nodeName + " " + versionNumber + " (Ethereum)"; + + return cb({name: name, status: 'on'}); + }); }); }); - }); - this.registerModule('whisper', { - addCheck: this.servicesMonitor.addCheck.bind(this.servicesMonitor), - communicationConfig: this.config.communicationConfig, - web3: this.web3 + self.registerModule('whisper', { + addCheck: self.servicesMonitor.addCheck.bind(self.servicesMonitor), + communicationConfig: self.config.communicationConfig, + web3: self.web3 + }); }); } diff --git a/lib/i18n/locales/en.json b/lib/i18n/locales/en.json index f2437081..81f2aa87 100644 --- a/lib/i18n/locales/en.json +++ b/lib/i18n/locales/en.json @@ -3,7 +3,6 @@ "Contract Name": "Contract Name", "New Application": "New Application", "create a barebones project meant only for contract development": "create a barebones project meant only for contract development", - "language to use (default: en)": "language to use (default: en)", "create a working dapp with a SimpleStorage contract": "create a working dapp with a SimpleStorage contract", "filename to output logs (default: none)": "filename to output logs (default: none)", "level of logging to display": "level of logging to display", @@ -26,9 +25,6 @@ "custom gas limit (default: %s)": "custom gas limit (default: %s)", "run tests": "run tests", "resets embarks state on this dapp including clearing cache": "resets embarks state on this dapp including clearing cache", - "Graph will not include undeployed contracts": "Graph will not include undeployed contracts", - "Graph will not include functions": "Graph will not include functions", - "Graph will not include events": "Graph will not include events", "generates documentation based on the smart contracts configured": "generates documentation based on the smart contracts configured", "Upload your dapp to a decentralized storage": "Upload your dapp to a decentralized storage", "output the version number": "output the version number", @@ -42,12 +38,7 @@ "dashboard start": "dashboard start", "loaded plugins": "loaded plugins", "loading solc compiler": "loading solc compiler", - "Cannot upload: {{platform}} node is not running on {{url}}.": "Cannot upload: {{platform}} node is not running on {{url}}.", - "http:// Cannot upload: {{platform}} node is not running on {{url}}.": "http:// Cannot upload: {{platform}} node is not running on {{url}}.", - "Cannot upload:": "Cannot upload:", - "node is not running on": "node is not running on", "compiling solidity contracts": "compiling solidity contracts", - "Cannot upload: {{platform}} node is not running on {{protocol}}://{{host}}:{{port}}.": "Cannot upload: {{platform}} node is not running on {{protocol}}://{{host}}:{{port}}.", "%s doesn't have a compatible contract compiler. Maybe a plugin exists for it.": "%s doesn't have a compatible contract compiler. Maybe a plugin exists for it.", "assuming %s to be an interface": "assuming %s to be an interface", "{{className}}: couldn't find instanceOf contract {{parentContractName}}": "{{className}}: couldn't find instanceOf contract {{parentContractName}}", @@ -79,15 +70,15 @@ "to immediatly exit (alias: exit)": "to immediatly exit (alias: exit)", "The web3 object and the interfaces for the deployed contracts and their methods are also available": "The web3 object and the interfaces for the deployed contracts and their methods are also available", "versions in use": "versions in use", + "language to use (default: en)": "language to use (default: en)", "executing": "executing", - "finished deploying": "finished deploying", "writing file": "writing file", "errors found while generating": "errors found while generating", - "deploying to swarm!": "deploying to swarm!", - "adding %s to swarm": "adding %s to swarm", - "error uploading to swarm": "error uploading to swarm", "Looking for documentation? You can find it at": "Looking for documentation? You can find it at", "Ready": "Ready", + "Graph will not include undeployed contracts": "Graph will not include undeployed contracts", + "Graph will not include functions": "Graph will not include functions", + "Graph will not include events": "Graph will not include events", "Embark Blockchain Using: %s": "Embark Blockchain Using: %s", "running: %s": "running: %s", "Initializing Embark Template....": "Initializing Embark Template....", @@ -110,5 +101,8 @@ "downloading {{packageName}} {{version}}....": "downloading {{packageName}} {{version}}....", "Swarm node is offline...": "Swarm node is offline...", "Swarm node detected...": "Swarm node detected...", - "file not found, creating it...": "file not found, creating it..." -} + "Ethereum node (version unknown)": "Ethereum node (version unknown)", + "No Blockchain node found": "No Blockchain node found", + "Couldn't connect to an Ethereum node are you sure it's on?": "Couldn't connect to an Ethereum node are you sure it's on?", + "make sure you have an Ethereum node or simulator running. e.g '%s'": "make sure you have an Ethereum node or simulator running. e.g '%s'" +} \ No newline at end of file diff --git a/test/provider.js b/test/accountParser.js similarity index 64% rename from test/provider.js rename to test/accountParser.js index 1fc0ebbf..d79b002f 100644 --- a/test/provider.js +++ b/test/accountParser.js @@ -1,44 +1,37 @@ -/*global describe, it, before*/ +/*global describe, it*/ const assert = require('assert'); const sinon = require('sinon'); -const Provider = require('../lib/contracts/provider'); +const AccountParser = require('../lib/contracts/accountParser'); let TestLogger = require('../lib/tests/test_logger.js'); -describe('embark.provider', function () { +describe('embark.AccountParser', function () { describe('#getAccount', function () { - let provider; - - before(() => { - const web3 = { - eth: { - accounts: { - privateKeyToAccount: sinon.stub().callsFake((key) => { - return {key}; - }) - } + const web3 = { + eth: { + accounts: { + privateKeyToAccount: sinon.stub().callsFake((key) => { + return {key}; + }) } - }; - - provider = new Provider({ - accountsConfig: [], - logger: new TestLogger({}), - web3Endpoint: 'http:localhost:555', - web3 - }); - }); + }, + utils: { + isHexStrict: sinon.stub().returns(true) + } + }; + const testLogger = new TestLogger({}); it('should return one account with the key', function () { - const account = provider.getAccount({ + const account = AccountParser.getAccount({ privateKey: 'myKey' - }); + }, web3, testLogger); assert.deepEqual(account, {key: '0xmyKey'}); }); it('should return two accounts from the keys in the file', function () { - const account = provider.getAccount({ + const account = AccountParser.getAccount({ privateKeyFile: 'test/keyFiles/twoKeys' - }); + }, web3, testLogger); assert.deepEqual(account, [ {key: '0xkey1'}, @@ -47,20 +40,19 @@ describe('embark.provider', function () { }); it('should return one account from the mnemonic', function () { - const account = provider.getAccount({ + const account = AccountParser.getAccount({ mnemonic: 'example exile argue silk regular smile grass bomb merge arm assist farm' - }); - + }, web3, testLogger); assert.deepEqual(account, [{key: "0xf942d5d524ec07158df4354402bfba8d928c99d0ab34d0799a6158d56156d986"}]); }); it('should return two accounts from the mnemonic using numAddresses', function () { - const account = provider.getAccount({ + const account = AccountParser.getAccount({ mnemonic: 'example exile argue silk regular smile grass bomb merge arm assist farm', numAddresses: 2 - }); + }, web3, testLogger); assert.deepEqual(account, [ @@ -70,9 +62,9 @@ describe('embark.provider', function () { }); it('should return nothing with bad config', function () { - const account = provider.getAccount({ + const account = AccountParser.getAccount({ badConfig: 'not working' - }); + }, web3, testLogger); assert.strictEqual(account, null); }); diff --git a/test/blockchain.js b/test/blockchain.js index 06957e0f..bcb48d08 100644 --- a/test/blockchain.js +++ b/test/blockchain.js @@ -31,7 +31,7 @@ describe('embark.Blockchain', function () { whisper: true, account: {}, bootnodes: "", - wsApi: [ "eth", "web3", "net", "shh" ], + wsApi: ["eth", "web3", "net", "shh"], wsHost: "localhost", wsOrigins: false, wsPort: 8546, @@ -68,7 +68,7 @@ describe('embark.Blockchain', function () { whisper: false, account: {}, bootnodes: "", - wsApi: [ "eth", "web3", "net", "shh" ], + wsApi: ["eth", "web3", "net", "shh"], wsHost: "localhost", wsOrigins: false, wsPort: 8546,