mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Fetch swap quote refresh time from API (#10069)
This commit is contained in:
parent
820decab1c
commit
c4aa0b3c9a
@ -17,6 +17,7 @@ import {
|
||||
import {
|
||||
fetchTradesInfo as defaultFetchTradesInfo,
|
||||
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness,
|
||||
fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime,
|
||||
} from '../../../ui/app/pages/swaps/swaps.util'
|
||||
|
||||
const METASWAP_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'
|
||||
@ -28,6 +29,14 @@ const MAX_GAS_LIMIT = 2500000
|
||||
// 3 seems to be an appropriate balance of giving users the time they need when MetaMask is not left idle, and turning polling off when it is.
|
||||
const POLL_COUNT_LIMIT = 3
|
||||
|
||||
// If for any reason the MetaSwap API fails to provide a refresh time,
|
||||
// provide a reasonable fallback to avoid further errors
|
||||
const FALLBACK_QUOTE_REFRESH_TIME = 60000
|
||||
|
||||
// This is the amount of time to wait, after successfully fetching quotes
|
||||
// and their gas estimates, before fetching for new quotes
|
||||
const QUOTE_POLLING_DIFFERENCE_INTERVAL = 10 * 1000
|
||||
|
||||
function calculateGasEstimateWithRefund(
|
||||
maxGas = MAX_GAS_LIMIT,
|
||||
estimatedRefund = 0,
|
||||
@ -42,9 +51,6 @@ function calculateGasEstimateWithRefund(
|
||||
return gasEstimateWithRefund
|
||||
}
|
||||
|
||||
// This is the amount of time to wait, after successfully fetching quotes and their gas estimates, before fetching for new quotes
|
||||
const QUOTE_POLLING_INTERVAL = 50 * 1000
|
||||
|
||||
const initialState = {
|
||||
swapsState: {
|
||||
quotes: {},
|
||||
@ -61,6 +67,7 @@ const initialState = {
|
||||
topAggId: null,
|
||||
routeState: '',
|
||||
swapsFeatureIsLive: false,
|
||||
swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME,
|
||||
},
|
||||
}
|
||||
|
||||
@ -73,6 +80,7 @@ export default class SwapsController {
|
||||
tokenRatesStore,
|
||||
fetchTradesInfo = defaultFetchTradesInfo,
|
||||
fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness,
|
||||
fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime,
|
||||
}) {
|
||||
this.store = new ObservableStore({
|
||||
swapsState: { ...initialState.swapsState },
|
||||
@ -80,6 +88,7 @@ export default class SwapsController {
|
||||
|
||||
this._fetchTradesInfo = fetchTradesInfo
|
||||
this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness
|
||||
this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime
|
||||
|
||||
this.getBufferedGasLimit = getBufferedGasLimit
|
||||
this.tokenRatesStore = tokenRatesStore
|
||||
@ -101,11 +110,31 @@ export default class SwapsController {
|
||||
this._setupSwapsLivenessFetching()
|
||||
}
|
||||
|
||||
// Sets the refresh rate for quote updates from the MetaSwap API
|
||||
async _setSwapsQuoteRefreshTime() {
|
||||
// Default to fallback time unless API returns valid response
|
||||
let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME
|
||||
try {
|
||||
swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime()
|
||||
} catch (e) {
|
||||
console.error('Request for swaps quote refresh time failed: ', e)
|
||||
}
|
||||
|
||||
const { swapsState } = this.store.getState()
|
||||
this.store.updateState({
|
||||
swapsState: { ...swapsState, swapsQuoteRefreshTime },
|
||||
})
|
||||
}
|
||||
|
||||
// Once quotes are fetched, we poll for new ones to keep the quotes up to date. Market and aggregator contract conditions can change fast enough
|
||||
// that quotes will no longer be available after 1 or 2 minutes. When fetchAndSetQuotes is first called it, receives fetch that parameters are stored in
|
||||
// state. These stored parameters are used on subsequent calls made during polling.
|
||||
// Note: we stop polling after 3 requests, until new quotes are explicitly asked for. The logic that enforces that maximum is in the body of fetchAndSetQuotes
|
||||
pollForNewQuotes() {
|
||||
const {
|
||||
swapsState: { swapsQuoteRefreshTime },
|
||||
} = this.store.getState()
|
||||
|
||||
this.pollingTimeout = setTimeout(() => {
|
||||
const { swapsState } = this.store.getState()
|
||||
this.fetchAndSetQuotes(
|
||||
@ -113,7 +142,7 @@ export default class SwapsController {
|
||||
swapsState.fetchParams?.metaData,
|
||||
true,
|
||||
)
|
||||
}, QUOTE_POLLING_INTERVAL)
|
||||
}, swapsQuoteRefreshTime - QUOTE_POLLING_DIFFERENCE_INTERVAL)
|
||||
}
|
||||
|
||||
stopPollingForQuotes() {
|
||||
@ -128,7 +157,6 @@ export default class SwapsController {
|
||||
if (!fetchParams) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Every time we get a new request that is not from the polling, we reset the poll count so we can poll for up to three more sets of quotes with these new params.
|
||||
if (!isPolledRequest) {
|
||||
this.pollCount = 0
|
||||
@ -144,7 +172,10 @@ export default class SwapsController {
|
||||
const indexOfCurrentCall = this.indexOfNewestCallInFlight + 1
|
||||
this.indexOfNewestCallInFlight = indexOfCurrentCall
|
||||
|
||||
let newQuotes = await this._fetchTradesInfo(fetchParams)
|
||||
let [newQuotes] = await Promise.all([
|
||||
this._fetchTradesInfo(fetchParams),
|
||||
this._setSwapsQuoteRefreshTime(),
|
||||
])
|
||||
|
||||
newQuotes = mapValues(newQuotes, (quote) => ({
|
||||
...quote,
|
||||
@ -422,6 +453,7 @@ export default class SwapsController {
|
||||
tokens: swapsState.tokens,
|
||||
fetchParams: swapsState.fetchParams,
|
||||
swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
|
||||
swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime,
|
||||
},
|
||||
})
|
||||
clearTimeout(this.pollingTimeout)
|
||||
@ -435,6 +467,7 @@ export default class SwapsController {
|
||||
...initialState.swapsState,
|
||||
tokens: swapsState.tokens,
|
||||
swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
|
||||
swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime,
|
||||
},
|
||||
})
|
||||
clearTimeout(this.pollingTimeout)
|
||||
|
@ -121,12 +121,14 @@ const EMPTY_INIT_STATE = {
|
||||
topAggId: null,
|
||||
routeState: '',
|
||||
swapsFeatureIsLive: false,
|
||||
swapsQuoteRefreshTime: 60000,
|
||||
},
|
||||
}
|
||||
|
||||
const sandbox = sinon.createSandbox()
|
||||
const fetchTradesInfoStub = sandbox.stub()
|
||||
const fetchSwapsFeatureLivenessStub = sandbox.stub()
|
||||
const fetchSwapsQuoteRefreshTimeStub = sandbox.stub()
|
||||
|
||||
describe('SwapsController', function () {
|
||||
let provider
|
||||
@ -140,6 +142,7 @@ describe('SwapsController', function () {
|
||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
||||
fetchTradesInfo: fetchTradesInfoStub,
|
||||
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
|
||||
fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub,
|
||||
})
|
||||
}
|
||||
|
||||
@ -639,9 +642,9 @@ describe('SwapsController', function () {
|
||||
const quotes = await swapsController.fetchAndSetQuotes(undefined)
|
||||
assert.strictEqual(quotes, null)
|
||||
})
|
||||
|
||||
it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () {
|
||||
fetchTradesInfoStub.resolves(getMockQuotes())
|
||||
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime())
|
||||
|
||||
// Make it so approval is not required
|
||||
sandbox
|
||||
@ -682,9 +685,9 @@ describe('SwapsController', function () {
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('performs the allowance check', async function () {
|
||||
fetchTradesInfoStub.resolves(getMockQuotes())
|
||||
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime())
|
||||
|
||||
// Make it so approval is not required
|
||||
const allowanceStub = sandbox
|
||||
@ -707,6 +710,7 @@ describe('SwapsController', function () {
|
||||
|
||||
it('gets the gas limit if approval is required', async function () {
|
||||
fetchTradesInfoStub.resolves(MOCK_QUOTES_APPROVAL_REQUIRED)
|
||||
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime())
|
||||
|
||||
// Ensure approval is required
|
||||
sandbox
|
||||
@ -732,6 +736,7 @@ describe('SwapsController', function () {
|
||||
|
||||
it('marks the best quote', async function () {
|
||||
fetchTradesInfoStub.resolves(getMockQuotes())
|
||||
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime())
|
||||
|
||||
// Make it so approval is not required
|
||||
sandbox
|
||||
@ -762,6 +767,7 @@ describe('SwapsController', function () {
|
||||
}
|
||||
const quotes = { ...getMockQuotes(), [bestAggId]: bestQuote }
|
||||
fetchTradesInfoStub.resolves(quotes)
|
||||
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime())
|
||||
|
||||
// Make it so approval is not required
|
||||
sandbox
|
||||
@ -779,6 +785,7 @@ describe('SwapsController', function () {
|
||||
|
||||
it('does not mark as best quote if no conversion rate exists for destination token', async function () {
|
||||
fetchTradesInfoStub.resolves(getMockQuotes())
|
||||
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime())
|
||||
|
||||
// Make it so approval is not required
|
||||
sandbox
|
||||
@ -805,6 +812,7 @@ describe('SwapsController', function () {
|
||||
assert.deepStrictEqual(swapsState, {
|
||||
...EMPTY_INIT_STATE.swapsState,
|
||||
tokens: old.tokens,
|
||||
swapsQuoteRefreshTime: old.swapsQuoteRefreshTime,
|
||||
})
|
||||
})
|
||||
|
||||
@ -850,8 +858,14 @@ describe('SwapsController', function () {
|
||||
const tokens = 'test'
|
||||
const fetchParams = 'test'
|
||||
const swapsFeatureIsLive = false
|
||||
const swapsQuoteRefreshTime = 0
|
||||
swapsController.store.updateState({
|
||||
swapsState: { tokens, fetchParams, swapsFeatureIsLive },
|
||||
swapsState: {
|
||||
tokens,
|
||||
fetchParams,
|
||||
swapsFeatureIsLive,
|
||||
swapsQuoteRefreshTime,
|
||||
},
|
||||
})
|
||||
|
||||
swapsController.resetPostFetchState()
|
||||
@ -862,6 +876,7 @@ describe('SwapsController', function () {
|
||||
tokens,
|
||||
fetchParams,
|
||||
swapsFeatureIsLive,
|
||||
swapsQuoteRefreshTime,
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1615,3 +1630,7 @@ function getTopQuoteAndSavingsBaseExpectedResults() {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function getMockQuoteRefreshTime() {
|
||||
return 45000
|
||||
}
|
||||
|
@ -224,6 +224,9 @@ const getSwapsState = (state) => state.metamask.swapsState
|
||||
export const getSwapsFeatureLiveness = (state) =>
|
||||
state.metamask.swapsState.swapsFeatureIsLive
|
||||
|
||||
export const getSwapsQuoteRefreshTime = (state) =>
|
||||
state.metamask.swapsState.swapsQuoteRefreshTime
|
||||
|
||||
export const getBackgroundSwapRouteState = (state) =>
|
||||
state.metamask.swapsState.routeState
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { Duration } from 'luxon'
|
||||
import { I18nContext } from '../../../contexts/i18n'
|
||||
import InfoTooltip from '../../../components/ui/info-tooltip'
|
||||
|
||||
const TIMER_BASE = 60000
|
||||
import { getSwapsQuoteRefreshTime } from '../../../ducks/swaps/swaps'
|
||||
|
||||
// Return the mm:ss start time of the countdown timer.
|
||||
// If time has elapsed between `timeStarted` the time current time,
|
||||
@ -31,7 +31,7 @@ function timeBelowWarningTime(timer, warningTime) {
|
||||
export default function CountdownTimer({
|
||||
timeStarted,
|
||||
timeOnly,
|
||||
timerBase = TIMER_BASE,
|
||||
timerBase,
|
||||
warningTime,
|
||||
labelKey,
|
||||
infoTooltipLabelKey,
|
||||
@ -40,9 +40,12 @@ export default function CountdownTimer({
|
||||
const intervalRef = useRef()
|
||||
const initialTimeStartedRef = useRef()
|
||||
|
||||
const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime)
|
||||
const timerStart = Number(timerBase) || swapsQuoteRefreshTime
|
||||
|
||||
const [currentTime, setCurrentTime] = useState(() => Date.now())
|
||||
const [timer, setTimer] = useState(() =>
|
||||
getNewTimer(currentTime, timeStarted, timerBase),
|
||||
getNewTimer(currentTime, timeStarted, timerStart),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -67,14 +70,14 @@ export default function CountdownTimer({
|
||||
initialTimeStartedRef.current = timeStarted
|
||||
const newCurrentTime = Date.now()
|
||||
setCurrentTime(newCurrentTime)
|
||||
setTimer(getNewTimer(newCurrentTime, timeStarted, timerBase))
|
||||
setTimer(getNewTimer(newCurrentTime, timeStarted, timerStart))
|
||||
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = setInterval(() => {
|
||||
setTimer(decreaseTimerByOne)
|
||||
}, 1000)
|
||||
}
|
||||
}, [timeStarted, timer, timerBase])
|
||||
}, [timeStarted, timer, timerStart])
|
||||
|
||||
const formattedTimer = Duration.fromMillis(timer).toFormat('m:ss')
|
||||
let time
|
||||
|
@ -24,20 +24,24 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH =
|
||||
|
||||
const CACHE_REFRESH_ONE_HOUR = 3600000
|
||||
|
||||
const METASWAP_API_HOST = 'https://api.metaswap.codefi.network'
|
||||
|
||||
const getBaseApi = function (type) {
|
||||
switch (type) {
|
||||
case 'trade':
|
||||
return `https://api.metaswap.codefi.network/trades?`
|
||||
return `${METASWAP_API_HOST}/trades?`
|
||||
case 'tokens':
|
||||
return `https://api.metaswap.codefi.network/tokens`
|
||||
return `${METASWAP_API_HOST}/tokens`
|
||||
case 'topAssets':
|
||||
return `https://api.metaswap.codefi.network/topAssets`
|
||||
return `${METASWAP_API_HOST}/topAssets`
|
||||
case 'featureFlag':
|
||||
return `https://api.metaswap.codefi.network/featureFlag`
|
||||
return `${METASWAP_API_HOST}/featureFlag`
|
||||
case 'aggregatorMetadata':
|
||||
return `https://api.metaswap.codefi.network/aggregatorMetadata`
|
||||
return `${METASWAP_API_HOST}/aggregatorMetadata`
|
||||
case 'gasPrices':
|
||||
return `https://api.metaswap.codefi.network/gasPrices`
|
||||
return `${METASWAP_API_HOST}/gasPrices`
|
||||
case 'refreshTime':
|
||||
return `${METASWAP_API_HOST}/quoteRefreshRate`
|
||||
default:
|
||||
throw new Error('getBaseApi requires an api call type')
|
||||
}
|
||||
@ -328,6 +332,23 @@ export async function fetchSwapsFeatureLiveness() {
|
||||
return status?.active
|
||||
}
|
||||
|
||||
export async function fetchSwapsQuoteRefreshTime() {
|
||||
const response = await fetchWithCache(
|
||||
getBaseApi('refreshTime'),
|
||||
{ method: 'GET' },
|
||||
{ cacheRefreshTime: 600000 },
|
||||
)
|
||||
|
||||
// We presently use milliseconds in the UI
|
||||
if (typeof response?.seconds === 'number' && response.seconds > 0) {
|
||||
return response.seconds * 1000
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`MetaMask - refreshTime provided invalid response: ${response}`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchTokenPrice(address) {
|
||||
const query = `contract_addresses=${address}&vs_currencies=eth`
|
||||
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
signAndSendTransactions,
|
||||
getBackgroundSwapRouteState,
|
||||
swapsQuoteSelected,
|
||||
getSwapsQuoteRefreshTime,
|
||||
} from '../../../ducks/swaps/swaps'
|
||||
import {
|
||||
conversionRateSelector,
|
||||
@ -115,6 +116,7 @@ export default function ViewQuote() {
|
||||
const topQuote = useSelector(getTopQuote)
|
||||
const usedQuote = selectedQuote || topQuote
|
||||
const tradeValue = usedQuote?.trade?.value ?? '0x0'
|
||||
const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime)
|
||||
|
||||
const { isBestQuote } = usedQuote
|
||||
const fetchParamsSourceToken = fetchParams?.sourceToken
|
||||
@ -263,14 +265,23 @@ export default function ViewQuote() {
|
||||
useEffect(() => {
|
||||
const currentTime = Date.now()
|
||||
const timeSinceLastFetched = currentTime - quotesLastFetched
|
||||
if (timeSinceLastFetched > 60000 && !dispatchedSafeRefetch) {
|
||||
if (
|
||||
timeSinceLastFetched > swapsQuoteRefreshTime &&
|
||||
!dispatchedSafeRefetch
|
||||
) {
|
||||
setDispatchedSafeRefetch(true)
|
||||
dispatch(safeRefetchQuotes())
|
||||
} else if (timeSinceLastFetched > 60000) {
|
||||
} else if (timeSinceLastFetched > swapsQuoteRefreshTime) {
|
||||
dispatch(setSwapsErrorKey(QUOTES_EXPIRED_ERROR))
|
||||
history.push(SWAPS_ERROR_ROUTE)
|
||||
}
|
||||
}, [quotesLastFetched, dispatchedSafeRefetch, dispatch, history])
|
||||
}, [
|
||||
quotesLastFetched,
|
||||
dispatchedSafeRefetch,
|
||||
dispatch,
|
||||
history,
|
||||
swapsQuoteRefreshTime,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!originalApproveAmount && approveAmount) {
|
||||
|
Loading…
Reference in New Issue
Block a user