Skip to content

Commit

Permalink
fix: improve token filtering perfomance
Browse files Browse the repository at this point in the history
  • Loading branch information
chybisov committed Apr 20, 2022
1 parent be23def commit 383a8a5
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 137 deletions.
2 changes: 1 addition & 1 deletion libs/react-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/widget/src/components/SwapButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')],
});
Expand Down
28 changes: 15 additions & 13 deletions packages/widget/src/components/TokenList/TokenList.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -46,16 +46,16 @@ export const TokenList: FC<PropsWithChildren<TokenListProps>> = ({
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()]
Expand All @@ -73,9 +73,11 @@ export const TokenList: FC<PropsWithChildren<TokenListProps>> = ({
return chainTokens;
}, [
debouncedSearchTokensFilter,
filteredChainTokens,
isFilteredChainTokensLoading,
selectedChainId,
isBalancesLoading,
isLoading,
myTokensFilter,
tokens,
tokensWithBalance,
]);

const parentRef = useRef<HTMLUListElement | null>(null);
Expand Down Expand Up @@ -128,7 +130,7 @@ export const TokenList: FC<PropsWithChildren<TokenListProps>> = ({
</Box>
);
}
const token = chainTokens[item.index];
const token = chainTokens[item.index] as TokenAmount;
if (token.name.includes(skeletonKey)) {
return (
<TokenListItemSkeleton
Expand Down
2 changes: 1 addition & 1 deletion packages/widget/src/components/TokenList/TokenListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const TokenListItem: React.FC<TokenListItemProps> = memo(
<Skeleton variant="text" width={50} height={24} />
) : (
<Typography variant="body1" noWrap>
{token.amount}
{token.amount ?? '0'}
</Typography>
)
}
Expand Down
1 change: 1 addition & 0 deletions packages/widget/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export * from './useHasSufficientBalance';
export * from './useSwapRoutes';
export * from './useToken';
export * from './useTokenBalance';
export * from './useTokenBalances';
export * from './useTokens';
31 changes: 14 additions & 17 deletions packages/widget/src/hooks/useHasSufficientBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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,
);

Expand All @@ -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]);
Expand All @@ -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(
Expand All @@ -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);
Expand Down
60 changes: 60 additions & 0 deletions packages/widget/src/hooks/useTokenBalances.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
110 changes: 10 additions & 100 deletions packages/widget/src/hooks/useTokens.ts
Original file line number Diff line number Diff line change
@@ -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<TokenAmount>;
}

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>(
(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<Array<TokenAmountList>>(
(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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const WidgetContext = createContext<WidgetContextProps>(initialContext);
export const useWidgetConfig = (): WidgetContextProps =>
useContext(WidgetContext);

export const WidgetProvider: React.FC<React.PropsWithChildren<WidgetProviderProps>> = ({
export const WidgetProvider: React.FC<
React.PropsWithChildren<WidgetProviderProps>
> = ({
children,
config: {
enabledChains,
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 383a8a5

Please sign in to comment.