From 135fde0a85503402080a5f4b2d69e720906b130b Mon Sep 17 00:00:00 2001 From: emizzle Date: Fri, 21 Dec 2018 18:57:10 +1100 Subject: [PATCH] feat(@embark/core): Disable regular txs until needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- embark-ui/src/actions/index.js | 7 +++ embark-ui/src/containers/AppContainer.js | 43 ++++++++----- embark-ui/src/sagas/index.js | 8 ++- embark-ui/src/services/api.js | 4 ++ embark-ui/src/utils/utils.js | 12 +++- src/lib/constants.json | 4 +- src/lib/modules/blockchain_listener/index.js | 40 ++++++++++++ .../modules/blockchain_process/blockchain.js | 63 +++++++++++++++---- .../blockchain_process/blockchainProcess.js | 6 ++ .../blockchainProcessLauncher.js | 11 +++- .../modules/blockchain_process/dev_funds.js | 15 +++-- .../code_templates/web3-connector.js.ejs | 2 +- src/lib/modules/code_generator/index.js | 4 +- src/test/devFunds.js | 2 +- 14 files changed, 182 insertions(+), 39 deletions(-) diff --git a/embark-ui/src/actions/index.js b/embark-ui/src/actions/index.js index a17d6ef07..2caf23e3b 100644 --- a/embark-ui/src/actions/index.js +++ b/embark-ui/src/actions/index.js @@ -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'; diff --git a/embark-ui/src/containers/AppContainer.js b/embark-ui/src/containers/AppContainer.js index 2e1d810ad..e38122234 100644 --- a/embark-ui/src/containers/AppContainer.js +++ b/embark-ui/src/containers/AppContainer.js @@ -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 ( - + ); } else if (this.props.credentials.authenticating) { - return ; + return ; } return ( this.toggleTheme()} - currentTheme={this.props.theme}> + logout={this.props.logout} + toggleTheme={() => this.toggleTheme()} + currentTheme={this.props.theme}> {routes} ); @@ -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)); diff --git a/embark-ui/src/sagas/index.js b/embark-ui/src/sagas/index.js index 8f1df6c05..3c21ca5e8 100644 --- a/embark-ui/src/sagas/index.js +++ b/embark-ui/src/sagas/index.js @@ -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) ]); } diff --git a/embark-ui/src/services/api.js b/embark-ui/src/services/api.js index abaef78ab..9db453969 100644 --- a/embark-ui/src/services/api.js +++ b/embark-ui/src/services/api.js @@ -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'); } diff --git a/embark-ui/src/utils/utils.js b/embark-ui/src/utils/utils.js index 51ddd1402..fe1629fbd 100644 --- a/embark-ui/src/utils/utils.js +++ b/embark-ui/src/utils/utils.js @@ -23,8 +23,12 @@ export function ansiToHtml(text) { return convert.toHtml(text.replace(/\n/g,'
')); } +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; diff --git a/src/lib/constants.json b/src/lib/constants.json index 47bfa9a27..3599f9587 100644 --- a/src/lib/constants.json +++ b/src/lib/constants.json @@ -53,7 +53,9 @@ "eth_sendTransaction": "eth_sendTransaction", "eth_sendRawTransaction": "eth_sendRawTransaction", "eth_getTransactionReceipt": "eth_getTransactionReceipt" - } + }, + "startRegularTxs": "startRegularTxs", + "stopRegularTxs": "stopRegularTxs" }, "storage": { "init": "init", diff --git a/src/lib/modules/blockchain_listener/index.js b/src/lib/modules/blockchain_listener/index.js index 4a436db6d..527506313 100644 --- a/src/lib/modules/blockchain_listener/index.js +++ b/src/lib/modules/blockchain_listener/index.js @@ -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; diff --git a/src/lib/modules/blockchain_process/blockchain.js b/src/lib/modules/blockchain_process/blockchain.js index 9758ecd47..9da45f8af 100644 --- a/src/lib/modules/blockchain_process/blockchain.js +++ b/src/lib/modules/blockchain_process/blockchain.js @@ -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); }; diff --git a/src/lib/modules/blockchain_process/blockchainProcess.js b/src/lib/modules/blockchain_process/blockchainProcess.js index e44d72223..b68754976 100644 --- a/src/lib/modules/blockchain_process/blockchainProcess.js +++ b/src/lib/modules/blockchain_process/blockchainProcess.js @@ -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(() => {}); + } }); diff --git a/src/lib/modules/blockchain_process/blockchainProcessLauncher.js b/src/lib/modules/blockchain_process/blockchainProcessLauncher.js index 6bf058b72..e8cdd50c5 100644 --- a/src/lib/modules/blockchain_process/blockchainProcessLauncher.js +++ b/src/lib/modules/blockchain_process/blockchainProcessLauncher.js @@ -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'); diff --git a/src/lib/modules/blockchain_process/dev_funds.js b/src/lib/modules/blockchain_process/dev_funds.js index bc012d0c7..a269b77a3 100644 --- a/src/lib/modules/blockchain_process/dev_funds.js +++ b/src/lib/modules/blockchain_process/dev_funds.js @@ -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); diff --git a/src/lib/modules/code_generator/code_templates/web3-connector.js.ejs b/src/lib/modules/code_generator/code_templates/web3-connector.js.ejs index 69a2625ec..d88ccd659 100644 --- a/src/lib/modules/code_generator/code_templates/web3-connector.js.ejs +++ b/src/lib/modules/code_generator/code_templates/web3-connector.js.ejs @@ -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 %> }); diff --git a/src/lib/modules/code_generator/index.js b/src/lib/modules/code_generator/index.js index c25db16f2..23c2419c4 100644 --- a/src/lib/modules/code_generator/index.js +++ b/src/lib/modules/code_generator/index.js @@ -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 }); } diff --git a/src/test/devFunds.js b/src/test/devFunds.js index 730aad34f..db503a382 100644 --- a/src/test/devFunds.js +++ b/src/test/devFunds.js @@ -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