mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Quotes prefetching in Swaps (#11915)
This commit is contained in:
parent
9e4b43defd
commit
28fc2d471f
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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."
|
||||
},
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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."
|
||||
},
|
||||
|
@ -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"
|
||||
|
@ -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": "交易可能失败,最大滑点过低。"
|
||||
},
|
||||
|
@ -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,12 +192,14 @@ export default class SwapsController {
|
||||
swapsState.fetchParams?.metaData,
|
||||
true,
|
||||
);
|
||||
}, swapsQuoteRefreshTime - QUOTE_POLLING_DIFFERENCE_INTERVAL);
|
||||
}, quotesRefreshRateInMs);
|
||||
}
|
||||
|
||||
stopPollingForQuotes() {
|
||||
if (this.pollingTimeout) {
|
||||
clearTimeout(this.pollingTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAndSetQuotes(
|
||||
fetchParams,
|
||||
@ -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.
|
||||
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 (this.pollCount < POLL_COUNT_LIMIT + 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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -212,6 +212,7 @@ export const createSwapsMockStore = () => {
|
||||
approveTxId: null,
|
||||
quotesLastFetched: 1519211809934,
|
||||
swapsQuoteRefreshTime: 60000,
|
||||
swapsQuotePrefetchingRefreshTime: 60000,
|
||||
customMaxGas: '',
|
||||
customGasPrice: null,
|
||||
selectedAggId: 'TEST_AGG_2',
|
||||
|
@ -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;
|
||||
// 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);
|
||||
|
@ -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 (
|
||||
<div className="awaiting-swap">
|
||||
<div className="awaiting-swap__content">
|
||||
|
@ -19,6 +19,10 @@ import SlippageButtons from '../slippage-buttons';
|
||||
import { getTokens, getConversionRate } from '../../../ducks/metamask/metamask';
|
||||
import InfoTooltip from '../../../components/ui/info-tooltip';
|
||||
import ActionableMessage from '../../../components/ui/actionable-message/actionable-message';
|
||||
import {
|
||||
VIEW_QUOTE_ROUTE,
|
||||
LOADING_QUOTES_ROUTE,
|
||||
} from '../../../helpers/constants/routes';
|
||||
|
||||
import {
|
||||
fetchQuotesAndSetQuoteState,
|
||||
@ -29,6 +33,8 @@ import {
|
||||
getBalanceError,
|
||||
getTopAssets,
|
||||
getFetchParams,
|
||||
getQuotes,
|
||||
setReviewSwapClickedTimestamp,
|
||||
} from '../../../ducks/swaps/swaps';
|
||||
import {
|
||||
getSwapsDefaultToken,
|
||||
@ -60,7 +66,13 @@ import {
|
||||
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
|
||||
} from '../../../../shared/constants/swaps';
|
||||
|
||||
import { resetSwapsPostFetchState, removeToken } from '../../../store/actions';
|
||||
import {
|
||||
resetSwapsPostFetchState,
|
||||
removeToken,
|
||||
setBackgroundSwapRouteState,
|
||||
clearSwapsQuotes,
|
||||
stopPollingForQuotes,
|
||||
} from '../../../store/actions';
|
||||
import {
|
||||
fetchTokenPrice,
|
||||
fetchTokenBalance,
|
||||
@ -76,6 +88,8 @@ const fuseSearchKeys = [
|
||||
|
||||
const MAX_ALLOWED_SLIPPAGE = 15;
|
||||
|
||||
let timeoutIdForQuotesPrefetching;
|
||||
|
||||
export default function BuildQuote({
|
||||
inputValue,
|
||||
onInputChange,
|
||||
@ -110,6 +124,8 @@ export default function BuildQuote({
|
||||
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
|
||||
const tokenList = useSelector(getTokenList);
|
||||
const useTokenDetection = useSelector(getUseTokenDetection);
|
||||
const quotes = useSelector(getQuotes, isEqual);
|
||||
const areQuotesPresent = Object.keys(quotes).length > 0;
|
||||
|
||||
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
|
||||
const conversionRate = useSelector(getConversionRate);
|
||||
@ -168,6 +184,7 @@ export default function BuildQuote({
|
||||
decimals: fromTokenDecimals,
|
||||
balance: rawFromTokenBalance,
|
||||
} = selectedFromToken || {};
|
||||
const { address: toTokenAddress } = selectedToToken || {};
|
||||
|
||||
const fromTokenBalance =
|
||||
rawFromTokenBalance &&
|
||||
@ -348,6 +365,7 @@ export default function BuildQuote({
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(resetSwapsPostFetchState());
|
||||
dispatch(setReviewSwapClickedTimestamp());
|
||||
}, [dispatch]);
|
||||
|
||||
const BlockExplorerLink = () => {
|
||||
@ -392,6 +410,51 @@ export default function BuildQuote({
|
||||
fromTokenAddress,
|
||||
selectedToToken.address,
|
||||
);
|
||||
const isReviewSwapButtonDisabled =
|
||||
tokenFromError ||
|
||||
!isFeatureFlagLoaded ||
|
||||
!Number(inputValue) ||
|
||||
!selectedToToken?.address ||
|
||||
Number(maxSlippage) < 0 ||
|
||||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE ||
|
||||
(toTokenIsNotDefault && occurrences < 2 && !verificationClicked);
|
||||
|
||||
// It's triggered every time there is a change in form values (token from, token to, amount and slippage).
|
||||
useEffect(() => {
|
||||
dispatch(clearSwapsQuotes());
|
||||
dispatch(stopPollingForQuotes());
|
||||
const prefetchQuotesWithoutRedirecting = async () => {
|
||||
const pageRedirectionDisabled = true;
|
||||
await dispatch(
|
||||
fetchQuotesAndSetQuoteState(
|
||||
history,
|
||||
inputValue,
|
||||
maxSlippage,
|
||||
metaMetricsEvent,
|
||||
pageRedirectionDisabled,
|
||||
),
|
||||
);
|
||||
};
|
||||
// Delay fetching quotes until a user is done typing an input value. If they type a new char in less than a second,
|
||||
// we will cancel previous setTimeout call and start running a new one.
|
||||
timeoutIdForQuotesPrefetching = setTimeout(() => {
|
||||
timeoutIdForQuotesPrefetching = null;
|
||||
if (!isReviewSwapButtonDisabled) {
|
||||
// Only do quotes prefetching if the Review Swap button is enabled.
|
||||
prefetchQuotesWithoutRedirecting();
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeoutIdForQuotesPrefetching);
|
||||
}, [
|
||||
dispatch,
|
||||
history,
|
||||
maxSlippage,
|
||||
metaMetricsEvent,
|
||||
isReviewSwapButtonDisabled,
|
||||
inputValue,
|
||||
fromTokenAddress,
|
||||
toTokenAddress,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="build-quote">
|
||||
@ -401,6 +464,7 @@ export default function BuildQuote({
|
||||
{!isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && (
|
||||
<div
|
||||
className="build-quote__max-button"
|
||||
data-testid="build-quote__max-button"
|
||||
onClick={() =>
|
||||
onInputChange(fromTokenBalance || '0', fromTokenBalance)
|
||||
}
|
||||
@ -583,7 +647,13 @@ export default function BuildQuote({
|
||||
)}
|
||||
</div>
|
||||
<SwapsFooter
|
||||
onSubmit={() => {
|
||||
onSubmit={async () => {
|
||||
// We need this to know how long it took to go from clicking on the Review Swap button to rendered View Quote page.
|
||||
dispatch(setReviewSwapClickedTimestamp(Date.now()));
|
||||
// In case that quotes prefetching is waiting to be executed, but hasn't started yet,
|
||||
// we want to cancel it and fetch quotes from here.
|
||||
if (timeoutIdForQuotesPrefetching) {
|
||||
clearTimeout(timeoutIdForQuotesPrefetching);
|
||||
dispatch(
|
||||
fetchQuotesAndSetQuoteState(
|
||||
history,
|
||||
@ -592,17 +662,17 @@ export default function BuildQuote({
|
||||
metaMetricsEvent,
|
||||
),
|
||||
);
|
||||
} else if (areQuotesPresent) {
|
||||
// If there are prefetched quotes already, go directly to the View Quote page.
|
||||
history.push(VIEW_QUOTE_ROUTE);
|
||||
} else {
|
||||
// If the "Review Swap" button was clicked while quotes are being fetched, go to the Loading Quotes page.
|
||||
await dispatch(setBackgroundSwapRouteState('loading'));
|
||||
history.push(LOADING_QUOTES_ROUTE);
|
||||
}
|
||||
}}
|
||||
submitText={t('swapReviewSwap')}
|
||||
disabled={
|
||||
tokenFromError ||
|
||||
!isFeatureFlagLoaded ||
|
||||
!Number(inputValue) ||
|
||||
!selectedToToken?.address ||
|
||||
Number(maxSlippage) < 0 ||
|
||||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE ||
|
||||
(toTokenIsNotDefault && occurrences < 2 && !verificationClicked)
|
||||
}
|
||||
disabled={isReviewSwapButtonDisabled}
|
||||
hideCancel
|
||||
showTermsOfService
|
||||
/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
renderWithProvider,
|
||||
@ -14,7 +15,7 @@ const createProps = (customProps = {}) => {
|
||||
return {
|
||||
inputValue: '5',
|
||||
onInputChange: jest.fn(),
|
||||
ethBalance: '6 ETH',
|
||||
ethBalance: '0x8',
|
||||
setMaxSlippage: jest.fn(),
|
||||
maxSlippage: 15,
|
||||
selectedAccountAddress: 'selectedAccountAddress',
|
||||
@ -26,6 +27,10 @@ const createProps = (customProps = {}) => {
|
||||
|
||||
setBackgroundConnection({
|
||||
resetPostFetchState: jest.fn(),
|
||||
removeToken: jest.fn(),
|
||||
setBackgroundSwapRouteState: jest.fn(),
|
||||
clearSwapsQuotes: jest.fn(),
|
||||
stopPollingForQuotes: jest.fn(),
|
||||
});
|
||||
|
||||
describe('BuildQuote', () => {
|
||||
@ -44,4 +49,28 @@ describe('BuildQuote', () => {
|
||||
document.querySelector('.slippage-buttons__button-group'),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('clicks on the max button', () => {
|
||||
const store = configureMockStore(middleware)(createSwapsMockStore());
|
||||
const props = createProps();
|
||||
const { getByTestId } = renderWithProvider(
|
||||
<BuildQuote {...props} />,
|
||||
store,
|
||||
);
|
||||
fireEvent.click(getByTestId('build-quote__max-button'));
|
||||
expect(props.onInputChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('types a number inside the input field', () => {
|
||||
const store = configureMockStore(middleware)(createSwapsMockStore());
|
||||
const props = createProps();
|
||||
const { getByDisplayValue } = renderWithProvider(
|
||||
<BuildQuote {...props} />,
|
||||
store,
|
||||
);
|
||||
fireEvent.change(getByDisplayValue('5'), {
|
||||
target: { value: '8' },
|
||||
});
|
||||
expect(props.onInputChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
fetchSwapsLiveness,
|
||||
getUseNewSwapsApi,
|
||||
getFromToken,
|
||||
getReviewSwapClickedTimestamp,
|
||||
} from '../../ducks/swaps/swaps';
|
||||
import {
|
||||
checkNetworkAndAccountSupports1559,
|
||||
@ -123,6 +124,8 @@ export default function Swap() {
|
||||
const fromToken = useSelector(getFromToken);
|
||||
const tokenList = useSelector(getTokenList);
|
||||
const listTokenValues = shuffle(Object.values(tokenList));
|
||||
const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp);
|
||||
const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp);
|
||||
|
||||
if (networkAndAccountSupports1559) {
|
||||
// This will pre-load gas fees before going to the View Quote page.
|
||||
@ -255,10 +258,12 @@ export default function Swap() {
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (swapsErrorKey && !isSwapsErrorRoute) {
|
||||
// If there is a swapsErrorKey and reviewSwapClicked is false, there was an error in silent quotes prefetching
|
||||
// and we don't want to show the error page in that case, because another API call for quotes can be successful.
|
||||
if (swapsErrorKey && !isSwapsErrorRoute && reviewSwapClicked) {
|
||||
history.push(SWAPS_ERROR_ROUTE);
|
||||
}
|
||||
}, [history, swapsErrorKey, isSwapsErrorRoute]);
|
||||
}, [history, swapsErrorKey, isSwapsErrorRoute, reviewSwapClicked]);
|
||||
|
||||
const beforeUnloadEventAddedRef = useRef();
|
||||
useEffect(() => {
|
||||
@ -393,7 +398,6 @@ export default function Swap() {
|
||||
}
|
||||
onDone={async () => {
|
||||
await dispatch(setBackgroundSwapRouteState(''));
|
||||
|
||||
if (
|
||||
swapsErrorKey === ERROR_FETCHING_QUOTES ||
|
||||
swapsErrorKey === QUOTES_NOT_AVAILABLE_ERROR
|
||||
|
@ -1,18 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AggregatorLogo renders the component with initial props 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="loading-swaps-quotes__logo"
|
||||
>
|
||||
<div
|
||||
style="background: rgb(0, 0, 0); box-shadow: 0px 4px 20px rgba(0, 0, NaN, 0.25);"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src="data:image/png;base64,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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Inspired by https://stackoverflow.com/a/28056903/4727685
|
||||
function hexToRGB(hex, alpha) {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
export default function AggregatorLogo({ name, icon, color }) {
|
||||
return (
|
||||
<div className="loading-swaps-quotes__logo">
|
||||
{icon && color ? (
|
||||
<div
|
||||
style={{
|
||||
background: color,
|
||||
boxShadow: `0px 4px 20px ${hexToRGB(color, 0.25)}`,
|
||||
}}
|
||||
>
|
||||
<img src={icon} alt="" />
|
||||
</div>
|
||||
) : (
|
||||
name && (
|
||||
<div>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AggregatorLogo.propTypes = {
|
||||
name: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { renderWithProvider } from '../../../../test/jest';
|
||||
import AggregatorLogo from './aggregator-logo';
|
||||
|
||||
const createProps = (customProps = {}) => {
|
||||
return {
|
||||
color: '#000',
|
||||
icon:
|
||||
'data:image/png;base64,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',
|
||||
...customProps,
|
||||
};
|
||||
};
|
||||
|
||||
describe('AggregatorLogo', () => {
|
||||
it('renders the component with initial props', () => {
|
||||
const { container } = renderWithProvider(
|
||||
<AggregatorLogo {...createProps()} />,
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { shuffle } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
navigateBackToBuildQuote,
|
||||
getFetchParams,
|
||||
@ -19,44 +18,6 @@ import { MetaMetricsContext } from '../../../contexts/metametrics.new';
|
||||
import Mascot from '../../../components/ui/mascot';
|
||||
import SwapsFooter from '../swaps-footer';
|
||||
import BackgroundAnimation from './background-animation';
|
||||
import AggregatorLogo from './aggregator-logo';
|
||||
|
||||
// These locations reference where we want the top-left corner of the logo div to appear in relation to the
|
||||
// centre point of the fox
|
||||
const AGGREGATOR_LOCATIONS = [
|
||||
{ x: -125, y: -75 },
|
||||
{ x: 30, y: -75 },
|
||||
{ x: -145, y: 0 },
|
||||
{ x: 50, y: 0 },
|
||||
{ x: -135, y: 46 },
|
||||
{ x: 40, y: 46 },
|
||||
];
|
||||
|
||||
function getRandomLocations(numberOfLocations) {
|
||||
const randomLocations = shuffle(AGGREGATOR_LOCATIONS);
|
||||
if (numberOfLocations <= AGGREGATOR_LOCATIONS.length) {
|
||||
return randomLocations.slice(0, numberOfLocations);
|
||||
}
|
||||
const numberOfExtraLocations =
|
||||
numberOfLocations - AGGREGATOR_LOCATIONS.length;
|
||||
return [...randomLocations, ...getRandomLocations(numberOfExtraLocations)];
|
||||
}
|
||||
|
||||
function getMascotTarget(aggregatorName, centerPoint, aggregatorLocationMap) {
|
||||
const location = aggregatorLocationMap[aggregatorName];
|
||||
|
||||
if (!location || !centerPoint) {
|
||||
return centerPoint ?? {};
|
||||
}
|
||||
|
||||
// The aggregator logos are 94px x 40px. For the fox to look at the center of each logo, the target needs to be
|
||||
// the coordinates for the centre point of the fox + the desired top and left coordinates of the logo + half
|
||||
// the height and width of the logo.
|
||||
return {
|
||||
x: location.x + centerPoint.x + 47,
|
||||
y: location.y + centerPoint.y + 20,
|
||||
};
|
||||
}
|
||||
|
||||
export default function LoadingSwapsQuotes({
|
||||
aggregatorMetadata,
|
||||
@ -97,20 +58,6 @@ export default function LoadingSwapsQuotes({
|
||||
const currentMascotContainer = mascotContainer.current;
|
||||
|
||||
const [quoteCount, updateQuoteCount] = useState(0);
|
||||
// is an array of randomized items from AGGREGATOR_LOCATIONS, containing
|
||||
// numberOfQuotes number of items it is randomized so that the order in
|
||||
// which the fox looks at locations is random
|
||||
const [aggregatorLocations] = useState(() =>
|
||||
getRandomLocations(numberOfQuotes),
|
||||
);
|
||||
const _aggregatorLocationMap = aggregatorNames.reduce(
|
||||
(nameLocationMap, name, index) => ({
|
||||
...nameLocationMap,
|
||||
[name]: aggregatorLocations[index],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
const [aggregatorLocationMap] = useState(_aggregatorLocationMap);
|
||||
const [midPointTarget, setMidpointTarget] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -122,7 +69,7 @@ export default function LoadingSwapsQuotes({
|
||||
|
||||
if (loadingComplete) {
|
||||
// If loading is complete, but the quoteCount is not, we quickly display the remaining logos/names/fox looks. 0.2s each
|
||||
timeoutLength = 200;
|
||||
timeoutLength = 20;
|
||||
} else {
|
||||
// If loading is not complete, we display remaining logos/names/fox looks at random intervals between 0.5s and 2s, to simulate the
|
||||
// sort of loading a user would experience in most async scenarios
|
||||
@ -167,13 +114,7 @@ export default function LoadingSwapsQuotes({
|
||||
</span>
|
||||
</div>
|
||||
<div className="loading-swaps-quotes__quote-name-check">
|
||||
<span>
|
||||
{quoteCount === numberOfQuotes
|
||||
? t('swapFinalizing')
|
||||
: t('swapCheckingQuote', [
|
||||
aggregatorMetadata[aggregatorNames[quoteCount]].title,
|
||||
])}
|
||||
</span>
|
||||
<span>{t('swapFetchingQuotes')}</span>
|
||||
</div>
|
||||
<div className="loading-swaps-quotes__loading-bar-container">
|
||||
<div
|
||||
@ -195,37 +136,9 @@ export default function LoadingSwapsQuotes({
|
||||
width="90"
|
||||
height="90"
|
||||
followMouse={false}
|
||||
lookAtTarget={getMascotTarget(
|
||||
aggregatorNames[quoteCount],
|
||||
midPointTarget,
|
||||
aggregatorLocationMap,
|
||||
)}
|
||||
lookAtTarget={midPointTarget}
|
||||
/>
|
||||
</div>
|
||||
{currentMascotContainer &&
|
||||
midPointTarget &&
|
||||
aggregatorNames.map((aggName) => (
|
||||
<div
|
||||
className={classnames('loading-swaps-quotes__logo', {
|
||||
'loading-swaps-quotes__logo--transition':
|
||||
aggName === aggregatorNames[quoteCount],
|
||||
})}
|
||||
style={{
|
||||
opacity: aggName === aggregatorNames[quoteCount] ? 1 : 0,
|
||||
top:
|
||||
aggregatorLocationMap[aggName]?.y + midPointTarget?.y ?? 0,
|
||||
left:
|
||||
aggregatorLocationMap[aggName]?.x + midPointTarget?.x ?? 0,
|
||||
}}
|
||||
key={`aggregator-logo-${aggName}`}
|
||||
>
|
||||
<AggregatorLogo
|
||||
name={aggregatorMetadata[aggName]?.title}
|
||||
icon={aggregatorMetadata[aggName]?.icon}
|
||||
color={aggregatorMetadata[aggName]?.color}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<SwapsFooter
|
||||
|
@ -35,7 +35,6 @@ describe('LoadingSwapsQuotes', () => {
|
||||
store,
|
||||
);
|
||||
expect(getByText('Quote 1 of 2')).toBeInTheDocument();
|
||||
expect(getByText('Checking agg', { exact: false })).toBeInTheDocument();
|
||||
expect(getByText('Back')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -76,7 +76,7 @@ const getBaseUrlForNewSwapsApi = (type, chainId) => {
|
||||
return `${v2ApiBaseUrl}/networks/${chainIdDecimal}`;
|
||||
};
|
||||
|
||||
const getBaseApi = function (
|
||||
export const getBaseApi = function (
|
||||
type,
|
||||
chainId = MAINNET_CHAIN_ID,
|
||||
useNewSwapsApi = false,
|
||||
@ -84,6 +84,7 @@ const getBaseApi = function (
|
||||
const baseUrl = useNewSwapsApi
|
||||
? getBaseUrlForNewSwapsApi(type, chainId)
|
||||
: METASWAP_CHAINID_API_HOST_MAP[chainId];
|
||||
const chainIdDecimal = chainId && parseInt(chainId, 16);
|
||||
if (!baseUrl) {
|
||||
throw new Error(`Swaps API calls are disabled for chainId: ${chainId}`);
|
||||
}
|
||||
@ -100,8 +101,9 @@ const getBaseApi = function (
|
||||
return `${baseUrl}/aggregatorMetadata`;
|
||||
case 'gasPrices':
|
||||
return `${baseUrl}/gasPrices`;
|
||||
case 'refreshTime':
|
||||
return `${baseUrl}/quoteRefreshRate`;
|
||||
case 'network':
|
||||
// Only use v2 for this endpoint.
|
||||
return `${SWAPS_API_V2_BASE_URL}/networks/${chainIdDecimal}`;
|
||||
default:
|
||||
throw new Error('getBaseApi requires an api call type');
|
||||
}
|
||||
@ -441,23 +443,6 @@ export async function fetchSwapsFeatureFlags() {
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function fetchSwapsQuoteRefreshTime(chainId, useNewSwapsApi) {
|
||||
const response = await fetchWithCache(
|
||||
getBaseApi('refreshTime', chainId, useNewSwapsApi),
|
||||
{ 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`;
|
||||
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
getBackgroundSwapRouteState,
|
||||
swapsQuoteSelected,
|
||||
getSwapsQuoteRefreshTime,
|
||||
getReviewSwapClickedTimestamp,
|
||||
} from '../../../ducks/swaps/swaps';
|
||||
import {
|
||||
conversionRateSelector,
|
||||
@ -56,6 +57,7 @@ import {
|
||||
setCustomApproveTxData,
|
||||
setSwapsErrorKey,
|
||||
showModal,
|
||||
setSwapsQuotesPollingLimitEnabled,
|
||||
} from '../../../store/actions';
|
||||
import {
|
||||
ASSET_ROUTE,
|
||||
@ -105,6 +107,8 @@ export default function ViewQuote() {
|
||||
const [warningHidden, setWarningHidden] = useState(false);
|
||||
const [originalApproveAmount, setOriginalApproveAmount] = useState(null);
|
||||
const [showEditGasPopover, setShowEditGasPopover] = useState(false);
|
||||
// We need to have currentTimestamp in state, otherwise it would change with each rerender.
|
||||
const [currentTimestamp] = useState(Date.now());
|
||||
|
||||
const [
|
||||
acknowledgedPriceDifference,
|
||||
@ -150,6 +154,7 @@ export default function ViewQuote() {
|
||||
const defaultSwapsToken = useSelector(getSwapsDefaultToken);
|
||||
const chainId = useSelector(getCurrentChainId);
|
||||
const nativeCurrencySymbol = useSelector(getNativeCurrency);
|
||||
const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp);
|
||||
|
||||
let gasFeeInputs;
|
||||
if (networkAndAccountSupports1559) {
|
||||
@ -371,6 +376,9 @@ export default function ViewQuote() {
|
||||
const showInsufficientWarning =
|
||||
(balanceError || tokenBalanceNeeded || ethBalanceNeeded) && !warningHidden;
|
||||
|
||||
const hardwareWalletUsed = useSelector(isHardwareWallet);
|
||||
const hardwareWalletType = useSelector(getHardwareWalletType);
|
||||
|
||||
const numberOfQuotes = Object.values(quotes).length;
|
||||
const bestQuoteReviewedEventSent = useRef();
|
||||
const eventObjectBase = {
|
||||
@ -384,10 +392,10 @@ export default function ViewQuote() {
|
||||
response_time: fetchParams?.responseTime,
|
||||
best_quote_source: topQuote?.aggregator,
|
||||
available_quotes: numberOfQuotes,
|
||||
is_hardware_wallet: hardwareWalletUsed,
|
||||
hardware_wallet_type: hardwareWalletType,
|
||||
};
|
||||
|
||||
const hardwareWalletUsed = useSelector(isHardwareWallet);
|
||||
const hardwareWalletType = useSelector(getHardwareWalletType);
|
||||
const allAvailableQuotesOpened = useNewMetricEvent({
|
||||
event: 'All Available Quotes Opened',
|
||||
category: 'swaps',
|
||||
@ -398,8 +406,6 @@ export default function ViewQuote() {
|
||||
usedQuote?.aggregator === topQuote?.aggregator
|
||||
? null
|
||||
: usedQuote?.aggregator,
|
||||
is_hardware_wallet: hardwareWalletUsed,
|
||||
hardware_wallet_type: hardwareWalletType,
|
||||
},
|
||||
});
|
||||
const quoteDetailsOpened = useNewMetricEvent({
|
||||
@ -412,8 +418,6 @@ export default function ViewQuote() {
|
||||
usedQuote?.aggregator === topQuote?.aggregator
|
||||
? null
|
||||
: usedQuote?.aggregator,
|
||||
is_hardware_wallet: hardwareWalletUsed,
|
||||
hardware_wallet_type: hardwareWalletType,
|
||||
},
|
||||
});
|
||||
const editSpendLimitOpened = useNewMetricEvent({
|
||||
@ -424,8 +428,6 @@ export default function ViewQuote() {
|
||||
custom_spend_limit_set: originalApproveAmount === approveAmount,
|
||||
custom_spend_limit_amount:
|
||||
originalApproveAmount === approveAmount ? null : approveAmount,
|
||||
is_hardware_wallet: hardwareWalletUsed,
|
||||
hardware_wallet_type: hardwareWalletType,
|
||||
},
|
||||
});
|
||||
|
||||
@ -435,10 +437,18 @@ export default function ViewQuote() {
|
||||
sensitiveProperties: {
|
||||
...eventObjectBase,
|
||||
network_fees: feeInFiat,
|
||||
is_hardware_wallet: hardwareWalletUsed,
|
||||
hardware_wallet_type: hardwareWalletType,
|
||||
},
|
||||
});
|
||||
|
||||
const viewQuotePageLoadedEvent = useNewMetricEvent({
|
||||
event: 'View Quote Page Loaded',
|
||||
category: 'swaps',
|
||||
sensitiveProperties: {
|
||||
...eventObjectBase,
|
||||
response_time: currentTimestamp - reviewSwapClickedTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!bestQuoteReviewedEventSent.current &&
|
||||
@ -653,6 +663,14 @@ export default function ViewQuote() {
|
||||
setShowEditGasPopover(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Thanks to the next line we will only do quotes polling 3 times before showing a Quote Timeout modal.
|
||||
dispatch(setSwapsQuotesPollingLimitEnabled(true));
|
||||
if (reviewSwapClickedTimestamp) {
|
||||
viewQuotePageLoadedEvent();
|
||||
}
|
||||
}, [dispatch, viewQuotePageLoadedEvent, reviewSwapClickedTimestamp]);
|
||||
|
||||
return (
|
||||
<div className="view-quote">
|
||||
<div
|
||||
|
@ -47,6 +47,7 @@ setBackgroundConnection({
|
||||
getGasFeeEstimatesAndStartPolling: jest.fn(),
|
||||
updateTransaction: jest.fn(),
|
||||
getGasFeeTimeEstimate: jest.fn(),
|
||||
setSwapsQuotesPollingLimitEnabled: jest.fn(),
|
||||
});
|
||||
|
||||
describe('ViewQuote', () => {
|
||||
|
@ -2161,6 +2161,13 @@ export function setSwapsTokens(tokens) {
|
||||
};
|
||||
}
|
||||
|
||||
export function clearSwapsQuotes() {
|
||||
return async (dispatch) => {
|
||||
await promisifiedBackground.clearSwapsQuotes();
|
||||
await forceUpdateMetamaskState(dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
export function resetBackgroundSwapsState() {
|
||||
return async (dispatch) => {
|
||||
const id = await promisifiedBackground.resetSwapsState();
|
||||
@ -2214,6 +2221,15 @@ export function updateSwapsUserFeeLevel(swapsCustomUserFeeLevel) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setSwapsQuotesPollingLimitEnabled(quotesPollingLimitEnabled) {
|
||||
return async (dispatch) => {
|
||||
await promisifiedBackground.setSwapsQuotesPollingLimitEnabled(
|
||||
quotesPollingLimitEnabled,
|
||||
);
|
||||
await forceUpdateMetamaskState(dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
export function customSwapsGasParamsUpdated(gasLimit, gasPrice) {
|
||||
return async (dispatch) => {
|
||||
await promisifiedBackground.setSwapsTxGasPrice(gasPrice);
|
||||
|
Loading…
Reference in New Issue
Block a user