From f538fabbc5be9d29883058a0a18e2f0b186cdb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Wed, 20 May 2026 15:45:33 -0700 Subject: [PATCH] Fix issue where a downed lightning node would take down the API server Closes: https://github.com/damus-io/api/issues/5 --- src/invoicing.js | 29 ++++++++++++++++++---------- test/ln_flow.test.js | 46 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/invoicing.js b/src/invoicing.js index 617c930..3032c59 100644 --- a/src/invoicing.js +++ b/src/invoicing.js @@ -255,17 +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 + 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 { + const res = await Promise.race([ + this.ln_rpc({ method: "waitinvoice", params }), + new Promise((resolve) => { + setTimeout(() => { + resolve(undefined) + }, timeout_ms) + }) + ]) + + if (res == null) { + return undefined + } + + return res.error ? false : true + } catch (e) { + error("Error checking invoice %s: %s", label, e.toString()) return undefined } } diff --git a/test/ln_flow.test.js b/test/ln_flow.test.js index 4247b7a..21e3bf7 100644 --- a/test/ln_flow.test.js +++ b/test/ln_flow.test.js @@ -240,3 +240,49 @@ test('LN Flow — Background polling processes paid invoices without client chec t.end(); }); + +test('LN Flow — failed LN node connection during check-invoice returns unpaid checkout and server keeps working', async (t) => { + const purple_api_controller = await PurpleTestController.new(t); + const user_pubkey_1 = purple_api_controller.new_client(); + + const new_checkout_response = await purple_api_controller.clients[user_pubkey_1].new_checkout(PURPLE_ONE_MONTH); + t.same(new_checkout_response.statusCode, 200); + + const verify_checkout_response = await purple_api_controller.clients[user_pubkey_1].verify_checkout(new_checkout_response.body.id); + t.same(verify_checkout_response.statusCode, 200); + + const originalLNSocket = purple_api_controller.purple_api.invoice_manager.constructor.LNSocket; + purple_api_controller.purple_api.invoice_manager.constructor.LNSocket = async () => ({ + genkey() {}, + async connect_and_init() { + throw new Error('connect ECONNREFUSED 24.86.66.39:9735'); + }, + destroy() {} + }); + + t.teardown(() => { + purple_api_controller.purple_api.invoice_manager.constructor.LNSocket = originalLNSocket; + }); + + const failedCheckResponse = await purple_api_controller.clients[user_pubkey_1].check_invoice(new_checkout_response.body.id); + t.same(failedCheckResponse.statusCode, 200); + t.same(failedCheckResponse.body.completed, false); + t.same(failedCheckResponse.body.invoice?.paid, undefined); + + const checkoutAfterFailure = await purple_api_controller.clients[user_pubkey_1].get_checkout(new_checkout_response.body.id); + t.same(checkoutAfterFailure.statusCode, 200); + t.same(checkoutAfterFailure.body.completed, false); + t.same(checkoutAfterFailure.body.invoice?.paid, undefined); + + const accountAfterFailure = await purple_api_controller.clients[user_pubkey_1].get_account(); + t.same(accountAfterFailure.statusCode, 404); + + const productsResponse = await purple_api_controller.clients[user_pubkey_1].get_products(); + t.same(productsResponse.statusCode, 200); + t.ok(productsResponse.body[PURPLE_ONE_MONTH]); + + const secondCheckoutResponse = await purple_api_controller.clients[user_pubkey_1].new_checkout(PURPLE_ONE_MONTH); + t.same(secondCheckoutResponse.statusCode, 200); +}); + +