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:
191
back_end/src/controllers/admin.ts
Normal file
191
back_end/src/controllers/admin.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user