From c7de11d5eae22afc18299a4a94e57f303af30654 Mon Sep 17 00:00:00 2001 From: Perlover Date: Thu, 22 Oct 2026 17:32:46 +0200 Subject: [PATCH] fix(payments): parse ISO 8601 time/expiry returned by LNbits >= 1.0 LNbits 1.x returns details.time and details.expiry as ISO 8601 strings ("2025-10-11T15:24:27+00:00") instead of unix seconds, so `new Date(value * 1000)` produced Invalid Date. Paid admissions were never confirmed (confirmInvoice throws RangeError on confirmedAt.toISOString()) and expired invoices stayed pending forever. Accept both unix seconds (LNbits < 1.0) and ISO strings. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0116ucVjyUPmPdBqnQsWxEws --- .changeset/lnbits-iso-dates.md | 5 +++ .../lnbits-payment-processor.ts | 14 ++++--- .../lnbits-payment-processor.spec.ts | 38 +++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 .changeset/lnbits-iso-dates.md diff --git a/.changeset/lnbits-iso-dates.md b/.changeset/lnbits-iso-dates.md new file mode 100644 index 000000000..3cd5b7279 --- /dev/null +++ b/.changeset/lnbits-iso-dates.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +fix(payments): parse the ISO 8601 `time`/`expiry` returned by LNbits >= 1.0 so paid admissions are confirmed and expired invoices are retired diff --git a/src/payments-processors/lnbits-payment-processor.ts b/src/payments-processors/lnbits-payment-processor.ts index f14d2af7d..cf97d2a89 100644 --- a/src/payments-processors/lnbits-payment-processor.ts +++ b/src/payments-processors/lnbits-payment-processor.ts @@ -11,6 +11,10 @@ import { Settings } from '../@types/settings' const logger = createLogger('lnbits-payments-processor') +// LNbits < 1.0 returns unix seconds for details.time/details.expiry, +// LNbits >= 1.0 returns ISO 8601 strings ("2025-10-11T15:24:27+00:00"). +const toDate = (value: number | string): Date => (typeof value === 'number' ? new Date(value * 1000) : new Date(value)) + export class LNbitsInvoice implements Invoice { id: string pubkey: Pubkey @@ -62,7 +66,7 @@ export class LNbitsPaymentsProcessor implements IPaymentsProcessor { invoice.amountPaid = BigInt(Math.floor(data.details.amount / 1000)) } invoice.unit = InvoiceUnit.SATS - invoice.expiresAt = new Date(data.details.expiry * 1000) + invoice.expiresAt = toDate(data.details.expiry) if (data.paid) { invoice.status = InvoiceStatus.COMPLETED } else if (isExpiredInvoice(invoice)) { @@ -71,8 +75,8 @@ export class LNbitsPaymentsProcessor implements IPaymentsProcessor { invoice.status = InvoiceStatus.PENDING } invoice.description = data.details.memo - invoice.confirmedAt = data.paid ? new Date(data.details.time * 1000) : null - invoice.createdAt = new Date(data.details.time * 1000) + invoice.confirmedAt = data.paid ? toDate(data.details.time) : null + invoice.createdAt = toDate(data.details.time) invoice.updatedAt = new Date() return invoice } catch (error) { @@ -129,8 +133,8 @@ export class LNbitsPaymentsProcessor implements IPaymentsProcessor { invoice.status = data.paid ? InvoiceStatus.COMPLETED : InvoiceStatus.PENDING invoice.description = data.details.memo invoice.confirmedAt = null - invoice.expiresAt = new Date(data.details.expiry * 1000) - invoice.createdAt = new Date(data.details.time * 1000) + invoice.expiresAt = toDate(data.details.expiry) + invoice.createdAt = toDate(data.details.time) invoice.rawResponse = JSON.stringify({ invoiceResponse: invoiceResponse.data, createData: response.data, diff --git a/test/unit/payments-processors/lnbits-payment-processor.spec.ts b/test/unit/payments-processors/lnbits-payment-processor.spec.ts index d3f12725b..0f3ba96e3 100644 --- a/test/unit/payments-processors/lnbits-payment-processor.spec.ts +++ b/test/unit/payments-processors/lnbits-payment-processor.spec.ts @@ -81,5 +81,43 @@ describe('LNbitsPaymentsProcessor', () => { expect(invoice.status).to.equal(InvoiceStatus.COMPLETED) expect(invoice.amountPaid).to.equal(42n) }) + + it('parses ISO 8601 time and expiry returned by LNbits >= 1.0', async () => { + const time = new Date(Date.now() - 120000) + const expiry = new Date(Date.now() - 60000) + const { processor } = makeProcessor( + invoiceResponse({ + details: { + time: time.toISOString(), + expiry: expiry.toISOString(), + }, + }), + ) + + const invoice = await processor.getInvoice('lnbits-payment-hash') + + expect(invoice.status).to.equal(InvoiceStatus.EXPIRED) + expect(invoice.expiresAt?.getTime()).to.equal(expiry.getTime()) + expect(invoice.createdAt?.getTime()).to.equal(time.getTime()) + }) + + it('sets a valid confirmedAt for paid invoices with an ISO 8601 time', async () => { + const { processor } = makeProcessor( + invoiceResponse({ + data: { + paid: true, + }, + details: { + time: '2025-10-11T15:24:27+00:00', + expiry: '2025-10-12T15:24:27+00:00', + }, + }), + ) + + const invoice = await processor.getInvoice('lnbits-payment-hash') + + expect(invoice.status).to.equal(InvoiceStatus.COMPLETED) + expect(invoice.confirmedAt?.toISOString()).to.equal('2025-10-11T15:24:27.000Z') + }) }) })