first commit
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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:', {
|
||||
url: `${config.url}${apiEndpoint}`,
|
||||
amount: params.amount,
|
||||
unit: payload.unit,
|
||||
memo: params.memo,
|
||||
webhook: params.webhookUrl,
|
||||
});
|
||||
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user