add staking page

This commit is contained in:
2026-06-15 23:06:24 +03:00
parent 16ab237551
commit 7c3c0159e9
31 changed files with 2445 additions and 165 deletions

View File

@@ -0,0 +1,139 @@
import { useState } from 'react'
import { Notification } from '@shared/ui'
import { ChainToggle } from './ChainToggle'
import { StakeCard } from './StakeCard'
import { PositionsPanel } from './PositionsPanel'
import { UnstakeModal } from './UnstakeModal'
import {
useStake,
useUnstake,
useClaim,
getStakingErrorMessage,
type StakeChain,
type SolPosition,
} from '@features/staking'
import { CHAIN_META } from '../model/meta'
import styles from './StakingWidget.module.css'
type ModalState = { kind: 'eth'; amount: string } | { kind: 'sol'; position: SolPosition } | null
type Toast = { status: 'success' | 'error' | 'info'; message: string } | null
export function StakingWidget() {
const [chain, setChain] = useState<StakeChain>('ETH')
const [amount, setAmount] = useState('')
const [modal, setModal] = useState<ModalState>(null)
const [toast, setToast] = useState<Toast>(null)
const stakeM = useStake()
const unstakeM = useUnstake()
const claimM = useClaim()
function handleChain(next: StakeChain) {
setChain(next)
setAmount('')
}
function handleStake() {
stakeM.mutate(
{ chain, amountHuman: amount },
{
onSuccess: () => {
setToast({ status: 'success', message: `Стейкинг ${amount} ${CHAIN_META[chain].symbol} выполнен` })
setAmount('')
},
onError: (e) => setToast({ status: 'error', message: getStakingErrorMessage(e) }),
}
)
}
function handleConfirm(mode?: 'swap' | 'queue') {
if (modal?.kind === 'eth') {
const selected = mode ?? 'swap'
unstakeM.mutate(
{ chain: 'ETH', amountHuman: modal.amount, mode: selected },
{
onSuccess: () => {
setModal(null)
setToast({
status: 'success',
message:
selected === 'queue' ? 'Заявка на вывод добавлена в очередь Lido' : 'Вывод stETH → ETH выполнен',
})
},
onError: (e) => setToast({ status: 'error', message: getStakingErrorMessage(e) }),
}
)
} else if (modal?.kind === 'sol') {
unstakeM.mutate(
{ chain: 'SOL', stakeAccount: modal.position.stakeAccount },
{
onSuccess: (res) => {
setModal(null)
const withdrawn = 'action' in res && res.action === 'withdraw'
setToast({
status: 'success',
message: withdrawn ? 'Средства выведены на кошелёк' : 'Деактивация запущена — вывод через 12 эпохи',
})
},
onError: (e) => setToast({ status: 'error', message: getStakingErrorMessage(e) }),
}
)
}
}
function handleClaim(requestId: string) {
claimM.mutate(requestId, {
onSuccess: () => setToast({ status: 'success', message: 'ETH забран на кошелёк' }),
onError: (e) => setToast({ status: 'error', message: getStakingErrorMessage(e) }),
})
}
return (
<div className={styles.widget}>
<ChainToggle value={chain} onChange={handleChain} />
<div className={styles.grid}>
<StakeCard
chain={chain}
amount={amount}
onAmountChange={setAmount}
onStake={handleStake}
isStaking={stakeM.isPending}
/>
<PositionsPanel
chain={chain}
onUnstakeEth={(amountHuman) => setModal({ kind: 'eth', amount: amountHuman })}
onUnstakeSol={(position) => setModal({ kind: 'sol', position })}
onClaim={handleClaim}
unstakePending={unstakeM.isPending}
claimPending={claimM.isPending}
/>
</div>
{modal?.kind === 'eth' && (
<UnstakeModal
chain="ETH"
symbol="ETH"
amount={modal.amount}
pending={unstakeM.isPending}
onConfirm={handleConfirm}
onClose={() => setModal(null)}
/>
)}
{modal?.kind === 'sol' && (
<UnstakeModal
chain="SOL"
symbol="SOL"
amount={modal.position.lamportsHuman}
solState={modal.position.state}
pending={unstakeM.isPending}
onConfirm={handleConfirm}
onClose={() => setModal(null)}
/>
)}
{toast && <Notification status={toast.status} message={toast.message} onClose={() => setToast(null)} />}
</div>
)
}