diff --git a/core/api/src/app/wallets/update-pending-invoices.ts b/core/api/src/app/wallets/update-pending-invoices.ts index 6c543ddf549..8a2bc37a08c 100644 --- a/core/api/src/app/wallets/update-pending-invoices.ts +++ b/core/api/src/app/wallets/update-pending-invoices.ts @@ -116,10 +116,11 @@ export const declineHeldInvoice = wrapAsyncToRunInSpan({ }) if (lnInvoiceLookup instanceof InvoiceNotFoundError) { - const isDeleted = await walletInvoicesRepo.deleteByPaymentHash(paymentHash) - if (isDeleted instanceof Error) { - pendingInvoiceLogger.error("impossible to delete WalletInvoice entry") - return isDeleted + const processingCompletedInvoice = + await walletInvoicesRepo.markAsProcessingCompleted(paymentHash) + if (processingCompletedInvoice instanceof Error) { + pendingInvoiceLogger.error("Unable to mark invoice as processingCompleted") + return processingCompletedInvoice } return false } @@ -148,9 +149,10 @@ export const declineHeldInvoice = wrapAsyncToRunInSpan({ const invoiceSettled = await lndService.cancelInvoice({ pubkey, paymentHash }) if (invoiceSettled instanceof Error) return invoiceSettled - const isDeleted = await walletInvoicesRepo.deleteByPaymentHash(paymentHash) - if (isDeleted instanceof Error) { - pendingInvoiceLogger.error("impossible to delete WalletInvoice entry") + const processingCompletedInvoice = + await walletInvoicesRepo.markAsProcessingCompleted(paymentHash) + if (processingCompletedInvoice instanceof Error) { + pendingInvoiceLogger.error("Unable to mark invoice as processingCompleted") } return true @@ -211,10 +213,11 @@ const updatePendingInvoiceBeforeFinally = async ({ const lnInvoiceLookup = await lndService.lookupInvoice({ pubkey, paymentHash }) if (lnInvoiceLookup instanceof InvoiceNotFoundError) { - const isDeleted = await walletInvoicesRepo.deleteByPaymentHash(paymentHash) - if (isDeleted instanceof Error) { - pendingInvoiceLogger.error("impossible to delete WalletInvoice entry") - return isDeleted + const processingCompletedInvoice = + await walletInvoicesRepo.markAsProcessingCompleted(paymentHash) + if (processingCompletedInvoice instanceof Error) { + pendingInvoiceLogger.error("Unable to mark invoice as processingCompleted") + return processingCompletedInvoice } return false } @@ -230,6 +233,17 @@ const updatePendingInvoiceBeforeFinally = async ({ return true } + if (lnInvoiceLookup.isCanceled) { + pendingInvoiceLogger.info("invoice has been canceled") + const processingCompletedInvoice = + await walletInvoicesRepo.markAsProcessingCompleted(paymentHash) + if (processingCompletedInvoice instanceof Error) { + pendingInvoiceLogger.error("Unable to mark invoice as processingCompleted") + return processingCompletedInvoice + } + return false + } + if (!lnInvoiceLookup.isHeld && !lnInvoiceLookup.isSettled) { pendingInvoiceLogger.info("invoice has not been paid yet") return false @@ -302,6 +316,7 @@ const updatePendingInvoiceBeforeFinally = async ({ "invoices.finalRecipient": JSON.stringify(recipientWalletDescriptor), }) + // Is this crossing the lock boundary? if (!lnInvoiceLookup.isSettled) { const invoiceSettled = await lndService.settleInvoice({ pubkey, secret }) if (invoiceSettled instanceof Error) return invoiceSettled diff --git a/core/api/src/debug/migrate-ln-payments-trackpaymentsv2.ts b/core/api/src/debug/migrate-ln-payments-trackpaymentsv2.ts index 7efac48227a..6df1ce35d64 100644 --- a/core/api/src/debug/migrate-ln-payments-trackpaymentsv2.ts +++ b/core/api/src/debug/migrate-ln-payments-trackpaymentsv2.ts @@ -102,23 +102,23 @@ const migrateLnPayment = async ( payment.status === PaymentStatus.Pending ? partialLnPayment : payment.status === PaymentStatus.Failed - ? { - ...partialLnPayment, - status: PaymentStatus.Failed, - confirmedDetails: undefined, - attempts: undefined, - isCompleteRecord: true, - } - : { - ...partialLnPayment, - createdAt: payment.createdAt, - status: payment.status, - milliSatsAmount: payment.milliSatsAmount, - roundedUpAmount: payment.roundedUpAmount, - confirmedDetails: payment.confirmedDetails, - attempts: payment.attempts, // will be null, only comes from getPayments - isCompleteRecord: true, - } + ? { + ...partialLnPayment, + status: PaymentStatus.Failed, + confirmedDetails: undefined, + attempts: undefined, + isCompleteRecord: true, + } + : { + ...partialLnPayment, + createdAt: payment.createdAt, + status: payment.status, + milliSatsAmount: payment.milliSatsAmount, + roundedUpAmount: payment.roundedUpAmount, + confirmedDetails: payment.confirmedDetails, + attempts: payment.attempts, // will be null, only comes from getPayments + isCompleteRecord: true, + } const updatedPaymentLookup = await LnPaymentsRepository().persistNew(newLnPayment) if (updatedPaymentLookup instanceof Error) { diff --git a/core/api/src/domain/payments/payment-flow-builder.ts b/core/api/src/domain/payments/payment-flow-builder.ts index 0e1e021306d..6759af7c918 100644 --- a/core/api/src/domain/payments/payment-flow-builder.ts +++ b/core/api/src/domain/payments/payment-flow-builder.ts @@ -44,8 +44,8 @@ export const LightningPaymentFlowBuilder = ( destination === undefined ? SettlementMethod.IntraLedger : config.localNodeIds.includes(destination) - ? SettlementMethod.IntraLedger - : SettlementMethod.Lightning + ? SettlementMethod.IntraLedger + : SettlementMethod.Lightning return { settlementMethod, btcProtocolAndBankFee: @@ -533,8 +533,8 @@ const LPFBWithConversion = ( const hash = state.paymentHash ? { paymentHash: state.paymentHash } : state.intraLedgerHash - ? { intraLedgerHash: state.intraLedgerHash } - : new InvalidLightningPaymentFlowStateError() + ? { intraLedgerHash: state.intraLedgerHash } + : new InvalidLightningPaymentFlowStateError() if (hash instanceof Error) return hash return PaymentFlow({ diff --git a/core/api/src/domain/shared/error-parsers-unknown.ts b/core/api/src/domain/shared/error-parsers-unknown.ts index 7ef1ebcb800..bb5c8cc9ee2 100644 --- a/core/api/src/domain/shared/error-parsers-unknown.ts +++ b/core/api/src/domain/shared/error-parsers-unknown.ts @@ -5,9 +5,9 @@ export const parseUnknownDomainErrorFromUnknown = (error: unknown): DomainError error instanceof Error ? error : typeof error === "string" - ? new UnknownDomainError(error) - : error instanceof Object - ? new UnknownDomainError(JSON.stringify(error)) - : new UnknownDomainError("Unknown error") + ? new UnknownDomainError(error) + : error instanceof Object + ? new UnknownDomainError(JSON.stringify(error)) + : new UnknownDomainError("Unknown error") return err } diff --git a/core/api/src/domain/shared/error-parsers.ts b/core/api/src/domain/shared/error-parsers.ts index c091d837f1d..816e199f2e9 100644 --- a/core/api/src/domain/shared/error-parsers.ts +++ b/core/api/src/domain/shared/error-parsers.ts @@ -3,10 +3,10 @@ export const parseErrorMessageFromUnknown = (error: unknown): string => { error instanceof Error ? error.message : typeof error === "string" - ? error - : error instanceof Object - ? JSON.stringify(error) - : "Unknown error" + ? error + : error instanceof Object + ? JSON.stringify(error) + : "Unknown error" return errMsg } @@ -15,9 +15,9 @@ export const parseErrorFromUnknown = (error: unknown): Error => { error instanceof Error ? error : typeof error === "string" - ? new Error(error) - : error instanceof Object - ? new Error(JSON.stringify(error)) - : new Error("Unknown error") + ? new Error(error) + : error instanceof Object + ? new Error(JSON.stringify(error)) + : new Error("Unknown error") return err } diff --git a/core/api/src/domain/wallet-invoices/index.types.d.ts b/core/api/src/domain/wallet-invoices/index.types.d.ts index 4c2368fe53e..749d4094bc8 100644 --- a/core/api/src/domain/wallet-invoices/index.types.d.ts +++ b/core/api/src/domain/wallet-invoices/index.types.d.ts @@ -93,6 +93,7 @@ type WalletInvoiceWithOptionalLnInvoice = { recipientWalletDescriptor: PartialWalletDescriptor paid: boolean createdAt: Date + processingCompleted?: boolean lnInvoice?: LnInvoice // LnInvoice is optional because some older invoices don't have it } @@ -197,7 +198,7 @@ interface IWalletInvoicesRepository { yieldPending: () => AsyncGenerator | RepositoryError - deleteByPaymentHash: (paymentHash: PaymentHash) => Promise - - deleteUnpaidOlderThan: (before: Date) => Promise + markAsProcessingCompleted: ( + paymentHash: PaymentHash, + ) => Promise } diff --git a/core/api/src/graphql/admin/root/query/lightning-payment.ts b/core/api/src/graphql/admin/root/query/lightning-payment.ts index 5ab04eff18b..4fddcdbb62d 100644 --- a/core/api/src/graphql/admin/root/query/lightning-payment.ts +++ b/core/api/src/graphql/admin/root/query/lightning-payment.ts @@ -23,8 +23,8 @@ const LightningPaymentQuery = GT.Field({ const paymentRequest = !(lightningPayment instanceof Error) ? lightningPayment.paymentRequest : "paymentRequest" in lightningPaymentFromLnd - ? lightningPaymentFromLnd.paymentRequest - : undefined + ? lightningPaymentFromLnd.paymentRequest + : undefined return { ...lightningPaymentFromLnd, diff --git a/core/api/src/servers/cron.ts b/core/api/src/servers/cron.ts index 0d37e4eaa19..ec1cc2a73ab 100644 --- a/core/api/src/servers/cron.ts +++ b/core/api/src/servers/cron.ts @@ -12,7 +12,6 @@ import { } from "@/services/tracing" import { deleteExpiredLightningPaymentFlows, - deleteExpiredWalletInvoice, deleteFailedPaymentsAttemptAllLnds, updateEscrows, updateRoutingRevenues, @@ -40,10 +39,6 @@ const updateLegacyOnChainReceipt = async () => { if (txNumber instanceof Error) throw txNumber } -const deleteExpiredInvoices = async () => { - await deleteExpiredWalletInvoice() -} - const deleteExpiredPaymentFlows = async () => { await deleteExpiredLightningPaymentFlows() } @@ -85,7 +80,6 @@ const main = async () => { ...(cronConfig.rebalanceEnabled ? [rebalance] : []), ...(cronConfig.swapEnabled ? [swapOutJob] : []), deleteExpiredPaymentFlows, - deleteExpiredInvoices, deleteLndPaymentsBefore2Months, deleteFailedPaymentsAttemptAllLnds, ] diff --git a/core/api/src/services/lnd/utils.ts b/core/api/src/services/lnd/utils.ts index 114d37ec6af..4ef196dd0ba 100644 --- a/core/api/src/services/lnd/utils.ts +++ b/core/api/src/services/lnd/utils.ts @@ -35,29 +35,11 @@ import { updateLndEscrow, } from "@/services/ledger/admin-legacy" import { baseLogger } from "@/services/logger" -import { PaymentFlowStateRepository, WalletInvoicesRepository } from "@/services/mongoose" +import { PaymentFlowStateRepository } from "@/services/mongoose" import { DbMetadata } from "@/services/mongoose/schema" import { timestampDaysAgo } from "@/utils" -export const deleteExpiredWalletInvoice = async (): Promise => { - const walletInvoicesRepo = WalletInvoicesRepository() - - // this should be longer than the invoice validity time - const delta = 90 // days - - const date = new Date() - date.setDate(date.getDate() - delta) - - const result = await walletInvoicesRepo.deleteUnpaidOlderThan(date) - if (result instanceof Error) { - baseLogger.error({ error: result }, "error deleting expired invoices") - return 0 - } - - return result -} - export const deleteFailedPaymentsAttemptAllLnds = async () => { const lnds = offchainLnds for (const { lnd, socket } of lnds) { diff --git a/core/api/src/services/mongoose/payment-flow.ts b/core/api/src/services/mongoose/payment-flow.ts index 3141853ad37..6be5d45d425 100644 --- a/core/api/src/services/mongoose/payment-flow.ts +++ b/core/api/src/services/mongoose/payment-flow.ts @@ -46,8 +46,8 @@ export const PaymentFlowStateRepository = ( const hash = paymentHash ? { paymentHash } : intraLedgerHash - ? { intraLedgerHash } - : new BadInputsForFindError(JSON.stringify(args)) + ? { intraLedgerHash } + : new BadInputsForFindError(JSON.stringify(args)) if (hash instanceof Error) return hash try { @@ -192,10 +192,10 @@ const paymentFlowFromRaw = ( const hash = paymentHash ? { paymentHash: paymentHash as PaymentHash } : intraLedgerHash - ? { intraLedgerHash: intraLedgerHash as IntraLedgerHash } - : new InvalidLightningPaymentFlowStateError( - "Missing valid 'paymentHash' or 'intraLedgerHash'", - ) + ? { intraLedgerHash: intraLedgerHash as IntraLedgerHash } + : new InvalidLightningPaymentFlowStateError( + "Missing valid 'paymentHash' or 'intraLedgerHash'", + ) if (hash instanceof Error) return hash const btcPaymentAmount = paymentAmountFromNumber({ @@ -262,10 +262,10 @@ const rawFromPaymentFlow = ( const hash = paymentHash ? { paymentHash } : intraLedgerHash - ? { intraLedgerHash } - : new InvalidLightningPaymentFlowStateError( - "Missing valid 'paymentHash' or 'intraLedgerHash'", - ) + ? { intraLedgerHash } + : new InvalidLightningPaymentFlowStateError( + "Missing valid 'paymentHash' or 'intraLedgerHash'", + ) if (hash instanceof Error) return hash return { @@ -307,10 +307,10 @@ const rawIndexFromPaymentFlowIndex = ( const hash = paymentHash ? { paymentHash } : intraLedgerHash - ? { intraLedgerHash } - : new InvalidLightningPaymentFlowStateError( - "Missing valid 'paymentHash' or 'intraLedgerHash'", - ) + ? { intraLedgerHash } + : new InvalidLightningPaymentFlowStateError( + "Missing valid 'paymentHash' or 'intraLedgerHash'", + ) if (hash instanceof Error) return hash return { diff --git a/core/api/src/services/mongoose/schema.ts b/core/api/src/services/mongoose/schema.ts index 7eb7043ea42..1662e874ee7 100644 --- a/core/api/src/services/mongoose/schema.ts +++ b/core/api/src/services/mongoose/schema.ts @@ -78,6 +78,11 @@ const walletInvoiceSchema = new Schema({ default: false, }, + processingCompleted: { + type: Boolean, + default: false, + }, + paymentRequest: { type: String, }, diff --git a/core/api/src/services/mongoose/schema.types.d.ts b/core/api/src/services/mongoose/schema.types.d.ts index 3e64a08a250..6fb8c28f0a3 100644 --- a/core/api/src/services/mongoose/schema.types.d.ts +++ b/core/api/src/services/mongoose/schema.types.d.ts @@ -69,6 +69,7 @@ interface WalletInvoiceRecord { currency: string timestamp: Date selfGenerated: boolean + processingCompleted?: boolean pubkey: string paid: boolean paymentRequest?: string // optional because we historically did not store it diff --git a/core/api/src/services/mongoose/wallet-invoices.ts b/core/api/src/services/mongoose/wallet-invoices.ts index beef11f6817..ffff1de9435 100644 --- a/core/api/src/services/mongoose/wallet-invoices.ts +++ b/core/api/src/services/mongoose/wallet-invoices.ts @@ -48,7 +48,27 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { try { const walletInvoice = await WalletInvoice.findOneAndUpdate( { _id: paymentHash }, - { paid: true }, + { paid: true, processingCompleted: true }, + { + new: true, + }, + ) + if (!walletInvoice) { + return new CouldNotFindWalletInvoiceError() + } + return walletInvoiceFromRaw(walletInvoice) + } catch (err) { + return parseRepositoryError(err) + } + } + + const markAsProcessingCompleted = async ( + paymentHash: PaymentHash, + ): Promise => { + try { + const walletInvoice = await WalletInvoice.findOneAndUpdate( + { _id: paymentHash }, + { processingCompleted: true }, { new: true, }, @@ -98,7 +118,13 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { | RepositoryError { let pending try { - pending = WalletInvoice.find({ paid: false }).cursor({ + pending = WalletInvoice.find({ + paid: false, + $or: [ + { processingCompleted: false }, + { processingCompleted: { $exists: false } }, // TODO remove this after migration + ], + }).cursor({ batchSize: 100, }) } catch (error) { @@ -110,34 +136,6 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { } } - const deleteByPaymentHash = async ( - paymentHash: PaymentHash, - ): Promise => { - try { - const result = await WalletInvoice.deleteOne({ _id: paymentHash }) - if (result.deletedCount === 0) { - return new CouldNotFindWalletInvoiceError(paymentHash) - } - return true - } catch (error) { - return new UnknownRepositoryError(error) - } - } - - const deleteUnpaidOlderThan = async ( - before: Date, - ): Promise => { - try { - const result = await WalletInvoice.deleteMany({ - timestamp: { $lt: before }, - paid: false, - }) - return result.deletedCount - } catch (error) { - return new UnknownRepositoryError(error) - } - } - const findInvoicesForWallets = async ({ walletIds, paginationArgs, @@ -251,11 +249,10 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { return { persistNew, markAsPaid, + markAsProcessingCompleted, findByPaymentHash, findForWalletByPaymentHash, yieldPending, - deleteByPaymentHash, - deleteUnpaidOlderThan, findInvoicesForWallets, } } @@ -281,6 +278,7 @@ const walletInvoiceFromRaw = ( paid: result.paid as boolean, usdAmount: result.cents ? UsdPaymentAmount(BigInt(result.cents)) : undefined, createdAt: new Date(result.timestamp.getTime()), + processingCompleted: result.processingCompleted, lnInvoice, } } diff --git a/core/api/test/legacy-integration/services/lnd/utils.spec.ts b/core/api/test/legacy-integration/services/lnd/utils.spec.ts index 3ec7cc3822e..effc95de492 100644 --- a/core/api/test/legacy-integration/services/lnd/utils.spec.ts +++ b/core/api/test/legacy-integration/services/lnd/utils.spec.ts @@ -1,8 +1,8 @@ import { MS_PER_DAY, ONE_DAY } from "@/config" -import { deleteExpiredWalletInvoice, updateRoutingRevenues } from "@/services/lnd/utils" +import { updateRoutingRevenues } from "@/services/lnd/utils" import { baseLogger } from "@/services/logger" import { ledgerAdmin } from "@/services/mongodb" -import { DbMetadata, WalletInvoice } from "@/services/mongoose/schema" +import { DbMetadata } from "@/services/mongoose/schema" import { sleep, timestampDaysAgo } from "@/utils" @@ -129,21 +129,4 @@ describe("lndUtils", () => { expect((endBalance - initBalance) * 1000).toBeCloseTo(totalFees, 0) }) - - it("deletes expired WalletInvoice without throw an exception", async () => { - const delta = 90 // days - const mockDate = new Date() - mockDate.setDate(mockDate.getDate() + delta) - jest.spyOn(global.Date, "now").mockImplementation(() => new Date(mockDate).valueOf()) - - const queryDate = new Date() - queryDate.setDate(queryDate.getDate() - delta) - - const invoicesCount = await WalletInvoice.countDocuments({ - timestamp: { $lt: queryDate }, - paid: false, - }) - const result = await deleteExpiredWalletInvoice() - expect(result).toBe(invoicesCount) - }) }) diff --git a/core/api/test/legacy-integration/services/mongoose/wallet-invoices.spec.ts b/core/api/test/legacy-integration/services/mongoose/wallet-invoices.spec.ts index 93e53a2d25c..b9253713e07 100644 --- a/core/api/test/legacy-integration/services/mongoose/wallet-invoices.spec.ts +++ b/core/api/test/legacy-integration/services/mongoose/wallet-invoices.spec.ts @@ -36,7 +36,7 @@ describe("WalletInvoices", () => { expect(lookedUpInvoice).toEqual(invoiceToPersist) }) - it("updates an invoice", async () => { + it("marks an invoice as paid", async () => { const repo = WalletInvoicesRepository() const invoiceToPersist = createMockWalletInvoice(walletDescriptor) const persistResult = await repo.persistNew(invoiceToPersist) @@ -46,23 +46,33 @@ describe("WalletInvoices", () => { const updatedResult = await repo.markAsPaid(invoiceToUpdate.paymentHash) expect(updatedResult).not.toBeInstanceOf(Error) expect(updatedResult).toHaveProperty("paid", true) + expect(updatedResult).toHaveProperty("processingCompleted", true) const { paymentHash } = updatedResult as WalletInvoice const lookedUpInvoice = await repo.findByPaymentHash(paymentHash) expect(lookedUpInvoice).not.toBeInstanceOf(Error) expect(lookedUpInvoice).toEqual(updatedResult) expect(lookedUpInvoice).toHaveProperty("paid", true) + expect(updatedResult).toHaveProperty("processingCompleted", true) }) - it("deletes an invoice by hash", async () => { + it("marks an invoice as processing completed", async () => { const repo = WalletInvoicesRepository() const invoiceToPersist = createMockWalletInvoice(walletDescriptor) const persistResult = await repo.persistNew(invoiceToPersist) expect(persistResult).not.toBeInstanceOf(Error) - const { paymentHash } = persistResult as WalletInvoice - const isDeleted = await repo.deleteByPaymentHash(paymentHash) - expect(isDeleted).not.toBeInstanceOf(Error) - expect(isDeleted).toEqual(true) + const invoiceToUpdate = persistResult as WalletInvoice + const updatedResult = await repo.markAsProcessingCompleted( + invoiceToUpdate.paymentHash, + ) + expect(updatedResult).not.toBeInstanceOf(Error) + expect(updatedResult).toHaveProperty("processingCompleted", true) + + const { paymentHash } = updatedResult as WalletInvoice + const lookedUpInvoice = await repo.findByPaymentHash(paymentHash) + expect(lookedUpInvoice).not.toBeInstanceOf(Error) + expect(lookedUpInvoice).toEqual(updatedResult) + expect(lookedUpInvoice).toHaveProperty("processingCompleted", true) }) }) diff --git a/core/api/test/unit/payments/onchain-payment-flow-builder.spec.ts b/core/api/test/unit/payments/onchain-payment-flow-builder.spec.ts index 7f5a5702895..922b6aa5bfd 100644 --- a/core/api/test/unit/payments/onchain-payment-flow-builder.spec.ts +++ b/core/api/test/unit/payments/onchain-payment-flow-builder.spec.ts @@ -1122,10 +1122,10 @@ describe("OnChainPaymentFlowBuilder", () => { amountCurrency === WalletCurrency.Usd ? (sendAmount as UsdPaymentAmount) : sendAmount.amount === 1n - ? ONE_CENT - : await convertForUsdWalletToUsdWallet.usdFromBtc( - sendAmount as BtcPaymentAmount, - ) + ? ONE_CENT + : await convertForUsdWalletToUsdWallet.usdFromBtc( + sendAmount as BtcPaymentAmount, + ) checkAddress(payment) checkSettlementMethod(payment) @@ -1180,10 +1180,10 @@ describe("OnChainPaymentFlowBuilder", () => { amountCurrency === WalletCurrency.Usd ? (sendAmount as UsdPaymentAmount) : sendAmount.amount === 1n - ? ONE_CENT - : await convertForUsdWalletToUsdWallet.usdFromBtc( - sendAmount as BtcPaymentAmount, - ) + ? ONE_CENT + : await convertForUsdWalletToUsdWallet.usdFromBtc( + sendAmount as BtcPaymentAmount, + ) checkAddress(payment) checkSettlementMethod(payment)