diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index 2dcec91de..3a2a1f576 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -1,6 +1,9 @@ -import { mockGasEstimateData } from './mock-gas-estimate-data' import { clone, uniqBy } from 'ramda' import BigNumber from 'bignumber.js' +import { + loadLocalStorageData, + saveLocalStorageData, +} from '../../lib/local-storage-helpers' // Actions const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED' @@ -15,6 +18,7 @@ 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' +const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED' // TODO: determine if this approach to initState is consistent with conventional ducks pattern const initState = { @@ -38,6 +42,7 @@ const initState = { basicEstimateIsLoading: true, gasEstimatesLoading: true, priceAndTimeEstimates: [], + priceAndTimeEstimatesLastRetrieved: 0, errors: {}, } @@ -108,6 +113,11 @@ export default function reducer ({ gas: gasState = initState }, action = {}) { ...action.value, }, } + case SET_API_ESTIMATES_LAST_RETRIEVED: + return { + ...newState, + priceAndTimeEstimatesLastRetrieved: action.value, + } case RESET_CUSTOM_DATA: return { ...newState, @@ -192,34 +202,51 @@ export function fetchBasicGasEstimates () { } export function fetchGasEstimates (blockTime) { - return (dispatch) => { + return (dispatch, getState) => { + const { + priceAndTimeEstimatesLastRetrieved, + priceAndTimeEstimates, + } = getState().gas + const timeLastRetrieved = priceAndTimeEstimatesLastRetrieved || loadLocalStorageData('GAS_API_ESTIMATES_LAST_RETRIEVED') + 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).toString(10) - return { - expectedTime, - expectedWait, - gasprice, - } + const promiseToFetch = Date.now() - timeLastRetrieved > 75000 + ? fetch('https://ethgasstation.info/json/predictTable.json', { + 'headers': {}, + 'referrer': 'http://ethgasstation.info/json/', + 'referrerPolicy': 'no-referrer-when-downgrade', + 'body': null, + 'method': 'GET', + 'mode': 'cors'} + ) + .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).toString(10) + return { + expectedTime, + expectedWait, + gasprice, + } + }) + + const timeRetrieved = Date.now() + dispatch(setApiEstimatesLastRetrieved(timeRetrieved)) + saveLocalStorageData(timeRetrieved, 'GAS_API_ESTIMATES_LAST_RETRIEVED') + saveLocalStorageData(timeMappedToSeconds.slice(1), 'GAS_API_ESTIMATES') + + return timeMappedToSeconds.slice(1) }) - dispatch(setPricesAndTimeEstimates(timeMappedToSeconds.slice(1))) + : Promise.resolve(priceAndTimeEstimates.length + ? priceAndTimeEstimates + : loadLocalStorageData('GAS_API_ESTIMATES') + ) + + return promiseToFetch.then(estimates => { + dispatch(setPricesAndTimeEstimates(estimates)) dispatch(gasEstimatesLoadingFinished()) }) } @@ -267,6 +294,13 @@ export function setCustomGasErrors (newErrors) { } } +export function setApiEstimatesLastRetrieved (retrievalTime) { + return { + type: SET_API_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 464b122ae..96c00383b 100644 --- a/ui/app/ducks/tests/gas-duck.test.js +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -14,33 +14,52 @@ import GasReducer, { gasEstimatesLoadingStarted, gasEstimatesLoadingFinished, setPricesAndTimeEstimates, + fetchGasEstimates, + setApiEstimatesLastRetrieved, } from '../gas.duck.js' describe('Gas Duck', () => { let tempFetch - const fetchStub = sinon.stub().returns(new Promise(resolve => resolve({ - json: () => new Promise(resolve => resolve({ - average: 'mockAverage', - avgWait: 'mockAvgWait', - block_time: 'mockBlock_time', - blockNum: 'mockBlockNum', - fast: 'mockFast', - fastest: 'mockFastest', - fastestWait: 'mockFastestWait', - fastWait: 'mockFastWait', - safeLow: 'mockSafeLow', - safeLowWait: 'mockSafeLowWait', - speed: 'mockSpeed', - })), - }))) + let tempDateNow + const mockEthGasApiResponse = { + average: 'mockAverage', + avgWait: 'mockAvgWait', + block_time: 'mockBlock_time', + blockNum: 'mockBlockNum', + fast: 'mockFast', + fastest: 'mockFastest', + fastestWait: 'mockFastestWait', + fastWait: 'mockFastWait', + safeLow: 'mockSafeLow', + safeLowWait: 'mockSafeLowWait', + speed: 'mockSpeed', + } + const mockPredictTableResponse = [ + { expectedTime: 100, expectedWait: 10, gasprice: 1, somethingElse: 'foobar' }, + { expectedTime: 50, expectedWait: 5, gasprice: 2, somethingElse: 'foobar' }, + { expectedTime: 20, expectedWait: 4, gasprice: 4, somethingElse: 'foobar' }, + { expectedTime: 10, expectedWait: 2, gasprice: 10, somethingElse: 'foobar' }, + { expectedTime: 1, expectedWait: 0.5, gasprice: 20, somethingElse: 'foobar' }, + ] + const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => { + const dataToResolve = url.match(/ethgasAPI/) + ? mockEthGasApiResponse + : mockPredictTableResponse + resolve({ + json: () => new Promise(resolve => resolve(dataToResolve)), + }) + })) beforeEach(() => { tempFetch = global.fetch + tempDateNow = global.Date.now global.fetch = fetchStub + global.Date.now = () => 2000000 }) afterEach(() => { global.fetch = tempFetch + global.Date.now = tempDateNow }) const mockState = { @@ -70,6 +89,7 @@ describe('Gas Duck', () => { errors: {}, gasEstimatesLoading: true, priceAndTimeEstimates: [], + priceAndTimeEstimatesLastRetrieved: 0, } const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED' @@ -83,6 +103,7 @@ describe('Gas Duck', () => { 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' + const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED' describe('GasReducer()', () => { it('should initialize state', () => { @@ -193,6 +214,16 @@ describe('Gas Duck', () => { ) }) + it('should set priceAndTimeEstimatesLastRetrieved when receivinga SET_API_ESTIMATES_LAST_RETRIEVED action', () => { + assert.deepEqual( + GasReducer(mockState, { + type: SET_API_ESTIMATES_LAST_RETRIEVED, + value: 1500000000000, + }), + Object.assign({ priceAndTimeEstimatesLastRetrieved: 1500000000000 }, mockState.gas) + ) + }) + it('should set errors when receiving a SET_CUSTOM_GAS_ERRORS action', () => { assert.deepEqual( GasReducer(mockState, { @@ -279,6 +310,75 @@ describe('Gas Duck', () => { }) }) + describe('fetchGasEstimates', () => { + const mockDistpatch = sinon.spy() + it('should call fetch with the expected params', async () => { + global.fetch.resetHistory() + await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign( + {}, + initState, + { priceAndTimeEstimatesLastRetrieved: 1000000 } + ) })) + assert.deepEqual( + mockDistpatch.getCall(0).args, + [{ type: GAS_ESTIMATE_LOADING_STARTED} ] + ) + assert.deepEqual( + global.fetch.getCall(0).args, + [ + 'https://ethgasstation.info/json/predictTable.json', + { + 'headers': {}, + 'referrer': 'http://ethgasstation.info/json/', + 'referrerPolicy': 'no-referrer-when-downgrade', + 'body': null, + 'method': 'GET', + 'mode': 'cors', + }, + ] + ) + + assert.deepEqual( + mockDistpatch.getCall(1).args, + [{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 }] + ) + + assert.deepEqual( + mockDistpatch.getCall(2).args, + [{ + type: SET_PRICE_AND_TIME_ESTIMATES, + value: [ + { + expectedTime: '25', + expectedWait: 5, + gasprice: 2, + }, + { + expectedTime: '20', + expectedWait: 4, + gasprice: 4, + }, + { + expectedTime: '10', + expectedWait: 2, + gasprice: 10, + }, + { + expectedTime: '2.5', + expectedWait: 0.5, + gasprice: 20, + }, + ], + + }] + ) + assert.deepEqual( + mockDistpatch.getCall(3).args, + [{ type: GAS_ESTIMATE_LOADING_FINISHED }] + ) + }) + }) + describe('gasEstimatesLoadingStarted', () => { it('should create the correct action', () => { assert.deepEqual( @@ -351,6 +451,15 @@ describe('Gas Duck', () => { }) }) + describe('setApiEstimatesLastRetrieved', () => { + it('should create the correct action', () => { + assert.deepEqual( + setApiEstimatesLastRetrieved(1234), + { type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 1234 } + ) + }) + }) + describe('resetCustomGasState', () => { it('should create the correct action', () => { assert.deepEqual( diff --git a/ui/lib/local-storage-helpers.js b/ui/lib/local-storage-helpers.js new file mode 100644 index 000000000..287586c49 --- /dev/null +++ b/ui/lib/local-storage-helpers.js @@ -0,0 +1,20 @@ +export function loadLocalStorageData (itemKey) { + try { + const serializedData = localStorage.getItem(itemKey) + if (serializedData === null) { + return undefined + } + return JSON.parse(serializedData) + } catch (err) { + return undefined + } +} + +export function saveLocalStorageData (data, itemKey) { + try { + const serializedData = JSON.stringify(data) + localStorage.setItem(itemKey, serializedData) + } catch (err) { + console.warn(err) + } +}