14.05.2026 rip
This commit is contained in:
@@ -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', {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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) })
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
134
src/widgets/swap-form/ui/TrxConfirmModal.module.css
Normal file
134
src/widgets/swap-form/ui/TrxConfirmModal.module.css
Normal 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);
|
||||||
|
}
|
||||||
54
src/widgets/swap-form/ui/TrxConfirmModal.tsx
Normal file
54
src/widgets/swap-form/ui/TrxConfirmModal.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user