From 383a8a5da7fb0927d96ce4a9708ad5f37094e89b Mon Sep 17 00:00:00 2001 From: Eugene Chybisov Date: Wed, 20 Apr 2022 14:08:29 +0200 Subject: [PATCH] fix: improve token filtering perfomance --- libs/react-scripts/package.json | 2 +- packages/widget/src/components/SwapButton.tsx | 3 + .../src/components/TokenList/TokenList.tsx | 28 ++--- .../components/TokenList/TokenListItem.tsx | 2 +- packages/widget/src/hooks/index.ts | 1 + .../src/hooks/useHasSufficientBalance.ts | 31 +++-- packages/widget/src/hooks/useTokenBalances.ts | 60 ++++++++++ packages/widget/src/hooks/useTokens.ts | 110 ++---------------- .../WidgetProvider/WidgetProvider.tsx | 4 +- yarn.lock | 8 +- 10 files changed, 112 insertions(+), 137 deletions(-) create mode 100644 packages/widget/src/hooks/useTokenBalances.ts diff --git a/libs/react-scripts/package.json b/libs/react-scripts/package.json index a05a1ba17..840071f5a 100644 --- a/libs/react-scripts/package.json +++ b/libs/react-scripts/package.json @@ -32,7 +32,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "@svgr/webpack": "^6.2.1", "babel-jest": "^27.5.0", - "babel-loader": "^8.2.4", + "babel-loader": "^8.2.5", "babel-plugin-import": "^1.13.5", "babel-plugin-named-asset-import": "^0.3.8", "babel-preset-react-app": "^10.0.1", diff --git a/packages/widget/src/components/SwapButton.tsx b/packages/widget/src/components/SwapButton.tsx index 89b7665ab..295b9ee36 100644 --- a/packages/widget/src/components/SwapButton.tsx +++ b/packages/widget/src/components/SwapButton.tsx @@ -23,12 +23,15 @@ export const SwapButton: React.FC = () => { const { getChainById } = useChains(); const { account, switchChain } = useWallet(); const { executeRoute } = useSwapExecutionContext(); + const { routes: swapRoutes } = useSwapRoutes(); + const { hasGasBalanceOnStartChain, hasGasOnCrossChain, hasSufficientBalance, } = useHasSufficientBalance(swapRoutes?.[0]); + const [chainId] = useWatch({ name: [SwapFormKeyHelper.getChainKey('from')], }); diff --git a/packages/widget/src/components/TokenList/TokenList.tsx b/packages/widget/src/components/TokenList/TokenList.tsx index 76fff489a..79cdd708d 100644 --- a/packages/widget/src/components/TokenList/TokenList.tsx +++ b/packages/widget/src/components/TokenList/TokenList.tsx @@ -1,10 +1,10 @@ +import { TokenAmount } from '@lifinance/sdk'; import { Box, List, Typography } from '@mui/material'; import { FC, PropsWithChildren, useCallback, useMemo, useRef } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { defaultRangeExtractor, Range, useVirtual } from 'react-virtual'; -import { useDebouncedWatch } from '../../hooks/useDebouncedWatch'; -import { useTokens } from '../../hooks/useTokens'; +import { useDebouncedWatch, useTokenBalances } from '../../hooks'; import { TokenFilterType } from '../../pages/SelectTokenPage'; import { SwapFormKey, @@ -46,16 +46,16 @@ export const TokenList: FC> = ({ isLoading, isBalancesLoading, isBalancesFetching, - } = useTokens(selectedChainId); - - const filteredChainTokens = - myTokensFilter === TokenFilterType.My ? tokensWithBalance : tokens; - const isFilteredChainTokensLoading = - myTokensFilter === TokenFilterType.My ? isBalancesLoading : isLoading; + } = useTokenBalances(selectedChainId); const chainTokens = useMemo(() => { - let chainTokens = filteredChainTokens?.[selectedChainId] ?? []; + let chainTokens = + (myTokensFilter === TokenFilterType.My + ? tokensWithBalance?.filter((token) => token.amount !== '0') + : tokens) ?? []; const searchFilter = debouncedSearchTokensFilter?.toUpperCase() ?? ''; + const isFilteredChainTokensLoading = + myTokensFilter === TokenFilterType.My ? isBalancesLoading : isLoading; chainTokens = isFilteredChainTokensLoading ? // tokenAmountMock is used as a first token to create sticky header for virtual list [tokenAmountMock, ...createTokenAmountSkeletons()] @@ -73,9 +73,11 @@ export const TokenList: FC> = ({ return chainTokens; }, [ debouncedSearchTokensFilter, - filteredChainTokens, - isFilteredChainTokensLoading, - selectedChainId, + isBalancesLoading, + isLoading, + myTokensFilter, + tokens, + tokensWithBalance, ]); const parentRef = useRef(null); @@ -128,7 +130,7 @@ export const TokenList: FC> = ({ ); } - const token = chainTokens[item.index]; + const token = chainTokens[item.index] as TokenAmount; if (token.name.includes(skeletonKey)) { return ( = memo( ) : ( - {token.amount} + {token.amount ?? '0'} ) } diff --git a/packages/widget/src/hooks/index.ts b/packages/widget/src/hooks/index.ts index 32f97a375..c981b3cc0 100644 --- a/packages/widget/src/hooks/index.ts +++ b/packages/widget/src/hooks/index.ts @@ -8,4 +8,5 @@ export * from './useHasSufficientBalance'; export * from './useSwapRoutes'; export * from './useToken'; export * from './useTokenBalance'; +export * from './useTokenBalances'; export * from './useTokens'; diff --git a/packages/widget/src/hooks/useHasSufficientBalance.ts b/packages/widget/src/hooks/useHasSufficientBalance.ts index 7a486276d..30c1839c7 100644 --- a/packages/widget/src/hooks/useHasSufficientBalance.ts +++ b/packages/widget/src/hooks/useHasSufficientBalance.ts @@ -3,7 +3,7 @@ import Big from 'big.js'; import { useMemo } from 'react'; import { useWatch } from 'react-hook-form'; import { SwapFormKeyHelper } from '../providers/SwapFormProvider'; -import { useTokens } from './useTokens'; +import { useTokenBalances } from './useTokenBalances'; export const useHasSufficientBalance = (route?: Route) => { const [fromChainId, toChainId] = useWatch({ @@ -13,8 +13,9 @@ export const useHasSufficientBalance = (route?: Route) => { ], }); const lastStep = route?.steps.at(-1); - const { tokensWithBalance: fromChainTokenBalances } = useTokens(fromChainId); - const { tokensWithBalance: toChainTokenBalances } = useTokens( + const { tokensWithBalance: fromChainTokenBalances } = + useTokenBalances(fromChainId); + const { tokensWithBalance: toChainTokenBalances } = useTokenBalances( lastStep?.action.fromChainId ?? toChainId, ); @@ -24,15 +25,15 @@ export const useHasSufficientBalance = (route?: Route) => { return true; } const balance = Big( - fromChainTokenBalances?.[route.fromChainId].find( - (t) => t.address === token.address, - )?.amount ?? 0, + fromChainTokenBalances?.find((t) => t.address === token.address) + ?.amount ?? 0, ); const requiredAmount = route.steps .filter((step) => step.action.fromChainId === route.fromChainId) - .map((step) => step.estimate.gasCosts?.[0].amount) - .map((amount) => Big(amount || 0)) - .reduce((a, b) => a.plus(b), Big(0)) + .reduce( + (big, step) => big.plus(Big(step.estimate.gasCosts?.[0].amount || 0)), + Big(0), + ) .div(10 ** token.decimals); return balance.gt(0) && balance.gte(requiredAmount); }, [fromChainTokenBalances, route]); @@ -43,9 +44,8 @@ export const useHasSufficientBalance = (route?: Route) => { return true; } const balance = Big( - toChainTokenBalances?.[lastStep.action.fromChainId].find( - (t) => t.address === token.address, - )?.amount ?? 0, + toChainTokenBalances?.find((t) => t.address === token.address)?.amount ?? + 0, ); const gasEstimate = lastStep.estimate.gasCosts?.[0].amount; const requiredAmount = Big(gasEstimate || 0).div( @@ -58,13 +58,10 @@ export const useHasSufficientBalance = (route?: Route) => { if (!route) { return true; } - const balance = Big( - fromChainTokenBalances?.[route.fromChainId].find( - (t) => t.address === route.fromToken.address, - )?.amount ?? 0, + fromChainTokenBalances?.find((t) => t.address === route.fromToken.address) + ?.amount ?? 0, ); - return Big(route.fromAmount) .div(10 ** (route.fromToken.decimals ?? 0)) .lte(balance); diff --git a/packages/widget/src/hooks/useTokenBalances.ts b/packages/widget/src/hooks/useTokenBalances.ts new file mode 100644 index 000000000..867113a6c --- /dev/null +++ b/packages/widget/src/hooks/useTokenBalances.ts @@ -0,0 +1,60 @@ +import { TokenAmount } from '@lifinance/sdk'; +import { useQuery } from 'react-query'; +import { LiFi } from '../lifi'; +import { useWallet } from '../providers/WalletProvider'; +import { formatTokenAmount } from '../utils/format'; +import { useChains } from './useChains'; +import { useTokens } from './useTokens'; + +export const useTokenBalances = (selectedChainId: number) => { + const { account } = useWallet(); + const { chains, isLoading: isChainsLoading } = useChains(); + const { tokens, isLoading, isFetching } = useTokens(selectedChainId); + + const isBalancesLoadingEnabled = + Boolean(account.address) && Boolean(tokens) && Boolean(chains); + + const { + data: tokensWithBalance, + isLoading: isBalancesLoading, + isFetching: isBalancesFetching, + refetch, + } = useQuery( + ['token-balances', selectedChainId, account.address], + async ({ queryKey: [_, chainId, account] }) => { + if (!account || !tokens) { + return []; + } + const tokenBalances = await LiFi.getTokenBalances( + account as string, + tokens, + ); + + const formatedTokens = ( + tokenBalances.length === 0 ? (tokens as TokenAmount[]) : tokenBalances + ).map((token) => { + token.amount = formatTokenAmount(token.amount); + return token; + }); + return formatedTokens; + }, + { + enabled: isBalancesLoadingEnabled, + refetchIntervalInBackground: true, + refetchInterval: 60_000, + staleTime: 60_000, + }, + ); + + return { + tokens: tokensWithBalance ?? tokens, + tokensWithBalance, + isLoading: (isLoading && isFetching) || isChainsLoading, + isBalancesLoading: + (isLoading && isFetching) || + isChainsLoading || + (isBalancesLoading && isBalancesLoadingEnabled), + isBalancesFetching, + updateBalances: refetch, + }; +}; diff --git a/packages/widget/src/hooks/useTokens.ts b/packages/widget/src/hooks/useTokens.ts index dd7982b19..72f50b312 100644 --- a/packages/widget/src/hooks/useTokens.ts +++ b/packages/widget/src/hooks/useTokens.ts @@ -1,108 +1,18 @@ -import { Token, TokenAmount } from '@lifinance/sdk'; -import { useCallback, useMemo } from 'react'; import { useQuery } from 'react-query'; import { LiFi } from '../lifi'; -import { useWallet } from '../providers/WalletProvider'; -import { formatTokenAmount } from '../utils/format'; -import { useChains } from './useChains'; - -interface TokenAmountList { - [chainId: number]: Array; -} export const useTokens = (selectedChainId: number) => { - const { account } = useWallet(); - const { chains, isLoading: isChainsLoading, getChainById } = useChains(); - const { data, isLoading, isFetching } = useQuery(['tokens'], () => - LiFi.getPossibilities({ include: ['tokens'] }), - ); - - const formatTokens = useCallback( - (tokens: Token[] = []) => { - if (!chains) { - return []; - } - const tokenAmountList: TokenAmountList = tokens.reduce( - (tokenAmountList, token) => { - const chain = getChainById(token.chainId); - if (!chain) { - return tokenAmountList; - } - if (!tokenAmountList[chain.id]) { - tokenAmountList[chain.id] = []; - } - (token as TokenAmount).amount = formatTokenAmount( - (token as TokenAmount).amount, - ); - tokenAmountList[chain.id].push({ amount: '0', ...token }); - return tokenAmountList; - }, - {}, - ); - const filteredTokenAmountList = chains.reduce>( - (tokens, chain) => { - if (tokenAmountList[chain.id]) { - tokens[0][chain.id] = tokenAmountList[chain.id]; - tokens[1][chain.id] = tokenAmountList[chain.id].filter( - (token) => token.amount !== '0', - ); - } - return tokens; - }, - [{}, {}], - ); - - return filteredTokenAmountList; - }, - [chains, getChainById], - ); - - const [tokens] = useMemo( - () => formatTokens(data?.tokens), - [formatTokens, data?.tokens], - ); - - const isBalancesLoadingEnabled = - Boolean(account.address) && Boolean(data) && Boolean(chains); - const { - data: tokensWithBalance, - isLoading: isBalancesLoading, - isFetching: isBalancesFetching, - refetch, - } = useQuery( - ['tokens', selectedChainId, account.address], - async ({ queryKey: [_, chainId, account] }) => { - if (!account || !data) { - return []; - } - const tokenBalances = await LiFi.getTokenBalances( - account as string, - tokens[chainId as number], - ); - - const formatedTokens = formatTokens( - tokenBalances.length === 0 ? tokens[chainId as number] : tokenBalances, - ); - return formatedTokens; - }, - { - enabled: isBalancesLoadingEnabled, - refetchIntervalInBackground: true, - refetchInterval: 60_000, - staleTime: 60_000, - }, - ); - + data: tokens, + isLoading, + isFetching, + } = useQuery(['tokens', selectedChainId], async () => { + const data = await LiFi.getPossibilities({ include: ['tokens'] }); + return data.tokens?.filter((token) => token.chainId === selectedChainId); + }); return { - tokens: tokensWithBalance?.[0] ?? tokens, - tokensWithBalance: tokensWithBalance?.[1], - isLoading: (isLoading && isFetching) || isChainsLoading, - isBalancesLoading: - (isLoading && isFetching) || - isChainsLoading || - (isBalancesLoading && isBalancesLoadingEnabled), - isBalancesFetching, - updateBalances: refetch, + tokens, + isLoading, + isFetching, }; }; diff --git a/packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx b/packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx index 48f4c1993..23f21c9f2 100644 --- a/packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx +++ b/packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx @@ -15,7 +15,9 @@ const WidgetContext = createContext(initialContext); export const useWidgetConfig = (): WidgetContextProps => useContext(WidgetContext); -export const WidgetProvider: React.FC> = ({ +export const WidgetProvider: React.FC< + React.PropsWithChildren +> = ({ children, config: { enabledChains, diff --git a/yarn.lock b/yarn.lock index 35401943e..3dd68fe5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4517,10 +4517,10 @@ babel-jest@^27.5.0, babel-jest@^27.5.1: graceful-fs "^4.2.9" slash "^3.0.0" -babel-loader@^8.2.4: - version "8.2.4" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.4.tgz#95f5023c791b2e9e2ca6f67b0984f39c82ff384b" - integrity sha512-8dytA3gcvPPPv4Grjhnt8b5IIiTcq/zeXOPk4iTYI0SVXcsmuGg7JtBRDp8S9X+gJfhQ8ektjXZlDu1Bb33U8A== +babel-loader@^8.2.5: + version "8.2.5" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.5.tgz#d45f585e654d5a5d90f5350a779d7647c5ed512e" + integrity sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ== dependencies: find-cache-dir "^3.3.1" loader-utils "^2.0.0"