From 28fc2d471fc71d7c152d7a104f0975ca86513431 Mon Sep 17 00:00:00 2001 From: Daniel <80175477+dan437@users.noreply.github.com> Date: Wed, 15 Sep 2021 15:13:18 +0200 Subject: [PATCH] Quotes prefetching in Swaps (#11915) --- app/_locales/en/messages.json | 10 +- app/_locales/es/messages.json | 7 -- app/_locales/es_419/messages.json | 7 -- app/_locales/hi/messages.json | 7 -- app/_locales/id/messages.json | 7 -- app/_locales/it/messages.json | 7 -- app/_locales/ja/messages.json | 7 -- app/_locales/ko/messages.json | 7 -- app/_locales/ph/messages.json | 7 -- app/_locales/pt_BR/messages.json | 7 -- app/_locales/ru/messages.json | 7 -- app/_locales/tl/messages.json | 7 -- app/_locales/vi/messages.json | 7 -- app/_locales/zh_CN/messages.json | 7 -- app/scripts/controllers/swaps.js | 101 ++++++++++++---- app/scripts/controllers/swaps.test.js | 19 ++- app/scripts/metamask-controller.js | 8 ++ test/jest/mock-store.js | 1 + ui/ducks/swaps/swaps.js | 21 +++- ui/pages/swaps/awaiting-swap/awaiting-swap.js | 10 +- ui/pages/swaps/build-quote/build-quote.js | 108 +++++++++++++++--- .../swaps/build-quote/build-quote.test.js | 31 ++++- ui/pages/swaps/index.js | 10 +- .../aggregator-logo.test.js.snap | 18 --- .../loading-swaps-quotes/aggregator-logo.js | 40 ------- .../aggregator-logo.test.js | 22 ---- .../loading-swaps-quotes.js | 93 +-------------- .../loading-swaps-quotes.test.js | 1 - ui/pages/swaps/swaps.util.js | 25 +--- ui/pages/swaps/view-quote/view-quote.js | 38 ++++-- ui/pages/swaps/view-quote/view-quote.test.js | 1 + ui/store/actions.js | 16 +++ 32 files changed, 302 insertions(+), 362 deletions(-) delete mode 100644 ui/pages/swaps/loading-swaps-quotes/__snapshots__/aggregator-logo.test.js.snap delete mode 100644 ui/pages/swaps/loading-swaps-quotes/aggregator-logo.js delete mode 100644 ui/pages/swaps/loading-swaps-quotes/aggregator-logo.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 948967e4e..31a10e9f1 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2155,10 +2155,6 @@ "message": "No tokens available matching $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Checking $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Confirm with your hardware wallet" }, @@ -2204,6 +2200,9 @@ "swapFailedErrorTitle": { "message": "Swap failed" }, + "swapFetchingQuotes": { + "message": "Fetching quotes" + }, "swapFetchingQuotesErrorDescription": { "message": "Hmmm... something went wrong. Try again, or if errors persist, contact customer support." }, @@ -2213,9 +2212,6 @@ "swapFetchingTokens": { "message": "Fetching tokens..." }, - "swapFinalizing": { - "message": "Finalizing..." - }, "swapFromTo": { "message": "The swap of $1 to $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 6e49c2494..2334f927e 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1870,10 +1870,6 @@ "message": "No hay tokens disponibles que coincidan con $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Comprobando $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Confirmar con la cartera de hardware" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "Capturando tokens…" }, - "swapFinalizing": { - "message": "Finalizando…" - }, "swapFromTo": { "message": "El canje de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 6e49c2494..2334f927e 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1870,10 +1870,6 @@ "message": "No hay tokens disponibles que coincidan con $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Comprobando $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Confirmar con la cartera de hardware" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "Capturando tokens…" }, - "swapFinalizing": { - "message": "Finalizando…" - }, "swapFromTo": { "message": "El canje de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 73f2e0dbf..322e798cb 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1870,10 +1870,6 @@ "message": "$1 के मिलान वाले कोई भी टोकन उपलब्ध नहीं हैं", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "$1 की जाँच की जा रही है", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "अपने हार्डवेयर वॉलेट से पुष्टि करें" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "टोकन प्राप्त किए जा रहे हैं..." }, - "swapFinalizing": { - "message": "अंतिम रूप दिया जा रहा है..." - }, "swapFromTo": { "message": "$1 से $2 का स्वैप", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index bd6d4b3f9..dc3ff2a7f 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1870,10 +1870,6 @@ "message": "Tidak ada token yang cocok yang tersedia $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Memeriksa $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Konfirmasikan dengan dompet perangkat keras Anda" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "Mengambil token..." }, - "swapFinalizing": { - "message": "Menyelesaikan..." - }, "swapFromTo": { "message": "Penukaran dari $1 ke $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index d947c64d6..88bee983f 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1522,10 +1522,6 @@ "message": "Non ci sono token disponibile con questo nome $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Verificando $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapCustom": { "message": "personalizza" }, @@ -1564,9 +1560,6 @@ "swapFetchingTokens": { "message": "Recuperando i token..." }, - "swapFinalizing": { - "message": "Finalizzando..." - }, "swapLowSlippageError": { "message": "La transazione può fallire, il massimo slippage è troppo basso." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 532f807e9..c567f7eff 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1870,10 +1870,6 @@ "message": "$1 と一致するトークンがありません", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "$1 をチェック中", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "ハードウェア ウォレットで確認する" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "トークンを取り出し中..." }, - "swapFinalizing": { - "message": "終了中..." - }, "swapFromTo": { "message": "$1 から $2 のスワップ", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 916a79da2..10b9cfab7 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1870,10 +1870,6 @@ "message": "$1와(과) 일치하는 토큰이 없습니다.", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "$1 확인 중", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "하드웨어 지갑으로 확인합니다." }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "토큰 가져오는 중..." }, - "swapFinalizing": { - "message": "마무리 중..." - }, "swapFromTo": { "message": "$1을(를) $2(으)로 스왑", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index c2fc07073..280e98d5e 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1870,10 +1870,6 @@ "message": "Walang available na token na tumutugma sa $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Sinusuri ang $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Kumpirmahin ang iyong hardware wallet" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "Kinukuha ang mga token..." }, - "swapFinalizing": { - "message": "Isinasapinal..." - }, "swapFromTo": { "message": "Ang pag-swap ng $1 sa $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index c9953baac..90ae89a9c 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1870,10 +1870,6 @@ "message": "Nenhum token disponível correspondente a $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Verificando $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Confirme com sua carteira de hardware" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "Fetch dos tokens..." }, - "swapFinalizing": { - "message": "Finalizando..." - }, "swapFromTo": { "message": "O swap de $1 para $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index ddf3f9b72..2cc556eb6 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1870,10 +1870,6 @@ "message": "Нет доступных токенов соответствующих $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Проверка $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Подтвердить с помощью аппаратного кошелька" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "Получение токенов..." }, - "swapFinalizing": { - "message": "Завершение..." - }, "swapFromTo": { "message": "Своп $1 на $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 4de586d30..02280e381 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1510,10 +1510,6 @@ "message": "Walang available na token na tumutugma sa $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Sinusuri ang $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapCustom": { "message": "custom" }, @@ -1552,9 +1548,6 @@ "swapFetchingTokens": { "message": "Kinukuha ang mga token..." }, - "swapFinalizing": { - "message": "Isinasapinal..." - }, "swapLowSlippageError": { "message": "Maaaring hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 565818338..b28cbf86a 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1870,10 +1870,6 @@ "message": "Không có token nào khớp với $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Đang kiểm tra $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Xác nhận ví cứng của bạn" }, @@ -1925,9 +1921,6 @@ "swapFetchingTokens": { "message": "Đang tìm nạp token..." }, - "swapFinalizing": { - "message": "Đang hoàn tất..." - }, "swapFromTo": { "message": "Giao dịch hoán đổi $1 sang $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 3427cec44..02ec5f565 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1516,10 +1516,6 @@ "message": "没有匹配的代币符合 $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "正在检查 $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapCustom": { "message": "自定义" }, @@ -1558,9 +1554,6 @@ "swapFetchingTokens": { "message": "获取代币中……" }, - "swapFinalizing": { - "message": "确定中……" - }, "swapLowSlippageError": { "message": "交易可能失败,最大滑点过低。" }, diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 238dba04c..dea26345b 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -24,8 +24,9 @@ import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils' import { fetchTradesInfo as defaultFetchTradesInfo, - fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime, + getBaseApi, } from '../../../ui/pages/swaps/swaps.util'; +import fetchWithCache from '../../../ui/helpers/utils/fetch-with-cache'; import { MINUTE, SECOND } from '../../../shared/constants/time'; import { NETWORK_EVENTS } from './network'; @@ -40,10 +41,6 @@ const POLL_COUNT_LIMIT = 3; // provide a reasonable fallback to avoid further errors const FALLBACK_QUOTE_REFRESH_TIME = MINUTE; -// 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 = SECOND * 10; - function calculateGasEstimateWithRefund( maxGas = MAX_GAS_LIMIT, estimatedRefund = 0, @@ -64,6 +61,7 @@ function calculateGasEstimateWithRefund( const initialState = { swapsState: { quotes: {}, + quotesPollingLimitEnabled: false, fetchParams: null, tokens: null, tradeTxId: null, @@ -82,6 +80,7 @@ const initialState = { swapsFeatureIsLive: true, useNewSwapsApi: false, swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME, + swapsQuotePrefetchingRefreshTime: FALLBACK_QUOTE_REFRESH_TIME, }, }; @@ -93,7 +92,6 @@ export default class SwapsController { getProviderConfig, tokenRatesStore, fetchTradesInfo = defaultFetchTradesInfo, - fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime, getCurrentChainId, getEIP1559GasFeeEstimates, }) { @@ -102,7 +100,6 @@ export default class SwapsController { }); this._fetchTradesInfo = fetchTradesInfo; - this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime; this._getCurrentChainId = getCurrentChainId; this._getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates; @@ -124,38 +121,70 @@ export default class SwapsController { }); } + async fetchSwapsRefreshRates(chainId, useNewSwapsApi) { + const response = await fetchWithCache( + getBaseApi('network', chainId, useNewSwapsApi), + { method: 'GET' }, + { cacheRefreshTime: 600000 }, + ); + const { refreshRates } = response || {}; + if ( + !refreshRates || + typeof refreshRates.quotes !== 'number' || + typeof refreshRates.quotesPrefetching !== 'number' + ) { + throw new Error( + `MetaMask - invalid response for refreshRates: ${response}`, + ); + } + // We presently use milliseconds in the UI. + return { + quotes: refreshRates.quotes * 1000, + quotesPrefetching: refreshRates.quotesPrefetching * 1000, + }; + } + // Sets the refresh rate for quote updates from the MetaSwap API - async _setSwapsQuoteRefreshTime() { + async _setSwapsRefreshRates() { const chainId = this._getCurrentChainId(); const { swapsState } = this.store.getState(); - - // Default to fallback time unless API returns valid response - let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME; + let swapsRefreshRates; try { - swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime( + swapsRefreshRates = await this.fetchSwapsRefreshRates( chainId, swapsState.useNewSwapsApi, ); } catch (e) { console.error('Request for swaps quote refresh time failed: ', e); } - const { swapsState: latestSwapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...latestSwapsState, swapsQuoteRefreshTime }, + swapsState: { + ...latestSwapsState, + swapsQuoteRefreshTime: + swapsRefreshRates?.quotes || FALLBACK_QUOTE_REFRESH_TIME, + swapsQuotePrefetchingRefreshTime: + swapsRefreshRates?.quotesPrefetching || FALLBACK_QUOTE_REFRESH_TIME, + }, }); } // 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 + // that quotes will no longer be available after 1 or 2 minutes. When fetchAndSetQuotes is first called, it receives fetch parameters that 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 }, + swapsState: { + swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime, + quotesPollingLimitEnabled, + }, } = this.store.getState(); - + // swapsQuoteRefreshTime is used on the View Quote page, swapsQuotePrefetchingRefreshTime is used on the Build Quote page. + const quotesRefreshRateInMs = quotesPollingLimitEnabled + ? swapsQuoteRefreshTime + : swapsQuotePrefetchingRefreshTime; this.pollingTimeout = setTimeout(() => { const { swapsState } = this.store.getState(); this.fetchAndSetQuotes( @@ -163,11 +192,13 @@ export default class SwapsController { swapsState.fetchParams?.metaData, true, ); - }, swapsQuoteRefreshTime - QUOTE_POLLING_DIFFERENCE_INTERVAL); + }, quotesRefreshRateInMs); } stopPollingForQuotes() { - clearTimeout(this.pollingTimeout); + if (this.pollingTimeout) { + clearTimeout(this.pollingTimeout); + } } async fetchAndSetQuotes( @@ -177,7 +208,7 @@ export default class SwapsController { ) { const { chainId } = fetchParamsMetaData; const { - swapsState: { useNewSwapsApi }, + swapsState: { useNewSwapsApi, quotesPollingLimitEnabled }, } = this.store.getState(); if (!fetchParams) { @@ -203,7 +234,7 @@ export default class SwapsController { ...fetchParamsMetaData, useNewSwapsApi, }), - this._setSwapsQuoteRefreshTime(), + this._setSwapsRefreshRates(), ]); newQuotes = mapValues(newQuotes, (quote) => ({ @@ -292,9 +323,13 @@ export default class SwapsController { }, }); - // We only want to do up to a maximum of three requests from polling. - this.pollCount += 1; - if (this.pollCount < POLL_COUNT_LIMIT + 1) { + if (quotesPollingLimitEnabled) { + // We only want to do up to a maximum of three requests from polling if polling limit is enabled. + // Otherwise we won't increase pollCount, so polling will run without a limit. + this.pollCount += 1; + } + + if (!quotesPollingLimitEnabled || this.pollCount < POLL_COUNT_LIMIT + 1) { this.pollForNewQuotes(); } else { this.resetPostFetchState(); @@ -322,6 +357,11 @@ export default class SwapsController { this.store.updateState({ swapsState: { ...swapsState, tokens } }); } + clearSwapsQuotes() { + const { swapsState } = this.store.getState(); + this.store.updateState({ swapsState: { ...swapsState, quotes: {} } }); + } + setSwapsErrorKey(errorKey) { const { swapsState } = this.store.getState(); this.store.updateState({ swapsState: { ...swapsState, errorKey } }); @@ -464,6 +504,13 @@ export default class SwapsController { }); } + setSwapsQuotesPollingLimitEnabled(quotesPollingLimitEnabled) { + const { swapsState } = this.store.getState(); + this.store.updateState({ + swapsState: { ...swapsState, quotesPollingLimitEnabled }, + }); + } + setSwapsTxMaxFeePriorityPerGas(maxPriorityFeePerGas) { const { swapsState } = this.store.getState(); this.store.updateState({ @@ -511,6 +558,8 @@ export default class SwapsController { swapsFeatureIsLive: swapsState.swapsFeatureIsLive, useNewSwapsApi: swapsState.useNewSwapsApi, swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime: + swapsState.swapsQuotePrefetchingRefreshTime, }, }); clearTimeout(this.pollingTimeout); @@ -523,6 +572,8 @@ export default class SwapsController { ...initialState.swapsState, tokens: swapsState.tokens, swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime: + swapsState.swapsQuotePrefetchingRefreshTime, }, }); clearTimeout(this.pollingTimeout); diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index 4ed83ef25..9264ab8b2 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -116,6 +116,7 @@ function getMockNetworkController() { const EMPTY_INIT_STATE = { swapsState: { quotes: {}, + quotesPollingLimitEnabled: false, fetchParams: null, tokens: null, tradeTxId: null, @@ -133,13 +134,13 @@ const EMPTY_INIT_STATE = { swapsFeatureIsLive: true, useNewSwapsApi: false, swapsQuoteRefreshTime: 60000, + swapsQuotePrefetchingRefreshTime: 60000, swapsUserFeeLevel: '', }, }; const sandbox = sinon.createSandbox(); const fetchTradesInfoStub = sandbox.stub(); -const fetchSwapsQuoteRefreshTimeStub = sandbox.stub(); const getCurrentChainIdStub = sandbox.stub(); getCurrentChainIdStub.returns(MAINNET_CHAIN_ID); const getEIP1559GasFeeEstimatesStub = sandbox.stub(() => { @@ -162,7 +163,6 @@ describe('SwapsController', function () { getProviderConfig: MOCK_GET_PROVIDER_CONFIG, tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, - fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub, getCurrentChainId: getCurrentChainIdStub, getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub, }); @@ -670,7 +670,6 @@ describe('SwapsController', function () { 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 @@ -716,7 +715,6 @@ describe('SwapsController', function () { it('performs the allowance check', async function () { fetchTradesInfoStub.resolves(getMockQuotes()); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Make it so approval is not required const allowanceStub = sandbox @@ -740,7 +738,6 @@ 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 @@ -766,7 +763,6 @@ describe('SwapsController', function () { it('marks the best quote', async function () { fetchTradesInfoStub.resolves(getMockQuotes()); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Make it so approval is not required sandbox @@ -797,7 +793,6 @@ describe('SwapsController', function () { }; const quotes = { ...getMockQuotes(), [bestAggId]: bestQuote }; fetchTradesInfoStub.resolves(quotes); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Make it so approval is not required sandbox @@ -815,7 +810,6 @@ 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 @@ -843,6 +837,8 @@ describe('SwapsController', function () { ...EMPTY_INIT_STATE.swapsState, tokens: old.tokens, swapsQuoteRefreshTime: old.swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime: + old.swapsQuotePrefetchingRefreshTime, }); }); @@ -890,6 +886,7 @@ describe('SwapsController', function () { const swapsFeatureIsLive = false; const useNewSwapsApi = false; const swapsQuoteRefreshTime = 0; + const swapsQuotePrefetchingRefreshTime = 0; swapsController.store.updateState({ swapsState: { tokens, @@ -897,6 +894,7 @@ describe('SwapsController', function () { swapsFeatureIsLive, useNewSwapsApi, swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime, }, }); @@ -909,6 +907,7 @@ describe('SwapsController', function () { fetchParams, swapsFeatureIsLive, swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime, }); }); }); @@ -1387,7 +1386,3 @@ function getTopQuoteAndSavingsBaseExpectedResults() { }, }; } - -function getMockQuoteRefreshTime() { - return 45000; -} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 891019324..77e84f530 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1073,6 +1073,10 @@ export default class MetamaskController extends EventEmitter { swapsController, ), setSwapsTokens: nodeify(swapsController.setSwapsTokens, swapsController), + clearSwapsQuotes: nodeify( + swapsController.clearSwapsQuotes, + swapsController, + ), setApproveTxId: nodeify(swapsController.setApproveTxId, swapsController), setTradeTxId: nodeify(swapsController.setTradeTxId, swapsController), setSwapsTxGasPrice: nodeify( @@ -1127,6 +1131,10 @@ export default class MetamaskController extends EventEmitter { swapsController.setSwapsUserFeeLevel, swapsController, ), + setSwapsQuotesPollingLimitEnabled: nodeify( + swapsController.setSwapsQuotesPollingLimitEnabled, + swapsController, + ), // MetaMetrics trackMetaMetricsEvent: nodeify( diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 19dbcf48b..a23d7ad35 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -212,6 +212,7 @@ export const createSwapsMockStore = () => { approveTxId: null, quotesLastFetched: 1519211809934, swapsQuoteRefreshTime: 60000, + swapsQuotePrefetchingRefreshTime: 60000, customMaxGas: '', customGasPrice: null, selectedAggId: 'TEST_AGG_2', diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 5cbb8dedc..4acfcad50 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -86,6 +86,7 @@ const initialState = { fetchingQuotes: false, fromToken: null, quotesFetchStartTime: null, + reviewSwapClickedTimestamp: null, topAssets: {}, toToken: null, customGas: { @@ -130,6 +131,9 @@ const slice = createSlice({ setQuotesFetchStartTime: (state, action) => { state.quotesFetchStartTime = action.payload; }, + setReviewSwapClickedTimestamp: (state, action) => { + state.reviewSwapClickedTimestamp = action.payload; + }, setTopAssets: (state, action) => { state.topAssets = action.payload; }, @@ -183,6 +187,9 @@ export const getFetchingQuotes = (state) => state.swaps.fetchingQuotes; export const getQuotesFetchStartTime = (state) => state.swaps.quotesFetchStartTime; +export const getReviewSwapClickedTimestamp = (state) => + state.swaps.reviewSwapClickedTimestamp; + export const getSwapsCustomizationModalPrice = (state) => state.swaps.customGas.price; @@ -236,6 +243,9 @@ export const getUseNewSwapsApi = (state) => export const getSwapsQuoteRefreshTime = (state) => state.metamask.swapsState.swapsQuoteRefreshTime; +export const getSwapsQuotePrefetchingRefreshTime = (state) => + state.metamask.swapsState.swapsQuotePrefetchingRefreshTime; + export const getBackgroundSwapRouteState = (state) => state.metamask.swapsState.routeState; @@ -323,6 +333,7 @@ const { setFetchingQuotes, setFromToken, setQuotesFetchStartTime, + setReviewSwapClickedTimestamp, setTopAssets, setToToken, swapCustomGasModalPriceEdited, @@ -338,6 +349,7 @@ export { setFetchingQuotes, setFromToken as setSwapsFromToken, setQuotesFetchStartTime as setSwapQuotesFetchStartTime, + setReviewSwapClickedTimestamp, setTopAssets, setToToken as setSwapToToken, swapCustomGasModalPriceEdited, @@ -412,6 +424,7 @@ export const fetchQuotesAndSetQuoteState = ( inputValue, maxSlippage, metaMetricsEvent, + pageRedirectionDisabled, ) => { return async (dispatch, getState) => { const state = getState(); @@ -461,8 +474,12 @@ export const fetchQuotesAndSetQuoteState = ( decimals: toTokenDecimals, iconUrl: toTokenIconUrl, } = selectedToToken; - await dispatch(setBackgroundSwapRouteState('loading')); - history.push(LOADING_QUOTES_ROUTE); + // pageRedirectionDisabled is true if quotes prefetching is active (a user is on the Build Quote page). + // In that case we just want to silently prefetch quotes without redirecting to the quotes loading page. + if (!pageRedirectionDisabled) { + await dispatch(setBackgroundSwapRouteState('loading')); + history.push(LOADING_QUOTES_ROUTE); + } dispatch(setFetchingQuotes(true)); const contractExchangeRates = getTokenExchangeRates(state); diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index e1a9e934d..d949c5441 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -1,5 +1,5 @@ import EventEmitter from 'events'; -import React, { useContext, useRef, useState } from 'react'; +import React, { useContext, useRef, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; @@ -42,6 +42,7 @@ import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.util import PulseLoader from '../../../components/ui/pulse-loader'; import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes'; +import { stopPollingForQuotes } from '../../../store/actions'; import { getRenderableNetworkFeesForQuote } from '../swaps.util'; import SwapsFooter from '../swaps-footer'; @@ -249,6 +250,13 @@ export default function AwaitingSwap({ ); }; + useEffect(() => { + if (errorKey) { + // If there was an error, stop polling for quotes. + dispatch(stopPollingForQuotes()); + } + }, [dispatch, errorKey]); + return (