Merge pull request #435 from embark-framework/features/fund-user-in-dev

Fund user accounts in dev
This commit is contained in:
Iuri Matias 2018-05-18 14:46:08 -04:00 committed by GitHub
commit c6970cfa4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 269 additions and 155 deletions

View File

@ -125,8 +125,9 @@ Blockchain.prototype.initChainAndGetAddress = function() {
}; };
var BlockchainClient = function(blockchainConfig, client, env, isDev) { var BlockchainClient = function(blockchainConfig, client, env, isDev) {
// TODO add other clients at some point
if (client === 'geth') { if (client === 'geth') {
return new Blockchain({blockchainConfig: blockchainConfig, client: GethCommands, env: env, isDev}); return new Blockchain({blockchainConfig, client: GethCommands, env, isDev});
} else { } else {
throw new Error('unknown client'); throw new Error('unknown client');
} }

View File

@ -1,4 +1,4 @@
let async = require('async'); const async = require('async');
// TODO: make all of this async // TODO: make all of this async
class GethCommands { class GethCommands {

View File

@ -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;

View File

@ -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;

View File

@ -1,98 +1,67 @@
const ProviderEngine = require('web3-provider-engine'); const ProviderEngine = require('web3-provider-engine');
const RpcSubprovider = require('web3-provider-engine/subproviders/rpc.js'); const RpcSubprovider = require('web3-provider-engine/subproviders/rpc.js');
const bip39 = require("bip39"); const async = require('async');
const hdkey = require('ethereumjs-wallet/hdkey'); const AccountParser = require('./accountParser');
const fs = require('../core/fs'); const fundAccount = require('./fundAccount');
const NO_ACCOUNTS = 'noAccounts';
class Provider { class Provider {
constructor(options) { constructor(options) {
const self = this;
this.web3 = options.web3; this.web3 = options.web3;
this.accountsConfig = options.accountsConfig; this.accountsConfig = options.accountsConfig;
this.web3Endpoint = options.web3Endpoint;
this.logger = options.logger; this.logger = options.logger;
this.isDev = options.isDev; this.isDev = options.isDev;
this.engine = new ProviderEngine(); this.engine = new ProviderEngine();
this.asyncMethods = {}; 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) { startWeb3Provider(callback) {
if (accountConfig.privateKey) { const self = this;
if (!accountConfig.privateKey.startsWith('0x')) { self.engine.addProvider(new RpcSubprovider({
accountConfig.privateKey = '0x' + accountConfig.privateKey; rpcUrl: self.web3Endpoint
} }));
return this.web3.eth.accounts.privateKeyToAccount(accountConfig.privateKey);
} // network connectivity error
if (accountConfig.privateKeyFile) { self.engine.on('error', (err) => {
let fileContent = fs.readFileSync(fs.dappPath(accountConfig.privateKeyFile)).toString(); // report connectivity errors
fileContent = fileContent.trim().split(/[,;]/); self.logger.error(err.stack);
return fileContent.map(key => { });
if (!key.startsWith('0x')) {
key = '0x' + key; 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 (!self.isDev) {
}); return next();
} }
if (accountConfig.mnemonic) { async.each(self.accounts, (account, eachCb) => {
const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(accountConfig.mnemonic.trim())); fundAccount(self.web3, account.address, eachCb);
}, next);
const addressIndex = accountConfig.addressIndex || 0; },
const numAddresses = accountConfig.numAddresses || 1; function populateWeb3Wallet(next) {
const wallet_hdpath = accountConfig.hdpath || "m/44'/60'/0'/0/"; self.accounts.forEach(account => {
self.addresses.push(account.address);
const accounts = []; self.web3.eth.accounts.wallet.add(account);
for (let i = addressIndex; i < addressIndex + numAddresses; i++) { });
const wallet = hdwallet.derivePath(wallet_hdpath + i).getWallet(); self.asyncMethods = {
accounts.push(this.web3.eth.accounts.privateKeyToAccount('0x' + wallet.getPrivateKey().toString('hex'))); eth_accounts: self.eth_accounts.bind(self)
};
next();
} }
return accounts; ], function (err) {
} if (err && err !== NO_ACCOUNTS) {
this.logger.warn('Unsupported account configuration: ' + JSON.stringify(accountConfig)); self.logger.error((err));
this.logger.warn('Try using one of those: ' + }
'{ "privateKey": "your-private-key", "privateKeyFile": "path/to/file/containing/key", "mnemonic": "12 word mnemonic" }'); callback();
return null; });
} }
eth_accounts(payload, cb) { eth_accounts(payload, cb) {

View File

@ -40,7 +40,7 @@ class Engine {
}, 0); }, 0);
if (this.interceptLogs || this.interceptLogs === undefined) { if (this.interceptLogs || this.interceptLogs === undefined) {
this.doInterceptLogs(); // this.doInterceptLogs();
} }
} }
@ -275,6 +275,7 @@ class Engine {
web3Service(options) { web3Service(options) {
let self = this; let self = this;
this.web3 = options.web3; this.web3 = options.web3;
let provider;
if (this.web3 === undefined) { if (this.web3 === undefined) {
this.web3 = new Web3(); this.web3 = new Web3();
if (this.config.contractsConfig.deployment.type === "rpc") { if (this.config.contractsConfig.deployment.type === "rpc") {
@ -286,43 +287,55 @@ class Engine {
isDev: this.isDev, isDev: this.isDev,
web3Endpoint web3Endpoint
}; };
this.web3.setProvider(new Provider(providerOptions)); provider = new Provider(providerOptions);
} else { } else {
throw new Error("contracts config error: unknown deployment type " + this.config.contractsConfig.deployment.type); throw new Error("contracts config error: unknown deployment type " + this.config.contractsConfig.deployment.type);
} }
} }
self.servicesMonitor.addCheck('Ethereum', function (cb) { async.waterfall([
if (self.web3.currentProvider === undefined) { function (next) {
return cb({name: __("No Blockchain node found"), status: 'off'}); if (!provider) {
return next();
}
provider.startWeb3Provider(next);
} }
], function (err) {
self.web3.eth.getAccounts(function(err, _accounts) { if (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'}); return cb({name: __("No Blockchain node found"), status: 'off'});
} }
// TODO: web3_clientVersion method is currently not implemented in web3.js 1.0 self.web3.eth.getAccounts(function(err, _accounts) {
self.web3._requestManager.send({method: 'web3_clientVersion', params: []}, (err, version) => {
if (err) { 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', { self.registerModule('whisper', {
addCheck: this.servicesMonitor.addCheck.bind(this.servicesMonitor), addCheck: self.servicesMonitor.addCheck.bind(self.servicesMonitor),
communicationConfig: this.config.communicationConfig, communicationConfig: self.config.communicationConfig,
web3: this.web3 web3: self.web3
});
}); });
} }

View File

@ -3,7 +3,6 @@
"Contract Name": "Contract Name", "Contract Name": "Contract Name",
"New Application": "New Application", "New Application": "New Application",
"create a barebones project meant only for contract development": "create a barebones project meant only for contract development", "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", "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)", "filename to output logs (default: none)": "filename to output logs (default: none)",
"level of logging to display": "level of logging to display", "level of logging to display": "level of logging to display",
@ -26,9 +25,6 @@
"custom gas limit (default: %s)": "custom gas limit (default: %s)", "custom gas limit (default: %s)": "custom gas limit (default: %s)",
"run tests": "run tests", "run tests": "run tests",
"resets embarks state on this dapp including clearing cache": "resets embarks state on this dapp including clearing cache", "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", "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", "Upload your dapp to a decentralized storage": "Upload your dapp to a decentralized storage",
"output the version number": "output the version number", "output the version number": "output the version number",
@ -42,12 +38,7 @@
"dashboard start": "dashboard start", "dashboard start": "dashboard start",
"loaded plugins": "loaded plugins", "loaded plugins": "loaded plugins",
"loading solc compiler": "loading solc compiler", "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", "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.", "%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", "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}}", "{{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)", "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", "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", "versions in use": "versions in use",
"language to use (default: en)": "language to use (default: en)",
"executing": "executing", "executing": "executing",
"finished deploying": "finished deploying",
"writing file": "writing file", "writing file": "writing file",
"errors found while generating": "errors found while generating", "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", "Looking for documentation? You can find it at": "Looking for documentation? You can find it at",
"Ready": "Ready", "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", "Embark Blockchain Using: %s": "Embark Blockchain Using: %s",
"running: %s": "running: %s", "running: %s": "running: %s",
"Initializing Embark Template....": "Initializing Embark Template....", "Initializing Embark Template....": "Initializing Embark Template....",
@ -110,5 +101,8 @@
"downloading {{packageName}} {{version}}....": "downloading {{packageName}} {{version}}....", "downloading {{packageName}} {{version}}....": "downloading {{packageName}} {{version}}....",
"Swarm node is offline...": "Swarm node is offline...", "Swarm node is offline...": "Swarm node is offline...",
"Swarm node detected...": "Swarm node detected...", "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'"
}

View File

@ -1,44 +1,37 @@
/*global describe, it, before*/ /*global describe, it*/
const assert = require('assert'); const assert = require('assert');
const sinon = require('sinon'); const sinon = require('sinon');
const Provider = require('../lib/contracts/provider'); const AccountParser = require('../lib/contracts/accountParser');
let TestLogger = require('../lib/tests/test_logger.js'); let TestLogger = require('../lib/tests/test_logger.js');
describe('embark.provider', function () { describe('embark.AccountParser', function () {
describe('#getAccount', function () { describe('#getAccount', function () {
let provider; const web3 = {
eth: {
before(() => { accounts: {
const web3 = { privateKeyToAccount: sinon.stub().callsFake((key) => {
eth: { return {key};
accounts: { })
privateKeyToAccount: sinon.stub().callsFake((key) => {
return {key};
})
}
} }
}; },
utils: {
provider = new Provider({ isHexStrict: sinon.stub().returns(true)
accountsConfig: [], }
logger: new TestLogger({}), };
web3Endpoint: 'http:localhost:555', const testLogger = new TestLogger({});
web3
});
});
it('should return one account with the key', function () { it('should return one account with the key', function () {
const account = provider.getAccount({ const account = AccountParser.getAccount({
privateKey: 'myKey' privateKey: 'myKey'
}); }, web3, testLogger);
assert.deepEqual(account, {key: '0xmyKey'}); assert.deepEqual(account, {key: '0xmyKey'});
}); });
it('should return two accounts from the keys in the file', function () { it('should return two accounts from the keys in the file', function () {
const account = provider.getAccount({ const account = AccountParser.getAccount({
privateKeyFile: 'test/keyFiles/twoKeys' privateKeyFile: 'test/keyFiles/twoKeys'
}); }, web3, testLogger);
assert.deepEqual(account, [ assert.deepEqual(account, [
{key: '0xkey1'}, {key: '0xkey1'},
@ -47,20 +40,19 @@ describe('embark.provider', function () {
}); });
it('should return one account from the mnemonic', 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' mnemonic: 'example exile argue silk regular smile grass bomb merge arm assist farm'
}); }, web3, testLogger);
assert.deepEqual(account, assert.deepEqual(account,
[{key: "0xf942d5d524ec07158df4354402bfba8d928c99d0ab34d0799a6158d56156d986"}]); [{key: "0xf942d5d524ec07158df4354402bfba8d928c99d0ab34d0799a6158d56156d986"}]);
}); });
it('should return two accounts from the mnemonic using numAddresses', function () { 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', mnemonic: 'example exile argue silk regular smile grass bomb merge arm assist farm',
numAddresses: 2 numAddresses: 2
}); }, web3, testLogger);
assert.deepEqual(account, assert.deepEqual(account,
[ [
@ -70,9 +62,9 @@ describe('embark.provider', function () {
}); });
it('should return nothing with bad config', function () { it('should return nothing with bad config', function () {
const account = provider.getAccount({ const account = AccountParser.getAccount({
badConfig: 'not working' badConfig: 'not working'
}); }, web3, testLogger);
assert.strictEqual(account, null); assert.strictEqual(account, null);
}); });

View File

@ -31,7 +31,7 @@ describe('embark.Blockchain', function () {
whisper: true, whisper: true,
account: {}, account: {},
bootnodes: "", bootnodes: "",
wsApi: [ "eth", "web3", "net", "shh" ], wsApi: ["eth", "web3", "net", "shh"],
wsHost: "localhost", wsHost: "localhost",
wsOrigins: false, wsOrigins: false,
wsPort: 8546, wsPort: 8546,
@ -68,7 +68,7 @@ describe('embark.Blockchain', function () {
whisper: false, whisper: false,
account: {}, account: {},
bootnodes: "", bootnodes: "",
wsApi: [ "eth", "web3", "net", "shh" ], wsApi: ["eth", "web3", "net", "shh"],
wsHost: "localhost", wsHost: "localhost",
wsOrigins: false, wsOrigins: false,
wsPort: 8546, wsPort: 8546,