From 7c8e812d4b26c851db8cc158b904215e235e59b8 Mon Sep 17 00:00:00 2001 From: rassadin11 Date: Thu, 14 May 2026 23:39:47 +0300 Subject: [PATCH] 14.05.2026 rip --- src/features/wallet/api/walletApi.ts | 33 +++++ src/features/wallet/index.ts | 4 +- src/features/wallet/model/useWalletData.ts | 19 ++- src/widgets/swap-form/ui/SwapForm.tsx | 76 ++++++++-- .../swap-form/ui/TrxConfirmModal.module.css | 134 ++++++++++++++++++ src/widgets/swap-form/ui/TrxConfirmModal.tsx | 54 +++++++ 6 files changed, 306 insertions(+), 14 deletions(-) create mode 100644 src/widgets/swap-form/ui/TrxConfirmModal.module.css create mode 100644 src/widgets/swap-form/ui/TrxConfirmModal.tsx diff --git a/src/features/wallet/api/walletApi.ts b/src/features/wallet/api/walletApi.ts index 1bfed26..e0aa6e2 100644 --- a/src/features/wallet/api/walletApi.ts +++ b/src/features/wallet/api/walletApi.ts @@ -255,6 +255,39 @@ export async function signSolTx(txData: unknown): Promise { return walletPost('/api/wallets/SOL/sign-and-broadcast-tx', txData) } +export interface TrxSwapQuotePayload { + from: string + to: string + amountHuman: string +} + +export interface TrxSwapQuoteData { + quoteId: string + expiresIn: number + expectedOutFormatted: string + minOutFormatted: string + fees: { + network: { amountFormatted: string; asset: string; amountUsd: number } + } +} + +export async function getTrxSwapQuote(payload: TrxSwapQuotePayload): Promise { + const res = await walletPost<{ success: boolean; data: TrxSwapQuoteData }>( + '/api/wallets/TRX/swap/quote', + payload + ) + return res.data +} + +export async function executeTrxSwap(quoteId: string): Promise { + return walletPost( + '/api/wallets/TRX/swap', + { quoteId }, + true, + { 'Idempotency-Key': `trx-${Date.now()}` } + ) +} + export async function createWallet(): Promise { await walletPost('/api/wallets/create', {}) } diff --git a/src/features/wallet/index.ts b/src/features/wallet/index.ts index 666832b..1225064 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, useCreateWallet, useRevealMnemonic } from './model/useWalletData' -export type { Chain, FormattedAmount, WalletBalanceData, PriceEntry, SendWalletPayload, SendWalletResponse, WalletAddress, PortfolioData, PortfolioChain, PortfolioNative, PortfolioToken, TokenInfo, RelayQuotePayload, RelayQuoteResponse, RelaySwapResponse, RelaySwapStep } from './api/walletApi' +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 { CHAINS } from './api/walletApi' diff --git a/src/features/wallet/model/useWalletData.ts b/src/features/wallet/model/useWalletData.ts index 9ba2e7e..0c4ff97 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, createWallet, revealMnemonic, CHAINS, type Chain, type SendWalletPayload, type RelayQuotePayload, type RelaySwapStep } from '../api/walletApi' +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' export function useWalletBalance(chain: Chain) { return useQuery({ @@ -97,3 +97,20 @@ export function useSignSwap() { }, }) } + +export function useTrxSwapQuote(payload: TrxSwapQuotePayload | null) { + return useQuery({ + queryKey: ['trx', 'quote', payload?.from, payload?.to, payload?.amountHuman], + queryFn: () => getTrxSwapQuote(payload!), + enabled: !!payload, + staleTime: 10_000, + }) +} + +export function useFetchTrxQuote() { + return useMutation({ mutationFn: getTrxSwapQuote }) +} + +export function useExecuteTrxSwap() { + return useMutation({ mutationFn: (quoteId: string) => executeTrxSwap(quoteId) }) +} diff --git a/src/widgets/swap-form/ui/SwapForm.tsx b/src/widgets/swap-form/ui/SwapForm.tsx index 22ced64..3e61230 100644 --- a/src/widgets/swap-form/ui/SwapForm.tsx +++ b/src/widgets/swap-form/ui/SwapForm.tsx @@ -1,12 +1,18 @@ import { useState, useEffect } from 'react' import { PrimaryButton } from '@shared/ui' -import { useWalletBalance, useWalletAddresses, useTokensList, useRelayQuote, useExecuteRelaySwap, useSignSwap, type Chain, type RelaySwapResponse } from '@features/wallet' +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 '../model/useSwapForm' import { SwapCard } from './SwapCard' import { SwapDirectionButton } from './SwapDirectionButton' import { SwapInfoPanel } from './SwapInfoPanel' import { SwapConfirmModal } from './SwapConfirmModal' +import { TrxConfirmModal } from './TrxConfirmModal' import styles from './SwapForm.module.css' @@ -29,6 +35,9 @@ export function SwapForm() { const [fromNetwork, setFromNetwork] = useState('ETH') 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 @@ -43,13 +52,15 @@ export function SwapForm() { const { data: addresses } = useWalletAddresses() const { data: tokensList } = useTokensList() + const parsedAmount = parseFloat(debouncedAmount) + + // ── Relay (ETH / BSC / SOL) ───────────────────────────────────────────── 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 parsedAmount = parseFloat(debouncedAmount) - const quotePayload = chainId && walletAddress && parsedAmount > 0 + const quotePayload = !isTrxNetwork && chainId && walletAddress && parsedAmount > 0 ? { user: walletAddress, recipient: walletAddress, @@ -66,15 +77,44 @@ export function SwapForm() { const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap() const { mutate: signSwap } = useSignSwap() - const displayToAmount = quoteData?.details.currencyOut.amountFormatted ?? '0' - const displayToUsd = quoteData?.details.currencyOut.amountUsd - const gasFee = quoteData?.fees.gas.amountUsd + // ── TRX ───────────────────────────────────────────────────────────────── + 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() + + // ── Display values ─────────────────────────────────────────────────────── + 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 (!quotePayload) return - executeSwap(quotePayload, { - onSuccess: (data) => setModalData(data), - }) + if (isTrxNetwork) { + if (!trxQuotePayload) return + fetchTrxQuote(trxQuotePayload, { + onSuccess: (quote) => setTrxModalQuote(quote), + }) + } else { + if (!quotePayload) return + executeSwap(quotePayload, { + onSuccess: (data) => setModalData(data), + }) + } } return ( @@ -105,7 +145,7 @@ export function SwapForm() { - + {modalData && ( )} + + {trxModalQuote && ( + setTrxModalQuote(null)} + onConfirm={() => { + executeTrxSwap(trxModalQuote.quoteId) + setTrxModalQuote(null) + }} + /> + )} ) } diff --git a/src/widgets/swap-form/ui/TrxConfirmModal.module.css b/src/widgets/swap-form/ui/TrxConfirmModal.module.css new file mode 100644 index 0000000..9383871 --- /dev/null +++ b/src/widgets/swap-form/ui/TrxConfirmModal.module.css @@ -0,0 +1,134 @@ +.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); +} diff --git a/src/widgets/swap-form/ui/TrxConfirmModal.tsx b/src/widgets/swap-form/ui/TrxConfirmModal.tsx new file mode 100644 index 0000000..47464ac --- /dev/null +++ b/src/widgets/swap-form/ui/TrxConfirmModal.tsx @@ -0,0 +1,54 @@ +import type { TrxSwapQuoteData } from '@features/wallet' +import styles from './TrxConfirmModal.module.css' + +interface Props { + quote: TrxSwapQuoteData + fromSymbol: string + toSymbol: string + amountHuman: string + onConfirm: () => void + onClose: () => void +} + +export function TrxConfirmModal({ quote, fromSymbol, toSymbol, amountHuman, onConfirm, onClose }: Props) { + const { expectedOutFormatted, minOutFormatted, fees } = quote + + return ( +
+
e.stopPropagation()}> +
+ Подтвердить своп + +
+ +
+
+ Отдаёте + {amountHuman} {fromSymbol} +
+ +
+ +
+ Получаете + {expectedOutFormatted} {toSymbol} + Минимум: {minOutFormatted} {toSymbol} +
+
+ +
+
+ Комиссия сети + + {fees.network.amountFormatted} {fees.network.asset} (≈${fees.network.amountUsd}) + +
+
+ + +
+
+ ) +}