- Enhanced the sendPaymentWithAddress method in BTCPayService and StrikeService to accept an optional comment parameter. - Updated resolveLightningAddress to include the comment in the callback URL if supported by the LNURL-pay endpoint. - Modified PaymentService to construct a descriptive comment for Lightning invoices, improving clarity for users. These changes enhance the payment experience by allowing users to include contextual information with their transactions.
634 lines
21 KiB
TypeScript
634 lines
21 KiB
TypeScript
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
|
import { LightningService } from './lightning.service';
|
|
import { Payment } from '../../entities/payment.entity';
|
|
import { LightningPaymentDTO } from '../../dto/response/lightning-payment.dto';
|
|
import Invoice from '../dto/strike/invoice';
|
|
import InvoiceQuote from '../dto/strike/invoice-quote';
|
|
import axios from 'axios';
|
|
import type {
|
|
BTCPayInvoiceResponse,
|
|
BTCPayPaymentMethod,
|
|
BTCPayLightningPayResponse,
|
|
BTCPayInternalLnInvoice,
|
|
BTCPayCreateLnInvoiceRequest,
|
|
} from '../dto/btcpay/btcpay-invoice';
|
|
|
|
/**
|
|
* BTCPay Server Greenfield API client.
|
|
*
|
|
* Replaces StrikeService as the Lightning payment provider.
|
|
* Handles invoice creation (receiving payments), Lightning payouts
|
|
* (paying creator Lightning addresses), and BTC rate fetching.
|
|
*/
|
|
@Injectable()
|
|
export class BTCPayService implements LightningService {
|
|
private readonly baseUrl: string;
|
|
private readonly storeId: string;
|
|
private readonly apiKey: string;
|
|
private readonly routeHintsEnabled: boolean;
|
|
|
|
constructor() {
|
|
this.baseUrl = (process.env.BTCPAY_URL || process.env.BTCPAY_SERVER_URL || '').replace(/\/+$/, '');
|
|
this.storeId = process.env.BTCPAY_STORE_ID || '';
|
|
this.apiKey = process.env.BTCPAY_API_KEY || '';
|
|
|
|
// Create a separate internal Lightning invoice with privateRouteHints.
|
|
// Only needed when BTCPay's own store-level route hints are NOT enabled.
|
|
// Since BTCPay can include route hints natively (Lightning settings →
|
|
// "Include hop hints"), this defaults to false to avoid duplicating
|
|
// invoices. Set BTCPAY_ROUTE_HINTS=true only if you cannot enable
|
|
// route hints in BTCPay's UI and still need them.
|
|
const envHints = process.env.BTCPAY_ROUTE_HINTS;
|
|
this.routeHintsEnabled = envHints === 'true';
|
|
|
|
if (!this.baseUrl || !this.storeId || !this.apiKey) {
|
|
Logger.warn(
|
|
'BTCPay Server environment variables not fully configured (BTCPAY_URL, BTCPAY_STORE_ID, BTCPAY_API_KEY)',
|
|
'BTCPayService',
|
|
);
|
|
}
|
|
|
|
if (this.routeHintsEnabled) {
|
|
Logger.log('Private route hints enabled for Lightning invoices', 'BTCPayService');
|
|
}
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
private get headers() {
|
|
return {
|
|
Authorization: `token ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
};
|
|
}
|
|
|
|
private storeUrl(path: string): string {
|
|
return `${this.baseUrl}/api/v1/stores/${this.storeId}${path}`;
|
|
}
|
|
|
|
// ── Invoice Creation (receiving payments from users) ─────────────────────
|
|
|
|
/**
|
|
* Create a BTCPay invoice denominated in sats, paid via Lightning.
|
|
* The amount parameter is in satoshis. We convert to BTC for the
|
|
* API call since not all BTCPay versions support "SATS" as a currency.
|
|
*
|
|
* Returns data shaped like Strike's Invoice DTO for drop-in
|
|
* compatibility with the existing RentsService.
|
|
*/
|
|
async issueInvoice(
|
|
amountSats: number,
|
|
description = 'Invoice for order',
|
|
correlationId?: string,
|
|
): Promise<Invoice> {
|
|
try {
|
|
// Convert sats to BTC (1 BTC = 100,000,000 sats)
|
|
const amountBtc = (amountSats / 100_000_000).toFixed(8);
|
|
|
|
const body = {
|
|
amount: amountBtc,
|
|
currency: 'BTC',
|
|
metadata: {
|
|
orderId: correlationId || undefined,
|
|
itemDesc: description,
|
|
amountSats, // Store original sats amount for reference
|
|
},
|
|
checkout: {
|
|
paymentMethods: ['BTC-LN'],
|
|
expirationMinutes: 15,
|
|
monitoringMinutes: 60,
|
|
speedPolicy: 'HighSpeed',
|
|
},
|
|
};
|
|
|
|
const { data } = await axios.post<BTCPayInvoiceResponse>(
|
|
this.storeUrl('/invoices'),
|
|
body,
|
|
{ headers: this.headers },
|
|
);
|
|
|
|
// Map to the Invoice DTO the rest of the codebase expects
|
|
return {
|
|
invoiceId: data.id,
|
|
amount: { amount: data.amount, currency: data.currency },
|
|
description,
|
|
created: new Date(data.createdTime * 1000),
|
|
state: this.mapInvoiceState(data.status),
|
|
correlationId,
|
|
};
|
|
} catch (error) {
|
|
Logger.error('BTCPay invoice creation failed: ' + error.message, 'BTCPayService');
|
|
throw new BadRequestException(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch the Lightning BOLT11 for an existing invoice.
|
|
*
|
|
* When route hints are enabled (default), this creates an internal
|
|
* Lightning invoice with `privateRouteHints: true` so that payers
|
|
* can find a route even when our node only has private channels.
|
|
* The store invoice is kept for webhook-based payment tracking.
|
|
* On subsequent polls, if the internal invoice is paid we mark the
|
|
* store invoice as complete, which triggers the normal webhook flow.
|
|
*
|
|
* Returns data shaped like Strike's InvoiceQuote DTO.
|
|
*/
|
|
async issueQuote(invoiceId: string): Promise<InvoiceQuote> {
|
|
try {
|
|
const invoice = await this.getRawInvoice(invoiceId);
|
|
|
|
if (!this.routeHintsEnabled) {
|
|
return this.issueQuoteFromPaymentMethods(invoiceId, invoice);
|
|
}
|
|
|
|
// ── Route-hints path: use the internal Lightning node API ───────
|
|
const internalLnId = invoice.metadata?.internalLnInvoiceId as
|
|
| string
|
|
| undefined;
|
|
|
|
let bolt11: string;
|
|
let expirationDate: Date;
|
|
let paid = invoice.status === 'Settled';
|
|
|
|
if (internalLnId) {
|
|
// We already created an internal LN invoice — re-fetch it
|
|
const lnInvoice = await this.getInternalLnInvoice(internalLnId);
|
|
bolt11 = lnInvoice.BOLT11;
|
|
expirationDate = new Date(lnInvoice.expiresAt * 1000);
|
|
|
|
if (lnInvoice.status === 'Paid') {
|
|
paid = true;
|
|
// Detect payment on the internal invoice and settle the store invoice
|
|
if (invoice.status !== 'Settled') {
|
|
Logger.log(
|
|
`Internal LN invoice ${internalLnId} paid — marking store invoice ${invoiceId} as complete`,
|
|
'BTCPayService',
|
|
);
|
|
await this.markInvoiceComplete(invoiceId);
|
|
}
|
|
}
|
|
} else {
|
|
// First call — create a new internal LN invoice with route hints
|
|
const amountSats =
|
|
(invoice.metadata?.amountSats as number) ||
|
|
Math.round(parseFloat(invoice.amount) * 100_000_000);
|
|
|
|
// Match expiry to the store invoice so both expire together
|
|
const expirySeconds = Math.max(
|
|
60,
|
|
Math.floor((invoice.expirationTime * 1000 - Date.now()) / 1000),
|
|
);
|
|
|
|
try {
|
|
const lnInvoice = await this.createInternalLnInvoice(
|
|
amountSats * 1000, // BTCPay internal API expects millisats
|
|
(invoice.metadata?.itemDesc as string) || 'Invoice',
|
|
expirySeconds,
|
|
);
|
|
|
|
bolt11 = lnInvoice.BOLT11;
|
|
expirationDate = new Date(lnInvoice.expiresAt * 1000);
|
|
|
|
// Persist the link so subsequent polls re-use this invoice
|
|
await this.updateInvoiceMetadata(invoiceId, {
|
|
...invoice.metadata,
|
|
internalLnInvoiceId: lnInvoice.id,
|
|
});
|
|
|
|
Logger.log(
|
|
`Created internal LN invoice ${lnInvoice.id} with route hints for store invoice ${invoiceId}`,
|
|
'BTCPayService',
|
|
);
|
|
} catch (err) {
|
|
// If the internal Lightning API is unavailable, fall back gracefully
|
|
Logger.warn(
|
|
`Failed to create internal LN invoice with route hints: ${err.message}. Falling back to store payment method BOLT11.`,
|
|
'BTCPayService',
|
|
);
|
|
return this.issueQuoteFromPaymentMethods(invoiceId, invoice);
|
|
}
|
|
}
|
|
|
|
const expirationInSec = Math.max(
|
|
0,
|
|
Math.floor((expirationDate.getTime() - Date.now()) / 1000),
|
|
);
|
|
|
|
return {
|
|
quoteId: invoiceId,
|
|
description: (invoice.metadata?.itemDesc as string) || 'Invoice',
|
|
lnInvoice: bolt11,
|
|
onchainAddress: '',
|
|
expiration: expirationDate,
|
|
expirationInSec,
|
|
paid,
|
|
targetAmount: {
|
|
amount: invoice.amount,
|
|
currency: invoice.currency,
|
|
},
|
|
sourceAmount: {
|
|
amount: invoice.amount,
|
|
currency: 'BTC',
|
|
},
|
|
conversionRate: {
|
|
amount: '1',
|
|
sourceCurrency: 'BTC',
|
|
targetCurrency: invoice.currency,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
Logger.error(
|
|
'BTCPay quote retrieval failed: ' + error.message,
|
|
'BTCPayService',
|
|
);
|
|
throw new BadRequestException(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Original quote logic: fetch the BOLT11 from the store invoice's
|
|
* payment methods. Used as fallback when route hints are disabled
|
|
* or the internal Lightning API is unavailable.
|
|
*/
|
|
private async issueQuoteFromPaymentMethods(
|
|
invoiceId: string,
|
|
invoice: BTCPayInvoiceResponse,
|
|
): Promise<InvoiceQuote> {
|
|
const { data: paymentMethods } = await axios.get<BTCPayPaymentMethod[]>(
|
|
this.storeUrl(`/invoices/${invoiceId}/payment-methods`),
|
|
{ headers: this.headers },
|
|
);
|
|
|
|
// BTCPay's payment method IDs vary between versions (BTC-LN, BTC_LN,
|
|
// BTC-LightningNetwork, etc.). Match any Lightning-related method.
|
|
const lightning = paymentMethods.find((pm) => {
|
|
const id = (pm.paymentMethod || pm.paymentMethodId || '').toUpperCase();
|
|
return (
|
|
id.includes('LN') ||
|
|
id.includes('LIGHTNING') ||
|
|
id === 'BTC-LNURL'
|
|
);
|
|
});
|
|
|
|
if (!lightning) {
|
|
const availableMethods = paymentMethods.map(
|
|
(pm) => pm.paymentMethod || pm.paymentMethodId || 'unknown',
|
|
);
|
|
Logger.warn(
|
|
`No Lightning payment method found. Available methods: ${JSON.stringify(availableMethods)}`,
|
|
'BTCPayService',
|
|
);
|
|
throw new Error(
|
|
'No Lightning payment method found for this invoice. ' +
|
|
'Ensure the BTCPay store has a Lightning node configured.',
|
|
);
|
|
}
|
|
|
|
const expirationDate = new Date(invoice.expirationTime * 1000);
|
|
const expirationInSec = Math.max(
|
|
0,
|
|
Math.floor((expirationDate.getTime() - Date.now()) / 1000),
|
|
);
|
|
|
|
return {
|
|
quoteId: invoiceId,
|
|
description: (invoice.metadata?.itemDesc as string) || 'Invoice',
|
|
lnInvoice: lightning.destination,
|
|
onchainAddress: '',
|
|
expiration: expirationDate,
|
|
expirationInSec,
|
|
paid: invoice.status === 'Settled',
|
|
targetAmount: {
|
|
amount: invoice.amount,
|
|
currency: invoice.currency,
|
|
},
|
|
sourceAmount: {
|
|
amount: lightning.amount,
|
|
currency: 'BTC',
|
|
},
|
|
conversionRate: {
|
|
amount: lightning.rate,
|
|
sourceCurrency: 'BTC',
|
|
targetCurrency: invoice.currency,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the status of an existing invoice.
|
|
* Returns data shaped like Strike's Invoice DTO.
|
|
*/
|
|
async getInvoice(invoiceId: string): Promise<Invoice> {
|
|
try {
|
|
const raw = await this.getRawInvoice(invoiceId);
|
|
return {
|
|
invoiceId: raw.id,
|
|
amount: { amount: raw.amount, currency: raw.currency },
|
|
description: (raw.metadata?.itemDesc as string) || '',
|
|
created: new Date(raw.createdTime * 1000),
|
|
state: this.mapInvoiceState(raw.status),
|
|
correlationId: raw.metadata?.orderId as string,
|
|
};
|
|
} catch (error) {
|
|
Logger.error('BTCPay getInvoice failed: ' + error.message, 'BTCPayService');
|
|
throw new BadRequestException(error.message);
|
|
}
|
|
}
|
|
|
|
// ── Outgoing Lightning Payments (creator payouts) ───────────────────────
|
|
|
|
/**
|
|
* Pay a creator's Lightning address by resolving LNURL-pay,
|
|
* fetching a BOLT11, then paying it via BTCPay's Lightning node.
|
|
*/
|
|
async sendPaymentWithAddress(
|
|
address: string,
|
|
payment: Payment,
|
|
comment?: string,
|
|
): Promise<LightningPaymentDTO> {
|
|
try {
|
|
const sats = Math.floor(payment.milisatsAmount / 1000);
|
|
|
|
// Resolve the Lightning address to a BOLT11 invoice
|
|
const bolt11 = await this.resolveLightningAddress(address, sats, comment);
|
|
|
|
// Pay the BOLT11 via BTCPay's internal Lightning node
|
|
const payUrl = this.storeUrl('/lightning/BTC/invoices/pay');
|
|
Logger.log(
|
|
`Paying ${sats} sats to ${address} via ${payUrl}`,
|
|
'BTCPayService',
|
|
);
|
|
|
|
const { data } = await axios.post<BTCPayLightningPayResponse>(
|
|
payUrl,
|
|
{ BOLT11: bolt11 },
|
|
{ headers: this.headers },
|
|
);
|
|
|
|
Logger.log(
|
|
`BTCPay pay response: ${JSON.stringify(data)}`,
|
|
'BTCPayService',
|
|
);
|
|
|
|
// BTCPay Greenfield API returns different shapes depending on version.
|
|
// Older versions return { result, errorDetail, paymentHash }.
|
|
// Newer versions may return the payment hash/preimage at top level
|
|
// or an empty 200 on success.
|
|
const result = data?.result;
|
|
const paymentHash =
|
|
data?.paymentHash ?? (data as any)?.payment_hash ?? 'btcpay-payment';
|
|
|
|
if (result && result !== 'Ok') {
|
|
throw new Error(
|
|
`Lightning payment failed: ${result} — ${data?.errorDetail || 'unknown error'}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
id: paymentHash,
|
|
status: 'COMPLETED',
|
|
};
|
|
} catch (error) {
|
|
const detail =
|
|
error.response?.data
|
|
? JSON.stringify(error.response.data)
|
|
: error.message;
|
|
Logger.error(
|
|
`BTCPay sendPayment failed: ${detail}`,
|
|
'BTCPayService',
|
|
);
|
|
throw new Error(detail);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate a Lightning address by attempting to resolve it.
|
|
*/
|
|
async validateAddress(address: string): Promise<boolean> {
|
|
try {
|
|
const [username, domain] = address.split('@');
|
|
if (!username || !domain) return false;
|
|
|
|
const url = `https://${domain}/.well-known/lnurlp/${username}`;
|
|
const { status } = await axios.get(url, { timeout: 10_000 });
|
|
return status === 200;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ── Rate Fetching ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get the current USD value of 1 satoshi.
|
|
* Uses CoinGecko as a reliable public rate source.
|
|
*/
|
|
async getSatoshiRate(): Promise<number> {
|
|
try {
|
|
// Try BTCPay's built-in rate provider first
|
|
const { data } = await axios.get(
|
|
`${this.baseUrl}/api/v1/stores/${this.storeId}/rates`,
|
|
{ headers: this.headers, timeout: 5_000 },
|
|
);
|
|
|
|
const btcUsd = data?.find?.(
|
|
(r: { currencyPair: string }) => r.currencyPair === 'BTC_USD',
|
|
);
|
|
|
|
if (btcUsd?.rate) {
|
|
return Number(btcUsd.rate) * 0.000_000_01; // Convert BTC price to sat price
|
|
}
|
|
|
|
throw new Error('Rate not found in BTCPay response');
|
|
} catch {
|
|
// Fallback to CoinGecko
|
|
try {
|
|
const { data } = await axios.get(
|
|
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd',
|
|
{ timeout: 10_000 },
|
|
);
|
|
const btcPrice = data?.bitcoin?.usd;
|
|
if (!btcPrice) throw new Error('CoinGecko returned no BTC price');
|
|
return btcPrice * 0.000_000_01;
|
|
} catch (fallbackError) {
|
|
Logger.error('Rate fetch failed from all sources: ' + fallbackError.message, 'BTCPayService');
|
|
throw new BadRequestException('Could not get satoshi rate');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Internal Lightning Node API (route hints) ─────────────────────────
|
|
|
|
/**
|
|
* Create a Lightning invoice directly on the node with private route
|
|
* hints enabled. This allows payers to discover routes to our node
|
|
* even when it only has private (unannounced) channels.
|
|
*
|
|
* @param amountMsat Amount in **millisatoshis**
|
|
* @param description Invoice description / memo
|
|
* @param expiry Seconds until expiry
|
|
*/
|
|
private async createInternalLnInvoice(
|
|
amountMsat: number,
|
|
description: string,
|
|
expiry: number,
|
|
): Promise<BTCPayInternalLnInvoice> {
|
|
const body: BTCPayCreateLnInvoiceRequest = {
|
|
amount: amountMsat.toString(),
|
|
description,
|
|
expiry,
|
|
privateRouteHints: true,
|
|
};
|
|
|
|
const { data } = await axios.post<BTCPayInternalLnInvoice>(
|
|
this.storeUrl('/lightning/BTC/invoices'),
|
|
body,
|
|
{ headers: this.headers },
|
|
);
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Fetch an existing internal Lightning invoice by its ID.
|
|
*/
|
|
private async getInternalLnInvoice(
|
|
invoiceId: string,
|
|
): Promise<BTCPayInternalLnInvoice> {
|
|
const { data } = await axios.get<BTCPayInternalLnInvoice>(
|
|
this.storeUrl(`/lightning/BTC/invoices/${invoiceId}`),
|
|
{ headers: this.headers },
|
|
);
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Mark a store invoice as complete (settled).
|
|
* This triggers the `InvoiceSettled` webhook so the normal payment
|
|
* processing pipeline (rents, subscriptions) runs automatically.
|
|
*/
|
|
private async markInvoiceComplete(invoiceId: string): Promise<void> {
|
|
try {
|
|
await axios.post(
|
|
this.storeUrl(`/invoices/${invoiceId}/status`),
|
|
{ status: 'Complete' },
|
|
{ headers: this.headers },
|
|
);
|
|
Logger.log(
|
|
`Store invoice ${invoiceId} marked as complete`,
|
|
'BTCPayService',
|
|
);
|
|
} catch (err) {
|
|
Logger.error(
|
|
`Failed to mark store invoice ${invoiceId} as complete: ${err.message}`,
|
|
'BTCPayService',
|
|
);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the metadata on a store invoice (e.g. to persist the
|
|
* internal Lightning invoice ID for subsequent polls).
|
|
*/
|
|
private async updateInvoiceMetadata(
|
|
invoiceId: string,
|
|
metadata: Record<string, unknown>,
|
|
): Promise<void> {
|
|
await axios.put(
|
|
this.storeUrl(`/invoices/${invoiceId}`),
|
|
{ metadata },
|
|
{ headers: this.headers },
|
|
);
|
|
}
|
|
|
|
// ── Private Helpers ─────────────────────────────────────────────────────
|
|
|
|
private async getRawInvoice(invoiceId: string): Promise<BTCPayInvoiceResponse> {
|
|
const { data } = await axios.get<BTCPayInvoiceResponse>(
|
|
this.storeUrl(`/invoices/${invoiceId}`),
|
|
{ headers: this.headers },
|
|
);
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Map BTCPay invoice status to Strike-compatible state string.
|
|
*/
|
|
private mapInvoiceState(
|
|
status: BTCPayInvoiceResponse['status'],
|
|
): Invoice['state'] {
|
|
switch (status) {
|
|
case 'Settled':
|
|
return 'PAID';
|
|
case 'Processing':
|
|
return 'PENDING';
|
|
case 'Expired':
|
|
case 'Invalid':
|
|
return 'CANCELLED';
|
|
case 'New':
|
|
default:
|
|
return 'UNPAID';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a Lightning address (user@domain.com) to a BOLT11 invoice
|
|
* via the LNURL-pay protocol.
|
|
*/
|
|
private async resolveLightningAddress(
|
|
address: string,
|
|
amountSats: number,
|
|
comment?: string,
|
|
): Promise<string> {
|
|
const [username, domain] = address.split('@');
|
|
if (!username || !domain) {
|
|
throw new Error(`Invalid Lightning address: ${address}`);
|
|
}
|
|
|
|
// Step 1: Fetch the LNURL-pay metadata
|
|
const metadataUrl = `https://${domain}/.well-known/lnurlp/${username}`;
|
|
const { data: lnurlData } = await axios.get(metadataUrl, { timeout: 10_000 });
|
|
|
|
if (lnurlData.status === 'ERROR') {
|
|
throw new Error(`LNURL error: ${lnurlData.reason}`);
|
|
}
|
|
|
|
const amountMillisats = amountSats * 1000;
|
|
const minSendable = lnurlData.minSendable || 1000;
|
|
const maxSendable = lnurlData.maxSendable || 1_000_000_000_000;
|
|
|
|
if (amountMillisats < minSendable || amountMillisats > maxSendable) {
|
|
throw new Error(
|
|
`Amount ${amountSats} sats is outside the allowed range (${minSendable / 1000}-${maxSendable / 1000} sats)`,
|
|
);
|
|
}
|
|
|
|
// Step 2: Request the BOLT11 invoice
|
|
const callbackUrl = new URL(lnurlData.callback);
|
|
callbackUrl.searchParams.set('amount', amountMillisats.toString());
|
|
|
|
// Include a descriptive comment if the endpoint supports it.
|
|
// LNURL-pay endpoints advertise commentAllowed (max char length).
|
|
const commentAllowed = lnurlData.commentAllowed || 0;
|
|
if (comment && commentAllowed > 0) {
|
|
callbackUrl.searchParams.set(
|
|
'comment',
|
|
comment.slice(0, commentAllowed),
|
|
);
|
|
}
|
|
|
|
const { data: invoiceData } = await axios.get(callbackUrl.toString(), { timeout: 10_000 });
|
|
|
|
if (invoiceData.status === 'ERROR') {
|
|
throw new Error(`LNURL callback error: ${invoiceData.reason}`);
|
|
}
|
|
|
|
if (!invoiceData.pr) {
|
|
throw new Error('No payment request (BOLT11) returned from LNURL callback');
|
|
}
|
|
|
|
return invoiceData.pr;
|
|
}
|
|
}
|