mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
ec40b2a325
* Remove unused state 'gas.basicPriceAndTimeEstimates' * Check for invalid estimates from local storage Gas estimates were being cached in local storage then later retrieved, but the retrieved values were not being checked. If the data failed to save, failed to load, or was cleared since being saved, it would result in the gas estimates being set to undefined. The estimates retrieved from local storage are now checked before they are used. If they are falsy, the estimates are retrieved from the network instead. This should fix this Sentry issue: [METAMASK-6W0T](https://sentry.io/share/issue/cfe470314a5741768b19050815322aa4/) A few additional changes were made to the gas-duck tests to accommodate the use of `sinon.restore`. `restore` is strongly recommended by the `sinon` team, as neglecting to use it can result in memory leaks. It has the additional benefit of ensuring you create fresh stubs/spies for each test, which means they no longer need to be reset between tests.
524 lines
15 KiB
JavaScript
524 lines
15 KiB
JavaScript
import { clone, uniqBy, flatten } from 'ramda'
|
|
import BigNumber from 'bignumber.js'
|
|
import {
|
|
loadLocalStorageData,
|
|
saveLocalStorageData,
|
|
} from '../../../lib/local-storage-helpers'
|
|
import {
|
|
decGWEIToHexWEI,
|
|
} from '../../helpers/utils/conversions.util'
|
|
import {
|
|
isEthereumNetwork,
|
|
} from '../../selectors/selectors'
|
|
|
|
// 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'
|
|
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'
|
|
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 = {
|
|
customData: {
|
|
price: null,
|
|
limit: null,
|
|
},
|
|
basicEstimates: {
|
|
average: null,
|
|
fastestWait: null,
|
|
fastWait: null,
|
|
fast: null,
|
|
safeLowWait: null,
|
|
blockNum: null,
|
|
avgWait: null,
|
|
blockTime: null,
|
|
speed: null,
|
|
fastest: null,
|
|
safeLow: null,
|
|
},
|
|
basicEstimateIsLoading: true,
|
|
gasEstimatesLoading: true,
|
|
priceAndTimeEstimates: [],
|
|
priceAndTimeEstimatesLastRetrieved: 0,
|
|
basicPriceAndTimeEstimatesLastRetrieved: 0,
|
|
basicPriceEstimatesLastRetrieved: 0,
|
|
errors: {},
|
|
}
|
|
|
|
// Reducer
|
|
export default function reducer ({ gas: gasState = initState }, action = {}) {
|
|
const newState = clone(gasState)
|
|
|
|
switch (action.type) {
|
|
case BASIC_GAS_ESTIMATE_LOADING_STARTED:
|
|
return {
|
|
...newState,
|
|
basicEstimateIsLoading: true,
|
|
}
|
|
case BASIC_GAS_ESTIMATE_LOADING_FINISHED:
|
|
return {
|
|
...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,
|
|
basicEstimates: action.value,
|
|
}
|
|
case SET_CUSTOM_GAS_PRICE:
|
|
return {
|
|
...newState,
|
|
customData: {
|
|
...newState.customData,
|
|
price: action.value,
|
|
},
|
|
}
|
|
case SET_CUSTOM_GAS_LIMIT:
|
|
return {
|
|
...newState,
|
|
customData: {
|
|
...newState.customData,
|
|
limit: action.value,
|
|
},
|
|
}
|
|
case SET_CUSTOM_GAS_TOTAL:
|
|
return {
|
|
...newState,
|
|
customData: {
|
|
...newState.customData,
|
|
total: action.value,
|
|
},
|
|
}
|
|
case SET_PRICE_AND_TIME_ESTIMATES:
|
|
return {
|
|
...newState,
|
|
priceAndTimeEstimates: action.value,
|
|
}
|
|
case SET_CUSTOM_GAS_ERRORS:
|
|
return {
|
|
...newState,
|
|
errors: {
|
|
...newState.errors,
|
|
...action.value,
|
|
},
|
|
}
|
|
case SET_API_ESTIMATES_LAST_RETRIEVED:
|
|
return {
|
|
...newState,
|
|
priceAndTimeEstimatesLastRetrieved: action.value,
|
|
}
|
|
case SET_BASIC_API_ESTIMATES_LAST_RETRIEVED:
|
|
return {
|
|
...newState,
|
|
basicPriceAndTimeEstimatesLastRetrieved: action.value,
|
|
}
|
|
case SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED:
|
|
return {
|
|
...newState,
|
|
basicPriceEstimatesLastRetrieved: action.value,
|
|
}
|
|
case RESET_CUSTOM_DATA:
|
|
return {
|
|
...newState,
|
|
customData: clone(initState.customData),
|
|
}
|
|
case RESET_CUSTOM_GAS_STATE:
|
|
return clone(initState)
|
|
default:
|
|
return newState
|
|
}
|
|
}
|
|
|
|
// Action Creators
|
|
export function basicGasEstimatesLoadingStarted () {
|
|
return {
|
|
type: BASIC_GAS_ESTIMATE_LOADING_STARTED,
|
|
}
|
|
}
|
|
|
|
export function basicGasEstimatesLoadingFinished () {
|
|
return {
|
|
type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
|
|
}
|
|
}
|
|
|
|
export function gasEstimatesLoadingStarted () {
|
|
return {
|
|
type: GAS_ESTIMATE_LOADING_STARTED,
|
|
}
|
|
}
|
|
|
|
export function gasEstimatesLoadingFinished () {
|
|
return {
|
|
type: GAS_ESTIMATE_LOADING_FINISHED,
|
|
}
|
|
}
|
|
|
|
export function fetchBasicGasEstimates () {
|
|
return async (dispatch, getState) => {
|
|
const { basicPriceEstimatesLastRetrieved } = getState().gas
|
|
const timeLastRetrieved = basicPriceEstimatesLastRetrieved || loadLocalStorageData('BASIC_PRICE_ESTIMATES_LAST_RETRIEVED') || 0
|
|
|
|
dispatch(basicGasEstimatesLoadingStarted())
|
|
|
|
let basicEstimates
|
|
if (Date.now() - timeLastRetrieved > 75000) {
|
|
basicEstimates = await fetchExternalBasicGasEstimates(dispatch)
|
|
} else {
|
|
const cachedBasicEstimates = loadLocalStorageData('BASIC_PRICE_ESTIMATES')
|
|
basicEstimates = cachedBasicEstimates || await fetchExternalBasicGasEstimates(dispatch)
|
|
}
|
|
|
|
dispatch(setBasicGasEstimateData(basicEstimates))
|
|
dispatch(basicGasEstimatesLoadingFinished())
|
|
|
|
return basicEstimates
|
|
}
|
|
}
|
|
|
|
async function fetchExternalBasicGasEstimates (dispatch) {
|
|
const response = await fetch('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'}
|
|
)
|
|
const {
|
|
safeLow,
|
|
standard: average,
|
|
fast,
|
|
fastest,
|
|
block_time: blockTime,
|
|
blockNum,
|
|
} = await response.json()
|
|
|
|
const basicEstimates = {
|
|
safeLow,
|
|
average,
|
|
fast,
|
|
fastest,
|
|
blockTime,
|
|
blockNum,
|
|
}
|
|
|
|
const timeRetrieved = Date.now()
|
|
saveLocalStorageData(basicEstimates, 'BASIC_PRICE_ESTIMATES')
|
|
saveLocalStorageData(timeRetrieved, 'BASIC_PRICE_ESTIMATES_LAST_RETRIEVED')
|
|
dispatch(setBasicPriceEstimatesLastRetrieved(timeRetrieved))
|
|
|
|
return basicEstimates
|
|
}
|
|
|
|
export function fetchBasicGasAndTimeEstimates () {
|
|
return async (dispatch, getState) => {
|
|
const { basicPriceAndTimeEstimatesLastRetrieved } = getState().gas
|
|
const timeLastRetrieved = basicPriceAndTimeEstimatesLastRetrieved || loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') || 0
|
|
|
|
dispatch(basicGasEstimatesLoadingStarted())
|
|
|
|
let basicEstimates
|
|
if (Date.now() - timeLastRetrieved > 75000) {
|
|
basicEstimates = await fetchExternalBasicGasAndTimeEstimates(dispatch)
|
|
} else {
|
|
const cachedBasicEstimates = loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES')
|
|
basicEstimates = cachedBasicEstimates || await fetchExternalBasicGasAndTimeEstimates(dispatch)
|
|
}
|
|
|
|
dispatch(setBasicGasEstimateData(basicEstimates))
|
|
dispatch(basicGasEstimatesLoadingFinished())
|
|
return basicEstimates
|
|
}
|
|
}
|
|
|
|
async function fetchExternalBasicGasAndTimeEstimates (dispatch) {
|
|
const response = await fetch('https://ethgasstation.info/json/ethgasAPI.json', {
|
|
'headers': {},
|
|
'referrer': 'http://ethgasstation.info/json/',
|
|
'referrerPolicy': 'no-referrer-when-downgrade',
|
|
'body': null,
|
|
'method': 'GET',
|
|
'mode': 'cors'}
|
|
)
|
|
const {
|
|
average: averageTimes10,
|
|
avgWait,
|
|
block_time: blockTime,
|
|
blockNum,
|
|
fast: fastTimes10,
|
|
fastest: fastestTimes10,
|
|
fastestWait,
|
|
fastWait,
|
|
safeLow: safeLowTimes10,
|
|
safeLowWait,
|
|
speed,
|
|
} = await response.json()
|
|
const [average, fast, fastest, safeLow] = [
|
|
averageTimes10,
|
|
fastTimes10,
|
|
fastestTimes10,
|
|
safeLowTimes10,
|
|
].map(price => (new BigNumber(price)).div(10).toNumber())
|
|
|
|
const basicEstimates = {
|
|
average,
|
|
avgWait,
|
|
blockTime,
|
|
blockNum,
|
|
fast,
|
|
fastest,
|
|
fastestWait,
|
|
fastWait,
|
|
safeLow,
|
|
safeLowWait,
|
|
speed,
|
|
}
|
|
|
|
const timeRetrieved = Date.now()
|
|
saveLocalStorageData(basicEstimates, 'BASIC_GAS_AND_TIME_API_ESTIMATES')
|
|
saveLocalStorageData(timeRetrieved, 'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED')
|
|
dispatch(setBasicApiEstimatesLastRetrieved(timeRetrieved))
|
|
|
|
return basicEstimates
|
|
}
|
|
|
|
function extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) {
|
|
higherY = new BigNumber(higherY, 10)
|
|
lowerY = new BigNumber(lowerY, 10)
|
|
higherX = new BigNumber(higherX, 10)
|
|
lowerX = new BigNumber(lowerX, 10)
|
|
xForExtrapolation = new BigNumber(xForExtrapolation, 10)
|
|
const slope = (higherY.minus(lowerY)).div(higherX.minus(lowerX))
|
|
const newTimeEstimate = slope.times(higherX.minus(xForExtrapolation)).minus(higherY).negated()
|
|
|
|
return Number(newTimeEstimate.toPrecision(10))
|
|
}
|
|
|
|
function getRandomArbitrary (min, max) {
|
|
min = new BigNumber(min, 10)
|
|
max = new BigNumber(max, 10)
|
|
const random = new BigNumber(String(Math.random()), 10)
|
|
return new BigNumber(random.times(max.minus(min)).plus(min)).toPrecision(10)
|
|
}
|
|
|
|
function calcMedian (list) {
|
|
const medianPos = (Math.floor(list.length / 2) + Math.ceil(list.length / 2)) / 2
|
|
return medianPos === Math.floor(medianPos)
|
|
? (list[medianPos - 1] + list[medianPos]) / 2
|
|
: list[Math.floor(medianPos)]
|
|
}
|
|
|
|
function quartiles (data) {
|
|
const lowerHalf = data.slice(0, Math.floor(data.length / 2))
|
|
const upperHalf = data.slice(Math.floor(data.length / 2) + (data.length % 2 === 0 ? 0 : 1))
|
|
const median = calcMedian(data)
|
|
const lowerQuartile = calcMedian(lowerHalf)
|
|
const upperQuartile = calcMedian(upperHalf)
|
|
return {
|
|
median,
|
|
lowerQuartile,
|
|
upperQuartile,
|
|
}
|
|
}
|
|
|
|
function inliersByIQR (data, prop) {
|
|
const { lowerQuartile, upperQuartile } = quartiles(data.map(d => prop ? d[prop] : d))
|
|
const IQR = upperQuartile - lowerQuartile
|
|
const lowerBound = lowerQuartile - 1.5 * IQR
|
|
const upperBound = upperQuartile + 1.5 * IQR
|
|
return data.filter(d => {
|
|
const value = prop ? d[prop] : d
|
|
return value >= lowerBound && value <= upperBound
|
|
})
|
|
}
|
|
|
|
export function fetchGasEstimates (blockTime) {
|
|
return (dispatch, getState) => {
|
|
const state = getState()
|
|
|
|
if (!isEthereumNetwork(state)) {
|
|
return Promise.resolve(null)
|
|
}
|
|
|
|
const {
|
|
priceAndTimeEstimatesLastRetrieved,
|
|
priceAndTimeEstimates,
|
|
} = state.gas
|
|
const timeLastRetrieved = priceAndTimeEstimatesLastRetrieved || loadLocalStorageData('GAS_API_ESTIMATES_LAST_RETRIEVED') || 0
|
|
|
|
dispatch(gasEstimatesLoadingStarted())
|
|
|
|
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 withSupplementalTimeEstimates = flatten(estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }, i, arr) => {
|
|
const next = arr[i + 1]
|
|
if (!next) {
|
|
return [{ expectedWait, gasprice }]
|
|
} else {
|
|
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 timeRetrieved = Date.now()
|
|
dispatch(setApiEstimatesLastRetrieved(timeRetrieved))
|
|
saveLocalStorageData(timeRetrieved, 'GAS_API_ESTIMATES_LAST_RETRIEVED')
|
|
saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES')
|
|
|
|
return timeMappedToSeconds
|
|
})
|
|
: Promise.resolve(priceAndTimeEstimates.length
|
|
? priceAndTimeEstimates
|
|
: loadLocalStorageData('GAS_API_ESTIMATES')
|
|
)
|
|
|
|
return promiseToFetch.then(estimates => {
|
|
dispatch(setPricesAndTimeEstimates(estimates))
|
|
dispatch(gasEstimatesLoadingFinished())
|
|
})
|
|
}
|
|
}
|
|
|
|
export function setCustomGasPriceForRetry (newPrice) {
|
|
return (dispatch) => {
|
|
if (newPrice !== '0x0') {
|
|
dispatch(setCustomGasPrice(newPrice))
|
|
} else {
|
|
const { fast } = loadLocalStorageData('BASIC_PRICE_ESTIMATES')
|
|
dispatch(setCustomGasPrice(decGWEIToHexWEI(fast)))
|
|
}
|
|
}
|
|
}
|
|
|
|
export function setBasicGasEstimateData (basicGasEstimateData) {
|
|
return {
|
|
type: SET_BASIC_GAS_ESTIMATE_DATA,
|
|
value: basicGasEstimateData,
|
|
}
|
|
}
|
|
|
|
export function setPricesAndTimeEstimates (estimatedPricesAndTimes) {
|
|
return {
|
|
type: SET_PRICE_AND_TIME_ESTIMATES,
|
|
value: estimatedPricesAndTimes,
|
|
}
|
|
}
|
|
|
|
export function setCustomGasPrice (newPrice) {
|
|
return {
|
|
type: SET_CUSTOM_GAS_PRICE,
|
|
value: newPrice,
|
|
}
|
|
}
|
|
|
|
export function setCustomGasLimit (newLimit) {
|
|
return {
|
|
type: SET_CUSTOM_GAS_LIMIT,
|
|
value: newLimit,
|
|
}
|
|
}
|
|
|
|
export function setCustomGasTotal (newTotal) {
|
|
return {
|
|
type: SET_CUSTOM_GAS_TOTAL,
|
|
value: newTotal,
|
|
}
|
|
}
|
|
|
|
export function setCustomGasErrors (newErrors) {
|
|
return {
|
|
type: SET_CUSTOM_GAS_ERRORS,
|
|
value: newErrors,
|
|
}
|
|
}
|
|
|
|
export function setApiEstimatesLastRetrieved (retrievalTime) {
|
|
return {
|
|
type: SET_API_ESTIMATES_LAST_RETRIEVED,
|
|
value: retrievalTime,
|
|
}
|
|
}
|
|
|
|
export function setBasicApiEstimatesLastRetrieved (retrievalTime) {
|
|
return {
|
|
type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED,
|
|
value: retrievalTime,
|
|
}
|
|
}
|
|
|
|
export function setBasicPriceEstimatesLastRetrieved (retrievalTime) {
|
|
return {
|
|
type: SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED,
|
|
value: retrievalTime,
|
|
}
|
|
}
|
|
|
|
export function resetCustomGasState () {
|
|
return { type: RESET_CUSTOM_GAS_STATE }
|
|
}
|
|
|
|
export function resetCustomData () {
|
|
return { type: RESET_CUSTOM_DATA }
|
|
}
|