// package.json deps: express, better-sqlite3, crypto, node-cron import express from "express"; import Database from "better-sqlite3"; import crypto from "crypto"; import cron from "node-cron"; const app = express(); app.use(express.json()); const db = new Database("lotto.db"); // Initialize tables db.exec(` CREATE TABLE IF NOT EXISTS rounds ( id INTEGER PRIMARY KEY, status TEXT NOT NULL, ticket_price INTEGER NOT NULL, max_tickets_per_user INTEGER, cap INTEGER, open_at TEXT, close_at TEXT, server_seed_commit TEXT, server_seed_reveal TEXT, external_entropy_ref TEXT, external_entropy_value TEXT, winner_entry_id INTEGER, jackpot_amount INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY, round_id INTEGER NOT NULL, player_id TEXT NOT NULL, tickets INTEGER NOT NULL, amount INTEGER NOT NULL, source_ref TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS receipts ( id INTEGER PRIMARY KEY, round_id INTEGER NOT NULL, player_id TEXT NOT NULL, amount INTEGER NOT NULL, direction TEXT NOT NULL, external_tx_id TEXT, status TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY, type TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ); `); // Helpers const sha256 = (buf) => crypto.createHash("sha256").update(buf).digest("hex"); const hmac = (key, msg) => crypto.createHmac("sha256", key).update(msg).digest("hex"); // Open a new round app.post("/rounds", (req, res) => { const { ticket_price, max_tickets_per_user, cap, close_at, external_entropy_ref } = req.body; const serverSeed = crypto.randomBytes(32).toString("hex"); const commit = sha256(serverSeed); const stmt = db.prepare(` INSERT INTO rounds (status, ticket_price, max_tickets_per_user, cap, open_at, close_at, server_seed_commit, external_entropy_ref) VALUES ('open', @ticket_price, @max_tickets_per_user, @cap, datetime('now'), @close_at, @commit, @external_entropy_ref) `); const info = stmt.run({ ticket_price, max_tickets_per_user, cap, close_at, commit: commit, external_entropy_ref }); db.prepare(`INSERT INTO events (type, payload_json) VALUES ('round_open', json_extract(json(?), '$'))`) .run(JSON.stringify({ round_id: info.lastInsertRowid, server_seed_commit: commit })); res.json({ round_id: info.lastInsertRowid, server_seed_commit: commit }); }); // Ingest entry (adapter will call this after validating payment) app.post("/rounds/:id/enter", (req, res) => { const round = db.prepare(`SELECT * FROM rounds WHERE id = ?`).get(req.params.id); if (!round || round.status !== "open") return res.status(400).json({ error: "round not open" }); const { player_id, amount, source_ref } = req.body; if (!player_id || !amount || !source_ref) return res.status(400).json({ error: "missing fields" }); const tickets = Math.floor(amount / round.ticket_price); if (tickets < 1) return res.status(400).json({ error: "insufficient amount" }); if (round.cap) { const sold = db.prepare(`SELECT COALESCE(SUM(tickets),0) as t FROM entries WHERE round_id = ?`).get(round.id).t; if (sold + tickets > round.cap) return res.status(400).json({ error: "cap reached" }); } if (round.max_tickets_per_user) { const userTickets = db.prepare(`SELECT COALESCE(SUM(tickets),0) as t FROM entries WHERE round_id = ? AND player_id = ?`).get(round.id, player_id).t; if (userTickets + tickets > round.max_tickets_per_user) return res.status(400).json({ error: "user ticket limit" }); } const info = db.prepare(`INSERT INTO entries (round_id, player_id, tickets, amount, source_ref) VALUES (?,?,?,?,?)`) .run(round.id, player_id, tickets, amount, source_ref); db.prepare(`UPDATE rounds SET jackpot_amount = jackpot_amount + ? WHERE id = ?`).run(amount, round.id); db.prepare(`INSERT INTO events (type, payload_json) VALUES ('entry', json(?))`) .run(JSON.stringify({ round_id: round.id, entry_id: info.lastInsertRowid, player_id, tickets, source_ref })); res.json({ entry_id: info.lastInsertRowid, tickets }); }); // Close a round app.post("/rounds/:id/close", (req, res) => { const round = db.prepare(`SELECT * FROM rounds WHERE id = ?`).get(req.params.id); if (!round || round.status !== "open") return res.status(400).json({ error: "round not open" }); db.prepare(`UPDATE rounds SET status = 'drawing' WHERE id = ?`).run(round.id); res.json({ ok: true }); }); // Set external entropy (e.g., a future block hash) app.post("/rounds/:id/entropy", (req, res) => { const { external_entropy_value } = req.body; const round = db.prepare(`SELECT * FROM rounds WHERE id = ?`).get(req.params.id); if (!round || round.status !== "drawing") return res.status(400).json({ error: "round not drawing" }); if (!external_entropy_value || external_entropy_value.length < 16) return res.status(400).json({ error: "bad entropy" }); // Reveal server seed from secure storage; here we (for demo) load it from an env map // In production, store seeds encrypted and mapped by commit. const serverSeed = process.env[`SEED_${round.server_seed_commit}`]; if (!serverSeed) return res.status(500).json({ error: "server seed not found for commit" }); const commitCheck = sha256(serverSeed); if (commitCheck !== round.server_seed_commit) return res.status(500).json({ error: "seed mismatch" }); const totalTickets = db.prepare(`SELECT COALESCE(SUM(tickets),0) as t FROM entries WHERE round_id = ?`).get(round.id).t; if (totalTickets < 1) return res.status(400).json({ error: "no tickets" }); const digestHex = hmac(serverSeed, external_entropy_value); const big = BigInt("0x" + digestHex); const winnerIndex = Number(big % BigInt(totalTickets)); // Map to entry const rows = db.prepare(`SELECT id, player_id, tickets FROM entries WHERE round_id = ? ORDER BY id ASC`).all(round.id); let acc = 0, winnerEntryId = null, winnerPlayer = null; for (const r of rows) { const start = acc; const end = acc + r.tickets - 1; if (winnerIndex >= start && winnerIndex <= end) { winnerEntryId = r.id; winnerPlayer = r.player_id; break; } acc += r.tickets; } db.prepare(`UPDATE rounds SET status='closed', server_seed_reveal=?, external_entropy_value=?, winner_entry_id=? WHERE id=?`) .run(serverSeed, external_entropy_value, winnerEntryId, round.id); db.prepare(`INSERT INTO events (type, payload_json) VALUES ('draw', json(?))`) .run(JSON.stringify({ round_id: round.id, winner_entry_id: winnerEntryId, winner_index: winnerIndex, digestHex })); res.json({ winner_entry_id: winnerEntryId, winner_player_id: winnerPlayer, winner_index: winnerIndex, total_tickets: totalTickets }); }); // Simple public endpoints app.get("/rounds/:id", (req, res) => { const round = db.prepare(`SELECT * FROM rounds WHERE id = ?`).get(req.params.id); const entries = db.prepare(`SELECT id, player_id, tickets, amount, created_at FROM entries WHERE round_id = ? ORDER BY id`).all(req.params.id); res.json({ round, entries }); }); app.listen(8080, () => console.log("lotto core running on :8080"));