diff --git a/server/src/app.ts b/server/src/app.ts index 5cef627..dd68076 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -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)) diff --git a/server/src/routes/tournaments.ts b/server/src/routes/tournaments.ts new file mode 100644 index 0000000..f3baf04 --- /dev/null +++ b/server/src/routes/tournaments.ts @@ -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) + } +})