aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan J Miller <danjm.com@gmail.com>2018-12-11 05:51:00 +0800
committerDan Finlay <542863+danfinlay@users.noreply.github.com>2018-12-11 05:51:00 +0800
commit1fbdce8916151df2b31eebc5de29a1365e5dadff (patch)
tree2333aa889dd6a6ec83a1665591006daca8e68859
parent49971e9ec250888746546f62fa176ed129bf9c74 (diff)
downloadtangerine-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.
-rw-r--r--development/states/send-edit.json1
-rw-r--r--development/states/send-new-ui.json4
-rw-r--r--test/e2e/beta/metamask-beta-ui.spec.js3
-rw-r--r--test/integration/lib/send-new-ui.js16
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js103
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss14
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js157
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js10
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js5
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js1
-rw-r--r--ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js4
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.container.js4
-rw-r--r--ui/app/ducks/gas.duck.js43
-rw-r--r--ui/app/ducks/tests/gas-duck.test.js62
-rw-r--r--ui/app/selectors/custom-gas.js36
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
}