Files
frontend/src/widgets/staking/ui/StakingWidget.tsx
2026-07-01 16:42:37 +03:00

145 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { Notification } from '@shared/ui'
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
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('')
}
// stakeAmountHuman — это сумма стейка A (= введённое газ), её и отправляем на бэкенд.
function handleStake(stakeAmountHuman: string) {
stakeM.mutate(
{ chain, amountHuman: stakeAmountHuman },
{
onSuccess: () => {
setToast({
status: 'success',
message: `Стейкинг ${truncateDecimals(stakeAmountHuman, 6)} ${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)} duration={5000} />}
</div>
)
}