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,130 @@
import { Router } from 'express';
import {
listCycles,
runDrawManually,
retryPayout,
listPayouts
} from '../controllers/admin';
import { verifyAdmin } from '../middleware/auth';
const router = Router();
// All admin routes require admin key
router.use(verifyAdmin);
/**
* @swagger
* /admin/cycles:
* get:
* summary: List all jackpot cycles
* tags: [Admin]
* security:
* - adminKey: []
* parameters:
* - in: query
* name: status
* schema:
* type: string
* description: Filter by status
* - in: query
* name: cycle_type
* schema:
* type: string
* description: Filter by cycle type
* - in: query
* name: limit
* schema:
* type: integer
* default: 50
* - in: query
* name: offset
* schema:
* type: integer
* default: 0
* responses:
* 200:
* description: List of cycles
* 403:
* description: Invalid admin key
*/
router.get('/cycles', listCycles);
/**
* @swagger
* /admin/cycles/{id}/run-draw:
* post:
* summary: Manually trigger a draw for a cycle
* tags: [Admin]
* security:
* - adminKey: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: Draw executed successfully
* 400:
* description: Draw failed or invalid cycle
* 403:
* description: Invalid admin key
*/
router.post('/cycles/:id/run-draw', runDrawManually);
/**
* @swagger
* /admin/payouts:
* get:
* summary: List all payouts
* tags: [Admin]
* security:
* - adminKey: []
* parameters:
* - in: query
* name: status
* schema:
* type: string
* description: Filter by status
* - in: query
* name: limit
* schema:
* type: integer
* default: 50
* responses:
* 200:
* description: List of payouts
* 403:
* description: Invalid admin key
*/
router.get('/payouts', listPayouts);
/**
* @swagger
* /admin/payouts/{id}/retry:
* post:
* summary: Retry a failed payout
* tags: [Admin]
* security:
* - adminKey: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: Payout retry successful
* 400:
* description: Retry failed or invalid payout
* 403:
* description: Invalid admin key
*/
router.post('/payouts/:id/retry', retryPayout);
export default router;

View File

@@ -0,0 +1,191 @@
import { Router } from 'express';
import {
getNextJackpot,
buyTickets,
getTicketStatus,
getPastWins
} from '../controllers/public';
import { buyRateLimiter, ticketStatusRateLimiter } from '../middleware/rateLimit';
import { optionalAuth } from '../middleware/auth';
const router = Router();
/**
* @swagger
* /jackpot/next:
* get:
* summary: Get next upcoming jackpot cycle
* tags: [Public]
* responses:
* 200:
* description: Next jackpot cycle information
* content:
* application/json:
* schema:
* type: object
* properties:
* version:
* type: string
* example: "1.0"
* data:
* type: object
* properties:
* lottery:
* $ref: '#/components/schemas/Lottery'
* cycle:
* $ref: '#/components/schemas/JackpotCycle'
* 503:
* description: No active lottery or cycle available
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.get('/jackpot/next', getNextJackpot);
/**
* @swagger
* /jackpot/buy:
* post:
* summary: Purchase lottery tickets
* tags: [Public]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - tickets
* - lightning_address
* properties:
* tickets:
* type: integer
* minimum: 1
* maximum: 100
* description: Number of tickets to purchase
* example: 5
* lightning_address:
* type: string
* description: Lightning Address for receiving payouts
* example: "user@getalby.com"
* nostr_pubkey:
* type: string
* description: Optional Nostr public key
* example: "npub1..."
* responses:
* 200:
* description: Invoice created successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* version:
* type: string
* data:
* type: object
* properties:
* ticket_purchase_id:
* type: string
* format: uuid
* public_url:
* type: string
* invoice:
* type: object
* properties:
* payment_request:
* type: string
* description: BOLT11 invoice
* amount_sats:
* type: integer
* 400:
* description: Invalid input
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.post('/jackpot/buy', buyRateLimiter, optionalAuth, buyTickets);
/**
* @swagger
* /jackpot/past-wins:
* get:
* summary: List recent jackpot winners
* tags: [Public]
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* - in: query
* name: offset
* schema:
* type: integer
* default: 0
* responses:
* 200:
* description: List of completed jackpots and their winners
* 500:
* description: Failed to load past wins
*/
router.get('/jackpot/past-wins', getPastWins);
/**
* @swagger
* /tickets/{id}:
* get:
* summary: Get ticket purchase status
* tags: [Public]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* description: Ticket purchase ID
* responses:
* 200:
* description: Ticket status retrieved successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* version:
* type: string
* data:
* type: object
* properties:
* purchase:
* $ref: '#/components/schemas/TicketPurchase'
* tickets:
* type: array
* items:
* $ref: '#/components/schemas/Ticket'
* cycle:
* $ref: '#/components/schemas/JackpotCycle'
* result:
* type: object
* properties:
* has_drawn:
* type: boolean
* is_winner:
* type: boolean
* payout:
* type: object
* nullable: true
* 404:
* description: Ticket purchase not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.get('/tickets/:id', ticketStatusRateLimiter, getTicketStatus);
export default router;

152
back_end/src/routes/user.ts Normal file
View File

@@ -0,0 +1,152 @@
import { Router } from 'express';
import {
nostrAuth,
getProfile,
updateLightningAddress,
getUserTickets,
getUserWins
} from '../controllers/user';
import { verifyToken } from '../middleware/auth';
const router = Router();
/**
* @swagger
* /auth/nostr:
* post:
* summary: Authenticate with Nostr
* tags: [User]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - nostr_pubkey
* - signed_message
* - nonce
* properties:
* nostr_pubkey:
* type: string
* description: Nostr public key (hex or npub)
* signed_message:
* type: string
* description: Signature of the nonce
* nonce:
* type: string
* description: Random nonce for signature verification
* responses:
* 200:
* description: Authentication successful
* content:
* application/json:
* schema:
* type: object
* properties:
* version:
* type: string
* data:
* type: object
* properties:
* token:
* type: string
* description: JWT token
* user:
* type: object
* 400:
* description: Invalid public key or signature
*/
router.post('/auth/nostr', nostrAuth);
/**
* @swagger
* /me:
* get:
* summary: Get user profile
* tags: [User]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: User profile and statistics
* 401:
* description: Unauthorized
*/
router.get('/me', verifyToken, getProfile);
/**
* @swagger
* /me/lightning-address:
* patch:
* summary: Update user's Lightning Address
* tags: [User]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - lightning_address
* properties:
* lightning_address:
* type: string
* example: "user@getalby.com"
* responses:
* 200:
* description: Lightning Address updated
* 400:
* description: Invalid Lightning Address
* 401:
* description: Unauthorized
*/
router.patch('/me/lightning-address', verifyToken, updateLightningAddress);
/**
* @swagger
* /me/tickets:
* get:
* summary: Get user's ticket purchases
* tags: [User]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* default: 50
* - in: query
* name: offset
* schema:
* type: integer
* default: 0
* responses:
* 200:
* description: User's ticket purchase history
* 401:
* description: Unauthorized
*/
router.get('/me/tickets', verifyToken, getUserTickets);
/**
* @swagger
* /me/wins:
* get:
* summary: Get user's wins and payouts
* tags: [User]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: User's wins
* 401:
* description: Unauthorized
*/
router.get('/me/wins', verifyToken, getUserWins);
export default router;

View File

@@ -0,0 +1,44 @@
import { Router } from 'express';
import { handleLNbitsPayment } from '../controllers/webhooks';
const router = Router();
/**
* @swagger
* /webhooks/lnbits/payment:
* post:
* summary: LNbits payment webhook callback
* description: LNbits calls this endpoint when a Lightning invoice is paid.
* tags: [Webhooks]
* parameters:
* - in: header
* name: X-Webhook-Secret
* schema:
* type: string
* description: Shared secret configured in LNbits (or supply `secret` query param)
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* payment_hash:
* type: string
* amount:
* type: number
* description: Amount paid in sats
* paid:
* type: boolean
* responses:
* 200:
* description: Webhook processed
* 403:
* description: Invalid secret
* 500:
* description: Internal error processing the webhook
*/
router.post('/lnbits/payment', handleLNbitsPayment);
export default router;