Files
Spanglish/backend/src/lib/lnbits.ts
T
MichilisandCursor a6840ea953 Harden auth, payments, and frontend against review findings.
Close exploitable gaps in booking/payment flows, enforce token versioning and account checks, gate sensitive payment data, and add middleware plus input validation across admin routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 19:59:02 +00:00

210 lines
5.6 KiB
TypeScript

/**
* 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<string, any>;
}
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<string, any>; // Additional metadata
}
/**
* 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<LNbitsInvoice> {
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<boolean> {
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';
}