Merge branch 'develop' into i5846-ProviderCrashes

feature/default_network_editable
Dan Finlay 6 years ago
commit 3bfd4d1524
  1. 1
      development/states/send-edit.json
  2. 4
      development/states/send-new-ui.json
  3. 3
      test/e2e/beta/metamask-beta-ui.spec.js
  4. 16
      test/integration/lib/send-new-ui.js
  5. 103
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
  6. 14
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss
  7. 157
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js
  8. 10
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
  9. 5
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  10. 1
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js
  11. 4
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
  12. 4
      ui/app/components/transaction-list-item/transaction-list-item.container.js
  13. 43
      ui/app/ducks/gas.duck.js
  14. 62
      ui/app/ducks/tests/gas-duck.test.js
  15. 36
      ui/app/selectors/custom-gas.js

@ -197,6 +197,7 @@
}, },
"basicEstimateIsLoading": false, "basicEstimateIsLoading": false,
"gasEstimatesLoading": false, "gasEstimatesLoading": false,
"basicPriceAndTimeEstimates": [],
"priceAndTimeEstimates": [ "priceAndTimeEstimates": [
{ {
"expectedTime": "1374.1168296452973076627", "expectedTime": "1374.1168296452973076627",

@ -139,7 +139,8 @@
"send": { "send": {
"fromDropdownOpen": false, "fromDropdownOpen": false,
"toDropdownOpen": false, "toDropdownOpen": false,
"errors": {} "errors": {},
"gasButtonGroupShown": true
}, },
"confirmTransaction": { "confirmTransaction": {
"txData": {}, "txData": {},
@ -179,6 +180,7 @@
}, },
"basicEstimateIsLoading": false, "basicEstimateIsLoading": false,
"gasEstimatesLoading": false, "gasEstimatesLoading": false,
"basicPriceAndTimeEstimates": [],
"priceAndTimeEstimates": [ "priceAndTimeEstimates": [
{ {
"expectedTime": "1374.1168296452973076627", "expectedTime": "1374.1168296452973076627",

@ -69,6 +69,7 @@ describe('MetaMask', function () {
beforeEach(async function () { beforeEach(async function () {
await driver.executeScript( await driver.executeScript(
'window.origFetch = window.fetch.bind(window);' +
'window.fetch = ' + 'window.fetch = ' +
'(...args) => { ' + '(...args) => { ' +
'if (args[0] === "https://ethgasstation.info/json/ethgasAPI.json") { return ' + '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 ' + 'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } else if ' +
'(args[0] === "https://dev.blockscale.net/api/gasexpress.json") { return ' + '(args[0] === "https://dev.blockscale.net/api/gasexpress.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.gasExpress + '\')) }); } ' + 'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.gasExpress + '\')) }); } ' +
'return window.fetch(...args); }' 'return window.origFetch(...args); }'
) )
}) })

@ -4,6 +4,7 @@ const {
queryAsync, queryAsync,
findAsync, findAsync,
} = require('../../lib/util') } = require('../../lib/util')
const fetchMockResponses = require('../../e2e/beta/fetch-mocks.js')
QUnit.module('new ui send flow') QUnit.module('new ui send flow')
@ -22,6 +23,19 @@ global.ethQuery = {
global.ethereumProvider = {} global.ethereumProvider = {}
async function runSendFlowTest (assert, done) { 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') console.log('*** start runSendFlowTest')
const selectState = await queryAsync($, 'select') const selectState = await queryAsync($, 'select')
selectState.val('send new ui') 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') const cancelButtonInEdit = await queryAsync($, '.btn-default.btn--large.page-container__footer-button')
cancelButtonInEdit[0].click() cancelButtonInEdit[0].click()
global.fetch = tempFetch
// sendButtonInEdit[0].click() // sendButtonInEdit[0].click()
// // TODO: Need a way to mock background so that we can test correct transition from editing to confirm // // TODO: Need a way to mock background so that we can test correct transition from editing to confirm

@ -21,6 +21,8 @@ export default class AdvancedTabContent extends Component {
timeRemaining: PropTypes.string, timeRemaining: PropTypes.string,
gasChartProps: PropTypes.object, gasChartProps: PropTypes.object,
insufficientBalance: PropTypes.bool, insufficientBalance: PropTypes.bool,
customPriceIsSafe: PropTypes.bool,
isSpeedUp: PropTypes.bool,
} }
constructor (props) { 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 ( return (
<div className="advanced-tab__gas-edit-row__input-wrapper"> <div className="advanced-tab__gas-edit-row__input-wrapper">
<input <input
className={classnames('advanced-tab__gas-edit-row__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" type="number"
value={value} value={value}
min={min}
onChange={event => onChange(Number(event.target.value))} onChange={event => onChange(Number(event.target.value))}
/> />
<div className={classnames('advanced-tab__gas-edit-row__input-arrows', { <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-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 className="advanced-tab__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value - 1)}><i className="fa fa-sm fa-angle-down" /></div>
</div> </div>
{insufficientBalance && <div className="advanced-tab__gas-edit-row__insufficient-balance"> { isInError
Insufficient Balance ? <div className={`advanced-tab__gas-edit-row__${errorType}-text`}>
</div>} { errorText }
</div>
: null }
</div> </div>
) )
} }
@ -83,23 +120,45 @@ export default class AdvancedTabContent extends Component {
) )
} }
renderGasEditRow (labelKey, ...gasInputArgs) { renderGasEditRow (gasInputArgs) {
return ( return (
<div className="advanced-tab__gas-edit-row"> <div className="advanced-tab__gas-edit-row">
<div className="advanced-tab__gas-edit-row__label"> <div className="advanced-tab__gas-edit-row__label">
{ this.context.t(labelKey) } { this.context.t(gasInputArgs.labelKey) }
{ this.infoButton(() => {}) } { this.infoButton(() => {}) }
</div> </div>
{ this.gasInput(...gasInputArgs) } { this.gasInput(gasInputArgs) }
</div> </div>
) )
} }
renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit, insufficientBalance) { renderGasEditRows ({
customGasPrice,
updateCustomGasPrice,
customGasLimit,
updateCustomGasLimit,
insufficientBalance,
customPriceIsSafe,
isSpeedUp,
}) {
return ( return (
<div className="advanced-tab__gas-edit-rows"> <div className="advanced-tab__gas-edit-rows">
{ this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, insufficientBalance, true) } { this.renderGasEditRow({
{ this.renderGasEditRow('gasLimit', customGasLimit, this.onChangeGasLimit, customGasLimit, insufficientBalance) } labelKey: 'gasPrice',
value: customGasPrice,
onChange: updateCustomGasPrice,
insufficientBalance,
customPriceIsSafe,
showGWEI: true,
isSpeedUp,
}) }
{ this.renderGasEditRow({
labelKey: 'gasLimit',
value: customGasLimit,
onChange: this.onChangeGasLimit,
insufficientBalance,
customPriceIsSafe,
}) }
</div> </div>
) )
} }
@ -115,19 +174,23 @@ export default class AdvancedTabContent extends Component {
totalFee, totalFee,
gasChartProps, gasChartProps,
gasEstimatesLoading, gasEstimatesLoading,
customPriceIsSafe,
isSpeedUp,
} = this.props } = this.props
return ( return (
<div className="advanced-tab"> <div className="advanced-tab">
{ this.renderDataSummary(totalFee, timeRemaining) } { this.renderDataSummary(totalFee, timeRemaining) }
<div className="advanced-tab__fee-chart"> <div className="advanced-tab__fee-chart">
{ this.renderGasEditRows( { this.renderGasEditRows({
customGasPrice, customGasPrice,
updateCustomGasPrice, updateCustomGasPrice,
customGasLimit, customGasLimit,
updateCustomGasLimit, updateCustomGasLimit,
insufficientBalance insufficientBalance,
) } customPriceIsSafe,
isSpeedUp,
}) }
<div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div> <div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div>
{!gasEstimatesLoading {!gasEstimatesLoading
? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} /> ? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} />

@ -102,11 +102,15 @@
} }
} }
&__insufficient-balance { &__error-text {
font-size: 12px; font-size: 12px;
color: red; color: red;
} }
&__warning-text {
font-size: 12px;
color: orange;
}
&__input-wrapper { &__input-wrapper {
position: relative; position: relative;
@ -128,6 +132,10 @@
border: 1px solid $red; border: 1px solid $red;
} }
&__input--warning {
border: 1px solid $orange;
}
&__input-arrows { &__input-arrows {
position: absolute; position: absolute;
top: 7px; top: 7px;
@ -169,6 +177,10 @@
border: 1px solid $red; border: 1px solid $red;
} }
&__input-arrows--warning {
border: 1px solid $orange;
}
input[type="number"]::-webkit-inner-spin-button { input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;

@ -16,6 +16,7 @@ sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow')
sinon.spy(AdvancedTabContent.prototype, 'gasInput') sinon.spy(AdvancedTabContent.prototype, 'gasInput')
sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows') sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows')
sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary') sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary')
sinon.spy(AdvancedTabContent.prototype, 'gasInputError')
describe('AdvancedTabContent Component', function () { describe('AdvancedTabContent Component', function () {
let wrapper let wrapper
@ -29,6 +30,8 @@ describe('AdvancedTabContent Component', function () {
timeRemaining={21500} timeRemaining={21500}
totalFee={'$0.25'} totalFee={'$0.25'}
insufficientBalance={false} insufficientBalance={false}
customPriceIsSafe={true}
isSpeedUp={false}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
}) })
@ -86,9 +89,15 @@ describe('AdvancedTabContent Component', function () {
it('should call renderGasEditRows with the expected params', () => { it('should call renderGasEditRows with the expected params', () => {
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args
assert.deepEqual(renderGasEditRowArgs, [ assert.deepEqual(renderGasEditRowArgs, [{
11, propsMethodSpies.updateCustomGasPrice, 23456, propsMethodSpies.updateCustomGasLimit, false, 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(() => { beforeEach(() => {
AdvancedTabContent.prototype.gasInput.resetHistory() AdvancedTabContent.prototype.gasInput.resetHistory()
gasEditRow = shallow(wrapper.instance().renderGasEditRow( gasEditRow = shallow(wrapper.instance().renderGasEditRow({
'mockLabelKey', 'argA', 'argB' labelKey: 'mockLabelKey',
)) someArg: 'argA',
}))
}) })
it('should render the gas-edit-row root node', () => { 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', () => { it('should call this.gasInput with the correct args', () => {
const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.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', () => { it('should call this.renderGasEditRow twice, with the expected args', () => {
const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args
assert.equal(renderGasEditRowSpyArgs.length, 2) assert.equal(renderGasEditRowSpyArgs.length, 2)
assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [ assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [{
'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', false, true, labelKey: 'gasPrice',
].map(String)) value: 'mockGasLimit',
assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [ onChange: () => 'mockOnChangeGasLimit',
'gasLimit', 'mockGasLimit', () => 'mockOnChangeGasLimit', 'mockGasLimit', false, insufficientBalance: false,
].map(String)) 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(() => { beforeEach(() => {
AdvancedTabContent.prototype.renderGasEditRow.resetHistory() AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
gasInput = shallow(wrapper.instance().gasInput( AdvancedTabContent.prototype.gasInputError.resetHistory()
321, gasInput = shallow(wrapper.instance().gasInput({
value => value + 7, labelKey: 'gasPrice',
0, value: 321,
false, onChange: value => value + 7,
8 insufficientBalance: false,
)) showGWEI: true,
customPriceIsSafe: true,
isSpeedUp: false,
}))
}) })
it('should render the input-wrapper root node', () => { 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')) 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', () => { it('should call the passed onChange method with the value of the input onChange event', () => {
const inputOnChange = gasInput.find('input').props().onChange const inputOnChange = gasInput.find('input').props().onChange
assert.equal(inputOnChange({ target: { value: 8} }), 15) 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', () => { 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) const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0)
assert.equal(upArrow.props().onClick(), 329) assert.equal(upArrow.props().onClick(), 329)
const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1) const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1)
assert.equal(downArrow.props().onClick(), 327) 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)
})
}) })
}) })

@ -35,6 +35,9 @@ export default class GasModalPageContainer extends Component {
PropTypes.string, PropTypes.string,
PropTypes.number, PropTypes.number,
]), ]),
customPriceIsSafe: PropTypes.bool,
isSpeedUp: PropTypes.bool,
disableSave: PropTypes.bool,
} }
state = {} state = {}
@ -69,6 +72,8 @@ export default class GasModalPageContainer extends Component {
currentTimeEstimate, currentTimeEstimate,
insufficientBalance, insufficientBalance,
gasEstimatesLoading, gasEstimatesLoading,
customPriceIsSafe,
isSpeedUp,
}) { }) {
const { transactionFee } = this.props const { transactionFee } = this.props
return ( return (
@ -83,6 +88,8 @@ export default class GasModalPageContainer extends Component {
gasChartProps={gasChartProps} gasChartProps={gasChartProps}
insufficientBalance={insufficientBalance} insufficientBalance={insufficientBalance}
gasEstimatesLoading={gasEstimatesLoading} gasEstimatesLoading={gasEstimatesLoading}
customPriceIsSafe={customPriceIsSafe}
isSpeedUp={isSpeedUp}
/> />
) )
} }
@ -153,6 +160,7 @@ export default class GasModalPageContainer extends Component {
onSubmit, onSubmit,
customModalGasPriceInHex, customModalGasPriceInHex,
customModalGasLimitInHex, customModalGasLimitInHex,
disableSave,
...tabProps ...tabProps
} = this.props } = this.props
@ -162,7 +170,7 @@ export default class GasModalPageContainer extends Component {
title={this.context.t('customGas')} title={this.context.t('customGas')}
subtitle={this.context.t('customGasSubTitle')} subtitle={this.context.t('customGasSubTitle')}
tabsComponent={this.renderTabs(infoRowProps, tabProps)} tabsComponent={this.renderTabs(infoRowProps, tabProps)}
disabled={tabProps.insufficientBalance} disabled={disableSave}
onCancel={() => cancelAndClose()} onCancel={() => cancelAndClose()}
onClose={() => cancelAndClose()} onClose={() => cancelAndClose()}
onSubmit={() => { onSubmit={() => {

@ -40,6 +40,7 @@ import {
getEstimatedGasTimes, getEstimatedGasTimes,
getRenderableBasicEstimateData, getRenderableBasicEstimateData,
getBasicGasEstimateBlockTime, getBasicGasEstimateBlockTime,
isCustomPriceSafe,
} from '../../../selectors/custom-gas' } from '../../../selectors/custom-gas'
import { import {
submittedPendingTransactionsSelector, submittedPendingTransactionsSelector,
@ -107,6 +108,7 @@ const mapStateToProps = (state, ownProps) => {
newTotalFiat, newTotalFiat,
currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes), currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes),
blockTime: getBasicGasEstimateBlockTime(state), blockTime: getBasicGasEstimateBlockTime(state),
customPriceIsSafe: isCustomPriceSafe(state),
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {
buttonDataLoading, buttonDataLoading,
defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex),
@ -167,7 +169,7 @@ const mapDispatchToProps = dispatch => {
} }
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { gasPriceButtonGroupProps, isConfirm, isSpeedUp, txId } = stateProps const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, customGasPrice } = stateProps
const { const {
updateCustomGasPrice: dispatchUpdateCustomGasPrice, updateCustomGasPrice: dispatchUpdateCustomGasPrice,
hideGasButtonGroup: dispatchHideGasButtonGroup, hideGasButtonGroup: dispatchHideGasButtonGroup,
@ -208,6 +210,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
dispatchHideSidebar() dispatchHideSidebar()
} }
}, },
disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0),
} }
} }

@ -78,6 +78,7 @@ describe('GasModalPageContainer Component', function () {
customGasPriceInHex={'mockCustomGasPriceInHex'} customGasPriceInHex={'mockCustomGasPriceInHex'}
customGasLimitInHex={'mockCustomGasLimitInHex'} customGasLimitInHex={'mockCustomGasLimitInHex'}
insufficientBalance={false} insufficientBalance={false}
disableSave={false}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
}) })

@ -75,6 +75,7 @@ describe('gas-modal-page-container container', () => {
gas: { gas: {
basicEstimates: { basicEstimates: {
blockTime: 12, blockTime: 12,
safeLow: 2,
}, },
customData: { customData: {
limit: 'aaaaaaaa', limit: 'aaaaaaaa',
@ -107,9 +108,10 @@ describe('gas-modal-page-container container', () => {
blockTime: 12, blockTime: 12,
customModalGasLimitInHex: 'aaaaaaaa', customModalGasLimitInHex: 'aaaaaaaa',
customModalGasPriceInHex: 'ffffffff', customModalGasPriceInHex: 'ffffffff',
customPriceIsSafe: true,
gasChartProps: { gasChartProps: {
'currentPrice': 4.294967295, 'currentPrice': 4.294967295,
estimatedTimes: ['31', '62', '93', '124'], estimatedTimes: [31, 62, 93, 124],
estimatedTimesMax: '31', estimatedTimesMax: '31',
gasPrices: [3, 4, 5, 6], gasPrices: [3, 4, 5, 6],
gasPricesMax: 6, gasPricesMax: 6,

@ -11,7 +11,7 @@ import { formatDate } from '../../util'
import { import {
fetchBasicGasAndTimeEstimates, fetchBasicGasAndTimeEstimates,
fetchGasEstimates, fetchGasEstimates,
setCustomGasPrice, setCustomGasPriceForRetry,
setCustomGasLimit, setCustomGasLimit,
} from '../../ducks/gas.duck' } from '../../ducks/gas.duck'
@ -21,7 +21,7 @@ const mapDispatchToProps = dispatch => {
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)), setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
retryTransaction: (transaction, gasPrice) => { retryTransaction: (transaction, gasPrice) => {
dispatch(setCustomGasPrice(gasPrice || transaction.txParams.gasPrice)) dispatch(setCustomGasPriceForRetry(gasPrice || transaction.txParams.gasPrice))
dispatch(setCustomGasLimit(transaction.txParams.gas)) dispatch(setCustomGasLimit(transaction.txParams.gas))
dispatch(showSidebar({ dispatch(showSidebar({
transitionName: 'sidebar-left', transitionName: 'sidebar-left',

@ -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_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_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_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 // TODO: determine if this approach to initState is consistent with conventional ducks pattern
const initState = { const initState = {
@ -49,6 +50,7 @@ const initState = {
basicPriceAndTimeEstimates: [], basicPriceAndTimeEstimates: [],
priceAndTimeEstimatesLastRetrieved: 0, priceAndTimeEstimatesLastRetrieved: 0,
basicPriceAndTimeEstimatesLastRetrieved: 0, basicPriceAndTimeEstimatesLastRetrieved: 0,
basicPriceEstimatesLastRetrieved: 0,
errors: {}, errors: {},
} }
@ -129,6 +131,11 @@ export default function reducer ({ gas: gasState = initState }, action = {}) {
...newState, ...newState,
basicPriceAndTimeEstimatesLastRetrieved: action.value, basicPriceAndTimeEstimatesLastRetrieved: action.value,
} }
case SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED:
return {
...newState,
basicPriceEstimatesLastRetrieved: action.value,
}
case RESET_CUSTOM_DATA: case RESET_CUSTOM_DATA:
return { return {
...newState, ...newState,
@ -167,10 +174,17 @@ export function gasEstimatesLoadingFinished () {
} }
export function fetchBasicGasEstimates () { 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()) 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': {}, 'headers': {},
'referrer': 'https://dev.blockscale.net/api/', 'referrer': 'https://dev.blockscale.net/api/',
'referrerPolicy': 'no-referrer-when-downgrade', 'referrerPolicy': 'no-referrer-when-downgrade',
@ -195,10 +209,24 @@ export function fetchBasicGasEstimates () {
blockTime, blockTime,
blockNum, 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 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 () { export function resetCustomGasState () {
return { type: RESET_CUSTOM_GAS_STATE } return { type: RESET_CUSTOM_GAS_STATE }
} }

@ -20,6 +20,7 @@ const {
setCustomGasErrors, setCustomGasErrors,
resetCustomGasState, resetCustomGasState,
fetchBasicGasAndTimeEstimates, fetchBasicGasAndTimeEstimates,
fetchBasicGasEstimates,
gasEstimatesLoadingStarted, gasEstimatesLoadingStarted,
gasEstimatesLoadingFinished, gasEstimatesLoadingFinished,
setPricesAndTimeEstimates, setPricesAndTimeEstimates,
@ -43,6 +44,7 @@ describe('Gas Duck', () => {
safeLow: 10, safeLow: 10,
safeLowWait: 'mockSafeLowWait', safeLowWait: 'mockSafeLowWait',
speed: 'mockSpeed', speed: 'mockSpeed',
standard: 20,
} }
const mockPredictTableResponse = [ const mockPredictTableResponse = [
{ expectedTime: 400, expectedWait: 40, gasprice: 0.25, somethingElse: 'foobar' }, { 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' }, { expectedTime: 1, expectedWait: 0.5, gasprice: 20, somethingElse: 'foobar' },
] ]
const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => { const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => {
const dataToResolve = url.match(/ethgasAPI/) const dataToResolve = url.match(/ethgasAPI|gasexpress/)
? mockEthGasApiResponse ? mockEthGasApiResponse
: mockPredictTableResponse : mockPredictTableResponse
resolve({ resolve({
@ -83,6 +85,7 @@ describe('Gas Duck', () => {
}) })
afterEach(() => { afterEach(() => {
fetchStub.resetHistory()
global.fetch = tempFetch global.fetch = tempFetch
global.Date.now = tempDateNow global.Date.now = tempDateNow
}) })
@ -117,8 +120,7 @@ describe('Gas Duck', () => {
priceAndTimeEstimatesLastRetrieved: 0, priceAndTimeEstimatesLastRetrieved: 0,
basicPriceAndTimeEstimates: [], basicPriceAndTimeEstimates: [],
basicPriceAndTimeEstimatesLastRetrieved: 0, basicPriceAndTimeEstimatesLastRetrieved: 0,
basicPriceEstimatesLastRetrieved: 0,
} }
const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED' 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' 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_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_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_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()', () => { describe('GasReducer()', () => {
it('should initialize state', () => { 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', () => { describe('fetchBasicGasAndTimeEstimates', () => {
const mockDistpatch = sinon.spy() const mockDistpatch = sinon.spy()
it('should call fetch with the expected params', async () => { it('should call fetch with the expected params', async () => {

@ -2,6 +2,7 @@ import { pipe, partialRight } from 'ramda'
import { import {
conversionUtil, conversionUtil,
multiplyCurrencies, multiplyCurrencies,
conversionGreaterThan,
} from '../conversion-util' } from '../conversion-util'
import { import {
getCurrentCurrency, getCurrentCurrency,
@ -38,6 +39,8 @@ const selectors = {
getRenderableBasicEstimateData, getRenderableBasicEstimateData,
getRenderableEstimateDataForSmallButtonsFromGWEI, getRenderableEstimateDataForSmallButtonsFromGWEI,
priceEstimateToWei, priceEstimateToWei,
getSafeLowEstimate,
isCustomPriceSafe,
} }
module.exports = selectors 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) { function getBasicGasEstimateBlockTime (state) {
return state.gas.basicEstimates.blockTime return state.gas.basicEstimates.blockTime
} }

Loading…
Cancel
Save