1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-20 00:13:22 +01:00
metamask-extension/ui/app/ducks/gas/gas.duck.js

604 lines
17 KiB
JavaScript
Raw Normal View History

2020-07-08 22:17:53 +02:00
import { uniqBy, cloneDeep, flatten } from 'lodash'
import BigNumber from 'bignumber.js'
import {
loadLocalStorageData,
saveLocalStorageData,
} from '../../../lib/local-storage-helpers'
2020-11-03 00:41:28 +01:00
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util'
import { isEthereumNetwork } from '../../selectors'
// Actions
2020-11-03 00:41:28 +01:00
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'
2020-11-03 00:41:28 +01:00
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'
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
2020-11-03 00:41:28 +01:00
export default function reducer(state = initState, action) {
switch (action.type) {
case BASIC_GAS_ESTIMATE_LOADING_STARTED:
return {
...state,
basicEstimateIsLoading: true,
}
case BASIC_GAS_ESTIMATE_LOADING_FINISHED:
return {
...state,
basicEstimateIsLoading: false,
}
case GAS_ESTIMATE_LOADING_STARTED:
return {
...state,
gasEstimatesLoading: true,
}
case GAS_ESTIMATE_LOADING_FINISHED:
return {
...state,
gasEstimatesLoading: false,
}
case SET_BASIC_GAS_ESTIMATE_DATA:
return {
...state,
basicEstimates: action.value,
}
case SET_CUSTOM_GAS_PRICE:
return {
...state,
customData: {
...state.customData,
price: action.value,
},
}
case SET_CUSTOM_GAS_LIMIT:
return {
...state,
customData: {
...state.customData,
limit: action.value,
},
}
case SET_CUSTOM_GAS_TOTAL:
return {
...state,
customData: {
...state.customData,
total: action.value,
},
}
case SET_PRICE_AND_TIME_ESTIMATES:
return {
...state,
priceAndTimeEstimates: action.value,
}
case SET_CUSTOM_GAS_ERRORS:
return {
...state,
errors: {
...state.errors,
...action.value,
},
}
case SET_API_ESTIMATES_LAST_RETRIEVED:
return {
...state,
priceAndTimeEstimatesLastRetrieved: action.value,
}
case SET_BASIC_API_ESTIMATES_LAST_RETRIEVED:
return {
...state,
basicPriceAndTimeEstimatesLastRetrieved: action.value,
}
case SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED:
return {
...state,
basicPriceEstimatesLastRetrieved: action.value,
}
case RESET_CUSTOM_DATA:
return {
...state,
2020-07-08 22:17:53 +02:00
customData: cloneDeep(initState.customData),
}
case RESET_CUSTOM_GAS_STATE:
2020-07-08 22:17:53 +02:00
return cloneDeep(initState)
default:
return state
}
}
// Action Creators
2020-11-03 00:41:28 +01:00
export function basicGasEstimatesLoadingStarted() {
return {
type: BASIC_GAS_ESTIMATE_LOADING_STARTED,
}
}
2020-11-03 00:41:28 +01:00
export function basicGasEstimatesLoadingFinished() {
return {
type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
}
}
2020-11-03 00:41:28 +01:00
export function gasEstimatesLoadingStarted() {
return {
type: GAS_ESTIMATE_LOADING_STARTED,
}
}
2020-11-03 00:41:28 +01:00
export function gasEstimatesLoadingFinished() {
return {
type: GAS_ESTIMATE_LOADING_FINISHED,
}
}
2020-11-03 00:41:28 +01:00
async function queryEthGasStationBasic() {
const apiKey = process.env.ETH_GAS_STATION_API_KEY
? `?api-key=${process.env.ETH_GAS_STATION_API_KEY}`
: ''
const url = `https://ethgasstation.info/json/ethgasAPI.json${apiKey}`
return await window.fetch(url, {
2020-11-03 00:41:28 +01:00
headers: {},
referrer: 'http://ethgasstation.info/json/',
referrerPolicy: 'no-referrer-when-downgrade',
body: null,
method: 'GET',
mode: 'cors',
})
}
2020-11-03 00:41:28 +01:00
async function queryEthGasStationPredictionTable() {
const apiKey = process.env.ETH_GAS_STATION_API_KEY
? `?api-key=${process.env.ETH_GAS_STATION_API_KEY}`
: ''
const url = `https://ethgasstation.info/json/predictTable.json${apiKey}`
return await window.fetch(url, {
2020-11-03 00:41:28 +01:00
headers: {},
referrer: 'http://ethgasstation.info/json/',
referrerPolicy: 'no-referrer-when-downgrade',
body: null,
method: 'GET',
mode: 'cors',
})
}
2020-11-03 00:41:28 +01:00
export function fetchBasicGasEstimates() {
return async (dispatch, getState) => {
const { basicPriceEstimatesLastRetrieved } = getState().gas
2020-11-03 00:41:28 +01:00
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')
2020-11-03 00:41:28 +01:00
basicEstimates =
cachedBasicEstimates || (await fetchExternalBasicGasEstimates(dispatch))
}
dispatch(setBasicGasEstimateData(basicEstimates))
dispatch(basicGasEstimatesLoadingFinished())
return basicEstimates
}
}
2020-11-03 00:41:28 +01:00
async function fetchExternalBasicGasEstimates(dispatch) {
const response = await queryEthGasStationBasic()
const {
safeLow: safeLowTimes10,
average: averageTimes10,
fast: fastTimes10,
fastest: fastestTimes10,
block_time: blockTime,
blockNum,
} = await response.json()
const [average, fast, fastest, safeLow] = [
averageTimes10,
fastTimes10,
fastestTimes10,
safeLowTimes10,
2020-11-03 00:41:28 +01:00
].map((price) => new BigNumber(price).div(10).toNumber())
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
}
2020-11-03 00:41:28 +01:00
export function fetchBasicGasAndTimeEstimates() {
return async (dispatch, getState) => {
const { basicPriceAndTimeEstimatesLastRetrieved } = getState().gas
2020-11-03 00:41:28 +01:00
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 {
2020-11-03 00:41:28 +01:00
const cachedBasicEstimates = loadLocalStorageData(
'BASIC_GAS_AND_TIME_API_ESTIMATES',
)
basicEstimates =
cachedBasicEstimates ||
(await fetchExternalBasicGasAndTimeEstimates(dispatch))
}
2018-11-27 18:30:41 +01:00
dispatch(setBasicGasEstimateData(basicEstimates))
dispatch(basicGasEstimatesLoadingFinished())
return basicEstimates
}
}
2020-11-03 00:41:28 +01:00
async function fetchExternalBasicGasAndTimeEstimates(dispatch) {
const response = await queryEthGasStationBasic()
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,
2020-11-03 00:41:28 +01:00
].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')
2020-11-03 00:41:28 +01:00
saveLocalStorageData(
timeRetrieved,
'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED',
)
dispatch(setBasicApiEstimatesLastRetrieved(timeRetrieved))
return basicEstimates
}
2020-11-03 00:41:28 +01:00
function extrapolateY({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) {
/* eslint-disable no-param-reassign */
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)
/* eslint-enable no-param-reassign */
2020-11-03 00:41:28 +01:00
const slope = higherY.minus(lowerY).div(higherX.minus(lowerX))
const newTimeEstimate = slope
.times(higherX.minus(xForExtrapolation))
.minus(higherY)
.negated()
return Number(newTimeEstimate.toPrecision(10))
}
2020-11-03 00:41:28 +01:00
function getRandomArbitrary(minStr, maxStr) {
const min = new BigNumber(minStr, 10)
const max = new BigNumber(maxStr, 10)
const random = new BigNumber(String(Math.random()), 10)
return new BigNumber(random.times(max.minus(min)).plus(min)).toPrecision(10)
}
2020-11-03 00:41:28 +01:00
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)]
}
2020-11-03 00:41:28 +01:00
function quartiles(data) {
const lowerHalf = data.slice(0, Math.floor(data.length / 2))
2020-11-03 00:41:28 +01:00
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,
}
}
2020-11-03 00:41:28 +01:00
function inliersByIQR(data, prop) {
const { lowerQuartile, upperQuartile } = quartiles(
data.map((d) => (prop ? d[prop] : d)),
)
const IQR = upperQuartile - lowerQuartile
2020-11-03 00:41:28 +01:00
const lowerBound = lowerQuartile - 1.5 * IQR
const upperBound = upperQuartile + 1.5 * IQR
2020-02-15 21:34:12 +01:00
return data.filter((d) => {
const value = prop ? d[prop] : d
return value >= lowerBound && value <= upperBound
})
}
2020-11-03 00:41:28 +01:00
export function fetchGasEstimates(blockTime) {
return (dispatch, getState) => {
const state = getState()
if (!isEthereumNetwork(state)) {
return Promise.resolve(null)
}
const {
priceAndTimeEstimatesLastRetrieved,
priceAndTimeEstimates,
} = state.gas
2020-11-03 00:41:28 +01:00
const timeLastRetrieved =
priceAndTimeEstimatesLastRetrieved ||
loadLocalStorageData('GAS_API_ESTIMATES_LAST_RETRIEVED') ||
0
dispatch(gasEstimatesLoadingStarted())
2020-11-03 00:41:28 +01:00
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 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 timeRetrieved = Date.now()
dispatch(setApiEstimatesLastRetrieved(timeRetrieved))
saveLocalStorageData(
timeRetrieved,
'GAS_API_ESTIMATES_LAST_RETRIEVED',
)
saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES')
return timeMappedToSeconds
})
2020-11-03 00:41:28 +01:00
: Promise.resolve(
priceAndTimeEstimates.length
? priceAndTimeEstimates
: loadLocalStorageData('GAS_API_ESTIMATES'),
)
2020-02-15 21:34:12 +01:00
return promiseToFetch.then((estimates) => {
dispatch(setPricesAndTimeEstimates(estimates))
dispatch(gasEstimatesLoadingFinished())
})
}
}
2020-11-03 00:41:28 +01:00
export function setCustomGasPriceForRetry(newPrice) {
2018-12-09 21:48:06 +01:00
return (dispatch) => {
if (newPrice === '0x0') {
2018-12-09 21:48:06 +01:00
const { fast } = loadLocalStorageData('BASIC_PRICE_ESTIMATES')
dispatch(setCustomGasPrice(decGWEIToHexWEI(fast)))
} else {
dispatch(setCustomGasPrice(newPrice))
2018-12-09 21:48:06 +01:00
}
}
}
2020-11-03 00:41:28 +01:00
export function setBasicGasEstimateData(basicGasEstimateData) {
return {
type: SET_BASIC_GAS_ESTIMATE_DATA,
value: basicGasEstimateData,
}
}
2020-11-03 00:41:28 +01:00
export function setPricesAndTimeEstimates(estimatedPricesAndTimes) {
return {
type: SET_PRICE_AND_TIME_ESTIMATES,
value: estimatedPricesAndTimes,
}
}
2020-11-03 00:41:28 +01:00
export function setCustomGasPrice(newPrice) {
return {
type: SET_CUSTOM_GAS_PRICE,
value: newPrice,
}
}
2020-11-03 00:41:28 +01:00
export function setCustomGasLimit(newLimit) {
return {
type: SET_CUSTOM_GAS_LIMIT,
value: newLimit,
}
}
2020-11-03 00:41:28 +01:00
export function setCustomGasTotal(newTotal) {
return {
type: SET_CUSTOM_GAS_TOTAL,
value: newTotal,
}
}
2020-11-03 00:41:28 +01:00
export function setCustomGasErrors(newErrors) {
return {
type: SET_CUSTOM_GAS_ERRORS,
value: newErrors,
}
}
2020-11-03 00:41:28 +01:00
export function setApiEstimatesLastRetrieved(retrievalTime) {
return {
type: SET_API_ESTIMATES_LAST_RETRIEVED,
value: retrievalTime,
}
}
2020-11-03 00:41:28 +01:00
export function setBasicApiEstimatesLastRetrieved(retrievalTime) {
return {
type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED,
value: retrievalTime,
}
}
2020-11-03 00:41:28 +01:00
export function setBasicPriceEstimatesLastRetrieved(retrievalTime) {
return {
type: SET_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED,
value: retrievalTime,
}
}
2020-11-03 00:41:28 +01:00
export function resetCustomGasState() {
return { type: RESET_CUSTOM_GAS_STATE }
}
2020-11-03 00:41:28 +01:00
export function resetCustomData() {
return { type: RESET_CUSTOM_DATA }
}