14.05.2026 rip

This commit is contained in:
2026-05-14 23:39:47 +03:00
parent 0d114e12c2
commit 7c8e812d4b
6 changed files with 306 additions and 14 deletions

View File

@@ -255,6 +255,39 @@ export async function signSolTx(txData: unknown): Promise<unknown> {
return walletPost('/api/wallets/SOL/sign-and-broadcast-tx', txData) 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<TrxSwapQuoteData> {
const res = await walletPost<{ success: boolean; data: TrxSwapQuoteData }>(
'/api/wallets/TRX/swap/quote',
payload
)
return res.data
}
export async function executeTrxSwap(quoteId: string): Promise<unknown> {
return walletPost(
'/api/wallets/TRX/swap',
{ quoteId },
true,
{ 'Idempotency-Key': `trx-${Date.now()}` }
)
}
export async function createWallet(): Promise<void> { export async function createWallet(): Promise<void> {
await walletPost<unknown>('/api/wallets/create', {}) await walletPost<unknown>('/api/wallets/create', {})
} }

View File

@@ -1,3 +1,3 @@
export { useAllWalletBalances, usePrices, useSendWallet, useWalletAddresses, useWalletBalance, usePortfolio, useTokensList, useRelayQuote, useExecuteRelaySwap, useSignSwap, useCreateWallet, useRevealMnemonic } from './model/useWalletData' 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 } from './api/walletApi' 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' export { CHAINS } from './api/walletApi'

View File

@@ -1,5 +1,5 @@
import { useQuery, useQueries, useMutation } from '@tanstack/react-query' 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) { export function useWalletBalance(chain: Chain) {
return useQuery({ 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) })
}

View File

@@ -1,12 +1,18 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { PrimaryButton } from '@shared/ui' 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 { 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'
import { SwapDirectionButton } from './SwapDirectionButton' import { SwapDirectionButton } from './SwapDirectionButton'
import { SwapInfoPanel } from './SwapInfoPanel' import { SwapInfoPanel } from './SwapInfoPanel'
import { SwapConfirmModal } from './SwapConfirmModal' import { SwapConfirmModal } from './SwapConfirmModal'
import { TrxConfirmModal } from './TrxConfirmModal'
import styles from './SwapForm.module.css' import styles from './SwapForm.module.css'
@@ -29,6 +35,9 @@ 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 isTrxNetwork = fromNetwork === 'TRX'
const { data: walletData } = useWalletBalance(fromNetwork as Chain) const { data: walletData } = useWalletBalance(fromNetwork as Chain)
const tokenOptions = walletData ? buildTokensFromBalance(walletData) : TOKENS_LIST const tokenOptions = walletData ? buildTokensFromBalance(walletData) : TOKENS_LIST
@@ -43,13 +52,15 @@ export function SwapForm() {
const { data: addresses } = useWalletAddresses() const { data: addresses } = useWalletAddresses()
const { data: tokensList } = useTokensList() const { data: tokensList } = useTokensList()
const parsedAmount = parseFloat(debouncedAmount)
// ── Relay (ETH / BSC / SOL) ─────────────────────────────────────────────
const chainId = CHAIN_ID[fromNetwork] const chainId = CHAIN_ID[fromNetwork]
const walletAddress = addresses?.find(a => a.chain === fromNetwork)?.address 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 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 toContract = tokensList?.find(t => t.chain === fromNetwork && t.symbol === toToken.symbol)?.contract ?? nativeAddr(fromNetwork)
const parsedAmount = parseFloat(debouncedAmount) const quotePayload = !isTrxNetwork && chainId && walletAddress && parsedAmount > 0
const quotePayload = chainId && walletAddress && parsedAmount > 0
? { ? {
user: walletAddress, user: walletAddress,
recipient: walletAddress, recipient: walletAddress,
@@ -66,15 +77,44 @@ export function SwapForm() {
const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap() const { mutate: executeSwap, isPending: isSwapping } = useExecuteRelaySwap()
const { mutate: signSwap } = useSignSwap() const { mutate: signSwap } = useSignSwap()
const displayToAmount = quoteData?.details.currencyOut.amountFormatted ?? '0' // ── TRX ─────────────────────────────────────────────────────────────────
const displayToUsd = quoteData?.details.currencyOut.amountUsd const trxQuotePayload = isTrxNetwork && parsedAmount > 0
const gasFee = quoteData?.fees.gas.amountUsd ? { 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() { function handleSwap() {
if (!quotePayload) return if (isTrxNetwork) {
executeSwap(quotePayload, { if (!trxQuotePayload) return
onSuccess: (data) => setModalData(data), fetchTrxQuote(trxQuotePayload, {
}) onSuccess: (quote) => setTrxModalQuote(quote),
})
} else {
if (!quotePayload) return
executeSwap(quotePayload, {
onSuccess: (data) => setModalData(data),
})
}
} }
return ( return (
@@ -105,7 +145,7 @@ export function SwapForm() {
<SwapInfoPanel gasFee={gasFee} /> <SwapInfoPanel gasFee={gasFee} />
<PrimaryButton onClick={handleSwap} disabled={!quotePayload || isSwapping} /> <PrimaryButton onClick={handleSwap} disabled={isButtonDisabled} />
{modalData && ( {modalData && (
<SwapConfirmModal <SwapConfirmModal
@@ -118,6 +158,20 @@ export function SwapForm() {
}} }}
/> />
)} )}
{trxModalQuote && (
<TrxConfirmModal
quote={trxModalQuote}
fromSymbol={fromToken.symbol}
toSymbol={toToken.symbol}
amountHuman={fromAmount}
onClose={() => setTrxModalQuote(null)}
onConfirm={() => {
executeTrxSwap(trxModalQuote.quoteId)
setTrxModalQuote(null)
}}
/>
)}
</div> </div>
) )
} }

View File

@@ -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);
}

View File

@@ -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 (
<div className={styles.overlay} onClick={onClose}>
<div className={styles.card} onClick={e => e.stopPropagation()}>
<div className={styles.header}>
<span className={styles.title}>Подтвердить своп</span>
<button className={styles.closeBtn} onClick={onClose}>×</button>
</div>
<div className={styles.flow}>
<div className={styles.token}>
<span className={styles.tokenLabel}>Отдаёте</span>
<span className={styles.tokenAmount}>{amountHuman} {fromSymbol}</span>
</div>
<div className={styles.arrow}></div>
<div className={styles.token}>
<span className={styles.tokenLabel}>Получаете</span>
<span className={styles.tokenAmount}>{expectedOutFormatted} {toSymbol}</span>
<span className={styles.minOut}>Минимум: {minOutFormatted} {toSymbol}</span>
</div>
</div>
<div className={styles.details}>
<div className={styles.row}>
<span className={styles.rowLabel}>Комиссия сети</span>
<span className={styles.rowValue}>
{fees.network.amountFormatted} {fees.network.asset} (${fees.network.amountUsd})
</span>
</div>
</div>
<button className={styles.confirmBtn} onClick={onConfirm}>
Подтвердить
</button>
</div>
</div>
)
}