Skip to content

Commit a6efc63

Browse files
MostronatorCoder[bot]MostronatorCoder[bot]
authored andcommitted
fix: check invoice state via LND instead of order status before resubscribing
- Use getInvoice() to check is_confirmed/is_canceled directly from LND instead of checking order status (which may not reflect invoice state) - Remove TERMINAL_STATUSES set (no longer needed) - pendingReconnects guard prevents duplicate subscriptions from simultaneous error+end events
1 parent ed478e2 commit a6efc63

File tree

1 file changed

+20
-17
lines changed

1 file changed

+20
-17
lines changed

ln/subscribe_invoice.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { subscribeToInvoice } from 'lightning';
22
import { Order, User } from '../models';
33
import { payToBuyer } from './pay_request';
4+
import { getInvoice } from './hold_invoice';
45
import lnd from './connect';
56
import * as messages from '../bot/messages';
67
import * as ordersActions from '../bot/ordersActions';
@@ -19,16 +20,7 @@ type LockCountedMutex = {
1920
// when both 'error' and 'end' events fire for the same invoice
2021
const 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

3325
class 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

Comments
 (0)