import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useQueryClient } from '@tanstack/react-query' import { useJumperTokens, useWalletBalance, useWalletAddresses, useFetchJumperQuote, useExecuteBridge, type Chain, type JumperToken, type WalletBalanceData, type JumperQuote, } from '@features/wallet' import { Notification, PrimaryButton } from '@shared/ui' import { ROUTES } from '@shared/config/routes' import { toBaseUnits } from '@shared/lib/utils/baseUnits' import { TOKEN_META, buildTokensFromBalance, useSwapForm, type Token, } from '../../swap-form/model/useSwapForm' import { SwapCard } from '../../swap-form/ui/SwapCard' import { NetworkSelect } from './NetworkSelect' import { BridgeConfirmModal } from './BridgeConfirmModal' import styles from './BridgeForm.module.css' const NETWORKS = ['ETH', 'BSC', 'SOL', 'TRX', 'BTC'] const CHAIN_ID_BY_NET: Record = { ETH: '1', BSC: '56', SOL: '1151111081099710', TRX: '728126428', BTC: '20000000000001', } const NET_BY_CHAIN_ID: Record = Object.fromEntries( Object.entries(CHAIN_ID_BY_NET).map(([net, id]) => [id, net]) ) function mapJumperToken(t: JumperToken): Token { const meta = TOKEN_META[t.symbol] return { symbol: t.symbol, letter: meta?.letter ?? t.symbol[0], color: meta?.color ?? '#888', logo: t.logoURI ?? meta?.logo, network: NET_BY_CHAIN_ID[String(t.chainId)] ?? t.symbol, balance: 0, usdRate: parseFloat(t.priceUSD) || 0, decimals: t.decimals, } } function balanceBySymbol(data: WalletBalanceData): Record { const map: Record = {} for (const t of buildTokensFromBalance(data)) map[t.symbol] = t.balance return map } export function BridgeForm() { const navigate = useNavigate() const queryClient = useQueryClient() const { fromAmount, fromUsd, fromToken, toToken, setFromAmount, setPercent, setFromToken, setToToken, } = useSwapForm() const [fromNetwork, setFromNetwork] = useState('ETH') const [toNetwork, setToNetwork] = useState('BSC') const [quote, setQuote] = useState(null) const [errorMessage, setErrorMessage] = useState(null) const { data: jumperData } = useJumperTokens() const { data: fromWalletData } = useWalletBalance(fromNetwork as Chain) const { data: toWalletData } = useWalletBalance(toNetwork as Chain) const { data: addresses } = useWalletAddresses() const { mutate: fetchQuote, isPending } = useFetchJumperQuote() const { mutate: executeBridge, isPending: isExecuting } = useExecuteBridge() function optionsFor(network: string, walletData?: WalletBalanceData): Token[] { const list = (jumperData?.[CHAIN_ID_BY_NET[network]] ?? []).map(mapJumperToken) if (!walletData) return list const balances = balanceBySymbol(walletData) return list.map(t => (balances[t.symbol] != null ? { ...t, balance: balances[t.symbol] } : t)) } const fromTokenOptions = optionsFor(fromNetwork, fromWalletData) const toTokenOptions = optionsFor(toNetwork, toWalletData) useEffect(() => { if (fromTokenOptions.length === 0) return setFromToken(t => fromTokenOptions.find(o => o.symbol === t.symbol) ?? fromTokenOptions[0]) }, [jumperData, fromWalletData, fromNetwork]) useEffect(() => { if (toTokenOptions.length === 0) return setToToken(t => toTokenOptions.find(o => o.symbol === t.symbol) ?? toTokenOptions[0]) }, [jumperData, toWalletData, toNetwork]) const fromJumper = jumperData?.[CHAIN_ID_BY_NET[fromNetwork]]?.find(t => t.symbol === fromToken.symbol) const toJumper = jumperData?.[CHAIN_ID_BY_NET[toNetwork]]?.find(t => t.symbol === toToken.symbol) const fromAddress = addresses?.find(a => a.chain === fromNetwork)?.address const toAddress = addresses?.find(a => a.chain === toNetwork)?.address const parsedAmount = parseFloat(fromAmount) const canQuote = !!fromJumper && !!toJumper && !!fromAddress && !!toAddress && parsedAmount > 0 function handleConfirm() { if (!fromJumper || !toJumper || !fromAddress || !toAddress) return setErrorMessage(null) fetchQuote( { fromChain: CHAIN_ID_BY_NET[fromNetwork], toChain: CHAIN_ID_BY_NET[toNetwork], fromToken: fromJumper.address, toToken: toJumper.address, fromAmount: toBaseUnits(fromAmount, fromToken.decimals), fromAddress, toAddress, slippage: 0.005, }, { onSuccess: (q) => setQuote(q), onError: (err) => setErrorMessage(err instanceof Error ? err.message : 'Не удалось получить котировку'), }, ) } function handleExecute() { if (!quote) return setErrorMessage(null) executeBridge( { provider: 'jumper', fromChain: quote.action.fromChainId, toChain: quote.action.toChainId, fromToken: quote.action.fromToken.address, toToken: quote.action.toToken.address, fromAmount: quote.action.fromAmount, fromAddress: quote.action.fromAddress, toAddress: quote.action.toAddress, acceptedMinOut: quote.estimate.toAmountMin, }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', fromNetwork] }) queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', toNetwork] }) queryClient.invalidateQueries({ queryKey: ['wallet', 'portfolio'] }) setQuote(null) navigate(ROUTES.WALLET) }, onError: (err) => setErrorMessage(err instanceof Error ? err.message : 'Не удалось выполнить бридж'), }, ) } if (!jumperData) { return
} return (
n !== toNetwork)} /> n !== fromNetwork)} /> {quote && ( setQuote(null)} /> )} {errorMessage && ( setErrorMessage(null)} /> )}
) }