From 6fc9f381829a7c3829bce370f814c752630d1ed3 Mon Sep 17 00:00:00 2001 From: rassadin11 Date: Thu, 14 May 2026 23:57:47 +0300 Subject: [PATCH] 14.05.2026 rip --- src/app/providers/RouterProvider.tsx | 2 + src/pages/bridge/index.ts | 1 + src/pages/bridge/ui/BridgePage.module.css | 52 +++++ src/pages/bridge/ui/BridgePage.tsx | 37 ++++ src/pages/swap/ui/SwapPage.tsx | 15 +- src/shared/config/routes.ts | 1 + src/widgets/bridge-form/index.ts | 1 + .../bridge-form/ui/BridgeForm.module.css | 7 + src/widgets/bridge-form/ui/BridgeForm.tsx | 177 ++++++++++++++++++ .../bridge-form/ui/NetworkSelect.module.css | 42 +++++ src/widgets/bridge-form/ui/NetworkSelect.tsx | 26 +++ src/widgets/swap-form/ui/SwapCard.tsx | 5 +- src/widgets/wallet-header/ui/WalletHeader.tsx | 6 +- 13 files changed, 360 insertions(+), 12 deletions(-) create mode 100644 src/pages/bridge/index.ts create mode 100644 src/pages/bridge/ui/BridgePage.module.css create mode 100644 src/pages/bridge/ui/BridgePage.tsx create mode 100644 src/widgets/bridge-form/index.ts create mode 100644 src/widgets/bridge-form/ui/BridgeForm.module.css create mode 100644 src/widgets/bridge-form/ui/BridgeForm.tsx create mode 100644 src/widgets/bridge-form/ui/NetworkSelect.module.css create mode 100644 src/widgets/bridge-form/ui/NetworkSelect.tsx diff --git a/src/app/providers/RouterProvider.tsx b/src/app/providers/RouterProvider.tsx index 79bd560..cbb7944 100644 --- a/src/app/providers/RouterProvider.tsx +++ b/src/app/providers/RouterProvider.tsx @@ -2,6 +2,7 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom' import { HomePage } from '@pages/home' import { WalletPage } from '@pages/wallet' import { SwapPage } from '@pages/swap' +import { BridgePage } from '@pages/bridge' import { ProfilePage } from '@pages/profile' import { LoginPage } from '@pages/login' import { RegisterPage } from '@pages/register' @@ -30,6 +31,7 @@ export function RouterProvider() { }> } /> } /> + } /> } /> } /> diff --git a/src/pages/bridge/index.ts b/src/pages/bridge/index.ts new file mode 100644 index 0000000..98ba981 --- /dev/null +++ b/src/pages/bridge/index.ts @@ -0,0 +1 @@ +export { BridgePage } from './ui/BridgePage' diff --git a/src/pages/bridge/ui/BridgePage.module.css b/src/pages/bridge/ui/BridgePage.module.css new file mode 100644 index 0000000..c389a4c --- /dev/null +++ b/src/pages/bridge/ui/BridgePage.module.css @@ -0,0 +1,52 @@ +.page { + display: flex; + flex-direction: column; + min-height: 100vh; + background: var(--bg-deep); +} + +.tabs { + display: flex; + gap: 8px; + padding: 24px 28px 0; +} + +.tab { + padding: 10px 24px; + border-radius: 10px; + font-size: 14px; + font-weight: 700; + cursor: pointer; + border: none; + font-family: var(--font-sans); + letter-spacing: 0.5px; + transition: all 0.2s; +} + +.active { + background: linear-gradient(135deg, var(--grad-edge), var(--grad-center)); + color: var(--text-primary); +} + +.inactive { + background: rgba(255, 255, 255, 0.06); + color: var(--text-secondary); +} + +.inactive:hover { + color: var(--text-primary); +} + +.main { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 32px 20px 48px; +} + +@media (max-width: 650px) { + .main { + padding: 32px 20px; + } +} diff --git a/src/pages/bridge/ui/BridgePage.tsx b/src/pages/bridge/ui/BridgePage.tsx new file mode 100644 index 0000000..59cf0c6 --- /dev/null +++ b/src/pages/bridge/ui/BridgePage.tsx @@ -0,0 +1,37 @@ +import { useNavigate } from 'react-router-dom' +import { Footer } from '@widgets/footer' +import { BridgeForm } from '@widgets/bridge-form' +import { WalletHeader } from '@widgets/wallet-header' +import { ROUTES } from '@shared/config/routes' +import styles from './BridgePage.module.css' + +export function BridgePage() { + const navigate = useNavigate() + + return ( +
+ + +
+ + +
+ +
+ +
+ +
+
+ ) +} diff --git a/src/pages/swap/ui/SwapPage.tsx b/src/pages/swap/ui/SwapPage.tsx index b527701..08cb479 100644 --- a/src/pages/swap/ui/SwapPage.tsx +++ b/src/pages/swap/ui/SwapPage.tsx @@ -1,13 +1,12 @@ -import { useState } from 'react' +import { useNavigate } from 'react-router-dom' import { Footer } from '@widgets/footer' import { SwapForm } from '@widgets/swap-form' import { WalletHeader } from '@widgets/wallet-header' +import { ROUTES } from '@shared/config/routes' import styles from './SwapPage.module.css' -type Tab = 'swap' | 'bridge' - export function SwapPage() { - const [tab, setTab] = useState('swap') + const navigate = useNavigate() return (
@@ -15,14 +14,14 @@ export function SwapPage() {
diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts index f0daf31..cccf058 100644 --- a/src/shared/config/routes.ts +++ b/src/shared/config/routes.ts @@ -2,6 +2,7 @@ export const ROUTES = { HOME: '/', WALLET: '/wallet', SWAP: "/swap", + BRIDGE: "/bridge", LOGIN: '/login', REGISTER: '/register', PROFILE: '/profile', diff --git a/src/widgets/bridge-form/index.ts b/src/widgets/bridge-form/index.ts new file mode 100644 index 0000000..0b91483 --- /dev/null +++ b/src/widgets/bridge-form/index.ts @@ -0,0 +1 @@ +export { BridgeForm } from './ui/BridgeForm' diff --git a/src/widgets/bridge-form/ui/BridgeForm.module.css b/src/widgets/bridge-form/ui/BridgeForm.module.css new file mode 100644 index 0000000..7fee56d --- /dev/null +++ b/src/widgets/bridge-form/ui/BridgeForm.module.css @@ -0,0 +1,7 @@ +.form { + display: flex; + flex-direction: column; + width: 100%; + max-width: 520px; + gap: 8px; +} diff --git a/src/widgets/bridge-form/ui/BridgeForm.tsx b/src/widgets/bridge-form/ui/BridgeForm.tsx new file mode 100644 index 0000000..dac30a7 --- /dev/null +++ b/src/widgets/bridge-form/ui/BridgeForm.tsx @@ -0,0 +1,177 @@ +import { useState, useEffect } from 'react' +import { PrimaryButton } from '@shared/ui' +import { + useWalletBalance, useWalletAddresses, useTokensList, + useRelayQuote, useExecuteRelaySwap, useSignSwap, + useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap, + type Chain, type RelaySwapResponse, type TrxSwapQuoteData, +} from '@features/wallet' +import { useDebounce } from '@shared/lib/hooks/useDebounce' +import { TOKENS_LIST, buildTokensFromBalance, useSwapForm } 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 styles from './BridgeForm.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 BridgeForm() { + const { + fromAmount, fromUsd, + fromToken, toToken, + setFromAmount, setPercent, swapTokens, + setFromToken, setToToken, + } = useSwapForm() + + const [fromNetwork, setFromNetwork] = useState('ETH') + const [toNetwork, setToNetwork] = useState('BSC') + const [modalData, setModalData] = useState(null) + const [trxModalQuote, setTrxModalQuote] = 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) + + 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 } = useSignSwap() + + 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 } = useExecuteTrxSwap() + + 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), + }) + } + } + + return ( +
+ + + + + + + + + + + + + {modalData && ( + setModalData(null)} + onConfirm={() => { + const txData = modalData.steps[0]?.items[0]?.data + if (txData) signSwap({ chain: fromNetwork as Chain, txData }) + setModalData(null) + }} + /> + )} + + {trxModalQuote && ( + setTrxModalQuote(null)} + onConfirm={() => { + executeTrxSwap(trxModalQuote.quoteId) + setTrxModalQuote(null) + }} + /> + )} +
+ ) +} diff --git a/src/widgets/bridge-form/ui/NetworkSelect.module.css b/src/widgets/bridge-form/ui/NetworkSelect.module.css new file mode 100644 index 0000000..4a917ab --- /dev/null +++ b/src/widgets/bridge-form/ui/NetworkSelect.module.css @@ -0,0 +1,42 @@ +.wrap { + display: flex; + align-items: center; + gap: 10px; + padding: 0 4px; + margin-bottom: 8px; +} + +.label { + font-size: 12px; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 1.2px; + font-weight: 700; + min-width: 20px; +} + +.select { + appearance: none; + background: rgba(255, 255, 255, 0.07); + border: 1px solid var(--glass-border); + border-radius: 8px; + color: var(--text-primary); + font-family: var(--font-sans); + font-size: 13px; + font-weight: 600; + padding: 5px 28px 5px 12px; + cursor: pointer; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + transition: border-color 0.2s; +} + +.select:focus { + outline: none; + border-color: var(--grad-center); +} + +.select option { + background: #1a1a2e; +} diff --git a/src/widgets/bridge-form/ui/NetworkSelect.tsx b/src/widgets/bridge-form/ui/NetworkSelect.tsx new file mode 100644 index 0000000..ead8f99 --- /dev/null +++ b/src/widgets/bridge-form/ui/NetworkSelect.tsx @@ -0,0 +1,26 @@ +import styles from './NetworkSelect.module.css' + +const NETWORKS = ['ETH', 'BSC', 'TRX', 'SOL'] + +interface Props { + label: string + value: string + onChange: (v: string) => void +} + +export function NetworkSelect({ label, value, onChange }: Props) { + return ( +
+ {label} + +
+ ) +} diff --git a/src/widgets/swap-form/ui/SwapCard.tsx b/src/widgets/swap-form/ui/SwapCard.tsx index 7a6ed8e..ea79524 100644 --- a/src/widgets/swap-form/ui/SwapCard.tsx +++ b/src/widgets/swap-form/ui/SwapCard.tsx @@ -13,6 +13,7 @@ interface Props { onSetPercent?: (p: number) => void selectedNetwork?: string onNetworkChange?: (network: string) => void + hideNetworkSelect?: boolean } const NETWORKS = ['ETH', 'BSC', 'TRX', 'SOL'] @@ -21,7 +22,7 @@ const PERCENTS = [25, 50, 100] export function SwapCard({ mode, token, tokenOptions, amount, usd, onTokenChange, onAmountChange, onSetPercent, - selectedNetwork, onNetworkChange, + selectedNetwork, onNetworkChange, hideNetworkSelect, }: Props) { const [intPart, decPart] = amount.split('.') @@ -45,7 +46,7 @@ export function SwapCard({
- {mode === 'from' && ( + {mode === 'from' && !hideNetworkSelect && (
{NETWORKS.map(n => ( {open && (