add redirects

This commit is contained in:
2026-05-28 20:24:56 +03:00
parent 2e6ed487fd
commit 2026230ff6
10 changed files with 506 additions and 168 deletions

View File

@@ -195,6 +195,136 @@ export async function getTokensList(): Promise<TokenInfo[]> {
return res.data return res.data
} }
export interface JumperToken {
address: string
chainId: number
symbol: string
decimals: number
name: string
coinKey?: string
logoURI?: string
priceUSD: string
}
export type JumperTokensMap = Record<string, JumperToken[]>
export async function getJumperTokens(): Promise<JumperTokensMap> {
const res = await walletGet<{ tokens?: JumperTokensMap; data?: { tokens: JumperTokensMap } }>(
'/api/jumper/tokens?chains=1,56,1151111081099710,728126428,20000000000001'
)
return res.data?.tokens ?? res.tokens ?? {}
}
export interface JumperQuotePayload {
fromChain: string
toChain: string
fromToken: string
toToken: string
fromAmount: string
fromAddress: string
toAddress: string
slippage: number
}
export interface JumperQuoteToken {
address: string
chainId: number
symbol: string
decimals: number
name: string
logoURI?: string
priceUSD: string
}
export interface JumperFeeCost {
name: string
description?: string
token: JumperQuoteToken
amount: string
amountUSD: string
percentage?: string
included?: boolean
}
export interface JumperQuote {
type: string
id: string
tool: string
toolDetails: { key: string; name: string; logoURI?: string }
action: {
fromToken: JumperQuoteToken
fromAmount: string
toToken: JumperQuoteToken
fromChainId: number
toChainId: number
slippage: number
fromAddress: string
toAddress: string
}
estimate: {
tool: string
approvalAddress?: string
toAmountMin: string
toAmount: string
fromAmount: string
feeCosts?: JumperFeeCost[]
}
}
export async function getJumperQuote(payload: JumperQuotePayload): Promise<JumperQuote> {
const qs = new URLSearchParams({
fromChain: payload.fromChain,
toChain: payload.toChain,
fromToken: payload.fromToken,
toToken: payload.toToken,
fromAmount: payload.fromAmount,
fromAddress: payload.fromAddress,
toAddress: payload.toAddress,
slippage: String(payload.slippage),
}).toString()
const res = await walletGet<{ body?: JumperQuote; data?: { body?: JumperQuote } }>(
`/api/jumper/quote-best?${qs}`
)
return (res.data?.body ?? res.body) as JumperQuote
}
export interface BridgeExecutePayload {
provider: string
fromChain: number
toChain: number
fromToken: string
toToken: string
fromAmount: string
fromAddress: string
toAddress: string
acceptedMinOut?: string
}
export interface BridgeExecuteResult {
provider: string
fromChain: number
toChain: number
toolName: string
feeTxid?: string
feeAmount?: string
bridgeTxid: string
fromAmount: string
toAmountMin: string
fromAmountUSD?: string
toAmountUSD?: string
trackerUrl?: string
}
export async function executeBridge(payload: BridgeExecutePayload): Promise<BridgeExecuteResult> {
const res = await walletPost<{ data?: { success: boolean; data: BridgeExecuteResult } }>(
'/api/bridge/execute',
payload,
true,
{ 'Idempotency-Key': crypto.randomUUID() }
)
return (res.data?.data ?? res) as BridgeExecuteResult
}
export async function getRelayQuote(payload: RelayQuotePayload): Promise<RelayQuoteResponse> { export async function getRelayQuote(payload: RelayQuotePayload): Promise<RelayQuoteResponse> {
return walletPost<RelayQuoteResponse>('/api/relay/quote', payload) return walletPost<RelayQuoteResponse>('/api/relay/quote', payload)
} }

View File

@@ -1,3 +1,3 @@
export { useAllWalletBalances, usePrices, useSendWallet, useWalletAddresses, useWalletBalance, usePortfolio, useTokensList, useRelayQuote, useExecuteRelaySwap, useSignSwap, useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap, useCreateWallet, useRevealMnemonic } from './model/useWalletData' export { useAllWalletBalances, usePrices, useSendWallet, useWalletAddresses, useWalletBalance, usePortfolio, useTokensList, useRelayQuote, useExecuteRelaySwap, useSignSwap, useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap, useJumperTokens, useFetchJumperQuote, useExecuteBridge, useCreateWallet, useRevealMnemonic } from './model/useWalletData'
export type { Chain, FormattedAmount, WalletBalanceData, PriceEntry, SendWalletPayload, SendWalletResponse, WalletAddress, PortfolioData, PortfolioChain, PortfolioNative, PortfolioToken, TokenInfo, RelayQuotePayload, RelayQuoteResponse, RelaySwapResponse, RelaySwapStep, TrxSwapQuotePayload, TrxSwapQuoteData } from './api/walletApi' export type { Chain, FormattedAmount, WalletBalanceData, PriceEntry, SendWalletPayload, SendWalletResponse, WalletAddress, PortfolioData, PortfolioChain, PortfolioNative, PortfolioToken, TokenInfo, RelayQuotePayload, RelayQuoteResponse, RelaySwapResponse, RelaySwapStep, TrxSwapQuotePayload, TrxSwapQuoteData, JumperToken, JumperTokensMap, JumperQuote, JumperQuotePayload, JumperQuoteToken, JumperFeeCost, BridgeExecutePayload, BridgeExecuteResult } from './api/walletApi'
export { CHAINS } from './api/walletApi' export { CHAINS } from './api/walletApi'

View File

@@ -1,5 +1,5 @@
import { useQuery, useQueries, useMutation } from '@tanstack/react-query' import { useQuery, useQueries, useMutation } from '@tanstack/react-query'
import { getWalletBalance, getPrices, sendWallet, getWalletAddresses, getPortfolio, getTokensList, getRelayQuote, executeRelaySwap, signRawEvmTx, signSolTx, getTrxSwapQuote, executeTrxSwap, createWallet, revealMnemonic, CHAINS, type Chain, type SendWalletPayload, type RelayQuotePayload, type RelaySwapStep, type TrxSwapQuotePayload } from '../api/walletApi' import { getWalletBalance, getPrices, sendWallet, getWalletAddresses, getPortfolio, getTokensList, getRelayQuote, executeRelaySwap, signRawEvmTx, signSolTx, getTrxSwapQuote, executeTrxSwap, getJumperTokens, getJumperQuote, executeBridge, createWallet, revealMnemonic, CHAINS, type Chain, type SendWalletPayload, type RelayQuotePayload, type RelaySwapStep, type TrxSwapQuotePayload, type JumperQuotePayload, type BridgeExecutePayload } from '../api/walletApi'
export function useWalletBalance(chain: Chain) { export function useWalletBalance(chain: Chain) {
return useQuery({ return useQuery({
@@ -58,6 +58,22 @@ export function useTokensList() {
}) })
} }
export function useJumperTokens() {
return useQuery({
queryKey: ['wallet', 'jumper', 'tokens'],
queryFn: getJumperTokens,
staleTime: 10 * 60 * 1000,
})
}
export function useFetchJumperQuote() {
return useMutation({ mutationFn: (payload: JumperQuotePayload) => getJumperQuote(payload) })
}
export function useExecuteBridge() {
return useMutation({ mutationFn: (payload: BridgeExecutePayload) => executeBridge(payload) })
}
export function useCreateWallet() { export function useCreateWallet() {
return useMutation({ mutationFn: createWallet }) return useMutation({ mutationFn: createWallet })
} }

View File

@@ -0,0 +1,13 @@
export function toBaseUnits(amountHuman: string, decimals: number): string {
const [intPart, fracRaw = ''] = amountHuman.split('.')
const frac = fracRaw.slice(0, decimals).padEnd(decimals, '0')
const combined = `${intPart}${frac}`.replace(/^0+(?=\d)/, '')
return combined || '0'
}
export function fromBaseUnits(raw: string, decimals: number): string {
const s = raw.padStart(decimals + 1, '0')
const int = s.slice(0, s.length - decimals)
const frac = s.slice(s.length - decimals).replace(/0+$/, '')
return frac ? `${int}.${frac}` : int
}

View File

@@ -0,0 +1,141 @@
.overlay {
position: fixed;
inset: 0;
background: rgba(10, 11, 46, 0.75);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.card {
background: var(--bg-mid);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 32px;
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
gap: 24px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
}
.closeBtn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 22px;
cursor: pointer;
line-height: 1;
padding: 0;
font-family: var(--font-sans);
}
.closeBtn:hover {
color: var(--text-primary);
}
.flow {
display: flex;
flex-direction: column;
gap: 8px;
}
.token {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 4px;
}
.tokenLabel {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-secondary);
font-weight: 700;
}
.tokenAmount {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
}
.minOut {
font-size: 12px;
color: var(--text-secondary);
}
.arrow {
text-align: center;
color: var(--text-secondary);
font-size: 18px;
line-height: 1;
}
.details {
display: flex;
flex-direction: column;
gap: 10px;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
}
.rowLabel {
font-size: 13px;
color: var(--text-secondary);
}
.rowValue {
font-size: 13px;
color: var(--text-primary);
font-weight: 600;
}
.confirmBtn {
width: 100%;
height: 56px;
background: linear-gradient(135deg, var(--grad-edge), var(--grad-center));
border: none;
border-radius: 14px;
color: var(--text-primary);
font-size: 17px;
font-weight: 700;
cursor: pointer;
font-family: var(--font-sans);
letter-spacing: 0.3px;
transition: filter 0.25s, box-shadow 0.25s;
}
.confirmBtn:hover {
filter: brightness(1.15);
box-shadow: 0 0 24px rgba(91, 61, 184, 0.5);
}
.confirmBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: none;
box-shadow: none;
}

View File

@@ -0,0 +1,66 @@
import type { JumperQuote } from '@features/wallet'
import { fromBaseUnits } from '@shared/lib/utils/baseUnits'
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
import styles from './BridgeConfirmModal.module.css'
interface Props {
quote: JumperQuote
fromAmountHuman: string
isExecuting?: boolean
onConfirm: () => void
onClose: () => void
}
export function BridgeConfirmModal({ quote, fromAmountHuman, isExecuting, onConfirm, onClose }: Props) {
const { action, estimate, toolDetails } = quote
const toSymbol = action.toToken.symbol
const fromSymbol = action.fromToken.symbol
const toAmount = truncateDecimals(fromBaseUnits(estimate.toAmount, action.toToken.decimals), 8)
const toMin = truncateDecimals(fromBaseUnits(estimate.toAmountMin, action.toToken.decimals), 8)
const feesUsd = (estimate.feeCosts ?? [])
.reduce((sum, f) => sum + (parseFloat(f.amountUSD) || 0), 0)
.toFixed(2)
return (
<div className={styles.overlay} onClick={onClose}>
<div className={styles.card} onClick={e => e.stopPropagation()}>
<div className={styles.header}>
<span className={styles.title}>Подтвердить бридж</span>
<button className={styles.closeBtn} onClick={onClose}>×</button>
</div>
<div className={styles.flow}>
<div className={styles.token}>
<span className={styles.tokenLabel}>Отдаёте</span>
<span className={styles.tokenAmount}>{fromAmountHuman} {fromSymbol}</span>
</div>
<div className={styles.arrow}></div>
<div className={styles.token}>
<span className={styles.tokenLabel}>Получаете</span>
<span className={styles.tokenAmount}>{toAmount} {toSymbol}</span>
<span className={styles.minOut}>Минимум: {toMin} {toSymbol}</span>
</div>
</div>
<div className={styles.details}>
<div className={styles.row}>
<span className={styles.rowLabel}>Комиссия</span>
<span className={styles.rowValue}>${feesUsd}</span>
</div>
<div className={styles.row}>
<span className={styles.rowLabel}>Мост</span>
<span className={styles.rowValue}>{toolDetails.name}</span>
</div>
</div>
<button className={styles.confirmBtn} onClick={onConfirm} disabled={isExecuting}>
{isExecuting ? 'Обработка...' : 'Подтвердить бридж'}
</button>
</div>
</div>
)
}

View File

@@ -1,141 +1,166 @@
import { useState, useEffect } from 'react' import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { Notification, PrimaryButton } from '@shared/ui'
import { import {
useWalletBalance, useWalletAddresses, useTokensList, useJumperTokens, useWalletBalance, useWalletAddresses, useFetchJumperQuote, useExecuteBridge,
useRelayQuote, useExecuteRelaySwap, useSignSwap, type Chain, type JumperToken, type WalletBalanceData, type JumperQuote,
useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap,
type Chain, type RelaySwapResponse, type TrxSwapQuoteData,
} from '@features/wallet' } from '@features/wallet'
import { Notification, PrimaryButton } from '@shared/ui'
import { ROUTES } from '@shared/config/routes' import { ROUTES } from '@shared/config/routes'
import { useDebounce } from '@shared/lib/hooks/useDebounce' import { toBaseUnits } from '@shared/lib/utils/baseUnits'
import { TOKENS_LIST, buildTokensFromBalance, useSwapForm } from '../../swap-form/model/useSwapForm' import {
TOKEN_META, buildTokensFromBalance, useSwapForm,
type Token,
} from '../../swap-form/model/useSwapForm'
import { SwapCard } from '../../swap-form/ui/SwapCard' import { SwapCard } from '../../swap-form/ui/SwapCard'
import { SwapDirectionButton } from '../../swap-form/ui/SwapDirectionButton'
import { SwapInfoPanel } from '../../swap-form/ui/SwapInfoPanel'
import { SwapConfirmModal } from '../../swap-form/ui/SwapConfirmModal'
import { TrxConfirmModal } from '../../swap-form/ui/TrxConfirmModal'
import { NetworkSelect } from './NetworkSelect' import { NetworkSelect } from './NetworkSelect'
import { BridgeConfirmModal } from './BridgeConfirmModal'
import styles from './BridgeForm.module.css' import styles from './BridgeForm.module.css'
const CHAIN_ID: Record<string, number> = { ETH: 1, BSC: 56, SOL: 792703809 } const NETWORKS = ['ETH', 'BSC', 'SOL', 'TRX', 'BTC']
const NATIVE_ADDR: Record<string, string> = {
SOL: '11111111111111111111111111111111', const CHAIN_ID_BY_NET: Record<string, string> = {
DEFAULT: '0x0000000000000000000000000000000000000000', ETH: '1',
BSC: '56',
SOL: '1151111081099710',
TRX: '728126428',
BTC: '20000000000001',
} }
function nativeAddr(chain: string) {
return NATIVE_ADDR[chain] ?? NATIVE_ADDR.DEFAULT 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() { export function BridgeForm() {
const navigate = useNavigate() const navigate = useNavigate()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { const {
fromAmount, fromUsd, fromAmount, fromUsd,
fromToken, toToken, fromToken, toToken,
setFromAmount, setPercent, swapTokens, setFromAmount, setPercent,
setFromToken, setToToken, setFromToken, setToToken,
} = useSwapForm() } = useSwapForm()
const [fromNetwork, setFromNetwork] = useState('ETH') const [fromNetwork, setFromNetwork] = useState('ETH')
const [toNetwork, setToNetwork] = useState('BSC') const [toNetwork, setToNetwork] = useState('BSC')
const [modalData, setModalData] = useState<RelaySwapResponse | null>(null) const [quote, setQuote] = useState<JumperQuote | null>(null)
const [trxModalQuote, setTrxModalQuote] = useState<TrxSwapQuoteData | null>(null)
const [errorMessage, setErrorMessage] = useState<string | null>(null) const [errorMessage, setErrorMessage] = useState<string | null>(null)
const isTrxNetwork = fromNetwork === 'TRX' const { data: jumperData } = useJumperTokens()
const { data: fromWalletData } = useWalletBalance(fromNetwork as Chain) const { data: fromWalletData } = useWalletBalance(fromNetwork as Chain)
const { data: toWalletData } = useWalletBalance(toNetwork as Chain) const { data: toWalletData } = useWalletBalance(toNetwork as Chain)
const { data: addresses } = useWalletAddresses()
const { mutate: fetchQuote, isPending } = useFetchJumperQuote()
const { mutate: executeBridge, isPending: isExecuting } = useExecuteBridge()
const fromTokenOptions = fromWalletData ? buildTokensFromBalance(fromWalletData) : TOKENS_LIST function optionsFor(network: string, walletData?: WalletBalanceData): Token[] {
const toTokenOptions = toWalletData ? buildTokensFromBalance(toWalletData) : TOKENS_LIST 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(() => { useEffect(() => {
if (fromTokenOptions.length === 0) return if (fromTokenOptions.length === 0) return
setFromToken(t => fromTokenOptions.find(o => o.symbol === t.symbol) ?? fromTokenOptions[0]) setFromToken(t => fromTokenOptions.find(o => o.symbol === t.symbol) ?? fromTokenOptions[0])
}, [fromWalletData, fromNetwork]) }, [jumperData, fromWalletData, fromNetwork])
useEffect(() => { useEffect(() => {
if (toTokenOptions.length === 0) return if (toTokenOptions.length === 0) return
setToToken(t => toTokenOptions.find(o => o.symbol === t.symbol) ?? toTokenOptions[0]) setToToken(t => toTokenOptions.find(o => o.symbol === t.symbol) ?? toTokenOptions[0])
}, [toWalletData, toNetwork]) }, [jumperData, toWalletData, toNetwork])
const debouncedAmount = useDebounce(fromAmount, 500) const fromJumper = jumperData?.[CHAIN_ID_BY_NET[fromNetwork]]?.find(t => t.symbol === fromToken.symbol)
const { data: addresses } = useWalletAddresses() const toJumper = jumperData?.[CHAIN_ID_BY_NET[toNetwork]]?.find(t => t.symbol === toToken.symbol)
const { data: tokensList } = useTokensList()
const parsedAmount = parseFloat(debouncedAmount)
const fromChainId = CHAIN_ID[fromNetwork]
const toChainId = CHAIN_ID[toNetwork]
const fromAddress = addresses?.find(a => a.chain === fromNetwork)?.address const fromAddress = addresses?.find(a => a.chain === fromNetwork)?.address
const toAddress = addresses?.find(a => a.chain === toNetwork)?.address const toAddress = addresses?.find(a => a.chain === toNetwork)?.address
const fromContract = tokensList?.find(t => t.chain === fromNetwork && t.symbol === fromToken.symbol)?.contract ?? nativeAddr(fromNetwork) const parsedAmount = parseFloat(fromAmount)
const toContract = tokensList?.find(t => t.chain === toNetwork && t.symbol === toToken.symbol)?.contract ?? nativeAddr(toNetwork) const canQuote = !!fromJumper && !!toJumper && !!fromAddress && !!toAddress && parsedAmount > 0
const quotePayload = !isTrxNetwork && fromChainId && toChainId && fromAddress && parsedAmount > 0 function handleConfirm() {
? { if (!fromJumper || !toJumper || !fromAddress || !toAddress) return
user: fromAddress, setErrorMessage(null)
recipient: toAddress ?? fromAddress, fetchQuote(
originChainId: fromChainId, {
destinationChainId: toChainId, fromChain: CHAIN_ID_BY_NET[fromNetwork],
originCurrency: fromContract, toChain: CHAIN_ID_BY_NET[toNetwork],
destinationCurrency: toContract, fromToken: fromJumper.address,
amount: Math.round(parsedAmount * Math.pow(10, fromToken.decimals)).toString(), toToken: toJumper.address,
tradeType: 'EXACT_INPUT' as const, fromAmount: toBaseUnits(fromAmount, fromToken.decimals),
fromAddress,
toAddress,
slippage: 0.005,
},
{
onSuccess: (q) => setQuote(q),
onError: (err) => setErrorMessage(err instanceof Error ? err.message : 'Не удалось получить котировку'),
},
)
} }
: null
const { data: quoteData } = useRelayQuote(quotePayload) function handleExecute() {
const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap() if (!quote) return
const { mutate: signSwap, isPending: isSigning } = useSignSwap() setErrorMessage(null)
executeBridge(
const trxQuotePayload = isTrxNetwork && parsedAmount > 0 {
? { from: fromToken.symbol, to: toToken.symbol, amountHuman: debouncedAmount } provider: 'jumper',
: null fromChain: quote.action.fromChainId,
toChain: quote.action.toChainId,
const { data: trxQuoteData } = useTrxSwapQuote(trxQuotePayload) fromToken: quote.action.fromToken.address,
const { mutate: fetchTrxQuote, isPending: isFetchingTrxQuote } = useFetchTrxQuote() toToken: quote.action.toToken.address,
const { mutate: executeTrxSwap, isPending: isExecutingTrxSwap } = useExecuteTrxSwap() fromAmount: quote.action.fromAmount,
fromAddress: quote.action.fromAddress,
const isProcessing = isSigning || isExecutingTrxSwap toAddress: quote.action.toAddress,
acceptedMinOut: quote.estimate.toAmountMin,
const displayToAmount = isTrxNetwork },
? (trxQuoteData?.expectedOutFormatted ?? '0') {
: (quoteData?.details.currencyOut.amountFormatted ?? '0') onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', fromNetwork] })
const displayToUsd = isTrxNetwork queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', toNetwork] })
? undefined queryClient.invalidateQueries({ queryKey: ['wallet', 'portfolio'] })
: quoteData?.details.currencyOut.amountUsd setQuote(null)
navigate(ROUTES.WALLET)
const gasFee = isTrxNetwork },
? trxQuoteData?.fees.network.amountUsd?.toString() onError: (err) => setErrorMessage(err instanceof Error ? err.message : 'Не удалось выполнить бридж'),
: quoteData?.fees.gas.amountUsd },
)
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),
})
} }
if (!jumperData) {
return <div className={styles.form} />
} }
return ( return (
<div className={styles.form}> <div className={styles.form}>
<NetworkSelect label="ИЗ" value={fromNetwork} onChange={setFromNetwork} /> <NetworkSelect
label="ИЗ"
value={fromNetwork}
onChange={setFromNetwork}
options={NETWORKS.filter(n => n !== toNetwork)}
/>
<SwapCard <SwapCard
mode="from" mode="from"
token={fromToken} token={fromToken}
@@ -148,81 +173,30 @@ export function BridgeForm() {
hideNetworkSelect hideNetworkSelect
/> />
<SwapDirectionButton onClick={swapTokens} /> <NetworkSelect
label="В"
<NetworkSelect label="В" value={toNetwork} onChange={setToNetwork} /> value={toNetwork}
onChange={setToNetwork}
options={NETWORKS.filter(n => n !== fromNetwork)}
/>
<SwapCard <SwapCard
mode="to" mode="to"
token={toToken} token={toToken}
tokenOptions={toTokenOptions} tokenOptions={toTokenOptions}
amount={displayToAmount} amount="0"
usd={displayToUsd}
onTokenChange={setToToken} onTokenChange={setToToken}
hideNetworkSelect hideNetworkSelect
/> />
<SwapInfoPanel gasFee={gasFee} /> <PrimaryButton label="Подтвердить бридж" onClick={handleConfirm} disabled={!canQuote || isPending} />
<PrimaryButton label="Подтвердить бридж" onClick={handleSwap} disabled={isButtonDisabled} /> {quote && (
<BridgeConfirmModal
{modalData && ( quote={quote}
<SwapConfirmModal fromAmountHuman={fromAmount}
data={modalData} isExecuting={isExecuting}
onClose={() => setModalData(null)} onConfirm={handleExecute}
onConfirm={() => { onClose={() => setQuote(null)}
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', 'balance', toNetwork] })
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}
onClose={() => setTrxModalQuote(null)}
onConfirm={() => {
setErrorMessage(null)
executeTrxSwap(trxModalQuote.quoteId, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', 'TRX'] })
queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', toNetwork] })
queryClient.invalidateQueries({ queryKey: ['wallet', 'portfolio'] })
navigate(ROUTES.WALLET)
},
onError: (err) => {
setErrorMessage(err instanceof Error ? err.message : 'Не удалось выполнить бридж')
},
})
setTrxModalQuote(null)
}}
/>
)}
{isProcessing && (
<Notification
status="warning"
message="Обработка транзакции..."
onClose={() => {}}
/> />
)} )}

View File

@@ -1,14 +1,13 @@
import styles from './NetworkSelect.module.css' import styles from './NetworkSelect.module.css'
const NETWORKS = ['ETH', 'BSC', 'TRX', 'SOL']
interface Props { interface Props {
label: string label: string
value: string value: string
onChange: (v: string) => void onChange: (v: string) => void
options: string[]
} }
export function NetworkSelect({ label, value, onChange }: Props) { export function NetworkSelect({ label, value, onChange, options }: Props) {
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
<span className={styles.label}>{label}</span> <span className={styles.label}>{label}</span>
@@ -17,7 +16,7 @@ export function NetworkSelect({ label, value, onChange }: Props) {
value={value} value={value}
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
> >
{NETWORKS.map(n => ( {options.map(n => (
<option key={n} value={n}>{n}</option> <option key={n} value={n}>{n}</option>
))} ))}
</select> </select>

View File

@@ -18,7 +18,6 @@ import { SwapConfirmModal } from './SwapConfirmModal'
import { TrxConfirmModal } from './TrxConfirmModal' import { TrxConfirmModal } from './TrxConfirmModal'
import styles from './SwapForm.module.css' import styles from './SwapForm.module.css'
const CHAIN_ID: Record<string, number> = { ETH: 1, BSC: 56, SOL: 792703809 } const CHAIN_ID: Record<string, number> = { ETH: 1, BSC: 56, SOL: 792703809 }
const NATIVE_ADDR: Record<string, string> = { const NATIVE_ADDR: Record<string, string> = {
SOL: '11111111111111111111111111111111', SOL: '11111111111111111111111111111111',

File diff suppressed because one or more lines are too long