add redirects

This commit is contained in:
2026-05-27 20:33:33 +03:00
parent acd30dbb13
commit 2e6ed487fd
11 changed files with 309 additions and 166 deletions

1
dist/assets/index-BrS4UPZI.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

161
dist/assets/index-woqmDgbc.js vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ЭКСА — Ваш мост в мир цифровых активов</title> <title>ЭКСА — Ваш мост в мир цифровых активов</title>
<script type="module" crossorigin src="/assets/index-H9RMF-3p.js"></script> <script type="module" crossorigin src="/assets/index-woqmDgbc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DianSEHM.css"> <link rel="stylesheet" crossorigin href="/assets/index-BrS4UPZI.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

10
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@reduxjs/toolkit": "^2.5.1", "@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-query": "^5.100.9", "@tanstack/react-query": "^5.100.9",
"axios": "^1.7.9", "axios": "^1.7.9",
"qrcode.react": "^4.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-easy-crop": "^5.5.7", "react-easy-crop": "^5.5.7",
@@ -3223,6 +3224,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "19.2.5", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",

View File

@@ -13,6 +13,7 @@
"@reduxjs/toolkit": "^2.5.1", "@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-query": "^5.100.9", "@tanstack/react-query": "^5.100.9",
"axios": "^1.7.9", "axios": "^1.7.9",
"qrcode.react": "^4.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-easy-crop": "^5.5.7", "react-easy-crop": "^5.5.7",

View File

@@ -1,11 +1,14 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { PrimaryButton } from '@shared/ui' import { useNavigate } from 'react-router-dom'
import { useQueryClient } from '@tanstack/react-query'
import { Notification, PrimaryButton } from '@shared/ui'
import { import {
useWalletBalance, useWalletAddresses, useTokensList, useWalletBalance, useWalletAddresses, useTokensList,
useRelayQuote, useExecuteRelaySwap, useSignSwap, useRelayQuote, useExecuteRelaySwap, useSignSwap,
useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap, useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap,
type Chain, type RelaySwapResponse, type TrxSwapQuoteData, type Chain, type RelaySwapResponse, type TrxSwapQuoteData,
} from '@features/wallet' } from '@features/wallet'
import { ROUTES } from '@shared/config/routes'
import { useDebounce } from '@shared/lib/hooks/useDebounce' import { useDebounce } from '@shared/lib/hooks/useDebounce'
import { TOKENS_LIST, buildTokensFromBalance, useSwapForm } from '../../swap-form/model/useSwapForm' import { TOKENS_LIST, buildTokensFromBalance, useSwapForm } from '../../swap-form/model/useSwapForm'
import { SwapCard } from '../../swap-form/ui/SwapCard' import { SwapCard } from '../../swap-form/ui/SwapCard'
@@ -26,6 +29,9 @@ function nativeAddr(chain: string) {
} }
export function BridgeForm() { export function BridgeForm() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const { const {
fromAmount, fromUsd, fromAmount, fromUsd,
fromToken, toToken, fromToken, toToken,
@@ -37,6 +43,7 @@ export function BridgeForm() {
const [toNetwork, setToNetwork] = useState('BSC') const [toNetwork, setToNetwork] = useState('BSC')
const [modalData, setModalData] = useState<RelaySwapResponse | null>(null) const [modalData, setModalData] = useState<RelaySwapResponse | null>(null)
const [trxModalQuote, setTrxModalQuote] = useState<TrxSwapQuoteData | null>(null) const [trxModalQuote, setTrxModalQuote] = useState<TrxSwapQuoteData | null>(null)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const isTrxNetwork = fromNetwork === 'TRX' const isTrxNetwork = fromNetwork === 'TRX'
@@ -84,7 +91,7 @@ export function BridgeForm() {
const { data: quoteData } = useRelayQuote(quotePayload) const { data: quoteData } = useRelayQuote(quotePayload)
const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap() const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap()
const { mutate: signSwap } = useSignSwap() const { mutate: signSwap, isPending: isSigning } = useSignSwap()
const trxQuotePayload = isTrxNetwork && parsedAmount > 0 const trxQuotePayload = isTrxNetwork && parsedAmount > 0
? { from: fromToken.symbol, to: toToken.symbol, amountHuman: debouncedAmount } ? { from: fromToken.symbol, to: toToken.symbol, amountHuman: debouncedAmount }
@@ -92,7 +99,9 @@ export function BridgeForm() {
const { data: trxQuoteData } = useTrxSwapQuote(trxQuotePayload) const { data: trxQuoteData } = useTrxSwapQuote(trxQuotePayload)
const { mutate: fetchTrxQuote, isPending: isFetchingTrxQuote } = useFetchTrxQuote() const { mutate: fetchTrxQuote, isPending: isFetchingTrxQuote } = useFetchTrxQuote()
const { mutate: executeTrxSwap } = useExecuteTrxSwap() const { mutate: executeTrxSwap, isPending: isExecutingTrxSwap } = useExecuteTrxSwap()
const isProcessing = isSigning || isExecutingTrxSwap
const displayToAmount = isTrxNetwork const displayToAmount = isTrxNetwork
? (trxQuoteData?.expectedOutFormatted ?? '0') ? (trxQuoteData?.expectedOutFormatted ?? '0')
@@ -162,7 +171,23 @@ export function BridgeForm() {
onClose={() => setModalData(null)} onClose={() => setModalData(null)}
onConfirm={() => { onConfirm={() => {
const txData = modalData.steps[0]?.items[0]?.data const txData = modalData.steps[0]?.items[0]?.data
if (txData) signSwap({ chain: fromNetwork as Chain, txData }) 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) setModalData(null)
}} }}
/> />
@@ -176,11 +201,38 @@ export function BridgeForm() {
amountHuman={fromAmount} amountHuman={fromAmount}
onClose={() => setTrxModalQuote(null)} onClose={() => setTrxModalQuote(null)}
onConfirm={() => { onConfirm={() => {
executeTrxSwap(trxModalQuote.quoteId) 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) setTrxModalQuote(null)
}} }}
/> />
)} )}
{isProcessing && (
<Notification
status="warning"
message="Обработка транзакции..."
onClose={() => {}}
/>
)}
{errorMessage && (
<Notification
status="error"
message={errorMessage}
onClose={() => setErrorMessage(null)}
/>
)}
</div> </div>
) )
} }

View File

@@ -59,6 +59,15 @@
gap: 12px; gap: 12px;
} }
.qrWrap {
align-self: center;
padding: 12px;
background: #fff;
border-radius: 12px;
line-height: 0;
margin-top: 8px;
}
.label { .label {
font-size: 13px; font-size: 13px;
color: var(--text-secondary, rgba(255,255,255,0.5)); color: var(--text-secondary, rgba(255,255,255,0.5));

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { QRCodeSVG } from 'qrcode.react'
import { useWalletAddresses, type Chain } from '@features/wallet' import { useWalletAddresses, type Chain } from '@features/wallet'
import styles from './ReceiveModal.module.css' import styles from './ReceiveModal.module.css'
@@ -87,6 +88,18 @@ export function ReceiveModal({ open, onClose, chain }: ReceiveModalProps) {
{copied ? 'Скопировано!' : 'Копировать'} {copied ? 'Скопировано!' : 'Копировать'}
</button> </button>
</div> </div>
{address && (
<div className={styles.qrWrap}>
<QRCodeSVG
value={address}
size={196}
level="M"
bgColor="#ffffff"
fgColor="#000000"
marginSize={2}
/>
</div>
)}
</> </>
)} )}
</div> </div>

View File

@@ -1,11 +1,14 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { PrimaryButton } from '@shared/ui' import { useNavigate } from 'react-router-dom'
import { useQueryClient } from '@tanstack/react-query'
import { Notification, PrimaryButton } from '@shared/ui'
import { import {
useWalletBalance, useWalletAddresses, useTokensList, useWalletBalance, useWalletAddresses, useTokensList,
useRelayQuote, useExecuteRelaySwap, useSignSwap, useRelayQuote, useExecuteRelaySwap, useSignSwap,
useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap, useTrxSwapQuote, useFetchTrxQuote, useExecuteTrxSwap,
type Chain, type RelaySwapResponse, type TrxSwapQuoteData, type Chain, type RelaySwapResponse, type TrxSwapQuoteData,
} from '@features/wallet' } from '@features/wallet'
import { ROUTES } from '@shared/config/routes'
import { useDebounce } from '@shared/lib/hooks/useDebounce' import { useDebounce } from '@shared/lib/hooks/useDebounce'
import { TOKENS_LIST, buildTokensFromBalance, useSwapForm } from '../model/useSwapForm' import { TOKENS_LIST, buildTokensFromBalance, useSwapForm } from '../model/useSwapForm'
import { SwapCard } from './SwapCard' import { SwapCard } from './SwapCard'
@@ -26,6 +29,9 @@ function nativeAddr(chain: string) {
} }
export function SwapForm() { export function SwapForm() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const { const {
fromAmount, fromUsd, fromAmount, fromUsd,
fromToken, toToken, fromToken, toToken,
@@ -36,6 +42,7 @@ export function SwapForm() {
const [fromNetwork, setFromNetwork] = useState('ETH') const [fromNetwork, setFromNetwork] = useState('ETH')
const [modalData, setModalData] = useState<RelaySwapResponse | null>(null) const [modalData, setModalData] = useState<RelaySwapResponse | null>(null)
const [trxModalQuote, setTrxModalQuote] = useState<TrxSwapQuoteData | null>(null) const [trxModalQuote, setTrxModalQuote] = useState<TrxSwapQuoteData | null>(null)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const isTrxNetwork = fromNetwork === 'TRX' const isTrxNetwork = fromNetwork === 'TRX'
@@ -75,7 +82,7 @@ export function SwapForm() {
const { data: quoteData } = useRelayQuote(quotePayload) const { data: quoteData } = useRelayQuote(quotePayload)
const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap() const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap()
const { mutate: signSwap } = useSignSwap() const { mutate: signSwap, isPending: isSigning } = useSignSwap()
// ── TRX ───────────────────────────────────────────────────────────────── // ── TRX ─────────────────────────────────────────────────────────────────
const trxQuotePayload = isTrxNetwork && parsedAmount > 0 const trxQuotePayload = isTrxNetwork && parsedAmount > 0
@@ -84,7 +91,9 @@ export function SwapForm() {
const { data: trxQuoteData } = useTrxSwapQuote(trxQuotePayload) const { data: trxQuoteData } = useTrxSwapQuote(trxQuotePayload)
const { mutate: fetchTrxQuote, isPending: isFetchingTrxQuote } = useFetchTrxQuote() const { mutate: fetchTrxQuote, isPending: isFetchingTrxQuote } = useFetchTrxQuote()
const { mutate: executeTrxSwap } = useExecuteTrxSwap() const { mutate: executeTrxSwap, isPending: isExecutingTrxSwap } = useExecuteTrxSwap()
const isProcessing = isSigning || isExecutingTrxSwap
// ── Display values ─────────────────────────────────────────────────────── // ── Display values ───────────────────────────────────────────────────────
const displayToAmount = isTrxNetwork const displayToAmount = isTrxNetwork
@@ -153,7 +162,22 @@ export function SwapForm() {
onClose={() => setModalData(null)} onClose={() => setModalData(null)}
onConfirm={() => { onConfirm={() => {
const txData = modalData.steps[0]?.items[0]?.data const txData = modalData.steps[0]?.items[0]?.data
if (txData) signSwap({ chain: fromNetwork as Chain, txData }) if (txData) {
setErrorMessage(null)
signSwap(
{ chain: fromNetwork as Chain, txData },
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', fromNetwork] })
queryClient.invalidateQueries({ queryKey: ['wallet', 'portfolio'] })
navigate(ROUTES.WALLET)
},
onError: (err) => {
setErrorMessage(err instanceof Error ? err.message : 'Не удалось подписать транзакцию')
},
},
)
}
setModalData(null) setModalData(null)
}} }}
/> />
@@ -167,11 +191,37 @@ export function SwapForm() {
amountHuman={fromAmount} amountHuman={fromAmount}
onClose={() => setTrxModalQuote(null)} onClose={() => setTrxModalQuote(null)}
onConfirm={() => { onConfirm={() => {
executeTrxSwap(trxModalQuote.quoteId) setErrorMessage(null)
executeTrxSwap(trxModalQuote.quoteId, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['wallet', 'balance', 'TRX'] })
queryClient.invalidateQueries({ queryKey: ['wallet', 'portfolio'] })
navigate(ROUTES.WALLET)
},
onError: (err) => {
setErrorMessage(err instanceof Error ? err.message : 'Не удалось выполнить свап')
},
})
setTrxModalQuote(null) setTrxModalQuote(null)
}} }}
/> />
)} )}
{isProcessing && (
<Notification
status="warning"
message="Обработка транзакции..."
onClose={() => {}}
/>
)}
{errorMessage && (
<Notification
status="error"
message={errorMessage}
onClose={() => setErrorMessage(null)}
/>
)}
</div> </div>
) )
} }