beignet 0.21.2 → 0.21.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -199,6 +199,7 @@ class LightningNode extends events_1.EventEmitter {
199
199
  this.heldInvoiceHashes = new Set();
200
200
  this.heldHtlcs = new Map();
201
201
  this.resolvingHeldHtlcs = new Set();
202
+ this.heldResolutions = new Map();
202
203
  this.holdInvoiceEventQueue = [];
203
204
  this.emittingHoldInvoiceEvent = false;
204
205
  this.announcedAddresses = [];
@@ -1172,6 +1173,21 @@ class LightningNode extends events_1.EventEmitter {
1172
1173
  amountMsat: BigInt(h.amountMsat),
1173
1174
  cltvExpiry: h.cltvExpiry
1174
1175
  })));
1176
+ const r = entry.resolution;
1177
+ if (!r)
1178
+ continue;
1179
+ const resolved = r.resolved.map((h) => ({
1180
+ amountMsat: BigInt(h.amountMsat),
1181
+ cltvExpiry: h.cltvExpiry
1182
+ }));
1183
+ this.heldResolutions.set(entry.hashHex, r.outcome === 'settle'
1184
+ ? { outcome: 'settle', resolved }
1185
+ : {
1186
+ outcome: 'cancel',
1187
+ failureCode: r.failureCode ?? types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
1188
+ reason: r.reason ?? 'api',
1189
+ resolved
1190
+ });
1175
1191
  }
1176
1192
  }
1177
1193
  catch {
@@ -1723,6 +1739,7 @@ class LightningNode extends events_1.EventEmitter {
1723
1739
  this.drainParkedQuiescentHtlcs(channelId.toString('hex'));
1724
1740
  this.asyncPaymentManager.onChannelUsable(channelId.toString('hex'));
1725
1741
  this.retryOwedHeldForwardFailures(channelId.toString('hex'));
1742
+ this.retryHeldResolutions(channelId);
1726
1743
  const peer = this.channelManager.getPeerForChannel(channelId);
1727
1744
  if (peer)
1728
1745
  this.asyncPaymentManager.sendNotice(peer);
@@ -9372,9 +9389,10 @@ class LightningNode extends events_1.EventEmitter {
9372
9389
  const alreadyParked = parked.some((h) => h.channelId.equals(channelId) && h.htlcId === htlcId);
9373
9390
  const parkedMsat = parked.reduce((sum, h) => sum + h.amountMsat, 0n);
9374
9391
  if (!alreadyParked &&
9375
- finalInvoice?.amountMsat &&
9376
- finalInvoice.amountMsat > 0n &&
9377
- parkedMsat >= finalInvoice.amountMsat) {
9392
+ (this.heldResolutions.has(hashHex) ||
9393
+ (finalInvoice?.amountMsat &&
9394
+ finalInvoice.amountMsat > 0n &&
9395
+ parkedMsat >= finalInvoice.amountMsat))) {
9378
9396
  this.emitStructuredLog('htlc', 'held_set_complete', {
9379
9397
  paymentHash: hashHex,
9380
9398
  parkedMsat: parkedMsat.toString(),
@@ -9496,6 +9514,8 @@ class LightningNode extends events_1.EventEmitter {
9496
9514
  return false;
9497
9515
  if (this.resolvingHeldHtlcs.has(hashHex))
9498
9516
  return false;
9517
+ if (this.heldResolutions.get(hashHex)?.outcome === 'cancel')
9518
+ return false;
9499
9519
  const pre = preimage ?? this.preimages.get(hashHex);
9500
9520
  if (!pre) {
9501
9521
  throw new Error('settleHeldHtlc: no preimage available for hold invoice');
@@ -9512,30 +9532,42 @@ class LightningNode extends events_1.EventEmitter {
9512
9532
  this.preimages.set(hashHex, pre);
9513
9533
  this.channelManager.recordPreimage(paymentHash, pre);
9514
9534
  this.resolvingHeldHtlcs.add(hashHex);
9515
- let fulfilled = 0;
9535
+ const prior = this.heldResolutions.get(hashHex);
9536
+ const refused = [];
9516
9537
  try {
9538
+ this.heldResolutions.set(hashHex, {
9539
+ outcome: 'settle',
9540
+ resolved: prior?.resolved ?? []
9541
+ });
9542
+ this.persistHeldHtlcs();
9517
9543
  for (const h of held) {
9518
9544
  const key = `${h.channelId.toString('hex')}:${h.htlcId}`;
9519
9545
  if (!this.channelManager.fulfillHtlc(h.channelId, h.htlcId, pre).ok) {
9546
+ refused.push(h);
9520
9547
  continue;
9521
9548
  }
9522
- fulfilled++;
9523
9549
  this.cleanupHtlcSharedSecret(key);
9524
9550
  }
9525
9551
  }
9526
9552
  finally {
9527
9553
  this.resolvingHeldHtlcs.delete(hashHex);
9528
9554
  }
9529
- if (fulfilled === 0) {
9555
+ if (refused.length === held.length) {
9556
+ if (!prior) {
9557
+ this.heldResolutions.delete(hashHex);
9558
+ this.persistHeldHtlcs();
9559
+ }
9530
9560
  this.emitStructuredLog('htlc', 'held_settle_refused', {
9531
9561
  paymentHash: hashHex,
9532
9562
  htlcCount: held.length
9533
9563
  });
9534
9564
  return false;
9535
9565
  }
9536
- this.heldHtlcs.delete(hashHex);
9537
- this.heldInvoiceHashes.delete(hashHex);
9538
- this.persistHeldHtlcs();
9566
+ const settledSet = this.advanceHeldResolution(hashHex, held, refused, {
9567
+ outcome: 'settle'
9568
+ });
9569
+ if (!settledSet)
9570
+ return false;
9539
9571
  const payment = this.payments.get(hashHex);
9540
9572
  if (payment) {
9541
9573
  payment.status = types_7.PaymentStatus.COMPLETED;
@@ -9553,13 +9585,59 @@ class LightningNode extends events_1.EventEmitter {
9553
9585
  const event = {
9554
9586
  paymentHash,
9555
9587
  state: 'SETTLED',
9556
- heldAmountMsat: held.reduce((sum, h) => sum + h.amountMsat, 0n),
9557
- htlcCount: held.length,
9558
- ...this.heldSetExpiry(hashHex, held)
9588
+ heldAmountMsat: settledSet.reduce((sum, h) => sum + h.amountMsat, 0n),
9589
+ htlcCount: settledSet.length,
9590
+ ...this.heldSetExpiry(hashHex, settledSet)
9559
9591
  };
9560
9592
  this.emitHoldInvoiceEvent('hold:settled', event);
9561
9593
  return true;
9562
9594
  }
9595
+ advanceHeldResolution(hashHex, held, refused, chosen) {
9596
+ const resolved = [
9597
+ ...(this.heldResolutions.get(hashHex)?.resolved ?? []),
9598
+ ...held
9599
+ .filter((h) => !refused.includes(h))
9600
+ .map((h) => ({ amountMsat: h.amountMsat, cltvExpiry: h.cltvExpiry }))
9601
+ ];
9602
+ if (refused.length === 0) {
9603
+ this.heldHtlcs.delete(hashHex);
9604
+ this.heldResolutions.delete(hashHex);
9605
+ this.heldInvoiceHashes.delete(hashHex);
9606
+ this.persistHeldHtlcs();
9607
+ return resolved;
9608
+ }
9609
+ this.heldHtlcs.set(hashHex, refused);
9610
+ this.heldResolutions.set(hashHex, { ...chosen, resolved });
9611
+ this.persistHeldHtlcs();
9612
+ this.emitStructuredLog('htlc', `held_${chosen.outcome}_partial`, {
9613
+ paymentHash: hashHex,
9614
+ resolvedCount: resolved.length,
9615
+ refusedCount: refused.length
9616
+ });
9617
+ return null;
9618
+ }
9619
+ retryHeldResolutions(channelId) {
9620
+ for (const [hashHex, resolution] of [...this.heldResolutions]) {
9621
+ const held = this.heldHtlcs.get(hashHex) ?? [];
9622
+ if (channelId && !held.some((h) => h.channelId.equals(channelId))) {
9623
+ continue;
9624
+ }
9625
+ const paymentHash = Buffer.from(hashHex, 'hex');
9626
+ try {
9627
+ if (resolution.outcome === 'cancel')
9628
+ this.cancelHeldHtlc(paymentHash);
9629
+ else
9630
+ this.settleHeldHtlc(paymentHash);
9631
+ }
9632
+ catch (error) {
9633
+ this.emitStructuredLog('htlc', 'held_resolution_retry_failed', {
9634
+ paymentHash: hashHex,
9635
+ outcome: resolution.outcome,
9636
+ error: error instanceof Error ? error.message : String(error)
9637
+ });
9638
+ }
9639
+ }
9640
+ }
9563
9641
  emitInvoiceSettled(paymentHash, payment) {
9564
9642
  const invoice = this.invoices.get(paymentHash.toString('hex'));
9565
9643
  if (!invoice)
@@ -9581,9 +9659,23 @@ class LightningNode extends events_1.EventEmitter {
9581
9659
  return false;
9582
9660
  if (this.resolvingHeldHtlcs.has(hashHex))
9583
9661
  return false;
9662
+ const chosen = this.heldResolutions.get(hashHex);
9663
+ if (chosen?.outcome === 'settle')
9664
+ return false;
9665
+ if (chosen) {
9666
+ failureCode = chosen.failureCode;
9667
+ cancelReason = chosen.reason;
9668
+ }
9584
9669
  this.resolvingHeldHtlcs.add(hashHex);
9585
- let failed = 0;
9670
+ const refused = [];
9586
9671
  try {
9672
+ this.heldResolutions.set(hashHex, {
9673
+ outcome: 'cancel',
9674
+ failureCode,
9675
+ reason: cancelReason,
9676
+ resolved: chosen?.resolved ?? []
9677
+ });
9678
+ this.persistHeldHtlcs();
9587
9679
  for (const h of held) {
9588
9680
  const key = `${h.channelId.toString('hex')}:${h.htlcId}`;
9589
9681
  const ss = this.receivedHtlcSharedSecrets.get(key);
@@ -9593,16 +9685,20 @@ class LightningNode extends events_1.EventEmitter {
9593
9685
  : undefined)
9594
9686
  : Buffer.alloc(failures_1.FAILURE_MESSAGE_LENGTH);
9595
9687
  if (!this.channelManager.failHtlc(h.channelId, h.htlcId, reason).ok) {
9688
+ refused.push(h);
9596
9689
  continue;
9597
9690
  }
9598
- failed++;
9599
9691
  this.cleanupHtlcSharedSecret(key);
9600
9692
  }
9601
9693
  }
9602
9694
  finally {
9603
9695
  this.resolvingHeldHtlcs.delete(hashHex);
9604
9696
  }
9605
- if (failed === 0) {
9697
+ if (refused.length === held.length) {
9698
+ if (!chosen) {
9699
+ this.heldResolutions.delete(hashHex);
9700
+ this.persistHeldHtlcs();
9701
+ }
9606
9702
  this.emitStructuredLog('htlc', 'held_cancel_refused', {
9607
9703
  paymentHash: hashHex,
9608
9704
  reason: cancelReason,
@@ -9610,10 +9706,14 @@ class LightningNode extends events_1.EventEmitter {
9610
9706
  });
9611
9707
  return false;
9612
9708
  }
9613
- this.heldHtlcs.delete(hashHex);
9614
- this.heldInvoiceHashes.delete(hashHex);
9615
- this.persistHeldHtlcs();
9616
- this.markHoldInvoiceCancelled(hashHex, cancelReason, held.length, held.reduce((sum, h) => sum + h.amountMsat, 0n), held);
9709
+ const failedSet = this.advanceHeldResolution(hashHex, held, refused, {
9710
+ outcome: 'cancel',
9711
+ failureCode,
9712
+ reason: cancelReason
9713
+ });
9714
+ if (!failedSet)
9715
+ return false;
9716
+ this.markHoldInvoiceCancelled(hashHex, cancelReason, failedSet.length, failedSet.reduce((sum, h) => sum + h.amountMsat, 0n), failedSet);
9617
9717
  this.emitStructuredLog('htlc', 'held_cancelled', {
9618
9718
  paymentHash: hashHex,
9619
9719
  reason: cancelReason
@@ -9660,7 +9760,7 @@ class LightningNode extends events_1.EventEmitter {
9660
9760
  const hashHex = paymentHash.toString('hex');
9661
9761
  const held = this.heldHtlcs.get(hashHex);
9662
9762
  if (held && held.length > 0) {
9663
- const count = held.length;
9763
+ const count = held.length + (this.heldResolutions.get(hashHex)?.resolved.length ?? 0);
9664
9764
  if (!this.cancelHeldHtlc(paymentHash))
9665
9765
  return null;
9666
9766
  return { htlcsFailed: count };
@@ -9704,7 +9804,10 @@ class LightningNode extends events_1.EventEmitter {
9704
9804
  scanExpiringHeldHtlcs(height) {
9705
9805
  if (height <= 0)
9706
9806
  return;
9807
+ this.retryHeldResolutions();
9707
9808
  for (const [hashHex, held] of this.heldHtlcs) {
9809
+ if (this.heldResolutions.has(hashHex))
9810
+ continue;
9708
9811
  const soon = held.some((h) => h.cltvExpiry > 0 && h.cltvExpiry - height <= exports.HELD_HTLC_EXPIRY_MARGIN);
9709
9812
  if (soon) {
9710
9813
  this.cancelHeldHtlc(Buffer.from(hashHex, 'hex'), types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, 'expiry-scan');
@@ -9716,6 +9819,7 @@ class LightningNode extends events_1.EventEmitter {
9716
9819
  return;
9717
9820
  const serial = [];
9718
9821
  for (const [hashHex, held] of this.heldHtlcs) {
9822
+ const r = this.heldResolutions.get(hashHex);
9719
9823
  serial.push({
9720
9824
  hashHex,
9721
9825
  htlcs: held.map((h) => ({
@@ -9723,7 +9827,16 @@ class LightningNode extends events_1.EventEmitter {
9723
9827
  htlcId: h.htlcId.toString(),
9724
9828
  amountMsat: h.amountMsat.toString(),
9725
9829
  cltvExpiry: h.cltvExpiry
9726
- }))
9830
+ })),
9831
+ ...(r && {
9832
+ resolution: {
9833
+ ...r,
9834
+ resolved: r.resolved.map((h) => ({
9835
+ amountMsat: h.amountMsat.toString(),
9836
+ cltvExpiry: h.cltvExpiry
9837
+ }))
9838
+ }
9839
+ })
9727
9840
  });
9728
9841
  }
9729
9842
  this.safeStorage(() => this.storage.saveMetadata('held_htlcs', JSON.stringify(serial)), 'persistHeldHtlcs');
@@ -13226,9 +13339,13 @@ class LightningNode extends events_1.EventEmitter {
13226
13339
  if (!key.startsWith('received-'))
13227
13340
  continue;
13228
13341
  const paymentHashHex = htlc.paymentHash?.toString('hex');
13342
+ const resolution = paymentHashHex !== undefined
13343
+ ? this.heldResolutions.get(paymentHashHex)
13344
+ : undefined;
13229
13345
  const parkedHold = htlc.state !== types_1.HtlcState.FULFILLED &&
13230
13346
  paymentHashHex !== undefined &&
13231
- this.heldInvoiceHashes.has(paymentHashHex);
13347
+ this.heldInvoiceHashes.has(paymentHashHex) &&
13348
+ resolution?.outcome !== 'settle';
13232
13349
  const haveClaim = !parkedHold &&
13233
13350
  (htlc.state === types_1.HtlcState.FULFILLED ||
13234
13351
  (paymentHashHex !== undefined &&
@@ -13277,6 +13394,12 @@ class LightningNode extends events_1.EventEmitter {
13277
13394
  this._forceCloseWithReason(channelId, this.getSweepDestinationScript(), this.resolveForceCloseFeeRatePerVbyte(), 'FORWARD_TIMEOUT_FORCE_CLOSE');
13278
13395
  break;
13279
13396
  }
13397
+ if (resolution?.outcome === 'cancel' &&
13398
+ this.heldHtlcs
13399
+ .get(paymentHashHex)
13400
+ ?.some((h) => h.channelId.equals(channelId) && h.htlcId === htlc.id)) {
13401
+ continue;
13402
+ }
13280
13403
  const htlcSecretKey = `${channelId.toString('hex')}:${htlc.id}`;
13281
13404
  const blindedRole = this.blindedRoleFor(channelId, htlc.id);
13282
13405
  if (blindedRole) {