11import { subscribeToInvoice } from 'lightning' ;
22import { Order , User } from '../models' ;
33import { payToBuyer } from './pay_request' ;
4+ import { getInvoice } from './hold_invoice' ;
45import lnd from './connect' ;
56import * as messages from '../bot/messages' ;
67import * as ordersActions from '../bot/ordersActions' ;
@@ -19,16 +20,7 @@ type LockCountedMutex = {
1920// when both 'error' and 'end' events fire for the same invoice
2021const pendingReconnects : Set < string > = new Set ( ) ;
2122
22- // Terminal order statuses where the invoice lifecycle is complete
23- // and resubscription should NOT be attempted
24- const TERMINAL_STATUSES = new Set ( [
25- 'SUCCESS' ,
26- 'PAID_HOLD_INVOICE' ,
27- 'CANCELED' ,
28- 'EXPIRED' ,
29- 'COMPLETED_BY_ADMIN' ,
30- 'CLOSED' ,
31- ] ) ;
23+
3224
3325class PerOrderIdMutex {
3426 mutexes : Map < string , LockCountedMutex > = new Map ( ) ;
@@ -73,24 +65,31 @@ const subscribeInvoice = async (
7365 return ;
7466 }
7567
76- // Check if the order has reached a terminal state before resubscribing .
68+ // Check the invoice state directly via LND — this is the source of truth .
7769 // When an invoice is settled or canceled, the gRPC stream ends normally
7870 // (fires 'end' event). Without this check, we'd resubscribe in an
7971 // infinite loop for invoices that are already done.
8072 try {
81- const order = await Order . findOne ( { hash : id } ) ;
82- if ( order && TERMINAL_STATUSES . has ( order . status ) ) {
73+ const invoice = await getInvoice ( { hash : id } ) ;
74+ if ( ! invoice ) {
75+ logger . info (
76+ `subscribeInvoice: invoice ${ id } not found, not resubscribing (${ reason } )` ,
77+ ) ;
78+ return ;
79+ }
80+ if ( invoice . is_confirmed || invoice . is_canceled ) {
8381 logger . info (
84- `subscribeInvoice: order ${ order . _id } is in terminal status ${ order . status } , ` +
85- `not resubscribing invoice ${ id } (${ reason } )` ,
82+ `subscribeInvoice: invoice ${ id } is in terminal state ` +
83+ `(confirmed=${ invoice . is_confirmed } , canceled=${ invoice . is_canceled } ), ` +
84+ `not resubscribing (${ reason } )` ,
8685 ) ;
8786 return ;
8887 }
8988 } catch ( err ) {
9089 logger . error (
91- `subscribeInvoice: failed to check order status for hash ${ id } : ${ err } ` ,
90+ `subscribeInvoice: failed to check invoice status for hash ${ id } : ${ err } ` ,
9291 ) ;
93- // On DB error, still attempt resubscription as a safety measure
92+ // On LND error, still attempt resubscription as a safety measure
9493 }
9594
9695 pendingReconnects . add ( id ) ;
@@ -106,6 +105,10 @@ const subscribeInvoice = async (
106105 } , 5000 ) ;
107106 } ;
108107
108+ // Use a single combined handler for both error and end events to prevent
109+ // duplicate resubscriptions. When the gRPC stream disconnects, it may fire
110+ // both 'error' and 'end'. The pendingReconnects guard ensures only one
111+ // resubscription happens.
109112 sub . on ( 'error' , ( err : Error ) => {
110113 logger . error (
111114 `subscribeInvoice stream error for hash ${ id } : ${ err . message || err } ` ,
0 commit comments