diff --git a/src/features/wallet/api/walletApi.ts b/src/features/wallet/api/walletApi.ts index e0aa6e2..ea8be3e 100644 --- a/src/features/wallet/api/walletApi.ts +++ b/src/features/wallet/api/walletApi.ts @@ -195,6 +195,136 @@ export async function getTokensList(): Promise { 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 + +export async function getJumperTokens(): Promise { + 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 { + 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 { + 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 { return walletPost('/api/relay/quote', payload) } diff --git a/src/features/wallet/index.ts b/src/features/wallet/index.ts index 1225064..04a174d 100644 --- a/src/features/wallet/index.ts +++ b/src/features/wallet/index.ts @@ -1,3 +1,3 @@ -export { useAllWalletBalances, usePrices, useSendWallet, useWalletAddresses, useWalletBalance, usePortfolio, useTokensList, useRelayQuote, useExecuteRelaySwap, useSignSwap, useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap, 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 { 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, JumperToken, JumperTokensMap, JumperQuote, JumperQuotePayload, JumperQuoteToken, JumperFeeCost, BridgeExecutePayload, BridgeExecuteResult } from './api/walletApi' export { CHAINS } from './api/walletApi' diff --git a/src/features/wallet/model/useWalletData.ts b/src/features/wallet/model/useWalletData.ts index 1e519e4..bd60693 100644 --- a/src/features/wallet/model/useWalletData.ts +++ b/src/features/wallet/model/useWalletData.ts @@ -1,5 +1,5 @@ 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) { 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() { return useMutation({ mutationFn: createWallet }) } diff --git a/src/shared/lib/utils/baseUnits.ts b/src/shared/lib/utils/baseUnits.ts new file mode 100644 index 0000000..3e5c794 --- /dev/null +++ b/src/shared/lib/utils/baseUnits.ts @@ -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 +} diff --git a/src/widgets/bridge-form/ui/BridgeConfirmModal.module.css b/src/widgets/bridge-form/ui/BridgeConfirmModal.module.css new file mode 100644 index 0000000..60ea7e6 --- /dev/null +++ b/src/widgets/bridge-form/ui/BridgeConfirmModal.module.css @@ -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; +} diff --git a/src/widgets/bridge-form/ui/BridgeConfirmModal.tsx b/src/widgets/bridge-form/ui/BridgeConfirmModal.tsx new file mode 100644 index 0000000..bbc2475 --- /dev/null +++ b/src/widgets/bridge-form/ui/BridgeConfirmModal.tsx @@ -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 ( +
+
e.stopPropagation()}> +
+ Подтвердить бридж + +
+ +
+
+ Отдаёте + {fromAmountHuman} {fromSymbol} +
+ +
+ +
+ Получаете + {toAmount} {toSymbol} + Минимум: {toMin} {toSymbol} +
+
+ +
+
+ Комиссия + ≈${feesUsd} +
+
+ Мост + {toolDetails.name} +
+
+ + +
+
+ ) +} diff --git a/src/widgets/bridge-form/ui/BridgeForm.tsx b/src/widgets/bridge-form/ui/BridgeForm.tsx index 406cabe..48b0d74 100644 --- a/src/widgets/bridge-form/ui/BridgeForm.tsx +++ b/src/widgets/bridge-form/ui/BridgeForm.tsx @@ -1,141 +1,166 @@ -import { useState, useEffect } from 'react' +import { useEffect, useState } 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, + 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 { useDebounce } from '@shared/lib/hooks/useDebounce' -import { TOKENS_LIST, buildTokensFromBalance, useSwapForm } from '../../swap-form/model/useSwapForm' +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 { 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 { BridgeConfirmModal } from './BridgeConfirmModal' import styles from './BridgeForm.module.css' -const CHAIN_ID: Record = { ETH: 1, BSC: 56, SOL: 792703809 } -const NATIVE_ADDR: Record = { - SOL: '11111111111111111111111111111111', - DEFAULT: '0x0000000000000000000000000000000000000000', +const NETWORKS = ['ETH', 'BSC', 'SOL', 'TRX', 'BTC'] + +const CHAIN_ID_BY_NET: Record = { + 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 = 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, swapTokens, + setFromAmount, setPercent, setFromToken, setToToken, } = useSwapForm() const [fromNetwork, setFromNetwork] = useState('ETH') const [toNetwork, setToNetwork] = useState('BSC') - const [modalData, setModalData] = useState(null) - const [trxModalQuote, setTrxModalQuote] = useState(null) + const [quote, setQuote] = useState(null) const [errorMessage, setErrorMessage] = useState(null) - const isTrxNetwork = fromNetwork === 'TRX' - + 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() - const fromTokenOptions = fromWalletData ? buildTokensFromBalance(fromWalletData) : TOKENS_LIST - const toTokenOptions = toWalletData ? buildTokensFromBalance(toWalletData) : TOKENS_LIST + 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]) - }, [fromWalletData, fromNetwork]) + }, [jumperData, fromWalletData, fromNetwork]) useEffect(() => { if (toTokenOptions.length === 0) return setToToken(t => toTokenOptions.find(o => o.symbol === t.symbol) ?? toTokenOptions[0]) - }, [toWalletData, toNetwork]) + }, [jumperData, toWalletData, toNetwork]) - const debouncedAmount = useDebounce(fromAmount, 500) - const { data: addresses } = useWalletAddresses() - const { data: tokensList } = useTokensList() - - const parsedAmount = parseFloat(debouncedAmount) - - const fromChainId = CHAIN_ID[fromNetwork] - const toChainId = CHAIN_ID[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 fromContract = tokensList?.find(t => t.chain === fromNetwork && t.symbol === fromToken.symbol)?.contract ?? nativeAddr(fromNetwork) - const toContract = tokensList?.find(t => t.chain === toNetwork && t.symbol === toToken.symbol)?.contract ?? nativeAddr(toNetwork) + const parsedAmount = parseFloat(fromAmount) + const canQuote = !!fromJumper && !!toJumper && !!fromAddress && !!toAddress && parsedAmount > 0 - const quotePayload = !isTrxNetwork && fromChainId && toChainId && fromAddress && parsedAmount > 0 - ? { - user: fromAddress, - recipient: toAddress ?? fromAddress, - originChainId: fromChainId, - destinationChainId: toChainId, - originCurrency: fromContract, - destinationCurrency: toContract, - amount: Math.round(parsedAmount * Math.pow(10, fromToken.decimals)).toString(), - tradeType: 'EXACT_INPUT' as const, - } - : null + 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 : 'Не удалось получить котировку'), + }, + ) + } - const { data: quoteData } = useRelayQuote(quotePayload) - const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap() - const { mutate: signSwap, isPending: isSigning } = useSignSwap() + 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 : 'Не удалось выполнить бридж'), + }, + ) + } - 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 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 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
} return (
- + n !== toNetwork)} + /> - - - + n !== fromNetwork)} + /> - + - - - {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', 'balance', toNetwork] }) - 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', 'balance', toNetwork] }) - queryClient.invalidateQueries({ queryKey: ['wallet', 'portfolio'] }) - navigate(ROUTES.WALLET) - }, - onError: (err) => { - setErrorMessage(err instanceof Error ? err.message : 'Не удалось выполнить бридж') - }, - }) - setTrxModalQuote(null) - }} - /> - )} - - {isProcessing && ( - {}} + {quote && ( + setQuote(null)} /> )} diff --git a/src/widgets/bridge-form/ui/NetworkSelect.tsx b/src/widgets/bridge-form/ui/NetworkSelect.tsx index ead8f99..df66d96 100644 --- a/src/widgets/bridge-form/ui/NetworkSelect.tsx +++ b/src/widgets/bridge-form/ui/NetworkSelect.tsx @@ -1,14 +1,13 @@ import styles from './NetworkSelect.module.css' -const NETWORKS = ['ETH', 'BSC', 'TRX', 'SOL'] - interface Props { label: string value: string onChange: (v: string) => void + options: string[] } -export function NetworkSelect({ label, value, onChange }: Props) { +export function NetworkSelect({ label, value, onChange, options }: Props) { return (
{label} @@ -17,7 +16,7 @@ export function NetworkSelect({ label, value, onChange }: Props) { value={value} onChange={e => onChange(e.target.value)} > - {NETWORKS.map(n => ( + {options.map(n => ( ))} diff --git a/src/widgets/swap-form/ui/SwapForm.tsx b/src/widgets/swap-form/ui/SwapForm.tsx index 50704b2..ade08c0 100644 --- a/src/widgets/swap-form/ui/SwapForm.tsx +++ b/src/widgets/swap-form/ui/SwapForm.tsx @@ -18,7 +18,6 @@ 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', @@ -211,7 +210,7 @@ export function SwapForm() { {}} + onClose={() => { }} /> )} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 83a0a38..af945c7 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/app.tsx","./src/app/providers/guestroute.tsx","./src/app/providers/protectedroute.tsx","./src/app/providers/queryprovider.tsx","./src/app/providers/routerprovider.tsx","./src/app/providers/scrolltotop.tsx","./src/app/providers/index.ts","./src/features/auth/index.ts","./src/features/auth/api/profileapi.ts","./src/features/auth/api/registrationapi.ts","./src/features/auth/hooks/useauth.ts","./src/features/auth/hooks/useisauthenticated.ts","./src/features/auth/hooks/useme.ts","./src/features/auth/hooks/useupdatephone.ts","./src/features/auth/hooks/useuploadavatar.ts","./src/features/kyc/api/kycapi.ts","./src/features/payment/index.ts","./src/features/payment/api/paymentapi.ts","./src/features/payment/hooks/usecreateorder.ts","./src/features/payment/hooks/useorders.ts","./src/features/payment/hooks/usepaymentconfig.ts","./src/features/payment/hooks/usepaymentquote.ts","./src/features/payment/hooks/usepaymentquotebyrub.ts","./src/features/wallet/index.ts","./src/features/wallet/api/walletapi.ts","./src/features/wallet/model/usewalletdata.ts","./src/pages/bridge/index.ts","./src/pages/bridge/ui/bridgepage.tsx","./src/pages/converter/index.ts","./src/pages/converter/ui/converterpage.tsx","./src/pages/home/index.ts","./src/pages/home/ui/homepage.tsx","./src/pages/kyc/index.ts","./src/pages/kyc/ui/kycpage.tsx","./src/pages/login/index.ts","./src/pages/login/ui/loginpage.tsx","./src/pages/politika-cookie/index.ts","./src/pages/politika-cookie/ui/politikacookiepage.tsx","./src/pages/politika-personalnyh-dannyh/index.ts","./src/pages/politika-personalnyh-dannyh/ui/politikapage.tsx","./src/pages/profile/index.ts","./src/pages/profile/ui/profilepage.tsx","./src/pages/publichnaya-oferta/index.ts","./src/pages/publichnaya-oferta/ui/publichnayaofertapage.tsx","./src/pages/reestr-pd-rkn/index.ts","./src/pages/reestr-pd-rkn/ui/reestrypage.tsx","./src/pages/register/index.ts","./src/pages/register/ui/registerpage.tsx","./src/pages/restore-password/index.ts","./src/pages/restore-password/ui/restorepasswordpage.tsx","./src/pages/seed-phrase/index.ts","./src/pages/seed-phrase/ui/seedphrasepage.tsx","./src/pages/soglasie-personalnyh-dannyh/index.ts","./src/pages/soglasie-personalnyh-dannyh/ui/soglasiepage.tsx","./src/pages/swap/index.ts","./src/pages/swap/ui/swappage.tsx","./src/pages/transactions/index.ts","./src/pages/transactions/ui/transactionspage.tsx","./src/pages/wallet/index.ts","./src/pages/wallet/ui/walletpage.tsx","./src/shared/api/base.ts","./src/shared/api/csrf.ts","./src/shared/api/tokenstore.ts","./src/shared/api/types.ts","./src/shared/assets/coins/index.ts","./src/shared/config/constants.ts","./src/shared/config/env.ts","./src/shared/config/routes.ts","./src/shared/lib/hooks/usedebounce.ts","./src/shared/lib/hooks/uselocalstorage.ts","./src/shared/lib/utils/cn.ts","./src/shared/lib/utils/truncatedecimals.ts","./src/shared/types/index.ts","./src/shared/ui/index.ts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/formfield/formfield.tsx","./src/shared/ui/formfield/index.ts","./src/shared/ui/notification/notification.tsx","./src/shared/ui/notification/index.ts","./src/shared/ui/pill/pill.tsx","./src/shared/ui/pill/index.ts","./src/shared/ui/primarybutton/primarybutton.tsx","./src/shared/ui/primarybutton/index.ts","./src/shared/ui/title/title.tsx","./src/shared/ui/tokenicon/tokenicon.tsx","./src/shared/ui/tokenicon/index.ts","./src/widgets/about/index.ts","./src/widgets/about/ui/about.tsx","./src/widgets/balance-card/index.ts","./src/widgets/balance-card/ui/balancecard.tsx","./src/widgets/bridge-form/index.ts","./src/widgets/bridge-form/ui/bridgeform.tsx","./src/widgets/bridge-form/ui/networkselect.tsx","./src/widgets/converter-page/index.ts","./src/widgets/converter-page/ui/agreementcheck.tsx","./src/widgets/converter-page/ui/commissionpanel.tsx","./src/widgets/converter-page/ui/convertersection.tsx","./src/widgets/currency-converter/index.ts","./src/widgets/currency-converter/model/tiers.ts","./src/widgets/currency-converter/model/useconverter.ts","./src/widgets/currency-converter/ui/agreementcheckbox.tsx","./src/widgets/currency-converter/ui/commissiontable.tsx","./src/widgets/currency-converter/ui/converter.tsx","./src/widgets/currency-converter/ui/tiers.tsx","./src/widgets/footer/index.ts","./src/widgets/footer/ui/footer.tsx","./src/widgets/header/index.ts","./src/widgets/header/ui/header.tsx","./src/widgets/hero/index.ts","./src/widgets/hero/lib/usecountdown.ts","./src/widgets/hero/ui/conversionflow.tsx","./src/widgets/hero/ui/countdown.tsx","./src/widgets/hero/ui/exchangecard.tsx","./src/widgets/hero/ui/hero.tsx","./src/widgets/kyc-verification/index.ts","./src/widgets/kyc-verification/model/usekyc.ts","./src/widgets/kyc-verification/ui/kycmodal.tsx","./src/widgets/kyc-verification/ui/kycwidget.tsx","./src/widgets/login-form/index.ts","./src/widgets/login-form/model/useloginform.ts","./src/widgets/login-form/ui/loginform.tsx","./src/widgets/networks-table/index.ts","./src/widgets/networks-table/model/networks.ts","./src/widgets/networks-table/ui/networkstable.tsx","./src/widgets/profile/index.ts","./src/widgets/profile/ui/avatarcropmodal.tsx","./src/widgets/profile/ui/profileavatar.tsx","./src/widgets/profile/ui/profilesection.tsx","./src/widgets/profile/ui/getcroppedimg.ts","./src/widgets/receive-modal/index.ts","./src/widgets/receive-modal/ui/receivemodal.tsx","./src/widgets/register-form/index.ts","./src/widgets/register-form/model/useregisterform.ts","./src/widgets/register-form/ui/registerform.tsx","./src/widgets/restore-password-form/index.ts","./src/widgets/restore-password-form/ui/restorepasswordform.tsx","./src/widgets/seed-phrase/index.ts","./src/widgets/seed-phrase/model/useseedphrase.ts","./src/widgets/seed-phrase/ui/seedphrasewidget.tsx","./src/widgets/send-modal/index.ts","./src/widgets/send-modal/model/sendtypes.ts","./src/widgets/send-modal/ui/sendmodal.tsx","./src/widgets/swap-form/index.ts","./src/widgets/swap-form/model/useswapform.ts","./src/widgets/swap-form/ui/raterow.tsx","./src/widgets/swap-form/ui/swapcard.tsx","./src/widgets/swap-form/ui/swapconfirmmodal.tsx","./src/widgets/swap-form/ui/swapdirectionbutton.tsx","./src/widgets/swap-form/ui/swapform.tsx","./src/widgets/swap-form/ui/swapinfopanel.tsx","./src/widgets/swap-form/ui/tokenselect.tsx","./src/widgets/swap-form/ui/trxconfirmmodal.tsx","./src/widgets/token-table/index.ts","./src/widgets/token-table/model/tokens.ts","./src/widgets/token-table/model/usechaintokenrows.ts","./src/widgets/token-table/ui/tokentable.tsx","./src/widgets/transactions-list/index.ts","./src/widgets/transactions-list/model/format.ts","./src/widgets/transactions-list/model/paymentstatuslabels.ts","./src/widgets/transactions-list/ui/copybutton.tsx","./src/widgets/transactions-list/ui/orderaccordion.tsx","./src/widgets/transactions-list/ui/statusbadge.tsx","./src/widgets/transactions-list/ui/transactionslist.tsx","./src/widgets/wallet-chain-tabs/index.ts","./src/widgets/wallet-chain-tabs/ui/walletchaintabs.tsx","./src/widgets/wallet-header/index.ts","./src/widgets/wallet-header/ui/walletheader.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/app.tsx","./src/app/providers/guestroute.tsx","./src/app/providers/protectedroute.tsx","./src/app/providers/queryprovider.tsx","./src/app/providers/routerprovider.tsx","./src/app/providers/scrolltotop.tsx","./src/app/providers/index.ts","./src/features/auth/index.ts","./src/features/auth/api/profileapi.ts","./src/features/auth/api/registrationapi.ts","./src/features/auth/hooks/useauth.ts","./src/features/auth/hooks/useisauthenticated.ts","./src/features/auth/hooks/useme.ts","./src/features/auth/hooks/useupdatephone.ts","./src/features/auth/hooks/useuploadavatar.ts","./src/features/kyc/api/kycapi.ts","./src/features/payment/index.ts","./src/features/payment/api/paymentapi.ts","./src/features/payment/hooks/usecreateorder.ts","./src/features/payment/hooks/useorders.ts","./src/features/payment/hooks/usepaymentconfig.ts","./src/features/payment/hooks/usepaymentquote.ts","./src/features/payment/hooks/usepaymentquotebyrub.ts","./src/features/wallet/index.ts","./src/features/wallet/api/walletapi.ts","./src/features/wallet/model/usewalletdata.ts","./src/pages/bridge/index.ts","./src/pages/bridge/ui/bridgepage.tsx","./src/pages/converter/index.ts","./src/pages/converter/ui/converterpage.tsx","./src/pages/home/index.ts","./src/pages/home/ui/homepage.tsx","./src/pages/kyc/index.ts","./src/pages/kyc/ui/kycpage.tsx","./src/pages/login/index.ts","./src/pages/login/ui/loginpage.tsx","./src/pages/politika-cookie/index.ts","./src/pages/politika-cookie/ui/politikacookiepage.tsx","./src/pages/politika-personalnyh-dannyh/index.ts","./src/pages/politika-personalnyh-dannyh/ui/politikapage.tsx","./src/pages/profile/index.ts","./src/pages/profile/ui/profilepage.tsx","./src/pages/publichnaya-oferta/index.ts","./src/pages/publichnaya-oferta/ui/publichnayaofertapage.tsx","./src/pages/reestr-pd-rkn/index.ts","./src/pages/reestr-pd-rkn/ui/reestrypage.tsx","./src/pages/register/index.ts","./src/pages/register/ui/registerpage.tsx","./src/pages/restore-password/index.ts","./src/pages/restore-password/ui/restorepasswordpage.tsx","./src/pages/seed-phrase/index.ts","./src/pages/seed-phrase/ui/seedphrasepage.tsx","./src/pages/soglasie-personalnyh-dannyh/index.ts","./src/pages/soglasie-personalnyh-dannyh/ui/soglasiepage.tsx","./src/pages/swap/index.ts","./src/pages/swap/ui/swappage.tsx","./src/pages/transactions/index.ts","./src/pages/transactions/ui/transactionspage.tsx","./src/pages/wallet/index.ts","./src/pages/wallet/ui/walletpage.tsx","./src/shared/api/base.ts","./src/shared/api/csrf.ts","./src/shared/api/tokenstore.ts","./src/shared/api/types.ts","./src/shared/assets/coins/index.ts","./src/shared/config/constants.ts","./src/shared/config/env.ts","./src/shared/config/routes.ts","./src/shared/lib/hooks/usedebounce.ts","./src/shared/lib/hooks/uselocalstorage.ts","./src/shared/lib/utils/baseunits.ts","./src/shared/lib/utils/cn.ts","./src/shared/lib/utils/truncatedecimals.ts","./src/shared/types/index.ts","./src/shared/ui/index.ts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/formfield/formfield.tsx","./src/shared/ui/formfield/index.ts","./src/shared/ui/notification/notification.tsx","./src/shared/ui/notification/index.ts","./src/shared/ui/pill/pill.tsx","./src/shared/ui/pill/index.ts","./src/shared/ui/primarybutton/primarybutton.tsx","./src/shared/ui/primarybutton/index.ts","./src/shared/ui/title/title.tsx","./src/shared/ui/tokenicon/tokenicon.tsx","./src/shared/ui/tokenicon/index.ts","./src/widgets/about/index.ts","./src/widgets/about/ui/about.tsx","./src/widgets/balance-card/index.ts","./src/widgets/balance-card/ui/balancecard.tsx","./src/widgets/bridge-form/index.ts","./src/widgets/bridge-form/ui/bridgeconfirmmodal.tsx","./src/widgets/bridge-form/ui/bridgeform.tsx","./src/widgets/bridge-form/ui/networkselect.tsx","./src/widgets/converter-page/index.ts","./src/widgets/converter-page/ui/agreementcheck.tsx","./src/widgets/converter-page/ui/commissionpanel.tsx","./src/widgets/converter-page/ui/convertersection.tsx","./src/widgets/currency-converter/index.ts","./src/widgets/currency-converter/model/tiers.ts","./src/widgets/currency-converter/model/useconverter.ts","./src/widgets/currency-converter/ui/agreementcheckbox.tsx","./src/widgets/currency-converter/ui/commissiontable.tsx","./src/widgets/currency-converter/ui/converter.tsx","./src/widgets/currency-converter/ui/tiers.tsx","./src/widgets/footer/index.ts","./src/widgets/footer/ui/footer.tsx","./src/widgets/header/index.ts","./src/widgets/header/ui/header.tsx","./src/widgets/hero/index.ts","./src/widgets/hero/lib/usecountdown.ts","./src/widgets/hero/ui/conversionflow.tsx","./src/widgets/hero/ui/countdown.tsx","./src/widgets/hero/ui/exchangecard.tsx","./src/widgets/hero/ui/hero.tsx","./src/widgets/kyc-verification/index.ts","./src/widgets/kyc-verification/model/usekyc.ts","./src/widgets/kyc-verification/ui/kycmodal.tsx","./src/widgets/kyc-verification/ui/kycwidget.tsx","./src/widgets/login-form/index.ts","./src/widgets/login-form/model/useloginform.ts","./src/widgets/login-form/ui/loginform.tsx","./src/widgets/networks-table/index.ts","./src/widgets/networks-table/model/networks.ts","./src/widgets/networks-table/ui/networkstable.tsx","./src/widgets/profile/index.ts","./src/widgets/profile/ui/avatarcropmodal.tsx","./src/widgets/profile/ui/profileavatar.tsx","./src/widgets/profile/ui/profilesection.tsx","./src/widgets/profile/ui/getcroppedimg.ts","./src/widgets/receive-modal/index.ts","./src/widgets/receive-modal/ui/receivemodal.tsx","./src/widgets/register-form/index.ts","./src/widgets/register-form/model/useregisterform.ts","./src/widgets/register-form/ui/registerform.tsx","./src/widgets/restore-password-form/index.ts","./src/widgets/restore-password-form/ui/restorepasswordform.tsx","./src/widgets/seed-phrase/index.ts","./src/widgets/seed-phrase/model/useseedphrase.ts","./src/widgets/seed-phrase/ui/seedphrasewidget.tsx","./src/widgets/send-modal/index.ts","./src/widgets/send-modal/model/sendtypes.ts","./src/widgets/send-modal/ui/sendmodal.tsx","./src/widgets/swap-form/index.ts","./src/widgets/swap-form/model/useswapform.ts","./src/widgets/swap-form/ui/raterow.tsx","./src/widgets/swap-form/ui/swapcard.tsx","./src/widgets/swap-form/ui/swapconfirmmodal.tsx","./src/widgets/swap-form/ui/swapdirectionbutton.tsx","./src/widgets/swap-form/ui/swapform.tsx","./src/widgets/swap-form/ui/swapinfopanel.tsx","./src/widgets/swap-form/ui/tokenselect.tsx","./src/widgets/swap-form/ui/trxconfirmmodal.tsx","./src/widgets/token-table/index.ts","./src/widgets/token-table/model/tokens.ts","./src/widgets/token-table/model/usechaintokenrows.ts","./src/widgets/token-table/ui/tokentable.tsx","./src/widgets/transactions-list/index.ts","./src/widgets/transactions-list/model/format.ts","./src/widgets/transactions-list/model/paymentstatuslabels.ts","./src/widgets/transactions-list/ui/copybutton.tsx","./src/widgets/transactions-list/ui/orderaccordion.tsx","./src/widgets/transactions-list/ui/statusbadge.tsx","./src/widgets/transactions-list/ui/transactionslist.tsx","./src/widgets/wallet-chain-tabs/index.ts","./src/widgets/wallet-chain-tabs/ui/walletchaintabs.tsx","./src/widgets/wallet-header/index.ts","./src/widgets/wallet-header/ui/walletheader.tsx"],"version":"5.6.3"} \ No newline at end of file