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,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './index';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -0,0 +1,16 @@
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import jackpotReducer from './jackpotSlice';
import purchaseReducer from './purchaseSlice';
export const store = configureStore({
reducer: {
user: userReducer,
jackpot: jackpotReducer,
purchase: purchaseReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@@ -0,0 +1,45 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Cycle {
id: string;
cycle_type: string;
scheduled_at: string;
pot_total_sats: number;
ticket_price_sats: number;
status: string;
}
interface JackpotState {
cycle: Cycle | null;
loading: boolean;
error: string | null;
}
const initialState: JackpotState = {
cycle: null,
loading: false,
error: null,
};
const jackpotSlice = createSlice({
name: 'jackpot',
initialState,
reducers: {
setCycle: (state, action: PayloadAction<Cycle>) => {
state.cycle = action.payload;
state.loading = false;
state.error = null;
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload;
state.loading = false;
},
},
});
export const { setCycle, setLoading, setError } = jackpotSlice.actions;
export default jackpotSlice.reducer;

View File

@@ -0,0 +1,45 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Ticket {
id: string;
serial_number: number;
is_winning_ticket: boolean;
}
interface PurchaseState {
ticket_purchase_id: string | null;
invoice_status: string | null;
tickets: Ticket[];
loading: boolean;
error: string | null;
}
const initialState: PurchaseState = {
ticket_purchase_id: null,
invoice_status: null,
tickets: [],
loading: false,
error: null,
};
const purchaseSlice = createSlice({
name: 'purchase',
initialState,
reducers: {
setPurchase: (state, action: PayloadAction<Partial<PurchaseState>>) => {
return { ...state, ...action.payload, loading: false, error: null };
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload;
state.loading = false;
},
clearPurchase: () => initialState,
},
});
export const { setPurchase, setLoading, setError, clearPurchase } = purchaseSlice.actions;
export default purchaseSlice.reducer;

View File

@@ -0,0 +1,32 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface UserState {
authenticated: boolean;
pubkey: string | null;
lightning_address: string | null;
token: string | null;
displayName: string | null;
}
const initialState: UserState = {
authenticated: false,
pubkey: null,
lightning_address: null,
token: null,
displayName: null,
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action: PayloadAction<Partial<UserState>>) => {
return { ...state, ...action.payload, authenticated: true };
},
logout: () => initialState,
},
});
export const { setUser, logout } = userSlice.actions;
export default userSlice.reducer;