diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 3125016ea0b5..9101ea2ea0af 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -119,6 +119,7 @@ export const SENTRY_BACKGROUND_STATE = { quotes: [], quotesLastFetched: true, quotesLoadingStatus: true, + quotesRefreshCount: true, }, }, CronjobController: { diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index da06ba08e923..2f69bb3e3af6 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -6,6 +6,7 @@ import { flushPromises } from '../../../../test/lib/timer-helpers'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import * as bridgeUtil from '../../../../ui/pages/bridge/bridge.util'; +import * as balanceUtils from '../../../../shared/modules/bridge-utils/balance'; import BridgeController from './bridge-controller'; import { BridgeControllerMessenger } from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; @@ -37,7 +38,7 @@ describe('BridgeController', function () { .reply(200, { 'extension-config': { refreshRate: 3, - maxRefreshCount: 1, + maxRefreshCount: 3, }, 'extension-support': true, 'src-network-allowlist': [10, 534352], @@ -78,7 +79,7 @@ describe('BridgeController', function () { destNetworkAllowlist: [CHAIN_IDS.POLYGON, CHAIN_IDS.ARBITRUM], srcNetworkAllowlist: [CHAIN_IDS.OPTIMISM, CHAIN_IDS.SCROLL], extensionConfig: { - maxRefreshCount: 1, + maxRefreshCount: 3, refreshRate: 3, }, }; @@ -236,7 +237,13 @@ describe('BridgeController', function () { bridgeController, 'startPollingByNetworkClientId', ); - messengerMock.call.mockReturnValue({ address: '0x123' } as never); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(true); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); const fetchBridgeQuotesSpy = jest .spyOn(bridgeUtil, 'fetchBridgeQuotes') @@ -280,9 +287,10 @@ describe('BridgeController', function () { expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledWith('0x1', { ...quoteRequest, - insufficientBal: true, + insufficientBal: false, }); expect(bridgeController.state.bridgeState).toStrictEqual( @@ -302,7 +310,7 @@ describe('BridgeController', function () { expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( { ...quoteRequest, - insufficientBal: true, + insufficientBal: false, }, expect.any(AbortSignal), ); @@ -312,7 +320,7 @@ describe('BridgeController', function () { expect(bridgeController.state.bridgeState).toEqual( expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, + quoteRequest: { ...quoteRequest, insufficientBal: false }, quotes: [], quotesLoadingStatus: 0, }), @@ -323,7 +331,7 @@ describe('BridgeController', function () { await flushPromises(); expect(bridgeController.state.bridgeState).toEqual( expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, + quoteRequest: { ...quoteRequest, insufficientBal: false }, quotes: [1, 2, 3], quotesLoadingStatus: 1, }), @@ -335,14 +343,15 @@ describe('BridgeController', function () { // After 2nd fetch jest.advanceTimersByTime(50000); await flushPromises(); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(2); expect(bridgeController.state.bridgeState).toEqual( expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, + quoteRequest: { ...quoteRequest, insufficientBal: false }, quotes: [5, 6, 7], quotesLoadingStatus: 1, + quotesRefreshCount: 2, }), ); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(2); const secondFetchTime = bridgeController.state.bridgeState.quotesLastFetched; expect(secondFetchTime).toBeGreaterThan(firstFetchTime); @@ -353,14 +362,137 @@ describe('BridgeController', function () { expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(3); expect(bridgeController.state.bridgeState).toEqual( expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, + quoteRequest: { ...quoteRequest, insufficientBal: false }, quotes: [5, 6, 7], quotesLoadingStatus: 2, + quotesRefreshCount: 3, }), ); expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( secondFetchTime, ); + + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + }); + + it('updateBridgeQuoteRequestParams should only poll once if insufficientBal=true', async function () { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingByNetworkClientIdSpy = jest.spyOn( + bridgeController, + 'startPollingByNetworkClientId', + ); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(false); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const fetchBridgeQuotesSpy = jest + .spyOn(bridgeUtil, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([1, 2, 3] as never); + }, 5000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementation(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([5, 6, 7] as never); + }, 10000); + }); + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledWith('0x1', { + ...quoteRequest, + insufficientBal: true, + }); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // Loading state + jest.advanceTimersByTime(1000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( + { + ...quoteRequest, + insufficientBal: true, + }, + expect.any(AbortSignal), + ); + expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( + undefined, + ); + + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(10000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: [1, 2, 3], + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + }), + ); + const firstFetchTime = + bridgeController.state.bridgeState.quotesLastFetched ?? 0; + expect(firstFetchTime).toBeGreaterThan(0); + + // After 2nd fetch + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: [1, 2, 3], + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + }), + ); + const secondFetchTime = + bridgeController.state.bridgeState.quotesLastFetched; + expect(secondFetchTime).toStrictEqual(firstFetchTime); }); it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', function () { diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 3e46937f1b45..3164858391b2 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -109,6 +109,7 @@ export default class BridgeController extends StaticIntervalPollingController< quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, quotesLoadingStatus: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + quotesRefreshCount: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount, }; }); @@ -195,18 +196,31 @@ export default class BridgeController extends StaticIntervalPollingController< quoteRequest: request, }; }); + const { maxRefreshCount } = + bridgeState.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG]; + const newQuotesRefreshCount = bridgeState.quotesRefreshCount + 1; try { const quotes = await fetchBridgeQuotes( request, this.#abortController.signal, ); + + // Stop polling if the maximum number of refreshes has been reached + if ( + (request.insufficientBal && newQuotesRefreshCount >= 1) || + (!request.insufficientBal && newQuotesRefreshCount >= maxRefreshCount) + ) { + this.stopAllPolling(); + } + this.update((_state) => { _state.bridgeState = { ..._state.bridgeState, quotes, quotesLastFetched: Date.now(), quotesLoadingStatus: RequestStatus.FETCHED, + quotesRefreshCount: newQuotesRefreshCount, }; }); } catch (error) { @@ -220,6 +234,7 @@ export default class BridgeController extends StaticIntervalPollingController< _state.bridgeState = { ...bridgeState, quotesLoadingStatus: RequestStatus.ERROR, + quotesRefreshCount: newQuotesRefreshCount, }; }); console.log('Failed to fetch bridge quotes', error); diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index a4aa3264fdc8..aa2d74f0a08e 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -34,4 +34,5 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { quotes: [], quotesLastFetched: undefined, quotesLoadingStatus: undefined, + quotesRefreshCount: 0, }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index 527a885c5f38..588a317bc1d4 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -39,6 +39,7 @@ export type BridgeControllerState = { quotes: QuoteResponse[]; quotesLastFetched?: number; quotesLoadingStatus?: RequestStatus; + quotesRefreshCount: number; }; export enum BridgeUserAction { diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index 3b003b044b5a..38bafb2f030c 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -877,6 +877,7 @@ describe('Sentry errors', function () { }, quotesLastFetched: true, quotesLoadingStatus: true, + quotesRefreshCount: true, }, currentPopupId: false, // Initialized as undefined // Part of transaction controller store, but missing from the initial diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 1a871780591f..b228c8e4df7d 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -74,6 +74,7 @@ "srcTokenAddress": "0x0000000000000000000000000000000000000000" }, "quotes": {}, + "quotesRefreshCount": 0, "srcTokens": {}, "srcTopAssets": {} } diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index e8f8f81a6293..1d9f5016b8f5 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -279,6 +279,7 @@ "srcTokenAddress": "0x0000000000000000000000000000000000000000" }, "quotes": {}, + "quotesRefreshCount": 0, "srcTokens": {}, "srcTopAssets": {} }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 1a51023a2ca1..f938fd914873 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -50,23 +50,6 @@ }, "snapsInstallPrivacyWarningShown": true }, - "BridgeController": { - "bridgeState": { - "bridgeFeatureFlags": { - "extensionSupport": "boolean", - "srcNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - }, - "destNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - } - } - } - }, "CurrencyController": { "currentCurrency": "usd", "currencyRates": { diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index dbb49a20d66b..e39f73f2fa15 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -6,8 +6,10 @@ import { } from '../../../shared/constants/network'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { mockNetworkState } from '../../../test/stub/networks'; +import mockErc20Erc20Quotes from '../../../test/data/bridge/mock-quotes-erc20-erc20.json'; import { getAllBridgeableNetworks, + getBridgeQuotes, getFromAmount, getFromChain, getFromChains, @@ -491,4 +493,81 @@ describe('Bridge selectors', () => { expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); }); }); + + describe('getBridgeQuotes', () => { + it('returns quote list and fetch data, insufficientBal=false,quotesRefreshCount=5', () => { + const state = createBridgeMockStore( + { extensionConfig: { maxRefreshCount: 5 } }, + { toChainId: '0x1' }, + { + quoteRequest: { insufficientBal: false }, + quotes: mockErc20Erc20Quotes, + quotesFetchStatus: 1, + quotesRefreshCount: 5, + quotesLastFetched: 100, + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getBridgeQuotes(state as never); + + expect(result).toStrictEqual({ + quotes: mockErc20Erc20Quotes, + quotesLastFetchedMs: 100, + isLoading: false, + quotesRefreshCount: 5, + isQuoteGoingToRefresh: false, + }); + }); + + it('returns quote list and fetch data, insufficientBal=false,quotesRefreshCount=2', () => { + const state = createBridgeMockStore( + { extensionConfig: { maxRefreshCount: 5 } }, + { toChainId: '0x1' }, + { + quoteRequest: { insufficientBal: false }, + quotes: mockErc20Erc20Quotes, + quotesFetchStatus: 1, + quotesRefreshCount: 2, + quotesLastFetched: 100, + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getBridgeQuotes(state as never); + + expect(result).toStrictEqual({ + quotes: mockErc20Erc20Quotes, + quotesLastFetchedMs: 100, + isLoading: false, + quotesRefreshCount: 2, + isQuoteGoingToRefresh: true, + }); + }); + + it('returns quote list and fetch data, insufficientBal=true', () => { + const state = createBridgeMockStore( + { extensionConfig: { maxRefreshCount: 5 } }, + { toChainId: '0x1' }, + { + quoteRequest: { insufficientBal: true }, + quotes: mockErc20Erc20Quotes, + quotesFetchStatus: 1, + quotesRefreshCount: 1, + quotesLastFetched: 100, + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getBridgeQuotes(state as never); + + expect(result).toStrictEqual({ + quotes: mockErc20Erc20Quotes, + quotesLastFetchedMs: 100, + isLoading: false, + quotesRefreshCount: 1, + isQuoteGoingToRefresh: false, + }); + }); + }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 0401472ff784..60704aa6f094 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -130,15 +130,44 @@ export const getToToken = ( export const getFromAmount = (state: BridgeAppState): string | null => state.bridge.fromTokenInputValue; -export const getBridgeQuotes = (state: BridgeAppState) => { - return { - quotes: state.metamask.bridgeState.quotes, - quotesLastFetchedMs: state.metamask.bridgeState.quotesLastFetched, - isLoading: - state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING, - }; +export const getQuoteRequest = (state: BridgeAppState) => { + const { quoteRequest } = state.metamask.bridgeState; + return quoteRequest; }; +export const getBridgeQuotesConfig = (state: BridgeAppState) => + state.metamask.bridgeState?.bridgeFeatureFlags[ + BridgeFeatureFlagsKey.EXTENSION_CONFIG + ] ?? {}; + +export const getBridgeQuotes = createSelector( + (state: BridgeAppState) => state.metamask.bridgeState.quotes, + (state: BridgeAppState) => state.metamask.bridgeState.quotesLastFetched, + (state: BridgeAppState) => + state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING, + (state: BridgeAppState) => state.metamask.bridgeState.quotesRefreshCount, + getBridgeQuotesConfig, + getQuoteRequest, + ( + quotes, + quotesLastFetchedMs, + isLoading, + quotesRefreshCount, + { maxRefreshCount }, + { insufficientBal }, + ) => { + return { + quotes, + quotesLastFetchedMs, + isLoading, + quotesRefreshCount, + isQuoteGoingToRefresh: insufficientBal + ? false + : quotesRefreshCount < maxRefreshCount, + }; + }, +); + export const getRecommendedQuote = createSelector( getBridgeQuotes, ({ quotes }) => { @@ -146,11 +175,6 @@ export const getRecommendedQuote = createSelector( }, ); -export const getQuoteRequest = (state: BridgeAppState) => { - const { quoteRequest } = state.metamask.bridgeState; - return quoteRequest; -}; - export const getToAmount = createSelector(getRecommendedQuote, (quote) => quote ? calcTokenAmount( diff --git a/ui/hooks/bridge/useCountdownTimer.test.ts b/ui/hooks/bridge/useCountdownTimer.test.ts index 14ac21d725fd..f2cd1190b1ba 100644 --- a/ui/hooks/bridge/useCountdownTimer.test.ts +++ b/ui/hooks/bridge/useCountdownTimer.test.ts @@ -16,12 +16,22 @@ describe('useCountdownTimer', () => { it('returns time remaining', async () => { const quotesLastFetched = Date.now(); const { result } = renderUseCountdownTimer( - createBridgeMockStore({}, {}, { quotesLastFetched }), + createBridgeMockStore( + {}, + {}, + { + quotesLastFetched, + quotesRefreshCount: 0, + bridgeFeatureFlags: { + extensionConfig: { maxRefreshCount: 5, refreshRate: 40000 }, + }, + }, + ), ); let i = 0; - while (i <= 30) { - const secondsLeft = Math.min(30, 30 - i + 1); + while (i <= 40) { + const secondsLeft = Math.min(41, 40 - i + 2); expect(result.current).toStrictEqual( `0:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`, ); diff --git a/ui/hooks/bridge/useCountdownTimer.ts b/ui/hooks/bridge/useCountdownTimer.ts index 112d35b33f6a..39e7ac9d2eca 100644 --- a/ui/hooks/bridge/useCountdownTimer.ts +++ b/ui/hooks/bridge/useCountdownTimer.ts @@ -1,10 +1,10 @@ import { Duration } from 'luxon'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { getBridgeQuotes } from '../../ducks/bridge/selectors'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { REFRESH_INTERVAL_MS } from '../../../app/scripts/controllers/bridge/constants'; +import { + getBridgeQuotes, + getBridgeQuotesConfig, +} from '../../ducks/bridge/selectors'; import { SECOND } from '../../../shared/constants/time'; /** @@ -15,13 +15,15 @@ import { SECOND } from '../../../shared/constants/time'; * @returns The formatted remaining time in 'm:ss' format. */ export const useCountdownTimer = () => { - const [timeRemaining, setTimeRemaining] = useState(REFRESH_INTERVAL_MS); const { quotesLastFetchedMs } = useSelector(getBridgeQuotes); + const { refreshRate } = useSelector(getBridgeQuotesConfig); + + const [timeRemaining, setTimeRemaining] = useState(refreshRate); useEffect(() => { if (quotesLastFetchedMs) { setTimeRemaining( - REFRESH_INTERVAL_MS - (Date.now() - quotesLastFetchedMs), + refreshRate - (Date.now() - quotesLastFetchedMs) + SECOND, ); } }, [quotesLastFetchedMs]); diff --git a/ui/pages/bridge/quotes/bridge-quote-card.test.tsx b/ui/pages/bridge/quotes/bridge-quote-card.test.tsx index b9bfc69e8e5a..274ade65a4d1 100644 --- a/ui/pages/bridge/quotes/bridge-quote-card.test.tsx +++ b/ui/pages/bridge/quotes/bridge-quote-card.test.tsx @@ -23,9 +23,14 @@ describe('BridgeQuoteCard', () => { }, { fromTokenInputValue: 1 }, { + quoteRequest: { insufficientBal: false }, + quotesRefreshCount: 1, quotes: mockBridgeQuotesErc20Erc20, getQuotesLastFetched: Date.now(), quotesLoadingStatus: RequestStatus.FETCHED, + bridgeFeatureFlags: { + extensionConfig: { maxRefreshCount: 5, refreshRate: 30000 }, + }, }, ); const { container } = renderWithProvider( diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx index 2928565809af..fc1176c8c3f9 100644 --- a/ui/pages/bridge/quotes/bridge-quote-card.tsx +++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx @@ -20,7 +20,7 @@ import { BridgeQuotesModal } from './bridge-quotes-modal'; export const BridgeQuoteCard = () => { const t = useI18nContext(); const recommendedQuote = useSelector(getRecommendedQuote); - const { isLoading } = useSelector(getBridgeQuotes); + const { isLoading, isQuoteGoingToRefresh } = useSelector(getBridgeQuotes); const { etaInMinutes, totalFees, quoteRate } = getQuoteDisplayData(recommendedQuote); @@ -44,7 +44,7 @@ export const BridgeQuoteCard = () => { onClose={() => setShowAllQuotes(false)} /> - {!isLoading && ( + {!isLoading && isQuoteGoingToRefresh && ( {t('swapNewQuoteIn', [secondsUntilNextRefresh])} )}