UNPKG

@openocean.finance/widget

Version:

Openocean Widget for cross-chain bridging and swapping. It will drive your multi-chain strategy and attract new users from everywhere.

369 lines 18.9 kB
import { useAccount } from '@openocean.finance/wallet-management'; import { ChainType, OpenOceanErrorCode } from '@openocean.finance/widget-sdk'; import { useWallet } from '@solana/wallet-adapter-react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { parseUnits } from 'viem'; import { useConfig } from 'wagmi'; import { getWalletClient } from 'wagmi/actions'; import { getCrossChainQuote } from '../cross/crossChainQuote.js'; import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'; import { OpenOceanService } from '../services/OpenOceanService.js'; import { useFieldValues } from '../stores/form/useFieldValues.js'; import { useSetExecutableRoute } from '../stores/routes/useSetExecutableRoute.js'; import { useSettings } from '../stores/settings/useSettings.js'; import { defaultSlippage } from '../stores/settings/useSettingsStore.js'; import { useServerErrorStore } from '../stores/useServerErrorStore.js'; import { WidgetEvent } from '../types/events.js'; import { getChainTypeFromAddress } from '../utils/chainType.js'; import { useChain } from './useChain.js'; import { useDebouncedWatch } from './useDebouncedWatch.js'; import { useGasPrice } from './useGasPrice.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 wagmiConfig = useConfig(); const { wallet: solanaWallet } = useWallet(); const { subvariant, sdkConfig, fee, feeConfig, referrer } = 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 { gasPrice } = useGasPrice(fromChainId?.toString() || ''); 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; const toAddress = fromChainId === toChainId || (fromChain?.chainType === 'EVM' && toChain?.chainType === 'EVM') ? account.address : _toAddress; // When bridging between ecosystems, we need to ensure 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 only need to check if toAddress is set const isToAddressSatisfied = toAddress ? hasToAddressAndChainTypeSatisfied : true; const isEnabled = Boolean(Number(fromChain?.id)) && Boolean(Number(toChain?.id)) && Boolean(fromToken?.address) && Boolean(toToken?.address) && !Number.isNaN(slippage) && hasAmount && isToAddressSatisfied && contractCallQuoteEnabled && !isBatchingSupportedLoading; const queryKey = [ 'routes', account.address, fromChain?.id, fromToken?.address, fromTokenAmount, toAddress, toChain?.id, toToken?.address, toTokenAmount, contractCalls, slippage, swapOnly, disabledBridges, disabledExchanges, enabledBridges, enabledExchanges, routePriority, subvariant, sdkConfig?.routeOptions?.allowSwitchChain, enabledRefuel && enabledAutoRefuel, gasRecommendationFromAmount, feeConfig?.fee || fee, !!isBatchingSupported, observableRoute?.id, ]; 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,], signal, }) => { try { useServerErrorStore.getState().setError(null); const fromAmount = parseUnits(fromTokenAmount, fromToken.decimals); const formattedSlippage = slippage ? slippage : '1'; // Default slippage 1% let quoteResult; // Initialize quoteResult // Check if it's a cross-chain swap if (fromChainId !== toChainId) { // Use DebridgeService for cross-chain quotes if (fromToken && toToken) { // Construct Asset objects for DebridgeService const fromMsg = { address: fromTokenAddress, symbol: fromToken.symbol, decimals: fromToken.decimals, name: fromToken.name, icon: fromToken.logoURI, chainId: fromChainId, isNative: fromToken.isNative, }; const toMsg = { address: toTokenAddress, symbol: toToken.symbol, decimals: toToken.decimals, name: toToken.name, icon: toToken.logoURI, chainId: toChainId, isNative: toToken.isNative, }; // Get appropriate wallet client based on chain type let walletClient = undefined; if (fromChain?.chainType === ChainType.EVM) { try { walletClient = await getWalletClient(wagmiConfig); } catch (error) { console.warn('Failed to get wallet client for EVM chain:', error); // Continue without walletClient for non-EVM chains } } else if (fromChain?.chainType === ChainType.SVM) { // For Solana chains, use the Solana wallet adapter walletClient = solanaWallet?.adapter; } quoteResult = await getCrossChainQuote({ feeBps: 10, fromMsg, toMsg, inAmount: fromAmount.toString(), slippage_tolerance: formattedSlippage, account: account?.address || '', walletClient, recipient: toAddress || '', }); // quoteResult = await DebridgeService.swapUThenCross({ // fromMsg: fromMsg, // toMsg: toMsg, // inAmount: fromAmount.toString(), // slippage_tolerance: formattedSlippage, // Debridge might use a different format/unit // account: account?.address || '', // receiver: toAddress, // Assuming receiver is the same as account for now // }) // Add a flag or modify structure to indicate it's a Debridge route if (quoteResult) { if (quoteResult.error) { throw new Error(quoteResult.error); } if (quoteResult.data) { quoteResult.isBridge = true; } } } else { console.warn('Cannot get Debridge quote: Missing account address, fromToken, or toToken.'); quoteResult = null; // Or handle error appropriately } } else { // Use OpenOceanService for same-chain swaps if (account.address) { quoteResult = await OpenOceanService.getSwapQuote({ chain: fromChainId.toString(), inTokenSymbol: fromToken?.symbol || '', inTokenAddress: fromTokenAddress, outTokenSymbol: toToken?.symbol || '', outTokenAddress: toTokenAddress, amount: fromAmount.toString(), account: account.address, slippage: formattedSlippage, // OpenOcean expects slippage like '100' for 1% gasPrice: gasPrice || '10', // Consider chain-specific defaults enabledDexIds: fromChainId === 1151111081099710 ? '6' : '', // Example for Solana specific dex referrer: referrer?.address || '', referrerFee: referrer?.fee || '', }); } else { // Use getQuote if account is not connected (view mode) quoteResult = await OpenOceanService.getQuote({ chain: fromChainId.toString(), inTokenSymbol: fromToken?.symbol || '', inTokenAddress: fromTokenAddress, outTokenSymbol: toToken?.symbol || '', outTokenAddress: toTokenAddress, amount: fromAmount.toString(), slippage: formattedSlippage, // OpenOcean expects slippage like '100' for 1% gasPrice: gasPrice || '10', // Consider chain-specific defaults enabledDexIds: fromChainId === 1151111081099710 ? '6' : '', // Example for Solana specific dex }); } // Ensure the structure is consistent or add a flag if (quoteResult) { quoteResult.isBridge = false; } } if (!quoteResult) { return []; } // biome-ignore lint/complexity/useOptionalChain: <explanation> const data = (quoteResult && quoteResult.data) || {}; // minOutAmount calculation is now handled within DebridgeService or OpenOceanService const isBridge = quoteResult.isBridge; let toAmountMin = '0'; if (data?.minOutAmount && Number(data.minOutAmount) > 0) { toAmountMin = data.minOutAmount; } else if (isBridge) { toAmountMin = '0'; } else { const amount = Number(data?.outAmount || 0); const slippageValue = Number.parseFloat(slippage); const minAmount = (amount * (100 - slippageValue)) / 100; toAmountMin = minAmount.toFixed(20).replace(/\.?0+$/, ''); // If still in scientific notation, force convert to string if (toAmountMin.includes('e') || toAmountMin.includes('E')) { toAmountMin = minAmount.toLocaleString('fullwide', { useGrouping: false, maximumSignificantDigits: 21, }); toAmountMin = toAmountMin.replace(/\.?0+$/, ''); } } const route = { id: data?.orderId || Date.now().toString(), fromChainId: fromChainId, fromAmountUSD: data?.fromTokenUSD || '0', fromAmount: fromAmount.toString(), fromToken: fromToken, fromAddress: fromAddress || '', toChainId: toChainId, toAmountUSD: data?.toTokenUSD || '0', toAmount: data?.outAmount || '0', toAmountMin, toToken: toToken, toAddress: toAddress || '', insurance: { state: 'NOT_INSURABLE', feeAmountUsd: '0', }, steps: [ { id: '1', type: isBridge ? 'bridge' : 'swap', tool: isBridge ? 'bridge' : 'openocean', transactionRequest: { chainId: data?.chainId || fromChainId, from: data?.from, data: data?.transaction || data?.data || '0x', to: data?.to, value: data?.value || '0x0', gasPrice: data?.gasPrice, type: isBridge ? data?.quoteAdapterKey : data?.dexId || '0x0', }, toolDetails: { key: isBridge ? data?.quoteAdapterKey : 'openocean', name: isBridge ? data?.quoteAdapterName : 'OpenOcean', logoURI: isBridge ? 'https://s3.openocean.finance/static/debridge.svg' : 'https://assets.coingecko.com/coins/images/17014/small/ooe_log.png', }, action: { fromChainId: fromChainId, fromAmount: fromAmount.toString(), fromToken: fromToken, toChainId: toChainId, toToken: toToken, slippage: Number(formattedSlippage), fromAddress: fromAddress || '', toAddress: toAddress || '', }, estimate: { fromAmount: fromAmount.toString(), toAmount: data?.outAmount || '0', toAmountMin, approvalAddress: data?.approveContract || data?.to || '0x0', executionDuration: data?.executionDuration || Math.floor(Math.random() * 20) + 40, tool: isBridge ? 'bridge' : 'openocean', feeCosts: data?.feeCosts || [ { name: 'Gas Fee', description: 'Estimated gas fee', token: fromToken, amount: (data?.estimatedGas || '0').toString(), amountUSD: '0', percentage: '0', included: true, }, ], }, includedSteps: [], quoteData: isBridge ? data : null, }, ], ...data, }; const routes = [route]; emitter.emit(WidgetEvent.AvailableRoutes, routes); return routes; } catch (error) { console.error('Failed to fetch OpenOcean routes:', error); useServerErrorStore.getState().setError(error.message); return []; } }, 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 === OpenOceanErrorCode.NotFound) { return false; } return false; }, }); const setReviewableRoute = (route) => { const queryDataKey = queryKey.toSpliced(queryKey.length - 1, 1, route.id); queryClient.setQueryData(queryDataKey, { routes: [route] }, { updatedAt: dataUpdatedAt }); setExecutableRoute(route); }; return { routes: data, isLoading, isFetching, isFetched, dataUpdatedAt, refetchTime, refetch, fromChain, toChain, queryKey, setReviewableRoute, }; }; //# sourceMappingURL=useRoutes.js.map