@@ -36,6 +36,8 @@ const (
3636 defaultConfTarget = 3
3737
3838 DefaultPaymentTimeoutSeconds = 60
39+
40+ defaultInvoiceCleanupTimeout = 5 * time .Second
3941)
4042
4143var (
5759func (f * FSM ) InitHtlcAction (ctx context.Context ,
5860 _ fsm.EventContext ) fsm.EventType {
5961
62+ var event fsm.EventType
63+ invoiceNeedsCleanup := false
64+ defer func () {
65+ // If we created the private invoice but failed before persisting the
66+ // swap, cancel it so retries do not accumulate orphan invoices.
67+ if ! invoiceNeedsCleanup || event != fsm .OnError {
68+ return
69+ }
70+
71+ f .cancelSwapInvoice (ctx )
72+ }()
73+
74+ returnError := func (err error ) fsm.EventType {
75+ event = f .HandleError (err )
76+
77+ return event
78+ }
79+
6080 // Lock the deposits and transition them to the LoopingIn state.
6181 err := f .cfg .DepositManager .TransitionDeposits (
6282 ctx , f .loopIn .Deposits , deposit .OnLoopInInitiated ,
@@ -65,7 +85,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
6585 if err != nil {
6686 err = fmt .Errorf ("unable to loop-in deposits: %w" , err )
6787
68- return f . HandleError (err )
88+ return returnError (err )
6989 }
7090
7191 // Calculate the swap invoice amount. The server needs to pay us the
@@ -88,7 +108,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
88108 err = fmt .Errorf ("unable to create random swap preimage: %w" ,
89109 err )
90110
91- return f . HandleError (err )
111+ return returnError (err )
92112 }
93113 f .loopIn .SwapPreimage = swapPreimage
94114 f .loopIn .SwapHash = swapPreimage .Hash ()
@@ -100,7 +120,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
100120 if err != nil {
101121 err = fmt .Errorf ("unable to derive client htlc key: %w" , err )
102122
103- return f . HandleError (err )
123+ return returnError (err )
104124 }
105125 f .loopIn .ClientPubkey = keyDesc .PubKey
106126 f .loopIn .HtlcKeyLocator = keyDesc .KeyLocator
@@ -119,10 +139,14 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
119139 if err != nil {
120140 err = fmt .Errorf ("unable to create swap invoice: %w" , err )
121141
122- return f . HandleError (err )
142+ return returnError (err )
123143 }
124144 f .loopIn .SwapInvoice = swapInvoice
125145
146+ // From here until CreateLoopIn succeeds, any error path would otherwise
147+ // leave behind a live invoice with no persisted swap to recover it.
148+ invoiceNeedsCleanup = true
149+
126150 f .loopIn .ProtocolVersion = version .AddressProtocolVersion (
127151 version .CurrentRPCProtocolVersion (),
128152 )
@@ -149,7 +173,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
149173 err = fmt .Errorf ("unable to initiate the loop-in with the " +
150174 "server: %w" , err )
151175
152- return f . HandleError (err )
176+ return returnError (err )
153177 }
154178
155179 // Pushing empty sigs signals the server that we abandoned the swap
@@ -171,7 +195,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
171195 pushEmptySigs ()
172196 err = fmt .Errorf ("unable to parse server pubkey: %w" , err )
173197
174- return f . HandleError (err )
198+ return returnError (err )
175199 }
176200 f .loopIn .ServerPubkey = serverPubkey
177201
@@ -185,7 +209,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
185209 err = fmt .Errorf ("server response parameters are outside " +
186210 "our allowed range: %w" , err )
187211
188- return f . HandleError (err )
212+ return returnError (err )
189213 }
190214
191215 f .loopIn .HtlcCltvExpiry = loopInResp .HtlcExpiry
@@ -194,23 +218,23 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
194218 pushEmptySigs ()
195219 err = fmt .Errorf ("unable to convert server nonces: %w" , err )
196220
197- return f . HandleError (err )
221+ return returnError (err )
198222 }
199223 f .htlcServerNoncesHighFee , err = toNonces (
200224 loopInResp .HighFeeHtlcInfo .Nonces ,
201225 )
202226 if err != nil {
203227 pushEmptySigs ()
204228
205- return f . HandleError (err )
229+ return returnError (err )
206230 }
207231 f .htlcServerNoncesExtremelyHighFee , err = toNonces (
208232 loopInResp .ExtremeFeeHtlcInfo .Nonces ,
209233 )
210234 if err != nil {
211235 pushEmptySigs ()
212236
213- return f . HandleError (err )
237+ return returnError (err )
214238 }
215239
216240 // We need to defend against the server setting high fees for the htlc
@@ -232,7 +256,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
232256 log .Errorf ("server htlc tx fee is higher than the configured " +
233257 "allowed maximum: %v > %v" , fee , maxHtlcTxFee )
234258
235- return f . HandleError (ErrFeeTooHigh )
259+ return returnError (ErrFeeTooHigh )
236260 }
237261 f .loopIn .HtlcTxFeeRate = feeRate
238262
@@ -246,7 +270,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
246270 "configured allowed maximum: %v > %v" , fee ,
247271 maxHtlcTxBackupFee )
248272
249- return f . HandleError (ErrFeeTooHigh )
273+ return returnError (ErrFeeTooHigh )
250274 }
251275 f .loopIn .HtlcTxHighFeeRate = highFeeRate
252276
@@ -262,7 +286,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
262286 "configured allowed maximum: %v > %v" , fee ,
263287 maxHtlcTxBackupFee )
264288
265- return f . HandleError (ErrFeeTooHigh )
289+ return returnError (ErrFeeTooHigh )
266290 }
267291 f .loopIn .HtlcTxExtremelyHighFeeRate = extremelyHighFeeRate
268292
@@ -276,7 +300,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
276300 err = fmt .Errorf ("unable to derive htlc timeout sweep " +
277301 "address: %w" , err )
278302
279- return f . HandleError (err )
303+ return returnError (err )
280304 }
281305 f .loopIn .HtlcTimeoutSweepAddress = sweepAddress
282306
@@ -286,10 +310,31 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
286310 pushEmptySigs ()
287311 err = fmt .Errorf ("unable to store loop-in in db: %w" , err )
288312
289- return f . HandleError (err )
313+ return returnError (err )
290314 }
291315
292- return OnHtlcInitiated
316+ // Once the swap is stored, restart/recovery code owns invoice lifecycle.
317+ invoiceNeedsCleanup = false
318+
319+ event = OnHtlcInitiated
320+
321+ return event
322+ }
323+
324+ // cancelSwapInvoice best-effort cancels the current swap invoice using a
325+ // detached timeout-limited context so cleanup still runs even if the caller's
326+ // context is already done.
327+ func (f * FSM ) cancelSwapInvoice (ctx context.Context ) {
328+ cleanupCtx , cancel := context .WithTimeout (
329+ context .WithoutCancel (ctx ), defaultInvoiceCleanupTimeout ,
330+ )
331+ defer cancel ()
332+
333+ err := f .cfg .InvoicesClient .CancelInvoice (cleanupCtx , f .loopIn .SwapHash )
334+ if err != nil {
335+ f .Warnf ("unable to cancel invoice for swap %v: %v" ,
336+ f .loopIn .SwapHash , err )
337+ }
293338}
294339
295340// SignHtlcTxAction is called if the htlc was initialized and the server
@@ -557,11 +602,9 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
557602 // Cancel the lndclient invoice subscription.
558603 cancelInvoiceSubscription ()
559604
560- err = f .cfg .InvoicesClient .CancelInvoice (ctx , f .loopIn .SwapHash )
561- if err != nil {
562- f .Warnf ("unable to cancel invoice " +
563- "for swap hash: %v" , err )
564- }
605+ // Reuse the same helper as InitHtlcAction so timeout cleanup follows
606+ // the same detached-context path as early-init cleanup.
607+ f .cancelSwapInvoice (ctx )
565608 }
566609
567610 for {
0 commit comments