add staking page

This commit is contained in:
2026-06-17 18:10:23 +03:00
parent 7966ef1c52
commit a5bb7abce8
17 changed files with 928 additions and 330 deletions

View File

@@ -1,42 +1,135 @@
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { Notification } from '@shared/ui'
import { useDebounce } from '@shared/lib/hooks/useDebounce'
import {
usePools,
useLpPositions,
useLpQuote,
useAddLp,
useRemoveLp,
getPoolsErrorMessage,
type LpDepositInput,
type LpPosition,
} from '@features/pools'
import { PoolsTable } from './PoolsTable'
import { LpDepositCard } from './LpDepositCard'
import { LpPositionsPanel } from './LpPositionsPanel'
import { MOCK_POOLS, MOCK_POSITIONS, MOCK_QUOTE } from '../model/meta'
import { LpRemoveModal } from './LpRemoveModal'
import styles from './PoolsWidget.module.css'
type Toast = { status: 'success' | 'error' | 'info'; message: string } | null
// Предзаполнение диапазона при выборе пула: текущая цена ±5%.
const RANGE_SPREAD = 0.05
export function PoolsWidget() {
const [selected, setSelected] = useState<string | null>(MOCK_POOLS[0]?.poolAddress ?? null)
const poolsQuery = usePools()
const positionsQuery = useLpPositions()
const addM = useAddLp()
const removeM = useRemoveLp()
const pools = poolsQuery.data ?? []
const positions = positionsQuery.data?.positions ?? []
const [selected, setSelected] = useState<string | null>(null)
const [amount0, setAmount0] = useState('')
const [amount1, setAmount1] = useState('')
const [priceLower, setPriceLower] = useState('')
const [priceUpper, setPriceUpper] = useState('')
const [slippage, setSlippage] = useState('1')
const [useNativeEth, setUseNativeEth] = useState(true)
const [removeTarget, setRemoveTarget] = useState<LpPosition | null>(null)
const [toast, setToast] = useState<Toast>(null)
const pool = MOCK_POOLS.find((p) => p.poolAddress === selected) ?? null
const pool = pools.find((p) => p.poolAddress === selected) ?? null
// Заглушки: логика endpoint появится позже, пока только тосты.
function handleSubmit() {
setToast({ status: 'info', message: 'Депозит в пул будет доступен после подключения API' })
function handleSelect(poolAddress: string) {
setSelected(poolAddress)
const next = pools.find((p) => p.poolAddress === poolAddress)
// Предзаполняем диапазон ±5% от текущей цены (юзер может поправить вручную).
if (next) {
setPriceLower((next.currentPrice * (1 - RANGE_SPREAD)).toFixed(2))
setPriceUpper((next.currentPrice * (1 + RANGE_SPREAD)).toFixed(2))
}
setAmount0('')
setAmount1('')
}
function handleRemove() {
setToast({ status: 'info', message: 'Вывод позиции будет доступен после подключения API' })
// Дебаунсим вход квоты, чтобы не дёргать /lp/quote на каждый символ.
const dAmount0 = useDebounce(amount0, 400)
const dAmount1 = useDebounce(amount1, 400)
const dPriceLower = useDebounce(priceLower, 400)
const dPriceUpper = useDebounce(priceUpper, 400)
const dSlippage = useDebounce(slippage, 400)
const quoteInput = useMemo<LpDepositInput | null>(() => {
if (!pool) return null
const slippageNum = parseFloat(dSlippage)
return {
pool,
amount0Human: dAmount0,
amount1Human: dAmount1,
priceLower: parseFloat(dPriceLower) || 0,
priceUpper: parseFloat(dPriceUpper) || 0,
slippageBps: Number.isFinite(slippageNum) ? Math.round(slippageNum * 100) : undefined,
useNativeEth,
}
}, [pool, dAmount0, dAmount1, dPriceLower, dPriceUpper, dSlippage, useNativeEth])
const quoteQuery = useLpQuote(quoteInput)
function handleSubmit() {
if (!pool) return
const slippageNum = parseFloat(slippage)
const input: LpDepositInput = {
pool,
amount0Human: amount0,
amount1Human: amount1,
priceLower: parseFloat(priceLower) || 0,
priceUpper: parseFloat(priceUpper) || 0,
slippageBps: Number.isFinite(slippageNum) ? Math.round(slippageNum * 100) : undefined,
useNativeEth,
}
addM.mutate(input, {
onSuccess: () => {
setToast({ status: 'success', message: `Депозит в пул ${pool.symbol0}/${pool.symbol1} выполнен` })
setAmount0('')
setAmount1('')
},
onError: (e) => setToast({ status: 'error', message: getPoolsErrorMessage(e) }),
})
}
function handleConfirmRemove(percent: number) {
if (!removeTarget) return
removeM.mutate(
{ tokenId: removeTarget.tokenId, percent },
{
onSuccess: () => {
setRemoveTarget(null)
setToast({ status: 'success', message: 'Ликвидность выведена на кошелёк' })
},
onError: (e) => setToast({ status: 'error', message: getPoolsErrorMessage(e) }),
}
)
}
return (
<div className={styles.widget}>
<PoolsTable pools={MOCK_POOLS} selected={selected} onSelect={setSelected} />
<PoolsTable
pools={pools}
selected={selected}
onSelect={handleSelect}
loading={poolsQuery.isLoading}
error={poolsQuery.isError ? getPoolsErrorMessage(poolsQuery.error) : null}
/>
<div className={styles.grid}>
<LpDepositCard
pool={pool}
quote={MOCK_QUOTE}
quote={quoteQuery.data}
quoteFetching={quoteQuery.isFetching}
pending={addM.isPending}
amount0={amount0}
amount1={amount1}
priceLower={priceLower}
@@ -51,9 +144,24 @@ export function PoolsWidget() {
onUseNativeEthChange={setUseNativeEth}
onSubmit={handleSubmit}
/>
<LpPositionsPanel positions={MOCK_POSITIONS} onRemove={handleRemove} removePending={false} />
<LpPositionsPanel
positions={positions}
onRemove={(tokenId) => setRemoveTarget(positions.find((p) => p.tokenId === tokenId) ?? null)}
removePending={removeM.isPending}
isLoading={positionsQuery.isLoading}
error={positionsQuery.isError ? getPoolsErrorMessage(positionsQuery.error) : null}
/>
</div>
{removeTarget && (
<LpRemoveModal
position={removeTarget}
pending={removeM.isPending}
onConfirm={handleConfirmRemove}
onClose={() => setRemoveTarget(null)}
/>
)}
{toast && <Notification status={toast.status} message={toast.message} onClose={() => setToast(null)} />}
</div>
)