From b9028be68bb201d209b41c5ec84d92a29b6b14e7 Mon Sep 17 00:00:00 2001 From: Juan P Lopez Date: Fri, 3 Nov 2023 09:34:40 -0500 Subject: [PATCH] chore(core): improve lightning resilience --- core/api/src/services/lnd/index.ts | 93 ++++++++++++++++++---- core/api/src/services/lnd/index.types.d.ts | 14 ++++ 2 files changed, 93 insertions(+), 14 deletions(-) diff --git a/core/api/src/services/lnd/index.ts b/core/api/src/services/lnd/index.ts index 2471494906d..957a0b5d56c 100644 --- a/core/api/src/services/lnd/index.ts +++ b/core/api/src/services/lnd/index.ts @@ -29,11 +29,8 @@ import { settleHodlInvoice, } from "lightning" import lnService from "ln-service" - import sumBy from "lodash.sumby" -import { KnownLndErrorDetails } from "./errors" - import { getActiveLnd, getActiveOnchainLnd, @@ -42,9 +39,12 @@ import { parseLndErrorDetails, } from "./config" +import { checkAllLndHealth } from "./health" + +import { KnownLndErrorDetails } from "./errors" + import { NETWORK, SECS_PER_5_MINS } from "@/config" -import { toMilliSatsFromString, toSats } from "@/domain/bitcoin" import { BadPaymentDataError, CorruptLndDbError, @@ -76,9 +76,10 @@ import { UnknownRouteNotFoundError, decodeInvoice, } from "@/domain/bitcoin/lightning" -import { IncomingOnChainTransaction } from "@/domain/bitcoin/onchain" import { CacheKeys } from "@/domain/cache" import { LnFees } from "@/domain/payments" +import { toMilliSatsFromString, toSats } from "@/domain/bitcoin" +import { IncomingOnChainTransaction } from "@/domain/bitcoin/onchain" import { WalletCurrency, paymentAmountFromNumber } from "@/domain/shared" import { LocalCacheService } from "@/services/cache" @@ -106,6 +107,9 @@ export const LndService = (): ILightningService | LightningServiceError => { const listActivePubkeys = (): Pubkey[] => getLnds({ active: true, type: "offchain" }).map((lndAuth) => lndAuth.pubkey as Pubkey) + const listActiveLnd = (): AuthenticatedLnd[] => + getLnds({ active: true, type: "offchain" }).map((lndAuth) => lndAuth.lnd) + const listAllPubkeys = (): Pubkey[] => getLnds({ type: "offchain" }).map((lndAuth) => lndAuth.pubkey as Pubkey) @@ -478,15 +482,18 @@ export const LndService = (): ILightningService | LightningServiceError => { } } - const registerInvoice = async ({ + const registerLndInvoice = async ({ + lnd, paymentHash, sats, description, descriptionHash, expiresAt, - }: RegisterInvoiceArgs): Promise => { + }: RegisterInvoiceArgs & { lnd: AuthenticatedLnd }): Promise< + RegisteredInvoice | LightningServiceError + > => { const input = { - lnd: defaultLnd, + lnd, id: paymentHash, description, description_hash: descriptionHash, @@ -511,6 +518,30 @@ export const LndService = (): ILightningService | LightningServiceError => { } } + const registerInvoice = async ({ + paymentHash, + sats, + description, + descriptionHash, + expiresAt, + }: RegisterInvoiceArgs): Promise => { + const lnds = listActiveLnd() + for (const lnd of lnds) { + const result = await registerLndInvoice({ + lnd, + paymentHash, + sats, + description, + descriptionHash, + expiresAt, + }) + if (isConnectionError(result)) continue + return result + } + + return new OffChainServiceUnavailableError("no active lightning node (for offchain)") + } + const lookupInvoice = async ({ pubkey, paymentHash, @@ -782,11 +813,13 @@ export const LndService = (): ILightningService | LightningServiceError => { } } - const payInvoiceViaPaymentDetails = async ({ + const payInvoiceViaPaymentDetailsWithLnd = async ({ + lnd, decodedInvoice, btcPaymentAmount, maxFeeAmount, }: { + lnd: AuthenticatedLnd decodedInvoice: LnInvoice btcPaymentAmount: BtcPaymentAmount maxFeeAmount: BtcPaymentAmount | undefined @@ -808,7 +841,7 @@ export const LndService = (): ILightningService | LightningServiceError => { } const paymentDetailsArgs: PayViaPaymentDetailsArgs = { - lnd: defaultLnd, + lnd, id: decodedInvoice.paymentHash, destination: decodedInvoice.destination, mtokens: milliSatsAmount.toString(), @@ -856,6 +889,30 @@ export const LndService = (): ILightningService | LightningServiceError => { } } + const payInvoiceViaPaymentDetails = async ({ + decodedInvoice, + btcPaymentAmount, + maxFeeAmount, + }: { + decodedInvoice: LnInvoice + btcPaymentAmount: BtcPaymentAmount + maxFeeAmount: BtcPaymentAmount | undefined + }): Promise => { + const lnds = listActiveLnd() + for (const lnd of lnds) { + const result = await payInvoiceViaPaymentDetailsWithLnd({ + lnd, + decodedInvoice, + btcPaymentAmount, + maxFeeAmount, + }) + if (isConnectionError(result)) continue + return result + } + + return new OffChainServiceUnavailableError("no active lightning node (for offchain)") + } + return wrapAsyncFunctionsToRunInSpan({ namespace: "services.lnd.offchain", fns: { @@ -982,16 +1039,17 @@ const lookupPaymentByPubkeyAndHash = async ({ } } -/* eslint @typescript-eslint/ban-ts-comment: "off" */ -// @ts-ignore-next-line no-implicit-any error -const translateLnPaymentLookup = (p): LnPaymentLookup => ({ +const isPaymentConfirmed = (p: PaymentResult): p is ConfirmedPaymentResult => + p.is_confirmed + +const translateLnPaymentLookup = (p: PaymentResult): LnPaymentLookup => ({ createdAt: new Date(p.created_at), status: p.is_confirmed ? PaymentStatus.Settled : PaymentStatus.Pending, paymentHash: p.id as PaymentHash, paymentRequest: p.request as EncodedPaymentRequest, milliSatsAmount: toMilliSatsFromString(p.mtokens), roundedUpAmount: toSats(p.safe_tokens), - confirmedDetails: p.is_confirmed + confirmedDetails: isPaymentConfirmed(p) ? { confirmedAt: new Date(p.confirmed_at), destination: p.destination as Pubkey, @@ -1139,8 +1197,10 @@ const handleCommonLightningServiceErrors = (err: Error | unknown) => { switch (true) { case match(KnownLndErrorDetails.ConnectionDropped): case match(KnownLndErrorDetails.NoConnectionEstablished): + checkAllLndHealth() return new OffChainServiceUnavailableError() case match(KnownLndErrorDetails.ConnectionRefused): + checkAllLndHealth() return new OffChainServiceBusyError() default: return new UnknownLightningServiceError(msgForUnknown(err as LnError)) @@ -1153,6 +1213,7 @@ const handleCommonRouteNotFoundErrors = (err: Error | unknown) => { switch (true) { case match(KnownLndErrorDetails.ConnectionDropped): case match(KnownLndErrorDetails.NoConnectionEstablished): + checkAllLndHealth() return new OffChainServiceUnavailableError() case match(KnownLndErrorDetails.MissingDependentFeature): @@ -1163,6 +1224,10 @@ const handleCommonRouteNotFoundErrors = (err: Error | unknown) => { } } +const isConnectionError = (result: unknown | LightningServiceError): boolean => + result instanceof OffChainServiceUnavailableError || + result instanceof OffChainServiceBusyError + const msgForUnknown = (err: LnError) => JSON.stringify({ parsedLndErrorDetails: parseLndErrorDetails(err), diff --git a/core/api/src/services/lnd/index.types.d.ts b/core/api/src/services/lnd/index.types.d.ts index d776bc561f4..95519dcf89b 100644 --- a/core/api/src/services/lnd/index.types.d.ts +++ b/core/api/src/services/lnd/index.types.d.ts @@ -8,6 +8,20 @@ type GetPaymentsArgs = import("lightning").GetPaymentsArgs type GetPendingPaymentsArgs = import("lightning").GetPendingPaymentsArgs type GetPendingPaymentsResult = import("lightning").GetPendingPaymentsResult +type ConfirmedPaymentResult = Extract< + GetPaymentsResult, + { payments: unknown } +>["payments"][0] +type PendingPaymentResult = Extract< + GetPendingPaymentsResult, + { payments: unknown } +>["payments"][0] +type FailedPaymentResult = Extract< + GetFailedPaymentsResult, + { payments: unknown } +>["payments"][0] +type PaymentResult = ConfirmedPaymentResult | PendingPaymentResult | FailedPaymentResult + type PaymentFnFactory = | import("lightning").AuthenticatedLightningMethod< GetFailedPaymentsArgs,