From a798bdc291d4c7803944368df071ac86aa9d8478 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Fri, 3 May 2024 15:11:35 -0400 Subject: [PATCH 01/10] refactor(core): make 'pubkey' optional in LnPayments repository --- .../src/domain/bitcoin/lightning/payments/index.types.d.ts | 2 +- core/api/src/services/lnd/schema.ts | 1 - core/api/src/services/lnd/schema.types.d.ts | 2 +- core/api/src/services/mongoose/ln-payments.ts | 5 ++++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts b/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts index c61a0e8d0f..3f1642e214 100644 --- a/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts +++ b/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts @@ -1,7 +1,7 @@ type LnPaymentPartial = { readonly paymentHash: PaymentHash readonly paymentRequest: EncodedPaymentRequest | undefined - readonly sentFromPubkey: Pubkey + readonly sentFromPubkey: Pubkey | undefined } // Makes all properties non-readonly except the properties passed in as K diff --git a/core/api/src/services/lnd/schema.ts b/core/api/src/services/lnd/schema.ts index f3e5b59eac..456d8f0c4b 100644 --- a/core/api/src/services/lnd/schema.ts +++ b/core/api/src/services/lnd/schema.ts @@ -49,7 +49,6 @@ const paymentSchema = new Schema({ paymentRequest: String, sentFromPubkey: { type: String, - required: true, }, milliSatsAmount: Number, roundedUpAmount: Number, diff --git a/core/api/src/services/lnd/schema.types.d.ts b/core/api/src/services/lnd/schema.types.d.ts index c659f10437..441e9131b3 100644 --- a/core/api/src/services/lnd/schema.types.d.ts +++ b/core/api/src/services/lnd/schema.types.d.ts @@ -4,7 +4,7 @@ interface LnPaymentType { status: string paymentHash: string paymentRequest: string - sentFromPubkey: string + sentFromPubkey: string | undefined milliSatsAmount: number roundedUpAmount: number confirmedDetails: LnPaymentConfirmedDetails | undefined diff --git a/core/api/src/services/mongoose/ln-payments.ts b/core/api/src/services/mongoose/ln-payments.ts index f70614fa70..0071359b1c 100644 --- a/core/api/src/services/mongoose/ln-payments.ts +++ b/core/api/src/services/mongoose/ln-payments.ts @@ -98,5 +98,8 @@ const lnPaymentFromRaw = (result: LnPaymentType): PersistedLnPaymentLookup => ({ const lnPaymentPartialFromRaw = (result: LnPaymentType): LnPaymentPartial => ({ paymentHash: result.paymentHash as PaymentHash, paymentRequest: result.paymentRequest as EncodedPaymentRequest, - sentFromPubkey: result.sentFromPubkey as Pubkey, + sentFromPubkey: + result.sentFromPubkey === undefined + ? result.sentFromPubkey + : (result.sentFromPubkey as Pubkey), }) From b03a558f877d5183fa111ac961e07492bf28526b Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Fri, 3 May 2024 17:50:30 -0400 Subject: [PATCH 02/10] refactor(core): make 'pubkey' optional in LnPayments app calls --- core/api/src/app/lightning/delete-ln-payments.ts | 6 +++--- core/api/src/app/lightning/update-ln-payments.ts | 2 ++ core/api/src/app/payments/send-lightning.ts | 2 +- .../src/domain/bitcoin/lightning/payments/index.types.d.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/api/src/app/lightning/delete-ln-payments.ts b/core/api/src/app/lightning/delete-ln-payments.ts index f9c5b5e10a..ab56e81e5a 100644 --- a/core/api/src/app/lightning/delete-ln-payments.ts +++ b/core/api/src/app/lightning/delete-ln-payments.ts @@ -16,9 +16,9 @@ export const deleteLnPaymentsBefore = async ( ): Promise => { const paymentHashesBefore = listAllPaymentsBefore(timestamp) - for await (const paymentHash of paymentHashesBefore) { - if (paymentHash instanceof Error) return paymentHash - await checkAndDeletePaymentForHash(paymentHash) + for await (const paymentHashAndPubkey of paymentHashesBefore) { + if (paymentHashAndPubkey instanceof Error) return paymentHashAndPubkey + await checkAndDeletePaymentForHash(paymentHashAndPubkey) } return true diff --git a/core/api/src/app/lightning/update-ln-payments.ts b/core/api/src/app/lightning/update-ln-payments.ts index 3276ade8b3..a34e22f839 100644 --- a/core/api/src/app/lightning/update-ln-payments.ts +++ b/core/api/src/app/lightning/update-ln-payments.ts @@ -133,6 +133,8 @@ const updateLnPaymentsPaginated = async ({ ) if (!persistedPaymentLookup) return { after: updatedAfter, processedLnPaymentsHashes } + persistedPaymentLookup.sentFromPubkey = pubkey + persistedPaymentLookup.createdAt = payment.createdAt persistedPaymentLookup.status = payment.status persistedPaymentLookup.milliSatsAmount = payment.milliSatsAmount diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index 578ee414df..dcc519f5d9 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -948,7 +948,7 @@ const lockedPaymentViaLnSteps = async ({ await LnPaymentsRepository().persistNew({ paymentHash: decodedInvoice.paymentHash, paymentRequest: decodedInvoice.paymentRequest, - sentFromPubkey: outgoingNodePubkey || lndService.defaultPubkey(), + sentFromPubkey: outgoingNodePubkey, }) if (!(payResult instanceof Error)) diff --git a/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts b/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts index 3f1642e214..70394cf6a7 100644 --- a/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts +++ b/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts @@ -10,7 +10,7 @@ type Writeable = Pick & { } type PersistedLnPaymentLookup = Writeable & { - readonly sentFromPubkey: Pubkey + sentFromPubkey: Pubkey isCompleteRecord: boolean } From 82b9b9eda06429001203e8785ddc71994069ade7 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Fri, 3 May 2024 17:49:22 -0400 Subject: [PATCH 03/10] refactor(core): make 'pubkey' optional in LnSendMetadata --- core/api/src/app/payments/send-lightning.ts | 2 +- core/api/src/services/ledger/facade/tx-metadata.ts | 2 +- core/api/src/services/ledger/index.types.d.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index dcc519f5d9..22ff0a9d4d 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -879,7 +879,7 @@ const lockedPaymentViaLnSteps = async ({ displayCurrency: senderDisplayCurrency, paymentAmounts: paymentFlow, - pubkey: outgoingNodePubkey || lndService.defaultPubkey(), + pubkey: outgoingNodePubkey, paymentHash, feeKnownInAdvance: !!rawRoute, diff --git a/core/api/src/services/ledger/facade/tx-metadata.ts b/core/api/src/services/ledger/facade/tx-metadata.ts index e2c0c8add0..dc20e4fb9f 100644 --- a/core/api/src/services/ledger/facade/tx-metadata.ts +++ b/core/api/src/services/ledger/facade/tx-metadata.ts @@ -127,7 +127,7 @@ export const LnSendLedgerMetadata = ({ memoOfPayer, }: { paymentHash: PaymentHash - pubkey: Pubkey + pubkey: Pubkey | undefined paymentAmounts: AmountsAndFees feeDisplayCurrency: DisplayCurrencyBaseAmount diff --git a/core/api/src/services/ledger/index.types.d.ts b/core/api/src/services/ledger/index.types.d.ts index f358586afb..6c646ea191 100644 --- a/core/api/src/services/ledger/index.types.d.ts +++ b/core/api/src/services/ledger/index.types.d.ts @@ -95,7 +95,7 @@ type AddLnSendLedgerMetadata = LedgerMetadata & LedgerSendMetadata & SendAmountsMetadata & { hash: PaymentHash - pubkey: Pubkey + pubkey: Pubkey | undefined feeKnownInAdvance: boolean } From 3bce327806084bf4130d2d95c4c3c768e5f15f2a Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Fri, 3 May 2024 17:58:12 -0400 Subject: [PATCH 04/10] fix(core): use correct pubkey in payViaPaymentDetails --- core/api/src/services/lnd/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/api/src/services/lnd/index.ts b/core/api/src/services/lnd/index.ts index 02f5f036d6..9854de4160 100644 --- a/core/api/src/services/lnd/index.ts +++ b/core/api/src/services/lnd/index.ts @@ -111,9 +111,6 @@ 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 listActiveLndsWithPubkeys = (): { lnd: AuthenticatedLnd; pubkey: Pubkey }[] => getLnds({ active: true, type: "offchain" }).map((lndAuth) => ({ lnd: lndAuth.lnd, @@ -814,11 +811,13 @@ export const LndService = (): ILightningService | LightningServiceError => { const payInvoiceViaPaymentDetailsWithLnd = async ({ lnd, + pubkey, decodedInvoice, btcPaymentAmount, maxFeeAmount, }: { lnd: AuthenticatedLnd + pubkey: Pubkey decodedInvoice: LnInvoice btcPaymentAmount: BtcPaymentAmount maxFeeAmount: BtcPaymentAmount | undefined @@ -877,7 +876,7 @@ export const LndService = (): ILightningService | LightningServiceError => { return { roundedUpFee: toSats(paymentResult.safe_fee), revealedPreImage: paymentResult.secret as RevealedPreImage, - sentFromPubkey: defaultPubkey, + sentFromPubkey: pubkey, } } catch (err) { if (err instanceof Error && err.message === "Timeout") { @@ -897,10 +896,11 @@ export const LndService = (): ILightningService | LightningServiceError => { btcPaymentAmount: BtcPaymentAmount maxFeeAmount: BtcPaymentAmount | undefined }): Promise => { - const lnds = listActiveLnd() - for (const lnd of lnds) { + const lnds = listActiveLndsWithPubkeys() + for (const { lnd, pubkey } of lnds) { const result = await payInvoiceViaPaymentDetailsWithLnd({ lnd, + pubkey, decodedInvoice, btcPaymentAmount, maxFeeAmount, From 46ec0973b0ff2ca75ab2ef6465c8a44ad1974ccd Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Mon, 6 May 2024 08:47:18 -0400 Subject: [PATCH 05/10] refactor(core): add 'sentFromPubkey' field to LnPaymentLookup types --- .../domain/bitcoin/lightning/index.types.d.ts | 2 ++ core/api/src/services/lnd/index.ts | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/api/src/domain/bitcoin/lightning/index.types.d.ts b/core/api/src/domain/bitcoin/lightning/index.types.d.ts index 362b833b9d..679931dcb9 100644 --- a/core/api/src/domain/bitcoin/lightning/index.types.d.ts +++ b/core/api/src/domain/bitcoin/lightning/index.types.d.ts @@ -91,10 +91,12 @@ type LnPaymentLookup = { readonly confirmedDetails: LnPaymentConfirmedDetails | undefined readonly attempts: LnPaymentAttempt[] | undefined + readonly sentFromPubkey: Pubkey } type LnFailedPartialPaymentLookup = { readonly status: FailedPaymentStatus + readonly sentFromPubkey: Pubkey } type LnInvoice = { diff --git a/core/api/src/services/lnd/index.ts b/core/api/src/services/lnd/index.ts index 9854de4160..545967940d 100644 --- a/core/api/src/services/lnd/index.ts +++ b/core/api/src/services/lnd/index.ts @@ -614,7 +614,9 @@ export const LndService = (): ILightningService | LightningServiceError => { try { const { payments, next } = await getPaymentsFn({ lnd, ...pagingArgs }) return { - lnPayments: payments.map(translateLnPaymentLookup), + lnPayments: payments.map((p) => + translateLnPaymentLookup({ p, sentFromPubkey: pubkey }), + ), endCursor: (next as PagingContinueToken) || false, } } catch (err) { @@ -1023,6 +1025,7 @@ const lookupPaymentByPubkeyAndHash = async ({ hopPubkeys: undefined, }, attempts: undefined, + sentFromPubkey: pubkey, } } @@ -1036,11 +1039,12 @@ const lookupPaymentByPubkeyAndHash = async ({ roundedUpAmount: toSats(pending.safe_tokens), confirmedDetails: undefined, attempts: undefined, + sentFromPubkey: pubkey, } } if (status === PaymentStatus.Failed) { - return { status: PaymentStatus.Failed } + return { status: PaymentStatus.Failed, sentFromPubkey: pubkey } } return new BadPaymentDataError(JSON.stringify(result)) @@ -1063,7 +1067,13 @@ const lookupPaymentByPubkeyAndHash = async ({ const isPaymentConfirmed = (p: PaymentResult): p is ConfirmedPaymentResult => p.is_confirmed -const translateLnPaymentLookup = (p: PaymentResult): LnPaymentLookup => ({ +const translateLnPaymentLookup = ({ + p, + sentFromPubkey, +}: { + p: PaymentResult + sentFromPubkey: Pubkey +}): LnPaymentLookup => ({ createdAt: new Date(p.created_at), status: p.is_confirmed ? PaymentStatus.Settled : PaymentStatus.Pending, paymentHash: p.id as PaymentHash, @@ -1081,6 +1091,7 @@ const translateLnPaymentLookup = (p: PaymentResult): LnPaymentLookup => ({ } : undefined, attempts: p.attempts, + sentFromPubkey, }) const translateLnInvoiceLookup = ( From de5e381609d123217a7ecaae0b620a2253932101 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Mon, 6 May 2024 08:50:25 -0400 Subject: [PATCH 06/10] refactor(core): add 'updatePubkeyByHash' call to payments use-cases --- core/api/src/app/payments/send-lightning.ts | 8 ++++++++ .../app/payments/update-pending-payments.ts | 8 ++++++++ .../services/ledger/facade/offchain-send.ts | 20 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index 22ff0a9d4d..7cbc6172e7 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -1004,6 +1004,14 @@ const lockedPaymentViaLnSteps = async ({ const settled = await LedgerFacade.settlePendingLnSend(paymentHash) if (settled instanceof Error) return LnSendAttemptResult.err(settled) + const updatedPubkey = await LedgerFacade.updatePubkeyByHash({ + paymentHash, + pubkey: payResult.sentFromPubkey, + }) + if (updatedPubkey instanceof Error) { + recordExceptionInCurrentSpan({ error: updatedPubkey }) + } + if (!rawRoute) { const reimbursed = await reimburseFee({ paymentFlow, diff --git a/core/api/src/app/payments/update-pending-payments.ts b/core/api/src/app/payments/update-pending-payments.ts index 4f2d4e4015..45cc95eb5f 100644 --- a/core/api/src/app/payments/update-pending-payments.ts +++ b/core/api/src/app/payments/update-pending-payments.ts @@ -329,6 +329,14 @@ const lockedPendingPaymentSteps = async ({ paymentLogger.error({ error: settled }, "no transaction to update") return settled } + const updatedPubkey = await LedgerFacade.updatePubkeyByHash({ + paymentHash, + pubkey: lnPaymentLookup.sentFromPubkey, + }) + if (updatedPubkey instanceof Error) { + paymentLogger.error({ error: updatedPubkey }, "no transaction to update") + return updatedPubkey + } let roundedUpFee: Satoshis = toSats(0) let satsAmount: Satoshis | undefined = undefined diff --git a/core/api/src/services/ledger/facade/offchain-send.ts b/core/api/src/services/ledger/facade/offchain-send.ts index dc42a708c5..e0792fe9db 100644 --- a/core/api/src/services/ledger/facade/offchain-send.ts +++ b/core/api/src/services/ledger/facade/offchain-send.ts @@ -11,6 +11,7 @@ import { staticAccountIds } from "./static-account-ids" import { UnknownLedgerError } from "@/domain/ledger" import { ZERO_CENTS, ZERO_SATS } from "@/domain/shared" +import { NoTransactionToUpdateError } from "@/domain/errors" export const recordSendOffChain = async ({ description, @@ -91,3 +92,22 @@ export const settlePendingLnSend = async ( return new UnknownLedgerError(err) } } + +export const updatePubkeyByHash = async ({ + paymentHash, + pubkey, +}: { + paymentHash: PaymentHash + pubkey: Pubkey +}): Promise => { + try { + const result = await Transaction.updateMany({ hash: paymentHash }, { pubkey }) + const success = result.modifiedCount > 0 + if (!success) { + return new NoTransactionToUpdateError(paymentHash) + } + return true + } catch (err) { + return new UnknownLedgerError(err) + } +} From 3b24b9294f15a2b27a84709d778ad3f799d7fc3c Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Mon, 6 May 2024 10:02:17 -0400 Subject: [PATCH 07/10] refactor(core): use new 'LnPaymentAttemptResult' type in LnService pay methods --- .../domain/bitcoin/lightning/index.types.d.ts | 31 ++++++++- .../bitcoin/lightning/ln-payment-result.ts | 25 +++++++ core/api/src/services/lnd/index.ts | 66 ++++++++++++++----- .../integration/services/lnd-service.spec.ts | 35 ++++++---- 4 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 core/api/src/domain/bitcoin/lightning/ln-payment-result.ts diff --git a/core/api/src/domain/bitcoin/lightning/index.types.d.ts b/core/api/src/domain/bitcoin/lightning/index.types.d.ts index 679931dcb9..743c380277 100644 --- a/core/api/src/domain/bitcoin/lightning/index.types.d.ts +++ b/core/api/src/domain/bitcoin/lightning/index.types.d.ts @@ -137,6 +137,10 @@ type PayInvoiceResult = { sentFromPubkey: Pubkey } +type PayInvoicePartialResult = { + sentFromPubkey: Pubkey +} + type ListLnPaymentsArgs = { after: PagingStartToken | PagingContinueToken pubkey: Pubkey @@ -160,6 +164,29 @@ type ListLnInvoicesArgs = { createdAfter?: Date } +type LnPaymentAttemptResultTypeObj = + typeof import("./ln-payment-result").LnPaymentAttemptResultType +type LnPaymentAttemptResultType = + LnPaymentAttemptResultTypeObj[keyof LnPaymentAttemptResultTypeObj] + +type LnPaymentAttemptResult = + | { + type: LnPaymentAttemptResultTypeObj["Ok"] + result: PayInvoiceResult + } + | { + type: LnPaymentAttemptResultTypeObj["Pending"] + result: PayInvoicePartialResult + } + | { + type: LnPaymentAttemptResultTypeObj["AlreadyPaid"] + result: PayInvoicePartialResult + } + | { + type: LnPaymentAttemptResultTypeObj["Error"] + error: LightningServiceError + } + interface ILightningService { isLocal(pubkey: Pubkey): boolean @@ -278,7 +305,7 @@ interface ILightningService { paymentHash: PaymentHash rawRoute: RawRoute | undefined pubkey: Pubkey | undefined - }): Promise + }): Promise payInvoiceViaPaymentDetails({ decodedInvoice, @@ -288,7 +315,7 @@ interface ILightningService { decodedInvoice: LnInvoice btcPaymentAmount: BtcPaymentAmount maxFeeAmount: BtcPaymentAmount | undefined - }): Promise + }): Promise } // from Alex Bosworth invoice library diff --git a/core/api/src/domain/bitcoin/lightning/ln-payment-result.ts b/core/api/src/domain/bitcoin/lightning/ln-payment-result.ts new file mode 100644 index 0000000000..880f15c41b --- /dev/null +++ b/core/api/src/domain/bitcoin/lightning/ln-payment-result.ts @@ -0,0 +1,25 @@ +export const LnPaymentAttemptResultType = { + Ok: "success", + Pending: "pending", + AlreadyPaid: "alreadyPaid", + Error: "error", +} as const + +export const LnPaymentAttemptResult = { + ok: (result: PayInvoiceResult): LnPaymentAttemptResult => ({ + type: LnPaymentAttemptResultType.Ok, + result, + }), + pending: (result: PayInvoicePartialResult): LnPaymentAttemptResult => ({ + type: LnPaymentAttemptResultType.Pending, + result, + }), + alreadyPaid: (result: PayInvoicePartialResult): LnPaymentAttemptResult => ({ + type: LnPaymentAttemptResultType.AlreadyPaid, + result, + }), + err: (error: LightningServiceError): LnPaymentAttemptResult => ({ + type: LnPaymentAttemptResultType.Error, + error, + }), +} diff --git a/core/api/src/services/lnd/index.ts b/core/api/src/services/lnd/index.ts index 545967940d..4fefbaec2d 100644 --- a/core/api/src/services/lnd/index.ts +++ b/core/api/src/services/lnd/index.ts @@ -56,8 +56,6 @@ import { InvoiceExpiredOrBadPaymentHashError, InvoiceNotFoundError, LightningServiceError, - LnAlreadyPaidError, - LnPaymentPendingError, LookupPaymentTimedOutError, OffChainServiceBusyError, OffChainServiceUnavailableError, @@ -89,6 +87,10 @@ import { LocalCacheService } from "@/services/cache" import { wrapAsyncFunctionsToRunInSpan } from "@/services/tracing" import { timeoutWithCancel } from "@/utils" +import { + LnPaymentAttemptResult, + LnPaymentAttemptResultType, +} from "@/domain/bitcoin/lightning/ln-payment-result" const TIMEOUT_PAYMENT = NETWORK !== "regtest" ? 45000 : 3000 @@ -773,13 +775,13 @@ export const LndService = (): ILightningService | LightningServiceError => { paymentHash: PaymentHash rawRoute: RawRoute pubkey: Pubkey - }): Promise => { + }): Promise => { let cancelTimeout = () => { return } try { const lnd = getLndFromPubkey({ pubkey }) - if (lnd instanceof Error) return lnd + if (lnd instanceof Error) return LnPaymentAttemptResult.err(lnd) const paymentPromise = payViaRoutes({ lnd, @@ -797,17 +799,27 @@ export const LndService = (): ILightningService | LightningServiceError => { ])) as PayViaRoutesResult cancelTimeout() - return { + return LnPaymentAttemptResult.ok({ roundedUpFee: toSats(paymentResult.safe_fee), revealedPreImage: paymentResult.secret as RevealedPreImage, sentFromPubkey: pubkey, - } + }) } catch (err) { if (err instanceof Error && err.message === "Timeout") { - return new LnPaymentPendingError() + return LnPaymentAttemptResult.pending({ + sentFromPubkey: pubkey, + }) } cancelTimeout() - return handleSendPaymentLndErrors({ err, paymentHash }) + + const errDetails = parseLndErrorDetails(err) + if (KnownLndErrorDetails.InvoiceAlreadyPaid.test(errDetails)) { + return LnPaymentAttemptResult.alreadyPaid({ + sentFromPubkey: pubkey, + }) + } + + return LnPaymentAttemptResult.err(handleSendPaymentLndErrors({ err, paymentHash })) } } @@ -823,7 +835,7 @@ export const LndService = (): ILightningService | LightningServiceError => { decodedInvoice: LnInvoice btcPaymentAmount: BtcPaymentAmount maxFeeAmount: BtcPaymentAmount | undefined - }): Promise => { + }): Promise => { const milliSatsAmount = btcPaymentAmount.amount * 1000n const maxFee = maxFeeAmount !== undefined ? Number(maxFeeAmount.amount) : undefined @@ -875,17 +887,29 @@ export const LndService = (): ILightningService | LightningServiceError => { ])) as PayViaPaymentDetailsResult cancelTimeout() - return { + return LnPaymentAttemptResult.ok({ roundedUpFee: toSats(paymentResult.safe_fee), revealedPreImage: paymentResult.secret as RevealedPreImage, sentFromPubkey: pubkey, - } + }) } catch (err) { if (err instanceof Error && err.message === "Timeout") { - return new LnPaymentPendingError() + return LnPaymentAttemptResult.pending({ + sentFromPubkey: pubkey, + }) } cancelTimeout() - return handleSendPaymentLndErrors({ err, paymentHash: decodedInvoice.paymentHash }) + + const errDetails = parseLndErrorDetails(err) + if (KnownLndErrorDetails.InvoiceAlreadyPaid.test(errDetails)) { + return LnPaymentAttemptResult.alreadyPaid({ + sentFromPubkey: pubkey, + }) + } + + return LnPaymentAttemptResult.err( + handleSendPaymentLndErrors({ err, paymentHash: decodedInvoice.paymentHash }), + ) } } @@ -897,7 +921,7 @@ export const LndService = (): ILightningService | LightningServiceError => { decodedInvoice: LnInvoice btcPaymentAmount: BtcPaymentAmount maxFeeAmount: BtcPaymentAmount | undefined - }): Promise => { + }): Promise => { const lnds = listActiveLndsWithPubkeys() for (const { lnd, pubkey } of lnds) { const result = await payInvoiceViaPaymentDetailsWithLnd({ @@ -907,11 +931,19 @@ export const LndService = (): ILightningService | LightningServiceError => { btcPaymentAmount, maxFeeAmount, }) - if (isConnectionError(result)) continue + if ( + result.type === LnPaymentAttemptResultType.Error && + isConnectionError(result.error) + ) { + continue + } + return result } - return new OffChainServiceUnavailableError("no active lightning node (for offchain)") + return LnPaymentAttemptResult.err( + new OffChainServiceUnavailableError("no active lightning node (for offchain)"), + ) } return wrapAsyncFunctionsToRunInSpan({ @@ -1196,8 +1228,6 @@ const handleSendPaymentLndErrors = ({ const errDetails = parseLndErrorDetails(err) const match = (knownErrDetail: RegExp): boolean => knownErrDetail.test(errDetails) switch (true) { - case match(KnownLndErrorDetails.InvoiceAlreadyPaid): - return new LnAlreadyPaidError() case match(KnownLndErrorDetails.UnableToFindRoute): case match(KnownLndErrorDetails.FailedToFindRoute): return new RouteNotFoundError() diff --git a/core/api/test/integration/services/lnd-service.spec.ts b/core/api/test/integration/services/lnd-service.spec.ts index 07ddf39181..103c3ef1f7 100644 --- a/core/api/test/integration/services/lnd-service.spec.ts +++ b/core/api/test/integration/services/lnd-service.spec.ts @@ -15,7 +15,6 @@ import { WalletCurrency } from "@/domain/shared" import { toSats } from "@/domain/bitcoin" import { InvoiceNotFoundError, - LnAlreadyPaidError, PaymentNotFoundError, PaymentRejectedByDestinationError, PaymentStatus, @@ -43,6 +42,7 @@ import { waitFor, } from "test/helpers" import { BitcoindWalletClient } from "test/helpers/bitcoind" +import { LnPaymentAttemptResultType } from "@/domain/bitcoin/lightning/ln-payment-result" const amountInvoice = toSats(1000) const btcPaymentAmount = { amount: BigInt(amountInvoice), currency: WalletCurrency.Btc } @@ -236,15 +236,18 @@ describe("Lnd", () => { btcPaymentAmount, maxFeeAmount: undefined, }) - if (paid instanceof Error) throw paid - expect(paid.revealedPreImage).toHaveLength(64) + if (paid.type === LnPaymentAttemptResultType.Error) throw paid.error + if (!(paid.type === LnPaymentAttemptResultType.Ok)) { + throw new Error(JSON.stringify(paid)) + } + expect(paid.result.revealedPreImage).toHaveLength(64) const retryPaid = await lndService.payInvoiceViaPaymentDetails({ decodedInvoice: lnInvoice, btcPaymentAmount, maxFeeAmount: undefined, }) - expect(retryPaid).toBeInstanceOf(LnAlreadyPaidError) + expect(retryPaid.type).toEqual(LnPaymentAttemptResultType.AlreadyPaid) }) it("fails to pay when channel capacity exceeded", async () => { @@ -257,7 +260,9 @@ describe("Lnd", () => { btcPaymentAmount, maxFeeAmount: undefined, }) - expect(paid).toBeInstanceOf(PaymentRejectedByDestinationError) + expect(paid.type === LnPaymentAttemptResultType.Error && paid.error).toBeInstanceOf( + PaymentRejectedByDestinationError, + ) }) it("pay invoice with High CLTV Delta", async () => { @@ -275,8 +280,10 @@ describe("Lnd", () => { btcPaymentAmount, maxFeeAmount: undefined, }) - if (paid instanceof Error) throw paid - expect(paid.revealedPreImage).toHaveLength(64) + if (paid.type === LnPaymentAttemptResultType.Error) throw paid.error + expect( + paid.type === LnPaymentAttemptResultType.Ok && paid.result.revealedPreImage, + ).toHaveLength(64) }) it("pays high fee route with no max limit", async () => { @@ -289,9 +296,13 @@ describe("Lnd", () => { btcPaymentAmount, maxFeeAmount: undefined, }) - if (paid instanceof Error) throw paid - expect(paid.revealedPreImage).toHaveLength(64) - expect(paid.roundedUpFee).toEqual( + if (paid.type === LnPaymentAttemptResultType.Error) throw paid.error + if (!(paid.type === LnPaymentAttemptResultType.Ok)) { + throw new Error(JSON.stringify(paid)) + } + expect(paid.result.revealedPreImage).toHaveLength(64) + + expect(paid.result.roundedUpFee).toEqual( Number(btcPaymentAmount.amount) * ROUTE_PPM_PERCENT, ) }) @@ -306,7 +317,9 @@ describe("Lnd", () => { btcPaymentAmount, maxFeeAmount: LnFees().maxProtocolAndBankFee(btcPaymentAmount), }) - expect(paid).toBeInstanceOf(RouteNotFoundError) + expect(paid.type === LnPaymentAttemptResultType.Error && paid.error).toBeInstanceOf( + RouteNotFoundError, + ) }) it("fails to probe across route with fee higher than payment amount", async () => { From f3bc86da3d4ffc3a90b101ea826aa9e63d8d9bbf Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Mon, 6 May 2024 10:52:18 -0400 Subject: [PATCH 08/10] refactor(core): use new 'LnPaymentAttemptResult' type in use-cases --- core/api/src/app/payments/send-lightning.ts | 59 +++++++++++-------- .../app/payments/update-pending-payments.ts | 8 --- .../api/src/domain/bitcoin/lightning/index.ts | 1 + core/api/src/services/lnd/index.ts | 6 +- .../app/wallets/send-lightning.spec.ts | 19 ++++-- .../integration/services/lnd-service.spec.ts | 2 +- 6 files changed, 52 insertions(+), 43 deletions(-) diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index 7cbc6172e7..e6d6878745 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -12,10 +12,11 @@ import { AccountValidator } from "@/domain/accounts" import { decodeInvoice, defaultTimeToExpiryInSeconds, - LnAlreadyPaidError, - LnPaymentPendingError, PaymentSendStatus, + LnPaymentAttemptResult, + LnPaymentAttemptResultType, } from "@/domain/bitcoin/lightning" +import { ResourceExpiredLockServiceError } from "@/domain/lock" import { AlreadyPaidError, CouldNotFindLightningPaymentFlowError } from "@/domain/errors" import { DisplayAmountsConverter } from "@/domain/fiat" import { @@ -74,8 +75,6 @@ import { validateIsUsdWallet, } from "@/app/wallets" -import { ResourceExpiredLockServiceError } from "@/domain/lock" - const dealer = DealerPriceService() const paymentFlowRepo = PaymentFlowStateRepository(defaultTimeToExpiryInSeconds) @@ -916,7 +915,7 @@ const lockedPaymentViaLnSteps = async ({ const { journalId } = journal // Execute payment - let payResult: PayInvoiceResult | LightningServiceError + let payResult: LnPaymentAttemptResult if (rawRoute) { payResult = await lndService.payInvoiceViaRoutes({ paymentHash, @@ -937,28 +936,44 @@ const lockedPaymentViaLnSteps = async ({ payResult = maxFeeCheck instanceof Error - ? maxFeeCheck + ? LnPaymentAttemptResult.err(maxFeeCheck) : await lndService.payInvoiceViaPaymentDetails({ ...maxFeeCheckArgs, decodedInvoice, }) } - if (!(payResult instanceof LnAlreadyPaidError)) { + if (!(payResult.type === LnPaymentAttemptResultType.AlreadyPaid)) { await LnPaymentsRepository().persistNew({ paymentHash: decodedInvoice.paymentHash, paymentRequest: decodedInvoice.paymentRequest, - sentFromPubkey: outgoingNodePubkey, + sentFromPubkey: + payResult.type === LnPaymentAttemptResultType.Error + ? outgoingNodePubkey + : payResult.result.sentFromPubkey, }) - if (!(payResult instanceof Error)) + if (payResult.type === LnPaymentAttemptResultType.Ok) await LedgerFacade.updateMetadataByHash({ hash: paymentHash, - revealedPreImage: payResult.revealedPreImage, + revealedPreImage: payResult.result.revealedPreImage, }) } - if (payResult instanceof LnPaymentPendingError) { + if ( + outgoingNodePubkey === undefined && + !(payResult.type === LnPaymentAttemptResultType.Error) + ) { + const updatedPubkey = await LedgerFacade.updatePubkeyByHash({ + paymentHash, + pubkey: payResult.result.sentFromPubkey, + }) + if (updatedPubkey instanceof Error) { + recordExceptionInCurrentSpan({ error: updatedPubkey }) + } + } + + if (payResult.type === LnPaymentAttemptResultType.Pending) { paymentFlow.paymentSentAndPending = true const updateResult = await paymentFlowRepo.updateLightningPaymentFlow(paymentFlow) if (updateResult instanceof Error) { @@ -977,7 +992,10 @@ const lockedPaymentViaLnSteps = async ({ } // Settle and record reversion entries - if (payResult instanceof Error) { + if ( + payResult.type === LnPaymentAttemptResultType.Error || + payResult.type === LnPaymentAttemptResultType.AlreadyPaid + ) { const settled = await LedgerFacade.settlePendingLnSend(paymentHash) if (settled instanceof Error) return LnSendAttemptResult.err(settled) @@ -995,31 +1013,24 @@ const lockedPaymentViaLnSteps = async ({ if (updateJournalTxnsState instanceof Error) { return LnSendAttemptResult.err(updateJournalTxnsState) } - return payResult instanceof LnAlreadyPaidError + + return payResult.type === LnPaymentAttemptResultType.AlreadyPaid ? LnSendAttemptResult.alreadyPaid(journalId) - : LnSendAttemptResult.errWithJournal({ journalId, error: payResult }) + : LnSendAttemptResult.errWithJournal({ journalId, error: payResult.error }) } // Settle and conditionally record reimbursement entries const settled = await LedgerFacade.settlePendingLnSend(paymentHash) if (settled instanceof Error) return LnSendAttemptResult.err(settled) - const updatedPubkey = await LedgerFacade.updatePubkeyByHash({ - paymentHash, - pubkey: payResult.sentFromPubkey, - }) - if (updatedPubkey instanceof Error) { - recordExceptionInCurrentSpan({ error: updatedPubkey }) - } - if (!rawRoute) { const reimbursed = await reimburseFee({ paymentFlow, senderDisplayAmount: toDisplayBaseAmount(displayAmount), senderDisplayCurrency, journalId, - actualFee: payResult.roundedUpFee, - revealedPreImage: payResult.revealedPreImage, + actualFee: payResult.result.roundedUpFee, + revealedPreImage: payResult.result.revealedPreImage, }) if (reimbursed instanceof Error) return LnSendAttemptResult.err(reimbursed) } diff --git a/core/api/src/app/payments/update-pending-payments.ts b/core/api/src/app/payments/update-pending-payments.ts index 45cc95eb5f..4f2d4e4015 100644 --- a/core/api/src/app/payments/update-pending-payments.ts +++ b/core/api/src/app/payments/update-pending-payments.ts @@ -329,14 +329,6 @@ const lockedPendingPaymentSteps = async ({ paymentLogger.error({ error: settled }, "no transaction to update") return settled } - const updatedPubkey = await LedgerFacade.updatePubkeyByHash({ - paymentHash, - pubkey: lnPaymentLookup.sentFromPubkey, - }) - if (updatedPubkey instanceof Error) { - paymentLogger.error({ error: updatedPubkey }, "no transaction to update") - return updatedPubkey - } let roundedUpFee: Satoshis = toSats(0) let satsAmount: Satoshis | undefined = undefined diff --git a/core/api/src/domain/bitcoin/lightning/index.ts b/core/api/src/domain/bitcoin/lightning/index.ts index 7c435381a3..7df2d983af 100644 --- a/core/api/src/domain/bitcoin/lightning/index.ts +++ b/core/api/src/domain/bitcoin/lightning/index.ts @@ -7,6 +7,7 @@ export { invoiceExpirationForCurrency, defaultTimeToExpiryInSeconds, } from "./invoice-expiration" +export * from "./ln-payment-result" export * from "./errors" export const PaymentStatus = { diff --git a/core/api/src/services/lnd/index.ts b/core/api/src/services/lnd/index.ts index 4fefbaec2d..48af91052e 100644 --- a/core/api/src/services/lnd/index.ts +++ b/core/api/src/services/lnd/index.ts @@ -76,6 +76,8 @@ import { decodeInvoice, InvalidInvoiceAmountError, InvoiceAlreadySettledError, + LnPaymentAttemptResult, + LnPaymentAttemptResultType, } from "@/domain/bitcoin/lightning" import { CacheKeys } from "@/domain/cache" import { LnFees } from "@/domain/payments" @@ -87,10 +89,6 @@ import { LocalCacheService } from "@/services/cache" import { wrapAsyncFunctionsToRunInSpan } from "@/services/tracing" import { timeoutWithCancel } from "@/utils" -import { - LnPaymentAttemptResult, - LnPaymentAttemptResultType, -} from "@/domain/bitcoin/lightning/ln-payment-result" const TIMEOUT_PAYMENT = NETWORK !== "regtest" ? 45000 : 3000 diff --git a/core/api/test/integration/app/wallets/send-lightning.spec.ts b/core/api/test/integration/app/wallets/send-lightning.spec.ts index 1b4c0bc593..3b294f44cd 100644 --- a/core/api/test/integration/app/wallets/send-lightning.spec.ts +++ b/core/api/test/integration/app/wallets/send-lightning.spec.ts @@ -8,6 +8,7 @@ import { MaxFeeTooLargeForRoutelessPaymentError, PaymentSendStatus, decodeInvoice, + LnPaymentAttemptResultType, } from "@/domain/bitcoin/lightning" import { UsdDisplayCurrency, toCents } from "@/domain/fiat" import { LnPaymentRequestNonZeroAmountRequiredError } from "@/domain/payments" @@ -430,9 +431,12 @@ describe("initiated via lightning", () => { defaultPubkey: (): Pubkey => DEFAULT_PUBKEY, listAllPubkeys: () => [], payInvoiceViaPaymentDetails: () => ({ - roundedUpFee: toSats(0), - revealedPreImage: "revealedPreImage" as RevealedPreImage, - sentFromPubkey: DEFAULT_PUBKEY, + type: LnPaymentAttemptResultType.Ok, + result: { + roundedUpFee: toSats(0), + revealedPreImage: "revealedPreImage" as RevealedPreImage, + sentFromPubkey: DEFAULT_PUBKEY, + }, }), }) @@ -549,9 +553,12 @@ describe("initiated via lightning", () => { defaultPubkey: (): Pubkey => DEFAULT_PUBKEY, listAllPubkeys: () => [], payInvoiceViaPaymentDetails: () => ({ - roundedUpFee: toSats(0), - revealedPreImage: "revealedPreImage" as RevealedPreImage, - sentFromPubkey: DEFAULT_PUBKEY, + type: LnPaymentAttemptResultType.Ok, + result: { + roundedUpFee: toSats(0), + revealedPreImage: "revealedPreImage" as RevealedPreImage, + sentFromPubkey: DEFAULT_PUBKEY, + }, }), }) diff --git a/core/api/test/integration/services/lnd-service.spec.ts b/core/api/test/integration/services/lnd-service.spec.ts index 103c3ef1f7..18d47248d4 100644 --- a/core/api/test/integration/services/lnd-service.spec.ts +++ b/core/api/test/integration/services/lnd-service.spec.ts @@ -20,6 +20,7 @@ import { PaymentStatus, RouteNotFoundError, decodeInvoice, + LnPaymentAttemptResultType, } from "@/domain/bitcoin/lightning" import { LnFees } from "@/domain/payments" @@ -42,7 +43,6 @@ import { waitFor, } from "test/helpers" import { BitcoindWalletClient } from "test/helpers/bitcoind" -import { LnPaymentAttemptResultType } from "@/domain/bitcoin/lightning/ln-payment-result" const amountInvoice = toSats(1000) const btcPaymentAmount = { amount: BigInt(amountInvoice), currency: WalletCurrency.Btc } From 79341a7423acd7f1934df9882f4571aa8372c135 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Mon, 6 May 2024 10:52:33 -0400 Subject: [PATCH 09/10] chore(core): remove unused ln payment types --- core/api/src/domain/bitcoin/lightning/errors.ts | 2 -- core/api/src/graphql/error-map.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/core/api/src/domain/bitcoin/lightning/errors.ts b/core/api/src/domain/bitcoin/lightning/errors.ts index 0f8ab8fd23..561ebefbe5 100644 --- a/core/api/src/domain/bitcoin/lightning/errors.ts +++ b/core/api/src/domain/bitcoin/lightning/errors.ts @@ -20,8 +20,6 @@ export class SecretDoesNotMatchAnyExistingHodlInvoiceError extends LightningServ export class InvoiceNotFoundError extends LightningServiceError {} export class InvoiceAlreadySettledError extends LightningServiceError {} -export class LnPaymentPendingError extends LightningServiceError {} -export class LnAlreadyPaidError extends LightningServiceError {} export class MaxFeeTooLargeForRoutelessPaymentError extends LightningServiceError { level = ErrorLevel.Critical } diff --git a/core/api/src/graphql/error-map.ts b/core/api/src/graphql/error-map.ts index 6b5852c404..52bef7673f 100644 --- a/core/api/src/graphql/error-map.ts +++ b/core/api/src/graphql/error-map.ts @@ -563,8 +563,6 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { case "InvoiceNotFoundError": case "InvoiceAlreadySettledError": case "InvoiceNotPaidError": - case "LnPaymentPendingError": - case "LnAlreadyPaidError": case "PaymentNotFoundError": case "OperationInterruptedError": case "InconsistentDataError": From 6513270bf85883c2b9fd4accc4c45888e42922b4 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Mon, 6 May 2024 11:49:03 -0400 Subject: [PATCH 10/10] revert(core): lookup payment only by payment hash (#4420) This reverts commit dc50d894c1c15cd3ba77a4eb95548ec1fb61c221. --- core/api/src/app/payments/update-pending-payments.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/api/src/app/payments/update-pending-payments.ts b/core/api/src/app/payments/update-pending-payments.ts index 4f2d4e4015..e20accf0af 100644 --- a/core/api/src/app/payments/update-pending-payments.ts +++ b/core/api/src/app/payments/update-pending-payments.ts @@ -172,7 +172,10 @@ const updatePendingPayment = wrapAsyncToRunInSpan({ const lndService = LndService() if (lndService instanceof Error) return lndService - const lnPaymentLookup = await lndService.lookupPayment({ paymentHash }) + const lnPaymentLookup = await lndService.lookupPayment({ + pubkey, + paymentHash, + }) if (lnPaymentLookup instanceof Error) { logger.error( {