/** * LNbits API client for Lightning Network payments * * Uses LNbits API to create and manage Lightning invoices with webhook support * for payment detection. */ // Read environment variables dynamically to ensure dotenv has loaded function getConfig() { return { url: process.env.LNBITS_URL || '', apiKey: process.env.LNBITS_API_KEY || '', // Invoice/read key webhookSecret: process.env.LNBITS_WEBHOOK_SECRET || '', }; } export interface LNbitsInvoice { paymentHash: string; paymentRequest: string; // BOLT11 invoice string checkingId: string; amount: number; // Amount in satoshis (after conversion) fiatAmount?: number; // Original fiat amount fiatCurrency?: string; // Original fiat currency memo: string; expiry: string; status: string; extra?: Record; } export interface CreateInvoiceParams { amount: number; // Amount in the specified unit unit?: string; // Currency unit: 'sat', 'USD', 'PYG', etc. (default: 'sat') memo: string; webhookUrl?: string; expiry?: number; // Expiry in seconds (default 3600 = 1 hour) extra?: Record; // Additional metadata } // How long a booking's Lightning invoice is valid for, in seconds. Shared // between initial booking creation and invoice regeneration so both produce // invoices with the same lifetime. export const LNBITS_INVOICE_EXPIRY_SECONDS = 900; // 15 minutes /** * Check if LNbits is configured */ export function isLNbitsConfigured(): boolean { const config = getConfig(); return !!(config.url && config.apiKey); } /** * Create a Lightning invoice using LNbits * LNbits supports fiat currencies directly - it will convert to sats automatically */ export async function createInvoice(params: CreateInvoiceParams): Promise { const config = getConfig(); if (!config.url || !config.apiKey) { throw new Error('LNbits is not configured. Please set LNBITS_URL and LNBITS_API_KEY.'); } const apiEndpoint = '/api/v1/payments'; // LNbits supports fiat currencies via the 'unit' parameter // It will automatically convert to sats using its exchange rate provider const payload: any = { out: false, // false = create invoice for receiving payment amount: params.amount, unit: params.unit || 'sat', // Support fiat currencies like 'USD', 'PYG', etc. memo: params.memo, }; if (params.webhookUrl) { payload.webhook = params.webhookUrl; } if (params.expiry) { payload.expiry = params.expiry; } if (params.extra) { payload.extra = params.extra; } console.log('Creating LNbits invoice:', { amount: params.amount, unit: payload.unit, }); const response = await fetch(`${config.url}${apiEndpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Api-Key': config.apiKey, }, body: JSON.stringify(payload), }); if (!response.ok) { const errorText = await response.text(); console.error('LNbits invoice creation failed:', { status: response.status, statusText: response.statusText, error: errorText, }); throw new Error(`Failed to create Lightning invoice: ${errorText}`); } const data = await response.json(); // LNbits returns amount in millisatoshis for the actual invoice // Convert to satoshis for display const amountSats = data.amount ? Math.round(data.amount / 1000) : params.amount; console.log('LNbits invoice created successfully:', { paymentHash: data.payment_hash, amountMsats: data.amount, amountSats, hasPaymentRequest: !!data.payment_request, }); return { paymentHash: data.payment_hash, paymentRequest: data.payment_request || data.bolt11, checkingId: data.checking_id, amount: amountSats, // Amount in satoshis fiatAmount: params.unit && params.unit !== 'sat' ? params.amount : undefined, fiatCurrency: params.unit && params.unit !== 'sat' ? params.unit : undefined, memo: params.memo, expiry: data.expiry || '', status: data.status || 'pending', extra: params.extra, }; } /** * Get invoice/payment status from LNbits */ export async function getPaymentStatus(paymentHash: string): Promise<{ paid: boolean; status: string; preimage?: string; } | null> { const config = getConfig(); if (!config.url || !config.apiKey) { throw new Error('LNbits is not configured'); } const apiEndpoint = `/api/v1/payments/${paymentHash}`; const response = await fetch(`${config.url}${apiEndpoint}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Api-Key': config.apiKey, }, }); if (!response.ok) { if (response.status === 404) { return null; } throw new Error('Failed to get payment status from LNbits'); } const data = await response.json(); // LNbits payment status: "pending", "complete", "failed" // For incoming payments, "complete" means paid const isPaid = data.status === 'complete' || data.paid === true; return { paid: isPaid, status: data.status, preimage: data.preimage, }; } /** * Verify webhook payload from LNbits * LNbits webhooks don't have signature verification by default, * but we can verify the payment hash matches and check payment status */ export async function verifyWebhookPayment(paymentHash: string): Promise { try { const status = await getPaymentStatus(paymentHash); return status?.paid === true; } catch (error) { console.error('Error verifying webhook payment:', error); return false; } } /** * Payment status types */ export type LNbitsPaymentStatus = 'pending' | 'complete' | 'failed' | 'expired'; /** * Check if payment is complete */ export function isPaymentComplete(status: string): boolean { return status === 'complete'; } /** * Check if payment is still pending */ export function isPaymentPending(status: string): boolean { return status === 'pending'; }