@lifi/widget
Version:
LI.FI Widget for cross-chain bridging and swapping. It will drive your multi-chain strategy and attract new users from everywhere.
143 lines • 7.48 kB
JavaScript
import { ChainType, isRelayerStep } from '@lifi/sdk';
import { useAccount } from '@lifi/wallet-management';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js';
import { getQueryKey } from '../utils/queries.js';
import { useAvailableChains } from './useAvailableChains.js';
import { useIsContractAddress } from './useIsContractAddress.js';
import { getTokenBalancesWithRetry } from './useTokenBalance.js';
const refetchInterval = 30000;
export const useGasSufficiency = (route) => {
const { getChainById } = useAvailableChains();
const { account: EVMAccount, accounts } = useAccount({
chainType: ChainType.EVM,
});
const { keyPrefix } = useWidgetConfig();
const { relevantAccounts, relevantAccountsQueryKey } = useMemo(() => {
const chainTypes = route?.steps.reduce((acc, step) => {
const chainType = getChainById(step.action.fromChainId)?.chainType;
if (chainType) {
acc.add(chainType);
}
return acc;
}, new Set());
const relevantAccounts = accounts.filter((account) => account.isConnected &&
account.address &&
chainTypes?.has(account.chainType));
return {
relevantAccounts,
relevantAccountsQueryKey: relevantAccounts
.map((account) => account.address)
.join(','),
};
}, [accounts, route?.steps, getChainById]);
const { isContractAddress, isLoading: isContractAddressLoading } = useIsContractAddress(EVMAccount.address, route?.fromChainId, EVMAccount.chainType);
const { data: insufficientGas, isLoading } = useQuery({
queryKey: [
getQueryKey('gas-sufficiency-check', keyPrefix),
relevantAccountsQueryKey,
route?.id,
isContractAddress,
],
queryFn: async () => {
if (!route) {
return [];
}
// Filter out steps that are relayer steps or have primaryType 'Permit' or 'Order'
const filteredSteps = route.steps.filter((step) => !isRelayerStep(step) &&
!step.typedData?.some((t) => t.primaryType === 'Permit' || t.primaryType === 'Order'));
// If all steps are filtered out, we don't need to check for gas sufficiency
if (!filteredSteps.length) {
return [];
}
// We assume that LI.Fuel protocol always refuels the destination chain
const hasRefuelStep = route.steps
.flatMap((step) => step.includedSteps)
.some((includedStep) => includedStep.tool === 'gasZip');
const gasCosts = filteredSteps
.filter((step) => !step.execution || step.execution.status !== 'DONE')
.reduce((groupedGasCosts, step) => {
// We need to avoid destination chain step sufficiency check if we have LI.Fuel protocol sub-step
const skipDueToRefuel = step.action.fromChainId === route.toChainId && hasRefuelStep;
if (step.estimate.gasCosts && !skipDueToRefuel) {
const { token } = step.estimate.gasCosts[0];
const gasCostAmount = step.estimate.gasCosts.reduce((amount, gasCost) => amount + BigInt(Number(gasCost.amount).toFixed(0)), 0n);
groupedGasCosts[token.chainId] = {
gasAmount: groupedGasCosts[token.chainId]
? groupedGasCosts[token.chainId].gasAmount + gasCostAmount
: gasCostAmount,
token,
chain: getChainById(token.chainId),
};
}
// Add fees paid in native tokens to gas sufficiency check (included: false)
const nonIncludedFeeCosts = step.estimate.feeCosts?.filter((feeCost) => !feeCost.included);
if (nonIncludedFeeCosts?.length) {
const { token } = nonIncludedFeeCosts[0];
const feeCostAmount = nonIncludedFeeCosts.reduce((amount, feeCost) => amount + BigInt(Number(feeCost.amount).toFixed(0)), 0n);
groupedGasCosts[token.chainId] = {
gasAmount: groupedGasCosts[token.chainId]
? groupedGasCosts[token.chainId].gasAmount + feeCostAmount
: feeCostAmount,
token,
chain: getChainById(token.chainId),
};
}
return groupedGasCosts;
}, {});
// Check whether we are sending a native token
// For native tokens we want to check for the total amount, including the network fee
if (route.fromToken.address === gasCosts[route.fromChainId]?.token.address) {
gasCosts[route.fromChainId].tokenAmount =
gasCosts[route.fromChainId]?.gasAmount + BigInt(route.fromAmount);
}
const gasCostsValues = Object.values(gasCosts);
const balanceChecks = await Promise.allSettled(relevantAccounts.map((account) => {
const relevantTokens = gasCostsValues
.filter((gasCost) => gasCost.chain?.chainType === account.chainType)
.map((item) => item.token);
return getTokenBalancesWithRetry(account.address, relevantTokens);
}));
const tokenBalances = balanceChecks
.filter((result) => result.status === 'fulfilled' && Boolean(result.value))
.flatMap((result) => result.value);
if (!tokenBalances?.length) {
return [];
}
Object.keys(gasCosts).forEach((chainId) => {
if (gasCosts[chainId]) {
const gasTokenBalance = tokenBalances?.find((t) => t.chainId === gasCosts[chainId].token.chainId &&
t.address === gasCosts[chainId].token.address)?.amount ?? 0n;
const insufficient = gasTokenBalance <= 0n ||
gasTokenBalance < gasCosts[chainId].gasAmount ||
gasTokenBalance < (gasCosts[chainId].tokenAmount ?? 0n);
const insufficientAmount = insufficient
? gasCosts[chainId].tokenAmount
? gasCosts[chainId].tokenAmount - gasTokenBalance
: gasCosts[chainId].gasAmount - gasTokenBalance
: undefined;
gasCosts[chainId] = {
...gasCosts[chainId],
insufficient,
insufficientAmount,
chain: insufficient ? getChainById(Number(chainId)) : undefined,
};
}
});
const gasCostResult = Object.values(gasCosts).filter((gasCost) => gasCost.insufficient);
return gasCostResult;
},
enabled: Boolean(!isContractAddress &&
!isContractAddressLoading &&
relevantAccounts.length > 0 &&
route),
refetchInterval,
staleTime: refetchInterval,
});
return {
insufficientGas,
isLoading,
};
};
//# sourceMappingURL=useGasSufficiency.js.map