import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { useQueryClient } from '@tanstack/react-query' import { Notification, PrimaryButton } from '@shared/ui' import { useWalletBalance, useWalletAddresses, useTokensList, useRelayQuote, useExecuteRelaySwap, useSignSwap, useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap, type Chain, type RelaySwapResponse, type TrxSwapQuoteData, } from '@features/wallet' import { ROUTES } from '@shared/config/routes' import { useDebounce } from '@shared/lib/hooks/useDebounce' import { TOKENS_LIST, buildTokensFromBalance, useSwapForm } from '../model/useSwapForm' import { SwapCard } from './SwapCard' import { SwapDirectionButton } from './SwapDirectionButton' import { SwapInfoPanel } from './SwapInfoPanel' import { SwapConfirmModal } from './SwapConfirmModal' import { TrxConfirmModal } from './TrxConfirmModal' import styles from './SwapForm.module.css' const CHAIN_ID: Record = { ETH: 1, BSC: 56, SOL: 792703809 } const NATIVE_ADDR: Record = { SOL: '11111111111111111111111111111111', DEFAULT: '0x0000000000000000000000000000000000000000', } function nativeAddr(chain: string) { return NATIVE_ADDR[chain] ?? NATIVE_ADDR.DEFAULT } export function SwapForm() { const navigate = useNavigate() const queryClient = useQueryClient() const { fromAmount, fromUsd, fromToken, toToken, setFromAmount, setPercent, swapTokens, setFromToken, setToToken, } = useSwapForm() const [fromNetwork, setFromNetwork] = useState('ETH') const [modalData, setModalData] = useState(null) const [trxModalQuote, setTrxModalQuote] = useState(null) const [errorMessage, setErrorMessage] = useState(null) const isTrxNetwork = fromNetwork === 'TRX' const { data: walletData } = useWalletBalance(fromNetwork as Chain) const tokenOptions = walletData ? buildTokensFromBalance(walletData) : TOKENS_LIST useEffect(() => { if (tokenOptions.length === 0) return setFromToken(t => tokenOptions.find(o => o.symbol === t.symbol) ?? tokenOptions[0]) setToToken(t => tokenOptions.find(o => o.symbol === t.symbol) ?? (tokenOptions[1] ?? tokenOptions[0])) }, [walletData, fromNetwork]) const debouncedAmount = useDebounce(fromAmount, 500) const { data: addresses } = useWalletAddresses() const { data: tokensList } = useTokensList() const parsedAmount = parseFloat(debouncedAmount) // ── Relay (ETH / BSC / SOL) ───────────────────────────────────────────── const chainId = CHAIN_ID[fromNetwork] const walletAddress = addresses?.find(a => a.chain === fromNetwork)?.address const fromContract = tokensList?.find(t => t.chain === fromNetwork && t.symbol === fromToken.symbol)?.contract ?? nativeAddr(fromNetwork) const toContract = tokensList?.find(t => t.chain === fromNetwork && t.symbol === toToken.symbol)?.contract ?? nativeAddr(fromNetwork) const quotePayload = !isTrxNetwork && chainId && walletAddress && parsedAmount > 0 ? { user: walletAddress, recipient: walletAddress, originChainId: chainId, destinationChainId: chainId, originCurrency: fromContract, destinationCurrency: toContract, amount: Math.round(parsedAmount * Math.pow(10, fromToken.decimals)).toString(), tradeType: 'EXACT_INPUT' as const, } : null const { data: quoteData } = useRelayQuote(quotePayload) const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap() const { mutate: signSwap, isPending: isSigning } = useSignSwap() // ── TRX ───────────────────────────────────────────────────────────────── const trxQuotePayload = isTrxNetwork && parsedAmount > 0 ? { from: fromToken.symbol, to: toToken.symbol, amountHuman: debouncedAmount } : null const { data: trxQuoteData } = useTrxSwapQuote(trxQuotePayload) const { mutate: fetchTrxQuote, isPending: isFetchingTrxQuote } = useFetchTrxQuote() const { mutate: executeTrxSwap, isPending: isExecutingTrxSwap } = useExecuteTrxSwap() const isProcessing = isSigning || isExecutingTrxSwap const insufficientBalance = (parseFloat(fromAmount) || 0) > fromToken.balance // ── Display values ─────────────────────────────────────────────────────── const displayToAmount = isTrxNetwork ? (trxQuoteData?.expectedOutFormatted ?? '0') : (quoteData?.details.currencyOut.amountFormatted ?? '0') const displayToUsd = isTrxNetwork ? undefined : quoteData?.details.currencyOut.amountUsd const gasFee = isTrxNetwork ? trxQuoteData?.fees.network.amountUsd?.toString() : quoteData?.fees.gas.amountUsd const serviceFee = quoteData?.appCommission?.usd?.toString() const serviceFeeToken = quoteData?.appCommission?.inToken const isButtonDisabled = isTrxNetwork ? parsedAmount <= 0 || isFetchingTrxQuote : !quotePayload || isSwapping function handleSwap() { if (isTrxNetwork) { if (!trxQuotePayload) return fetchTrxQuote(trxQuotePayload, { onSuccess: (quote) => setTrxModalQuote(quote), }) } else { if (!quotePayload) return executeSwap(quotePayload, { onSuccess: (data) => setModalData(data), }) } } return (
{modalData && ( setModalData(null)} onConfirm={() => { const txData = modalData.steps[0]?.items[0]?.data if (txData) { setErrorMessage(null) signSwap( { chain: fromNetwork as Chain, txData }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', fromNetwork] }) queryClient.invalidateQueries({ queryKey: ['wallet', 'portfolio'] }) navigate(ROUTES.WALLET) }, onError: (err) => { setErrorMessage(err instanceof Error ? err.message : 'Не удалось подписать транзакцию') }, }, ) } setModalData(null) }} /> )} {trxModalQuote && ( setTrxModalQuote(null)} onConfirm={() => { setErrorMessage(null) executeTrxSwap(trxModalQuote.quoteId, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', 'TRX'] }) queryClient.invalidateQueries({ queryKey: ['wallet', 'portfolio'] }) navigate(ROUTES.WALLET) }, onError: (err) => { setErrorMessage(err instanceof Error ? err.message : 'Не удалось выполнить свап') }, }) setTrxModalQuote(null) }} /> )} {isProcessing && ( { }} /> )} {errorMessage && ( setErrorMessage(null)} /> )}
) }