From 5f254f7325b2b3a74916643c31383196ba012c0b Mon Sep 17 00:00:00 2001 From: ricky Date: Fri, 27 Sep 2019 00:30:36 -0400 Subject: [PATCH] Add advanced setting to enable editing nonce on confirmation screens (#7089) * Add UseNonce toggle * Get the toggle actually working and dispatching * Display nonce field on confirmation page * Remove console.log * Add placeholder * Set customNonceValue * Add nonce key/value to txParams * remove customNonceValue from component state * Use translation file and existing CSS class * Use existing TextField component * Remove console.log * Fix lint nits * Okay this sorta works? * Move nonce toggle to advanced tab * Set min to 0 * Wrap value in Number() * Add customNonceMap * Update custom nonce translation * Update styles * Reset CustomNonce * Fix lint * Get tests passing * Add customNonceValue to defaults * Fix test * Fix comments * Update tests * Use camel case * Ensure custom nonce can only be whole number * Correct font size for custom nonce input * UX improvements for custom nonce feature * Fix advanced-tab-component tests for custom nonce changes * Update title of nonce toggle in settings * Remove unused locale message * Cast custom nonce to string in confirm-transaction-base.component * Handle string conversion and invalid values for custom nonces in handler * Don't call getNonceLock in tx controller if there is a custom nonce * Set nonce details for cases where nonce is customized * Fix incorrectly use value for deciding whether to getnoncelock in approveTransaction * Default nonceLock to empty object in approveTransaction * Reapply use on nonceLock in cases where customNonceValue in approveTransaction. * Show warning message if custom nonce is higher than MetaMask's next nonce * Fix e2e test failure caused by custom nonce and 3box toggle conflict * Update nonce warning message to include the suggested nonce * Handle nextNonce comparison and update logic in lifecycle * Default nonce field to suggested nonce * Clear custom nonce on reject or confirm * Fix bug where nonces are not shown in tx list on self sent transactions * Ensure custom nonce is reset after tx is created in background * Convert customNonceValue to number in approve tranasction controller * Lint fix * Call getNextNonce after updating custom nonce --- app/_locales/en/messages.json | 16 ++++ app/scripts/controllers/preferences.js | 22 +++++ app/scripts/controllers/transactions/index.js | 9 +- app/scripts/metamask-controller.js | 16 ++++ test/e2e/threebox.spec.js | 2 +- .../confirm-detail-row/index.scss | 7 ++ .../transaction-list-item.container.js | 4 +- ui/app/ducks/metamask/metamask.js | 14 ++- .../confirm-transaction-base.component.js | 89 +++++++++++++++++-- .../confirm-transaction-base.container.js | 33 ++++++- ...confirm-transaction-base.container.test.js | 20 +++++ .../advanced-tab/advanced-tab.component.js | 29 ++++++ .../advanced-tab/advanced-tab.container.js | 4 + .../tests/advanced-tab-component.test.js | 4 +- .../tests/advanced-tab-container.test.js | 3 + ui/app/selectors/selectors.js | 10 +++ ui/app/store/actions.js | 55 ++++++++++++ 17 files changed, 319 insertions(+), 18 deletions(-) create mode 100644 ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 16c1999990ab..cb6227af602a 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -195,6 +195,18 @@ "blockiesIdenticon": { "message": "Use Blockies Identicon" }, + "nonceField": { + "message": "Customize transaction nonce" + }, + "nonceFieldPlaceholder": { + "message": "Automatically calculate" + }, + "nonceFieldHeading": { + "message": "Custom Nonce" + }, + "nonceFieldDescription": { + "message": "Turn this on to change the nonce (transaction number) on confirmation screens. This is an advanced feature, use cautiously." + }, "browserNotSupported": { "message": "Your Browser is not supported..." }, @@ -851,6 +863,10 @@ "next": { "message": "Next" }, + "nextNonceWarning": { + "message": "Nonce is higher than suggested nonce of $1", + "description": "The next nonce according to MetaMask's internal logic" + }, "noAddressForName": { "message": "No address has been set for this name." }, diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index b3523cc768b0..dfeeaeb82015 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -17,6 +17,7 @@ class PreferencesController { * @property {object} store.accountTokens The tokens stored per account and then per network type * @property {object} store.assetImages Contains assets objects related to assets added * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI + * @property {boolean} store.useNonceField The users preference for nonce field within the UI * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * user wishes to see that feature. * @@ -35,6 +36,7 @@ class PreferencesController { tokens: [], suggestedTokens: {}, useBlockie: false, + useNonceField: false, // WARNING: Do not use feature flags for security-sensitive things. // Feature flag toggling is available in the global namespace @@ -89,6 +91,16 @@ class PreferencesController { this.store.updateState({ useBlockie: val }) } + /** + * Setter for the `useNonceField` property + * + * @param {boolean} val Whether or not the user prefers to set nonce + * + */ + setUseNonceField (val) { + this.store.updateState({ useNonceField: val }) + } + /** * Setter for the `participateInMetaMetrics` property * @@ -204,6 +216,16 @@ class PreferencesController { return this.store.getState().useBlockie } + /** + * Getter for the `getUseNonceField` property + * + * @returns {boolean} this.store.getUseNonceField + * + */ + getUseNonceField () { + return this.store.getState().useNonceField + } + /** * Setter for the `currentLocale` property * diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 008d1c72dfa1..c69b82673d03 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -373,14 +373,21 @@ class TransactionController extends EventEmitter { const txMeta = this.txStateManager.getTx(txId) const fromAddress = txMeta.txParams.from // wait for a nonce + let { customNonceValue = null } = txMeta + customNonceValue = Number(customNonceValue) nonceLock = await this.nonceTracker.getNonceLock(fromAddress) // add nonce to txParams // if txMeta has lastGasPrice then it is a retry at same nonce with higher // gas price transaction and their for the nonce should not be calculated const nonce = txMeta.lastGasPrice ? txMeta.txParams.nonce : nonceLock.nextNonce - txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16)) + const customOrNonce = customNonceValue || nonce + + txMeta.txParams.nonce = ethUtil.addHexPrefix(customOrNonce.toString(16)) // add nonce debugging information to txMeta txMeta.nonceDetails = nonceLock.nonceDetails + if (customNonceValue) { + txMeta.nonceDetails.customNonceValue = customNonceValue + } this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction') // sign transaction const rawTx = await this.signTransaction(txId) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2986add10f21..d6c19deb859a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -440,6 +440,7 @@ module.exports = class MetamaskController extends EventEmitter { getState: (cb) => cb(null, this.getState()), setCurrentCurrency: this.setCurrentCurrency.bind(this), setUseBlockie: this.setUseBlockie.bind(this), + setUseNonceField: this.setUseNonceField.bind(this), setParticipateInMetaMetrics: this.setParticipateInMetaMetrics.bind(this), setMetaMetricsSendCount: this.setMetaMetricsSendCount.bind(this), setFirstTimeFlowType: this.setFirstTimeFlowType.bind(this), @@ -519,6 +520,7 @@ module.exports = class MetamaskController extends EventEmitter { getFilteredTxList: nodeify(txController.getFilteredTxList, txController), isNonceTaken: nodeify(txController.isNonceTaken, txController), estimateGas: nodeify(this.estimateGas, this), + getPendingNonce: nodeify(this.getPendingNonce, this), // messageManager signMessage: nodeify(this.signMessage, this), @@ -1727,6 +1729,20 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * Sets whether or not to use the nonce field. + * @param {boolean} val - True for nonce field, false for not nonce field. + * @param {Function} cb - A callback function called when complete. + */ + setUseNonceField (val, cb) { + try { + this.preferencesController.setUseNonceField(val) + cb(null) + } catch (err) { + cb(err) + } + } + /** * Sets whether or not the user will have usage data tracked with MetaMetrics * @param {boolean} bool - True for users that wish to opt-in, false for users that wish to remain out. diff --git a/test/e2e/threebox.spec.js b/test/e2e/threebox.spec.js index eb0eebcb2fe3..a60f76e4134d 100644 --- a/test/e2e/threebox.spec.js +++ b/test/e2e/threebox.spec.js @@ -116,7 +116,7 @@ describe('MetaMask', function () { await advancedButton.click() const threeBoxToggle = await findElements(driver, By.css('.toggle-button')) - const threeBoxToggleButton = await threeBoxToggle[3].findElement(By.css('div')) + const threeBoxToggleButton = await threeBoxToggle[4].findElement(By.css('div')) await threeBoxToggleButton.click() }) diff --git a/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss index 1672ef8c60e4..80eb9cce6ae6 100644 --- a/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss @@ -47,4 +47,11 @@ .advanced-gas-inputs__gas-edit-rows { margin-bottom: 16px; } + + .custom-nonce-input { + input { + width: 90px; + font-size: 1rem; + } + } } diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js index 27b9e2608c10..c3cf0295bcc7 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js @@ -21,10 +21,10 @@ const mapStateToProps = (state, ownProps) => { const { showFiatInTestnets } = preferencesSelector(state) const isMainnet = getIsMainnet(state) const { transactionGroup: { primaryTransaction } = {} } = ownProps - const { txParams: { gas: gasLimit, gasPrice, data, to } = {} } = primaryTransaction + const { txParams: { gas: gasLimit, gasPrice, data } = {}, transactionCategory } = primaryTransaction const selectedAddress = getSelectedAddress(state) const selectedAccountBalance = accounts[selectedAddress].balance - const isDeposit = selectedAddress === to + const isDeposit = transactionCategory === 'incoming' const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget) const { rpcPrefs } = selectRpcInfo || {} diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index f1fa7cd030fe..74e71df21f25 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -25,6 +25,7 @@ function reduceMetamask (state, action) { tokenExchangeRates: {}, tokens: [], pendingTokens: {}, + customNonceValue: '', send: { gasLimit: null, gasPrice: null, @@ -57,6 +58,7 @@ function reduceMetamask (state, action) { knownMethodData: {}, participateInMetaMetrics: null, metaMetricsSendCount: 0, + nextNonce: null, }, state.metamask) switch (action.type) { @@ -188,7 +190,10 @@ function reduceMetamask (state, action) { gasLimit: action.value, }, }) - + case actions.UPDATE_CUSTOM_NONCE: + return extend(metamaskState, { + customNonceValue: action.value, + }) case actions.UPDATE_GAS_PRICE: return extend(metamaskState, { send: { @@ -412,6 +417,13 @@ function reduceMetamask (state, action) { }) } + case actions.SET_NEXT_NONCE: { + console.log('action.value', action.value) + return extend(metamaskState, { + nextNonce: action.value, + }) + } + default: return metamaskState diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index 87771502dce4..8265dd20b7fb 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -15,6 +15,7 @@ import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transa import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display' import { PRIMARY, SECONDARY } from '../../helpers/constants/common' import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs' +import TextField from '../../components/ui/text-field' export default class ConfirmTransactionBase extends Component { static contextTypes = { @@ -50,6 +51,9 @@ export default class ConfirmTransactionBase extends Component { isTxReprice: PropTypes.bool, methodData: PropTypes.object, nonce: PropTypes.string, + useNonceField: PropTypes.bool, + customNonceValue: PropTypes.string, + updateCustomNonce: PropTypes.func, assetImage: PropTypes.string, sendTransaction: PropTypes.func, showCustomizeGasModal: PropTypes.func, @@ -96,11 +100,14 @@ export default class ConfirmTransactionBase extends Component { insufficientBalance: PropTypes.bool, hideFiatConversion: PropTypes.bool, transactionCategory: PropTypes.string, + getNextNonce: PropTypes.func, + nextNonce: PropTypes.number, } state = { submitting: false, submitError: null, + submitWarning: '', } componentDidUpdate (prevProps) { @@ -109,11 +116,21 @@ export default class ConfirmTransactionBase extends Component { showTransactionConfirmedModal, history, clearConfirmTransaction, + nextNonce, + customNonceValue, } = this.props const { transactionStatus: prevTxStatus } = prevProps const statusUpdated = transactionStatus !== prevTxStatus const txDroppedOrConfirmed = transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS + if (nextNonce !== prevProps.nextNonce || customNonceValue !== prevProps.customNonceValue) { + if (customNonceValue > nextNonce) { + this.setState({ submitWarning: this.context.t('nextNonceWarning', [nextNonce]) }) + } else { + this.setState({ submitWarning: '' }) + } + } + if (statusUpdated && txDroppedOrConfirmed) { showTransactionConfirmedModal({ onSubmit: () => { @@ -204,11 +221,16 @@ export default class ConfirmTransactionBase extends Component { hexTransactionFee, hexTransactionTotal, hideDetails, + useNonceField, + customNonceValue, + updateCustomNonce, advancedInlineGasShown, customGas, insufficientBalance, updateGasAndCalculate, hideFiatConversion, + nextNonce, + getNextNonce, } = this.props if (hideDetails) { @@ -240,7 +262,7 @@ export default class ConfirmTransactionBase extends Component { : null } -
+
+ {useNonceField ?
+
+
+ { this.context.t('nonceFieldHeading') } +
+
+ { + if (!value.length || Number(value) < 0) { + updateCustomNonce('') + } else { + updateCustomNonce(String(Math.floor(value))) + } + getNextNonce() + }} + fullWidth + margin="dense" + value={customNonceValue || nextNonce || ''} + /> +
+
+
: null}
) ) @@ -347,7 +394,17 @@ export default class ConfirmTransactionBase extends Component { handleCancel () { const { metricsEvent } = this.context - const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, actionKey, txData: { origin }, methodData = {} } = this.props + const { + onCancel, + txData, + cancelTransaction, + history, + clearConfirmTransaction, + actionKey, + txData: { origin }, + methodData = {}, + updateCustomNonce, + } = this.props metricsEvent({ eventOpts: { @@ -361,6 +418,7 @@ export default class ConfirmTransactionBase extends Component { origin, }, }) + updateCustomNonce('') if (onCancel) { onCancel(txData) } else { @@ -374,7 +432,19 @@ export default class ConfirmTransactionBase extends Component { handleSubmit () { const { metricsEvent } = this.context - const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, actionKey, metaMetricsSendCount = 0, setMetaMetricsSendCount, methodData = {} } = this.props + const { + txData: { origin }, + sendTransaction, + clearConfirmTransaction, + txData, + history, + onSubmit, + actionKey, + metaMetricsSendCount = 0, + setMetaMetricsSendCount, + methodData = {}, + updateCustomNonce, + } = this.props const { submitting } = this.state if (submitting) { @@ -406,6 +476,7 @@ export default class ConfirmTransactionBase extends Component { this.setState({ submitting: false, }) + updateCustomNonce('') }) } else { sendTransaction(txData) @@ -415,6 +486,7 @@ export default class ConfirmTransactionBase extends Component { submitting: false, }, () => { history.push(DEFAULT_ROUTE) + updateCustomNonce('') }) }) .catch(error => { @@ -493,7 +565,7 @@ export default class ConfirmTransactionBase extends Component { } componentDidMount () { - const { txData: { origin, id } = {}, cancelTransaction } = this.props + const { txData: { origin, id } = {}, cancelTransaction, getNextNonce } = this.props const { metricsEvent } = this.context metricsEvent({ eventOpts: { @@ -521,6 +593,8 @@ export default class ConfirmTransactionBase extends Component { cancelTransaction({ id }) } } + + getNextNonce() } render () { @@ -543,12 +617,13 @@ export default class ConfirmTransactionBase extends Component { contentComponent, onEdit, nonce, + customNonceValue, assetImage, warning, unapprovedTxCount, transactionCategory, } = this.props - const { submitting, submitError } = this.state + const { submitting, submitError, submitWarning } = this.state const { name } = methodData const { valid, errorKey } = this.getErrorKey() @@ -572,13 +647,13 @@ export default class ConfirmTransactionBase extends Component { detailsComponent={this.renderDetails()} dataComponent={this.renderData()} contentComponent={contentComponent} - nonce={nonce} + nonce={customNonceValue || nonce} unapprovedTxCount={unapprovedTxCount} assetImage={assetImage} identiconAddress={identiconAddress} errorMessage={errorMessage || submitError} errorKey={propsErrorKey || errorKey} - warning={warning} + warning={warning || submitWarning} totalTx={totalTx} positionOfCurrentTx={positionOfCurrentTx} nextTxId={nextTxId} diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index 700883f17630..6ffa13bd0dd8 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -8,7 +8,17 @@ import { clearConfirmTransaction, } from '../../ducks/confirm-transaction/confirm-transaction.duck' -import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal, setMetaMetricsSendCount, updateTransaction } from '../../store/actions' +import { + updateCustomNonce, + clearSend, + cancelTx, + cancelTxs, + updateAndApproveTx, + showModal, + setMetaMetricsSendCount, + updateTransaction, + getNextNonce, +} from '../../store/actions' import { INSUFFICIENT_FUNDS_ERROR_KEY, GAS_LIMIT_TOO_LOW_ERROR_KEY, @@ -18,7 +28,7 @@ import { isBalanceSufficient, calcGasTotal } from '../send/send.utils' import { conversionGreaterThan } from '../../helpers/utils/conversion-util' import { MIN_GAS_LIMIT_DEC } from '../send/send.constants' import { checksumAddress, addressSlicer, valuesFor } from '../../helpers/utils/util' -import { getMetaMaskAccounts, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet, getKnownMethodData } from '../../selectors/selectors' +import { getMetaMaskAccounts, getCustomNonceValue, getUseNonceField, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet, getKnownMethodData } from '../../selectors/selectors' import { transactionFeeSelector } from '../../selectors/confirm-transaction' const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { @@ -28,6 +38,12 @@ const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { } }, {}) +let customNonceValue = '' +const customNonceMerge = txData => customNonceValue ? ({ + ...txData, + customNonceValue, +}) : txData + const mapStateToProps = (state, ownProps) => { const { toAddress: propsToAddress, match: { params = {} } } = ownProps const { id: paramsTransactionId } = params @@ -45,6 +61,7 @@ const mapStateToProps = (state, ownProps) => { network, unapprovedTxs, metaMetricsSendCount, + nextNonce, } = metamask const { tokenData, @@ -146,16 +163,23 @@ const mapStateToProps = (state, ownProps) => { gasPrice, }, advancedInlineGasShown: getAdvancedInlineGasShown(state), + useNonceField: getUseNonceField(state), + customNonceValue: getCustomNonceValue(state), insufficientBalance, hideSubtitle: (!isMainnet && !showFiatInTestnets), hideFiatConversion: (!isMainnet && !showFiatInTestnets), metaMetricsSendCount, transactionCategory, + nextNonce, } } -const mapDispatchToProps = dispatch => { +export const mapDispatchToProps = dispatch => { return { + updateCustomNonce: value => { + customNonceValue = value + dispatch(updateCustomNonce(value)) + }, clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), clearSend: () => dispatch(clearSend()), showTransactionConfirmedModal: ({ onSubmit }) => { @@ -172,8 +196,9 @@ const mapDispatchToProps = dispatch => { }, cancelTransaction: ({ id }) => dispatch(cancelTx({ id })), cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)), - sendTransaction: txData => dispatch(updateAndApproveTx(txData)), + sendTransaction: txData => dispatch(updateAndApproveTx(customNonceMerge(txData))), setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)), + getNextNonce: () => dispatch(getNextNonce()), } } diff --git a/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js new file mode 100644 index 000000000000..a8045d0e086d --- /dev/null +++ b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js @@ -0,0 +1,20 @@ +import assert from 'assert' +import { mapDispatchToProps } from '../confirm-transaction-base.container' + +describe('Confirm Transaction Base Container', () => { + it('should map dispatch to props correctly', () => { + const props = mapDispatchToProps(() => 'mockDispatch') + + assert.ok(typeof props.updateCustomNonce === 'function') + assert.ok(typeof props.clearConfirmTransaction === 'function') + assert.ok(typeof props.clearSend === 'function') + assert.ok(typeof props.showTransactionConfirmedModal === 'function') + assert.ok(typeof props.showCustomizeGasModal === 'function') + assert.ok(typeof props.updateGasAndCalculate === 'function') + assert.ok(typeof props.showRejectTransactionsConfirmationModal === 'function') + assert.ok(typeof props.cancelTransaction === 'function') + assert.ok(typeof props.cancelAllTransactions === 'function') + assert.ok(typeof props.sendTransaction === 'function') + assert.ok(typeof props.setMetaMetricsSendCount === 'function') + }) +}) diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js index 55f25607f657..9e7b70a9356d 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js @@ -14,6 +14,8 @@ export default class AdvancedTab extends PureComponent { } static propTypes = { + setUseNonceField: PropTypes.func, + useNonceField: PropTypes.bool, setHexDataFeatureFlag: PropTypes.func, setRpcTarget: PropTypes.func, displayWarning: PropTypes.func, @@ -212,6 +214,32 @@ export default class AdvancedTab extends PureComponent { ) } + renderUseNonceOptIn () { + const { t } = this.context + const { useNonceField, setUseNonceField } = this.props + + return ( +
+
+ { this.context.t('nonceField') } +
+ { t('nonceFieldDescription') } +
+
+
+
+ setUseNonceField(!value)} + offLabel={t('off')} + onLabel={t('on')} + /> +
+
+
+ ) + } + renderAutoLogoutTimeLimit () { const { t } = this.context const { @@ -311,6 +339,7 @@ export default class AdvancedTab extends PureComponent { { this.renderAdvancedGasInputInline() } { this.renderHexDataOptIn() } { this.renderShowConversionInTestnets() } + { this.renderUseNonceOptIn() } { this.renderAutoLogoutTimeLimit() } { this.renderThreeBoxControl() } diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js index f4f3eaee9825..8b64e864fdac 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js @@ -11,6 +11,7 @@ import { setAutoLogoutTimeLimit, setThreeBoxSyncingPermission, turnThreeBoxSyncingOnAndInitialize, + setUseNonceField, } from '../../../store/actions' import {preferencesSelector} from '../../../selectors/selectors' @@ -23,6 +24,7 @@ export const mapStateToProps = state => { } = {}, threeBoxSyncingAllowed, threeBoxDisabled, + useNonceField, } = metamask const { showFiatInTestnets, autoLogoutTimeLimit } = preferencesSelector(state) @@ -34,6 +36,7 @@ export const mapStateToProps = state => { autoLogoutTimeLimit, threeBoxSyncingAllowed, threeBoxDisabled, + useNonceField, } } @@ -44,6 +47,7 @@ export const mapDispatchToProps = dispatch => { displayWarning: warning => dispatch(displayWarning(warning)), showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)), + setUseNonceField: value => dispatch(setUseNonceField(value)), setShowFiatConversionOnTestnetsPreference: value => { return dispatch(setShowFiatConversionOnTestnetsPreference(value)) }, diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js index 914330d05dd4..760f62e548a0 100644 --- a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js +++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js @@ -16,7 +16,7 @@ describe('AdvancedTab Component', () => { } ) - assert.equal(root.find('.settings-page__content-row').length, 8) + assert.equal(root.find('.settings-page__content-row').length, 9) }) it('should update autoLogoutTimeLimit', () => { @@ -32,7 +32,7 @@ describe('AdvancedTab Component', () => { } ) - const autoTimeout = root.find('.settings-page__content-row').at(6) + const autoTimeout = root.find('.settings-page__content-row').at(7) const textField = autoTimeout.find(TextField) textField.props().onChange({ target: { value: 1440 } }) diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js index dab0177ff985..7f5223315f07 100644 --- a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js +++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js @@ -17,6 +17,7 @@ const defaultState = { }, threeBoxSyncingAllowed: false, threeBoxDisabled: false, + useNonceField: false, }, } @@ -31,6 +32,7 @@ describe('AdvancedTab Container', () => { autoLogoutTimeLimit: 0, threeBoxSyncingAllowed: false, threeBoxDisabled: false, + useNonceField: false, } assert.deepEqual(props, expected) @@ -46,5 +48,6 @@ describe('AdvancedTab Container', () => { assert.ok(typeof props.setAdvancedInlineGasFeatureFlag === 'function') assert.ok(typeof props.setShowFiatConversionOnTestnetsPreference === 'function') assert.ok(typeof props.setAutoLogoutTimeLimit === 'function') + assert.ok(typeof props.setUseNonceField === 'function') }) }) diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 96c646caf0c8..943ceba0ce59 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -45,6 +45,8 @@ const selectors = { getNetworkIdentifier, isBalanceCached, getAdvancedInlineGasShown, + getUseNonceField, + getCustomNonceValue, getIsMainnet, getCurrentNetworkId, getSelectedAsset, @@ -341,6 +343,14 @@ function getAdvancedInlineGasShown (state) { return Boolean(state.metamask.featureFlags.advancedInlineGas) } +function getUseNonceField (state) { + return Boolean(state.metamask.useNonceField) +} + +function getCustomNonceValue (state) { + return String(state.metamask.customNonceValue) +} + function getMetaMetricState (state) { return { network: getCurrentNetworkId(state), diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index ee4f577cd9b7..cb4d4f41f66f 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -178,6 +178,9 @@ var actions = { VIEW_PENDING_TX: 'VIEW_PENDING_TX', updateTransactionParams, UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS', + setNextNonce, + SET_NEXT_NONCE: 'SET_NEXT_NONCE', + getNextNonce, // send screen UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', @@ -298,6 +301,10 @@ var actions = { SET_USE_BLOCKIE: 'SET_USE_BLOCKIE', setUseBlockie, + SET_USE_NONCEFIELD: 'SET_USE_NONCEFIELD', + setUseNonceField, + UPDATE_CUSTOM_NONCE: 'UPDATE_CUSTOM_NONCE', + updateCustomNonce, SET_PARTICIPATE_IN_METAMETRICS: 'SET_PARTICIPATE_IN_METAMETRICS', SET_METAMETRICS_SEND_COUNT: 'SET_METAMETRICS_SEND_COUNT', @@ -1063,6 +1070,13 @@ function updateSendAmount (amount) { } } +function updateCustomNonce (value) { + return { + type: actions.UPDATE_CUSTOM_NONCE, + value: value, + } +} + function updateSendMemo (memo) { return { type: actions.UPDATE_SEND_MEMO, @@ -1208,6 +1222,7 @@ function updateAndApproveTx (txData) { dispatch(actions.clearSend()) dispatch(actions.completedTx(txData.id)) dispatch(actions.hideLoadingIndication()) + dispatch(actions.updateCustomNonce('')) dispatch(closeCurrentNotificationWindow()) return txData @@ -2591,6 +2606,23 @@ function setUseBlockie (val) { } } +function setUseNonceField (val) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setUseNonceField`) + background.setUseNonceField(val, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + }) + dispatch({ + type: actions.SET_USE_NONCEFIELD, + value: val, + }) + } +} + function updateCurrentLocale (key) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -2888,3 +2920,26 @@ function turnThreeBoxSyncingOnAndInitialize () { await dispatch(initializeThreeBox(true)) } } + +function setNextNonce (nextNonce) { + return { + type: actions.SET_NEXT_NONCE, + value: nextNonce, + } +} + +function getNextNonce () { + return (dispatch, getState) => { + const address = getState().metamask.selectedAddress + return new Promise((resolve, reject) => { + background.getPendingNonce(address, (err, pendingNonce) => { + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + dispatch(setNextNonce(pendingNonce)) + resolve(pendingNonce) + }) + }) + } +}