add staking page
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user