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
feature/default_network_editable
ricky 5 years ago committed by Dan J Miller
parent 970e90ea70
commit 5f254f7325
  1. 16
      app/_locales/en/messages.json
  2. 22
      app/scripts/controllers/preferences.js
  3. 9
      app/scripts/controllers/transactions/index.js
  4. 16
      app/scripts/metamask-controller.js
  5. 2
      test/e2e/threebox.spec.js
  6. 7
      ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss
  7. 4
      ui/app/components/app/transaction-list-item/transaction-list-item.container.js
  8. 14
      ui/app/ducks/metamask/metamask.js
  9. 89
      ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
  10. 33
      ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
  11. 20
      ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js
  12. 29
      ui/app/pages/settings/advanced-tab/advanced-tab.component.js
  13. 4
      ui/app/pages/settings/advanced-tab/advanced-tab.container.js
  14. 4
      ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js
  15. 3
      ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js
  16. 10
      ui/app/selectors/selectors.js
  17. 55
      ui/app/store/actions.js

@ -195,6 +195,18 @@
"blockiesIdenticon": { "blockiesIdenticon": {
"message": "Use Blockies Identicon" "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": { "browserNotSupported": {
"message": "Your Browser is not supported..." "message": "Your Browser is not supported..."
}, },
@ -851,6 +863,10 @@
"next": { "next": {
"message": "Next" "message": "Next"
}, },
"nextNonceWarning": {
"message": "Nonce is higher than suggested nonce of $1",
"description": "The next nonce according to MetaMask's internal logic"
},
"noAddressForName": { "noAddressForName": {
"message": "No address has been set for this name." "message": "No address has been set for this name."
}, },

@ -17,6 +17,7 @@ class PreferencesController {
* @property {object} store.accountTokens The tokens stored per account and then per network type * @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 {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.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 * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature. * user wishes to see that feature.
* *
@ -35,6 +36,7 @@ class PreferencesController {
tokens: [], tokens: [],
suggestedTokens: {}, suggestedTokens: {},
useBlockie: false, useBlockie: false,
useNonceField: false,
// WARNING: Do not use feature flags for security-sensitive things. // WARNING: Do not use feature flags for security-sensitive things.
// Feature flag toggling is available in the global namespace // Feature flag toggling is available in the global namespace
@ -89,6 +91,16 @@ class PreferencesController {
this.store.updateState({ useBlockie: val }) 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 * Setter for the `participateInMetaMetrics` property
* *
@ -204,6 +216,16 @@ class PreferencesController {
return this.store.getState().useBlockie 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 * Setter for the `currentLocale` property
* *

@ -373,14 +373,21 @@ class TransactionController extends EventEmitter {
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
const fromAddress = txMeta.txParams.from const fromAddress = txMeta.txParams.from
// wait for a nonce // wait for a nonce
let { customNonceValue = null } = txMeta
customNonceValue = Number(customNonceValue)
nonceLock = await this.nonceTracker.getNonceLock(fromAddress) nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams // add nonce to txParams
// if txMeta has lastGasPrice then it is a retry at same nonce with higher // 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 // gas price transaction and their for the nonce should not be calculated
const nonce = txMeta.lastGasPrice ? txMeta.txParams.nonce : nonceLock.nextNonce 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 // add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails txMeta.nonceDetails = nonceLock.nonceDetails
if (customNonceValue) {
txMeta.nonceDetails.customNonceValue = customNonceValue
}
this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction') this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction')
// sign transaction // sign transaction
const rawTx = await this.signTransaction(txId) const rawTx = await this.signTransaction(txId)

@ -440,6 +440,7 @@ module.exports = class MetamaskController extends EventEmitter {
getState: (cb) => cb(null, this.getState()), getState: (cb) => cb(null, this.getState()),
setCurrentCurrency: this.setCurrentCurrency.bind(this), setCurrentCurrency: this.setCurrentCurrency.bind(this),
setUseBlockie: this.setUseBlockie.bind(this), setUseBlockie: this.setUseBlockie.bind(this),
setUseNonceField: this.setUseNonceField.bind(this),
setParticipateInMetaMetrics: this.setParticipateInMetaMetrics.bind(this), setParticipateInMetaMetrics: this.setParticipateInMetaMetrics.bind(this),
setMetaMetricsSendCount: this.setMetaMetricsSendCount.bind(this), setMetaMetricsSendCount: this.setMetaMetricsSendCount.bind(this),
setFirstTimeFlowType: this.setFirstTimeFlowType.bind(this), setFirstTimeFlowType: this.setFirstTimeFlowType.bind(this),
@ -519,6 +520,7 @@ module.exports = class MetamaskController extends EventEmitter {
getFilteredTxList: nodeify(txController.getFilteredTxList, txController), getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
isNonceTaken: nodeify(txController.isNonceTaken, txController), isNonceTaken: nodeify(txController.isNonceTaken, txController),
estimateGas: nodeify(this.estimateGas, this), estimateGas: nodeify(this.estimateGas, this),
getPendingNonce: nodeify(this.getPendingNonce, this),
// messageManager // messageManager
signMessage: nodeify(this.signMessage, this), 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 * 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. * @param {boolean} bool - True for users that wish to opt-in, false for users that wish to remain out.

@ -116,7 +116,7 @@ describe('MetaMask', function () {
await advancedButton.click() await advancedButton.click()
const threeBoxToggle = await findElements(driver, By.css('.toggle-button')) 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() await threeBoxToggleButton.click()
}) })

@ -47,4 +47,11 @@
.advanced-gas-inputs__gas-edit-rows { .advanced-gas-inputs__gas-edit-rows {
margin-bottom: 16px; margin-bottom: 16px;
} }
.custom-nonce-input {
input {
width: 90px;
font-size: 1rem;
}
}
} }

@ -21,10 +21,10 @@ const mapStateToProps = (state, ownProps) => {
const { showFiatInTestnets } = preferencesSelector(state) const { showFiatInTestnets } = preferencesSelector(state)
const isMainnet = getIsMainnet(state) const isMainnet = getIsMainnet(state)
const { transactionGroup: { primaryTransaction } = {} } = ownProps 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 selectedAddress = getSelectedAddress(state)
const selectedAccountBalance = accounts[selectedAddress].balance const selectedAccountBalance = accounts[selectedAddress].balance
const isDeposit = selectedAddress === to const isDeposit = transactionCategory === 'incoming'
const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget) const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget)
const { rpcPrefs } = selectRpcInfo || {} const { rpcPrefs } = selectRpcInfo || {}

@ -25,6 +25,7 @@ function reduceMetamask (state, action) {
tokenExchangeRates: {}, tokenExchangeRates: {},
tokens: [], tokens: [],
pendingTokens: {}, pendingTokens: {},
customNonceValue: '',
send: { send: {
gasLimit: null, gasLimit: null,
gasPrice: null, gasPrice: null,
@ -57,6 +58,7 @@ function reduceMetamask (state, action) {
knownMethodData: {}, knownMethodData: {},
participateInMetaMetrics: null, participateInMetaMetrics: null,
metaMetricsSendCount: 0, metaMetricsSendCount: 0,
nextNonce: null,
}, state.metamask) }, state.metamask)
switch (action.type) { switch (action.type) {
@ -188,7 +190,10 @@ function reduceMetamask (state, action) {
gasLimit: action.value, gasLimit: action.value,
}, },
}) })
case actions.UPDATE_CUSTOM_NONCE:
return extend(metamaskState, {
customNonceValue: action.value,
})
case actions.UPDATE_GAS_PRICE: case actions.UPDATE_GAS_PRICE:
return extend(metamaskState, { return extend(metamaskState, {
send: { 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: default:
return metamaskState return metamaskState

@ -15,6 +15,7 @@ import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transa
import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display' import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../helpers/constants/common' import { PRIMARY, SECONDARY } from '../../helpers/constants/common'
import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs' import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs'
import TextField from '../../components/ui/text-field'
export default class ConfirmTransactionBase extends Component { export default class ConfirmTransactionBase extends Component {
static contextTypes = { static contextTypes = {
@ -50,6 +51,9 @@ export default class ConfirmTransactionBase extends Component {
isTxReprice: PropTypes.bool, isTxReprice: PropTypes.bool,
methodData: PropTypes.object, methodData: PropTypes.object,
nonce: PropTypes.string, nonce: PropTypes.string,
useNonceField: PropTypes.bool,
customNonceValue: PropTypes.string,
updateCustomNonce: PropTypes.func,
assetImage: PropTypes.string, assetImage: PropTypes.string,
sendTransaction: PropTypes.func, sendTransaction: PropTypes.func,
showCustomizeGasModal: PropTypes.func, showCustomizeGasModal: PropTypes.func,
@ -96,11 +100,14 @@ export default class ConfirmTransactionBase extends Component {
insufficientBalance: PropTypes.bool, insufficientBalance: PropTypes.bool,
hideFiatConversion: PropTypes.bool, hideFiatConversion: PropTypes.bool,
transactionCategory: PropTypes.string, transactionCategory: PropTypes.string,
getNextNonce: PropTypes.func,
nextNonce: PropTypes.number,
} }
state = { state = {
submitting: false, submitting: false,
submitError: null, submitError: null,
submitWarning: '',
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
@ -109,11 +116,21 @@ export default class ConfirmTransactionBase extends Component {
showTransactionConfirmedModal, showTransactionConfirmedModal,
history, history,
clearConfirmTransaction, clearConfirmTransaction,
nextNonce,
customNonceValue,
} = this.props } = this.props
const { transactionStatus: prevTxStatus } = prevProps const { transactionStatus: prevTxStatus } = prevProps
const statusUpdated = transactionStatus !== prevTxStatus const statusUpdated = transactionStatus !== prevTxStatus
const txDroppedOrConfirmed = transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS 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) { if (statusUpdated && txDroppedOrConfirmed) {
showTransactionConfirmedModal({ showTransactionConfirmedModal({
onSubmit: () => { onSubmit: () => {
@ -204,11 +221,16 @@ export default class ConfirmTransactionBase extends Component {
hexTransactionFee, hexTransactionFee,
hexTransactionTotal, hexTransactionTotal,
hideDetails, hideDetails,
useNonceField,
customNonceValue,
updateCustomNonce,
advancedInlineGasShown, advancedInlineGasShown,
customGas, customGas,
insufficientBalance, insufficientBalance,
updateGasAndCalculate, updateGasAndCalculate,
hideFiatConversion, hideFiatConversion,
nextNonce,
getNextNonce,
} = this.props } = this.props
if (hideDetails) { if (hideDetails) {
@ -240,7 +262,7 @@ export default class ConfirmTransactionBase extends Component {
: null : null
} }
</div> </div>
<div> <div className={useNonceField ? 'confirm-page-container-content__gas-fee' : null}>
<ConfirmDetailRow <ConfirmDetailRow
label="Total" label="Total"
value={hexTransactionTotal} value={hexTransactionTotal}
@ -251,6 +273,31 @@ export default class ConfirmTransactionBase extends Component {
primaryValueTextColor="#2f9ae0" primaryValueTextColor="#2f9ae0"
/> />
</div> </div>
{useNonceField ? <div>
<div className="confirm-detail-row">
<div className="confirm-detail-row__label">
{ this.context.t('nonceFieldHeading') }
</div>
<div className="custom-nonce-input">
<TextField
type="number"
min="0"
placeholder={ this.context.t('nonceFieldPlaceholder') }
onChange={({ target: { value } }) => {
if (!value.length || Number(value) < 0) {
updateCustomNonce('')
} else {
updateCustomNonce(String(Math.floor(value)))
}
getNextNonce()
}}
fullWidth
margin="dense"
value={customNonceValue || nextNonce || ''}
/>
</div>
</div>
</div> : null}
</div> </div>
) )
) )
@ -347,7 +394,17 @@ export default class ConfirmTransactionBase extends Component {
handleCancel () { handleCancel () {
const { metricsEvent } = this.context 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({ metricsEvent({
eventOpts: { eventOpts: {
@ -361,6 +418,7 @@ export default class ConfirmTransactionBase extends Component {
origin, origin,
}, },
}) })
updateCustomNonce('')
if (onCancel) { if (onCancel) {
onCancel(txData) onCancel(txData)
} else { } else {
@ -374,7 +432,19 @@ export default class ConfirmTransactionBase extends Component {
handleSubmit () { handleSubmit () {
const { metricsEvent } = this.context 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 const { submitting } = this.state
if (submitting) { if (submitting) {
@ -406,6 +476,7 @@ export default class ConfirmTransactionBase extends Component {
this.setState({ this.setState({
submitting: false, submitting: false,
}) })
updateCustomNonce('')
}) })
} else { } else {
sendTransaction(txData) sendTransaction(txData)
@ -415,6 +486,7 @@ export default class ConfirmTransactionBase extends Component {
submitting: false, submitting: false,
}, () => { }, () => {
history.push(DEFAULT_ROUTE) history.push(DEFAULT_ROUTE)
updateCustomNonce('')
}) })
}) })
.catch(error => { .catch(error => {
@ -493,7 +565,7 @@ export default class ConfirmTransactionBase extends Component {
} }
componentDidMount () { componentDidMount () {
const { txData: { origin, id } = {}, cancelTransaction } = this.props const { txData: { origin, id } = {}, cancelTransaction, getNextNonce } = this.props
const { metricsEvent } = this.context const { metricsEvent } = this.context
metricsEvent({ metricsEvent({
eventOpts: { eventOpts: {
@ -521,6 +593,8 @@ export default class ConfirmTransactionBase extends Component {
cancelTransaction({ id }) cancelTransaction({ id })
} }
} }
getNextNonce()
} }
render () { render () {
@ -543,12 +617,13 @@ export default class ConfirmTransactionBase extends Component {
contentComponent, contentComponent,
onEdit, onEdit,
nonce, nonce,
customNonceValue,
assetImage, assetImage,
warning, warning,
unapprovedTxCount, unapprovedTxCount,
transactionCategory, transactionCategory,
} = this.props } = this.props
const { submitting, submitError } = this.state const { submitting, submitError, submitWarning } = this.state
const { name } = methodData const { name } = methodData
const { valid, errorKey } = this.getErrorKey() const { valid, errorKey } = this.getErrorKey()
@ -572,13 +647,13 @@ export default class ConfirmTransactionBase extends Component {
detailsComponent={this.renderDetails()} detailsComponent={this.renderDetails()}
dataComponent={this.renderData()} dataComponent={this.renderData()}
contentComponent={contentComponent} contentComponent={contentComponent}
nonce={nonce} nonce={customNonceValue || nonce}
unapprovedTxCount={unapprovedTxCount} unapprovedTxCount={unapprovedTxCount}
assetImage={assetImage} assetImage={assetImage}
identiconAddress={identiconAddress} identiconAddress={identiconAddress}
errorMessage={errorMessage || submitError} errorMessage={errorMessage || submitError}
errorKey={propsErrorKey || errorKey} errorKey={propsErrorKey || errorKey}
warning={warning} warning={warning || submitWarning}
totalTx={totalTx} totalTx={totalTx}
positionOfCurrentTx={positionOfCurrentTx} positionOfCurrentTx={positionOfCurrentTx}
nextTxId={nextTxId} nextTxId={nextTxId}

@ -8,7 +8,17 @@ import {
clearConfirmTransaction, clearConfirmTransaction,
} from '../../ducks/confirm-transaction/confirm-transaction.duck' } 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 { import {
INSUFFICIENT_FUNDS_ERROR_KEY, INSUFFICIENT_FUNDS_ERROR_KEY,
GAS_LIMIT_TOO_LOW_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 { conversionGreaterThan } from '../../helpers/utils/conversion-util'
import { MIN_GAS_LIMIT_DEC } from '../send/send.constants' import { MIN_GAS_LIMIT_DEC } from '../send/send.constants'
import { checksumAddress, addressSlicer, valuesFor } from '../../helpers/utils/util' 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' import { transactionFeeSelector } from '../../selectors/confirm-transaction'
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { 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 mapStateToProps = (state, ownProps) => {
const { toAddress: propsToAddress, match: { params = {} } } = ownProps const { toAddress: propsToAddress, match: { params = {} } } = ownProps
const { id: paramsTransactionId } = params const { id: paramsTransactionId } = params
@ -45,6 +61,7 @@ const mapStateToProps = (state, ownProps) => {
network, network,
unapprovedTxs, unapprovedTxs,
metaMetricsSendCount, metaMetricsSendCount,
nextNonce,
} = metamask } = metamask
const { const {
tokenData, tokenData,
@ -146,16 +163,23 @@ const mapStateToProps = (state, ownProps) => {
gasPrice, gasPrice,
}, },
advancedInlineGasShown: getAdvancedInlineGasShown(state), advancedInlineGasShown: getAdvancedInlineGasShown(state),
useNonceField: getUseNonceField(state),
customNonceValue: getCustomNonceValue(state),
insufficientBalance, insufficientBalance,
hideSubtitle: (!isMainnet && !showFiatInTestnets), hideSubtitle: (!isMainnet && !showFiatInTestnets),
hideFiatConversion: (!isMainnet && !showFiatInTestnets), hideFiatConversion: (!isMainnet && !showFiatInTestnets),
metaMetricsSendCount, metaMetricsSendCount,
transactionCategory, transactionCategory,
nextNonce,
} }
} }
const mapDispatchToProps = dispatch => { export const mapDispatchToProps = dispatch => {
return { return {
updateCustomNonce: value => {
customNonceValue = value
dispatch(updateCustomNonce(value))
},
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
clearSend: () => dispatch(clearSend()), clearSend: () => dispatch(clearSend()),
showTransactionConfirmedModal: ({ onSubmit }) => { showTransactionConfirmedModal: ({ onSubmit }) => {
@ -172,8 +196,9 @@ const mapDispatchToProps = dispatch => {
}, },
cancelTransaction: ({ id }) => dispatch(cancelTx({ id })), cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)), cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)),
sendTransaction: txData => dispatch(updateAndApproveTx(txData)), sendTransaction: txData => dispatch(updateAndApproveTx(customNonceMerge(txData))),
setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)), setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)),
getNextNonce: () => dispatch(getNextNonce()),
} }
} }

@ -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')
})
})

@ -14,6 +14,8 @@ export default class AdvancedTab extends PureComponent {
} }
static propTypes = { static propTypes = {
setUseNonceField: PropTypes.func,
useNonceField: PropTypes.bool,
setHexDataFeatureFlag: PropTypes.func, setHexDataFeatureFlag: PropTypes.func,
setRpcTarget: PropTypes.func, setRpcTarget: PropTypes.func,
displayWarning: 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 (
<div className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{ this.context.t('nonceField') }</span>
<div className="settings-page__content-description">
{ t('nonceFieldDescription') }
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<ToggleButton
value={useNonceField}
onToggle={value => setUseNonceField(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
</div>
</div>
</div>
)
}
renderAutoLogoutTimeLimit () { renderAutoLogoutTimeLimit () {
const { t } = this.context const { t } = this.context
const { const {
@ -311,6 +339,7 @@ export default class AdvancedTab extends PureComponent {
{ this.renderAdvancedGasInputInline() } { this.renderAdvancedGasInputInline() }
{ this.renderHexDataOptIn() } { this.renderHexDataOptIn() }
{ this.renderShowConversionInTestnets() } { this.renderShowConversionInTestnets() }
{ this.renderUseNonceOptIn() }
{ this.renderAutoLogoutTimeLimit() } { this.renderAutoLogoutTimeLimit() }
{ this.renderThreeBoxControl() } { this.renderThreeBoxControl() }
</div> </div>

@ -11,6 +11,7 @@ import {
setAutoLogoutTimeLimit, setAutoLogoutTimeLimit,
setThreeBoxSyncingPermission, setThreeBoxSyncingPermission,
turnThreeBoxSyncingOnAndInitialize, turnThreeBoxSyncingOnAndInitialize,
setUseNonceField,
} from '../../../store/actions' } from '../../../store/actions'
import {preferencesSelector} from '../../../selectors/selectors' import {preferencesSelector} from '../../../selectors/selectors'
@ -23,6 +24,7 @@ export const mapStateToProps = state => {
} = {}, } = {},
threeBoxSyncingAllowed, threeBoxSyncingAllowed,
threeBoxDisabled, threeBoxDisabled,
useNonceField,
} = metamask } = metamask
const { showFiatInTestnets, autoLogoutTimeLimit } = preferencesSelector(state) const { showFiatInTestnets, autoLogoutTimeLimit } = preferencesSelector(state)
@ -34,6 +36,7 @@ export const mapStateToProps = state => {
autoLogoutTimeLimit, autoLogoutTimeLimit,
threeBoxSyncingAllowed, threeBoxSyncingAllowed,
threeBoxDisabled, threeBoxDisabled,
useNonceField,
} }
} }
@ -44,6 +47,7 @@ export const mapDispatchToProps = dispatch => {
displayWarning: warning => dispatch(displayWarning(warning)), displayWarning: warning => dispatch(displayWarning(warning)),
showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)), setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)),
setUseNonceField: value => dispatch(setUseNonceField(value)),
setShowFiatConversionOnTestnetsPreference: value => { setShowFiatConversionOnTestnetsPreference: value => {
return dispatch(setShowFiatConversionOnTestnetsPreference(value)) return dispatch(setShowFiatConversionOnTestnetsPreference(value))
}, },

@ -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', () => { 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) const textField = autoTimeout.find(TextField)
textField.props().onChange({ target: { value: 1440 } }) textField.props().onChange({ target: { value: 1440 } })

@ -17,6 +17,7 @@ const defaultState = {
}, },
threeBoxSyncingAllowed: false, threeBoxSyncingAllowed: false,
threeBoxDisabled: false, threeBoxDisabled: false,
useNonceField: false,
}, },
} }
@ -31,6 +32,7 @@ describe('AdvancedTab Container', () => {
autoLogoutTimeLimit: 0, autoLogoutTimeLimit: 0,
threeBoxSyncingAllowed: false, threeBoxSyncingAllowed: false,
threeBoxDisabled: false, threeBoxDisabled: false,
useNonceField: false,
} }
assert.deepEqual(props, expected) assert.deepEqual(props, expected)
@ -46,5 +48,6 @@ describe('AdvancedTab Container', () => {
assert.ok(typeof props.setAdvancedInlineGasFeatureFlag === 'function') assert.ok(typeof props.setAdvancedInlineGasFeatureFlag === 'function')
assert.ok(typeof props.setShowFiatConversionOnTestnetsPreference === 'function') assert.ok(typeof props.setShowFiatConversionOnTestnetsPreference === 'function')
assert.ok(typeof props.setAutoLogoutTimeLimit === 'function') assert.ok(typeof props.setAutoLogoutTimeLimit === 'function')
assert.ok(typeof props.setUseNonceField === 'function')
}) })
}) })

@ -45,6 +45,8 @@ const selectors = {
getNetworkIdentifier, getNetworkIdentifier,
isBalanceCached, isBalanceCached,
getAdvancedInlineGasShown, getAdvancedInlineGasShown,
getUseNonceField,
getCustomNonceValue,
getIsMainnet, getIsMainnet,
getCurrentNetworkId, getCurrentNetworkId,
getSelectedAsset, getSelectedAsset,
@ -341,6 +343,14 @@ function getAdvancedInlineGasShown (state) {
return Boolean(state.metamask.featureFlags.advancedInlineGas) 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) { function getMetaMetricState (state) {
return { return {
network: getCurrentNetworkId(state), network: getCurrentNetworkId(state),

@ -178,6 +178,9 @@ var actions = {
VIEW_PENDING_TX: 'VIEW_PENDING_TX', VIEW_PENDING_TX: 'VIEW_PENDING_TX',
updateTransactionParams, updateTransactionParams,
UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS', UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS',
setNextNonce,
SET_NEXT_NONCE: 'SET_NEXT_NONCE',
getNextNonce,
// send screen // send screen
UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT',
UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
@ -298,6 +301,10 @@ var actions = {
SET_USE_BLOCKIE: 'SET_USE_BLOCKIE', SET_USE_BLOCKIE: 'SET_USE_BLOCKIE',
setUseBlockie, setUseBlockie,
SET_USE_NONCEFIELD: 'SET_USE_NONCEFIELD',
setUseNonceField,
UPDATE_CUSTOM_NONCE: 'UPDATE_CUSTOM_NONCE',
updateCustomNonce,
SET_PARTICIPATE_IN_METAMETRICS: 'SET_PARTICIPATE_IN_METAMETRICS', SET_PARTICIPATE_IN_METAMETRICS: 'SET_PARTICIPATE_IN_METAMETRICS',
SET_METAMETRICS_SEND_COUNT: 'SET_METAMETRICS_SEND_COUNT', 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) { function updateSendMemo (memo) {
return { return {
type: actions.UPDATE_SEND_MEMO, type: actions.UPDATE_SEND_MEMO,
@ -1208,6 +1222,7 @@ function updateAndApproveTx (txData) {
dispatch(actions.clearSend()) dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id)) dispatch(actions.completedTx(txData.id))
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
dispatch(actions.updateCustomNonce(''))
dispatch(closeCurrentNotificationWindow()) dispatch(closeCurrentNotificationWindow())
return txData 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) { function updateCurrentLocale (key) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
@ -2888,3 +2920,26 @@ function turnThreeBoxSyncingOnAndInitialize () {
await dispatch(initializeThreeBox(true)) 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)
})
})
}
}

Loading…
Cancel
Save