From e05989d371d660a28e3099e671ca14585cbd3f67 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 27 Nov 2024 19:46:40 +0100 Subject: [PATCH] Improve LNC realiability by being nicer (#1658) * be nicer to lnc * decrease timeout to 4 seconds --- wallets/lnc/client.js | 157 +++++++++++++++++++++--------------------- 1 file changed, 80 insertions(+), 77 deletions(-) diff --git a/wallets/lnc/client.js b/wallets/lnc/client.js index 6c8def20..9305429a 100644 --- a/wallets/lnc/client.js +++ b/wallets/lnc/client.js @@ -3,106 +3,109 @@ import { bolt11Tags } from '@/lib/bolt11' import { Mutex } from 'async-mutex' export * from '@/wallets/lnc' -async function disconnect (lnc, logger) { - if (lnc) { - try { - lnc.disconnect() - logger.info('disconnecting...') - // wait for lnc to disconnect before releasing the mutex - await new Promise((resolve, reject) => { - let counter = 0 - const interval = setInterval(() => { - if (lnc?.isConnected) { - if (counter++ > 100) { - logger.error('failed to disconnect from lnc') - clearInterval(interval) - reject(new Error('failed to disconnect from lnc')) - } - return - } - clearInterval(interval) - resolve() - }) - }, 50) - logger.info('disconnected') - } catch (err) { - logger.error('failed to disconnect from lnc: ' + err) - } - } -} +const mutex = new Mutex() +const serverHost = 'mailbox.terminal.lightning.today:443' export async function testSendPayment (credentials, { logger }) { - let lnc - try { - lnc = await getLNC(credentials) - - logger.info('connecting ...') - await lnc.connect() - logger.info('connected') - - logger.info('validating permissions ...') - await validateNarrowPerms(lnc) - logger.info('permissions ok') - - return lnc.credentials.credentials - } finally { - await disconnect(lnc, logger) - } + const lnc = await getLNC(credentials, { logger }) + logger?.info('validating permissions ...') + await validateNarrowPerms(lnc) + logger?.info('permissions ok') + return lnc.credentials.credentials } -const mutex = new Mutex() - export async function sendPayment (bolt11, credentials, { logger }) { const hash = bolt11Tags(bolt11).payment_hash - return await mutex.runExclusive(async () => { - let lnc try { - lnc = await getLNC(credentials) - - await lnc.connect() - const { paymentError, paymentPreimage: preimage } = - await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 }) - + const lnc = await getLNC(credentials, { logger }) + const { paymentError, paymentPreimage: preimage } = await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 }) if (paymentError) throw new Error(paymentError) if (!preimage) throw new Error('No preimage in response') - return preimage } catch (err) { const msg = err.message || err.toString?.() - if (msg.includes('invoice expired')) { - throw new InvoiceExpiredError(hash) - } - if (msg.includes('canceled')) { - throw new InvoiceCanceledError(hash) - } + if (msg.includes('invoice expired')) throw new InvoiceExpiredError(hash) + if (msg.includes('canceled')) throw new InvoiceCanceledError(hash) throw err - } finally { - await disconnect(lnc, logger) } }) } -async function getLNC (credentials = {}) { - const serverHost = 'mailbox.terminal.lightning.today:443' - // XXX we MUST reuse the same instance of LNC because it references a global Go object - // that holds closures to the first LNC instance it's created with - if (window.lnc) { - window.lnc.credentials.credentials = { - ...window.lnc.credentials.credentials, +async function disconnectLNC (lnc, { logger } = {}) { + try { + if (!lnc?.isConnected) return + lnc.disconnect() + logger?.info('disconnecting...') + // wait for lnc to disconnect + await new Promise((resolve, reject) => { + let counter = 0 + const interval = setInterval(() => { + if (lnc?.isConnected) { + if (counter++ > 100) { + logger?.error('failed to disconnect from lnc') + clearInterval(interval) + reject(new Error('failed to disconnect from lnc')) + } + return + } + clearInterval(interval) + resolve() + }) + }, 50) + logger?.info('disconnected') + } catch (err) { + logger?.error('failed to disconnect from lnc: ' + err) + } +} + +async function getLNC (credentials = {}, { logger } = {}) { + if (window.snLncKillerTimeout) clearTimeout(window.snLncKillerTimeout) + + if (!window.snLnc) { // create new instance + const { default: LNC } = await import('@lightninglabs/lnc-web') + window.snLnc = new LNC({ + credentialStore: new LncCredentialStore({ + ...credentials, + serverHost + }) + }) + + window.addEventListener('beforeunload', () => { + // try to disconnect gracefully when the page is closed + disconnectLNC(window.snLnc, { logger }) + }) + } else if (JSON.stringify(window.snLncCredentials ?? {}) !== JSON.stringify(credentials)) { + console.log('LNC instance has new credentials') + // disconnect and update credentials if they've changed + await disconnectLNC(window.snLnc, { logger }) + // XXX we MUST reuse the same instance of LNC because it references a global Go object + // that holds closures to the first LNC instance it's created with + window.snLnc.credentials.credentials = { + ...window.snLnc.credentials.credentials, ...credentials, serverHost } - return window.lnc } - const { default: LNC } = await import('@lightninglabs/lnc-web') - window.lnc = new LNC({ - credentialStore: new LncCredentialStore({ - ...credentials, - serverHost + + if (!window.snLnc.isConnected) { + logger?.info('connecting ...') + await window.snLnc.connect() + logger?.info('connected') + } + + window.snLncCredentials = { + ...credentials + } + + window.snLncKillerTimeout = setTimeout(() => { + logger?.info('disconnecting from lnc due to inactivity ...') + mutex.runExclusive(async () => { + await disconnectLNC(window.snLnc, { logger }) }) - }) - return window.lnc + }, 4000) + + return window.snLnc } function validateNarrowPerms (lnc) {