From c2b7738b537f4cccfcfd6379cfb497cd520b2e54 Mon Sep 17 00:00:00 2001 From: maybeast <78227110+maybeast@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:46:27 +0200 Subject: [PATCH] feat: abandon swaps that trigger MC reset loop --- lib/swap/PaymentHandler.ts | 20 ++++++++++++++++++++ test/unit/swap/PaymentHandler.spec.ts | 23 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/swap/PaymentHandler.ts b/lib/swap/PaymentHandler.ts index 315510bd..95e82740 100644 --- a/lib/swap/PaymentHandler.ts +++ b/lib/swap/PaymentHandler.ts @@ -75,6 +75,7 @@ class PaymentHandler { private static readonly errCltvTooSmall = 'CLTV limit too small'; private lastResetMissionControl: number | undefined = undefined; + private missionControlResetTriggers: Map = new Map(); constructor( private readonly logger: Logger, @@ -256,10 +257,18 @@ class PaymentHandler { Date.now() - this.lastResetMissionControl >= PaymentHandler.resetMissionControlInterval ) { + if (this.hasPreviouslyTriggeredMissionControlReset(swap.preimageHash)) { + this.logger.debug( + `Canceling swap ${swap.id} because it has already triggered mission control reset`, + ); + await this.abandonSwap(swap, errorMessage); + return undefined; + } this.logger.debug( `Resetting ${lndClient.symbol} ${LndClient.serviceName} mission control`, ); this.lastResetMissionControl = Date.now(); + this.missionControlResetTriggers.set(swap.preimageHash, true); await lndClient.resetMissionControl(); } else { this.logger.debug( @@ -346,6 +355,17 @@ class PaymentHandler { `Could not pay invoice of Swap ${swap.id} because: ${errorMessage}`, ); }; + + private hasPreviouslyTriggeredMissionControlReset( + preimageHash: string, + ): boolean { + const previouslyTriggered = + this.missionControlResetTriggers.has(preimageHash); + if (previouslyTriggered) { + this.missionControlResetTriggers.delete(preimageHash); + } + return previouslyTriggered; + } } export default PaymentHandler; diff --git a/test/unit/swap/PaymentHandler.spec.ts b/test/unit/swap/PaymentHandler.spec.ts index 0cf5b1b3..8988ad34 100644 --- a/test/unit/swap/PaymentHandler.spec.ts +++ b/test/unit/swap/PaymentHandler.spec.ts @@ -9,6 +9,7 @@ import TimeoutDeltaProvider from '../../../lib/service/TimeoutDeltaProvider'; import { InvoiceType } from '../../../lib/sidecar/DecodedInvoice'; import Sidecar from '../../../lib/sidecar/Sidecar'; import ChannelNursery from '../../../lib/swap/ChannelNursery'; +import swapErrors from '../../../lib/swap/Errors'; import NodeSwitch from '../../../lib/swap/NodeSwitch'; import PaymentHandler from '../../../lib/swap/PaymentHandler'; import { Currency } from '../../../lib/wallet/WalletManager'; @@ -211,14 +212,34 @@ describe('PaymentHandler', () => { expect(btcCurrency.lndClient!.resetMissionControl).toHaveBeenCalledTimes(1); expect(handler['lastResetMissionControl']).toEqual(lastCallBefore); - // After interval, it is called again + // After interval, it is not called again for the same swap jest.useFakeTimers(); jest.advanceTimersByTime(PaymentHandler['resetMissionControlInterval'] + 1); await expect(handler.payInvoice(swap, null, undefined)).resolves.toEqual( undefined, ); + expect(btcCurrency.lndClient!.resetMissionControl).toHaveBeenCalledTimes(1); + expect(handler['lastResetMissionControl']! - Date.now()).toBeLessThan(1000); + expect(swap.update).toHaveBeenCalledTimes(1); + expect(swap.update).toHaveBeenCalledWith({ + failureReason: swapErrors.INVOICE_COULD_NOT_BE_PAID().message, + status: SwapUpdateEvent.InvoiceFailedToPay, + }); + + // After interval, it is called again for different swap + const newSwap = { + ...swap, + id: 'newTest', + preimageHash: 'newPreimageHash', + } as any as Swap; + + await expect(handler.payInvoice(newSwap, null, undefined)).resolves.toEqual( + undefined, + ); + expect(btcCurrency.lndClient!.resetMissionControl).toHaveBeenCalledTimes(2); expect(handler['lastResetMissionControl']! - Date.now()).toBeLessThan(1000); + expect(swap.update).toHaveBeenCalledTimes(1); }); });