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.

394 lines 18.7 kB
import { ChainType, convertQuoteToRoute, getContractCallsQuote, getRelayerQuote, getRoutes, isRelayerStep, LiFiErrorCode, } from '@lifi/sdk'; import { useAccount } from '@lifi/wallet-management'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { parseUnits } from 'viem'; import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'; import { useFieldValues } from '../stores/form/useFieldValues.js'; import { useIntermediateRoutesStore } from '../stores/routes/useIntermediateRoutesStore.js'; import { useSetExecutableRoute } from '../stores/routes/useSetExecutableRoute.js'; import { useSettings } from '../stores/settings/useSettings.js'; import { defaultSlippage } from '../stores/settings/useSettingsStore.js'; import { WidgetEvent } from '../types/events.js'; import { getChainTypeFromAddress } from '../utils/chainType.js'; import { getQueryKey } from '../utils/queries.js'; import { useChain } from './useChain.js'; import { useDebouncedWatch } from './useDebouncedWatch.js'; import { useGasRefuel } from './useGasRefuel.js'; import { useIsBatchingSupported } from './useIsBatchingSupported.js'; import { useSwapOnly } from './useSwapOnly.js'; import { useToken } from './useToken.js'; import { useWidgetEvents } from './useWidgetEvents.js'; const refetchTime = 60000; export const useRoutes = ({ observableRoute } = {}) => { const { subvariant, subvariantOptions, sdkConfig, contractTool, bridges, exchanges, fee, feeConfig, useRelayerRoutes, keyPrefix, } = useWidgetConfig(); const setExecutableRoute = useSetExecutableRoute(); const queryClient = useQueryClient(); const emitter = useWidgetEvents(); const swapOnly = useSwapOnly(); const { disabledBridges, disabledExchanges, enabledBridges, enabledExchanges, enabledAutoRefuel, routePriority, slippage, } = useSettings([ 'disabledBridges', 'disabledExchanges', 'enabledBridges', 'enabledExchanges', 'enabledAutoRefuel', 'routePriority', 'slippage', ]); const [fromTokenAmount] = useDebouncedWatch(500, 'fromAmount'); const [fromChainId, fromTokenAddress, toAddress, toTokenAmount, toChainId, toTokenAddress, contractCalls,] = useFieldValues('fromChain', 'fromToken', 'toAddress', 'toAmount', 'toChain', 'toToken', 'contractCalls'); const { token: fromToken } = useToken(fromChainId, fromTokenAddress); const { token: toToken } = useToken(toChainId, toTokenAddress); const { chain: fromChain } = useChain(fromChainId); const { chain: toChain } = useChain(toChainId); const { enabled: enabledRefuel, fromAmount: gasRecommendationFromAmount } = useGasRefuel(); const { account } = useAccount({ chainType: fromChain?.chainType }); const { isBatchingSupported, isBatchingSupportedLoading } = useIsBatchingSupported(fromChain, account.address); const hasAmount = Number(fromTokenAmount) > 0 || Number(toTokenAmount) > 0; const contractCallQuoteEnabled = subvariant === 'custom' ? Boolean(contractCalls && account.address) : true; // When we bridge between ecosystems we need to be sure toAddress is set and has the same chainType as toChain // If toAddress is set, it must have the same chainType as toChain const hasToAddressAndChainTypeSatisfied = !!toChain && !!toAddress && getChainTypeFromAddress(toAddress) === toChain.chainType; // We need to check for toAddress only if it is set const isToAddressSatisfied = toAddress ? hasToAddressAndChainTypeSatisfied : true; // toAddress might be an empty string, but we need to pass undefined if there is no value const toWalletAddress = toAddress || undefined; // We need to send the full allowed tools array if custom tool settings are applied const allowedBridges = bridges?.allow?.length || bridges?.deny?.length ? enabledBridges : undefined; const allowedExchanges = exchanges?.allow?.length || exchanges?.deny?.length ? enabledExchanges : undefined; const allowSwitchChain = sdkConfig?.routeOptions?.allowSwitchChain; const isEnabled = Boolean(Number(fromChain?.id)) && Boolean(Number(toChain?.id)) && Boolean(fromToken?.address) && Boolean(toToken?.address) && !Number.isNaN(slippage) && hasAmount && isToAddressSatisfied && contractCallQuoteEnabled && !isBatchingSupportedLoading; // Some values should be strictly typed and isEnabled ensures that const queryKey = useMemo(() => [ getQueryKey('routes', keyPrefix), account.address, fromChain?.id, fromToken?.address, fromTokenAmount, toWalletAddress, toChain?.id, toToken?.address, toTokenAmount, contractCalls, slippage, swapOnly, disabledBridges, disabledExchanges, allowedBridges, allowedExchanges, routePriority, subvariant, allowSwitchChain, enabledRefuel && enabledAutoRefuel, gasRecommendationFromAmount, feeConfig?.fee || fee, !!isBatchingSupported, observableRoute?.id, ], [ keyPrefix, account.address, fromChain?.id, fromToken?.address, fromTokenAmount, toWalletAddress, toChain?.id, toToken?.address, toTokenAmount, contractCalls, slippage, swapOnly, disabledBridges, disabledExchanges, allowedBridges, allowedExchanges, routePriority, subvariant, allowSwitchChain, enabledRefuel, enabledAutoRefuel, gasRecommendationFromAmount, feeConfig?.fee, fee, isBatchingSupported, observableRoute?.id, ]); const { getIntermediateRoutes, setIntermediateRoutes } = useIntermediateRoutesStore(); const { data, isLoading, isFetching, isFetched, dataUpdatedAt, refetch } = useQuery({ queryKey, queryFn: async ({ queryKey: [_, fromAddress, fromChainId, fromTokenAddress, fromTokenAmount, toAddress, toChainId, toTokenAddress, toTokenAmount, contractCalls, slippage = defaultSlippage, swapOnly, disabledBridges, disabledExchanges, allowedBridges, allowedExchanges, routePriority, subvariant, allowSwitchChain, enabledRefuel, gasRecommendationFromAmount, fee, isBatchingSupported, // _observableRouteId must be the last element in the query key _observableRouteId,], signal, }) => { const fromAmount = parseUnits(fromTokenAmount, fromToken.decimals); const toAmount = parseUnits(toTokenAmount, toToken.decimals); const formattedSlippage = slippage ? Number.parseFloat(slippage) / 100 : defaultSlippage; const allowBridges = swapOnly ? [] : observableRoute ? observableRoute.steps.flatMap((step) => step.includedSteps.reduce((toolKeys, includedStep) => { if (includedStep.type === 'cross') { toolKeys.push(includedStep.toolDetails.key); } return toolKeys; }, [])) : allowedBridges; const allowExchanges = observableRoute ? observableRoute.steps.flatMap((step) => step.includedSteps.reduce((toolKeys, includedStep) => { if (includedStep.type === 'swap') { toolKeys.push(includedStep.toolDetails.key); } return toolKeys; }, [])) : allowedExchanges; const calculatedFee = await feeConfig?.calculateFee?.({ fromChain: fromChain, toChain: toChain, fromToken: fromToken, toToken: toToken, fromAddress, toAddress, fromAmount, toAmount, slippage: formattedSlippage, }); if (subvariant === 'custom' && contractCalls && toAmount) { const contractCallQuote = await getContractCallsQuote({ // Contract calls are enabled only when fromAddress is set fromAddress: fromAddress, fromChain: fromChainId, fromToken: fromTokenAddress, toAmount: toAmount.toString(), toChain: toChainId, toToken: toTokenAddress, contractCalls, denyBridges: disabledBridges.length ? disabledBridges : undefined, denyExchanges: disabledExchanges.length ? disabledExchanges : undefined, allowBridges, allowExchanges, toFallbackAddress: toAddress, slippage: formattedSlippage, fee: calculatedFee || fee, }, { signal }); contractCallQuote.action.toToken = toToken; const customStep = subvariant === 'custom' ? contractCallQuote.includedSteps?.find((step) => step.type === 'custom') : undefined; if (customStep && contractTool) { const toolDetails = { key: contractTool.name, name: contractTool.name, logoURI: contractTool.logoURI, }; customStep.toolDetails = toolDetails; contractCallQuote.toolDetails = toolDetails; } const route = convertQuoteToRoute(contractCallQuote); return [route]; } // Prevent sending a request for the same chain token combinations. // Exception: proceed anyway if subvariant is custom and subvariantOptions is deposit if (fromChainId === toChainId && fromTokenAddress === toTokenAddress && !(subvariant === 'custom' && subvariantOptions?.custom === 'deposit')) { return; } const isObservableRelayerRoute = observableRoute?.steps?.some(isRelayerStep); const shouldUseMainRoutes = !observableRoute || !isObservableRelayerRoute; const shouldUseRelayerQuote = fromAddress && fromChain?.chainType === ChainType.EVM && fromChain.permit2 && fromChain.permit2Proxy && fromChain.relayerSupported && fromChain.nativeToken.address !== fromTokenAddress && useRelayerRoutes && !isBatchingSupported && (!observableRoute || isObservableRelayerRoute); const mainRoutesPromise = shouldUseMainRoutes ? getRoutes({ fromAddress, fromAmount: fromAmount.toString(), fromChainId, fromTokenAddress, toAddress, toChainId, toTokenAddress, fromAmountForGas: enabledRefuel && gasRecommendationFromAmount ? gasRecommendationFromAmount : undefined, options: { allowSwitchChain: subvariant === 'refuel' ? false : allowSwitchChain, bridges: allowBridges?.length || disabledBridges.length ? { allow: allowBridges, deny: disabledBridges.length ? disabledBridges : undefined, } : undefined, exchanges: allowExchanges?.length || disabledExchanges.length ? { allow: allowExchanges, deny: disabledExchanges.length ? disabledExchanges : undefined, } : undefined, order: routePriority, slippage: formattedSlippage, fee: calculatedFee || fee, }, }, { signal }) : Promise.resolve(null); const relayerQuotePromise = shouldUseRelayerQuote ? getRelayerQuote({ fromAddress, fromAmount: fromAmount.toString(), fromChain: fromChainId, fromToken: fromTokenAddress, toAddress, toChain: toChainId, toToken: toTokenAddress, fromAmountForGas: enabledRefuel && gasRecommendationFromAmount ? gasRecommendationFromAmount : undefined, order: routePriority, slippage: formattedSlippage, fee: calculatedFee || fee, ...(allowBridges?.length || disabledBridges.length ? { allowBridges: allowBridges, denyBridges: disabledBridges.length ? disabledBridges : undefined, } : undefined), ...(allowExchanges?.length || disabledExchanges.length ? { allowExchanges: allowExchanges, denyExchanges: disabledExchanges.length ? disabledExchanges : undefined, } : undefined), }, { signal }) .then(convertQuoteToRoute) .catch(() => null) : Promise.resolve(null); // Wait for the main routes to complete first const routesResult = await mainRoutesPromise; if (routesResult?.routes[0] && fromAddress) { // Update local tokens cache to keep priceUSD in sync const { fromToken, toToken } = routesResult.routes[0]; [fromToken, toToken].forEach((token) => { queryClient.setQueriesData({ queryKey: [getQueryKey('tokens', keyPrefix)] }, (data) => { if (data) { const clonedData = { ...data }; const index = clonedData.tokens?.[token.chainId]?.findIndex((dataToken) => dataToken.address === token.address); if (index >= 0) { clonedData.tokens[token.chainId][index] = { ...clonedData.tokens[token.chainId][index], ...token, }; } return clonedData; } }); queryClient.setQueriesData({ queryKey: [ getQueryKey('token-balances', keyPrefix), fromAddress, token.chainId, ], }, (data) => { if (data) { const clonedData = [...data]; const index = clonedData.findIndex((dataToken) => dataToken.address === token.address); if (index >= 0) { clonedData[index] = { ...clonedData[index], ...token, }; } return clonedData; } }); }); } const initialRoutes = routesResult?.routes ?? []; if (shouldUseRelayerQuote && initialRoutes.length) { setIntermediateRoutes(queryKey, initialRoutes); emitter.emit(WidgetEvent.AvailableRoutes, initialRoutes); // Return early if we're only using main routes } else if (shouldUseMainRoutes) { // If we don't need relayer quote, return the initial routes emitter.emit(WidgetEvent.AvailableRoutes, initialRoutes); return initialRoutes; } const relayerRouteResult = await relayerQuotePromise; // If we have a relayer route, add it to the routes array if (relayerRouteResult) { // Insert the relayer route at position 1 (after the first route) initialRoutes.splice(1, 0, relayerRouteResult); // Emit the updated routes emitter.emit(WidgetEvent.AvailableRoutes, initialRoutes); } return initialRoutes; }, enabled: isEnabled, staleTime: refetchTime, refetchInterval(query) { return Math.min(Math.abs(refetchTime - (Date.now() - query.state.dataUpdatedAt)), refetchTime); }, retry(failureCount, error) { if (process.env.NODE_ENV === 'development') { console.warn('Route query failed:', { failureCount, error }); } if (failureCount >= 3) { return false; } if (error?.code === LiFiErrorCode.NotFound) { return false; } return true; }, }); const setReviewableRoute = useCallback((route) => { const queryDataKey = queryKey.toSpliced(queryKey.length - 1, 1, route.id); queryClient.setQueryData(queryDataKey, [route], { updatedAt: dataUpdatedAt || Date.now(), }); setExecutableRoute(route); }, [queryClient, dataUpdatedAt, setExecutableRoute, queryKey]); return { routes: data || getIntermediateRoutes(queryKey), isLoading: isEnabled && isLoading, isFetching, isFetched, dataUpdatedAt, refetchTime, refetch, fromChain, toChain, queryKey, setReviewableRoute, }; }; //# sourceMappingURL=useRoutes.js.map