Files
frontend/src/widgets/swap-form/ui/SwapForm.tsx
2026-06-26 16:01:44 +03:00

238 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, number> = { ETH: 1, BSC: 56, SOL: 792703809 }
const NATIVE_ADDR: Record<string, string> = {
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<RelaySwapResponse | null>(null)
const [trxModalQuote, setTrxModalQuote] = useState<TrxSwapQuoteData | null>(null)
const [errorMessage, setErrorMessage] = useState<string | null>(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 (
<div className={styles.form}>
<SwapCard
mode="from"
token={fromToken}
tokenOptions={tokenOptions}
amount={fromAmount}
usd={fromUsd}
onAmountChange={setFromAmount}
onSetPercent={setPercent}
onTokenChange={setFromToken}
selectedNetwork={fromNetwork}
onNetworkChange={setFromNetwork}
/>
<SwapDirectionButton onClick={swapTokens} />
<SwapCard
mode="to"
token={toToken}
tokenOptions={tokenOptions}
amount={displayToAmount}
usd={displayToUsd}
onTokenChange={setToToken}
/>
<SwapInfoPanel gasFee={gasFee} serviceFee={serviceFee} serviceFeeToken={serviceFeeToken} />
<PrimaryButton
label={isSwapping || isFetchingTrxQuote ? 'Загрузка...' : undefined}
onClick={handleSwap}
disabled={isButtonDisabled}
/>
{modalData && (
<SwapConfirmModal
data={modalData}
insufficientBalance={insufficientBalance}
onClose={() => 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 && (
<TrxConfirmModal
quote={trxModalQuote}
fromSymbol={fromToken.symbol}
toSymbol={toToken.symbol}
amountHuman={fromAmount}
insufficientBalance={insufficientBalance}
onClose={() => 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 && (
<Notification
status="warning"
message="Обработка транзакции..."
onClose={() => { }}
/>
)}
{errorMessage && (
<Notification
status="error"
message={errorMessage}
onClose={() => setErrorMessage(null)}
/>
)}
</div>
)
}