From 164501c600454f4bdd499dc02fb6655475ef9127 Mon Sep 17 00:00:00 2001 From: Eugene Chybisov Date: Thu, 21 Jul 2022 15:05:51 +0100 Subject: [PATCH] fix: improve gas sufficiency handling --- .../GasSufficiencyMessage.style.ts} | 1 + .../GasSufficiencyMessage.tsx | 62 ++++++++ .../components/GasSufficiencyMessage/index.ts | 1 + .../InsufficientGasOrFundsMessage.tsx | 46 ------ .../InsufficientGasOrFundsMessage/index.ts | 1 - .../src/components/SwapButton/SwapButton.tsx | 10 +- packages/widget/src/hooks/index.ts | 2 +- .../widget/src/hooks/useGasSufficiency.ts | 135 ++++++++++++++++++ .../src/hooks/useHasSufficientBalance.ts | 89 ------------ packages/widget/src/i18n/en/translation.json | 8 +- .../widget/src/pages/MainPage/MainPage.tsx | 4 +- .../widget/src/pages/SwapPage/SwapPage.tsx | 4 +- 12 files changed, 211 insertions(+), 152 deletions(-) rename packages/widget/src/components/{InsufficientGasOrFundsMessage/InsufficientGasOrFundsMessage.style.ts => GasSufficiencyMessage/GasSufficiencyMessage.style.ts} (93%) create mode 100644 packages/widget/src/components/GasSufficiencyMessage/GasSufficiencyMessage.tsx create mode 100644 packages/widget/src/components/GasSufficiencyMessage/index.ts delete mode 100644 packages/widget/src/components/InsufficientGasOrFundsMessage/InsufficientGasOrFundsMessage.tsx delete mode 100644 packages/widget/src/components/InsufficientGasOrFundsMessage/index.ts create mode 100644 packages/widget/src/hooks/useGasSufficiency.ts delete mode 100644 packages/widget/src/hooks/useHasSufficientBalance.ts diff --git a/packages/widget/src/components/InsufficientGasOrFundsMessage/InsufficientGasOrFundsMessage.style.ts b/packages/widget/src/components/GasSufficiencyMessage/GasSufficiencyMessage.style.ts similarity index 93% rename from packages/widget/src/components/InsufficientGasOrFundsMessage/InsufficientGasOrFundsMessage.style.ts rename to packages/widget/src/components/GasSufficiencyMessage/GasSufficiencyMessage.style.ts index bf87300c5..f6b30d5e2 100644 --- a/packages/widget/src/components/InsufficientGasOrFundsMessage/InsufficientGasOrFundsMessage.style.ts +++ b/packages/widget/src/components/GasSufficiencyMessage/GasSufficiencyMessage.style.ts @@ -9,4 +9,5 @@ export const MessageCard = styled(Box)(({ theme }) => ({ borderRadius: theme.shape.borderRadius, position: 'relative', display: 'flex', + whiteSpace: 'pre-line', })); diff --git a/packages/widget/src/components/GasSufficiencyMessage/GasSufficiencyMessage.tsx b/packages/widget/src/components/GasSufficiencyMessage/GasSufficiencyMessage.tsx new file mode 100644 index 000000000..47e0134f6 --- /dev/null +++ b/packages/widget/src/components/GasSufficiencyMessage/GasSufficiencyMessage.tsx @@ -0,0 +1,62 @@ +import { Warning as WarningIcon } from '@mui/icons-material'; +import { Box, BoxProps, Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { useGasSufficiency } from '../../hooks'; +import { CardTitle } from '../Card'; +import { MessageCard } from './GasSufficiencyMessage.style'; + +export const GasSufficiencyMessage: React.FC = (props) => { + const { t } = useTranslation(); + const { insufficientFunds, insufficientGas } = useGasSufficiency(); + + if (!insufficientFunds || !insufficientGas.length) { + return null; + } + + let title; + let message; + if (insufficientFunds) { + message = t(`swap.warning.message.insufficientFunds`); + } + if (insufficientGas.length) { + title = t(`swap.warning.title.insufficientGas`); + message = t(`swap.warning.message.insufficientGas`); + } + return ( + + + + {title ? {title} : null} + + {message} + + {insufficientGas.length + ? insufficientGas.map((item, index) => ( + + {t(`swap.gasAmount`, { + amount: item.insufficientAmount?.toString(), + tokenSymbol: item.token.symbol, + chainName: item.chain?.name, + })} + + )) + : null} + + + ); +}; diff --git a/packages/widget/src/components/GasSufficiencyMessage/index.ts b/packages/widget/src/components/GasSufficiencyMessage/index.ts new file mode 100644 index 000000000..7921e619b --- /dev/null +++ b/packages/widget/src/components/GasSufficiencyMessage/index.ts @@ -0,0 +1 @@ +export * from './GasSufficiencyMessage'; diff --git a/packages/widget/src/components/InsufficientGasOrFundsMessage/InsufficientGasOrFundsMessage.tsx b/packages/widget/src/components/InsufficientGasOrFundsMessage/InsufficientGasOrFundsMessage.tsx deleted file mode 100644 index 7659b063e..000000000 --- a/packages/widget/src/components/InsufficientGasOrFundsMessage/InsufficientGasOrFundsMessage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Warning as WarningIcon } from '@mui/icons-material'; -import { Box, BoxProps, Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import { CardTitle } from '../../components/Card'; -import { useHasSufficientBalance } from '../../hooks'; -import { MessageCard } from './InsufficientGasOrFundsMessage.style'; - -export const InsufficientGasOrFundsMessage: React.FC = (props) => { - const { t } = useTranslation(); - const { hasGasOnStartChain, hasGasOnCrossChain, hasSufficientBalance } = - useHasSufficientBalance(); - - if (hasSufficientBalance && hasGasOnStartChain && hasGasOnCrossChain) { - return null; - } - - let title; - let message; - if (!hasSufficientBalance) { - message = t(`swap.warning.message.insufficientFunds`); - } - if (!hasGasOnStartChain) { - title = t(`swap.warning.title.insufficientGas`); - message = t(`swap.warning.message.insufficientGasOnStartChain`); - } - if (!hasGasOnCrossChain) { - title = t(`swap.warning.title.insufficientGas`); - message = t(`swap.warning.message.insufficientGasOnDestinationChain`); - } - return ( - - - - {title ? {title} : null} - - {message} - - - - ); -}; diff --git a/packages/widget/src/components/InsufficientGasOrFundsMessage/index.ts b/packages/widget/src/components/InsufficientGasOrFundsMessage/index.ts deleted file mode 100644 index e33325d18..000000000 --- a/packages/widget/src/components/InsufficientGasOrFundsMessage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './InsufficientGasOrFundsMessage'; diff --git a/packages/widget/src/components/SwapButton/SwapButton.tsx b/packages/widget/src/components/SwapButton/SwapButton.tsx index 46b2e318f..e128038b8 100644 --- a/packages/widget/src/components/SwapButton/SwapButton.tsx +++ b/packages/widget/src/components/SwapButton/SwapButton.tsx @@ -2,7 +2,7 @@ import { ChainId } from '@lifi/sdk'; import { useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { useChains, useHasSufficientBalance } from '../../hooks'; +import { useChains, useGasSufficiency } from '../../hooks'; import { SwapFormKeyHelper } from '../../providers/SwapFormProvider'; import { useWallet } from '../../providers/WalletProvider'; import { useWidgetConfig } from '../../providers/WidgetProvider'; @@ -21,8 +21,7 @@ export const SwapButton: React.FC = ({ const config = useWidgetConfig(); const { account, switchChain, connect: walletConnect } = useWallet(); - const { hasGasOnStartChain, hasGasOnCrossChain, hasSufficientBalance } = - useHasSufficientBalance(); + const { insufficientFunds, insufficientGas } = useGasSufficiency(); const [chainId] = useWatch({ name: [SwapFormKeyHelper.getChainKey('from')], @@ -64,10 +63,7 @@ export const SwapButton: React.FC = ({ onClick={handleSwapButtonClick} // loading={isLoading || isFetching} disabled={ - (!hasSufficientBalance || - !hasGasOnStartChain || - !hasGasOnCrossChain || - loading) && + (insufficientFunds || !!insufficientGas.length || loading) && isCurrentChainMatch } > diff --git a/packages/widget/src/hooks/index.ts b/packages/widget/src/hooks/index.ts index 606935f3a..0af4a9961 100644 --- a/packages/widget/src/hooks/index.ts +++ b/packages/widget/src/hooks/index.ts @@ -2,7 +2,7 @@ export * from './useChain'; export * from './useChains'; export * from './useContentHeight'; export * from './useDebouncedWatch'; -export * from './useHasSufficientBalance'; +export * from './useGasSufficiency'; export * from './useRouteExecution'; export * from './useScrollableContainer'; export * from './useSwapRoutes'; diff --git a/packages/widget/src/hooks/useGasSufficiency.ts b/packages/widget/src/hooks/useGasSufficiency.ts new file mode 100644 index 000000000..87b4998d2 --- /dev/null +++ b/packages/widget/src/hooks/useGasSufficiency.ts @@ -0,0 +1,135 @@ +import { EVMChain, Token } from '@lifi/sdk'; +import Big from 'big.js'; +import { useMemo } from 'react'; +import { useWatch } from 'react-hook-form'; +import { useChains, useDebouncedWatch } from '.'; +import { SwapFormKey, SwapFormKeyHelper } from '../providers/SwapFormProvider'; +import { useWallet } from '../providers/WalletProvider'; +import { useCurrentRoute } from '../stores'; +import { useTokenBalances } from './useTokenBalances'; + +interface GasSufficiency { + gasAmount: Big; + tokenAmount?: Big; + insufficientAmount?: Big; + insufficient?: boolean; + token: Token; + chain?: EVMChain; +} + +export const useGasSufficiency = () => { + const { account } = useWallet(); + const [route] = useCurrentRoute(); + const [fromChainId, toChainId, fromToken]: [number, number, string] = + useWatch({ + name: [ + SwapFormKeyHelper.getChainKey('from'), + SwapFormKeyHelper.getChainKey('to'), + SwapFormKey.FromToken, + ], + }); + const fromAmount = useDebouncedWatch(SwapFormKey.FromAmount, 250); + const { tokens: fromChainTokenBalances } = useTokenBalances(fromChainId); + const { tokens: toChainTokenBalances } = useTokenBalances(toChainId); + const { getChainById } = useChains(); + + const insufficientGas = useMemo(() => { + if (!account.isActive || !route || !fromAmount) { + return []; + } + + const tokenBalancesByChain = { + [fromChainId]: fromChainTokenBalances, + [toChainId]: toChainTokenBalances, + }; + + const gasCosts = route.steps.reduce((groupedGasCosts, step) => { + if (step.estimate.gasCosts) { + const { token } = step.estimate.gasCosts[0]; + const gasCostAmount = step.estimate.gasCosts + .reduce( + (amount, gasCost) => amount.plus(Big(gasCost.amount || 0)), + Big(0), + ) + .div(10 ** token.decimals); + const groupedGasCost = groupedGasCosts[token.chainId]; + const gasAmount = groupedGasCost + ? groupedGasCost.gasAmount.plus(gasCostAmount) + : gasCostAmount; + groupedGasCosts[token.chainId] = { + gasAmount, + tokenAmount: gasAmount, + token, + chain: getChainById(token.chainId), + }; + return groupedGasCosts; + } + return groupedGasCosts; + }, {} as Record); + + if ( + gasCosts[fromChainId] && + route.fromToken.address === gasCosts[fromChainId].token.address + ) { + gasCosts[fromChainId].tokenAmount = gasCosts[fromChainId]?.gasAmount.plus( + Big(fromAmount), + ); + } + + [fromChainId, toChainId].forEach((chainId) => { + if (gasCosts[chainId]) { + const gasTokenBalance = Big( + tokenBalancesByChain[chainId]?.find( + (t) => t.address === gasCosts[chainId].token.address, + )?.amount ?? 0, + ); + + const insufficientFromChainGas = + gasTokenBalance.lte(0) || + gasTokenBalance.lt(gasCosts[chainId].gasAmount ?? Big(0)) || + gasTokenBalance.lt(gasCosts[chainId].tokenAmount ?? Big(0)); + + const insufficientFromChainGasAmount = insufficientFromChainGas + ? gasCosts[chainId].tokenAmount?.minus(gasTokenBalance) ?? + gasCosts[chainId].gasAmount.minus(gasTokenBalance) + : undefined; + + gasCosts[chainId] = { + ...gasCosts[chainId], + insufficient: insufficientFromChainGas, + insufficientAmount: insufficientFromChainGasAmount, + }; + } + }); + + const gasCostResult = Object.values(gasCosts).filter( + (gasCost) => gasCost.insufficient, + ); + + return gasCostResult; + }, [ + account.isActive, + fromAmount, + fromChainId, + fromChainTokenBalances, + getChainById, + route, + toChainId, + toChainTokenBalances, + ]); + + const insufficientFunds = useMemo(() => { + if (!account.isActive || !fromToken || !fromAmount) { + return true; + } + const balance = Big( + fromChainTokenBalances?.find((t) => t.address === fromToken)?.amount ?? 0, + ); + return Big(fromAmount).lte(balance); + }, [account.isActive, fromAmount, fromChainTokenBalances, fromToken]); + + return { + insufficientGas, + insufficientFunds, + }; +}; diff --git a/packages/widget/src/hooks/useHasSufficientBalance.ts b/packages/widget/src/hooks/useHasSufficientBalance.ts deleted file mode 100644 index 9893024c9..000000000 --- a/packages/widget/src/hooks/useHasSufficientBalance.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { isSwapStep } from '@lifi/sdk'; -import Big from 'big.js'; -import { useMemo } from 'react'; -import { useWatch } from 'react-hook-form'; -import { useDebouncedWatch } from '.'; -import { SwapFormKey, SwapFormKeyHelper } from '../providers/SwapFormProvider'; -import { useWallet } from '../providers/WalletProvider'; -import { useCurrentRoute } from '../stores'; -import { useTokenBalances } from './useTokenBalances'; - -export const useHasSufficientBalance = () => { - const { account } = useWallet(); - const [route] = useCurrentRoute(); - const [fromChainId, toChainId, fromToken] = useWatch({ - name: [ - SwapFormKeyHelper.getChainKey('from'), - SwapFormKeyHelper.getChainKey('to'), - SwapFormKey.FromToken, - ], - }); - const fromAmount = useDebouncedWatch(SwapFormKey.FromAmount, 250); - const lastStep = route?.steps.at(-1); - const { tokens: fromChainTokenBalances } = useTokenBalances(fromChainId); - const { tokens: toChainTokenBalances } = useTokenBalances( - lastStep?.action.fromChainId ?? toChainId, - ); - - const hasGasOnStartChain = useMemo(() => { - const gasToken = route?.steps[0].estimate.gasCosts?.[0].token; - if (!account.isActive || !gasToken || !fromAmount) { - return true; - } - const gasTokenBalance = Big( - fromChainTokenBalances?.find((t) => t.address === gasToken.address) - ?.amount ?? 0, - ); - - let requiredAmount = route.steps - .filter((step) => step.action.fromChainId === route.fromChainId) - .reduce( - (big, step) => big.plus(Big(step.estimate.gasCosts?.[0].amount || 0)), - Big(0), - ) - .div(10 ** gasToken.decimals); - if (route.fromToken.address === gasToken.address) { - requiredAmount = requiredAmount.plus(Big(fromAmount)); - } - return gasTokenBalance.gt(0) && gasTokenBalance.gte(requiredAmount); - }, [ - account.isActive, - fromAmount, - fromChainTokenBalances, - route?.fromChainId, - route?.fromToken.address, - route?.steps, - ]); - - const hasGasOnCrossChain = useMemo(() => { - const gasToken = lastStep?.estimate.gasCosts?.[0].token; - if (!account.isActive || !gasToken || !isSwapStep(lastStep)) { - return true; - } - const balance = Big( - toChainTokenBalances?.find((t) => t.address === gasToken.address) - ?.amount ?? 0, - ); - const gasEstimate = lastStep.estimate.gasCosts?.[0].amount; - const requiredAmount = Big(gasEstimate || 0).div( - 10 ** (lastStep.estimate.gasCosts?.[0].token.decimals ?? 0), - ); - return balance.gt(0) && balance.gte(requiredAmount); - }, [account.isActive, lastStep, toChainTokenBalances]); - - const hasSufficientBalance = useMemo(() => { - if (!account.isActive || !fromToken || !fromAmount) { - return true; - } - const balance = Big( - fromChainTokenBalances?.find((t) => t.address === fromToken)?.amount ?? 0, - ); - return Big(fromAmount).lte(balance); - }, [account.isActive, fromAmount, fromChainTokenBalances, fromToken]); - - return { - hasGasOnStartChain, - hasGasOnCrossChain, - hasSufficientBalance, - }; -}; diff --git a/packages/widget/src/i18n/en/translation.json b/packages/widget/src/i18n/en/translation.json index 9d230c49a..a7a1e348d 100644 --- a/packages/widget/src/i18n/en/translation.json +++ b/packages/widget/src/i18n/en/translation.json @@ -53,6 +53,7 @@ "networkIsBusy": "Network is busy...", "crossStepDetails": "Bridge from {{from}} to {{to}} via {{tool}}", "swapStepDetails": "Swap on {{chain}} via {{tool}}", + "gasAmount": "{{amount}} {{tokenSymbol}} on {{chainName}}", "tags": { "recommended": "Recommended", "fastest": "Fast", @@ -73,7 +74,7 @@ "routeNotFound": "No routes available" }, "message": { - "routeNotFound": "Try another \"from\" and \"to\" token combination." + "routeNotFound": "Try another token combination." } }, "warning": { @@ -81,9 +82,8 @@ "insufficientGas": "Insufficient gas" }, "message": { - "insufficientFunds": "You don't have enough funds for this transaction on the start \"from\" chain.", - "insufficientGasOnStartChain": "You need to have enough gas to pay for this transaction on the start \"from\" chain.", - "insufficientGasOnDestinationChain": "You need to have enough gas to pay for this transaction on the destination \"to\" chain." + "insufficientFunds": "You don't have enough funds to execute the swap.", + "insufficientGas": "To execute the swap, you need to top up your wallet with at least:" } }, "error": { diff --git a/packages/widget/src/pages/MainPage/MainPage.tsx b/packages/widget/src/pages/MainPage/MainPage.tsx index a54c43c2a..02b7c5f92 100644 --- a/packages/widget/src/pages/MainPage/MainPage.tsx +++ b/packages/widget/src/pages/MainPage/MainPage.tsx @@ -1,5 +1,5 @@ import { Box } from '@mui/material'; -import { InsufficientGasOrFundsMessage } from '../../components/InsufficientGasOrFundsMessage'; +import { GasSufficiencyMessage } from '../../components/GasSufficiencyMessage'; import { SelectChainAndToken } from '../../components/SelectChainAndToken'; import { SwapInProgress } from '../../components/SwapInProgress'; import { SwapInput } from '../../components/SwapInput'; @@ -16,7 +16,7 @@ export const MainPage: React.FC = () => { - + diff --git a/packages/widget/src/pages/SwapPage/SwapPage.tsx b/packages/widget/src/pages/SwapPage/SwapPage.tsx index 0e65f361c..6cc0c88a8 100644 --- a/packages/widget/src/pages/SwapPage/SwapPage.tsx +++ b/packages/widget/src/pages/SwapPage/SwapPage.tsx @@ -2,7 +2,7 @@ import { Box } from '@mui/material'; import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import { InsufficientGasOrFundsMessage } from '../../components/InsufficientGasOrFundsMessage'; +import { GasSufficiencyMessage } from '../../components/GasSufficiencyMessage'; import { SwapButton } from '../../components/SwapButton'; import { useRouteExecution } from '../../hooks'; import { StatusBottomSheet } from './StatusBottomSheet'; @@ -66,7 +66,7 @@ export const SwapPage: React.FC = () => { ) : null} ))} - {status === 'idle' ? : null} + {status === 'idle' ? : null} {status === 'idle' || status === 'error' ? (