Files
frontend/src/widgets/bridge-form/ui/BridgeForm.tsx
2026-05-28 20:24:56 +03:00

213 lines
7.0 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 { 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<string, string> = {
ETH: '1',
BSC: '56',
SOL: '1151111081099710',
TRX: '728126428',
BTC: '20000000000001',
}
const NET_BY_CHAIN_ID: Record<string, string> = 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<string, number> {
const map: Record<string, number> = {}
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<JumperQuote | null>(null)
const [errorMessage, setErrorMessage] = useState<string | null>(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 <div className={styles.form} />
}
return (
<div className={styles.form}>
<NetworkSelect
label="ИЗ"
value={fromNetwork}
onChange={setFromNetwork}
options={NETWORKS.filter(n => n !== toNetwork)}
/>
<SwapCard
mode="from"
token={fromToken}
tokenOptions={fromTokenOptions}
amount={fromAmount}
usd={fromUsd}
onAmountChange={setFromAmount}
onSetPercent={setPercent}
onTokenChange={setFromToken}
hideNetworkSelect
/>
<NetworkSelect
label="В"
value={toNetwork}
onChange={setToNetwork}
options={NETWORKS.filter(n => n !== fromNetwork)}
/>
<SwapCard
mode="to"
token={toToken}
tokenOptions={toTokenOptions}
amount="0"
onTokenChange={setToToken}
hideNetworkSelect
/>
<PrimaryButton label="Подтвердить бридж" onClick={handleConfirm} disabled={!canQuote || isPending} />
{quote && (
<BridgeConfirmModal
quote={quote}
fromAmountHuman={fromAmount}
isExecuting={isExecuting}
onConfirm={handleExecute}
onClose={() => setQuote(null)}
/>
)}
{errorMessage && (
<Notification
status="error"
message={errorMessage}
onClose={() => setErrorMessage(null)}
/>
)}
</div>
)
}