From e56c66e4a83bbe6a9ccb403d0d88f071072b0336 Mon Sep 17 00:00:00 2001 From: limitofzero Date: Mon, 1 Dec 2025 17:15:49 +0400 Subject: [PATCH 1/8] feat: fix handle unsupported chain error from bff --- .../src/constants/bff-balances-swr-config.ts | 7 ++- .../src/hooks/usePersistBalancesFromBff.ts | 58 ++++++++++++++++--- .../src/state/isBffFailedAtom.ts | 21 +++++++ .../updaters/BalancesAndAllowancesUpdater.tsx | 4 +- .../src/utils/isBffSupportedNetwork.ts | 9 +++ 5 files changed, 89 insertions(+), 10 deletions(-) diff --git a/libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts b/libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts index 65077cb12de..a07d3f58865 100644 --- a/libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts +++ b/libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts @@ -46,7 +46,12 @@ export const BFF_BALANCES_SWR_CONFIG: SWRConfiguration = { // Pause only if focus has been lost for more than ${FOCUS_HIDDEN_DELAY} seconds return Date.now() - focusLostTimestamp > FOCUS_HIDDEN_DELAY }, - onErrorRetry: (_: unknown, __key, config, revalidate, { retryCount }) => { + onErrorRetry: (error: unknown, _key, config, revalidate, { retryCount }) => { + // Don't retry if error is "Unsupported chain" + if (error instanceof Error && error.message.toLowerCase().includes('unsupported chain')) { + return + } + const timeout = config.errorRetryInterval * Math.pow(2, retryCount - 1) setTimeout(() => revalidate({ retryCount }), timeout) diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts index af37a304932..fd6c85205e3 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts @@ -10,11 +10,12 @@ import useSWR, { SWRConfiguration } from 'swr' import { BFF_BALANCES_SWR_CONFIG } from '../constants/bff-balances-swr-config' import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' -import { useSetIsBffFailed } from '../state/isBffFailedAtom' -import { isBffSupportedNetwork } from '../utils/isBffSupportedNetwork' +import { useSetIsBffFailed, useAddUnsupportedChainId } from '../state/isBffFailedAtom' +import { useIsBffSupportedNetwork } from '../utils/isBffSupportedNetwork' type BalanceResponse = { balances: Record | null + message?: string } export interface PersistBalancesFromBffParams { @@ -25,15 +26,45 @@ export interface PersistBalancesFromBffParams { tokenAddresses: string[] } +function isUnsupportedChainError(errorMessage: string): boolean { + return errorMessage.toLowerCase().includes('unsupported chain') +} + +function parseErrorResponse(data: unknown, statusText: string): string { + if (typeof data === 'object' && data !== null && 'message' in data) { + return String(data.message) + } + return statusText +} + +async function parseBffResponse(res: Response): Promise { + try { + return await res.json() + } catch { + return { message: res.statusText } + } +} + +function handleBffError(res: Response, data: BalanceResponse | { message?: string }): never { + const errorMessage = parseErrorResponse(data, res.statusText) + + if (isUnsupportedChainError(errorMessage)) { + throw new Error('Unsupported chain') + } + + throw new Error(`BFF error: ${res.status} ${res.statusText}`) +} + export function usePersistBalancesFromBff(params: PersistBalancesFromBffParams): void { const { account, chainId, invalidateCacheTrigger, tokenAddresses } = params const { chainId: activeChainId, account: connectedAccount } = useWalletInfo() const targetAccount = account ?? connectedAccount const targetChainId = chainId ?? activeChainId - const isSupportedNetwork = isBffSupportedNetwork(targetChainId) + const isSupportedNetwork = useIsBffSupportedNetwork(targetChainId) const setIsBffFailed = useSetIsBffFailed() + const addUnsupportedChainId = useAddUnsupportedChainId() const lastTriggerRef = useRef(invalidateCacheTrigger) @@ -59,8 +90,15 @@ export function usePersistBalancesFromBff(params: PersistBalancesFromBffParams): }, [setBalances, isBalancesLoading, targetChainId, targetAccount]) useEffect(() => { + const hasUnsupportedChainError = error instanceof Error && + isUnsupportedChainError(error.message) + + if (hasUnsupportedChainError) { + addUnsupportedChainId(targetChainId) + } + setIsBffFailed(!!error) - }, [error, setIsBffFailed]) + }, [error, setIsBffFailed, addUnsupportedChainId, targetChainId]) useEffect(() => { if (!targetAccount || !data || error) return @@ -114,18 +152,22 @@ export async function getBffBalances( try { const res = await fetch(fullUrl) - const data: BalanceResponse = await res.json() + const data = await parseBffResponse(res) if (!res.ok) { - return Promise.reject(new Error(`BFF error: ${res.status} ${res.statusText}`)) + handleBffError(res, data) } - if (!data.balances) { + // Type guard to ensure data has balances property + if (!('balances' in data) || !data.balances) { return null } return data.balances } catch (error) { - return Promise.reject(error) + if (error instanceof Error && isUnsupportedChainError(error.message)) { + throw new Error('Unsupported chain') + } + throw error } } diff --git a/libs/balances-and-allowances/src/state/isBffFailedAtom.ts b/libs/balances-and-allowances/src/state/isBffFailedAtom.ts index cb0192f338b..75e44c9d29c 100644 --- a/libs/balances-and-allowances/src/state/isBffFailedAtom.ts +++ b/libs/balances-and-allowances/src/state/isBffFailedAtom.ts @@ -1,6 +1,8 @@ import { useSetAtom } from 'jotai' import { atom, useAtomValue } from 'jotai/index' +import { SupportedChainId } from '@cowprotocol/cow-sdk' + export const isBffFailedAtom = atom(false) export function useIsBffFailed(): boolean { @@ -10,3 +12,22 @@ export function useIsBffFailed(): boolean { export function useSetIsBffFailed(): (value: boolean) => void { return useSetAtom(isBffFailedAtom) } + +// Store chains that returned "Unsupported chain" error to prevent retries (runtime only, not persisted) +export const bffUnsupportedChainsAtom = atom(new Set()) + +export function useBffUnsupportedChains(): Set { + return useAtomValue(bffUnsupportedChainsAtom) +} + +export function useAddUnsupportedChainId(): (chainId: SupportedChainId) => void { + const setAtom = useSetAtom(bffUnsupportedChainsAtom) + return (chainId) => { + setAtom((prev) => { + if (prev.has(chainId)) { + return prev + } + return new Set([...prev, chainId]) + }) + } +} diff --git a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx index 01848130ed1..216f355955b 100644 --- a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx +++ b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx @@ -16,6 +16,7 @@ import { BASIC_MULTICALL_SWR_CONFIG } from '../consts' import { useNativeTokenBalance } from '../hooks/useNativeTokenBalance' import { useSwrConfigWithPauseForNetwork } from '../hooks/useSwrConfigWithPauseForNetwork' import { useUpdateTokenBalance } from '../hooks/useUpdateTokenBalance' +import { useIsBffSupportedNetwork } from '../utils/isBffSupportedNetwork' // A small gap between balances and allowances refresh intervals is needed to avoid high load to the node at the same time const RPC_BALANCES_SWR_CONFIG: SWRConfiguration = { ...BASIC_MULTICALL_SWR_CONFIG, refreshInterval: ms`31s` } @@ -40,6 +41,7 @@ export function BalancesAndAllowancesUpdater({ isBffEnabled, }: BalancesAndAllowancesUpdaterProps): ReactNode { const updateTokenBalance = useUpdateTokenBalance() + const isBffSupported = useIsBffSupportedNetwork(chainId) const allTokens = useAllActiveTokens() const { data: nativeTokenBalance } = useNativeTokenBalance(account, chainId) @@ -71,7 +73,7 @@ export function BalancesAndAllowancesUpdater({ return ( <> - {isBffEnabled && ( + {isBffEnabled && isBffSupported && ( Date: Mon, 1 Dec 2025 17:22:16 +0400 Subject: [PATCH 2/8] feat: update supported chains list --- .../src/hooks/usePersistBalancesFromBff.ts | 1 - libs/balances-and-allowances/src/state/isBffFailedAtom.ts | 5 ----- .../src/utils/isBffSupportedNetwork.ts | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts index fd6c85205e3..0087f2c1312 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts @@ -158,7 +158,6 @@ export async function getBffBalances( handleBffError(res, data) } - // Type guard to ensure data has balances property if (!('balances' in data) || !data.balances) { return null } diff --git a/libs/balances-and-allowances/src/state/isBffFailedAtom.ts b/libs/balances-and-allowances/src/state/isBffFailedAtom.ts index 75e44c9d29c..10867ce3736 100644 --- a/libs/balances-and-allowances/src/state/isBffFailedAtom.ts +++ b/libs/balances-and-allowances/src/state/isBffFailedAtom.ts @@ -13,13 +13,8 @@ export function useSetIsBffFailed(): (value: boolean) => void { return useSetAtom(isBffFailedAtom) } -// Store chains that returned "Unsupported chain" error to prevent retries (runtime only, not persisted) export const bffUnsupportedChainsAtom = atom(new Set()) -export function useBffUnsupportedChains(): Set { - return useAtomValue(bffUnsupportedChainsAtom) -} - export function useAddUnsupportedChainId(): (chainId: SupportedChainId) => void { const setAtom = useSetAtom(bffUnsupportedChainsAtom) return (chainId) => { diff --git a/libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts b/libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts index 2f94e7f24b2..412e4b4a057 100644 --- a/libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts +++ b/libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts @@ -5,7 +5,7 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { bffUnsupportedChainsAtom } from '../state/isBffFailedAtom' // TODO: check before Plasma launch. Currently unsupported on 2025/10/20 -const UNSUPPORTED_BFF_NETWORKS = [SupportedChainId.LENS, SupportedChainId.SEPOLIA, SupportedChainId.PLASMA] +const UNSUPPORTED_BFF_NETWORKS = [SupportedChainId.PLASMA] export function isBffSupportedNetwork(chainId: SupportedChainId): boolean { return !UNSUPPORTED_BFF_NETWORKS.includes(chainId) From 4a3f1d54770c2883ab1e357c990499a4cb609253 Mon Sep 17 00:00:00 2001 From: limitofzero Date: Mon, 1 Dec 2025 20:19:31 +0400 Subject: [PATCH 3/8] feat: update supported networks --- libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts b/libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts index 412e4b4a057..705fa7102ee 100644 --- a/libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts +++ b/libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts @@ -5,7 +5,7 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { bffUnsupportedChainsAtom } from '../state/isBffFailedAtom' // TODO: check before Plasma launch. Currently unsupported on 2025/10/20 -const UNSUPPORTED_BFF_NETWORKS = [SupportedChainId.PLASMA] +const UNSUPPORTED_BFF_NETWORKS = [SupportedChainId.PLASMA, SupportedChainId.SEPOLIA] export function isBffSupportedNetwork(chainId: SupportedChainId): boolean { return !UNSUPPORTED_BFF_NETWORKS.includes(chainId) From aad27de4bb6ebe6bcfcc97fd902ce2fe0d02053a Mon Sep 17 00:00:00 2001 From: limitofzero Date: Mon, 1 Dec 2025 21:30:43 +0400 Subject: [PATCH 4/8] test: add new cases and adjust old one --- .../hooks/usePersistBalancesFromBff.test.tsx | 113 ++++++++++++++++-- 1 file changed, 104 insertions(+), 9 deletions(-) diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx index 49ed1ebe902..5a12320faf5 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx @@ -1,11 +1,11 @@ -import { Provider } from 'jotai' +import { Provider, useAtomValue } from 'jotai' import { useHydrateAtoms } from 'jotai/utils' import React, { ReactNode } from 'react' import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { PersistentStateByChain } from '@cowprotocol/types' -import { renderHook } from '@testing-library/react' +import { renderHook, waitFor } from '@testing-library/react' import fetchMock from 'jest-fetch-mock' import useSWR from 'swr' @@ -14,16 +14,13 @@ import { PersistBalancesFromBffParams, usePersistBalancesFromBff } from './usePe import { BFF_BALANCES_SWR_CONFIG } from '../constants/bff-balances-swr-config' import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' -import * as isBffFailedAtom from '../state/isBffFailedAtom' -import * as bffUtils from '../utils/isBffSupportedNetwork' +import { bffUnsupportedChainsAtom } from '../state/isBffFailedAtom' // Enable fetch mocking fetchMock.enableMocks() // Mock modules jest.mock('swr') -jest.mock('../utils/isBffSupportedNetwork') -jest.mock('../state/isBffFailedAtom') // Create mock for useWalletInfo const mockUseWalletInfo = jest.fn() @@ -34,7 +31,6 @@ jest.mock('@cowprotocol/wallet', () => ({ })) describe('usePersistBalancesFromBff - invalidateCacheTrigger', () => { - const mockSetIsBffFailed = jest.fn() const mockWalletInfo = { chainId: SupportedChainId.MAINNET, account: '0x1234567890123456789012345678901234567890', @@ -67,6 +63,7 @@ describe('usePersistBalancesFromBff - invalidateCacheTrigger', () => { } as BalancesState, ], [balancesUpdateAtom, mockBalancesUpdate], + [bffUnsupportedChainsAtom, new Set()], ]) return <>{children} } @@ -82,8 +79,6 @@ describe('usePersistBalancesFromBff - invalidateCacheTrigger', () => { jest.clearAllMocks() fetchMock.resetMocks() mockUseWalletInfo.mockReturnValue(mockWalletInfo) - ;(isBffFailedAtom.useSetIsBffFailed as jest.Mock).mockReturnValue(mockSetIsBffFailed) - ;(bffUtils.isBffSupportedNetwork as jest.Mock).mockReturnValue(true) }) describe('hardcoded SWR config', () => { @@ -189,4 +184,104 @@ describe('usePersistBalancesFromBff - invalidateCacheTrigger', () => { }) }) + describe('unsupported chain handling', () => { + it('should not make requests for unsupported chains', () => { + const mockUseSWR = useSWR as jest.MockedFunction + mockUseSWR.mockReturnValue({ + data: undefined, + error: undefined, + isLoading: false, + isValidating: false, + mutate: jest.fn(), + } as ReturnType) + + const unsupportedChainParams: PersistBalancesFromBffParams = { + ...defaultParams, + chainId: SupportedChainId.SEPOLIA, // Unsupported network + } + + renderHook(() => usePersistBalancesFromBff(unsupportedChainParams), { wrapper }) + + // Should not make SWR call for unsupported network + expect(mockUseSWR).toHaveBeenCalledWith( + null, // Key should be null for unsupported network + expect.any(Function), + BFF_BALANCES_SWR_CONFIG, + ) + }) + + it('should add chain to unsupported list when "Unsupported chain" error occurs', async () => { + const mockUseSWR = useSWR as jest.MockedFunction + const unsupportedChainError = new Error('Unsupported chain') + + mockUseSWR.mockReturnValue({ + data: undefined, + error: unsupportedChainError, + isLoading: false, + isValidating: false, + mutate: jest.fn(), + } as ReturnType) + + const useUnsupportedChains = (): Set => { + usePersistBalancesFromBff(defaultParams) + return useAtomValue(bffUnsupportedChainsAtom) + } + + const { result } = renderHook(() => useUnsupportedChains(), { wrapper }) + + // Wait for effect to run and add chain to unsupported list + await waitFor( + () => { + expect(result.current.has(defaultParams.chainId)).toBe(true) + }, + { timeout: 3000 }, + ) + }) + + it('should stop making requests after chain is added to unsupported list', () => { + const mockUseSWR = useSWR as jest.MockedFunction + + const wrapperWithUnsupportedChain = ({ children }: { children: ReactNode }): ReactNode => { + const HydrateAtoms = ({ children }: { children: ReactNode }): ReactNode => { + useHydrateAtoms([ + [ + balancesAtom, + { + isLoading: false, + chainId: SupportedChainId.MAINNET, + values: {}, + fromCache: false, + } as BalancesState, + ], + [balancesUpdateAtom, mockBalancesUpdate], + [bffUnsupportedChainsAtom, new Set([SupportedChainId.MAINNET])], // Chain is in unsupported list + ]) + return <>{children} + } + + return ( + + {children} + + ) + } + + mockUseSWR.mockReturnValue({ + data: undefined, + error: undefined, + isLoading: false, + isValidating: false, + mutate: jest.fn(), + } as ReturnType) + + renderHook(() => usePersistBalancesFromBff(defaultParams), { wrapper: wrapperWithUnsupportedChain }) + + // Should not make SWR call because chain is in unsupported list + expect(mockUseSWR).toHaveBeenCalledWith( + null, // Key should be null + expect.any(Function), + BFF_BALANCES_SWR_CONFIG, + ) + }) + }) }) From 412d91504f6839b173ddf7baec79e7c6fb51ce4a Mon Sep 17 00:00:00 2001 From: limitofzero Date: Thu, 11 Dec 2025 00:49:46 +0400 Subject: [PATCH 5/8] refactor: wrap function into useCallback --- .../src/state/isBffFailedAtom.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/libs/balances-and-allowances/src/state/isBffFailedAtom.ts b/libs/balances-and-allowances/src/state/isBffFailedAtom.ts index 10867ce3736..d90cdc11df6 100644 --- a/libs/balances-and-allowances/src/state/isBffFailedAtom.ts +++ b/libs/balances-and-allowances/src/state/isBffFailedAtom.ts @@ -1,5 +1,6 @@ import { useSetAtom } from 'jotai' import { atom, useAtomValue } from 'jotai/index' +import { useCallback } from 'react' import { SupportedChainId } from '@cowprotocol/cow-sdk' @@ -17,12 +18,15 @@ export const bffUnsupportedChainsAtom = atom(new Set()) export function useAddUnsupportedChainId(): (chainId: SupportedChainId) => void { const setAtom = useSetAtom(bffUnsupportedChainsAtom) - return (chainId) => { - setAtom((prev) => { - if (prev.has(chainId)) { - return prev - } - return new Set([...prev, chainId]) - }) - } + return useCallback( + (chainId) => { + setAtom((prev) => { + if (prev.has(chainId)) { + return prev + } + return new Set([...prev, chainId]) + }) + }, + [setAtom] + ) } From b8eba805fd989a1103ae429f6a53a192fd2a36c5 Mon Sep 17 00:00:00 2001 From: limitofzero Date: Thu, 11 Dec 2025 01:29:48 +0400 Subject: [PATCH 6/8] refactor: create error class --- apps/cowswap-frontend/src/locales/en-US.po | 20 +++++++++------- .../src/hooks/usePersistBalancesFromBff.ts | 24 +++++++++---------- .../src/utils/UnsupportedChainError.ts | 10 ++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 libs/balances-and-allowances/src/utils/UnsupportedChainError.ts diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index 25ea766c9fd..6ab5d4fb347 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -311,7 +311,7 @@ msgstr "Easily set and manage your orders in USD" msgid "Click \"Wrap {nativeSymbol}\" to try again." msgstr "Click \"Wrap {nativeSymbol}\" to try again." -#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/index.tsx +#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/useSearchRows.ts msgid "Tokens from inactive lists. Import specific tokens below or click Manage to activate more lists." msgstr "Tokens from inactive lists. Import specific tokens below or click Manage to activate more lists." @@ -840,8 +840,8 @@ msgid "Copied" msgstr "Copied" #: apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx -msgid "Can't find your token on the list?" -msgstr "Can't find your token on the list?" +#~ msgid "Can't find your token on the list?" +#~ msgstr "Can't find your token on the list?" #: apps/cowswap-frontend/src/modules/trade/pure/ReceiveAmountTitle/index.tsx msgid "icon" @@ -860,8 +860,8 @@ msgid "Please connect your wallet to one of our supported networks." msgstr "Please connect your wallet to one of our supported networks." #: apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx -msgid "<0>Read our guide on how to add custom tokens." -msgstr "<0>Read our guide on how to add custom tokens." +#~ msgid "<0>Read our guide on how to add custom tokens." +#~ msgstr "<0>Read our guide on how to add custom tokens." #: apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx msgid "Retry" @@ -1166,7 +1166,7 @@ msgstr "Select an {accountProxyLabelString} to check for available refunds {chai msgid "Unsupported wallet" msgstr "Unsupported wallet" -#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/index.tsx +#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/useSearchRows.ts msgid "Expanded results from inactive Token Lists" msgstr "Expanded results from inactive Token Lists" @@ -2961,7 +2961,7 @@ msgstr "Safe confirmed signatures" msgid "Winning solver" msgstr "Winning solver" -#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/index.tsx +#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/useSearchRows.ts msgid "Tokens from external sources." msgstr "Tokens from external sources." @@ -3911,6 +3911,10 @@ msgstr "User rejected approval transaction" msgid "Swap on" msgstr "Swap on" +#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/GuideBanner.tsx +msgid "Can't find your token on the list? <0>Read our guide on how to add custom tokens." +msgstr "Can't find your token on the list? <0>Read our guide on how to add custom tokens." + #: apps/cowswap-frontend/src/modules/trade/pure/ProtocolFeeRow/index.tsx #~ msgid "The fee is {protocolFeeBps} BPS ({protocolFeeAsPercent}%), applied only if the trade is executed.<0/><1/>Solver rewards are taken from this fee amount." #~ msgstr "The fee is {protocolFeeBps} BPS ({protocolFeeAsPercent}%), applied only if the trade is executed.<0/><1/>Solver rewards are taken from this fee amount." @@ -4225,7 +4229,7 @@ msgstr "Version" msgid "All tokens" msgstr "All tokens" -#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/index.tsx +#: apps/cowswap-frontend/src/modules/tokensList/pure/TokenSearchContent/useSearchRows.ts msgid "Additional Results from External Sources" msgstr "Additional Results from External Sources" diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts index 0087f2c1312..9c28d1cfc37 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts @@ -10,8 +10,9 @@ import useSWR, { SWRConfiguration } from 'swr' import { BFF_BALANCES_SWR_CONFIG } from '../constants/bff-balances-swr-config' import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' -import { useSetIsBffFailed, useAddUnsupportedChainId } from '../state/isBffFailedAtom' +import { useAddUnsupportedChainId, useSetIsBffFailed } from '../state/isBffFailedAtom' import { useIsBffSupportedNetwork } from '../utils/isBffSupportedNetwork' +import { UnsupportedChainError, isUnsupportedChainError } from '../utils/UnsupportedChainError' type BalanceResponse = { balances: Record | null @@ -26,7 +27,7 @@ export interface PersistBalancesFromBffParams { tokenAddresses: string[] } -function isUnsupportedChainError(errorMessage: string): boolean { +function isUnsupportedChainMessage(errorMessage: string): boolean { return errorMessage.toLowerCase().includes('unsupported chain') } @@ -47,11 +48,11 @@ async function parseBffResponse(res: Response): Promise { - const hasUnsupportedChainError = error instanceof Error && - isUnsupportedChainError(error.message) - + const hasUnsupportedChainError = isUnsupportedChainError(error) + if (hasUnsupportedChainError) { addUnsupportedChainId(targetChainId) } - + setIsBffFailed(!!error) }, [error, setIsBffFailed, addUnsupportedChainId, targetChainId]) @@ -164,8 +164,8 @@ export async function getBffBalances( return data.balances } catch (error) { - if (error instanceof Error && isUnsupportedChainError(error.message)) { - throw new Error('Unsupported chain') + if (isUnsupportedChainError(error)) { + throw new UnsupportedChainError() } throw error } diff --git a/libs/balances-and-allowances/src/utils/UnsupportedChainError.ts b/libs/balances-and-allowances/src/utils/UnsupportedChainError.ts new file mode 100644 index 00000000000..5ec8a8b7c55 --- /dev/null +++ b/libs/balances-and-allowances/src/utils/UnsupportedChainError.ts @@ -0,0 +1,10 @@ +export class UnsupportedChainError extends Error { + constructor(message = 'Unsupported chain') { + super(message) + this.name = 'UnsupportedChainError' + } +} + +export function isUnsupportedChainError(error: unknown): error is UnsupportedChainError { + return error instanceof Error && error instanceof UnsupportedChainError +} From 3d3970923cd3a19dd58852b21a139776149a4fcc Mon Sep 17 00:00:00 2001 From: limitofzero Date: Thu, 11 Dec 2025 01:50:09 +0400 Subject: [PATCH 7/8] fix: tests and refactor --- .../useTradeBasicConfirmDetailsData.ts | 55 +++++++++++++++++++ .../hooks/usePersistBalancesFromBff.test.tsx | 3 +- .../src/hooks/usePersistBalancesFromBff.ts | 27 ++++----- 3 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/useTradeBasicConfirmDetailsData.ts diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/useTradeBasicConfirmDetailsData.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/useTradeBasicConfirmDetailsData.ts new file mode 100644 index 00000000000..8a79872274c --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/useTradeBasicConfirmDetailsData.ts @@ -0,0 +1,55 @@ +import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from 'react' + +import { Currency, CurrencyAmount, Price } from '@uniswap/sdk-core' + +import { Nullish } from 'types' + +import { useUsdAmount } from 'modules/usdAmount' + +import { ReceiveAmountInfo } from '../../types' +import { getLimitPriceFromReceiveAmount } from '../../utils/getLimitPriceFromReceiveAmount' +import { getOrderTypeReceiveAmounts } from '../../utils/getOrderTypeReceiveAmounts' + +interface UseTradeBasicConfirmDetailsDataParams { + receiveAmountInfo: ReceiveAmountInfo + hideUsdValues?: boolean + networkCostsSuffix?: ReactNode + networkCostsTooltipSuffix?: ReactNode +} + +interface UseTradeBasicConfirmDetailsDataResult { + isInvertedState: [boolean, Dispatch>] + amountAfterFees: CurrencyAmount + amountAfterSlippage: CurrencyAmount + amountAfterSlippageUsd: Nullish> + amountAfterFeesUsd: Nullish> + limitPrice: Price | null + networkCostsSuffix?: ReactNode + networkCostsTooltipSuffix?: ReactNode +} + +export function useTradeBasicConfirmDetailsData( + params: UseTradeBasicConfirmDetailsDataParams +): UseTradeBasicConfirmDetailsDataResult { + const { receiveAmountInfo, hideUsdValues, networkCostsSuffix, networkCostsTooltipSuffix } = params + + const isInvertedState = useState(false) + const { amountAfterFees, amountAfterSlippage } = getOrderTypeReceiveAmounts(receiveAmountInfo) + + const amountAfterSlippageUsd = useUsdAmount(hideUsdValues ? null : amountAfterSlippage).value + const amountAfterFeesUsd = useUsdAmount(hideUsdValues ? null : amountAfterFees).value + + const limitPrice = useMemo(() => getLimitPriceFromReceiveAmount(receiveAmountInfo), [receiveAmountInfo]) + + return { + isInvertedState, + amountAfterFees, + amountAfterSlippage, + amountAfterSlippageUsd, + amountAfterFeesUsd, + limitPrice, + networkCostsSuffix, + networkCostsTooltipSuffix, + } +} + diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx index 5a12320faf5..a9cc0b261df 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx @@ -15,6 +15,7 @@ import { PersistBalancesFromBffParams, usePersistBalancesFromBff } from './usePe import { BFF_BALANCES_SWR_CONFIG } from '../constants/bff-balances-swr-config' import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' import { bffUnsupportedChainsAtom } from '../state/isBffFailedAtom' +import { UnsupportedChainError } from '../utils/UnsupportedChainError' // Enable fetch mocking fetchMock.enableMocks() @@ -212,7 +213,7 @@ describe('usePersistBalancesFromBff - invalidateCacheTrigger', () => { it('should add chain to unsupported list when "Unsupported chain" error occurs', async () => { const mockUseSWR = useSWR as jest.MockedFunction - const unsupportedChainError = new Error('Unsupported chain') + const unsupportedChainError = new UnsupportedChainError() mockUseSWR.mockReturnValue({ data: undefined, diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts index 9c28d1cfc37..9f9a66e92c4 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts @@ -12,7 +12,7 @@ import { BFF_BALANCES_SWR_CONFIG } from '../constants/bff-balances-swr-config' import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' import { useAddUnsupportedChainId, useSetIsBffFailed } from '../state/isBffFailedAtom' import { useIsBffSupportedNetwork } from '../utils/isBffSupportedNetwork' -import { UnsupportedChainError, isUnsupportedChainError } from '../utils/UnsupportedChainError' +import { isUnsupportedChainError, UnsupportedChainError } from '../utils/UnsupportedChainError' type BalanceResponse = { balances: Record | null @@ -150,23 +150,16 @@ export async function getBffBalances( const queryParams = skipCache ? '?ignoreCache=true' : '' const fullUrl = url + queryParams - try { - const res = await fetch(fullUrl) - const data = await parseBffResponse(res) - - if (!res.ok) { - handleBffError(res, data) - } + const res = await fetch(fullUrl) + const data = await parseBffResponse(res) - if (!('balances' in data) || !data.balances) { - return null - } + if (!res.ok) { + handleBffError(res, data) + } - return data.balances - } catch (error) { - if (isUnsupportedChainError(error)) { - throw new UnsupportedChainError() - } - throw error + if (!('balances' in data) || !data.balances) { + return null } + + return data.balances } From 7e5eaa687f061a6701b046cb8b70f959746a73fc Mon Sep 17 00:00:00 2001 From: limitofzero Date: Thu, 11 Dec 2025 12:42:13 +0400 Subject: [PATCH 8/8] fix: review remarks --- .../useTradeBasicConfirmDetailsData.ts | 55 ------------------- .../src/constants/bff-balances-swr-config.ts | 3 +- .../src/hooks/usePersistBalancesFromBff.ts | 10 ++-- .../src/utils/UnsupportedChainError.ts | 4 ++ 4 files changed, 11 insertions(+), 61 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/useTradeBasicConfirmDetailsData.ts diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/useTradeBasicConfirmDetailsData.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/useTradeBasicConfirmDetailsData.ts deleted file mode 100644 index 8a79872274c..00000000000 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/useTradeBasicConfirmDetailsData.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from 'react' - -import { Currency, CurrencyAmount, Price } from '@uniswap/sdk-core' - -import { Nullish } from 'types' - -import { useUsdAmount } from 'modules/usdAmount' - -import { ReceiveAmountInfo } from '../../types' -import { getLimitPriceFromReceiveAmount } from '../../utils/getLimitPriceFromReceiveAmount' -import { getOrderTypeReceiveAmounts } from '../../utils/getOrderTypeReceiveAmounts' - -interface UseTradeBasicConfirmDetailsDataParams { - receiveAmountInfo: ReceiveAmountInfo - hideUsdValues?: boolean - networkCostsSuffix?: ReactNode - networkCostsTooltipSuffix?: ReactNode -} - -interface UseTradeBasicConfirmDetailsDataResult { - isInvertedState: [boolean, Dispatch>] - amountAfterFees: CurrencyAmount - amountAfterSlippage: CurrencyAmount - amountAfterSlippageUsd: Nullish> - amountAfterFeesUsd: Nullish> - limitPrice: Price | null - networkCostsSuffix?: ReactNode - networkCostsTooltipSuffix?: ReactNode -} - -export function useTradeBasicConfirmDetailsData( - params: UseTradeBasicConfirmDetailsDataParams -): UseTradeBasicConfirmDetailsDataResult { - const { receiveAmountInfo, hideUsdValues, networkCostsSuffix, networkCostsTooltipSuffix } = params - - const isInvertedState = useState(false) - const { amountAfterFees, amountAfterSlippage } = getOrderTypeReceiveAmounts(receiveAmountInfo) - - const amountAfterSlippageUsd = useUsdAmount(hideUsdValues ? null : amountAfterSlippage).value - const amountAfterFeesUsd = useUsdAmount(hideUsdValues ? null : amountAfterFees).value - - const limitPrice = useMemo(() => getLimitPriceFromReceiveAmount(receiveAmountInfo), [receiveAmountInfo]) - - return { - isInvertedState, - amountAfterFees, - amountAfterSlippage, - amountAfterSlippageUsd, - amountAfterFeesUsd, - limitPrice, - networkCostsSuffix, - networkCostsTooltipSuffix, - } -} - diff --git a/libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts b/libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts index a07d3f58865..7afae99ba46 100644 --- a/libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts +++ b/libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts @@ -2,6 +2,7 @@ import ms from 'ms.macro' import { SWRConfiguration } from 'swr' import { BASIC_MULTICALL_SWR_CONFIG } from '../consts' +import { isUnsupportedChainMessage } from '../utils/UnsupportedChainError' let focusLostTimestamp: number | null = null const FOCUS_HIDDEN_DELAY = ms`20s` @@ -48,7 +49,7 @@ export const BFF_BALANCES_SWR_CONFIG: SWRConfiguration = { }, onErrorRetry: (error: unknown, _key, config, revalidate, { retryCount }) => { // Don't retry if error is "Unsupported chain" - if (error instanceof Error && error.message.toLowerCase().includes('unsupported chain')) { + if (error instanceof Error && isUnsupportedChainMessage(error.message)) { return } diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts index 9f9a66e92c4..eebe4ebdaea 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts @@ -12,7 +12,11 @@ import { BFF_BALANCES_SWR_CONFIG } from '../constants/bff-balances-swr-config' import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' import { useAddUnsupportedChainId, useSetIsBffFailed } from '../state/isBffFailedAtom' import { useIsBffSupportedNetwork } from '../utils/isBffSupportedNetwork' -import { isUnsupportedChainError, UnsupportedChainError } from '../utils/UnsupportedChainError' +import { + isUnsupportedChainError, + isUnsupportedChainMessage, + UnsupportedChainError, +} from '../utils/UnsupportedChainError' type BalanceResponse = { balances: Record | null @@ -27,10 +31,6 @@ export interface PersistBalancesFromBffParams { tokenAddresses: string[] } -function isUnsupportedChainMessage(errorMessage: string): boolean { - return errorMessage.toLowerCase().includes('unsupported chain') -} - function parseErrorResponse(data: unknown, statusText: string): string { if (typeof data === 'object' && data !== null && 'message' in data) { return String(data.message) diff --git a/libs/balances-and-allowances/src/utils/UnsupportedChainError.ts b/libs/balances-and-allowances/src/utils/UnsupportedChainError.ts index 5ec8a8b7c55..41d5aa3d9f5 100644 --- a/libs/balances-and-allowances/src/utils/UnsupportedChainError.ts +++ b/libs/balances-and-allowances/src/utils/UnsupportedChainError.ts @@ -8,3 +8,7 @@ export class UnsupportedChainError extends Error { export function isUnsupportedChainError(error: unknown): error is UnsupportedChainError { return error instanceof Error && error instanceof UnsupportedChainError } + +export function isUnsupportedChainMessage(errorMessage: string): boolean { + return errorMessage.toLowerCase().includes('unsupported chain') +}