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 => (