import { Router } from 'express'; import { prisma } from '../db.js'; import { requireAuth, publicUser } from '../middleware.js'; import { maxVaultSlots, settleVaultYield, summarizeVaultState, } from '../vault.js'; import { isConnected } from '../presence.js'; const router = Router(); router.post('/stake', requireAuth, async (req, res) => { try { const ids = [...new Set((req.body.inventoryItemIds || []).map(Number).filter(Boolean))]; if (!ids.length) { return res.status(400).json({ error: 'Select at least one item to stake' }); } const mode = isConnected(req.session.userId) ? 'online' : 'offline'; const result = await prisma.$transaction(async (tx) => { const user = await tx.user.findUnique({ where: { id: req.session.userId } }); if (!user) { const err = new Error('User not found'); err.status = 401; throw err; } const maxSlots = maxVaultSlots(user); const used = await tx.inventoryItem.count({ where: { userId: user.id, staked: true }, }); const free = maxSlots - used; if (ids.length > free) { const err = new Error(`Vault full (${used}/${maxSlots} slots)`); err.status = 400; throw err; } const invItems = await tx.inventoryItem.findMany({ where: { id: { in: ids }, userId: user.id, locked: false, staked: false, }, }); if (invItems.length !== ids.length) { const err = new Error('Invalid, locked, or already staked items'); err.status = 400; throw err; } const now = new Date(); await tx.inventoryItem.updateMany({ where: { id: { in: ids }, userId: user.id }, data: { staked: true, stakedAt: now, lastYieldAt: now, }, }); // Shared timer: start only when vault was empty let updatedUser = user; if (used === 0 || !user.vaultYieldAt) { updatedUser = await tx.user.update({ where: { id: user.id }, data: { vaultYieldAt: now }, }); } const staked = await tx.inventoryItem.findMany({ where: { userId: user.id, staked: true }, include: { item: true }, }); return { user: updatedUser, stakedIds: ids, vault: summarizeVaultState(updatedUser, staked, mode), }; }); res.json({ user: publicUser(result.user), stakedIds: result.stakedIds, vault: result.vault, }); } catch (err) { const status = err.status || 500; if (status >= 500) console.error(err); res.status(status).json({ error: err.message || 'Failed to stake items' }); } }); router.post('/unstake', requireAuth, async (req, res) => { try { const ids = [...new Set((req.body.inventoryItemIds || []).map(Number).filter(Boolean))]; if (!ids.length) { return res.status(400).json({ error: 'Select at least one item to unstake' }); } const mode = isConnected(req.session.userId) ? 'online' : 'offline'; const settled = await settleVaultYield(req.session.userId, mode); const result = await prisma.$transaction(async (tx) => { const user = await tx.user.findUnique({ where: { id: req.session.userId } }); if (!user) { const err = new Error('User not found'); err.status = 401; throw err; } const invItems = await tx.inventoryItem.findMany({ where: { id: { in: ids }, userId: user.id, staked: true, }, }); if (invItems.length !== ids.length) { const err = new Error('Invalid or not-staked items'); err.status = 400; throw err; } await tx.inventoryItem.updateMany({ where: { id: { in: ids }, userId: user.id }, data: { staked: false, stakedAt: null, lastYieldAt: null, }, }); const remaining = await tx.inventoryItem.findMany({ where: { userId: user.id, staked: true }, include: { item: true }, }); let refreshed = user; if (remaining.length === 0 && user.vaultYieldAt) { refreshed = await tx.user.update({ where: { id: user.id }, data: { vaultYieldAt: null }, }); } else { refreshed = await tx.user.findUnique({ where: { id: user.id } }); } return { user: refreshed, unstakedIds: ids, vault: summarizeVaultState(refreshed, remaining, mode), }; }); res.json({ user: publicUser(result.user), unstakedIds: result.unstakedIds, yieldCredited: settled.credited, vault: result.vault, }); } catch (err) { const status = err.status || 500; if (status >= 500) console.error(err); res.status(status).json({ error: err.message || 'Failed to unstake items' }); } }); export default router;