Skip to content

Commit

Permalink
chore(core): improve lightning resilience (#3497)
Browse files Browse the repository at this point in the history
  • Loading branch information
dolcalmi authored Nov 17, 2023
1 parent af86960 commit e2e39ee
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 21 deletions.
111 changes: 90 additions & 21 deletions core/api/src/services/lnd/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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"
Expand All @@ -95,17 +96,21 @@ export const LndService = (): ILightningService | LightningServiceError => {
const defaultLnd = activeNode.lnd
const defaultPubkey = activeNode.pubkey as Pubkey

const activeOnchainNode = getActiveOnchainLnd()
if (activeOnchainNode instanceof Error) return activeOnchainNode

const defaultOnchainLnd = activeOnchainNode.lnd
const defaultOnchainLnd = () => {
const activeOnchainNode = getActiveOnchainLnd()
if (activeOnchainNode instanceof Error) return activeOnchainNode
return activeOnchainNode.lnd
}

const isLocal = (pubkey: Pubkey): boolean | LightningServiceError =>
getLnds({ type: "offchain" }).some((item) => item.pubkey === pubkey)

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)

Expand All @@ -127,7 +132,7 @@ export const LndService = (): ILightningService | LightningServiceError => {
pubkey?: Pubkey,
): Promise<Satoshis | LightningServiceError> => {
try {
const lndInstance = pubkey ? getLndFromPubkey({ pubkey }) : defaultOnchainLnd
const lndInstance = pubkey ? getLndFromPubkey({ pubkey }) : defaultOnchainLnd()
if (lndInstance instanceof Error) return lndInstance

const { chain_balance } = await getChainBalance({ lnd: lndInstance })
Expand All @@ -141,7 +146,7 @@ export const LndService = (): ILightningService | LightningServiceError => {
pubkey?: Pubkey,
): Promise<Satoshis | LightningServiceError> => {
try {
const lndInstance = pubkey ? getLndFromPubkey({ pubkey }) : defaultOnchainLnd
const lndInstance = pubkey ? getLndFromPubkey({ pubkey }) : defaultOnchainLnd()
if (lndInstance instanceof Error) return lndInstance

const { pending_chain_balance } = await getPendingChainBalance({ lnd: lndInstance })
Expand Down Expand Up @@ -179,8 +184,11 @@ export const LndService = (): ILightningService | LightningServiceError => {
// this is necessary for tests, otherwise `after` may be negative
const after = Math.max(0, blockHeight - scanDepth)

const lnd = defaultOnchainLnd()
if (lnd instanceof Error) return lnd

const txs = await getChainTransactions({
lnd: defaultOnchainLnd,
lnd,
after,
})

Expand Down Expand Up @@ -478,15 +486,18 @@ export const LndService = (): ILightningService | LightningServiceError => {
}
}

const registerInvoice = async ({
const registerLndInvoice = async ({
lnd,
paymentHash,
sats,
description,
descriptionHash,
expiresAt,
}: RegisterInvoiceArgs): Promise<RegisteredInvoice | LightningServiceError> => {
}: RegisterInvoiceArgs & { lnd: AuthenticatedLnd }): Promise<
RegisteredInvoice | LightningServiceError
> => {
const input = {
lnd: defaultLnd,
lnd,
id: paymentHash,
description,
description_hash: descriptionHash,
Expand All @@ -511,6 +522,30 @@ export const LndService = (): ILightningService | LightningServiceError => {
}
}

const registerInvoice = async ({
paymentHash,
sats,
description,
descriptionHash,
expiresAt,
}: RegisterInvoiceArgs): Promise<RegisteredInvoice | LightningServiceError> => {
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,
Expand Down Expand Up @@ -782,11 +817,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
Expand All @@ -808,7 +845,7 @@ export const LndService = (): ILightningService | LightningServiceError => {
}

const paymentDetailsArgs: PayViaPaymentDetailsArgs = {
lnd: defaultLnd,
lnd,
id: decodedInvoice.paymentHash,
destination: decodedInvoice.destination,
mtokens: milliSatsAmount.toString(),
Expand Down Expand Up @@ -856,6 +893,30 @@ export const LndService = (): ILightningService | LightningServiceError => {
}
}

const payInvoiceViaPaymentDetails = async ({
decodedInvoice,
btcPaymentAmount,
maxFeeAmount,
}: {
decodedInvoice: LnInvoice
btcPaymentAmount: BtcPaymentAmount
maxFeeAmount: BtcPaymentAmount | undefined
}): Promise<PayInvoiceResult | LightningServiceError> => {
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: {
Expand Down Expand Up @@ -982,16 +1043,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,
Expand Down Expand Up @@ -1139,8 +1201,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))
Expand All @@ -1153,6 +1217,7 @@ const handleCommonRouteNotFoundErrors = (err: Error | unknown) => {
switch (true) {
case match(KnownLndErrorDetails.ConnectionDropped):
case match(KnownLndErrorDetails.NoConnectionEstablished):
checkAllLndHealth()
return new OffChainServiceUnavailableError()

case match(KnownLndErrorDetails.MissingDependentFeature):
Expand All @@ -1163,6 +1228,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),
Expand Down
14 changes: 14 additions & 0 deletions core/api/src/services/lnd/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit e2e39ee

Please sign in to comment.