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])
};
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
export const WATCH_NEW_PROCESS_LOGS = 'WATCH_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 Layout from "../components/Layout";
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 {
@ -16,6 +16,7 @@ import {
plugins as pluginsAction,
listenToServices as listenToServicesAction,
listenToContracts as listenToContractsAction,
initRegularTxs as initRegularTxsAction,
changeTheme, fetchTheme
} from '../actions';
@ -25,6 +26,8 @@ import {
getCredentials, getAuthenticationError, getProcesses, getTheme
} from '../reducers/selectors';
const ENABLE_REGULAR_TXS = 'enableRegularTxs';
class AppContainer extends Component {
componentDidMount() {
this.props.fetchCredentials();
@ -50,26 +53,28 @@ class AppContainer extends Component {
const queryToken = getQueryToken(this.props.location);
if (queryToken && !(queryToken === this.props.credentials.token &&
this.props.credentials.host === DEFAULT_HOST)) {
this.props.credentials.host === DEFAULT_HOST)) {
return true;
}
if (!this.props.credentials.authenticated &&
this.props.credentials.host &&
this.props.credentials.token) {
this.props.credentials.host &&
this.props.credentials.token) {
return true;
}
return false;
}
componentDidUpdate(){
componentDidUpdate() {
if (this.requireAuthentication()) {
this.doAuthenticate();
}
if (getQueryToken(this.props.location) &&
(!this.props.credentials.authenticating ||
const enableRegularTxs = !!getQueryParam(this.props.location, ENABLE_REGULAR_TXS);
if (getQueryToken(this.props.location) &&
(!this.props.credentials.authenticating ||
this.props.credentials.authenticated)) {
this.props.history.replace(stripQueryToken(this.props.location));
}
@ -80,6 +85,10 @@ class AppContainer extends Component {
this.props.listenToServices();
this.props.fetchPlugins();
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() {
if (this.shouldRenderLogin()) {
return (
<Login credentials={this.props.credentials}
authenticate={this.props.authenticate}
error={this.props.authenticationError} />
<Login credentials={this.props.credentials}
authenticate={this.props.authenticate}
error={this.props.authenticationError} />
);
} else if (this.props.credentials.authenticating) {
return <React.Fragment/>;
return <React.Fragment />;
}
return (
<Layout location={this.props.location}
logout={this.props.logout}
toggleTheme={() => this.toggleTheme()}
currentTheme={this.props.theme}>
logout={this.props.logout}
toggleTheme={() => this.toggleTheme()}
currentTheme={this.props.theme}>
<React.Fragment>{routes}</React.Fragment>
</Layout>
);
@ -148,7 +157,8 @@ AppContainer.propTypes = {
fetchTheme: PropTypes.func,
history: PropTypes.object,
listenToServices: PropTypes.func,
listenToContracts: PropTypes.func
listenToContracts: PropTypes.func,
initRegularTxs: PropTypes.func
};
function mapStateToProps(state) {
@ -173,6 +183,7 @@ export default withRouter(connect(
fetchPlugins: pluginsAction.request,
changeTheme: changeTheme.request,
fetchTheme: fetchTheme.request,
listenToContracts: listenToContractsAction
listenToContracts: listenToContractsAction,
initRegularTxs: initRegularTxsAction.request
},
)(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 toggleBreakpoint = doRequest.bind(null, actions.toggleBreakpoint, api.toggleBreakpoint);
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 saveCredentials = doRequest.bind(null, actions.saveCredentials, storage.saveCredentials);
@ -343,6 +344,10 @@ export function *watchRemoveEditorTabsSuccess() {
yield takeEvery(actions.REMOVE_EDITOR_TABS[actions.SUCCESS], fetchEditorTabs);
}
export function *watchInitRegularTxs() {
yield takeEvery(actions.INIT_REGULAR_TXS[actions.REQUEST], initRegularTxs);
}
function createChannel(socket) {
return eventChannel(emit => {
socket.onmessage = ((message) => {
@ -585,6 +590,7 @@ export default function *root() {
fork(watchRemoveEditorTabsSuccess),
fork(watchPostFileSuccess),
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});
}
export function initRegularTxs(payload) {
return get('/regular-txs', {params: payload, credentials: payload.credentials});
}
export function listenToDebugger(credentials) {
return websocket(credentials, '/debugger');
}

View File

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

View File

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

View File

@ -26,6 +26,9 @@ class BlockchainListener {
if (this.ipc.isServer()) {
this._listenToBlockchainLogs();
this._listenToCommands();
this._registerConsoleCommands();
this._registerApiEndpoint();
}
}
@ -40,6 +43,43 @@ class BlockchainListener {
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;

View File

@ -65,6 +65,8 @@ var Blockchain = function(userConfig, clientClass) {
proxy: this.userConfig.proxy
};
this.devFunds = null;
if (this.userConfig.accounts) {
const nodeAccounts = this.userConfig.accounts.find(account => account.nodeAccounts);
if (nodeAccounts) {
@ -120,7 +122,7 @@ Blockchain.prototype.initStandaloneProcess = function () {
if (this.isStandalone) {
// 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)
this.events.on('log', (logLevel, message) => {
this.logger.events.on('log', (logLevel, message) => {
if (this.ipc.connected) {
this.ipc.request('blockchain:log', {logLevel, message});
}
@ -133,7 +135,14 @@ Blockchain.prototype.initStandaloneProcess = function () {
// `embark run` without restarting `embark blockchain`)
setInterval(() => {
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);
}
@ -244,11 +253,6 @@ Blockchain.prototype.run = function () {
data = data.toString();
if (!self.readyCalled && self.client.isReady(data)) {
self.readyCalled = true;
if (self.isDev) {
self.fundAccounts((err) => {
if (err) this.logger.error('Error funding accounts', err);
});
}
if (self.config.proxy) {
await self.setupProxy();
}
@ -280,14 +284,50 @@ Blockchain.prototype.run = function () {
};
Blockchain.prototype.fundAccounts = function(cb) {
DevFunds.new({blockchainConfig: this.config}).then(devFunds => {
devFunds.fundAccounts(this.client.needKeepAlive(), (err) => {
if(this.isDev && this.devFunds){
this.devFunds.fundAccounts((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 () {
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) {
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') {
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.logger = logger;
userConfig.certOptions = certOptions;
userConfig.isStandalone = isStandalone;
return new Blockchain(userConfig, clientClass);
};

View File

@ -52,4 +52,10 @@ process.on('message', (msg) => {
blockchainProcess = new BlockchainProcess(msg.options);
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,
isDev: this.isDev,
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.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.blockchainProcess.send('exit');

View File

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

View File

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

View File

@ -26,6 +26,7 @@ class CodeGenerator {
this.storageConfig = embark.config.storageConfig || {};
this.communicationConfig = embark.config.communicationConfig || {};
this.namesystemConfig = embark.config.namesystemConfig || {};
this.webServerConfig = embark.config.webServerConfig || {};
this.env = options.env || 'development';
this.plugins = options.plugins;
this.events = embark.events;
@ -124,7 +125,8 @@ class CodeGenerator {
autoEnable: this.contractsConfig.dappAutoEnable,
connectionList: connectionList,
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
});
devFunds.fundAccounts(devFunds.balance, (errFundAccounts) => {
devFunds.fundAccounts((errFundAccounts) => {
assert.equal(errFundAccounts, null);
// inject response for web3.eth.getAccounts