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

224
front_end/src/lib/api.ts Normal file
View File

@@ -0,0 +1,224 @@
import config from '@/config';
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '[::1]']);
const EXPLICIT_BACKEND_PORT = process.env.NEXT_PUBLIC_BACKEND_PORT;
const buildBaseUrl = (protocol: string, hostname: string, port?: string) => {
if (port && port.length > 0) {
return `${protocol}//${hostname}:${port}`;
}
return `${protocol}//${hostname}`;
};
const inferPort = (configuredPort?: string, runtimePort?: string, runtimeProtocol?: string): string | undefined => {
if (EXPLICIT_BACKEND_PORT && EXPLICIT_BACKEND_PORT.length > 0) {
return EXPLICIT_BACKEND_PORT;
}
if (configuredPort && configuredPort.length > 0) {
return configuredPort;
}
if (runtimePort && runtimePort.length > 0) {
return runtimePort === '3001' ? '3000' : runtimePort;
}
if (runtimeProtocol === 'https:') {
return undefined;
}
return undefined;
};
const resolveBrowserBaseUrl = (staticBaseUrl: string) => {
if (typeof window === 'undefined') {
return staticBaseUrl;
}
const { protocol, hostname, port } = window.location;
if (!staticBaseUrl || staticBaseUrl.length === 0) {
const inferredPort = inferPort(undefined, port, protocol);
return buildBaseUrl(protocol, hostname, inferredPort);
}
try {
const parsed = new URL(staticBaseUrl);
const shouldSwapHost = LOOPBACK_HOSTS.has(parsed.hostname) && !LOOPBACK_HOSTS.has(hostname);
if (shouldSwapHost) {
const inferredPort = inferPort(parsed.port, port, parsed.protocol);
return buildBaseUrl(parsed.protocol, hostname, inferredPort);
}
return staticBaseUrl;
} catch {
return staticBaseUrl;
}
};
interface ApiOptions {
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
headers?: Record<string, string>;
body?: any;
auth?: boolean;
}
class ApiClient {
private baseUrl: string;
private cachedBrowserBaseUrl: string | null = null;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private getBaseUrl(): string {
if (typeof window === 'undefined') {
return this.baseUrl || 'http://localhost:3000';
}
if (this.cachedBrowserBaseUrl) {
return this.cachedBrowserBaseUrl;
}
const resolved = resolveBrowserBaseUrl(this.baseUrl || 'http://localhost:3000');
this.cachedBrowserBaseUrl = resolved;
return resolved;
}
private getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_token');
}
async request<T = any>(path: string, options: ApiOptions = {}): Promise<T> {
const { method = 'GET', headers = {}, body, auth = false } = options;
const url = `${this.getBaseUrl()}${path}`;
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...headers,
};
if (auth) {
const token = this.getAuthToken();
if (token) {
requestHeaders['Authorization'] = `Bearer ${token}`;
}
}
const requestOptions: RequestInit = {
method,
headers: requestHeaders,
};
if (body && method !== 'GET') {
requestOptions.body = JSON.stringify(body);
}
try {
const response = await fetch(url, requestOptions);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'API request failed');
}
return data;
} catch (error: any) {
console.error('API request error:', error);
throw error;
}
}
// Public endpoints
async getNextJackpot() {
return this.request('/jackpot/next');
}
async buyTickets(lightningAddress: string, tickets: number, buyerName?: string) {
return this.request('/jackpot/buy', {
method: 'POST',
body: { lightning_address: lightningAddress, tickets, name: buyerName },
auth: true,
});
}
async getTicketStatus(ticketPurchaseId: string) {
return this.request(`/tickets/${ticketPurchaseId}`);
}
async getPastWins(limit = 25, offset = 0) {
const params = new URLSearchParams({
limit: String(limit),
offset: String(offset),
});
return this.request(`/jackpot/past-wins?${params.toString()}`);
}
// Auth endpoints
async nostrAuth(nostrPubkey: string, signedMessage: string, nonce: string) {
return this.request('/auth/nostr', {
method: 'POST',
body: { nostr_pubkey: nostrPubkey, signed_message: signedMessage, nonce },
});
}
// User endpoints
async getProfile() {
return this.request('/me', { auth: true });
}
async updateLightningAddress(lightningAddress: string) {
return this.request('/me/lightning-address', {
method: 'PATCH',
body: { lightning_address: lightningAddress },
auth: true,
});
}
async getUserTickets(limit = 50, offset = 0) {
return this.request(`/me/tickets?limit=${limit}&offset=${offset}`, { auth: true });
}
async getUserWins() {
return this.request('/me/wins', { auth: true });
}
// Admin endpoints
async listCycles(status?: string, cycleType?: string) {
const params = new URLSearchParams();
if (status) params.append('status', status);
if (cycleType) params.append('cycle_type', cycleType);
return this.request(`/admin/cycles?${params.toString()}`, {
headers: { 'X-Admin-Key': process.env.NEXT_PUBLIC_ADMIN_KEY || '' },
});
}
async runDrawManually(cycleId: string) {
return this.request(`/admin/cycles/${cycleId}/run-draw`, {
method: 'POST',
headers: { 'X-Admin-Key': process.env.NEXT_PUBLIC_ADMIN_KEY || '' },
});
}
async listPayouts(status?: string) {
const params = status ? `?status=${status}` : '';
return this.request(`/admin/payouts${params}`, {
headers: { 'X-Admin-Key': process.env.NEXT_PUBLIC_ADMIN_KEY || '' },
});
}
async retryPayout(payoutId: string) {
return this.request(`/admin/payouts/${payoutId}/retry`, {
method: 'POST',
headers: { 'X-Admin-Key': process.env.NEXT_PUBLIC_ADMIN_KEY || '' },
});
}
}
export const api = new ApiClient(config.apiBaseUrl);
export default api;