diff options
author | Dan J Miller <danjm.com@gmail.com> | 2018-12-11 05:51:00 +0800 |
---|---|---|
committer | Dan Finlay <542863+danfinlay@users.noreply.github.com> | 2018-12-11 05:51:00 +0800 |
commit | 1fbdce8916151df2b31eebc5de29a1365e5dadff (patch) | |
tree | 2333aa889dd6a6ec83a1665591006daca8e68859 | |
parent | 49971e9ec250888746546f62fa176ed129bf9c74 (diff) | |
download | tangerine-wallet-browser-1fbdce8916151df2b31eebc5de29a1365e5dadff.tar tangerine-wallet-browser-1fbdce8916151df2b31eebc5de29a1365e5dadff.tar.gz tangerine-wallet-browser-1fbdce8916151df2b31eebc5de29a1365e5dadff.tar.bz2 tangerine-wallet-browser-1fbdce8916151df2b31eebc5de29a1365e5dadff.tar.lz tangerine-wallet-browser-1fbdce8916151df2b31eebc5de29a1365e5dadff.tar.xz tangerine-wallet-browser-1fbdce8916151df2b31eebc5de29a1365e5dadff.tar.zst tangerine-wallet-browser-1fbdce8916151df2b31eebc5de29a1365e5dadff.zip |
Improve ux for low gas price set (#5862)
* Show user warning if they set gas price below safelow minimum, error if 0.
* Properly cache basic price estimate data.
* Default retry price to recommended price if original price was 0x0
* Use mock fetch in send-new-ui integration tests.
15 files changed, 395 insertions, 68 deletions
diff --git a/development/states/send-edit.json b/development/states/send-edit.json index a519f30b4..7951b06cf 100644 --- a/development/states/send-edit.json +++ b/development/states/send-edit.json @@ -197,6 +197,7 @@ }, "basicEstimateIsLoading": false, "gasEstimatesLoading": false, + "basicPriceAndTimeEstimates": [], "priceAndTimeEstimates": [ { "expectedTime": "1374.1168296452973076627", diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index 479b6d3e3..fd048596c 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -139,7 +139,8 @@ "send": { "fromDropdownOpen": false, "toDropdownOpen": false, - "errors": {} + "errors": {}, + "gasButtonGroupShown": true }, "confirmTransaction": { "txData": {}, @@ -179,6 +180,7 @@ }, "basicEstimateIsLoading": false, "gasEstimatesLoading": false, + "basicPriceAndTimeEstimates": [], "priceAndTimeEstimates": [ { "expectedTime": "1374.1168296452973076627", diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index c5b7f40e4..9980e874f 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -69,6 +69,7 @@ describe('MetaMask', function () { beforeEach(async function () { await driver.executeScript( + 'window.origFetch = window.fetch.bind(window);' + 'window.fetch = ' + '(...args) => { ' + 'if (args[0] === "https://ethgasstation.info/json/ethgasAPI.json") { return ' + @@ -77,7 +78,7 @@ describe('MetaMask', function () { 'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } else if ' + '(args[0] === "https://dev.blockscale.net/api/gasexpress.json") { return ' + 'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.gasExpress + '\')) }); } ' + - 'return window.fetch(...args); }' + 'return window.origFetch(...args); }' ) }) diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 98e5f549b..1acd85a35 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -4,6 +4,7 @@ const { queryAsync, findAsync, } = require('../../lib/util') +const fetchMockResponses = require('../../e2e/beta/fetch-mocks.js') QUnit.module('new ui send flow') @@ -22,6 +23,19 @@ global.ethQuery = { global.ethereumProvider = {} async function runSendFlowTest (assert, done) { + const tempFetch = global.fetch + + global.fetch = (...args) => { + if (args[0] === 'https://ethgasstation.info/json/ethgasAPI.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasBasic)) }) + } else if (args[0] === 'https://ethgasstation.info/json/predictTable.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasPredictTable)) }) + } else if (args[0] === 'https://dev.blockscale.net/api/gasexpress.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.gasExpress)) }) + } + return window.fetch(...args) + } + console.log('*** start runSendFlowTest') const selectState = await queryAsync($, 'select') selectState.val('send new ui') @@ -129,6 +143,8 @@ async function runSendFlowTest (assert, done) { const cancelButtonInEdit = await queryAsync($, '.btn-default.btn--large.page-container__footer-button') cancelButtonInEdit[0].click() + + global.fetch = tempFetch // sendButtonInEdit[0].click() // // TODO: Need a way to mock background so that we can test correct transition from editing to confirm diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index ba738ff75..7c3142d0d 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -21,6 +21,8 @@ export default class AdvancedTabContent extends Component { timeRemaining: PropTypes.string, gasChartProps: PropTypes.object, insufficientBalance: PropTypes.bool, + customPriceIsSafe: PropTypes.bool, + isSpeedUp: PropTypes.bool, } constructor (props) { @@ -37,27 +39,62 @@ export default class AdvancedTabContent extends Component { } } - gasInput (value, onChange, min, insufficientBalance, showGWEI) { + gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) { + let errorText + let errorType + let isInError = true + + + if (insufficientBalance) { + errorText = 'Insufficient Balance' + errorType = 'error' + } else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) { + errorText = 'Zero gas price on speed up' + errorType = 'error' + } else if (labelKey === 'gasPrice' && !customPriceIsSafe) { + errorText = 'Gas Price Extremely Low' + errorType = 'warning' + } else { + isInError = false + } + + return { + isInError, + errorText, + errorType, + } + } + + gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) { + const { + isInError, + errorText, + errorType, + } = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) + return ( <div className="advanced-tab__gas-edit-row__input-wrapper"> <input className={classnames('advanced-tab__gas-edit-row__input', { - 'advanced-tab__gas-edit-row__input--error': insufficientBalance, + 'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error', + 'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning', })} type="number" value={value} - min={min} onChange={event => onChange(Number(event.target.value))} /> <div className={classnames('advanced-tab__gas-edit-row__input-arrows', { - 'advanced-tab__gas-edit-row__input-arrows--error': insufficientBalance, + 'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error', + 'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning', })}> <div className="advanced-tab__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value + 1)}><i className="fa fa-sm fa-angle-up" /></div> <div className="advanced-tab__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value - 1)}><i className="fa fa-sm fa-angle-down" /></div> </div> - {insufficientBalance && <div className="advanced-tab__gas-edit-row__insufficient-balance"> - Insufficient Balance - </div>} + { isInError + ? <div className={`advanced-tab__gas-edit-row__${errorType}-text`}> + { errorText } + </div> + : null } </div> ) } @@ -83,23 +120,45 @@ export default class AdvancedTabContent extends Component { ) } - renderGasEditRow (labelKey, ...gasInputArgs) { + renderGasEditRow (gasInputArgs) { return ( <div className="advanced-tab__gas-edit-row"> <div className="advanced-tab__gas-edit-row__label"> - { this.context.t(labelKey) } + { this.context.t(gasInputArgs.labelKey) } { this.infoButton(() => {}) } </div> - { this.gasInput(...gasInputArgs) } + { this.gasInput(gasInputArgs) } </div> ) } - renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit, insufficientBalance) { + renderGasEditRows ({ + customGasPrice, + updateCustomGasPrice, + customGasLimit, + updateCustomGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + }) { return ( <div className="advanced-tab__gas-edit-rows"> - { this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, insufficientBalance, true) } - { this.renderGasEditRow('gasLimit', customGasLimit, this.onChangeGasLimit, customGasLimit, insufficientBalance) } + { this.renderGasEditRow({ + labelKey: 'gasPrice', + value: customGasPrice, + onChange: updateCustomGasPrice, + insufficientBalance, + customPriceIsSafe, + showGWEI: true, + isSpeedUp, + }) } + { this.renderGasEditRow({ + labelKey: 'gasLimit', + value: customGasLimit, + onChange: this.onChangeGasLimit, + insufficientBalance, + customPriceIsSafe, + }) } </div> ) } @@ -115,19 +174,23 @@ export default class AdvancedTabContent extends Component { totalFee, gasChartProps, gasEstimatesLoading, + customPriceIsSafe, + isSpeedUp, } = this.props return ( <div className="advanced-tab"> { this.renderDataSummary(totalFee, timeRemaining) } <div className="advanced-tab__fee-chart"> - { this.renderGasEditRows( - customGasPrice, - updateCustomGasPrice, - customGasLimit, - updateCustomGasLimit, - insufficientBalance - ) } + { this.renderGasEditRows({ + customGasPrice, + updateCustomGasPrice, + customGasLimit, + updateCustomGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + }) } <div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div> {!gasEstimatesLoading ? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} /> diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss index 88c69faf4..53cb84791 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -102,11 +102,15 @@ } } - &__insufficient-balance { + &__error-text { font-size: 12px; color: red; } + &__warning-text { + font-size: 12px; + color: orange; + } &__input-wrapper { position: relative; @@ -128,6 +132,10 @@ border: 1px solid $red; } + &__input--warning { + border: 1px solid $orange; + } + &__input-arrows { position: absolute; top: 7px; @@ -169,6 +177,10 @@ border: 1px solid $red; } + &__input-arrows--warning { + border: 1px solid $orange; + } + input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; -moz-appearance: none; diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js index d6920454d..00242e430 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -16,6 +16,7 @@ sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow') sinon.spy(AdvancedTabContent.prototype, 'gasInput') sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows') sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary') +sinon.spy(AdvancedTabContent.prototype, 'gasInputError') describe('AdvancedTabContent Component', function () { let wrapper @@ -29,6 +30,8 @@ describe('AdvancedTabContent Component', function () { timeRemaining={21500} totalFee={'$0.25'} insufficientBalance={false} + customPriceIsSafe={true} + isSpeedUp={false} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) }) @@ -86,9 +89,15 @@ describe('AdvancedTabContent Component', function () { it('should call renderGasEditRows with the expected params', () => { assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args - assert.deepEqual(renderGasEditRowArgs, [ - 11, propsMethodSpies.updateCustomGasPrice, 23456, propsMethodSpies.updateCustomGasLimit, false, - ]) + assert.deepEqual(renderGasEditRowArgs, [{ + customGasPrice: 11, + customGasLimit: 23456, + insufficientBalance: false, + customPriceIsSafe: true, + updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice, + updateCustomGasLimit: propsMethodSpies.updateCustomGasLimit, + isSpeedUp: false, + }]) }) }) @@ -124,9 +133,10 @@ describe('AdvancedTabContent Component', function () { beforeEach(() => { AdvancedTabContent.prototype.gasInput.resetHistory() - gasEditRow = shallow(wrapper.instance().renderGasEditRow( - 'mockLabelKey', 'argA', 'argB' - )) + gasEditRow = shallow(wrapper.instance().renderGasEditRow({ + labelKey: 'mockLabelKey', + someArg: 'argA', + })) }) it('should render the gas-edit-row root node', () => { @@ -149,7 +159,7 @@ describe('AdvancedTabContent Component', function () { it('should call this.gasInput with the correct args', () => { const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args - assert.deepEqual(gasInputSpyArgs[0], [ 'argA', 'argB' ]) + assert.deepEqual(gasInputSpyArgs[0], [ { labelKey: 'mockLabelKey', someArg: 'argA' } ]) }) }) @@ -188,12 +198,22 @@ describe('AdvancedTabContent Component', function () { it('should call this.renderGasEditRow twice, with the expected args', () => { const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args assert.equal(renderGasEditRowSpyArgs.length, 2) - assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [ - 'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', false, true, - ].map(String)) - assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [ - 'gasLimit', 'mockGasLimit', () => 'mockOnChangeGasLimit', 'mockGasLimit', false, - ].map(String)) + assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [{ + labelKey: 'gasPrice', + value: 'mockGasLimit', + onChange: () => 'mockOnChangeGasLimit', + insufficientBalance: false, + customPriceIsSafe: true, + showGWEI: true, + }].map(String)) + assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [{ + labelKey: 'gasPrice', + value: 'mockGasPrice', + onChange: () => 'mockUpdateCustomGasPriceReturn', + insufficientBalance: false, + customPriceIsSafe: true, + showGWEI: true, + }].map(String)) }) }) @@ -219,13 +239,16 @@ describe('AdvancedTabContent Component', function () { beforeEach(() => { AdvancedTabContent.prototype.renderGasEditRow.resetHistory() - gasInput = shallow(wrapper.instance().gasInput( - 321, - value => value + 7, - 0, - false, - 8 - )) + AdvancedTabContent.prototype.gasInputError.resetHistory() + gasInput = shallow(wrapper.instance().gasInput({ + labelKey: 'gasPrice', + value: 321, + onChange: value => value + 7, + insufficientBalance: false, + showGWEI: true, + customPriceIsSafe: true, + isSpeedUp: false, + })) }) it('should render the input-wrapper root node', () => { @@ -237,12 +260,6 @@ describe('AdvancedTabContent Component', function () { assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) }) - it('should pass the correct value min and precision props to the input', () => { - const inputProps = gasInput.find('input').props() - assert.equal(inputProps.min, 0) - assert.equal(inputProps.value, 321) - }) - it('should call the passed onChange method with the value of the input onChange event', () => { const inputOnChange = gasInput.find('input').props().onChange assert.equal(inputOnChange({ target: { value: 8} }), 15) @@ -256,18 +273,92 @@ describe('AdvancedTabContent Component', function () { }) it('should call onChange with the value incremented decremented when its onchange method is called', () => { - gasInput = shallow(wrapper.instance().gasInput( - 321, - value => value + 7, - 0, - 8, - false - )) const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0) assert.equal(upArrow.props().onClick(), 329) const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1) assert.equal(downArrow.props().onClick(), 327) }) + + it('should call gasInputError with the expected params', () => { + assert.equal(AdvancedTabContent.prototype.gasInputError.callCount, 1) + const gasInputErrorArgs = AdvancedTabContent.prototype.gasInputError.getCall(0).args + assert.deepEqual(gasInputErrorArgs, [{ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: true, + value: 321, + isSpeedUp: false, + }]) + }) + }) + + describe('gasInputError()', () => { + let gasInputError + + beforeEach(() => { + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + gasInputError = wrapper.instance().gasInputError({ + labelKey: '', + insufficientBalance: false, + customPriceIsSafe: true, + isSpeedUp: false, + }) + }) + + it('should return an insufficientBalance error', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: true, + customPriceIsSafe: true, + isSpeedUp: false, + value: 1, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'Insufficient Balance', + errorType: 'error', + }) + }) + + it('should return a zero gas on retry error', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: false, + isSpeedUp: true, + value: 0, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'Zero gas price on speed up', + errorType: 'error', + }) + }) + + it('should return a low gas warning', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: false, + isSpeedUp: false, + value: 1, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'Gas Price Extremely Low', + errorType: 'warning', + }) + }) + + it('should return isInError false if there is no error', () => { + gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: true, + value: 1, + }) + assert.equal(gasInputError.isInError, false) + }) }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index be91bef0f..64c2be353 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -35,6 +35,9 @@ export default class GasModalPageContainer extends Component { PropTypes.string, PropTypes.number, ]), + customPriceIsSafe: PropTypes.bool, + isSpeedUp: PropTypes.bool, + disableSave: PropTypes.bool, } state = {} @@ -69,6 +72,8 @@ export default class GasModalPageContainer extends Component { currentTimeEstimate, insufficientBalance, gasEstimatesLoading, + customPriceIsSafe, + isSpeedUp, }) { const { transactionFee } = this.props return ( @@ -83,6 +88,8 @@ export default class GasModalPageContainer extends Component { gasChartProps={gasChartProps} insufficientBalance={insufficientBalance} gasEstimatesLoading={gasEstimatesLoading} + customPriceIsSafe={customPriceIsSafe} + isSpeedUp={isSpeedUp} /> ) } @@ -153,6 +160,7 @@ export default class GasModalPageContainer extends Component { onSubmit, customModalGasPriceInHex, customModalGasLimitInHex, + disableSave, ...tabProps } = this.props @@ -162,7 +170,7 @@ export default class GasModalPageContainer extends Component { title={this.context.t('customGas')} subtitle={this.context.t('customGasSubTitle')} tabsComponent={this.renderTabs(infoRowProps, tabProps)} - disabled={tabProps.insufficientBalance} + disabled={disableSave} onCancel={() => cancelAndClose()} onClose={() => cancelAndClose()} onSubmit={() => { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index c619a0988..dde0f2b94 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -40,6 +40,7 @@ import { getEstimatedGasTimes, getRenderableBasicEstimateData, getBasicGasEstimateBlockTime, + isCustomPriceSafe, } from '../../../selectors/custom-gas' import { submittedPendingTransactionsSelector, @@ -107,6 +108,7 @@ const mapStateToProps = (state, ownProps) => { newTotalFiat, currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes), blockTime: getBasicGasEstimateBlockTime(state), + customPriceIsSafe: isCustomPriceSafe(state), gasPriceButtonGroupProps: { buttonDataLoading, defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), @@ -167,7 +169,7 @@ const mapDispatchToProps = dispatch => { } const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { gasPriceButtonGroupProps, isConfirm, isSpeedUp, txId } = stateProps + const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, customGasPrice } = stateProps const { updateCustomGasPrice: dispatchUpdateCustomGasPrice, hideGasButtonGroup: dispatchHideGasButtonGroup, @@ -208,6 +210,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { dispatchHideSidebar() } }, + disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0), } } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index 2ba2fa9e7..f068c40d0 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -78,6 +78,7 @@ describe('GasModalPageContainer Component', function () { customGasPriceInHex={'mockCustomGasPriceInHex'} customGasLimitInHex={'mockCustomGasLimitInHex'} insufficientBalance={false} + disableSave={false} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 512832866..077ec471d 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -75,6 +75,7 @@ describe('gas-modal-page-container container', () => { gas: { basicEstimates: { blockTime: 12, + safeLow: 2, }, customData: { limit: 'aaaaaaaa', @@ -107,9 +108,10 @@ describe('gas-modal-page-container container', () => { blockTime: 12, customModalGasLimitInHex: 'aaaaaaaa', customModalGasPriceInHex: 'ffffffff', + customPriceIsSafe: true, gasChartProps: { 'currentPrice': 4.294967295, - estimatedTimes: ['31', '62', '93', '124'], + estimatedTimes: [31, 62, 93, 124], estimatedTimesMax: '31', gasPrices: [3, 4, 5, 6], gasPricesMax: 6, diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js index 73d9d8250..e08d3232f 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js @@ -11,7 +11,7 @@ import { formatDate } from '../../util' import { fetchBasicGasAndTimeEstimates, fetchGasEstimates, - setCustomGasPrice, + setCustomGasPriceForRetry, setCustomGasLimit, } from '../../ducks/gas.duck' @@ -21,7 +21,7 @@ const mapDispatchToProps = dispatch => { fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)), retryTransaction: (transaction, gasPrice) => { - dispatch(setCustomGasPrice(gasPrice || transaction.txParams.gasPrice)) + dispatch(setCustomGasPriceForRetry(gasPrice || transaction.txParams.gasPrice)) dispatch(setCustomGasLimit(transaction.txParams.gas)) dispatch(showSidebar({ transitionName: 'sidebar-left', diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index 83c236d81..ee235a757 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -23,6 +23,7 @@ const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL' const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES' const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED' const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED' +const SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED' // TODO: determine if this approach to initState is consistent with conventional ducks pattern const initState = { @@ -49,6 +50,7 @@ const initState = { basicPriceAndTimeEstimates: [], priceAndTimeEstimatesLastRetrieved: 0, basicPriceAndTimeEstimatesLastRetrieved: 0, + basicPriceEstimatesLastRetrieved: 0, errors: {}, } @@ -129,6 +131,11 @@ export default function reducer ({ gas: gasState = initState }, action = {}) { ...newState, basicPriceAndTimeEstimatesLastRetrieved: action.value, } + case SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED: + return { + ...newState, + basicPriceEstimatesLastRetrieved: action.value, + } case RESET_CUSTOM_DATA: return { ...newState, @@ -167,10 +174,17 @@ export function gasEstimatesLoadingFinished () { } export function fetchBasicGasEstimates () { - return (dispatch) => { + return (dispatch, getState) => { + const { + basicPriceEstimatesLastRetrieved, + basicPriceAndTimeEstimates, + } = getState().gas + const timeLastRetrieved = basicPriceEstimatesLastRetrieved || loadLocalStorageData('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') || 0 + dispatch(basicGasEstimatesLoadingStarted()) - return fetch('https://dev.blockscale.net/api/gasexpress.json', { + const promiseToFetch = Date.now() - timeLastRetrieved > 75000 + ? fetch('https://dev.blockscale.net/api/gasexpress.json', { 'headers': {}, 'referrer': 'https://dev.blockscale.net/api/', 'referrerPolicy': 'no-referrer-when-downgrade', @@ -195,10 +209,24 @@ export function fetchBasicGasEstimates () { blockTime, blockNum, } - dispatch(setBasicGasEstimateData(basicEstimates)) - dispatch(basicGasEstimatesLoadingFinished()) + + const timeRetrieved = Date.now() + dispatch(setBasicPriceEstimatesLastRetrieved(timeRetrieved)) + saveLocalStorageData(timeRetrieved, 'BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') + saveLocalStorageData(basicEstimates, 'BASIC_PRICE_ESTIMATES') + return basicEstimates }) + : Promise.resolve(basicPriceAndTimeEstimates.length + ? basicPriceAndTimeEstimates + : loadLocalStorageData('BASIC_PRICE_ESTIMATES') + ) + + return promiseToFetch.then(basicEstimates => { + dispatch(setBasicGasEstimateData(basicEstimates)) + dispatch(basicGasEstimatesLoadingFinished()) + return basicEstimates + }) } } @@ -473,6 +501,13 @@ export function setBasicApiEstimatesLastRetrieved (retrievalTime) { } } +export function setBasicPriceEstimatesLastRetrieved (retrievalTime) { + return { + type: SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED, + value: retrievalTime, + } +} + export function resetCustomGasState () { return { type: RESET_CUSTOM_GAS_STATE } } diff --git a/ui/app/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js index bf374c7ec..3637d8f29 100644 --- a/ui/app/ducks/tests/gas-duck.test.js +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -20,6 +20,7 @@ const { setCustomGasErrors, resetCustomGasState, fetchBasicGasAndTimeEstimates, + fetchBasicGasEstimates, gasEstimatesLoadingStarted, gasEstimatesLoadingFinished, setPricesAndTimeEstimates, @@ -43,6 +44,7 @@ describe('Gas Duck', () => { safeLow: 10, safeLowWait: 'mockSafeLowWait', speed: 'mockSpeed', + standard: 20, } const mockPredictTableResponse = [ { expectedTime: 400, expectedWait: 40, gasprice: 0.25, somethingElse: 'foobar' }, @@ -67,7 +69,7 @@ describe('Gas Duck', () => { { expectedTime: 1, expectedWait: 0.5, gasprice: 20, somethingElse: 'foobar' }, ] const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => { - const dataToResolve = url.match(/ethgasAPI/) + const dataToResolve = url.match(/ethgasAPI|gasexpress/) ? mockEthGasApiResponse : mockPredictTableResponse resolve({ @@ -83,6 +85,7 @@ describe('Gas Duck', () => { }) afterEach(() => { + fetchStub.resetHistory() global.fetch = tempFetch global.Date.now = tempDateNow }) @@ -117,8 +120,7 @@ describe('Gas Duck', () => { priceAndTimeEstimatesLastRetrieved: 0, basicPriceAndTimeEstimates: [], basicPriceAndTimeEstimatesLastRetrieved: 0, - - + basicPriceEstimatesLastRetrieved: 0, } const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED' const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED' @@ -133,6 +135,7 @@ describe('Gas Duck', () => { const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES' const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED' const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED' + const SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED' describe('GasReducer()', () => { it('should initialize state', () => { @@ -301,6 +304,59 @@ describe('Gas Duck', () => { }) }) + describe('fetchBasicGasEstimates', () => { + const mockDistpatch = sinon.spy() + it('should call fetch with the expected params', async () => { + await fetchBasicGasEstimates()(mockDistpatch, () => ({ gas: Object.assign( + {}, + initState, + { basicPriceAEstimatesLastRetrieved: 1000000 } + ) })) + assert.deepEqual( + mockDistpatch.getCall(0).args, + [{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ] + ) + assert.deepEqual( + global.fetch.getCall(0).args, + [ + 'https://dev.blockscale.net/api/gasexpress.json', + { + 'headers': {}, + 'referrer': 'https://dev.blockscale.net/api/', + 'referrerPolicy': 'no-referrer-when-downgrade', + 'body': null, + 'method': 'GET', + 'mode': 'cors', + }, + ] + ) + + assert.deepEqual( + mockDistpatch.getCall(1).args, + [{ type: SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED, value: 2000000 } ] + ) + + assert.deepEqual( + mockDistpatch.getCall(2).args, + [{ + type: SET_BASIC_GAS_ESTIMATE_DATA, + value: { + average: 20, + blockTime: 'mockBlock_time', + blockNum: 'mockBlockNum', + fast: 30, + fastest: 40, + safeLow: 10, + }, + }] + ) + assert.deepEqual( + mockDistpatch.getCall(3).args, + [{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }] + ) + }) + }) + describe('fetchBasicGasAndTimeEstimates', () => { const mockDistpatch = sinon.spy() it('should call fetch with the expected params', async () => { diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index 59f240f9c..91c9109a5 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -2,6 +2,7 @@ import { pipe, partialRight } from 'ramda' import { conversionUtil, multiplyCurrencies, + conversionGreaterThan, } from '../conversion-util' import { getCurrentCurrency, @@ -38,6 +39,8 @@ const selectors = { getRenderableBasicEstimateData, getRenderableEstimateDataForSmallButtonsFromGWEI, priceEstimateToWei, + getSafeLowEstimate, + isCustomPriceSafe, } module.exports = selectors @@ -96,6 +99,39 @@ function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPri }) } +function getSafeLowEstimate (state) { + const { + gas: { + basicEstimates: { + safeLow, + }, + }, + } = state + + return safeLow +} + +function isCustomPriceSafe (state) { + const safeLow = getSafeLowEstimate(state) + const customGasPrice = getCustomGasPrice(state) + + if (!customGasPrice) { + return true + } + + const customPriceSafe = conversionGreaterThan( + { + value: customGasPrice, + fromNumericBase: 'hex', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + }, + { value: safeLow, fromNumericBase: 'dec' } + ) + + return customPriceSafe +} + function getBasicGasEstimateBlockTime (state) { return state.gas.basicEstimates.blockTime } |