feat: tournament API routes

POST /api/tournaments (create, admin-only),
POST /api/tournaments/:id/join, POST /api/tournaments/:id/start,
GET /api/tournaments (list), GET /api/tournaments/:id (bracket).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 20:58:23 +00:00
co-authored by Claude Opus 4.6
parent c0978ba4c8
commit b70c49075e
2 changed files with 100 additions and 0 deletions
+2
View File
@@ -10,6 +10,7 @@ import { authRouter } from './routes/auth.js'
import { docsRouter } from './routes/docs.js'
import { betsRouter } from './routes/bets.js'
import { paymentsRouter } from './routes/payments.js'
import { tournamentsRouter } from './routes/tournaments.js'
import { rateLimit } from './middleware/rate-limit.js'
import { existsSync, readFileSync } from 'fs'
@@ -58,6 +59,7 @@ app.route('/api/queue', queueRouter)
app.route('/api/docs', docsRouter)
app.route('/api/bets', betsRouter)
app.route('/api/payments', paymentsRouter)
app.route('/api/tournaments', tournamentsRouter)
// In production, serve the frontend SPA
const __dirname = dirname(fileURLToPath(import.meta.url))
+98
View File
@@ -0,0 +1,98 @@
import { Hono } from 'hono'
import { db, schema } from '../db/index.js'
import { eq } from 'drizzle-orm'
import {
createTournament,
joinTournament,
startTournament,
getTournamentBracket,
listTournaments,
} from '../engine/tournaments.js'
const CREATOR_PUBKEY = 'da5e0c1b646bdb13c2300f805b0ca3e5afe5b052c594ce78bac8978d21c3fa39'
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) => {
const body = await c.req.json<{
pubkey: string
name: string
format?: 'single_elim' | 'round_robin'
size?: 8 | 16 | 32
entrySats?: number
}>()
if (body.pubkey !== CREATOR_PUBKEY) {
return c.json({ error: 'Only the creator can create tournaments' }, 403)
}
if (!body.name || body.name.length < 1 || body.name.length > 100) {
return c.json({ error: 'Tournament name required (1-100 chars)' }, 400)
}
const format = body.format ?? 'single_elim'
const size = body.size ?? 8
if (![8, 16, 32].includes(size)) {
return c.json({ error: 'Size must be 8, 16, or 32' }, 400)
}
const id = createTournament(body.name, format, size, body.entrySats ?? 0)
return c.json({ id, name: body.name, format, size }, 201)
})
// Join a tournament
tournamentsRouter.post('/:id/join', async (c) => {
const tournamentId = c.req.param('id')
const body = await c.req.json<{ pubkey: string; paymentId?: string }>()
if (!body.pubkey) return c.json({ error: 'pubkey required' }, 400)
// 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) {
const message = err instanceof Error ? err.message : String(err)
return c.json({ error: message }, 400)
}
})
// Start a tournament (admin only)
tournamentsRouter.post('/:id/start', async (c) => {
const tournamentId = c.req.param('id')
const body = await c.req.json<{ pubkey: string }>()
if (body.pubkey !== CREATOR_PUBKEY) {
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) {
const message = err instanceof Error ? err.message : String(err)
return c.json({ error: message }, 400)
}
})