169 lines
5.8 KiB
TypeScript
169 lines
5.8 KiB
TypeScript
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 { 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 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 = pools.find((p) => p.poolAddress === selected) ?? null
|
|
|
|
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('')
|
|
}
|
|
|
|
// Дебаунсим вход квоты, чтобы не дёргать /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={pools}
|
|
selected={selected}
|
|
onSelect={handleSelect}
|
|
loading={poolsQuery.isLoading}
|
|
error={poolsQuery.isError ? getPoolsErrorMessage(poolsQuery.error) : null}
|
|
/>
|
|
|
|
<div className={styles.grid}>
|
|
<LpDepositCard
|
|
pool={pool}
|
|
quote={quoteQuery.data}
|
|
quoteFetching={quoteQuery.isFetching}
|
|
pending={addM.isPending}
|
|
amount0={amount0}
|
|
amount1={amount1}
|
|
priceLower={priceLower}
|
|
priceUpper={priceUpper}
|
|
slippage={slippage}
|
|
useNativeEth={useNativeEth}
|
|
onAmount0Change={setAmount0}
|
|
onAmount1Change={setAmount1}
|
|
onPriceLowerChange={setPriceLower}
|
|
onPriceUpperChange={setPriceUpper}
|
|
onSlippageChange={setSlippage}
|
|
onUseNativeEthChange={setUseNativeEth}
|
|
onSubmit={handleSubmit}
|
|
/>
|
|
<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>
|
|
)
|
|
}
|