diff --git a/packages/checkout/widgets-lib/src/components/Hero/NoGasHero.tsx b/packages/checkout/widgets-lib/src/components/Hero/NoGasHero.tsx
new file mode 100644
index 0000000000..8814e9774a
--- /dev/null
+++ b/packages/checkout/widgets-lib/src/components/Hero/NoGasHero.tsx
@@ -0,0 +1,35 @@
+/* eslint-disable max-len */
+import { Box } from '@biom3/react';
+
+export function NoGasHero() {
+ return (
+
+
+
+
+ );
+}
diff --git a/packages/checkout/widgets-lib/src/components/NotEnoughGasDrawer/NotEnoughGasDrawer.tsx b/packages/checkout/widgets-lib/src/components/NotEnoughGasDrawer/NotEnoughGasDrawer.tsx
new file mode 100644
index 0000000000..1e699aa193
--- /dev/null
+++ b/packages/checkout/widgets-lib/src/components/NotEnoughGasDrawer/NotEnoughGasDrawer.tsx
@@ -0,0 +1,97 @@
+import {
+ Body,
+ Box,
+ Button,
+ Drawer,
+ Heading,
+} from '@biom3/react';
+import { useTranslation } from 'react-i18next';
+import { RouteData } from '../../lib/squid/types';
+import { NoGasHero } from '../Hero/NoGasHero';
+
+export function NotEnoughGasDrawer({
+ visible,
+ routeData,
+ onTryAgainClick,
+ onToolkitClick,
+}: {
+ visible: boolean;
+ routeData?: RouteData;
+ onTryAgainClick: () => void;
+ onToolkitClick: () => void;
+}) {
+ const { t } = useTranslation();
+ const tokenName = routeData?.route.route.estimate.gasCosts[0].token.symbol ?? '';
+
+ return (
+
+
+
+
+
+
+ {t('views.ADD_TOKENS.noGasDrawer.heading', { token: tokenName })}
+
+
+
+ {t('views.ADD_TOKENS.noGasDrawer.body')}
+
+
+
+
+
+
+
+
+
+ );
+}
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 2d425fc91e..e43980a8af 100644
--- a/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts
+++ b/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts
@@ -4,8 +4,15 @@ 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;
+ }
+
+ // Sort by estimatedRouteDuration if isInsufficientGas is the same
const timeA = a.route.route.estimate.estimatedRouteDuration;
const timeB = b.route.route.estimate.estimatedRouteDuration;
+
return timeA - timeB;
});
};
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 31ead28296..713c39ecb9 100644
--- a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts
+++ b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts
@@ -13,6 +13,7 @@ import { isPassportProvider } from '../../provider';
import {
AmountData, RouteData, RouteResponseData, Token,
} from '../types';
+import { SQUID_NATIVE_TOKEN } from '../config';
const BASE_SLIPPAGE = 0.02;
@@ -293,28 +294,97 @@ export const useRoutes = () => {
}
};
- const getRoutes = async (
+ const getRoutesWithFeesValidation = async (
squid: Squid,
- amountDataArray: AmountData[],
toTokenAddress: string,
+ balances: TokenBalance[],
+ fromAmountArray: AmountData[],
): Promise => {
- const routePromises = amountDataArray.map((data) => getRoute(
- squid,
- data.fromToken,
- data.toToken,
- toTokenAddress,
- data.fromAmount,
- data.toAmount,
- ).then((route) => ({
- amountData: { ...data, additionalBuffer: route.additionalBuffer },
- route: route.route,
- })));
+ const getGasCost = (
+ route: RouteResponseData,
+ chainId: string | number,
+ ) => (route.route?.route.estimate.gasCosts || [])
+ .filter((gasCost) => gasCost.token.chainId === chainId.toString())
+ .reduce(
+ (sum, gasCost) => sum + parseFloat(utils.formatUnits(gasCost.amount, gasCost.token.decimals)),
+ 0,
+ );
+
+ const getTotalFees = (
+ route: RouteResponseData,
+ chainId: string | number,
+ ) => (route.route?.route.estimate.feeCosts || [])
+ .filter((fee) => fee.token.chainId === chainId.toString())
+ .reduce(
+ (sum, fee) => sum + parseFloat(utils.formatUnits(fee.amount, fee.token.decimals)),
+ 0,
+ );
+
+ const findUserGasBalance = (chainId: string | number) => balances.find(
+ (balance: TokenBalance) => balance.address.toLowerCase() === SQUID_NATIVE_TOKEN.toLowerCase()
+ && balance.chainId.toString() === chainId.toString(),
+ );
+
+ const hasSufficientNativeTokenBalance = (
+ userGasBalance: TokenBalance | undefined,
+ fromAmount: string,
+ fromToken: Token,
+ totalGasCost: number,
+ totalFeeCost: number,
+ ) => {
+ if (!userGasBalance) return false;
+
+ const userBalance = parseFloat(
+ utils.formatUnits(userGasBalance.balance, userGasBalance.decimals),
+ );
+
+ // If the fromToken is the native token, validate balance for both fromAmount and gas + fee costs
+ // Otherwise, only validate balance for gas + fee costs
+ const requiredAmount = fromToken.address.toLowerCase() === SQUID_NATIVE_TOKEN.toLowerCase()
+ ? parseFloat(fromAmount) + totalGasCost + totalFeeCost
+ : totalGasCost + totalFeeCost;
+
+ return userBalance >= requiredAmount;
+ };
- const routesData = await Promise.all(routePromises);
+ const routePromises = fromAmountArray.map(async (data: AmountData) => {
+ try {
+ const routeResponse = await getRoute(
+ squid,
+ data.fromToken,
+ data.toToken,
+ toTokenAddress,
+ data.fromAmount,
+ data.toAmount,
+ );
+
+ if (!routeResponse?.route) return null;
+
+ const gasCost = getGasCost(routeResponse, data.balance.chainId);
+ const feeCost = getTotalFees(routeResponse, data.balance.chainId);
+ const userGasBalance = findUserGasBalance(data.balance.chainId);
+
+ return {
+ amountData: data,
+ route: routeResponse.route,
+ isInsufficientGas: !hasSufficientNativeTokenBalance(
+ userGasBalance,
+ data.fromAmount,
+ data.fromToken,
+ gasCost,
+ feeCost,
+ ),
+ } as RouteData;
+ } catch (error) {
+ return null;
+ }
+ });
- return routesData.filter(
- (route): route is RouteData => route?.route !== undefined,
+ const routesData = (await Promise.all(routePromises)).filter(
+ (route): route is RouteData => route !== null,
);
+
+ return routesData;
};
const fetchRoutesWithRateLimit = async (
@@ -330,7 +400,7 @@ export const useRoutes = () => {
): Promise => {
const currentRequestId = ++latestRequestIdRef.current;
- let amountDataArray = getSufficientFromAmounts(
+ let fromAmountDataArray = getSufficientFromAmounts(
tokens,
balances,
toChanId,
@@ -339,23 +409,28 @@ export const useRoutes = () => {
);
if (!isSwapAllowed) {
- amountDataArray = amountDataArray.filter(
+ fromAmountDataArray = fromAmountDataArray.filter(
(amountData) => amountData.balance.chainId !== toChanId,
);
}
let allRoutes: RouteData[] = [];
await Promise.all(
- amountDataArray
+ fromAmountDataArray
.reduce((acc, _, i) => {
if (i % bulkNumber === 0) {
- acc.push(amountDataArray.slice(i, i + bulkNumber));
+ acc.push(fromAmountDataArray.slice(i, i + bulkNumber));
}
return acc;
- }, [] as (typeof amountDataArray)[])
- .map(async (slicedAmountDataArray) => {
+ }, [] as (typeof fromAmountDataArray)[])
+ .map(async (slicedFromAmountDataArray) => {
allRoutes.push(
- ...(await getRoutes(squid, slicedAmountDataArray, toTokenAddress)),
+ ...(await getRoutesWithFeesValidation(
+ squid,
+ toTokenAddress,
+ balances,
+ slicedFromAmountDataArray,
+ )),
);
await delay(delayMs);
}),
@@ -370,7 +445,6 @@ export const useRoutes = () => {
}
const sortedRoutes = sortRoutesByFastestTime(allRoutes);
-
// Only update routes if the request is the latest one
if (currentRequestId === latestRequestIdRef.current) {
setRoutes(sortedRoutes);
diff --git a/packages/checkout/widgets-lib/src/lib/squid/types.ts b/packages/checkout/widgets-lib/src/lib/squid/types.ts
index 29135f5f33..dd1ae23150 100644
--- a/packages/checkout/widgets-lib/src/lib/squid/types.ts
+++ b/packages/checkout/widgets-lib/src/lib/squid/types.ts
@@ -38,6 +38,7 @@ export type AmountData = {
export type RouteData = {
amountData: AmountData;
route: RouteResponse;
+ isInsufficientGas: 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 0e2e77671e..81fbe2c213 100644
--- a/packages/checkout/widgets-lib/src/locales/en.json
+++ b/packages/checkout/widgets-lib/src/locales/en.json
@@ -866,7 +866,14 @@
"fiatPricePrefix": "≈ USD",
"zeroFees": "Zero fees",
"includedFees": "Fees"
- }
+ },
+ "noGasDrawer": {
+ "heading": "You'll need more {{token}} for gas",
+ "body": "Add more gas and return to complete this transaction, or try another option such as lowering your amount, using another wallet, or buying with card.",
+ "primaryAction": "Try another option",
+ "secondaryAction": "Add more {{token}}"
+ },
+ "noGasRouteMessage": "Insufficient {{token}} for gas"
}
},
"footers": {
diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json
index b8f4ce7e3e..37c7a48450 100644
--- a/packages/checkout/widgets-lib/src/locales/ja.json
+++ b/packages/checkout/widgets-lib/src/locales/ja.json
@@ -849,7 +849,14 @@
"fiatPricePrefix": "≈ USD",
"zeroFees": "手数料無料",
"includedFees": "手数料"
- }
+ },
+ "noGasDrawer": {
+ "heading": "{{token}} がガスにもっと必要です",
+ "body": "ガスを追加してこの取引を完了するか、金額を減らす、別のウォレットを使用する、またはカードで購入するなど、他のオプションを試してください。",
+ "primaryAction": "別のオプションを試す",
+ "secondaryAction": "{{token}} をさらに追加する"
+ },
+ "noGasRouteMessage": "ガス用の {{token}} が不足しています"
}
},
"footers": {
diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json
index 942ddf2424..6f65394d6a 100644
--- a/packages/checkout/widgets-lib/src/locales/ko.json
+++ b/packages/checkout/widgets-lib/src/locales/ko.json
@@ -846,7 +846,14 @@
"fiatPricePrefix": "≈ USD",
"zeroFees": "수수료 없음",
"includedFees": "수수료"
- }
+ },
+ "noGasDrawer": {
+ "heading": "가스에 {{token}}이(가) 더 필요합니다",
+ "body": "가스를 추가하고 이 거래를 완료하거나 금액을 줄이기, 다른 지갑 사용, 카드로 구매하기와 같은 다른 옵션을 시도해보세요.",
+ "primaryAction": "다른 옵션 시도하기",
+ "secondaryAction": "{{token}} 더 추가하기"
+ },
+ "noGasRouteMessage": "가스에 필요한 {{token}}이(가) 부족합니다"
}
},
"footers": {
diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json
index 2bf2e1a903..775b9b6bb5 100644
--- a/packages/checkout/widgets-lib/src/locales/zh.json
+++ b/packages/checkout/widgets-lib/src/locales/zh.json
@@ -846,7 +846,14 @@
"fiatPricePrefix": "≈ 美元",
"zeroFees": "零手续费",
"includedFees": "费用"
- }
+ },
+ "noGasDrawer": {
+ "heading": "您需要更多 {{token}} 用于燃料费",
+ "body": "添加更多燃料费后返回完成此交易,或者尝试其他选项,例如降低金额、使用其他钱包或使用银行卡购买。",
+ "primaryAction": "尝试其他选项",
+ "secondaryAction": "添加更多 {{token}}"
+ },
+ "noGasRouteMessage": "用于燃料费的 {{token}} 不足"
}
},
"footers": {
diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/RouteOption.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/RouteOption.tsx
index 7e7311512a..f9c40f6731 100644
--- a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/RouteOption.tsx
+++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/RouteOption.tsx
@@ -98,6 +98,17 @@ export function RouteOption({
{`${t('views.ADD_TOKENS.fees.balance')} ${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${routeBalanceUsd}`}
+ {routeData.isInsufficientGas && (
+ <>
+
+
+ {t('views.ADD_TOKENS.noGasRouteMessage', {
+ token:
+ routeData.route.route.estimate.gasCosts[0].token.symbol,
+ })}
+
+ >
+ )}
diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/SelectedRouteOption.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/SelectedRouteOption.tsx
index d2a92d72f5..c0f003cfcf 100644
--- a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/SelectedRouteOption.tsx
+++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/SelectedRouteOption.tsx
@@ -182,6 +182,17 @@ export function SelectedRouteOption({
{`${t('views.ADD_TOKENS.fees.balance')} ${t(
'views.ADD_TOKENS.fees.fiatPricePrefix',
)} $${routeBalanceUsd}`}
+ {routeData?.isInsufficientGas && (
+ <>
+
+
+ {t('views.ADD_TOKENS.noGasRouteMessage', {
+ token:
+ routeData.route.route.estimate.gasCosts[0].token.symbol,
+ })}
+
+ >
+ )}
diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/TokenDrawerMenu.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/TokenDrawerMenu.tsx
index f9b075dc11..43f73da9cb 100644
--- a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/TokenDrawerMenu.tsx
+++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/TokenDrawerMenu.tsx
@@ -215,7 +215,7 @@ export function TokenDrawerMenu({
pos: 'relative',
cursor: 'pointer',
// eslint-disable-next-line @typescript-eslint/naming-convention
- '&:hover > div:first-child': {
+ '&:hover > div:first-of-type': {
boxShadow: ({ base }) => `0 0 0 ${base.border.size[200]} ${base.color.text.body.primary}`,
},
}}
diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts b/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts
index 467e9629b6..ed3c268bd4 100644
--- a/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts
+++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts
@@ -9,3 +9,5 @@ export const BLOCK_TXN_ANIMATION = '/blocked.riv';
export const ERROR_TXN_ANIMATION = '/error.riv';
export const TOKEN_PRIORITY_ORDER = ['IMX', 'USDC', 'ETH'];
+
+export const TOOLKIT_SQUID_URL = 'https://toolkit.immutable.com/squid-bridge/';
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 584a7a0c5a..0ab31e100f 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
@@ -72,6 +72,8 @@ import { getFormattedAmounts } from '../functions/getFormattedNumber';
import { RouteData } from '../../../lib/squid/types';
import { 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';
interface AddTokensProps {
checkout: Checkout;
@@ -147,6 +149,7 @@ export function AddTokens({
const [fetchingRoutes, setFetchingRoutes] = useState(false);
const [insufficientBalance, setInsufficientBalance] = useState(false);
const [isAmountInputSynced, setIsAmountInputSynced] = useState(false);
+ const [showNotEnoughGasDrawer, setShowNotEnoughGasDrawer] = useState(false);
const debouncedSetSelectedAmount = useRef(
debounce((value: string) => {
@@ -197,6 +200,7 @@ export function AddTokens({
hasEmbeddedSwap: !!route.route.route.estimate.actions.find(
(action) => action.type === ActionType.SWAP,
),
+ isInsufficientGas: route.isInsufficientGas,
},
});
}
@@ -543,7 +547,11 @@ export function AddTokens({
const loading = (routeInputsReady || fetchingRoutes)
&& !(selectedRouteData || insufficientBalance);
- const readyToReview = routeInputsReady && !!toAddress && !!selectedRouteData && !loading;
+ const readyToReview = routeInputsReady
+ && !!toAddress
+ && !!selectedRouteData
+ && !selectedRouteData.isInsufficientGas
+ && !loading;
const handleWalletConnected = (
providerType: 'from' | 'to',
@@ -595,6 +603,19 @@ export function AddTokens({
});
}, [id, experiments]);
+ useEffect(() => {
+ if (selectedRouteData?.isInsufficientGas) {
+ setShowNotEnoughGasDrawer(true);
+ } else {
+ setShowNotEnoughGasDrawer(false);
+ }
+ }, [selectedRouteData]);
+
+ const handleToolkitClick = () => {
+ setShowNotEnoughGasDrawer(false);
+ window.open(TOOLKIT_SQUID_URL, '_blank');
+ };
+
return (
+ setShowNotEnoughGasDrawer(false)}
+ onToolkitClick={handleToolkitClick}
+ />
);
}
diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx
index d0c4a273e0..738b429f69 100644
--- a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx
+++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx
@@ -50,6 +50,7 @@ import {
APPROVE_TXN_ANIMATION,
EXECUTE_TXN_ANIMATION,
FIXED_HANDOVER_DURATION,
+ TOOLKIT_SQUID_URL,
} from '../utils/config';
import {
useAnalytics,
@@ -590,7 +591,7 @@ export function Review({
rc={(
)}
@@ -604,7 +605,7 @@ export function Review({
),
onPrimaryButtonClick: () => {
window.open(
- 'https://toolkit.immutable.com/squid-bridge/',
+ TOOLKIT_SQUID_URL,
'_blank',
'noreferrer',
);