Initial commit: Lightning Lottery - Bitcoin Lightning Network powered lottery

Features:
- Lightning Network payments via LNbits integration
- Provably fair draws using CSPRNG
- Random ticket number generation
- Automatic payouts with retry/redraw logic
- Nostr authentication (NIP-07)
- Multiple draw cycles (hourly, daily, weekly, monthly)
- PostgreSQL and SQLite database support
- Real-time countdown and payment animations
- Swagger API documentation
- Docker support

Stack:
- Backend: Node.js, TypeScript, Express
- Frontend: Next.js, React, TailwindCSS, Redux
- Payments: LNbits
This commit is contained in:
Michilis
2025-11-27 22:13:37 +00:00
commit d3bf8080b6
75 changed files with 18184 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
import { Request, Response } from 'express';
import { db } from '../database';
import { executeDraw } from '../services/draw';
import { retryPayoutService } from '../services/payout';
import { JackpotCycle, Payout } from '../types';
/**
* GET /admin/cycles
* List all cycles with optional filters
*/
export async function listCycles(req: Request, res: Response) {
try {
const { status, cycle_type, limit = 50, offset = 0 } = req.query;
let query = `SELECT * FROM jackpot_cycles WHERE 1=1`;
const params: any[] = [];
let paramCount = 0;
if (status) {
paramCount++;
query += ` AND status = $${paramCount}`;
params.push(status);
}
if (cycle_type) {
paramCount++;
query += ` AND cycle_type = $${paramCount}`;
params.push(cycle_type);
}
query += ` ORDER BY scheduled_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
params.push(limit, offset);
const result = await db.query<JackpotCycle>(query, params);
const cycles = result.rows.map(c => ({
id: c.id,
lottery_id: c.lottery_id,
cycle_type: c.cycle_type,
sequence_number: c.sequence_number,
scheduled_at: c.scheduled_at.toISOString(),
status: c.status,
pot_total_sats: parseInt(c.pot_total_sats.toString()),
pot_after_fee_sats: c.pot_after_fee_sats ? parseInt(c.pot_after_fee_sats.toString()) : null,
winning_ticket_id: c.winning_ticket_id,
winning_lightning_address: c.winning_lightning_address,
}));
return res.json({
version: '1.0',
data: {
cycles,
total: cycles.length,
},
});
} catch (error: any) {
console.error('List cycles error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to list cycles',
});
}
}
/**
* POST /admin/cycles/:id/run-draw
* Manually trigger draw execution
*/
export async function runDrawManually(req: Request, res: Response) {
try {
const { id } = req.params;
const result = await executeDraw(id);
if (!result.success) {
return res.status(400).json({
version: '1.0',
error: result.error || 'DRAW_FAILED',
message: result.message || 'Failed to execute draw',
});
}
return res.json({
version: '1.0',
data: {
cycle_id: id,
winning_ticket_id: result.winningTicketId,
pot_after_fee_sats: result.potAfterFeeSats,
payout_status: result.payoutStatus,
},
});
} catch (error: any) {
console.error('Manual draw error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to run draw',
});
}
}
/**
* GET /admin/payouts
* List payouts with optional filters
*/
export async function listPayouts(req: Request, res: Response) {
try {
const { status, limit = 50, offset = 0 } = req.query;
let query = `SELECT * FROM payouts WHERE 1=1`;
const params: any[] = [];
let paramCount = 0;
if (status) {
paramCount++;
query += ` AND status = $${paramCount}`;
params.push(status);
}
query += ` ORDER BY created_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
params.push(limit, offset);
const result = await db.query<Payout>(query, params);
const payouts = result.rows.map(p => ({
id: p.id,
lottery_id: p.lottery_id,
cycle_id: p.cycle_id,
ticket_id: p.ticket_id,
lightning_address: p.lightning_address,
amount_sats: parseInt(p.amount_sats.toString()),
status: p.status,
error_message: p.error_message,
retry_count: p.retry_count,
created_at: p.created_at.toISOString(),
}));
return res.json({
version: '1.0',
data: {
payouts,
total: payouts.length,
},
});
} catch (error: any) {
console.error('List payouts error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to list payouts',
});
}
}
/**
* POST /admin/payouts/:id/retry
* Retry a failed payout
*/
export async function retryPayout(req: Request, res: Response) {
try {
const { id } = req.params;
const result = await retryPayoutService(id);
if (!result.success) {
return res.status(400).json({
version: '1.0',
error: result.error || 'RETRY_FAILED',
message: result.message || 'Failed to retry payout',
});
}
return res.json({
version: '1.0',
data: {
payout_id: id,
status: result.status,
retry_count: result.retryCount,
},
});
} catch (error: any) {
console.error('Retry payout error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to retry payout',
});
}
}