@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
text/typescript
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,
}
}