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:
130
back_end/src/routes/admin.ts
Normal file
130
back_end/src/routes/admin.ts
Normal 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;
|
||||
|
||||
191
back_end/src/routes/public.ts
Normal file
191
back_end/src/routes/public.ts
Normal 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
152
back_end/src/routes/user.ts
Normal 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;
|
||||
|
||||
44
back_end/src/routes/webhooks.ts
Normal file
44
back_end/src/routes/webhooks.ts
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user