feat(@embark/core): Disable regular txs until needed

Regular transactions (aka “dev funds”) exist in embark as a workaround to a known bug in geth when using metamask. The workaround is to send a transaction at a regular interval (1.5s), which pushes through any transactions that were stuck. The problem is that the transaction logs and trace logs become cluttered and difficult to parse visually.

This PR disables regular transactions until the following conditions are met:
1. Embark is running geth
2. The user is running metamask in their browser
3. The user authenticates to the cockpit with `enableRegularTxs=1|true` in the query string.

A console warning is show in large letters in the browser with a link to the cockpit URL that includes the special query string to enable regular txs.

This could be extended later to have a button in the cockpit that start/stops regular txs. Or at least extended to allow disabling of regular txs once started.

Support standalone blockchain process.
This commit is contained in:
emizzle 2018-12-21 18:57:10 +11:00
parent 8efa8895aa
commit 135fde0a85
14 changed files with 182 additions and 39 deletions

View File

@ -426,6 +426,13 @@ export const removeEditorTabs = {
failure: () => action(REMOVE_EDITOR_TABS[FAILURE]) failure: () => action(REMOVE_EDITOR_TABS[FAILURE])
}; };
export const INIT_REGULAR_TXS = createRequestTypes('INIT_REGULAR_TXS');
export const initRegularTxs = {
request: () => action(INIT_REGULAR_TXS[REQUEST], {mode: 'on'}),
success: () => action(INIT_REGULAR_TXS[SUCCESS]),
failure: () => action(INIT_REGULAR_TXS[FAILURE])
};
// Web Socket // Web Socket
export const WATCH_NEW_PROCESS_LOGS = 'WATCH_NEW_PROCESS_LOGS'; export const WATCH_NEW_PROCESS_LOGS = 'WATCH_NEW_PROCESS_LOGS';
export const STOP_NEW_PROCESS_LOGS = 'STOP_NEW_PROCESS_LOGS'; export const STOP_NEW_PROCESS_LOGS = 'STOP_NEW_PROCESS_LOGS';

View File

@ -6,7 +6,7 @@ import routes from '../routes';
import Login from '../components/Login'; import Login from '../components/Login';
import Layout from "../components/Layout"; import Layout from "../components/Layout";
import {DEFAULT_HOST} from '../constants'; import {DEFAULT_HOST} from '../constants';
import {getQueryToken, stripQueryToken} from '../utils/utils'; import {getQueryToken, stripQueryToken, getQueryParam, stripQueryParam} from '../utils/utils';
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import { import {
@ -16,6 +16,7 @@ import {
plugins as pluginsAction, plugins as pluginsAction,
listenToServices as listenToServicesAction, listenToServices as listenToServicesAction,
listenToContracts as listenToContractsAction, listenToContracts as listenToContractsAction,
initRegularTxs as initRegularTxsAction,
changeTheme, fetchTheme changeTheme, fetchTheme
} from '../actions'; } from '../actions';
@ -25,6 +26,8 @@ import {
getCredentials, getAuthenticationError, getProcesses, getTheme getCredentials, getAuthenticationError, getProcesses, getTheme
} from '../reducers/selectors'; } from '../reducers/selectors';
const ENABLE_REGULAR_TXS = 'enableRegularTxs';
class AppContainer extends Component { class AppContainer extends Component {
componentDidMount() { componentDidMount() {
this.props.fetchCredentials(); this.props.fetchCredentials();
@ -50,26 +53,28 @@ class AppContainer extends Component {
const queryToken = getQueryToken(this.props.location); const queryToken = getQueryToken(this.props.location);
if (queryToken && !(queryToken === this.props.credentials.token && if (queryToken && !(queryToken === this.props.credentials.token &&
this.props.credentials.host === DEFAULT_HOST)) { this.props.credentials.host === DEFAULT_HOST)) {
return true; return true;
} }
if (!this.props.credentials.authenticated && if (!this.props.credentials.authenticated &&
this.props.credentials.host && this.props.credentials.host &&
this.props.credentials.token) { this.props.credentials.token) {
return true; return true;
} }
return false; return false;
} }
componentDidUpdate(){ componentDidUpdate() {
if (this.requireAuthentication()) { if (this.requireAuthentication()) {
this.doAuthenticate(); this.doAuthenticate();
} }
if (getQueryToken(this.props.location) && const enableRegularTxs = !!getQueryParam(this.props.location, ENABLE_REGULAR_TXS);
(!this.props.credentials.authenticating ||
if (getQueryToken(this.props.location) &&
(!this.props.credentials.authenticating ||
this.props.credentials.authenticated)) { this.props.credentials.authenticated)) {
this.props.history.replace(stripQueryToken(this.props.location)); this.props.history.replace(stripQueryToken(this.props.location));
} }
@ -80,6 +85,10 @@ class AppContainer extends Component {
this.props.listenToServices(); this.props.listenToServices();
this.props.fetchPlugins(); this.props.fetchPlugins();
this.props.listenToContracts(); this.props.listenToContracts();
if (enableRegularTxs) {
this.props.initRegularTxs();
this.props.history.replace(stripQueryParam(this.props.location, ENABLE_REGULAR_TXS));
}
} }
} }
@ -99,18 +108,18 @@ class AppContainer extends Component {
renderBody() { renderBody() {
if (this.shouldRenderLogin()) { if (this.shouldRenderLogin()) {
return ( return (
<Login credentials={this.props.credentials} <Login credentials={this.props.credentials}
authenticate={this.props.authenticate} authenticate={this.props.authenticate}
error={this.props.authenticationError} /> error={this.props.authenticationError} />
); );
} else if (this.props.credentials.authenticating) { } else if (this.props.credentials.authenticating) {
return <React.Fragment/>; return <React.Fragment />;
} }
return ( return (
<Layout location={this.props.location} <Layout location={this.props.location}
logout={this.props.logout} logout={this.props.logout}
toggleTheme={() => this.toggleTheme()} toggleTheme={() => this.toggleTheme()}
currentTheme={this.props.theme}> currentTheme={this.props.theme}>
<React.Fragment>{routes}</React.Fragment> <React.Fragment>{routes}</React.Fragment>
</Layout> </Layout>
); );
@ -148,7 +157,8 @@ AppContainer.propTypes = {
fetchTheme: PropTypes.func, fetchTheme: PropTypes.func,
history: PropTypes.object, history: PropTypes.object,
listenToServices: PropTypes.func, listenToServices: PropTypes.func,
listenToContracts: PropTypes.func listenToContracts: PropTypes.func,
initRegularTxs: PropTypes.func
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
@ -173,6 +183,7 @@ export default withRouter(connect(
fetchPlugins: pluginsAction.request, fetchPlugins: pluginsAction.request,
changeTheme: changeTheme.request, changeTheme: changeTheme.request,
fetchTheme: fetchTheme.request, fetchTheme: fetchTheme.request,
listenToContracts: listenToContractsAction listenToContracts: listenToContractsAction,
initRegularTxs: initRegularTxsAction.request
}, },
)(AppContainer)); )(AppContainer));

View File

@ -79,6 +79,7 @@ export const debugStepIntoForward = doRequest.bind(null, actions.debugStepIntoFo
export const debugStepIntoBackward = doRequest.bind(null, actions.debugStepIntoBackward, api.debugStepIntoBackward); export const debugStepIntoBackward = doRequest.bind(null, actions.debugStepIntoBackward, api.debugStepIntoBackward);
export const toggleBreakpoint = doRequest.bind(null, actions.toggleBreakpoint, api.toggleBreakpoint); export const toggleBreakpoint = doRequest.bind(null, actions.toggleBreakpoint, api.toggleBreakpoint);
export const authenticate = doRequest.bind(null, actions.authenticate, api.authenticate); export const authenticate = doRequest.bind(null, actions.authenticate, api.authenticate);
export const initRegularTxs = doRequest.bind(null, actions.initRegularTxs, api.initRegularTxs);
export const fetchCredentials = doRequest.bind(null, actions.fetchCredentials, storage.fetchCredentials); export const fetchCredentials = doRequest.bind(null, actions.fetchCredentials, storage.fetchCredentials);
export const saveCredentials = doRequest.bind(null, actions.saveCredentials, storage.saveCredentials); export const saveCredentials = doRequest.bind(null, actions.saveCredentials, storage.saveCredentials);
@ -343,6 +344,10 @@ export function *watchRemoveEditorTabsSuccess() {
yield takeEvery(actions.REMOVE_EDITOR_TABS[actions.SUCCESS], fetchEditorTabs); yield takeEvery(actions.REMOVE_EDITOR_TABS[actions.SUCCESS], fetchEditorTabs);
} }
export function *watchInitRegularTxs() {
yield takeEvery(actions.INIT_REGULAR_TXS[actions.REQUEST], initRegularTxs);
}
function createChannel(socket) { function createChannel(socket) {
return eventChannel(emit => { return eventChannel(emit => {
socket.onmessage = ((message) => { socket.onmessage = ((message) => {
@ -585,6 +590,7 @@ export default function *root() {
fork(watchRemoveEditorTabsSuccess), fork(watchRemoveEditorTabsSuccess),
fork(watchPostFileSuccess), fork(watchPostFileSuccess),
fork(watchPostFolderSuccess), fork(watchPostFolderSuccess),
fork(watchListenContracts) fork(watchListenContracts),
fork(watchInitRegularTxs)
]); ]);
} }

View File

@ -228,6 +228,10 @@ export function toggleBreakpoint(payload) {
return post('/debugger/breakpoint', {params: payload, credentials: payload.credentials}); return post('/debugger/breakpoint', {params: payload, credentials: payload.credentials});
} }
export function initRegularTxs(payload) {
return get('/regular-txs', {params: payload, credentials: payload.credentials});
}
export function listenToDebugger(credentials) { export function listenToDebugger(credentials) {
return websocket(credentials, '/debugger'); return websocket(credentials, '/debugger');
} }

View File

@ -23,8 +23,12 @@ export function ansiToHtml(text) {
return convert.toHtml(text.replace(/\n/g,'<br>')); return convert.toHtml(text.replace(/\n/g,'<br>'));
} }
export function getQueryParam(location, param) {
return qs.parse(location.search, {ignoreQueryPrefix: true})[param];
}
export function getQueryToken(location) { export function getQueryToken(location) {
return qs.parse(location.search, {ignoreQueryPrefix: true}).token; return getQueryParam(location, 'token');
} }
export function getDebuggerTransactionHash(location) { export function getDebuggerTransactionHash(location) {
@ -32,9 +36,13 @@ export function getDebuggerTransactionHash(location) {
} }
export function stripQueryToken(location) { export function stripQueryToken(location) {
return stripQueryParam(location, 'token');
}
export function stripQueryParam(location, param) {
const _location = Object.assign({}, location); const _location = Object.assign({}, location);
_location.search = _location.search.replace( _location.search = _location.search.replace(
/(\?|&?)(token=[\w-]*)(&?)/, new RegExp(`(\\?|&?)(${param}=[\\w-]*)(&?)`),
(_, p1, p2, p3) => (p2 ? (p3 === '&' ? p1 : '') : '') (_, p1, p2, p3) => (p2 ? (p3 === '&' ? p1 : '') : '')
); );
return _location; return _location;

View File

@ -53,7 +53,9 @@
"eth_sendTransaction": "eth_sendTransaction", "eth_sendTransaction": "eth_sendTransaction",
"eth_sendRawTransaction": "eth_sendRawTransaction", "eth_sendRawTransaction": "eth_sendRawTransaction",
"eth_getTransactionReceipt": "eth_getTransactionReceipt" "eth_getTransactionReceipt": "eth_getTransactionReceipt"
} },
"startRegularTxs": "startRegularTxs",
"stopRegularTxs": "stopRegularTxs"
}, },
"storage": { "storage": {
"init": "init", "init": "init",

View File

@ -26,6 +26,9 @@ class BlockchainListener {
if (this.ipc.isServer()) { if (this.ipc.isServer()) {
this._listenToBlockchainLogs(); this._listenToBlockchainLogs();
this._listenToCommands();
this._registerConsoleCommands();
this._registerApiEndpoint();
} }
} }
@ -40,6 +43,43 @@ class BlockchainListener {
this.processLogsApi.logHandler.handleLog({logLevel, message}); this.processLogsApi.logHandler.handleLog({logLevel, message});
}); });
} }
_registerConsoleCommands() {
this.embark.registerConsoleCommand({
description: 'Toggles regular transactions used to prevent transactions from getting stuck when using Geth and Metamask',
matches: ['regularTxs on', 'regularTxs off'],
usage: "regularTxs on/off",
process: (cmd, callback) => {
const eventCmd = `regularTxs:${cmd.trim().endsWith('on') ? 'start' : 'stop'}`;
this.events.request(eventCmd, callback);
}
});
}
_registerApiEndpoint() {
this.embark.registerAPICall(
'get',
'/embark-api/regular-txs',
(req, _res) => {
this.events.request(`regularTxs:${req.query.mode === 'on' ? 'start' : 'stop'}`);
}
);
}
_listenToCommands() {
this.events.setCommandHandler('regularTxs:start', (cb) => {
this.events.emit('regularTxs:start');
this.ipc.broadcast('regularTxs', 'start');
return cb(null, 'Enabling regular transactions');
});
this.events.setCommandHandler('regularTxs:stop', (cb) => {
this.events.emit('regularTxs:stop');
this.ipc.broadcast('regularTxs', 'stop');
return cb(null, 'Disabling regular transactions');
});
}
} }
module.exports = BlockchainListener; module.exports = BlockchainListener;

View File

@ -65,6 +65,8 @@ var Blockchain = function(userConfig, clientClass) {
proxy: this.userConfig.proxy proxy: this.userConfig.proxy
}; };
this.devFunds = null;
if (this.userConfig.accounts) { if (this.userConfig.accounts) {
const nodeAccounts = this.userConfig.accounts.find(account => account.nodeAccounts); const nodeAccounts = this.userConfig.accounts.find(account => account.nodeAccounts);
if (nodeAccounts) { if (nodeAccounts) {
@ -120,7 +122,7 @@ Blockchain.prototype.initStandaloneProcess = function () {
if (this.isStandalone) { if (this.isStandalone) {
// on every log logged in logger (say that 3x fast), send the log // on every log logged in logger (say that 3x fast), send the log
// to the IPC serve listening (only if we're connected of course) // to the IPC serve listening (only if we're connected of course)
this.events.on('log', (logLevel, message) => { this.logger.events.on('log', (logLevel, message) => {
if (this.ipc.connected) { if (this.ipc.connected) {
this.ipc.request('blockchain:log', {logLevel, message}); this.ipc.request('blockchain:log', {logLevel, message});
} }
@ -133,7 +135,14 @@ Blockchain.prototype.initStandaloneProcess = function () {
// `embark run` without restarting `embark blockchain`) // `embark run` without restarting `embark blockchain`)
setInterval(() => { setInterval(() => {
if (!this.ipc.connected) { if (!this.ipc.connected) {
this.ipc.connect(() => {}); this.ipc.connect(() => {
if (this.ipc.connected) {
this.ipc.listenTo('regularTxs', (mode) => {
if(mode === 'start') this.startRegularTxs(() => {});
else if (mode === 'stop') this.stopRegularTxs(() => {});
});
}
});
} }
}, IPC_CONNECT_INTERVAL); }, IPC_CONNECT_INTERVAL);
} }
@ -244,11 +253,6 @@ Blockchain.prototype.run = function () {
data = data.toString(); data = data.toString();
if (!self.readyCalled && self.client.isReady(data)) { if (!self.readyCalled && self.client.isReady(data)) {
self.readyCalled = true; self.readyCalled = true;
if (self.isDev) {
self.fundAccounts((err) => {
if (err) this.logger.error('Error funding accounts', err);
});
}
if (self.config.proxy) { if (self.config.proxy) {
await self.setupProxy(); await self.setupProxy();
} }
@ -280,14 +284,50 @@ Blockchain.prototype.run = function () {
}; };
Blockchain.prototype.fundAccounts = function(cb) { Blockchain.prototype.fundAccounts = function(cb) {
DevFunds.new({blockchainConfig: this.config}).then(devFunds => { if(this.isDev && this.devFunds){
devFunds.fundAccounts(this.client.needKeepAlive(), (err) => { this.devFunds.fundAccounts((err) => {
cb(err); cb(err);
}); });
}); }
};
Blockchain.prototype.startRegularTxs = function(cb) {
if (this.client.needKeepAlive() && this.devFunds){
return this.devFunds.startRegularTxs(() => {
this.logger.info('Regular transactions have been enabled.');
cb();
});
}
cb();
};
Blockchain.prototype.stopRegularTxs = function(cb) {
if (this.client.needKeepAlive() && this.devFunds){
return this.devFunds.stopRegularTxs(() => {
this.logger.info('Regular transactions have been disabled.');
cb();
});
}
cb();
}; };
Blockchain.prototype.readyCallback = function () { Blockchain.prototype.readyCallback = function () {
if (this.isDev) {
if(!this.devFunds) {
DevFunds.new({blockchainConfig: this.config}).then(devFunds => {
this.devFunds = devFunds;
this.fundAccounts((err) => {
if (err) this.logger.error('Error funding accounts', err);
});
});
}
else {
this.fundAccounts((err) => {
if (err) this.logger.error('Error funding accounts', err);
});
}
}
if (this.onReadyCallback) { if (this.onReadyCallback) {
this.onReadyCallback(); this.onReadyCallback();
} }
@ -450,7 +490,7 @@ Blockchain.prototype.initChainAndGetAddress = function (callback) {
}); });
}; };
var BlockchainClient = function(userConfig, clientName, env, certOptions, onReadyCallback, onExitCallback, logger, _events, _isStandalone) { var BlockchainClient = function(userConfig, clientName, env, certOptions, onReadyCallback, onExitCallback, logger, _events, isStandalone) {
if ((userConfig === {} || JSON.stringify(userConfig) === '{"enabled":true}') && env !== 'development') { if ((userConfig === {} || JSON.stringify(userConfig) === '{"enabled":true}') && env !== 'development') {
logger.info("===> " + __("warning: running default config on a non-development environment")); logger.info("===> " + __("warning: running default config on a non-development environment"));
} }
@ -478,6 +518,7 @@ var BlockchainClient = function(userConfig, clientName, env, certOptions, onRead
userConfig.onExitCallback = onExitCallback; userConfig.onExitCallback = onExitCallback;
userConfig.logger = logger; userConfig.logger = logger;
userConfig.certOptions = certOptions; userConfig.certOptions = certOptions;
userConfig.isStandalone = isStandalone;
return new Blockchain(userConfig, clientClass); return new Blockchain(userConfig, clientClass);
}; };

View File

@ -52,4 +52,10 @@ process.on('message', (msg) => {
blockchainProcess = new BlockchainProcess(msg.options); blockchainProcess = new BlockchainProcess(msg.options);
return blockchainProcess.send({result: constants.blockchain.initiated}); return blockchainProcess.send({result: constants.blockchain.initiated});
} }
else if(msg.action === constants.blockchain.startRegularTxs){
blockchainProcess.blockchain.startRegularTxs(() => {});
}
else if(msg.action === constants.blockchain.stopRegularTxs){
blockchainProcess.blockchain.stopRegularTxs(() => {});
}
}); });

View File

@ -38,7 +38,8 @@ class BlockchainProcessLauncher {
env: this.env, env: this.env,
isDev: this.isDev, isDev: this.isDev,
locale: this.locale, locale: this.locale,
certOptions: this.embark.config.webServerConfig.certOptions certOptions: this.embark.config.webServerConfig.certOptions,
events: this.events
} }
}); });
@ -62,6 +63,14 @@ class BlockchainProcessLauncher {
this.events.on('logs:ethereum:disable', () => { this.events.on('logs:ethereum:disable', () => {
this.blockchainProcess.silent = true; this.blockchainProcess.silent = true;
}); });
this.events.on('regularTxs:start', () => {
this.blockchainProcess.send({action: constants.blockchain.startRegularTxs});
});
this.events.on('regularTxs:stop', () => {
this.blockchainProcess.send({action: constants.blockchain.stopRegularTxs});
});
this.events.on('exit', () => { this.events.on('exit', () => {
this.blockchainProcess.send('exit'); this.blockchainProcess.send('exit');

View File

@ -60,20 +60,28 @@ class DevFunds {
this.web3.eth.sendTransaction({value: "1000000000000000", to: "0xA2817254cb8E7b6269D1689c3E0eBadbB78889d1", from: this.web3.eth.defaultAccount}); this.web3.eth.sendTransaction({value: "1000000000000000", to: "0xA2817254cb8E7b6269D1689c3E0eBadbB78889d1", from: this.web3.eth.defaultAccount});
} }
_regularTxs(cb) { startRegularTxs(cb) {
const self = this; const self = this;
self.web3.eth.net.getId().then((networkId) => { self.web3.eth.net.getId().then((networkId) => {
self.networkId = networkId; self.networkId = networkId;
if (self.networkId !== 1337) { if (self.networkId !== 1337) {
return; return;
} }
setInterval(function() { self._sendTx(); }, 1500); this.regularTxsInt = setInterval(function() { self._sendTx(); }, 1500);
if (cb) { if (cb) {
cb(); cb();
} }
}); });
} }
stopRegularTxs(cb) {
if(this.regularTxsInt) {
clearInterval(this.regularTxsInt);
return cb();
}
cb('Regular txs not enabled.');
}
_fundAccounts(balance, cb) { _fundAccounts(balance, cb) {
async.each(this.accounts, (account, next) => { async.each(this.accounts, (account, next) => {
this.web3.eth.getBalance(account).then(currBalance => { this.web3.eth.getBalance(account).then(currBalance => {
@ -113,13 +121,12 @@ class DevFunds {
}, cb); }, cb);
} }
fundAccounts(pingForever = false, cb) { fundAccounts(cb) {
if (!this.web3) { if (!this.web3) {
return cb(); return cb();
} }
async.waterfall([ async.waterfall([
(next) => { (next) => {
if (pingForever) this._regularTxs();
this._fundAccounts(this.balance, next); this._fundAccounts(this.balance, next);
} }
], cb); ], cb);

View File

@ -1,4 +1,4 @@
EmbarkJS.Blockchain.autoEnable = <%= autoEnable %>; EmbarkJS.Blockchain.autoEnable = <%= autoEnable %>;
EmbarkJS.Blockchain.connect(<%- connectionList %>, {warnAboutMetamask: <%= warnAboutMetamask %>}, function(err) { EmbarkJS.Blockchain.connect(<%- connectionList %>, {warnAboutMetamask: <%= warnAboutMetamask %>, blockchainClient: "<%= blockchainClient %>"}, function(err) {
<%- done %> <%- done %>
}); });

View File

@ -26,6 +26,7 @@ class CodeGenerator {
this.storageConfig = embark.config.storageConfig || {}; this.storageConfig = embark.config.storageConfig || {};
this.communicationConfig = embark.config.communicationConfig || {}; this.communicationConfig = embark.config.communicationConfig || {};
this.namesystemConfig = embark.config.namesystemConfig || {}; this.namesystemConfig = embark.config.namesystemConfig || {};
this.webServerConfig = embark.config.webServerConfig || {};
this.env = options.env || 'development'; this.env = options.env || 'development';
this.plugins = options.plugins; this.plugins = options.plugins;
this.events = embark.events; this.events = embark.events;
@ -124,7 +125,8 @@ class CodeGenerator {
autoEnable: this.contractsConfig.dappAutoEnable, autoEnable: this.contractsConfig.dappAutoEnable,
connectionList: connectionList, connectionList: connectionList,
done: 'done(err);', done: 'done(err);',
warnAboutMetamask: isDev warnAboutMetamask: isDev,
blockchainClient: this.blockchainConfig.ethereumClientName
}); });
} }

View File

@ -110,7 +110,7 @@ describe('embark.DevFunds', function() {
// provider.injectResult('0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe'); // send tx response // provider.injectResult('0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe'); // send tx response
}); });
devFunds.fundAccounts(devFunds.balance, (errFundAccounts) => { devFunds.fundAccounts((errFundAccounts) => {
assert.equal(errFundAccounts, null); assert.equal(errFundAccounts, null);
// inject response for web3.eth.getAccounts // inject response for web3.eth.getAccounts