Read only connection of gas price chart to redux

feature/default_network_editable
Dan Miller 6 years ago
parent 2dbae581ac
commit a2bbf504b8
  1. 4
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
  2. 2
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
  3. 4
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  4. 4
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
  5. 45
      ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js
  6. 6
      ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
  7. 4
      ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
  8. 5
      ui/app/components/send/send.component.js
  9. 4
      ui/app/components/send/send.container.js
  10. 38
      ui/app/components/send/tests/send-component.test.js
  11. 85
      ui/app/ducks/gas.duck.js
  12. 3
      ui/app/ducks/mock-gas-estimate-data.js
  13. 69
      ui/app/ducks/tests/gas-duck.test.js

@ -15,6 +15,7 @@ export default class AdvancedTabContent extends Component {
millisecondsRemaining: PropTypes.number,
totalFee: PropTypes.string,
timeRemaining: PropTypes.string,
gasChartProps: PropTypes.object,
}
gasInput (value, onChange, min, precision, showGWEI) {
@ -82,6 +83,7 @@ export default class AdvancedTabContent extends Component {
customGasPrice,
customGasLimit,
totalFee,
gasChartProps,
} = this.props
return (
@ -95,7 +97,7 @@ export default class AdvancedTabContent extends Component {
updateCustomGasLimit
) }
<div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div>
<GasPriceChart />
<GasPriceChart {...gasChartProps} />
<div className="advanced-tab__fee-chart__speed-buttons">
<span>Slower</span>
<span>Faster</span>

@ -46,6 +46,7 @@ export default class GasModalPageContainer extends Component {
customGasPrice,
customGasLimit,
newTotalFiat,
gasChartProps,
}) {
const { transactionFee } = this.props
return (
@ -57,6 +58,7 @@ export default class GasModalPageContainer extends Component {
timeRemaining="1 min 31 sec"
transactionFee={transactionFee}
totalFee={newTotalFiat}
gasChartProps={gasChartProps}
/>
)
}

@ -73,12 +73,14 @@ const mapStateToProps = state => {
customGasPrice: calcCustomGasPrice(customModalGasPriceInHex),
customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
newTotalFiat,
transactionFee: addHexWEIsToRenderableFiat('0x0', customGasTotal, currentCurrency, conversionRate),
gasPriceButtonGroupProps: {
buttonDataLoading,
defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex),
gasButtonInfo,
},
gasChartProps: {
priceAndTimeEstimates: state.gas.priceAndTimeEstimates,
},
infoRowProps: {
originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate),
originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal),

@ -75,6 +75,7 @@ describe('gas-modal-page-container container', () => {
limit: 'aaaaaaaa',
price: 'ffffffff',
},
priceAndTimeEstimates: 'mockPriceAndTimeEstimates',
},
confirmTransaction: {
txData: {
@ -95,6 +96,9 @@ describe('gas-modal-page-container container', () => {
newTotalFiat: '637.41',
customModalGasLimitInHex: 'aaaaaaaa',
customModalGasPriceInHex: 'ffffffff',
gasChartProps: {
priceAndTimeEstimates: 'mockPriceAndTimeEstimates',
},
gasPriceButtonGroupProps: {
buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4',
defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff',

@ -1,18 +1,57 @@
import React from 'react'
import assert from 'assert'
import proxyquire from 'proxyquire'
import sinon from 'sinon'
import shallow from '../../../../../lib/shallow-with-context'
import GasPriceChart from '../gas-price-chart.component.js'
import * as d3 from 'd3'
const mockSelectReturn = {
...d3.select('div'),
node: () => ({
getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }),
}),
select: d3.select,
attr: sinon.spy(),
on: sinon.spy(),
}
const GasPriceChart = proxyquire('../gas-price-chart.component.js', {
'c3': {
generate: function () {
return {
internal: {
showTooltip: () => {},
showXGridFocus: () => {},
},
}
},
},
'd3': {
...d3,
select: function (...args) {
const result = d3.select(...args)
return result.empty()
? mockSelectReturn
: result
},
},
}).default
describe('GasPriceChart Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<GasPriceChart />)
wrapper = shallow(<GasPriceChart
priceAndTimeEstimates={[
{ gasprice: 1, expectedTime: 10 },
{ gasprice: 2, expectedTime: 20 },
{ gasprice: 3, expectedTime: 30 },
]}
/>)
})
describe('render()', () => {
it('should render', () => {
console.log('wrapper', wrapper.html())
assert(wrapper.hasClass('gas-price-chart'))
})

@ -32,7 +32,7 @@ export default class ConfirmTransaction extends Component {
setTransactionToConfirm: PropTypes.func,
confirmTransaction: PropTypes.object,
clearConfirmTransaction: PropTypes.func,
fetchGasEstimates: PropTypes.func,
fetchBasicGasEstimates: PropTypes.func,
}
getParamsTransactionId () {
@ -46,7 +46,7 @@ export default class ConfirmTransaction extends Component {
send = {},
history,
confirmTransaction: { txData: { id: transactionId } = {} },
fetchGasEstimates,
fetchBasicGasEstimates,
} = this.props
if (!totalUnapprovedCount && !send.to) {
@ -55,7 +55,7 @@ export default class ConfirmTransaction extends Component {
}
if (!transactionId) {
fetchGasEstimates()
fetchBasicGasEstimates()
this.setTransactionToConfirm()
}
}

@ -6,7 +6,7 @@ import {
clearConfirmTransaction,
} from '../../../ducks/confirm-transaction.duck'
import {
fetchGasEstimates,
fetchBasicGasEstimates,
} from '../../../ducks/gas.duck'
import ConfirmTransaction from './confirm-transaction.component'
import { getTotalUnapprovedCount } from '../../../selectors'
@ -27,7 +27,7 @@ const mapDispatchToProps = dispatch => {
return {
setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
fetchGasEstimates: () => dispatch(fetchGasEstimates()),
fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
}
}

@ -163,9 +163,10 @@ export default class SendTransactionScreen extends PersistentForm {
}
componentDidMount () {
this.props.fetchGasEstimates()
.then(() => {
this.props.fetchBasicGasEstimates()
.then(basicEstimates => {
this.updateGas()
this.props.fetchGasEstimates(basicEstimates.blockTime)
})
}

@ -37,6 +37,7 @@ import {
updateSendErrors,
} from '../../ducks/send.duck'
import {
fetchBasicGasEstimates,
fetchGasEstimates,
} from '../../ducks/gas.duck'
import {
@ -107,6 +108,7 @@ function mapDispatchToProps (dispatch) {
scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
fetchGasEstimates: () => dispatch(fetchGasEstimates()),
fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
}
}

@ -8,12 +8,23 @@ import SendHeader from '../send-header/send-header.container'
import SendContent from '../send-content/send-content.component'
import SendFooter from '../send-footer/send-footer.container'
function timeout (time) {
return new Promise((resolve, reject) => {
setTimeout(resolve, time || 1500)
})
}
const mockBasicGasEstimates = {
blockTime: 'mockBlockTime',
}
const propsMethodSpies = {
updateAndSetGasLimit: sinon.spy(),
updateSendErrors: sinon.spy(),
updateSendTokenBalance: sinon.spy(),
resetSendState: sinon.spy(),
fetchGasEstimates: sinon.stub().returns(Promise.resolve()),
fetchBasicGasEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)),
fetchGasEstimates: sinon.spy(),
}
const utilsMethodStubs = {
getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }),
@ -38,6 +49,7 @@ describe('Send Component', function () {
blockGasLimit={'mockBlockGasLimit'}
conversionRate={10}
editingTransactionId={'mockEditingTransactionId'}
fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates}
fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
from={ { address: 'mockAddress', balance: 'mockBalance' } }
gasLimit={'mockGasLimit'}
@ -65,7 +77,7 @@ describe('Send Component', function () {
utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory()
utilsMethodStubs.getAmountErrorObject.resetHistory()
utilsMethodStubs.getGasFeeErrorObject.resetHistory()
propsMethodSpies.fetchGasEstimates.resetHistory()
propsMethodSpies.fetchBasicGasEstimates.resetHistory()
propsMethodSpies.updateAndSetGasLimit.resetHistory()
propsMethodSpies.updateSendErrors.resetHistory()
propsMethodSpies.updateSendTokenBalance.resetHistory()
@ -76,19 +88,29 @@ describe('Send Component', function () {
})
describe('componentDidMount', () => {
it('should call props.fetchGasEstimates', () => {
propsMethodSpies.fetchGasEstimates.resetHistory()
assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 0)
it('should call props.fetchBasicGasEstimates', () => {
propsMethodSpies.fetchBasicGasEstimates.resetHistory()
assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 0)
wrapper.instance().componentDidMount()
assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1)
assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 1)
})
it('should call this.updateGas', () => {
it('should call this.updateGas', async () => {
SendTransactionScreen.prototype.updateGas.resetHistory()
propsMethodSpies.updateSendErrors.resetHistory()
assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
wrapper.instance().componentDidMount()
setTimeout(() => assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1), 250)
await timeout(250)
assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1)
})
it('should call props.fetchGasEstimates with the block time returned by fetchBasicGasEstimates', async () => {
propsMethodSpies.fetchGasEstimates.resetHistory()
assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 0)
wrapper.instance().componentDidMount()
await timeout(250)
assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1)
assert.equal(propsMethodSpies.fetchGasEstimates.getCall(0).args[0], 'mockBlockTime')
})
})

@ -1,8 +1,12 @@
import { clone } from 'ramda'
import { mockGasEstimateData } from './mock-gas-estimate-data'
import { clone, uniqBy } from 'ramda'
import BigNumber from 'bignumber.js'
// Actions
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 GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED'
const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED'
const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE'
const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA'
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'
@ -10,6 +14,7 @@ const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS'
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'
const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL'
const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES'
// TODO: determine if this approach to initState is consistent with conventional ducks pattern
const initState = {
@ -31,6 +36,8 @@ const initState = {
safeLow: null,
},
basicEstimateIsLoading: true,
gasEstimatesLoading: true,
priceAndTimeEstimates: [],
errors: {},
}
@ -49,6 +56,16 @@ export default function reducer ({ gas: gasState = initState }, action = {}) {
...newState,
basicEstimateIsLoading: false,
}
case GAS_ESTIMATE_LOADING_STARTED:
return {
...newState,
gasEstimatesLoading: true,
}
case GAS_ESTIMATE_LOADING_FINISHED:
return {
...newState,
gasEstimatesLoading: false,
}
case SET_BASIC_GAS_ESTIMATE_DATA:
return {
...newState,
@ -78,6 +95,11 @@ export default function reducer ({ gas: gasState = initState }, action = {}) {
total: action.value,
},
}
case SET_PRICE_AND_TIME_ESTIMATES:
return {
...newState,
priceAndTimeEstimates: action.value,
}
case SET_CUSTOM_GAS_ERRORS:
return {
...newState,
@ -111,7 +133,19 @@ export function basicGasEstimatesLoadingFinished () {
}
}
export function fetchGasEstimates () {
export function gasEstimatesLoadingStarted () {
return {
type: GAS_ESTIMATE_LOADING_STARTED,
}
}
export function gasEstimatesLoadingFinished () {
return {
type: GAS_ESTIMATE_LOADING_FINISHED,
}
}
export function fetchBasicGasEstimates () {
return (dispatch) => {
dispatch(basicGasEstimatesLoadingStarted())
@ -137,7 +171,7 @@ export function fetchGasEstimates () {
safeLowWait,
speed,
}) => {
dispatch(setBasicGasEstimateData({
const basicEstimates = {
average,
avgWait,
blockTime,
@ -149,8 +183,44 @@ export function fetchGasEstimates () {
safeLow,
safeLowWait,
speed,
}))
}
dispatch(setBasicGasEstimateData(basicEstimates))
dispatch(basicGasEstimatesLoadingFinished())
return basicEstimates
})
}
}
export function fetchGasEstimates (blockTime) {
return (dispatch) => {
dispatch(gasEstimatesLoadingStarted())
// TODO: uncomment code when live api is ready
// return fetch('https://ethgasstation.info/json/predictTable.json', {
// 'headers': {},
// 'referrer': 'http://ethgasstation.info/json/',
// 'referrerPolicy': 'no-referrer-when-downgrade',
// 'body': null,
// 'method': 'GET',
// 'mode': 'cors'}
// )
return new Promise(resolve => {
resolve(mockGasEstimateData)
})
// .then(r => r.json())
.then(r => {
const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice }))
const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes)
const timeMappedToSeconds = estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }) => {
const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).div(60, 10).toString(10)
return {
expectedTime,
expectedWait,
gasprice,
}
})
dispatch(setPricesAndTimeEstimates(timeMappedToSeconds.slice(1)))
dispatch(gasEstimatesLoadingFinished())
})
}
}
@ -162,6 +232,13 @@ export function setBasicGasEstimateData (basicGasEstimateData) {
}
}
export function setPricesAndTimeEstimates (estimatedPricesAndTimes) {
return {
type: SET_PRICE_AND_TIME_ESTIMATES,
value: estimatedPricesAndTimes,
}
}
export function setCustomGasPrice (newPrice) {
return {
type: SET_CUSTOM_GAS_PRICE,

File diff suppressed because one or more lines are too long

@ -10,7 +10,10 @@ import GasReducer, {
setCustomGasTotal,
setCustomGasErrors,
resetCustomGasState,
fetchGasEstimates,
fetchBasicGasEstimates,
gasEstimatesLoadingStarted,
gasEstimatesLoadingFinished,
setPricesAndTimeEstimates,
} from '../gas.duck.js'
describe('Gas Duck', () => {
@ -65,15 +68,21 @@ describe('Gas Duck', () => {
},
basicEstimateIsLoading: true,
errors: {},
gasEstimatesLoading: true,
priceAndTimeEstimates: [],
}
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 GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED'
const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED'
const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE'
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'
const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS'
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'
const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL'
const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES'
describe('GasReducer()', () => {
it('should initialize state', () => {
@ -111,6 +120,24 @@ describe('Gas Duck', () => {
)
})
it('should set gasEstimatesLoading to true when receiving a GAS_ESTIMATE_LOADING_STARTED action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: GAS_ESTIMATE_LOADING_STARTED,
}),
Object.assign({gasEstimatesLoading: true}, mockState.gas)
)
})
it('should set gasEstimatesLoading to false when receiving a GAS_ESTIMATE_LOADING_FINISHED action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: GAS_ESTIMATE_LOADING_FINISHED,
}),
Object.assign({gasEstimatesLoading: false}, mockState.gas)
)
})
it('should return a new object (and not just modify the existing state object)', () => {
assert.deepEqual(GasReducer(mockState), mockState.gas)
assert.notEqual(GasReducer(mockState), mockState.gas)
@ -126,6 +153,16 @@ describe('Gas Duck', () => {
)
})
it('should set priceAndTimeEstimates when receiving a SET_PRICE_AND_TIME_ESTIMATES action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_PRICE_AND_TIME_ESTIMATES,
value: { someProp: 'someData123' },
}),
Object.assign({priceAndTimeEstimates: {someProp: 'someData123'} }, mockState.gas)
)
})
it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => {
assert.deepEqual(
GasReducer(mockState, {
@ -194,10 +231,10 @@ describe('Gas Duck', () => {
})
})
describe('fetchGasEstimates', () => {
describe('fetchBasicGasEstimates', () => {
const mockDistpatch = sinon.spy()
it('should call fetch with the expected params', async () => {
await fetchGasEstimates()(mockDistpatch)
await fetchBasicGasEstimates()(mockDistpatch)
assert.deepEqual(
mockDistpatch.getCall(0).args,
[{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ]
@ -242,6 +279,32 @@ describe('Gas Duck', () => {
})
})
describe('gasEstimatesLoadingStarted', () => {
it('should create the correct action', () => {
assert.deepEqual(
gasEstimatesLoadingStarted(),
{ type: GAS_ESTIMATE_LOADING_STARTED }
)
})
})
describe('gasEstimatesLoadingFinished', () => {
it('should create the correct action', () => {
assert.deepEqual(
gasEstimatesLoadingFinished(),
{ type: GAS_ESTIMATE_LOADING_FINISHED }
)
})
})
describe('setPricesAndTimeEstimates', () => {
it('should create the correct action', () => {
assert.deepEqual(
setPricesAndTimeEstimates('mockPricesAndTimeEstimates'),
{ type: SET_PRICE_AND_TIME_ESTIMATES, value: 'mockPricesAndTimeEstimates' }
)
})
})
describe('setBasicGasEstimateData', () => {
it('should create the correct action', () => {

Loading…
Cancel
Save