From 61092dcd903c8d1b777091c7bda3060896310c73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 19:03:12 +0000 Subject: [PATCH 1/2] fix: prevent unhandled rejection in invoice wait flow Agent-Logs-Url: https://github.com/damus-io/api/sessions/cb180abc-925e-4c61-a500-ba8d094f4f4a Co-authored-by: danieldaquino <24692108+danieldaquino@users.noreply.github.com> --- src/invoicing.js | 30 +++++++++++++++++++----------- test/ln_flow.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/invoicing.js b/src/invoicing.js index 617c930..32e9e1e 100644 --- a/src/invoicing.js +++ b/src/invoicing.js @@ -255,18 +255,26 @@ class PurpleInvoiceManager { // Checks the status of an invoice once. Returns true if paid, false otherwise. async check_invoice_is_paid(label) { + const params = { label } + const timeout_ms = parseInt(process.env.LN_INVOICE_CHECK_TIMEOUT_MS) || 60000 + let timeout_id = null + + const timeout_promise = new Promise((resolve) => { + timeout_id = setTimeout(() => { + resolve(undefined) + }, timeout_ms) + }) + + const waitinvoice_promise = this.ln_rpc({ method: "waitinvoice", params }) + .then((res) => res.error ? false : true) + .catch(() => undefined) + try { - const params = { label } - return new Promise(async (resolve, reject) => { - setTimeout(() => { - resolve(undefined) - }, parseInt(process.env.LN_INVOICE_CHECK_TIMEOUT_MS) || 60000) - const res = await this.ln_rpc({ method: "waitinvoice", params }) - resolve(res.error ? false : true) - }) - } - catch { - return undefined + return await Promise.race([waitinvoice_promise, timeout_promise]) + } finally { + if (timeout_id) { + clearTimeout(timeout_id) + } } } diff --git a/test/ln_flow.test.js b/test/ln_flow.test.js index 4247b7a..10cdccf 100644 --- a/test/ln_flow.test.js +++ b/test/ln_flow.test.js @@ -240,3 +240,28 @@ test('LN Flow — Background polling processes paid invoices without client chec t.end(); }); + +test('LN Flow — check_invoice_is_paid handles LN RPC rejection without unhandled rejection', async (t) => { + const purple_api_controller = await PurpleTestController.new(t); + const manager = purple_api_controller.purple_api.invoice_manager; + manager.ln_rpc = async () => { + throw undefined + } + + let unhandled_rejection_received = false + const handle_unhandled_rejection = () => { + unhandled_rejection_received = true + } + process.on('unhandledRejection', handle_unhandled_rejection) + + try { + const result = await manager.check_invoice_is_paid('some-label') + await new Promise((resolve) => setImmediate(resolve)) + t.same(result, undefined) + t.equal(unhandled_rejection_received, false) + } finally { + process.removeListener('unhandledRejection', handle_unhandled_rejection) + } + + t.end() +}) From 1bd8183df52c00feb5c1d6db7402069dbdcff864 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 19:06:32 +0000 Subject: [PATCH 2/2] test: normalize naming and clarify undefined rejection simulation Agent-Logs-Url: https://github.com/damus-io/api/sessions/cb180abc-925e-4c61-a500-ba8d094f4f4a Co-authored-by: danieldaquino <24692108+danieldaquino@users.noreply.github.com> --- test/ln_flow.test.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/ln_flow.test.js b/test/ln_flow.test.js index 10cdccf..a4d9fd2 100644 --- a/test/ln_flow.test.js +++ b/test/ln_flow.test.js @@ -245,22 +245,23 @@ test('LN Flow — check_invoice_is_paid handles LN RPC rejection without unhandl const purple_api_controller = await PurpleTestController.new(t); const manager = purple_api_controller.purple_api.invoice_manager; manager.ln_rpc = async () => { + // Simulate the production failure mode where a promise rejection reason can be undefined. throw undefined } - let unhandled_rejection_received = false - const handle_unhandled_rejection = () => { - unhandled_rejection_received = true + let unhandledRejectionReceived = false + const handleUnhandledRejection = () => { + unhandledRejectionReceived = true } - process.on('unhandledRejection', handle_unhandled_rejection) + process.on('unhandledRejection', handleUnhandledRejection) try { const result = await manager.check_invoice_is_paid('some-label') await new Promise((resolve) => setImmediate(resolve)) t.same(result, undefined) - t.equal(unhandled_rejection_received, false) + t.equal(unhandledRejectionReceived, false) } finally { - process.removeListener('unhandledRejection', handle_unhandled_rejection) + process.removeListener('unhandledRejection', handleUnhandledRejection) } t.end()