beignet 0.21.12 → 0.22.0

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.
Files changed (43) hide show
  1. package/dist/cli/beignet-node.js +575 -143
  2. package/dist/cli/beignet-node.js.map +1 -1
  3. package/dist/cli/cli.js +10 -5
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/cli/config.js +7 -0
  6. package/dist/cli/config.js.map +1 -1
  7. package/dist/cli/daemon-options.js +1 -0
  8. package/dist/cli/daemon-options.js.map +1 -1
  9. package/dist/cli/daemon.js +19 -6
  10. package/dist/cli/daemon.js.map +1 -1
  11. package/dist/cli/index.js +2 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/node-storage-view.js +40 -0
  14. package/dist/cli/node-storage-view.js.map +1 -0
  15. package/dist/cli/payment-queue.js +215 -31
  16. package/dist/cli/payment-queue.js.map +1 -1
  17. package/dist/electrum/index.js +25 -6
  18. package/dist/electrum/index.js.map +1 -1
  19. package/dist/lightning/node/lightning-node.js +173 -17
  20. package/dist/lightning/node/lightning-node.js.map +1 -1
  21. package/dist/lightning/node/types.js.map +1 -1
  22. package/dist/lightning/transport/peer-manager.js +13 -7
  23. package/dist/lightning/transport/peer-manager.js.map +1 -1
  24. package/dist/lightning/watchtower/tower-connection.js +2 -4
  25. package/dist/lightning/watchtower/tower-connection.js.map +1 -1
  26. package/dist/lightning/watchtower/watchtower-client.js +3 -1
  27. package/dist/lightning/watchtower/watchtower-client.js.map +1 -1
  28. package/dist/types/cli/beignet-node.d.ts +29 -4
  29. package/dist/types/cli/index.d.ts +1 -1
  30. package/dist/types/cli/node-storage-view.d.ts +2 -0
  31. package/dist/types/cli/payment-queue.d.ts +27 -0
  32. package/dist/types/cli/types.d.ts +4 -0
  33. package/dist/types/electrum/index.d.ts +1 -0
  34. package/dist/types/lightning/node/lightning-node.d.ts +13 -1
  35. package/dist/types/lightning/node/types.d.ts +2 -0
  36. package/dist/types/lightning/transport/peer-manager.d.ts +14 -0
  37. package/dist/types/lightning/watchtower/tower-connection.d.ts +2 -0
  38. package/dist/types/lightning/watchtower/watchtower-client.d.ts +3 -0
  39. package/dist/types/wallet/index.d.ts +4 -0
  40. package/dist/wallet/index.js +125 -50
  41. package/dist/wallet/index.js.map +1 -1
  42. package/docs/AI_AGENT_GUIDE.md +9 -7
  43. package/package.json +1 -1
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.BeignetNode = exports.jitInvoiceError = exports.holdInvoiceEvent = exports.jsonSafeEvent = exports.jitReceiveRefusal = exports.leaseRatesRefusal = exports.clampDirectFundingMinimum = exports.MAX_ASYNC_SPEND_CLAIMS = exports.ASYNC_SPEND_CLAIM_TTL_MS = exports.jsonToRouteHops = exports.routeHopsToJson = exports.spliceRefusalError = exports.decodeOfferInput = exports.paymentSpendSats = exports.spendLimitSats = exports.decodeInvoiceInput = exports.parseScid = exports.formatScid = exports.gossipPrimeLatch = exports.defaultDataDirForMnemonic = exports.parseRecoveryMode = void 0;
26
+ exports.BeignetNode = exports.jitInvoiceError = exports.holdInvoiceEvent = exports.jsonSafeEvent = exports.jitReceiveRefusal = exports.leaseRatesRefusal = exports.clampDirectFundingMinimum = exports.DAILY_SPEND_STATE_KEY = exports.MAX_ASYNC_SPEND_CLAIMS = exports.ASYNC_SPEND_CLAIM_TTL_MS = exports.jsonToRouteHops = exports.routeHopsToJson = exports.spliceRefusalError = exports.decodeOfferInput = exports.paymentSpendSats = exports.spendLimitSats = exports.decodeInvoiceInput = exports.ENGINE_PAYMENT_ERROR_CODES = exports.parseScid = exports.formatScid = exports.gossipPrimeLatch = exports.defaultDataDirForMnemonic = exports.parseRecoveryMode = void 0;
27
27
  const offline_receive_1 = require("./offline-receive");
28
28
  const ffor_receive_1 = require("./ffor-receive");
29
29
  const path = __importStar(require("path"));
@@ -41,6 +41,7 @@ const helpers_1 = require("../utils/helpers");
41
41
  const conversion_1 = require("../utils/conversion");
42
42
  const wallet_3 = require("../types/wallet");
43
43
  const wallet_storage_1 = require("./wallet-storage");
44
+ const node_storage_view_1 = require("./node-storage-view");
44
45
  const electrum_1 = require("../types/electrum");
45
46
  const lightning_node_1 = require("../lightning/node/lightning-node");
46
47
  const types_1 = require("../lightning/direct-funding/receiver/types");
@@ -69,6 +70,7 @@ const direct_funding_2 = require("./direct-funding");
69
70
  const auth_1 = require("./auth");
70
71
  const types_5 = require("../lightning/node/types");
71
72
  const types_6 = require("../lightning/channel/types");
73
+ const channel_state_1 = require("../lightning/channel/channel-state");
72
74
  const decode_1 = require("../lightning/invoice/decode");
73
75
  const decode_2 = require("../lightning/offer/decode");
74
76
  const errors_1 = require("./errors");
@@ -169,6 +171,17 @@ function parseScid(scid) {
169
171
  throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, `Invalid short channel id: ${scid} (expected <block>x<txIndex>x<output> or 16-char hex)`);
170
172
  }
171
173
  exports.parseScid = parseScid;
174
+ exports.ENGINE_PAYMENT_ERROR_CODES = {
175
+ NO_ROUTE: 'NO_ROUTE',
176
+ DUPLICATE_PAYMENT: 'DUPLICATE_PAYMENT',
177
+ NO_CHANNEL_TO_HOP: 'PEER_NOT_CONNECTED',
178
+ FEE_EXCEEDS_MAX: 'PAYMENT_FAILED',
179
+ CLTV_EXCEEDS_MAX: 'CLTV_EXCEEDS_MAX',
180
+ MISSING_AMOUNT: 'INVALID_PARAMS',
181
+ INVALID_INVOICE: 'INVALID_PARAMS',
182
+ INVALID_KEYSEND: 'INVALID_PARAMS',
183
+ INVOICE_EXPIRED: 'INVOICE_EXPIRED'
184
+ };
172
185
  function decodeInvoiceInput(bolt11) {
173
186
  try {
174
187
  return (0, decode_1.decode)(bolt11);
@@ -279,6 +292,12 @@ exports.jsonToRouteHops = jsonToRouteHops;
279
292
  const GRAPH_DESCRIBE_MAX_LIMIT = 500;
280
293
  exports.ASYNC_SPEND_CLAIM_TTL_MS = 24 * 60 * 60 * 1000;
281
294
  exports.MAX_ASYNC_SPEND_CLAIMS = 4096;
295
+ exports.DAILY_SPEND_STATE_KEY = 'daemon:daily-spend:v1';
296
+ const nextUtcMidnight = () => {
297
+ const next = new Date();
298
+ next.setUTCHours(24, 0, 0, 0);
299
+ return next.getTime();
300
+ };
282
301
  const MAX_DIRECT_FUNDING_CHARGES = 1024;
283
302
  const DEFAULT_L402_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
284
303
  const DEFAULT_ELECTRUM = {
@@ -478,6 +497,21 @@ function isHeldRestore(ch) {
478
497
  ch.reestablishSecretMissing === true ||
479
498
  ch.restoreRevokedRisk === true);
480
499
  }
500
+ const CLOSING_CHANNEL_STATES = new Set([
501
+ types_6.ChannelState.SHUTTING_DOWN,
502
+ types_6.ChannelState.NEGOTIATING_CLOSING,
503
+ types_6.ChannelState.CLOSED,
504
+ types_6.ChannelState.FORCE_CLOSED,
505
+ types_6.ChannelState.ERRORED
506
+ ]);
507
+ function canBecomeUsable(channel) {
508
+ const st = channel.getFullState();
509
+ const effective = st.state === types_6.ChannelState.AWAITING_REESTABLISH && st.preReestablishState
510
+ ? st.preReestablishState
511
+ : st.state;
512
+ return !CLOSING_CHANNEL_STATES.has(effective) && !(0, channel_state_1.isRecencyUnproven)(st);
513
+ }
514
+ const CHANNELS_CHANGED = Symbol('channels-changed');
481
515
  const STALE_STATE_FORCE_CLOSE_REFUSAL = 'This channel was restored from a Recovery Capsule and its state ' +
482
516
  'cannot be proven current, so the node will not broadcast its ' +
483
517
  'commitment on its own initiative. If the peer holds a newer ' +
@@ -583,7 +617,6 @@ class BeignetNode extends events_1.EventEmitter {
583
617
  this._dailySpendResetTime = 0;
584
618
  this._pendingSpendSats = 0;
585
619
  this._asyncSpendClaims = new Map();
586
- this._blockingPaymentHashes = new Map();
587
620
  this._l402Credentials = new l402_1.MemoryL402CredentialStore();
588
621
  this._draining = false;
589
622
  this.peerStorageEnabled = true;
@@ -706,6 +739,9 @@ class BeignetNode extends events_1.EventEmitter {
706
739
  throw new errors_1.BeignetError('INVALID_PARAMS', `jitReceive: ${refusal}`);
707
740
  }
708
741
  }
742
+ if (opts.torProxyOnionOnly && !opts.torProxy) {
743
+ throw new errors_1.BeignetError('INVALID_PARAMS', 'torProxyOnionOnly needs torProxy: it only chooses which hosts use the configured proxy');
744
+ }
709
745
  const networkName = this.networkName;
710
746
  if (!opts.allowMultipleInstances) {
711
747
  const lockPath = path.join(this.dataDir, `${networkName}.lock`);
@@ -897,10 +933,13 @@ class BeignetNode extends events_1.EventEmitter {
897
933
  const [proxyHost, proxyPort] = opts.torProxy.split(':');
898
934
  const port = parseInt(proxyPort, 10);
899
935
  if (!proxyHost || !Number.isFinite(port)) {
900
- throw new errors_1.BeignetError('INVALID_PARAMS', `Invalid torProxy "${opts.torProxy}" — expected "host:port"`);
936
+ throw new errors_1.BeignetError('INVALID_PARAMS', `Invalid torProxy "${opts.torProxy}": expected "host:port"`);
901
937
  }
902
938
  socks5Proxy = { host: proxyHost, port };
903
939
  }
940
+ const socks5ProxyScope = opts.torProxyOnionOnly
941
+ ? 'onion'
942
+ : 'all';
904
943
  let announcedAddresses;
905
944
  if (opts.announceAddresses && opts.announceAddresses.length > 0) {
906
945
  announcedAddresses = opts.announceAddresses.map((addr) => {
@@ -920,10 +959,11 @@ class BeignetNode extends events_1.EventEmitter {
920
959
  this._initialGossipPrime = null;
921
960
  });
922
961
  }
962
+ this._nodeStorageView = (0, node_storage_view_1.nodeStorageView)(this.storage);
923
963
  this.node = lightning_node_1.LightningNode.fromMnemonic(this.mnemonic, {
924
964
  coinType,
925
965
  network: lnNetwork,
926
- storage: this.storage,
966
+ storage: this._nodeStorageView,
927
967
  newChannelsRefused: () => this.newChannelRefusal(),
928
968
  enableNetworking: true,
929
969
  autoReconnect: opts.autoReconnect ?? true,
@@ -1088,6 +1128,7 @@ class BeignetNode extends events_1.EventEmitter {
1088
1128
  logger: this.logger,
1089
1129
  sweepDestinationScript,
1090
1130
  socks5Proxy,
1131
+ socks5ProxyScope,
1091
1132
  ...(opts.guardianServe
1092
1133
  ? {
1093
1134
  guardianHost: {
@@ -1168,6 +1209,7 @@ class BeignetNode extends events_1.EventEmitter {
1168
1209
  });
1169
1210
  this.node.on('payment:failed', (info) => {
1170
1211
  const pi = this.toPaymentInfo(info);
1212
+ this._releaseAsyncSpendClaimsUnlessInFlight(info.paymentHash);
1171
1213
  this.log('warn', 'Payment failed', {
1172
1214
  paymentHash: pi.paymentHash,
1173
1215
  failureCode: pi.failureCode,
@@ -1182,6 +1224,61 @@ class BeignetNode extends events_1.EventEmitter {
1182
1224
  this.refreshStaticChannelBackup();
1183
1225
  this.emit('channel:ready', { channelId });
1184
1226
  });
1227
+ const usableNode = this.node;
1228
+ const deferEmit = (emit) => {
1229
+ setImmediate(() => {
1230
+ if (this.destroyed || this._resuming || this.node !== usableNode) {
1231
+ return;
1232
+ }
1233
+ try {
1234
+ emit();
1235
+ }
1236
+ catch (err) {
1237
+ try {
1238
+ this.log('warn', 'A payment-readiness listener failed', {
1239
+ error: err instanceof Error ? err.message : String(err)
1240
+ });
1241
+ }
1242
+ catch {
1243
+ }
1244
+ }
1245
+ });
1246
+ };
1247
+ const announceUsable = (channelId) => deferEmit(() => {
1248
+ const channel = usableNode.getChannelManager().getChannel(channelId);
1249
+ if (!channel?.acceptsNewHtlcs())
1250
+ return;
1251
+ this.emit('channel:usable', { channelId: channelId.toString('hex') });
1252
+ });
1253
+ this.node.on('channel:ready', (data) => announceUsable(data.channelId));
1254
+ this.node
1255
+ .getChannelManager()
1256
+ .on('channel:reestablished', (channelId) => announceUsable(channelId));
1257
+ for (const event of [
1258
+ 'splice:complete',
1259
+ 'splice:aborted',
1260
+ 'splice:reverted'
1261
+ ]) {
1262
+ this.node.on(event, (data) => announceUsable(data.channelId));
1263
+ }
1264
+ this.node.on('log', (entry) => {
1265
+ if (entry?.action !== 'funding_missing_quarantine_lifted')
1266
+ return;
1267
+ const idHex = entry.data?.channelId;
1268
+ if (typeof idHex === 'string')
1269
+ announceUsable(Buffer.from(idHex, 'hex'));
1270
+ });
1271
+ const channelsChanged = () => deferEmit(() => this.emit(CHANNELS_CHANGED));
1272
+ for (const event of [
1273
+ 'channel:pending-close',
1274
+ 'channel:force-closing',
1275
+ 'channel:closed',
1276
+ 'channel:voided',
1277
+ 'channel:aborted'
1278
+ ]) {
1279
+ this.node.on(event, channelsChanged);
1280
+ }
1281
+ this.node.getChannelManager().on('channel:errored', channelsChanged);
1185
1282
  this.node.on('channel:closed', (data) => {
1186
1283
  const channelId = data.channelId.toString('hex');
1187
1284
  this.log('info', 'Channel closed', { channelId });
@@ -1531,7 +1628,7 @@ class BeignetNode extends events_1.EventEmitter {
1531
1628
  if (opts.dailySpendLimitSats !== undefined &&
1532
1629
  opts.dailySpendLimitSats > 0) {
1533
1630
  this._dailySpendLimitSats = opts.dailySpendLimitSats;
1534
- this._resetDailySpendIfNeeded();
1631
+ this._loadSpendState();
1535
1632
  }
1536
1633
  if (opts.maxPaymentSats !== undefined && opts.maxPaymentSats > 0) {
1537
1634
  this._maxPaymentSats = opts.maxPaymentSats;
@@ -2207,6 +2304,13 @@ class BeignetNode extends events_1.EventEmitter {
2207
2304
  }
2208
2305
  this._resuming = false;
2209
2306
  this._bootTargetEmpty = false;
2307
+ if (this.paymentQueue) {
2308
+ this.whenReadyToPay(() => {
2309
+ this.paymentQueue?.start();
2310
+ this.paymentQueue?.resettle();
2311
+ this.paymentQueue?.poke();
2312
+ });
2313
+ }
2210
2314
  this._peerRetrievedCapsules.clear();
2211
2315
  const uncovered = uncoveredEntries.map((e) => e.channelId);
2212
2316
  if (uncovered.length > 0) {
@@ -2426,6 +2530,11 @@ class BeignetNode extends events_1.EventEmitter {
2426
2530
  }
2427
2531
  this.paymentQueue?.removeAllListeners();
2428
2532
  this.node.destroy();
2533
+ try {
2534
+ this.storage.close();
2535
+ }
2536
+ catch {
2537
+ }
2429
2538
  void this.wallet?.stop().catch(() => {
2430
2539
  });
2431
2540
  }
@@ -2447,6 +2556,19 @@ class BeignetNode extends events_1.EventEmitter {
2447
2556
  error: err instanceof Error ? err.message : String(err)
2448
2557
  });
2449
2558
  }
2559
+ try {
2560
+ for (const row of from.loadAllQueueEntries()) {
2561
+ to.saveQueueEntry(row);
2562
+ if (row.error !== undefined || row.completedAt !== undefined) {
2563
+ to.updateQueueEntryStatus(row.id, row.status, row.error, row.completedAt);
2564
+ }
2565
+ }
2566
+ }
2567
+ catch (err) {
2568
+ this.log('warn', 'Could not carry the payment queue into the restore', {
2569
+ error: err instanceof Error ? err.message : String(err)
2570
+ });
2571
+ }
2450
2572
  try {
2451
2573
  for (const peer of from.loadAllPeerAddresses()) {
2452
2574
  to.savePeerAddress(peer.pubkey, peer.host, peer.port);
@@ -2457,6 +2579,14 @@ class BeignetNode extends events_1.EventEmitter {
2457
2579
  error: err instanceof Error ? err.message : String(err)
2458
2580
  });
2459
2581
  }
2582
+ try {
2583
+ const spend = from.loadMetadata(exports.DAILY_SPEND_STATE_KEY);
2584
+ if (spend !== null)
2585
+ to.saveMetadata(exports.DAILY_SPEND_STATE_KEY, spend);
2586
+ }
2587
+ catch (err) {
2588
+ this.log('warn', 'Could not carry the daily spend ledger into the restore', { error: err instanceof Error ? err.message : String(err) });
2589
+ }
2460
2590
  }
2461
2591
  finishStagedCapsuleRestore(dbPath) {
2462
2592
  const markerPath = this.capsuleRestoreMarkerPath();
@@ -4937,15 +5067,14 @@ class BeignetNode extends events_1.EventEmitter {
4937
5067
  }
4938
5068
  return result;
4939
5069
  }
4940
- _resetDailySpendIfNeeded() {
4941
- const now = Date.now();
4942
- if (now >= this._dailySpendResetTime) {
4943
- const tomorrow = new Date();
4944
- tomorrow.setUTCHours(24, 0, 0, 0);
4945
- this._dailySpendResetTime = tomorrow.getTime();
5070
+ _resetDailySpendIfNeeded(persist = true) {
5071
+ if (Date.now() >= this._dailySpendResetTime) {
5072
+ this._dailySpendResetTime = nextUtcMidnight();
4946
5073
  this._dailySpentSats = 0;
4947
5074
  this._dailySpentLightningSats = 0;
4948
5075
  this._dailySpentOnchainSats = 0;
5076
+ if (persist)
5077
+ this._persistSpendState();
4949
5078
  }
4950
5079
  }
4951
5080
  _checkSpendLimit(amountSats) {
@@ -4969,7 +5098,7 @@ class BeignetNode extends events_1.EventEmitter {
4969
5098
  _recordSpend(amountSats, source = 'lightning') {
4970
5099
  if (this._dailySpendLimitSats === undefined)
4971
5100
  return;
4972
- this._resetDailySpendIfNeeded();
5101
+ this._resetDailySpendIfNeeded(false);
4973
5102
  this._dailySpentSats += amountSats;
4974
5103
  if (source === 'onchain') {
4975
5104
  this._dailySpentOnchainSats += amountSats;
@@ -4977,6 +5106,7 @@ class BeignetNode extends events_1.EventEmitter {
4977
5106
  else {
4978
5107
  this._dailySpentLightningSats += amountSats;
4979
5108
  }
5109
+ this._persistSpendState();
4980
5110
  }
4981
5111
  _openAsyncSpendClaim(paymentHashHex, amountSats) {
4982
5112
  if (this._dailySpendLimitSats === undefined)
@@ -5001,8 +5131,25 @@ class BeignetNode extends events_1.EventEmitter {
5001
5131
  else
5002
5132
  this._asyncSpendClaims.set(paymentHashHex, [claim]);
5003
5133
  this._pendingSpendSats += amountSats;
5134
+ this._persistSpendState();
5004
5135
  return claim;
5005
5136
  }
5137
+ _rekeyAsyncSpendClaim(fromKey, toKey, claim) {
5138
+ const from = this._asyncSpendClaims.get(fromKey);
5139
+ if (from) {
5140
+ const index = from.indexOf(claim);
5141
+ if (index !== -1)
5142
+ from.splice(index, 1);
5143
+ if (from.length === 0)
5144
+ this._asyncSpendClaims.delete(fromKey);
5145
+ }
5146
+ const to = this._asyncSpendClaims.get(toKey);
5147
+ if (to)
5148
+ to.push(claim);
5149
+ else
5150
+ this._asyncSpendClaims.set(toKey, [claim]);
5151
+ this._persistSpendState();
5152
+ }
5006
5153
  _closeAsyncSpendClaim(paymentHashHex, claim) {
5007
5154
  const claims = this._asyncSpendClaims.get(paymentHashHex);
5008
5155
  if (!claims)
@@ -5013,49 +5160,74 @@ class BeignetNode extends events_1.EventEmitter {
5013
5160
  claims.splice(index, 1);
5014
5161
  if (claims.length === 0)
5015
5162
  this._asyncSpendClaims.delete(paymentHashHex);
5016
- this._releaseAsyncSpendClaim(claim);
5163
+ this._releaseClaimReservation(claim);
5164
+ this._persistSpendState();
5017
5165
  }
5018
5166
  _releaseAsyncSpendClaim(claim) {
5167
+ if (this._releaseClaimReservation(claim))
5168
+ this._persistSpendState();
5169
+ }
5170
+ _releaseClaimReservation(claim) {
5019
5171
  if (!claim.reserved)
5020
- return;
5172
+ return false;
5021
5173
  claim.reserved = false;
5022
5174
  this._pendingSpendSats -= claim.sats;
5175
+ return true;
5023
5176
  }
5024
- _chargeAsyncSpendClaim(paymentHashHex) {
5025
- if (this._blockingPaymentHashes.has(paymentHashHex))
5177
+ _releaseAsyncSpendClaimsUnlessInFlight(paymentHash) {
5178
+ const claims = this._asyncSpendClaims.get(paymentHash.toString('hex'));
5179
+ if (!claims || !claims.some((claim) => claim.reserved))
5026
5180
  return;
5181
+ if (this.node.hasHtlcInFlight(paymentHash))
5182
+ return;
5183
+ let released = false;
5184
+ for (const claim of claims) {
5185
+ released = this._releaseClaimReservation(claim) || released;
5186
+ }
5187
+ if (released)
5188
+ this._persistSpendState();
5189
+ }
5190
+ _chargeAsyncSpendClaim(paymentHashHex, claim) {
5027
5191
  this._expireAsyncSpendClaims();
5028
5192
  const claims = this._asyncSpendClaims.get(paymentHashHex);
5029
5193
  if (!claims || claims.length === 0)
5030
5194
  return;
5031
- const reserved = claims.findIndex((entry) => entry.reserved);
5032
- const [claim] = claims.splice(reserved === -1 ? 0 : reserved, 1);
5195
+ let index;
5196
+ if (claim) {
5197
+ index = claims.indexOf(claim);
5198
+ if (index === -1)
5199
+ return;
5200
+ }
5201
+ else {
5202
+ const rank = (entry) => (entry.reserved ? 0 : 2) + (entry.settled ? 1 : 0);
5203
+ index = 0;
5204
+ for (let i = 1; i < claims.length; i++) {
5205
+ if (rank(claims[i]) < rank(claims[index]))
5206
+ index = i;
5207
+ }
5208
+ }
5209
+ const [charged] = claims.splice(index, 1);
5210
+ for (const rest of claims) {
5211
+ rest.settled = true;
5212
+ this._releaseClaimReservation(rest);
5213
+ }
5033
5214
  if (claims.length === 0)
5034
5215
  this._asyncSpendClaims.delete(paymentHashHex);
5035
- this._releaseAsyncSpendClaim(claim);
5036
- this._recordSpend(claim.sats);
5037
- }
5038
- _acquireBlockingPayment(paymentHashHex) {
5039
- this._blockingPaymentHashes.set(paymentHashHex, (this._blockingPaymentHashes.get(paymentHashHex) ?? 0) + 1);
5040
- }
5041
- _releaseBlockingPayment(paymentHashHex) {
5042
- const held = this._blockingPaymentHashes.get(paymentHashHex);
5043
- if (held === undefined)
5044
- return;
5045
- if (held > 1)
5046
- this._blockingPaymentHashes.set(paymentHashHex, held - 1);
5047
- else
5048
- this._blockingPaymentHashes.delete(paymentHashHex);
5216
+ this._releaseClaimReservation(charged);
5217
+ this._recordSpend(charged.sats);
5049
5218
  }
5050
- _expireAsyncSpendClaims() {
5219
+ _expireAsyncSpendClaims(persist = true) {
5051
5220
  const now = Date.now();
5221
+ let released = false;
5052
5222
  for (const claims of this._asyncSpendClaims.values()) {
5053
5223
  for (const claim of claims) {
5054
5224
  if (claim.reserved && claim.expiresAt <= now) {
5055
- this._releaseAsyncSpendClaim(claim);
5225
+ released = this._releaseClaimReservation(claim) || released;
5056
5226
  }
5057
5227
  }
5058
5228
  }
5229
+ if (released && persist)
5230
+ this._persistSpendState();
5059
5231
  }
5060
5232
  _countAsyncSpendClaims() {
5061
5233
  let total = 0;
@@ -5067,6 +5239,7 @@ class BeignetNode extends events_1.EventEmitter {
5067
5239
  let count = this._countAsyncSpendClaims();
5068
5240
  if (count <= target)
5069
5241
  return count;
5242
+ const before = count;
5070
5243
  for (const [paymentHashHex, claims] of this._asyncSpendClaims) {
5071
5244
  for (let i = 0; i < claims.length && count > target;) {
5072
5245
  if (claims[i].reserved) {
@@ -5081,8 +5254,164 @@ class BeignetNode extends events_1.EventEmitter {
5081
5254
  if (count <= target)
5082
5255
  break;
5083
5256
  }
5257
+ if (count < before)
5258
+ this._persistSpendState();
5084
5259
  return count;
5085
5260
  }
5261
+ _persistSpendState() {
5262
+ if (this._dailySpendLimitSats === undefined)
5263
+ return;
5264
+ const storage = this.storage;
5265
+ if (!storage)
5266
+ return;
5267
+ const claims = {};
5268
+ for (const [paymentHashHex, list] of this._asyncSpendClaims) {
5269
+ if (list.length > 0)
5270
+ claims[paymentHashHex] = list;
5271
+ }
5272
+ const state = {
5273
+ resetTime: this._dailySpendResetTime,
5274
+ totalSats: this._dailySpentSats,
5275
+ lightningSats: this._dailySpentLightningSats,
5276
+ onchainSats: this._dailySpentOnchainSats,
5277
+ claims
5278
+ };
5279
+ try {
5280
+ storage.saveMetadata(exports.DAILY_SPEND_STATE_KEY, JSON.stringify(state));
5281
+ }
5282
+ catch (err) {
5283
+ this.log('warn', 'Could not persist the daily spend ledger', {
5284
+ error: err instanceof Error ? err.message : String(err)
5285
+ });
5286
+ }
5287
+ }
5288
+ _loadSpendState() {
5289
+ for (const claims of this._asyncSpendClaims.values()) {
5290
+ for (const claim of claims)
5291
+ this._releaseClaimReservation(claim);
5292
+ }
5293
+ this._asyncSpendClaims.clear();
5294
+ this._dailySpendResetTime = 0;
5295
+ this._dailySpentSats = 0;
5296
+ this._dailySpentLightningSats = 0;
5297
+ this._dailySpentOnchainSats = 0;
5298
+ let stored;
5299
+ try {
5300
+ const raw = this.storage.loadMetadata(exports.DAILY_SPEND_STATE_KEY);
5301
+ if (raw)
5302
+ stored = JSON.parse(raw);
5303
+ }
5304
+ catch (err) {
5305
+ this.log('warn', 'Could not read the persisted daily spend ledger; the day starts at zero', { error: err instanceof Error ? err.message : String(err) });
5306
+ }
5307
+ const nonNegative = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
5308
+ if (stored &&
5309
+ nonNegative(stored.resetTime) &&
5310
+ Date.now() < stored.resetTime &&
5311
+ stored.resetTime <= nextUtcMidnight() &&
5312
+ nonNegative(stored.totalSats) &&
5313
+ nonNegative(stored.lightningSats) &&
5314
+ nonNegative(stored.onchainSats)) {
5315
+ this._dailySpendResetTime = stored.resetTime;
5316
+ this._dailySpentSats = stored.totalSats;
5317
+ this._dailySpentLightningSats = stored.lightningSats;
5318
+ this._dailySpentOnchainSats = stored.onchainSats;
5319
+ }
5320
+ else {
5321
+ this._resetDailySpendIfNeeded(false);
5322
+ }
5323
+ const isClaim = (value) => {
5324
+ const entry = value;
5325
+ return (typeof entry === 'object' &&
5326
+ entry !== null &&
5327
+ nonNegative(entry.sats) &&
5328
+ typeof entry.expiresAt === 'number' &&
5329
+ Number.isFinite(entry.expiresAt) &&
5330
+ typeof entry.reserved === 'boolean' &&
5331
+ (entry.settled === undefined || typeof entry.settled === 'boolean'));
5332
+ };
5333
+ let restored = 0;
5334
+ let charged = 0;
5335
+ let dropped = 0;
5336
+ const toCharge = [];
5337
+ const rows = stored?.claims && typeof stored.claims === 'object'
5338
+ ? Object.entries(stored.claims)
5339
+ : [];
5340
+ for (const [paymentHashHex, list] of rows) {
5341
+ const total = Array.isArray(list) ? list.length : 0;
5342
+ const claims = Array.isArray(list) ? list.filter(isClaim) : [];
5343
+ dropped += total - claims.length;
5344
+ if (claims.length === 0)
5345
+ continue;
5346
+ if (!/^[0-9a-f]{64}$/.test(paymentHashHex)) {
5347
+ dropped += claims.length;
5348
+ continue;
5349
+ }
5350
+ const paymentHash = Buffer.from(paymentHashHex, 'hex');
5351
+ const outcome = this._spendClaimOutcomeAtBoot(paymentHash, paymentHashHex);
5352
+ if (outcome === 'gone') {
5353
+ dropped += claims.length;
5354
+ continue;
5355
+ }
5356
+ const kept = claims.map((entry) => ({
5357
+ sats: entry.sats,
5358
+ expiresAt: entry.expiresAt,
5359
+ reserved: entry.reserved && outcome !== 'settled',
5360
+ ...(entry.settled ? { settled: true } : {})
5361
+ }));
5362
+ this._asyncSpendClaims.set(paymentHashHex, kept);
5363
+ for (const entry of kept) {
5364
+ if (entry.reserved)
5365
+ this._pendingSpendSats += entry.sats;
5366
+ }
5367
+ restored += kept.length;
5368
+ if (outcome === 'settled' && kept.some((entry) => !entry.settled)) {
5369
+ toCharge.push(paymentHashHex);
5370
+ }
5371
+ }
5372
+ for (const paymentHashHex of toCharge) {
5373
+ this._chargeAsyncSpendClaim(paymentHashHex);
5374
+ charged++;
5375
+ }
5376
+ this._expireAsyncSpendClaims(false);
5377
+ this._persistSpendState();
5378
+ if (stored) {
5379
+ this.log('info', 'Daily spend ledger restored', {
5380
+ spentSats: this._dailySpentSats,
5381
+ resetsAt: this._dailySpendResetTime,
5382
+ claimsRestored: restored,
5383
+ settledWhileDown: charged,
5384
+ claimsDropped: dropped
5385
+ });
5386
+ }
5387
+ }
5388
+ _spendClaimOutcomeAtBoot(paymentHash, paymentHashHex) {
5389
+ const view = this.node.getOutgoingHtlcs(paymentHash);
5390
+ if (view.preimage || view.status === types_5.PaymentStatus.COMPLETED) {
5391
+ return 'settled';
5392
+ }
5393
+ try {
5394
+ const durable = this.storage.loadPayment(paymentHashHex);
5395
+ if (durable?.direction === types_5.PaymentDirection.OUTGOING &&
5396
+ (durable.status === types_5.PaymentStatus.COMPLETED ||
5397
+ durable.preimage !== undefined ||
5398
+ this.storage.loadPreimage(paymentHashHex) !== null)) {
5399
+ return 'settled';
5400
+ }
5401
+ }
5402
+ catch (err) {
5403
+ this.log('warn', 'Could not read a payment record for the daily spend ledger; keeping its claim', {
5404
+ paymentHash: paymentHashHex,
5405
+ error: err instanceof Error ? err.message : String(err)
5406
+ });
5407
+ return 'live';
5408
+ }
5409
+ if (view.status === types_5.PaymentStatus.PENDING ||
5410
+ this.node.hasHtlcInFlight(paymentHash)) {
5411
+ return 'live';
5412
+ }
5413
+ return 'gone';
5414
+ }
5086
5415
  _builtOnchainTotalSats(amountSats) {
5087
5416
  const feeSats = this.wallet.transaction?.data?.fee;
5088
5417
  if (!Number.isFinite(feeSats) || feeSats < 0) {
@@ -5095,11 +5424,16 @@ class BeignetNode extends events_1.EventEmitter {
5095
5424
  }
5096
5425
  getDailySpendInfo() {
5097
5426
  this._resetDailySpendIfNeeded();
5427
+ this._expireAsyncSpendClaims();
5098
5428
  const limit = this._dailySpendLimitSats ?? null;
5429
+ const pendingSats = this._pendingSpendSats;
5099
5430
  return {
5100
5431
  limitSats: limit,
5101
5432
  spentSats: this._dailySpentSats,
5102
- remainingSats: limit !== null ? Math.max(0, limit - this._dailySpentSats) : Infinity,
5433
+ remainingSats: limit !== null
5434
+ ? Math.max(0, limit - this._dailySpentSats - pendingSats)
5435
+ : Infinity,
5436
+ pendingSats,
5103
5437
  resetsAt: this._dailySpendResetTime,
5104
5438
  totalSats: this._dailySpentSats,
5105
5439
  lightningSats: this._dailySpentLightningSats,
@@ -5326,6 +5660,35 @@ class BeignetNode extends events_1.EventEmitter {
5326
5660
  }
5327
5661
  return height + cltvLimit;
5328
5662
  }
5663
+ _paymentTimeout(paymentHash, what, timeoutMs, claim) {
5664
+ const failed = this.node.failPaymentUnlessInFlight(paymentHash);
5665
+ if (failed && claim)
5666
+ this._releaseAsyncSpendClaim(claim);
5667
+ return new errors_1.BeignetError('PAYMENT_TIMEOUT', failed
5668
+ ? `${what} timed out after ${timeoutMs}ms`
5669
+ : `${what} timed out after ${timeoutMs}ms; an HTLC is still in flight and the payment stays PENDING until it resolves; no further route is tried after the timeout`);
5670
+ }
5671
+ _toBeignetPaymentError(err) {
5672
+ if (err instanceof errors_1.BeignetError)
5673
+ return err;
5674
+ const msg = err instanceof Error ? err.message : String(err);
5675
+ let code = 'PAYMENT_FAILED';
5676
+ if (err instanceof Error && 'code' in err) {
5677
+ const lpErr = err;
5678
+ code = exports.ENGINE_PAYMENT_ERROR_CODES[lpErr.code] || 'PAYMENT_FAILED';
5679
+ }
5680
+ else if (msg.includes('No route found')) {
5681
+ code = 'NO_ROUTE';
5682
+ }
5683
+ else if (msg.includes('already in flight')) {
5684
+ code = 'DUPLICATE_PAYMENT';
5685
+ }
5686
+ else if (msg.includes('No channel to first hop') ||
5687
+ msg.includes('Peer not found')) {
5688
+ code = 'PEER_NOT_CONNECTED';
5689
+ }
5690
+ return new errors_1.BeignetError(code, msg);
5691
+ }
5329
5692
  async payInvoice(bolt11, timeoutMs = 60000, maxFeeSats, amountSats, metadata, cltvLimit) {
5330
5693
  this._checkDraining();
5331
5694
  const decoded = decodeInvoiceInput(bolt11);
@@ -5340,62 +5703,41 @@ class BeignetNode extends events_1.EventEmitter {
5340
5703
  1000n
5341
5704
  : undefined;
5342
5705
  const maxCltvExpiryHeight = this._cltvCeiling(cltvLimit);
5706
+ let claim;
5343
5707
  if (spendAmountSats > 0) {
5344
5708
  this._checkMaxPayment(spendAmountSats);
5345
5709
  this._checkSpendLimit(spendAmountSats);
5346
- this._pendingSpendSats += spendAmountSats;
5710
+ claim = this._openAsyncSpendClaim(paymentHashHex, spendAmountSats);
5347
5711
  }
5348
- this._acquireBlockingPayment(paymentHashHex);
5349
- let blockingReleased = false;
5350
- const releaseBlockingPayment = () => {
5351
- if (blockingReleased)
5352
- return;
5353
- blockingReleased = true;
5354
- this._releaseBlockingPayment(paymentHashHex);
5355
- };
5356
- let released = false;
5357
- const releaseReservation = () => {
5358
- if (spendAmountSats <= 0 || released)
5359
- return;
5360
- released = true;
5361
- this._pendingSpendSats -= spendAmountSats;
5362
- };
5363
5712
  try {
5364
5713
  if (metadata) {
5365
5714
  this.node.setPaymentMetadata(decoded.paymentHash, metadata);
5366
5715
  }
5367
5716
  }
5368
5717
  catch (err) {
5369
- releaseReservation();
5370
- releaseBlockingPayment();
5718
+ if (claim)
5719
+ this._closeAsyncSpendClaim(paymentHashHex, claim);
5371
5720
  throw err;
5372
5721
  }
5373
5722
  return new Promise((resolve, reject) => {
5374
5723
  const timer = setTimeout(() => {
5375
5724
  cleanup();
5376
- releaseReservation();
5377
- this.node.failPayment(decoded.paymentHash);
5378
- reject(new errors_1.BeignetError('PAYMENT_TIMEOUT', `Payment timed out after ${timeoutMs}ms`));
5725
+ reject(this._paymentTimeout(decoded.paymentHash, 'Payment', timeoutMs, claim));
5379
5726
  }, timeoutMs);
5380
5727
  const cleanup = () => {
5381
5728
  clearTimeout(timer);
5382
5729
  this.node.removeListener('payment:sent', onSent);
5383
5730
  this.node.removeListener('payment:failed', onFailed);
5384
- releaseBlockingPayment();
5385
5731
  };
5386
5732
  const onSent = (info) => {
5387
5733
  if (info.paymentHash.toString('hex') === paymentHashHex) {
5388
5734
  cleanup();
5389
- releaseReservation();
5390
- if (spendAmountSats > 0)
5391
- this._recordSpend(spendAmountSats);
5392
5735
  resolve(this.toPaymentInfo(info));
5393
5736
  }
5394
5737
  };
5395
5738
  const onFailed = (info) => {
5396
5739
  if (info.paymentHash.toString('hex') === paymentHashHex) {
5397
5740
  cleanup();
5398
- releaseReservation();
5399
5741
  const failDesc = info.failureCode !== undefined
5400
5742
  ? (0, errors_1.describeFailureCode)(info.failureCode)
5401
5743
  : 'unknown';
@@ -5409,33 +5751,9 @@ class BeignetNode extends events_1.EventEmitter {
5409
5751
  }
5410
5752
  catch (err) {
5411
5753
  cleanup();
5412
- releaseReservation();
5413
- const msg = err instanceof Error ? err.message : String(err);
5414
- let code = 'PAYMENT_FAILED';
5415
- if (err instanceof Error && 'code' in err) {
5416
- const lpErr = err;
5417
- const codeMap = {
5418
- NO_ROUTE: 'NO_ROUTE',
5419
- DUPLICATE_PAYMENT: 'DUPLICATE_PAYMENT',
5420
- NO_CHANNEL_TO_HOP: 'PEER_NOT_CONNECTED',
5421
- FEE_EXCEEDS_MAX: 'PAYMENT_FAILED',
5422
- CLTV_EXCEEDS_MAX: 'CLTV_EXCEEDS_MAX',
5423
- MISSING_AMOUNT: 'INVALID_PARAMS',
5424
- INVALID_INVOICE: 'INVALID_PARAMS',
5425
- INVOICE_EXPIRED: 'INVOICE_EXPIRED'
5426
- };
5427
- code = codeMap[lpErr.code] || 'PAYMENT_FAILED';
5428
- }
5429
- else {
5430
- if (msg.includes('No route found'))
5431
- code = 'NO_ROUTE';
5432
- else if (msg.includes('already in flight'))
5433
- code = 'DUPLICATE_PAYMENT';
5434
- else if (msg.includes('No channel to first hop') ||
5435
- msg.includes('Peer not found'))
5436
- code = 'PEER_NOT_CONNECTED';
5437
- }
5438
- reject(new errors_1.BeignetError(code, msg));
5754
+ if (claim)
5755
+ this._closeAsyncSpendClaim(paymentHashHex, claim);
5756
+ reject(this._toBeignetPaymentError(err));
5439
5757
  }
5440
5758
  });
5441
5759
  }
@@ -5457,7 +5775,7 @@ class BeignetNode extends events_1.EventEmitter {
5457
5775
  catch {
5458
5776
  }
5459
5777
  if (hashHex !== 'unknown') {
5460
- const existing = this.getPayment(hashHex);
5778
+ const existing = this.getPayment(hashHex) ?? this.durablePaymentFor(err, hashHex);
5461
5779
  if (existing)
5462
5780
  return existing;
5463
5781
  }
@@ -5489,7 +5807,8 @@ class BeignetNode extends events_1.EventEmitter {
5489
5807
  throw err;
5490
5808
  lastError = err;
5491
5809
  if (!(0, errors_1.isRetryableError)(err)) {
5492
- const pi = this.getPayment(paymentHashHex);
5810
+ const pi = this.getPayment(paymentHashHex) ??
5811
+ this.durablePaymentFor(err, paymentHashHex);
5493
5812
  if (pi)
5494
5813
  return { ...pi, attempts: attempt };
5495
5814
  return {
@@ -5601,11 +5920,7 @@ class BeignetNode extends events_1.EventEmitter {
5601
5920
  catch (err) {
5602
5921
  if (claim)
5603
5922
  this._closeAsyncSpendClaim(paymentHashHex, claim);
5604
- if (err instanceof Error &&
5605
- err.code === 'CLTV_EXCEEDS_MAX') {
5606
- throw new errors_1.BeignetError('CLTV_EXCEEDS_MAX', err.message);
5607
- }
5608
- throw err;
5923
+ throw this._toBeignetPaymentError(err);
5609
5924
  }
5610
5925
  if (claim && result.status === 'FAILED') {
5611
5926
  this._releaseAsyncSpendClaim(claim);
@@ -5624,14 +5939,8 @@ class BeignetNode extends events_1.EventEmitter {
5624
5939
  : undefined;
5625
5940
  this._checkMaxPayment(amountSats);
5626
5941
  this._checkSpendLimit(amountSats);
5627
- this._pendingSpendSats += amountSats;
5628
- let released = false;
5629
- const releaseReservation = () => {
5630
- if (released)
5631
- return;
5632
- released = true;
5633
- this._pendingSpendSats -= amountSats;
5634
- };
5942
+ const provisionalKey = `keysend:${crypto.randomBytes(8).toString('hex')}`;
5943
+ const claim = this._openAsyncSpendClaim(provisionalKey, amountSats);
5635
5944
  const destination = Buffer.from(pubkey, 'hex');
5636
5945
  let result;
5637
5946
  try {
@@ -5643,22 +5952,29 @@ class BeignetNode extends events_1.EventEmitter {
5643
5952
  });
5644
5953
  }
5645
5954
  catch (err) {
5646
- releaseReservation();
5647
- throw err;
5955
+ if (claim)
5956
+ this._closeAsyncSpendClaim(provisionalKey, claim);
5957
+ throw this._toBeignetPaymentError(err);
5648
5958
  }
5649
5959
  const paymentHashHex = result.paymentHash.toString('hex');
5960
+ if (claim) {
5961
+ this._rekeyAsyncSpendClaim(provisionalKey, paymentHashHex, claim);
5962
+ }
5650
5963
  if (result.status !== 'PENDING') {
5651
- releaseReservation();
5652
- if (result.status === 'COMPLETED')
5653
- this._recordSpend(amountSats);
5964
+ if (claim) {
5965
+ if (result.status === 'COMPLETED') {
5966
+ this._chargeAsyncSpendClaim(paymentHashHex, claim);
5967
+ }
5968
+ else {
5969
+ this._releaseAsyncSpendClaim(claim);
5970
+ }
5971
+ }
5654
5972
  return this.toPaymentInfo(result);
5655
5973
  }
5656
5974
  return new Promise((resolve, reject) => {
5657
5975
  const timer = setTimeout(() => {
5658
5976
  cleanup();
5659
- releaseReservation();
5660
- this.node.failPayment(result.paymentHash);
5661
- reject(new errors_1.BeignetError('PAYMENT_TIMEOUT', `Keysend timed out after ${timeoutMs}ms`));
5977
+ reject(this._paymentTimeout(result.paymentHash, 'Keysend', timeoutMs, claim));
5662
5978
  }, timeoutMs);
5663
5979
  const cleanup = () => {
5664
5980
  clearTimeout(timer);
@@ -5668,15 +5984,12 @@ class BeignetNode extends events_1.EventEmitter {
5668
5984
  const onSent = (info) => {
5669
5985
  if (info.paymentHash.toString('hex') === paymentHashHex) {
5670
5986
  cleanup();
5671
- releaseReservation();
5672
- this._recordSpend(amountSats);
5673
5987
  resolve(this.toPaymentInfo(info));
5674
5988
  }
5675
5989
  };
5676
5990
  const onFailed = (info) => {
5677
5991
  if (info.paymentHash.toString('hex') === paymentHashHex) {
5678
5992
  cleanup();
5679
- releaseReservation();
5680
5993
  const failDesc = info.failureCode !== undefined
5681
5994
  ? (0, errors_1.describeFailureCode)(info.failureCode)
5682
5995
  : 'unknown';
@@ -5740,6 +6053,19 @@ class BeignetNode extends events_1.EventEmitter {
5740
6053
  return null;
5741
6054
  return this.toPaymentInfo(p);
5742
6055
  }
6056
+ durablePaymentFor(err, hashHex) {
6057
+ if (!(err instanceof errors_1.BeignetError) ||
6058
+ err.code !== errors_1.BeignetErrorCode.DUPLICATE_PAYMENT) {
6059
+ return null;
6060
+ }
6061
+ try {
6062
+ const durable = this.storage.loadPayment(hashHex);
6063
+ return durable ? this.toPaymentInfo(durable) : null;
6064
+ }
6065
+ catch {
6066
+ return null;
6067
+ }
6068
+ }
5743
6069
  listForwards(filter) {
5744
6070
  return this.node.listForwards(filter).map((f) => ({
5745
6071
  id: f.id,
@@ -6166,45 +6492,31 @@ class BeignetNode extends events_1.EventEmitter {
6166
6492
  this._checkDraining();
6167
6493
  const paymentHashHex = bolt12Invoice.paymentHash.toString('hex');
6168
6494
  const spendAmountSats = paymentSpendSats(bolt12Invoice.amount, amountSats);
6495
+ let claim;
6169
6496
  if (spendAmountSats > 0) {
6170
6497
  this._checkMaxPayment(spendAmountSats);
6171
6498
  this._checkSpendLimit(spendAmountSats);
6172
- this._pendingSpendSats += spendAmountSats;
6499
+ claim = this._openAsyncSpendClaim(paymentHashHex, spendAmountSats);
6173
6500
  }
6174
- this._acquireBlockingPayment(paymentHashHex);
6175
- let released = false;
6176
- const releaseReservation = () => {
6177
- if (spendAmountSats <= 0 || released)
6178
- return;
6179
- released = true;
6180
- this._pendingSpendSats -= spendAmountSats;
6181
- };
6182
6501
  return new Promise((resolve, reject) => {
6183
6502
  const timer = setTimeout(() => {
6184
6503
  cleanup();
6185
- releaseReservation();
6186
- this.node.failPayment(bolt12Invoice.paymentHash);
6187
- reject(new errors_1.BeignetError('PAYMENT_TIMEOUT', `Payment timed out after ${timeoutMs}ms`));
6504
+ reject(this._paymentTimeout(bolt12Invoice.paymentHash, 'Payment', timeoutMs, claim));
6188
6505
  }, timeoutMs);
6189
6506
  const cleanup = () => {
6190
6507
  clearTimeout(timer);
6191
6508
  this.node.removeListener('payment:sent', onSent);
6192
6509
  this.node.removeListener('payment:failed', onFailed);
6193
- this._releaseBlockingPayment(paymentHashHex);
6194
6510
  };
6195
6511
  const onSent = (info) => {
6196
6512
  if (info.paymentHash.toString('hex') === paymentHashHex) {
6197
6513
  cleanup();
6198
- releaseReservation();
6199
- if (spendAmountSats > 0)
6200
- this._recordSpend(spendAmountSats);
6201
6514
  resolve(this.toPaymentInfo(info));
6202
6515
  }
6203
6516
  };
6204
6517
  const onFailed = (info) => {
6205
6518
  if (info.paymentHash.toString('hex') === paymentHashHex) {
6206
6519
  cleanup();
6207
- releaseReservation();
6208
6520
  const failDesc = info.failureCode !== undefined
6209
6521
  ? (0, errors_1.describeFailureCode)(info.failureCode)
6210
6522
  : 'unknown';
@@ -6218,9 +6530,9 @@ class BeignetNode extends events_1.EventEmitter {
6218
6530
  }
6219
6531
  catch (err) {
6220
6532
  cleanup();
6221
- releaseReservation();
6222
- const msg = err instanceof Error ? err.message : String(err);
6223
- reject(new errors_1.BeignetError('PAYMENT_FAILED', msg));
6533
+ if (claim)
6534
+ this._closeAsyncSpendClaim(paymentHashHex, claim);
6535
+ reject(this._toBeignetPaymentError(err));
6224
6536
  }
6225
6537
  });
6226
6538
  }
@@ -6873,12 +7185,121 @@ class BeignetNode extends events_1.EventEmitter {
6873
7185
  setPaymentMetadata(paymentHash, metadata) {
6874
7186
  this.node.setPaymentMetadata(Buffer.from(paymentHash, 'hex'), metadata);
6875
7187
  }
7188
+ async resolveInterruptedPayment(bolt11) {
7189
+ if (this.destroyed) {
7190
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NODE_DESTROYED, 'Node is shut down; an interrupted payment is settled at the next start');
7191
+ }
7192
+ if (this._restorePending || this._resuming) {
7193
+ throw new errors_1.BeignetError('NODE_RESTORE_PENDING', 'No node is running yet to settle an interrupted payment against');
7194
+ }
7195
+ if (this._restartRequired) {
7196
+ throw new errors_1.BeignetError('NODE_RESTART_REQUIRED', 'A capsule restore replaced this database; an interrupted payment ' +
7197
+ 'is settled after the restart');
7198
+ }
7199
+ let paymentHash;
7200
+ try {
7201
+ paymentHash = (0, decode_1.decode)(bolt11).paymentHash;
7202
+ }
7203
+ catch {
7204
+ return { status: 'unpaid' };
7205
+ }
7206
+ const hashHex = paymentHash.toString('hex');
7207
+ const view = await this.node.awaitPaymentResolution(paymentHash);
7208
+ if (view.preimage || view.status === types_5.PaymentStatus.COMPLETED) {
7209
+ return { status: 'completed', paymentHash: hashHex };
7210
+ }
7211
+ const durable = this.storage.loadPayment(hashHex);
7212
+ if (durable?.direction === types_5.PaymentDirection.OUTGOING &&
7213
+ (durable.status === types_5.PaymentStatus.COMPLETED ||
7214
+ durable.preimage !== undefined)) {
7215
+ return { status: 'completed', paymentHash: hashHex };
7216
+ }
7217
+ return { status: 'unpaid' };
7218
+ }
7219
+ canCarryHtlc() {
7220
+ const live = this.node
7221
+ .getChannelManager()
7222
+ .listChannels()
7223
+ .filter(canBecomeUsable);
7224
+ return live.length === 0 || live.some((ch) => ch.acceptsNewHtlcs());
7225
+ }
7226
+ whenReadyToPay(run) {
7227
+ if (this.destroyed)
7228
+ return;
7229
+ if (this._restorePending) {
7230
+ this.once('recovery:restored', () => this.whenReadyToPay(run));
7231
+ return;
7232
+ }
7233
+ if (this._resuming || this._restartRequired)
7234
+ return;
7235
+ const node = this.node;
7236
+ const live = () => !this.destroyed &&
7237
+ !this._resuming &&
7238
+ !this._restartRequired &&
7239
+ this.node === node;
7240
+ let ready;
7241
+ try {
7242
+ ready = node.waitForReady(BeignetNode.MAX_TIMER_MS);
7243
+ }
7244
+ catch {
7245
+ return;
7246
+ }
7247
+ ready
7248
+ .then(() => {
7249
+ if (!live())
7250
+ return;
7251
+ if (this.canCarryHtlc()) {
7252
+ run();
7253
+ return;
7254
+ }
7255
+ const recheck = () => {
7256
+ if (live() && !this.canCarryHtlc())
7257
+ return;
7258
+ this.removeListener('channel:usable', recheck);
7259
+ this.removeListener(CHANNELS_CHANGED, recheck);
7260
+ if (live())
7261
+ run();
7262
+ };
7263
+ this.on('channel:usable', recheck);
7264
+ this.on(CHANNELS_CHANGED, recheck);
7265
+ }, () => {
7266
+ })
7267
+ .catch((err) => {
7268
+ try {
7269
+ this.log('warn', 'Starting the payment queue failed', {
7270
+ error: err instanceof Error ? err.message : String(err)
7271
+ });
7272
+ }
7273
+ catch {
7274
+ }
7275
+ });
7276
+ }
6876
7277
  getPaymentQueue() {
6877
7278
  if (!this.paymentQueue) {
6878
- this.paymentQueue = new payment_queue_1.PaymentQueue((bolt11, timeout, maxFee, amount, meta) => this.payInvoiceSafe(bolt11, timeout, maxFee, amount, meta), (amount) => this.canSend(amount), undefined, this.storage);
7279
+ this.paymentQueue = new payment_queue_1.PaymentQueue((bolt11, timeout, maxFee, amount, meta) => this.payInvoiceSafe(bolt11, timeout, maxFee, amount, meta), (amount) => this.node
7280
+ ? this.canSend(amount)
7281
+ : { canSend: false, availableSats: 0 }, {
7282
+ resolveInterrupted: (b) => this.resolveInterruptedPayment(b),
7283
+ invoiceAmountSats: (b) => paymentSpendSats(decodeInvoiceInput(b).amountMsat)
7284
+ }, this.liveQueueStorage());
7285
+ if (this.destroyed) {
7286
+ this.paymentQueue.stop();
7287
+ }
7288
+ else {
7289
+ this.whenReadyToPay(() => this.paymentQueue?.start());
7290
+ this.on('channel:usable', () => this.paymentQueue?.poke());
7291
+ }
6879
7292
  }
6880
7293
  return this.paymentQueue;
6881
7294
  }
7295
+ liveQueueStorage() {
7296
+ return {
7297
+ saveQueueEntry: (entry) => this.storage.saveQueueEntry(entry),
7298
+ updateQueueEntryStatus: (id, status, error, completedAt) => this.storage.updateQueueEntryStatus(id, status, error, completedAt),
7299
+ deleteQueueEntry: (id) => this.storage.deleteQueueEntry(id),
7300
+ loadAllQueueEntries: () => this.storage.loadAllQueueEntries()
7301
+ };
7302
+ }
6882
7303
  enqueuePayment(bolt11, priority, opts) {
6883
7304
  return this.getPaymentQueue().enqueue(bolt11, priority, opts);
6884
7305
  }
@@ -7274,6 +7695,7 @@ class BeignetNode extends events_1.EventEmitter {
7274
7695
  clearInterval(this._fallbackRecoveryTimer);
7275
7696
  this._fallbackRecoveryTimer = undefined;
7276
7697
  }
7698
+ this.paymentQueue?.stop();
7277
7699
  this.paymentQueue?.removeAllListeners();
7278
7700
  this.directFundingSender?.stop();
7279
7701
  if (this._confirmTimer) {
@@ -7286,15 +7708,12 @@ class BeignetNode extends events_1.EventEmitter {
7286
7708
  await this._backupPromise.catch(() => {
7287
7709
  });
7288
7710
  }
7289
- await this.node?.gracefulShutdown(timeoutMs);
7290
- this.storage.close();
7291
- this.removeAllListeners();
7292
7711
  try {
7293
- await this.wallet?.stop();
7712
+ await this.node?.gracefulShutdown(timeoutMs);
7294
7713
  }
7295
- catch {
7714
+ finally {
7715
+ await this.stopWalletAndCloseStorage();
7296
7716
  }
7297
- this.releaseLock();
7298
7717
  }
7299
7718
  async destroy() {
7300
7719
  this.stopOfflineReceive();
@@ -7326,16 +7745,29 @@ class BeignetNode extends events_1.EventEmitter {
7326
7745
  }
7327
7746
  this.stopRecoveryLeaseCheck();
7328
7747
  this.clearAutoApplyTimers();
7748
+ this.paymentQueue?.stop();
7329
7749
  this.paymentQueue?.removeAllListeners();
7330
7750
  this.directFundingSender?.stop();
7331
- this.node?.destroy();
7332
- this.storage.close();
7751
+ try {
7752
+ this.node?.destroy();
7753
+ }
7754
+ finally {
7755
+ await this.stopWalletAndCloseStorage();
7756
+ }
7757
+ }
7758
+ async stopWalletAndCloseStorage() {
7759
+ this._nodeStorageView?.close();
7333
7760
  this.removeAllListeners();
7334
7761
  try {
7335
7762
  await this.wallet?.stop();
7336
7763
  }
7337
7764
  catch {
7338
7765
  }
7766
+ try {
7767
+ this.storage.close();
7768
+ }
7769
+ catch {
7770
+ }
7339
7771
  this.releaseLock();
7340
7772
  }
7341
7773
  releaseLock() {