2026-03-08 20:58:23 +00:00
|
|
|
import { Hono } from 'hono'
|
|
|
|
|
import { db, schema } from '../db/index.js'
|
|
|
|
|
import { eq } from 'drizzle-orm'
|
2026-03-08 21:44:17 +00:00
|
|
|
import { toError } from '../lib/utils.js'
|
2026-03-09 15:31:58 +00:00
|
|
|
import { isCreatorPubkey } from '../lib/constants.js'
|
2026-04-11 19:46:37 +01:00
|
|
|
import { extractPubkeyFromAuth } from '../middleware/jwt.js'
|
2026-03-08 20:58:23 +00:00
|
|
|
import {
|
|
|
|
|
createTournament,
|
|
|
|
|
joinTournament,
|
|
|
|
|
startTournament,
|
|
|
|
|
getTournamentBracket,
|
|
|
|
|
listTournaments,
|
|
|
|
|
} from '../engine/tournaments.js'
|
2026-03-13 09:16:04 +00:00
|
|
|
import { createTournamentSchema, joinTournamentSchema, startTournamentSchema, formatZodError } from '../lib/validators.js'
|
2026-03-08 20:58:23 +00:00
|
|
|
|
|
|
|
|
export const tournamentsRouter = new Hono()
|
|
|
|
|
|
|
|
|
|
// List tournaments (optionally filter by status)
|
|
|
|
|
tournamentsRouter.get('/', async (c) => {
|
|
|
|
|
const status = c.req.query('status') as 'open' | 'active' | 'finished' | undefined
|
|
|
|
|
const tournaments = listTournaments(status)
|
|
|
|
|
return c.json({ tournaments })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Get tournament bracket
|
|
|
|
|
tournamentsRouter.get('/:id', async (c) => {
|
|
|
|
|
const id = c.req.param('id')
|
|
|
|
|
const bracket = getTournamentBracket(id)
|
|
|
|
|
if (!bracket) return c.json({ error: 'Tournament not found' }, 404)
|
|
|
|
|
return c.json(bracket)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create a tournament (admin only — creator pubkey required)
|
|
|
|
|
tournamentsRouter.post('/', async (c) => {
|
2026-03-13 09:16:04 +00:00
|
|
|
const parsed = createTournamentSchema.safeParse(await c.req.json().catch(() => ({})))
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return c.json({ error: formatZodError(parsed.error, {
|
|
|
|
|
name: 'Tournament name required (1-100 chars)',
|
|
|
|
|
size: 'Size must be 8, 16, or 32',
|
|
|
|
|
}, 'Invalid tournament data') }, 400)
|
|
|
|
|
}
|
|
|
|
|
const body = parsed.data
|
2026-03-08 20:58:23 +00:00
|
|
|
|
2026-03-09 15:31:58 +00:00
|
|
|
if (!isCreatorPubkey(body.pubkey)) {
|
2026-03-08 20:58:23 +00:00
|
|
|
return c.json({ error: 'Only the creator can create tournaments' }, 403)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const format = body.format ?? 'single_elim'
|
|
|
|
|
const size = body.size ?? 8
|
|
|
|
|
|
|
|
|
|
const id = createTournament(body.name, format, size, body.entrySats ?? 0)
|
|
|
|
|
return c.json({ id, name: body.name, format, size }, 201)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-11 19:46:37 +01:00
|
|
|
// Join a tournament (requires JWT auth to prove pubkey ownership)
|
2026-03-08 20:58:23 +00:00
|
|
|
tournamentsRouter.post('/:id/join', async (c) => {
|
|
|
|
|
const tournamentId = c.req.param('id')
|
2026-03-13 09:16:04 +00:00
|
|
|
const parsed = joinTournamentSchema.safeParse(await c.req.json().catch(() => ({})))
|
|
|
|
|
if (!parsed.success) return c.json({ error: formatZodError(parsed.error, { pubkey: 'pubkey required' }, 'pubkey required') }, 400)
|
|
|
|
|
const body = parsed.data
|
2026-03-08 20:58:23 +00:00
|
|
|
|
2026-04-11 19:46:37 +01:00
|
|
|
// Verify caller owns the pubkey via JWT (prevents joining on behalf of others)
|
|
|
|
|
const authedPubkey = extractPubkeyFromAuth(c.req.header('Authorization'))
|
|
|
|
|
if (authedPubkey && authedPubkey !== body.pubkey) {
|
|
|
|
|
return c.json({ error: 'Pubkey does not match authenticated session' }, 403)
|
|
|
|
|
}
|
|
|
|
|
if (!authedPubkey && process.env.NODE_ENV === 'production') {
|
|
|
|
|
return c.json({ error: 'Authentication required' }, 401)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:58:23 +00:00
|
|
|
// Look up bot by pubkey
|
|
|
|
|
const bot = db.select().from(schema.bots)
|
|
|
|
|
.where(eq(schema.bots.publicKey, body.pubkey))
|
|
|
|
|
.get()
|
|
|
|
|
if (!bot) return c.json({ error: 'Bot not found for this pubkey' }, 404)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const entryId = joinTournament(tournamentId, bot.id, body.paymentId)
|
|
|
|
|
return c.json({ entryId, botId: bot.id })
|
|
|
|
|
} catch (err) {
|
2026-03-08 21:44:17 +00:00
|
|
|
const message = toError(err).message
|
2026-03-08 20:58:23 +00:00
|
|
|
return c.json({ error: message }, 400)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Start a tournament (admin only)
|
|
|
|
|
tournamentsRouter.post('/:id/start', async (c) => {
|
|
|
|
|
const tournamentId = c.req.param('id')
|
2026-03-13 09:16:04 +00:00
|
|
|
const parsed = startTournamentSchema.safeParse(await c.req.json().catch(() => ({})))
|
|
|
|
|
if (!parsed.success) return c.json({ error: formatZodError(parsed.error, { pubkey: 'pubkey required' }, 'pubkey required') }, 400)
|
|
|
|
|
const body = parsed.data
|
2026-03-08 20:58:23 +00:00
|
|
|
|
2026-03-09 15:31:58 +00:00
|
|
|
if (!isCreatorPubkey(body.pubkey)) {
|
2026-03-08 20:58:23 +00:00
|
|
|
return c.json({ error: 'Only the creator can start tournaments' }, 403)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
startTournament(tournamentId)
|
|
|
|
|
const bracket = getTournamentBracket(tournamentId)
|
|
|
|
|
return c.json({ status: 'active', bracket })
|
|
|
|
|
} catch (err) {
|
2026-03-08 21:44:17 +00:00
|
|
|
const message = toError(err).message
|
2026-03-08 20:58:23 +00:00
|
|
|
return c.json({ error: message }, 400)
|
|
|
|
|
}
|
|
|
|
|
})
|