diff --git a/package.json b/package.json index ac19dca58..7a71d40df 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "json-rpc-engine": "^5.3.0", "json-rpc-middleware-stream": "^2.1.1", "jsonschema": "^1.2.4", + "localforage": "^1.9.0", "lodash": "^4.17.19", "loglevel": "^1.4.1", "luxon": "^1.24.1", diff --git a/ui/app/ducks/gas/gas-duck.test.js b/ui/app/ducks/gas/gas-duck.test.js index 6db54d12a..ae770a732 100644 --- a/ui/app/ducks/gas/gas-duck.test.js +++ b/ui/app/ducks/gas/gas-duck.test.js @@ -2,10 +2,10 @@ import assert from 'assert' import sinon from 'sinon' import proxyquire from 'proxyquire' -const fakeLocalStorage = {} +const fakeStorage = {} const GasDuck = proxyquire('./gas.duck.js', { - '../../../lib/local-storage-helpers': fakeLocalStorage, + '../../../lib/storage-helpers': fakeStorage, }) const { @@ -160,8 +160,8 @@ describe('Gas Duck', function () { tempFetch = window.fetch tempDateNow = global.Date.now - fakeLocalStorage.loadLocalStorageData = sinon.stub() - fakeLocalStorage.saveLocalStorageData = sinon.spy() + fakeStorage.getStorageItem = sinon.stub() + fakeStorage.setStorageItem = sinon.spy() window.fetch = sinon.stub().callsFake(fakeFetch) global.Date.now = () => 2000000 }) @@ -412,21 +412,19 @@ describe('Gas Duck', function () { ]) }) - it('should fetch recently retrieved estimates from local storage', async function () { + it('should fetch recently retrieved estimates from storage', async function () { const mockDistpatch = sinon.spy() - fakeLocalStorage.loadLocalStorageData + fakeStorage.getStorageItem .withArgs('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') .returns(2000000 - 1) // one second ago from "now" - fakeLocalStorage.loadLocalStorageData - .withArgs('BASIC_PRICE_ESTIMATES') - .returns({ - average: 25, - blockTime: 'mockBlock_time', - blockNum: 'mockBlockNum', - fast: 35, - fastest: 45, - safeLow: 15, - }) + fakeStorage.getStorageItem.withArgs('BASIC_PRICE_ESTIMATES').returns({ + average: 25, + blockTime: 'mockBlock_time', + blockNum: 'mockBlockNum', + fast: 35, + fastest: 45, + safeLow: 15, + }) await fetchBasicGasEstimates()(mockDistpatch, () => ({ gas: { ...initState }, @@ -453,9 +451,9 @@ describe('Gas Duck', function () { ]) }) - it('should fallback to network if retrieving estimates from local storage fails', async function () { + it('should fallback to network if retrieving estimates from storage fails', async function () { const mockDistpatch = sinon.spy() - fakeLocalStorage.loadLocalStorageData + fakeStorage.getStorageItem .withArgs('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') .returns(2000000 - 1) // one second ago from "now" @@ -541,12 +539,12 @@ describe('Gas Duck', function () { ]) }) - it('should fetch recently retrieved estimates from local storage', async function () { + it('should fetch recently retrieved estimates from storage', async function () { const mockDistpatch = sinon.spy() - fakeLocalStorage.loadLocalStorageData + fakeStorage.getStorageItem .withArgs('BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') .returns(2000000 - 1) // one second ago from "now" - fakeLocalStorage.loadLocalStorageData + fakeStorage.getStorageItem .withArgs('BASIC_GAS_AND_TIME_API_ESTIMATES') .returns({ average: 5, @@ -596,9 +594,9 @@ describe('Gas Duck', function () { ]) }) - it('should fallback to network if retrieving estimates from local storage fails', async function () { + it('should fallback to network if retrieving estimates from storage fails', async function () { const mockDistpatch = sinon.spy() - fakeLocalStorage.loadLocalStorageData + fakeStorage.getStorageItem .withArgs('BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') .returns(2000000 - 1) // one second ago from "now" diff --git a/ui/app/ducks/gas/gas.duck.js b/ui/app/ducks/gas/gas.duck.js index 5a937e744..a1a8cce4d 100644 --- a/ui/app/ducks/gas/gas.duck.js +++ b/ui/app/ducks/gas/gas.duck.js @@ -1,9 +1,7 @@ import { uniqBy, cloneDeep, flatten } from 'lodash' import BigNumber from 'bignumber.js' -import { - loadLocalStorageData, - saveLocalStorageData, -} from '../../../lib/local-storage-helpers' +import { getStorageItem, setStorageItem } from '../../../lib/storage-helpers' + import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util' import { isEthereumNetwork } from '../../selectors' @@ -209,7 +207,7 @@ export function fetchBasicGasEstimates() { const { basicPriceEstimatesLastRetrieved } = getState().gas const timeLastRetrieved = basicPriceEstimatesLastRetrieved || - loadLocalStorageData('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') || + (await getStorageItem('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED')) || 0 dispatch(basicGasEstimatesLoadingStarted()) @@ -218,7 +216,7 @@ export function fetchBasicGasEstimates() { if (Date.now() - timeLastRetrieved > 75000) { basicEstimates = await fetchExternalBasicGasEstimates(dispatch) } else { - const cachedBasicEstimates = loadLocalStorageData('BASIC_PRICE_ESTIMATES') + const cachedBasicEstimates = await getStorageItem('BASIC_PRICE_ESTIMATES') basicEstimates = cachedBasicEstimates || (await fetchExternalBasicGasEstimates(dispatch)) } @@ -259,8 +257,10 @@ async function fetchExternalBasicGasEstimates(dispatch) { } const timeRetrieved = Date.now() - saveLocalStorageData(basicEstimates, 'BASIC_PRICE_ESTIMATES') - saveLocalStorageData(timeRetrieved, 'BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') + await Promise.all([ + setStorageItem('BASIC_PRICE_ESTIMATES', basicEstimates), + setStorageItem('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED', timeRetrieved), + ]) dispatch(setBasicPriceEstimatesLastRetrieved(timeRetrieved)) return basicEstimates @@ -271,7 +271,9 @@ export function fetchBasicGasAndTimeEstimates() { const { basicPriceAndTimeEstimatesLastRetrieved } = getState().gas const timeLastRetrieved = basicPriceAndTimeEstimatesLastRetrieved || - loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') || + (await getStorageItem( + 'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED', + )) || 0 dispatch(basicGasEstimatesLoadingStarted()) @@ -280,7 +282,7 @@ export function fetchBasicGasAndTimeEstimates() { if (Date.now() - timeLastRetrieved > 75000) { basicEstimates = await fetchExternalBasicGasAndTimeEstimates(dispatch) } else { - const cachedBasicEstimates = loadLocalStorageData( + const cachedBasicEstimates = await getStorageItem( 'BASIC_GAS_AND_TIME_API_ESTIMATES', ) basicEstimates = @@ -332,11 +334,13 @@ async function fetchExternalBasicGasAndTimeEstimates(dispatch) { } const timeRetrieved = Date.now() - saveLocalStorageData(basicEstimates, 'BASIC_GAS_AND_TIME_API_ESTIMATES') - saveLocalStorageData( - timeRetrieved, - 'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED', - ) + await Promise.all([ + setStorageItem('BASIC_GAS_AND_TIME_API_ESTIMATES', basicEstimates), + setStorageItem( + 'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED', + timeRetrieved, + ), + ]) dispatch(setBasicApiEstimatesLastRetrieved(timeRetrieved)) return basicEstimates @@ -403,11 +407,11 @@ function inliersByIQR(data, prop) { } export function fetchGasEstimates(blockTime) { - return (dispatch, getState) => { + return async (dispatch, getState) => { const state = getState() if (!isEthereumNetwork(state)) { - return Promise.resolve(null) + return } const { @@ -416,114 +420,112 @@ export function fetchGasEstimates(blockTime) { } = state.gas const timeLastRetrieved = priceAndTimeEstimatesLastRetrieved || - loadLocalStorageData('GAS_API_ESTIMATES_LAST_RETRIEVED') || + (await getStorageItem('GAS_API_ESTIMATES_LAST_RETRIEVED')) || 0 dispatch(gasEstimatesLoadingStarted()) - const promiseToFetch = - Date.now() - timeLastRetrieved > 75000 - ? queryEthGasStationPredictionTable() - .then((r) => r.json()) - .then((r) => { - const estimatedPricesAndTimes = r.map( - ({ expectedTime, expectedWait, gasprice }) => ({ - expectedTime, - expectedWait, - gasprice, - }), - ) - const estimatedTimeWithUniquePrices = uniqBy( - estimatedPricesAndTimes, - ({ expectedTime }) => expectedTime, - ) + const shouldGetFreshGasQuote = Date.now() - timeLastRetrieved > 75000 + let estimates + if (shouldGetFreshGasQuote) { + const response = await queryEthGasStationPredictionTable() + const tableJson = await response.json() - const withSupplementalTimeEstimates = flatten( - estimatedTimeWithUniquePrices.map( - ({ expectedWait, gasprice }, i, arr) => { - const next = arr[i + 1] - if (!next) { - return [{ expectedWait, gasprice }] - } - const supplementalPrice = getRandomArbitrary( - gasprice, - next.gasprice, - ) - const supplementalTime = extrapolateY({ - higherY: next.expectedWait, - lowerY: expectedWait, - higherX: next.gasprice, - lowerX: gasprice, - xForExtrapolation: supplementalPrice, - }) - const supplementalPrice2 = getRandomArbitrary( - supplementalPrice, - next.gasprice, - ) - const supplementalTime2 = extrapolateY({ - higherY: next.expectedWait, - lowerY: supplementalTime, - higherX: next.gasprice, - lowerX: supplementalPrice, - xForExtrapolation: supplementalPrice2, - }) - return [ - { expectedWait, gasprice }, - { - expectedWait: supplementalTime, - gasprice: supplementalPrice, - }, - { - expectedWait: supplementalTime2, - gasprice: supplementalPrice2, - }, - ] - }, - ), - ) - const withOutliersRemoved = inliersByIQR( - withSupplementalTimeEstimates.slice(0).reverse(), - 'expectedWait', - ).reverse() - const timeMappedToSeconds = withOutliersRemoved.map( - ({ expectedWait, gasprice }) => { - const expectedTime = new BigNumber(expectedWait) - .times(Number(blockTime), 10) - .toNumber() - return { - expectedTime, - gasprice: new BigNumber(gasprice, 10).toNumber(), - } - }, - ) + const estimatedPricesAndTimes = tableJson.map( + ({ expectedTime, expectedWait, gasprice }) => ({ + expectedTime, + expectedWait, + gasprice, + }), + ) + const estimatedTimeWithUniquePrices = uniqBy( + estimatedPricesAndTimes, + ({ expectedTime }) => expectedTime, + ) - const timeRetrieved = Date.now() - dispatch(setApiEstimatesLastRetrieved(timeRetrieved)) - saveLocalStorageData( - timeRetrieved, - 'GAS_API_ESTIMATES_LAST_RETRIEVED', - ) - saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES') - - return timeMappedToSeconds + const withSupplementalTimeEstimates = flatten( + estimatedTimeWithUniquePrices.map( + ({ expectedWait, gasprice }, i, arr) => { + const next = arr[i + 1] + if (!next) { + return [{ expectedWait, gasprice }] + } + const supplementalPrice = getRandomArbitrary( + gasprice, + next.gasprice, + ) + const supplementalTime = extrapolateY({ + higherY: next.expectedWait, + lowerY: expectedWait, + higherX: next.gasprice, + lowerX: gasprice, + xForExtrapolation: supplementalPrice, }) - : Promise.resolve( - priceAndTimeEstimates.length - ? priceAndTimeEstimates - : loadLocalStorageData('GAS_API_ESTIMATES'), - ) + const supplementalPrice2 = getRandomArbitrary( + supplementalPrice, + next.gasprice, + ) + const supplementalTime2 = extrapolateY({ + higherY: next.expectedWait, + lowerY: supplementalTime, + higherX: next.gasprice, + lowerX: supplementalPrice, + xForExtrapolation: supplementalPrice2, + }) + return [ + { expectedWait, gasprice }, + { + expectedWait: supplementalTime, + gasprice: supplementalPrice, + }, + { + expectedWait: supplementalTime2, + gasprice: supplementalPrice2, + }, + ] + }, + ), + ) + const withOutliersRemoved = inliersByIQR( + withSupplementalTimeEstimates.slice(0).reverse(), + 'expectedWait', + ).reverse() + const timeMappedToSeconds = withOutliersRemoved.map( + ({ expectedWait, gasprice }) => { + const expectedTime = new BigNumber(expectedWait) + .times(Number(blockTime), 10) + .toNumber() + return { + expectedTime, + gasprice: new BigNumber(gasprice, 10).toNumber(), + } + }, + ) - return promiseToFetch.then((estimates) => { - dispatch(setPricesAndTimeEstimates(estimates)) - dispatch(gasEstimatesLoadingFinished()) - }) + const timeRetrieved = Date.now() + dispatch(setApiEstimatesLastRetrieved(timeRetrieved)) + + await Promise.all([ + setStorageItem('GAS_API_ESTIMATES_LAST_RETRIEVED', timeRetrieved), + setStorageItem('GAS_API_ESTIMATES', timeMappedToSeconds), + ]) + + estimates = timeMappedToSeconds + } else if (priceAndTimeEstimates.length) { + estimates = priceAndTimeEstimates + } else { + estimates = await getStorageItem('GAS_API_ESTIMATES') + } + + dispatch(setPricesAndTimeEstimates(estimates)) + dispatch(gasEstimatesLoadingFinished()) } } export function setCustomGasPriceForRetry(newPrice) { - return (dispatch) => { + return async (dispatch) => { if (newPrice === '0x0') { - const { fast } = loadLocalStorageData('BASIC_PRICE_ESTIMATES') + const { fast } = await getStorageItem('BASIC_PRICE_ESTIMATES') dispatch(setCustomGasPrice(decGWEIToHexWEI(fast))) } else { dispatch(setCustomGasPrice(newPrice)) diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index 998648b35..9ff86dbd8 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -2,10 +2,8 @@ import { createSlice } from '@reduxjs/toolkit' import BigNumber from 'bignumber.js' import log from 'loglevel' -import { - loadLocalStorageData, - saveLocalStorageData, -} from '../../../lib/local-storage-helpers' +import { getStorageItem, setStorageItem } from '../../../lib/storage-helpers' + import { addToken, addUnapprovedTransaction, @@ -754,7 +752,7 @@ export function fetchMetaSwapsGasPriceEstimates() { ) const timeLastRetrieved = priceEstimatesLastRetrieved || - loadLocalStorageData('METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED') || + (await getStorageItem('METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED')) || 0 dispatch(swapGasPriceEstimatesFetchStarted()) @@ -764,7 +762,7 @@ export function fetchMetaSwapsGasPriceEstimates() { if (Date.now() - timeLastRetrieved > 30000) { priceEstimates = await fetchSwapsGasPrices() } else { - const cachedPriceEstimates = loadLocalStorageData( + const cachedPriceEstimates = await getStorageItem( 'METASWAP_GAS_PRICE_ESTIMATES', ) priceEstimates = cachedPriceEstimates || (await fetchSwapsGasPrices()) @@ -795,11 +793,13 @@ export function fetchMetaSwapsGasPriceEstimates() { const timeRetrieved = Date.now() - saveLocalStorageData(priceEstimates, 'METASWAP_GAS_PRICE_ESTIMATES') - saveLocalStorageData( - timeRetrieved, - 'METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED', - ) + await Promise.all([ + setStorageItem('METASWAP_GAS_PRICE_ESTIMATES', priceEstimates), + setStorageItem( + 'METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED', + timeRetrieved, + ), + ]) dispatch( swapGasPriceEstimatesFetchCompleted({ diff --git a/ui/app/helpers/utils/fetch-with-cache.js b/ui/app/helpers/utils/fetch-with-cache.js index c04d54086..351ee4ab9 100644 --- a/ui/app/helpers/utils/fetch-with-cache.js +++ b/ui/app/helpers/utils/fetch-with-cache.js @@ -1,7 +1,4 @@ -import { - loadLocalStorageData, - saveLocalStorageData, -} from '../../../lib/local-storage-helpers' +import { getStorageItem, setStorageItem } from '../../../lib/storage-helpers' import fetchWithTimeout from '../../../../app/scripts/lib/fetch-with-timeout' const fetchWithCache = async ( @@ -27,7 +24,7 @@ const fetchWithCache = async ( } const currentTime = Date.now() - const cachedFetch = loadLocalStorageData('cachedFetch') || {} + const cachedFetch = (await getStorageItem('cachedFetch')) || {} const { cachedResponse, cachedTime } = cachedFetch[url] || {} if (cachedResponse && currentTime - cachedTime < cacheRefreshTime) { return cachedResponse @@ -52,7 +49,7 @@ const fetchWithCache = async ( cachedTime: currentTime, } cachedFetch[url] = cacheEntry - saveLocalStorageData(cachedFetch, 'cachedFetch') + await setStorageItem('cachedFetch', cachedFetch) return responseJson } diff --git a/ui/app/helpers/utils/fetch-with-cache.test.js b/ui/app/helpers/utils/fetch-with-cache.test.js index c5981abd9..c0fcda3fc 100644 --- a/ui/app/helpers/utils/fetch-with-cache.test.js +++ b/ui/app/helpers/utils/fetch-with-cache.test.js @@ -3,15 +3,15 @@ import nock from 'nock' import sinon from 'sinon' import proxyquire from 'proxyquire' -const fakeLocalStorageHelpers = {} +const fakeStorage = {} const fetchWithCache = proxyquire('./fetch-with-cache', { - '../../../lib/local-storage-helpers': fakeLocalStorageHelpers, + '../../../lib/storage-helpers': fakeStorage, }).default describe('Fetch with cache', function () { beforeEach(function () { - fakeLocalStorageHelpers.loadLocalStorageData = sinon.stub() - fakeLocalStorageHelpers.saveLocalStorageData = sinon.stub() + fakeStorage.getStorageItem = sinon.stub() + fakeStorage.setStorageItem = sinon.stub() }) afterEach(function () { sinon.restore() @@ -36,7 +36,7 @@ describe('Fetch with cache', function () { .get('/price') .reply(200, '{"average": 2}') - fakeLocalStorageHelpers.loadLocalStorageData.returns({ + fakeStorage.getStorageItem.returns({ 'https://fetchwithcache.metamask.io/price': { cachedResponse: { average: 1 }, cachedTime: Date.now(), @@ -56,7 +56,7 @@ describe('Fetch with cache', function () { .get('/price') .reply(200, '{"average": 3}') - fakeLocalStorageHelpers.loadLocalStorageData.returns({ + fakeStorage.getStorageItem.returns({ 'https://fetchwithcache.metamask.io/cached': { cachedResponse: { average: 1 }, cachedTime: Date.now() - 1000, diff --git a/ui/app/hooks/useRetryTransaction.js b/ui/app/hooks/useRetryTransaction.js index c12000266..154e99853 100644 --- a/ui/app/hooks/useRetryTransaction.js +++ b/ui/app/hooks/useRetryTransaction.js @@ -38,7 +38,7 @@ export function useRetryTransaction(transactionGroup) { await dispatch(fetchGasEstimates(basicEstimates.blockTime)) const transaction = initialTransaction const increasedGasPrice = increaseLastGasPrice(gasPrice) - dispatch( + await dispatch( setCustomGasPriceForRetry( increasedGasPrice || transaction.txParams.gasPrice, ), diff --git a/ui/lib/local-storage-helpers.js b/ui/lib/local-storage-helpers.js deleted file mode 100644 index 58dc637ed..000000000 --- a/ui/lib/local-storage-helpers.js +++ /dev/null @@ -1,20 +0,0 @@ -export function loadLocalStorageData(itemKey) { - try { - const serializedData = window.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) - window.localStorage.setItem(itemKey, serializedData) - } catch (err) { - console.warn(err) - } -} diff --git a/ui/lib/storage-helpers.js b/ui/lib/storage-helpers.js new file mode 100644 index 000000000..737147405 --- /dev/null +++ b/ui/lib/storage-helpers.js @@ -0,0 +1,23 @@ +import localforage from 'localforage' + +export async function getStorageItem(key) { + try { + const serializedData = await localforage.getItem(key) + if (serializedData === null) { + return undefined + } + + return JSON.parse(serializedData) + } catch (err) { + return undefined + } +} + +export async function setStorageItem(key, value) { + try { + const serializedData = JSON.stringify(value) + await localforage.setItem(key, serializedData) + } catch (err) { + console.warn(err) + } +} diff --git a/yarn.lock b/yarn.lock index f12ac1a5a..c33e7a68d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16608,6 +16608,13 @@ localforage@1.8.1: dependencies: lie "3.1.1" +localforage@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1" + integrity sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g== + dependencies: + lie "3.1.1" + localstorage-down@^0.6.7: version "0.6.7" resolved "https://registry.yarnpkg.com/localstorage-down/-/localstorage-down-0.6.7.tgz#d0799a93b31e6c5fa5188ec06242eb1cce9d6d15"