UNPKG

@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.

219 lines (194 loc) 7.43 kB
import type { EVMChain, RouteExtended, Token, TokenAmount } from '@lifi/sdk' 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' export interface GasSufficiency { gasAmount: bigint tokenAmount?: bigint insufficientAmount?: bigint insufficient?: boolean token: Token chain?: EVMChain } const refetchInterval = 30_000 export const useGasSufficiency = (route?: RouteExtended) => { 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<ChainType>()) 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<GasSufficiency[]>({ queryKey: [ getQueryKey('gas-sufficiency-check', keyPrefix), relevantAccountsQueryKey, route?.id, isContractAddress, ] as const, 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 }, {} as Record<string, GasSufficiency> ) // 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 is PromiseFulfilledResult<TokenAmount[]> => 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, } }