diff --git a/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOption.tsx b/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOption.tsx index f80f31168c..5b7ed3bf30 100644 --- a/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOption.tsx +++ b/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOption.tsx @@ -28,6 +28,7 @@ export interface RouteOptionProps< size?: MenuItemSize; rc?: RC; selected?: boolean; + displayPriceDetails?: boolean; } export function RouteOption({ @@ -39,6 +40,7 @@ export function RouteOption({ size = 'small', rc = , selected = false, + displayPriceDetails = true, }: RouteOptionProps) { const { t } = useTranslation(); @@ -97,7 +99,7 @@ export function RouteOption({ {`${t('views.ADD_TOKENS.fees.balance')} ${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${routeBalanceUsd}`} - {routeData.isInsufficientGas && ( + { displayPriceDetails && routeData.isInsufficientGas && ( <>
@@ -108,9 +110,18 @@ export function RouteOption({ )} + + { displayPriceDetails && routeData.isInsufficientBalance && ( + <> +
+ + {t('views.ADD_TOKENS.noBalanceRouteMessage')} + + + )}
- + {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${fromAmountUsd}`} @@ -155,7 +166,7 @@ export function RouteOption({ /> )} { - `${t('views.ADD_TOKENS.fees.fee')} ${t('views.ADD_TOKENS.fees.fiatPricePrefix')} + `${t('views.ADD_TOKENS.fees.fee')} ${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${getFormattedAmounts(totalFeesUsd)}` } diff --git a/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOptions.tsx b/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOptions.tsx index 517cf4e230..35214566ff 100644 --- a/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOptions.tsx +++ b/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOptions.tsx @@ -26,6 +26,7 @@ export interface OptionsProps { showOnrampOption?: boolean; insufficientBalance?: boolean; selectedIndex: number; + displayPriceDetails?: boolean; } export function RouteOptions({ @@ -38,6 +39,7 @@ export function RouteOptions({ showOnrampOption, insufficientBalance, selectedIndex, + displayPriceDetails, }: OptionsProps) { const { t } = useTranslation(); @@ -94,6 +96,7 @@ export function RouteOptions({ isFastest={index === 0} selected={index === selectedIndex} rc={} + displayPriceDetails={displayPriceDetails} /> ))} {noRoutes && ( diff --git a/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOptionsDrawer.tsx b/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOptionsDrawer.tsx index 9dca7db538..3b209b05b7 100644 --- a/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOptionsDrawer.tsx +++ b/packages/checkout/widgets-lib/src/components/RouteOptionsDrawer/RouteOptionsDrawer.tsx @@ -25,6 +25,7 @@ type OptionsDrawerProps = { showSwapOption?: boolean; showBridgeOption?: boolean; insufficientBalance?: boolean; + displayPriceDetails?: boolean; }; export function RouteOptionsDrawer({ @@ -40,6 +41,7 @@ export function RouteOptionsDrawer({ // eslint-disable-next-line @typescript-eslint/no-unused-vars showBridgeOption, insufficientBalance, + displayPriceDetails, }: OptionsDrawerProps) { const { t } = useTranslation(); const { track } = useAnalytics(); @@ -126,6 +128,7 @@ export function RouteOptionsDrawer({ showOnrampOption={showOnrampOption} insufficientBalance={insufficientBalance} selectedIndex={selectedRouteIndex.current} + displayPriceDetails={displayPriceDetails} /> diff --git a/packages/checkout/widgets-lib/src/components/SelectedRouteOption/SelectedRouteOption.tsx b/packages/checkout/widgets-lib/src/components/SelectedRouteOption/SelectedRouteOption.tsx index fcbfa38593..df4ac65c89 100644 --- a/packages/checkout/widgets-lib/src/components/SelectedRouteOption/SelectedRouteOption.tsx +++ b/packages/checkout/widgets-lib/src/components/SelectedRouteOption/SelectedRouteOption.tsx @@ -1,6 +1,8 @@ import { AllDualVariantIconKeys, + Box, MenuItem, + ShimmerBox, Stack, Sticker, Tooltip, @@ -28,6 +30,7 @@ export interface SelectedRouteOptionProps { withSelectedWallet?: boolean; insufficientBalance?: boolean; showOnrampOption?: boolean; + displayPriceDetails?: boolean; } function SelectedRouteOptionContainer({ @@ -67,6 +70,7 @@ export function SelectedRouteOption({ withSelectedWallet = false, insufficientBalance = false, showOnrampOption = false, + displayPriceDetails = true, onClick, }: SelectedRouteOptionProps) { const { t } = useTranslation(); @@ -182,24 +186,58 @@ export function SelectedRouteOption({ {`${t('views.ADD_TOKENS.fees.balance')} ${t( 'views.ADD_TOKENS.fees.fiatPricePrefix', )} $${routeBalanceUsd}`} - {routeData?.isInsufficientGas && ( + { displayPriceDetails && routeData?.isInsufficientGas && ( + <> +
+ + {t('views.ADD_TOKENS.noGasRouteMessage', { + token: + routeData.route.route.estimate.gasCosts[0].token.symbol, + })} + + + )} + + { displayPriceDetails && routeData?.isInsufficientBalance && ( <>
- {t('views.ADD_TOKENS.noGasRouteMessage', { - token: - routeData.route.route.estimate.gasCosts[0].token.symbol, - })} + {t('views.ADD_TOKENS.noBalanceRouteMessage')} )} - - - {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${fromAmountUsd}`} - - + {loading && displayPriceDetails ? ( + + } + sx={{ + w: '36px', + h: '16px', + }} + /> + } + sx={{ + w: '72px', + h: '16px', + }} + /> + + ) : ( + + + {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${fromAmountUsd}`} + + + )} ); diff --git a/packages/checkout/widgets-lib/src/lib/squid/config.ts b/packages/checkout/widgets-lib/src/lib/squid/config.ts index 75339de528..f318478916 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/config.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/config.ts @@ -1,3 +1,9 @@ +import { ChainId } from '@imtbl/checkout-sdk'; + export const SQUID_SDK_BASE_URL = 'https://apiplus.squidrouter.com'; export const SQUID_NATIVE_TOKEN = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + +export const DEFAULT_DESTINATION_TOKEN = { + [ChainId.IMTBL_ZKEVM_MAINNET]: '0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2', +}; diff --git a/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts b/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts index e43980a8af..020aeb7d2d 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts @@ -4,15 +4,25 @@ export const sortRoutesByFastestTime = (routes: RouteData[]): RouteData[] => { if (!routes) return []; return routes.slice().sort((a, b) => { - // Prioritize isInsufficientGas = false - if (a.isInsufficientGas !== b.isInsufficientGas) { - return a.isInsufficientGas ? 1 : -1; + // 1.: Prioritise routes where both isInsufficientGas and isInsufficientBalance are false + if (a.isInsufficientGas !== b.isInsufficientGas || a.isInsufficientBalance !== b.isInsufficientBalance) { + if (!a.isInsufficientGas && !a.isInsufficientBalance) return -1; + if (!b.isInsufficientGas && !b.isInsufficientBalance) return 1; + if (a.isInsufficientGas && !a.isInsufficientBalance) return -1; // isInsufficientGas = true has higher priority + if (b.isInsufficientGas && !b.isInsufficientBalance) return 1; } - // Sort by estimatedRouteDuration if isInsufficientGas is the same + // 2.: Sort by estimatedRouteDuration const timeA = a.route.route.estimate.estimatedRouteDuration; const timeB = b.route.route.estimate.estimatedRouteDuration; - return timeA - timeB; + if (timeA !== timeB) return timeA - timeB; + + // 3.: Place isInsufficientGas before isInsufficientBalance + if (a.isInsufficientGas !== b.isInsufficientGas) { + return a.isInsufficientGas ? -1 : 1; + } + + return 0; }); }; diff --git a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts index 01f0c9aa4d..b177435010 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts @@ -16,6 +16,8 @@ import { import { SquidPostHook } from '../../primary-sales'; import { SQUID_NATIVE_TOKEN } from '../config'; +const MIN_BALANCE_FOR_ROUTES = 1; + const BASE_SLIPPAGE_HIGH_TIER = 0.005; const BASE_SLIPPAGE_MEDIUM_TIER = 0.01; const BASE_SLIPPAGE_LOW_TIER = 0.015; @@ -77,7 +79,7 @@ export const useRoutes = () => { chainId: string, ): Token | undefined => tokens.find( (value) => value.address.toLowerCase() === address.toLowerCase() - && value.chainId === chainId, + && value.chainId === chainId, ); const calculateFromAmount = ( @@ -139,10 +141,12 @@ export const useRoutes = () => { toAmount, balance, additionalBuffer, + isInsufficientBalance: false, + isInsufficientGas: false, }; }; - const getSufficientFromAmounts = ( + const getFromAmounts = ( tokens: Token[], balances: TokenBalance[], toChainId: string, @@ -152,24 +156,25 @@ export const useRoutes = () => { const filteredBalances = balances.filter( (balance) => !( balance.address.toLowerCase() === toTokenAddress.toLowerCase() - && balance.chainId === toChainId + && balance.chainId === toChainId ), ); - const amountDataArray: AmountData[] = filteredBalances - .map((balance) => getAmountData(tokens, balance, toAmount, toChainId, toTokenAddress)) - .filter((value) => value !== undefined); + return filteredBalances + .map((balance) => { + const amountData = getAmountData(tokens, balance, toAmount, toChainId, toTokenAddress); + if (!amountData) return null; - return amountDataArray.filter((data: AmountData) => { - const formattedBalance = utils.formatUnits( - data.balance.balance, - data.balance.decimals, - ); + const formattedBalance = parseFloat( + utils.formatUnits(balance.balance, balance.decimals), + ); - return ( - parseFloat(formattedBalance.toString()) > parseFloat(data.fromAmount) - ); - }); + return { + ...amountData, + isInsufficientBalance: formattedBalance < parseFloat(amountData.fromAmount), + }; + }) + .filter((data) => data !== null); }; const convertToFormattedAmount = (amount: string, decimals: number) => { @@ -403,16 +408,20 @@ export const useRoutes = () => { const feeCost = getTotalFees(routeResponse, data.balance.chainId); const userGasBalance = findUserGasBalance(data.balance.chainId); - return { - amountData: data, - route: routeResponse.route, - isInsufficientGas: !hasSufficientNativeTokenBalance( + const isInsufficientGas = !data.isInsufficientBalance + && !hasSufficientNativeTokenBalance( userGasBalance, data.fromAmount, data.fromToken, gasCost, feeCost, - ), + ); + + return { + amountData: data, + route: routeResponse.route, + isInsufficientGas, + isInsufficientBalance: data.isInsufficientBalance, } as RouteData; } catch (error) { return null; @@ -439,7 +448,7 @@ export const useRoutes = () => { ): Promise => { const currentRequestId = ++latestRequestIdRef.current; - let fromAmountDataArray = getSufficientFromAmounts( + let fromAmountDataArray = getFromAmounts( tokens, balances, toChanId, @@ -492,10 +501,96 @@ export const useRoutes = () => { return sortedRoutes; }; + const fetchRoutesForBalancesWithRateLimit = async ( + squid: Squid, + tokens: Token[], + balances: TokenBalance[], + toChainId: string, + toTokenAddress: string, + bulkNumber = 5, + delayMs = 1000, + isSwapAllowed = true, + ): Promise => { + const currentRequestId = ++latestRequestIdRef.current; + + let fromAmountDataArray = balances + .filter((balance) => !( + balance.address.toLowerCase() === toTokenAddress.toLowerCase() + && balance.chainId.toString() === toChainId + )) + .map((balance) => { + const fromToken = findToken(tokens, balance.address, balance.chainId.toString()); + const toToken = findToken(tokens, toTokenAddress, toChainId); + + if (!fromToken || !toToken) return undefined; + + const fromAmount = utils.formatUnits(balance.balance, balance.decimals); + // Skip tokens with total USD value less than $1 + const balanceUsdValue = parseFloat(fromAmount) * fromToken.usdPrice; + if (balanceUsdValue < MIN_BALANCE_FOR_ROUTES) return undefined; + + return { + fromToken, + fromAmount, + toToken, + toAmount: '0', // This will be determined by the route + balance, + additionalBuffer: 0, + } as AmountData; + }) + .filter((data): data is AmountData => data !== undefined); + + if (!isSwapAllowed) { + fromAmountDataArray = fromAmountDataArray.filter( + (amountData) => amountData.balance.chainId !== toChainId, + ); + } + + let allRoutes: RouteData[] = []; + await Promise.all( + fromAmountDataArray + .reduce((acc, _, i) => { + if (i % bulkNumber === 0) { + acc.push(fromAmountDataArray.slice(i, i + bulkNumber)); + } + return acc; + }, [] as (typeof fromAmountDataArray)[]) + .map(async (slicedFromAmountDataArray) => { + allRoutes.push( + ...(await getRoutesWithFeesValidation( + squid, + toTokenAddress, + balances, + slicedFromAmountDataArray, + )), + ); + await delay(delayMs); + }), + ); + + if (!isSwapAllowed) { + allRoutes = allRoutes.filter( + (routeData) => !routeData.route.route.estimate.actions.find( + (action) => action.type === ActionType.SWAP, + ), + ); + } + + const sortedRoutes = sortRoutesByFastestTime(allRoutes); + // Only update routes if the request is the latest one + if (currentRequestId === latestRequestIdRef.current) { + setRoutes(sortedRoutes); + } + + return sortedRoutes; + }; + return { fetchRoutesWithRateLimit, + fetchRoutesForBalancesWithRateLimit, getAmountData, getRoute, resetRoutes, + getSlippageTier, }; }; diff --git a/packages/checkout/widgets-lib/src/lib/squid/types.ts b/packages/checkout/widgets-lib/src/lib/squid/types.ts index dd1ae23150..210f2b4947 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/types.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/types.ts @@ -33,12 +33,15 @@ export type AmountData = { toAmount: string; balance: TokenBalance; additionalBuffer: number; + isInsufficientGas: boolean; + isInsufficientBalance: boolean; }; export type RouteData = { amountData: AmountData; route: RouteResponse; isInsufficientGas: boolean; + isInsufficientBalance: boolean; }; export type RouteResponseData = { diff --git a/packages/checkout/widgets-lib/src/locales/en.json b/packages/checkout/widgets-lib/src/locales/en.json index 81fbe2c213..58161c542a 100644 --- a/packages/checkout/widgets-lib/src/locales/en.json +++ b/packages/checkout/widgets-lib/src/locales/en.json @@ -655,7 +655,8 @@ "buttonText": "Choose the token you want", "tokenLabel": "Add", "drawerHeading": "Add Token", - "searchPlaceholder": "Search tokens" + "searchPlaceholder": "Search tokens", + "maxAmountButton": "MAX" }, "walletSelection": { "from":{ @@ -873,7 +874,8 @@ "primaryAction": "Try another option", "secondaryAction": "Add more {{token}}" }, - "noGasRouteMessage": "Insufficient {{token}} for gas" + "noGasRouteMessage": "Insufficient {{token}} for gas", + "noBalanceRouteMessage": "Insufficient balance" } }, "footers": { diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json index 37c7a48450..ecc1804594 100644 --- a/packages/checkout/widgets-lib/src/locales/ja.json +++ b/packages/checkout/widgets-lib/src/locales/ja.json @@ -364,7 +364,7 @@ "ready3": "資産を準備中", "processing1": "支払いを初期化中", "processing2": "資産を確保中", - "processing3": "支払いを完了中" + "processing3": "支払いを完���中" }, "handover": { "initial": "注文を準備中", @@ -638,7 +638,8 @@ "buttonText": "選択したいトークンを選んでください", "tokenLabel": "追加", "drawerHeading": "トークンを追加", - "searchPlaceholder": "トークンを検索" + "searchPlaceholder": "トークンを検索", + "maxAmountButton": "最大" }, "walletSelection": { "from": { @@ -856,7 +857,8 @@ "primaryAction": "別のオプションを試す", "secondaryAction": "{{token}} をさらに追加する" }, - "noGasRouteMessage": "ガス用の {{token}} が不足しています" + "noGasRouteMessage": "ガス用の {{token}} が不足しています", + "noBalanceRouteMessage": "残高不足" } }, "footers": { @@ -976,7 +978,7 @@ "networkSwitch": { "heading": "あなたの{{wallet}}でネットワークを切り替える", "manualSwitch": { - "body": "続行するにはモバイルウォレットを開き、{{chain}}ネットワークに切り替える必要があります" + "body": "続行するにはモバイルウォレットを開き、{{chain}}ネットワ��クに切り替える必要があります" }, "controlledSwitch": { "body": "{{chain}}ネットワークに切り替える必要があります" diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json index 6f65394d6a..0dbb3ce405 100644 --- a/packages/checkout/widgets-lib/src/locales/ko.json +++ b/packages/checkout/widgets-lib/src/locales/ko.json @@ -635,7 +635,8 @@ "buttonText": "원하는 토큰을 선택하세요", "tokenLabel": "추가", "drawerHeading": "토큰 추가", - "searchPlaceholder": "토큰 검색" + "searchPlaceholder": "토큰 검색", + "maxAmountButton": "최대" }, "walletSelection": { "from": { @@ -853,7 +854,8 @@ "primaryAction": "다른 옵션 시도하기", "secondaryAction": "{{token}} 더 추가하기" }, - "noGasRouteMessage": "가스에 필요한 {{token}}이(가) 부족합니다" + "noGasRouteMessage": "가스에 필요한 {{token}}이(가) 부족합니다", + "noBalanceRouteMessage": "잔액 부족" } }, "footers": { diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json index 775b9b6bb5..0de1ca29be 100644 --- a/packages/checkout/widgets-lib/src/locales/zh.json +++ b/packages/checkout/widgets-lib/src/locales/zh.json @@ -635,7 +635,8 @@ "buttonText": "选择您想要的代币", "tokenLabel": "添加", "drawerHeading": "添加代币", - "searchPlaceholder": "搜索代币" + "searchPlaceholder": "搜索代币", + "maxAmountButton": "最大" }, "walletSelection": { "from": { @@ -853,7 +854,8 @@ "primaryAction": "尝试其他选项", "secondaryAction": "添加更多 {{token}}" }, - "noGasRouteMessage": "用于燃料费的 {{token}} 不足" + "noGasRouteMessage": "用于燃料费的 {{token}} 不足", + "noBalanceRouteMessage": "余额不足" } }, "footers": { diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx index fd6e3313a9..94ca3a3614 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx @@ -30,6 +30,7 @@ import { Web3Provider } from '@ethersproject/providers'; import { useTranslation } from 'react-i18next'; import { ActionType } from '@0xsquid/squid-types'; import { trackFlow } from '@imtbl/metrics'; +import { BigNumber, utils } from 'ethers'; import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; import { @@ -65,7 +66,7 @@ import { SquidFooter } from '../../../lib/squid/components/SquidFooter'; import { TokenDrawerMenu } from '../components/TokenDrawerMenu'; import { PULSE_SHADOW } from '../utils/animation'; import { RouteData } from '../../../lib/squid/types'; -import { SQUID_NATIVE_TOKEN } from '../../../lib/squid/config'; +import { DEFAULT_DESTINATION_TOKEN, SQUID_NATIVE_TOKEN } from '../../../lib/squid/config'; import { identifyUser } from '../../../lib/analytics/identifyUser'; import { NotEnoughGasDrawer } from '../../../components/NotEnoughGasDrawer/NotEnoughGasDrawer'; import { TOOLKIT_SQUID_URL } from '../utils/config'; @@ -102,7 +103,9 @@ export function AddTokens({ }: AddTokensProps) { const inputRef = useRef(null); - const { fetchRoutesWithRateLimit, resetRoutes } = useRoutes(); + const { + fetchRoutesWithRateLimit, fetchRoutesForBalancesWithRateLimit, resetRoutes, getSlippageTier, + } = useRoutes(); const { showErrorHandover } = useError(config.environment); const { @@ -148,8 +151,8 @@ export function AddTokens({ ); const [fetchingRoutes, setFetchingRoutes] = useState(false); const [insufficientBalance, setInsufficientBalance] = useState(false); - const [isAmountInputSynced, setIsAmountInputSynced] = useState(false); const [showNotEnoughGasDrawer, setShowNotEnoughGasDrawer] = useState(false); + const [showMaxAmount, setShowMaxAmount] = useState(false); const debouncedSetSelectedAmount = useRef( debounce((value: string) => { @@ -168,16 +171,9 @@ export function AddTokens({ ); const setSelectedAmount = (value: string) => { - setIsAmountInputSynced(false); debouncedSetSelectedAmount.current(value); }; - useEffect(() => { - if (selectedAmount === inputValue) { - setIsAmountInputSynced(true); - } - }, [selectedAmount, inputValue]); - const setSelectedRouteData = (route: RouteData | undefined) => { if (route) { track({ @@ -201,6 +197,7 @@ export function AddTokens({ (action) => action.type === ActionType.SWAP, ), isInsufficientGas: route.isInsufficientGas, + isInsufficientBalance: route.isInsufficientBalance, }, }); } @@ -317,7 +314,6 @@ export function AddTokens({ useEffect(() => { resetRoutes(); setInsufficientBalance(false); - setSelectedRouteData(undefined); (async () => { const isValidAmount = validateToAmount(selectedAmount).isValid; @@ -329,6 +325,7 @@ export function AddTokens({ && isValidAmount ) { setFetchingRoutes(true); + const availableRoutes = await fetchRoutesWithRateLimit( squid, tokens, @@ -365,10 +362,75 @@ export function AddTokens({ }, [balances, squid, selectedToken, selectedAmount]); useEffect(() => { - if (!selectedRouteData && routes.length > 0) { - setSelectedRouteData(routes[0]); + resetRoutes(); + setInsufficientBalance(false); + + if (!squid || !balances || !tokens || selectedAmount) return; + + if (balances.length === 0) return; + + const hasSelectedToken = selectedToken?.address; + const toToken = selectedToken?.address ?? DEFAULT_DESTINATION_TOKEN[ChainId.IMTBL_ZKEVM_MAINNET]; + + const preloadRoutes = async () => { + if ( + balances + && squid + && tokens + && toToken + ) { + setFetchingRoutes(true); + + const availableRoutes = await fetchRoutesForBalancesWithRateLimit( + squid, + tokens, + balances, + ChainId.IMTBL_ZKEVM_MAINNET.toString(), + toToken === 'native' + ? SQUID_NATIVE_TOKEN + : toToken, + 5, + 1000, + isSwapAvailable, + ); + setFetchingRoutes(false); + + if (availableRoutes.length === 0) { + setInsufficientBalance(true); + } + + if (!hasSelectedToken && availableRoutes.length > 1) { + setShowOptionsDrawer(true); + } + } + }; + + console.log('Preloading routes'); + preloadRoutes(); + }, [balances, squid, selectedToken, selectedAmount]); + + const matchingRoute = useMemo(() => { + if (!selectedRouteData || routes.length === 0) return undefined; + + return routes.find( + (route) => route.amountData.fromToken.address === selectedRouteData.amountData.fromToken.address + && route.amountData.fromToken.chainId === selectedRouteData.amountData.fromToken.chainId, + ); + }, [routes, selectedRouteData]); + + useEffect(() => { + if (routes.length === 0) return; + + let routeToSelect = routes[0]; + + if (selectedRouteData) { + if (matchingRoute) { + routeToSelect = matchingRoute; + } } - }, [routes]); + + setSelectedRouteData(routeToSelect); + }, [routes, matchingRoute]); useEffect(() => { if (!checkout) return; @@ -530,6 +592,11 @@ export function AddTokens({ const showInitialEmptyState = !selectedToken; + const shouldDisplayRoutesBasedOnBalances = useMemo( + () => (!!(selectedToken && fromAddress && validateToAmount(selectedAmount).isValid)), + [selectedToken, fromAddress, selectedAmount], + ); + useEffect(() => { if (inputRef.current && !showInitialEmptyState) { inputRef.current.focus(); @@ -538,11 +605,7 @@ export function AddTokens({ const shouldShowBackButton = showBackButton && onBackButtonClick; - const routeInputsReady = !!selectedToken - && !!fromAddress - && validateToAmount(selectedAmount).isValid - && validateToAmount(inputValue).isValid - && isAmountInputSynced; + const routeInputsReady = Boolean(fromAddress); const loading = (routeInputsReady || fetchingRoutes) && !(selectedRouteData || insufficientBalance); @@ -551,6 +614,7 @@ export function AddTokens({ && !!toAddress && !!selectedRouteData && !selectedRouteData.isInsufficientGas + && !selectedRouteData.isInsufficientBalance && !loading; const handleWalletConnected = ( @@ -604,7 +668,7 @@ export function AddTokens({ }, [id, experiments]); useEffect(() => { - if (selectedRouteData?.isInsufficientGas) { + if (selectedRouteData?.isInsufficientGas && shouldDisplayRoutesBasedOnBalances) { setShowNotEnoughGasDrawer(true); } else { setShowNotEnoughGasDrawer(false); @@ -616,6 +680,32 @@ export function AddTokens({ window.open(TOOLKIT_SQUID_URL, '_blank'); }; + useEffect(() => { + if (!balances || !selectedToken) return; + if (balances.length > 0 && selectedToken && !fetchingRoutes) { + setShowMaxAmount(true); + } else { + setShowMaxAmount(false); + } + }, [balances, selectedToken, fetchingRoutes]); + + const handleMaxAmountClick = () => { + if (selectedRouteData?.route?.route?.estimate?.toAmount && selectedToken?.decimals) { + try { + const balance = BigNumber.from(selectedRouteData.amountData.balance.balance); + const formattedBalance = utils.formatUnits(balance, selectedRouteData.amountData.balance.decimals); + const balanceConverted = Number(formattedBalance) * Number(selectedRouteData.route.route.estimate.exchangeRate); + const slippageTier = getSlippageTier(0); + const toAmountLessBuffer = balanceConverted * (1 - slippageTier); + const roundedAmount = toAmountLessBuffer.toFixed(6); + setInputValue(roundedAmount); + setSelectedAmount(roundedAmount); + } catch (error) { + console.error('Error formatting max amount:', error); + } + } + }; + return ( - {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${getFormattedAmounts(selectedAmountUsd)}`} )} + {showMaxAmount && ( + + )} - {selectedToken && fromAddress && selectedAmount && isAmountInputSynced && ( + {fromAddress && ( <> )} -