beignet 0.26.0 → 0.28.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 (99) hide show
  1. package/README.md +11 -0
  2. package/dist/cli/auth.js +7 -0
  3. package/dist/cli/auth.js.map +1 -1
  4. package/dist/cli/beignet-node.js +301 -38
  5. package/dist/cli/beignet-node.js.map +1 -1
  6. package/dist/cli/cli.js +65 -2
  7. package/dist/cli/cli.js.map +1 -1
  8. package/dist/cli/config.js +7 -0
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/daemon-options.js +1 -0
  11. package/dist/cli/daemon-options.js.map +1 -1
  12. package/dist/cli/daemon.js +49 -4
  13. package/dist/cli/daemon.js.map +1 -1
  14. package/dist/cli/errors.js +7 -1
  15. package/dist/cli/errors.js.map +1 -1
  16. package/dist/cli/onchain-sweep.js +391 -0
  17. package/dist/cli/onchain-sweep.js.map +1 -0
  18. package/dist/cli/openapi.js +274 -6
  19. package/dist/cli/openapi.js.map +1 -1
  20. package/dist/lightning/chain/chain-monitor.js +3 -0
  21. package/dist/lightning/chain/chain-monitor.js.map +1 -1
  22. package/dist/lightning/chain/closing.js.map +1 -1
  23. package/dist/lightning/chain/types.js.map +1 -1
  24. package/dist/lightning/channel/channel-manager.js +111 -11
  25. package/dist/lightning/channel/channel-manager.js.map +1 -1
  26. package/dist/lightning/channel/channel-state.js.map +1 -1
  27. package/dist/lightning/channel/channel.js +386 -37
  28. package/dist/lightning/channel/channel.js.map +1 -1
  29. package/dist/lightning/channel/commitment-builder.js +2 -0
  30. package/dist/lightning/channel/commitment-builder.js.map +1 -1
  31. package/dist/lightning/channel/validation.js +10 -7
  32. package/dist/lightning/channel/validation.js.map +1 -1
  33. package/dist/lightning/channel/zero-reserve.js +3 -0
  34. package/dist/lightning/channel/zero-reserve.js.map +1 -0
  35. package/dist/lightning/features/flags.js +2 -0
  36. package/dist/lightning/features/flags.js.map +1 -1
  37. package/dist/lightning/gossip/gossip-sync.js +118 -34
  38. package/dist/lightning/gossip/gossip-sync.js.map +1 -1
  39. package/dist/lightning/gossip/network-graph.js +49 -20
  40. package/dist/lightning/gossip/network-graph.js.map +1 -1
  41. package/dist/lightning/gossip/pathfinding.js +3 -1
  42. package/dist/lightning/gossip/pathfinding.js.map +1 -1
  43. package/dist/lightning/gossip/pay-all.js +197 -0
  44. package/dist/lightning/gossip/pay-all.js.map +1 -0
  45. package/dist/lightning/gossip/rapid-sync.js +261 -86
  46. package/dist/lightning/gossip/rapid-sync.js.map +1 -1
  47. package/dist/lightning/keys/wallet-keys.js +10 -4
  48. package/dist/lightning/keys/wallet-keys.js.map +1 -1
  49. package/dist/lightning/message/codec.js +52 -1
  50. package/dist/lightning/message/codec.js.map +1 -1
  51. package/dist/lightning/message/dual-funding.js +30 -5
  52. package/dist/lightning/message/dual-funding.js.map +1 -1
  53. package/dist/lightning/node/lightning-node.js +385 -54
  54. package/dist/lightning/node/lightning-node.js.map +1 -1
  55. package/dist/lightning/node/types.js +3 -0
  56. package/dist/lightning/node/types.js.map +1 -1
  57. package/dist/lightning/storage/encryption.js +5 -5
  58. package/dist/lightning/storage/encryption.js.map +1 -1
  59. package/dist/lightning/storage/serialization.js +28 -0
  60. package/dist/lightning/storage/serialization.js.map +1 -1
  61. package/dist/lightning/storage/sqlite-storage.js +4 -2
  62. package/dist/lightning/storage/sqlite-storage.js.map +1 -1
  63. package/dist/lightning/watchtower/watchtower-client.js +8 -2
  64. package/dist/lightning/watchtower/watchtower-client.js.map +1 -1
  65. package/dist/types/cli/beignet-node.d.ts +20 -3
  66. package/dist/types/cli/errors.d.ts +3 -0
  67. package/dist/types/cli/index.d.ts +1 -0
  68. package/dist/types/cli/onchain-sweep.d.ts +57 -0
  69. package/dist/types/cli/types.d.ts +35 -0
  70. package/dist/types/lightning/chain/closing.d.ts +8 -0
  71. package/dist/types/lightning/chain/types.d.ts +1 -0
  72. package/dist/types/lightning/channel/channel-manager.d.ts +5 -1
  73. package/dist/types/lightning/channel/channel-state.d.ts +10 -0
  74. package/dist/types/lightning/channel/channel.d.ts +16 -2
  75. package/dist/types/lightning/channel/commitment-builder.d.ts +1 -0
  76. package/dist/types/lightning/channel/validation.d.ts +6 -3
  77. package/dist/types/lightning/channel/zero-reserve.d.ts +10 -0
  78. package/dist/types/lightning/features/flags.d.ts +1 -0
  79. package/dist/types/lightning/gossip/gossip-sync.d.ts +14 -0
  80. package/dist/types/lightning/gossip/network-graph.d.ts +8 -0
  81. package/dist/types/lightning/gossip/pathfinding.d.ts +8 -1
  82. package/dist/types/lightning/gossip/pay-all.d.ts +26 -0
  83. package/dist/types/lightning/gossip/rapid-sync.d.ts +10 -0
  84. package/dist/types/lightning/keys/wallet-keys.d.ts +2 -0
  85. package/dist/types/lightning/message/codec.d.ts +4 -0
  86. package/dist/types/lightning/message/dual-funding.d.ts +3 -0
  87. package/dist/types/lightning/node/lightning-node.d.ts +39 -3
  88. package/dist/types/lightning/node/types.d.ts +25 -0
  89. package/dist/types/lightning/storage/encryption.d.ts +1 -0
  90. package/dist/types/lightning/storage/serialization.d.ts +17 -0
  91. package/dist/types/types/wallet.d.ts +3 -1
  92. package/dist/types/wallet.js.map +1 -1
  93. package/dist/wallet/index.js +4 -2
  94. package/dist/wallet/index.js.map +1 -1
  95. package/docs/IROH-QUALIFICATION-2026-10-03.json +239 -0
  96. package/docs/IROH-QUALIFICATION-2026-10-03.md +115 -0
  97. package/docs/PAY-ALL.md +61 -0
  98. package/docs/ZERO-RESERVE.md +47 -0
  99. package/package.json +4 -4
@@ -43,6 +43,7 @@ const helpers_1 = require("../utils/helpers");
43
43
  const conversion_1 = require("../utils/conversion");
44
44
  const wallet_3 = require("../types/wallet");
45
45
  const wallet_storage_1 = require("./wallet-storage");
46
+ const onchain_sweep_1 = require("./onchain-sweep");
46
47
  const fs_utils_1 = require("./fs-utils");
47
48
  const backup_mac_1 = require("./backup-mac");
48
49
  const node_storage_view_1 = require("./node-storage-view");
@@ -174,6 +175,9 @@ function parseScid(scid) {
174
175
  }
175
176
  exports.parseScid = parseScid;
176
177
  exports.ENGINE_PAYMENT_ERROR_CODES = {
178
+ PAY_ALL_REVIEW_EXPIRED: 'PAY_ALL_REVIEW_EXPIRED',
179
+ PAY_ALL_REMAINDER: 'PAY_ALL_REMAINDER',
180
+ PAY_ALL_BUDGET_MISMATCH: 'PAY_ALL_BUDGET_MISMATCH',
177
181
  NO_ROUTE: 'NO_ROUTE',
178
182
  DUPLICATE_PAYMENT: 'DUPLICATE_PAYMENT',
179
183
  NO_CHANNEL_TO_HOP: 'PEER_NOT_CONNECTED',
@@ -718,6 +722,7 @@ class BeignetNode extends events_1.EventEmitter {
718
722
  this.rapidGossipSync = true;
719
723
  this._initialGossipPrime = null;
720
724
  this._releaseBootRgs = null;
725
+ this._rapidGossipSync = null;
721
726
  this._deferredGossipSyncs = new Set();
722
727
  this.electrumServerCount = 1;
723
728
  this._failoverInProgress = false;
@@ -861,6 +866,10 @@ class BeignetNode extends events_1.EventEmitter {
861
866
  throw new errors_1.BeignetError('INVALID_PARAMS', `leaseRates: ${refusal}`);
862
867
  }
863
868
  }
869
+ if (opts.waiveClientReserve !== undefined &&
870
+ typeof opts.waiveClientReserve !== 'boolean') {
871
+ throw new errors_1.BeignetError('INVALID_PARAMS', 'waiveClientReserve must be a boolean');
872
+ }
864
873
  if (opts.jitReceive !== undefined) {
865
874
  const refusal = jitReceiveRefusal(opts.jitReceive);
866
875
  if (refusal !== null) {
@@ -908,7 +917,7 @@ class BeignetNode extends events_1.EventEmitter {
908
917
  }
909
918
  let encryptionKey;
910
919
  if (opts.storageEncryption ?? true) {
911
- encryptionKey = (0, encryption_1.deriveStorageKey)(bip39.mnemonicToSeedSync(this.mnemonic));
920
+ encryptionKey = (0, encryption_1.deriveStorageKey)(this.walletSeed());
912
921
  }
913
922
  this._storageEncryptionKey = encryptionKey;
914
923
  this.finishStagedCapsuleRestore(dbPath);
@@ -969,6 +978,7 @@ class BeignetNode extends events_1.EventEmitter {
969
978
  };
970
979
  const walletResult = await wallet_1.Wallet.create({
971
980
  mnemonic: this.mnemonic,
981
+ seed: this.walletSeed(),
972
982
  network: beignetNetwork,
973
983
  disableRefreshOnCreate: true,
974
984
  ...(opts.addressType
@@ -1086,6 +1096,7 @@ class BeignetNode extends events_1.EventEmitter {
1086
1096
  this._nodeStorageView = (0, node_storage_view_1.nodeStorageView)(this.storage);
1087
1097
  this.node = lightning_node_1.LightningNode.fromMnemonic(this.mnemonic, {
1088
1098
  coinType,
1099
+ seed: this.walletSeed(),
1089
1100
  network: lnNetwork,
1090
1101
  storage: this._nodeStorageView,
1091
1102
  newChannelsRefused: () => this.newChannelRefusal(),
@@ -1135,6 +1146,11 @@ class BeignetNode extends events_1.EventEmitter {
1135
1146
  }
1136
1147
  : {}),
1137
1148
  ...(opts.fforIssuer === true ? { fforIssuer: { enabled: true } } : {}),
1149
+ zeroReserve: {
1150
+ role: 'primary',
1151
+ advertise: opts.waiveClientReserve === true,
1152
+ waiveClientReserve: opts.waiveClientReserve === true
1153
+ },
1138
1154
  jitReceive: opts.jitReceive?.enabled === true
1139
1155
  ? {
1140
1156
  enabled: true,
@@ -1770,6 +1786,10 @@ class BeignetNode extends events_1.EventEmitter {
1770
1786
  }
1771
1787
  if (this.rapidGossipSync && this.networkName === 'mainnet') {
1772
1788
  const rgs = this.syncRapidGossip().catch((err) => {
1789
+ if (err instanceof rapid_sync_1.RapidGossipCancelledError && this.destroyed) {
1790
+ this.log('debug', 'Rapid gossip sync cancelled by shutdown', {});
1791
+ return;
1792
+ }
1773
1793
  this.log('warn', 'Rapid gossip sync failed', {
1774
1794
  error: err instanceof Error ? err.message : String(err)
1775
1795
  });
@@ -2700,6 +2720,7 @@ class BeignetNode extends events_1.EventEmitter {
2700
2720
  return path.join(this.dataDir, `${this.networkName}.capsule-restore.json`);
2701
2721
  }
2702
2722
  carryDaemonState(from, to) {
2723
+ this.onchainSweeps().carryTo(to);
2703
2724
  const overrides = from.loadWalletData(auth_1.AUTH_KEY_OVERRIDES_STORAGE_KEY);
2704
2725
  if (overrides !== null) {
2705
2726
  to.saveWalletData(auth_1.AUTH_KEY_OVERRIDES_STORAGE_KEY, overrides);
@@ -3078,10 +3099,19 @@ class BeignetNode extends events_1.EventEmitter {
3078
3099
  }
3079
3100
  nodeSecret() {
3080
3101
  if (!this._nodeSecret) {
3081
- this._nodeSecret = (0, wallet_keys_1.deriveLightningKeysFromMnemonic)(this.mnemonic, undefined, this.toCoinType(this.networkName)).nodePrivateKey;
3102
+ if (!bip39.validateMnemonic(this.mnemonic)) {
3103
+ throw new Error('Invalid BIP39 mnemonic');
3104
+ }
3105
+ this._nodeSecret = (0, wallet_keys_1.deriveLightningKeysFromSeed)(this.walletSeed(), this.toCoinType(this.networkName)).nodePrivateKey;
3082
3106
  }
3083
3107
  return this._nodeSecret;
3084
3108
  }
3109
+ walletSeed() {
3110
+ if (!this._walletSeed) {
3111
+ this._walletSeed = bip39.mnemonicToSeedSync(this.mnemonic);
3112
+ }
3113
+ return Buffer.from(this._walletSeed);
3114
+ }
3085
3115
  async restoreFromGuardians() {
3086
3116
  if (this._restoreInFlight) {
3087
3117
  throw new errors_1.BeignetError('RESTORE_IN_PROGRESS', 'A guardian restore is already running; watch ' +
@@ -3286,9 +3316,15 @@ class BeignetNode extends events_1.EventEmitter {
3286
3316
  ]);
3287
3317
  let totalMsat = 0n;
3288
3318
  for (const ch of this.node.listChannels()) {
3289
- if (!recovering.has(ch.state))
3319
+ const effectiveState = ch.state === types_7.ChannelState.AWAITING_REESTABLISH
3320
+ ? this.node
3321
+ .getChannelManager()
3322
+ .getChannel(ch.channelId)
3323
+ ?.getFullState().preReestablishState ?? ch.state
3324
+ : ch.state;
3325
+ if (!recovering.has(effectiveState))
3290
3326
  continue;
3291
- if (ch.state === types_7.ChannelState.FORCE_CLOSED &&
3327
+ if (effectiveState === types_7.ChannelState.FORCE_CLOSED &&
3292
3328
  this.isForceCloseBalanceInWallet(ch.channelId)) {
3293
3329
  continue;
3294
3330
  }
@@ -3730,6 +3766,50 @@ class BeignetNode extends events_1.EventEmitter {
3730
3766
  }
3731
3767
  });
3732
3768
  }
3769
+ onchainSweeps() {
3770
+ return new onchain_sweep_1.OnchainSweeps({
3771
+ storage: this.storage,
3772
+ wallet: this.wallet,
3773
+ network: this.getBitcoinNetwork(),
3774
+ broadcast: (hex) => this._broadcastRawTx(hex),
3775
+ admitAndSave: (debitSats, save) => {
3776
+ this._checkMaxPayment(debitSats);
3777
+ this._checkSpendLimit(debitSats);
3778
+ const before = {
3779
+ total: this._dailySpentSats,
3780
+ onchain: this._dailySpentOnchainSats,
3781
+ lightning: this._dailySpentLightningSats,
3782
+ reset: this._dailySpendResetTime
3783
+ };
3784
+ try {
3785
+ this.storage.transaction(() => {
3786
+ this._recordSpend(debitSats, 'onchain');
3787
+ this._persistSpendState(true);
3788
+ save();
3789
+ });
3790
+ }
3791
+ catch (error) {
3792
+ this._dailySpentSats = before.total;
3793
+ this._dailySpentOnchainSats = before.onchain;
3794
+ this._dailySpentLightningSats = before.lightning;
3795
+ this._dailySpendResetTime = before.reset;
3796
+ throw error;
3797
+ }
3798
+ }
3799
+ });
3800
+ }
3801
+ prepareOnchainSweep(request) {
3802
+ return this._runOnchainSend(() => this.onchainSweeps().prepare(request));
3803
+ }
3804
+ submitOnchainSweep(requestId) {
3805
+ return this._runOnchainSend(() => this.onchainSweeps().submit(requestId));
3806
+ }
3807
+ getOnchainSweep(requestId) {
3808
+ return this.onchainSweeps().get(requestId);
3809
+ }
3810
+ cancelOnchainSweep(requestId) {
3811
+ return this._runOnchainSend(() => this.onchainSweeps().cancel(requestId));
3812
+ }
3733
3813
  async bumpFeeOnchain(txid, satsPerVbyte) {
3734
3814
  this._validateTxid(txid);
3735
3815
  this._validateFeeRate(satsPerVbyte, true);
@@ -3942,9 +4022,48 @@ class BeignetNode extends events_1.EventEmitter {
3942
4022
  };
3943
4023
  }
3944
4024
  listOnchainTransactions() {
3945
- return Object.values(this.wallet.transactions)
3946
- .map((tx) => this.toOnchainTxInfo(tx))
3947
- .sort((a, b) => b.timestamp - a.timestamp);
4025
+ const transactions = new Map(Object.values(this.wallet.transactions).map((tx) => {
4026
+ const info = this.toOnchainTxInfo(tx);
4027
+ return [info.txid, info];
4028
+ }));
4029
+ for (const close of this.node.listExternalClosePayments()) {
4030
+ const bitcoin = require('bitcoinjs-lib');
4031
+ transactions.set(close.txid, {
4032
+ txid: close.txid,
4033
+ type: 'sent',
4034
+ valueSats: close.amountSats,
4035
+ feeSats: close.feeSats,
4036
+ satsPerVbyte: close.satsPerVbyte,
4037
+ address: bitcoin.address.fromOutputScript(Buffer.from(close.scriptHex, 'hex'), this.getBitcoinNetwork()),
4038
+ confirmed: close.confirmationHeight > 0,
4039
+ ...(close.confirmationHeight > 0
4040
+ ? { height: close.confirmationHeight }
4041
+ : {}),
4042
+ timestamp: close.timestamp,
4043
+ source: 'cooperative-close',
4044
+ channelId: close.channelId
4045
+ });
4046
+ }
4047
+ if (typeof this.storage?.loadWalletData === 'function') {
4048
+ for (const sweep of this.onchainSweeps().list()) {
4049
+ if (!sweep.txid ||
4050
+ !['submitted', 'confirmed'].includes(sweep.status) ||
4051
+ transactions.has(sweep.txid))
4052
+ continue;
4053
+ transactions.set(sweep.txid, {
4054
+ txid: sweep.txid,
4055
+ type: 'sent',
4056
+ valueSats: sweep.amountSats,
4057
+ feeSats: sweep.feeSats,
4058
+ satsPerVbyte: sweep.satsPerVbyte,
4059
+ address: sweep.address,
4060
+ confirmed: sweep.status === 'confirmed',
4061
+ timestamp: sweep.createdAt,
4062
+ source: 'onchain-sweep'
4063
+ });
4064
+ }
4065
+ }
4066
+ return [...transactions.values()].sort((a, b) => b.timestamp - a.timestamp);
3948
4067
  }
3949
4068
  listUtxos() {
3950
4069
  return this.wallet.listUtxos().map((utxo) => ({
@@ -3958,19 +4077,23 @@ class BeignetNode extends events_1.EventEmitter {
3958
4077
  }
3959
4078
  async freezeUtxo(txid, index) {
3960
4079
  this._validateTxid(txid);
3961
- const res = await this.wallet.freezeUtxo({ txid, index });
3962
- if (res.isErr()) {
3963
- throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, res.error.message);
3964
- }
3965
- return { frozen: `${txid}:${index}` };
4080
+ return this._runOnchainSend(async () => {
4081
+ this.onchainSweeps().assertInputMutable(txid, index);
4082
+ const res = await this.wallet.freezeUtxo({ txid, index });
4083
+ if (res.isErr())
4084
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, res.error.message);
4085
+ return { frozen: `${txid}:${index}` };
4086
+ });
3966
4087
  }
3967
4088
  async unfreezeUtxo(txid, index) {
3968
4089
  this._validateTxid(txid);
3969
- const res = await this.wallet.unfreezeUtxo({ txid, index });
3970
- if (res.isErr()) {
3971
- throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, res.error.message);
3972
- }
3973
- return { unfrozen: `${txid}:${index}` };
4090
+ return this._runOnchainSend(async () => {
4091
+ this.onchainSweeps().assertInputMutable(txid, index);
4092
+ const res = await this.wallet.unfreezeUtxo({ txid, index });
4093
+ if (res.isErr())
4094
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, res.error.message);
4095
+ return { unfrozen: `${txid}:${index}` };
4096
+ });
3974
4097
  }
3975
4098
  async setAddressLabel(address, label) {
3976
4099
  const res = await this.wallet.setAddressLabel(address, label);
@@ -4085,24 +4208,58 @@ class BeignetNode extends events_1.EventEmitter {
4085
4208
  this._lastGraphSyncAt = Date.now();
4086
4209
  return synced;
4087
4210
  }
4088
- async syncRapidGossip() {
4211
+ syncRapidGossip() {
4212
+ if (this._rapidGossipSync)
4213
+ return this._rapidGossipSync;
4214
+ const run = this.runRapidGossipSync();
4215
+ this._rapidGossipSync = run;
4216
+ const clear = () => {
4217
+ if (this._rapidGossipSync === run)
4218
+ this._rapidGossipSync = null;
4219
+ };
4220
+ void run.then(clear, clear);
4221
+ return run;
4222
+ }
4223
+ async runRapidGossipSync() {
4089
4224
  if (this.networkName !== 'mainnet') {
4090
4225
  this.log('warn', 'Rapid gossip sync is only available on mainnet', {});
4091
4226
  return null;
4092
4227
  }
4093
4228
  const url = this.rapidGossipSyncUrl ?? rapid_sync_1.DEFAULT_RGS_URL;
4094
4229
  this.log('info', 'Rapid gossip sync: downloading snapshot', { url });
4230
+ const downloadStart = Date.now();
4095
4231
  const data = await (0, rapid_sync_1.fetchRapidGossipSnapshot)(url);
4096
- const result = this.node.loadRapidGossipSnapshot(data);
4232
+ const downloadMs = Date.now() - downloadStart;
4233
+ if (this.destroyed)
4234
+ throw new rapid_sync_1.RapidGossipCancelledError();
4235
+ let slices = 0;
4236
+ let busyMs = 0;
4237
+ const applyStart = Date.now();
4238
+ const result = await this.node.loadRapidGossipSnapshotAsync(data, {
4239
+ cancelled: () => this.destroyed,
4240
+ onSlice: (ms) => {
4241
+ slices++;
4242
+ busyMs += ms;
4243
+ }
4244
+ });
4245
+ const applyMs = Date.now() - applyStart;
4097
4246
  this._lastGraphSyncAt = Date.now();
4098
4247
  this.log('info', 'Rapid gossip sync complete', {
4099
4248
  channelsAdded: result.channelsAdded,
4100
4249
  updatesApplied: result.updatesApplied,
4101
- nodes: result.nodeCount
4250
+ nodes: result.nodeCount,
4251
+ downloadMs,
4252
+ applyMs,
4253
+ busyMs,
4254
+ slices
4102
4255
  });
4103
4256
  this.emit('gossip:synced', {
4104
4257
  channelsAdded: result.channelsAdded,
4105
- updatesApplied: result.updatesApplied
4258
+ updatesApplied: result.updatesApplied,
4259
+ downloadMs,
4260
+ applyMs,
4261
+ busyMs,
4262
+ slices
4106
4263
  });
4107
4264
  return {
4108
4265
  channelsAdded: result.channelsAdded,
@@ -4182,11 +4339,45 @@ class BeignetNode extends events_1.EventEmitter {
4182
4339
  await Promise.all(openPromises);
4183
4340
  return opened;
4184
4341
  }
4185
- async closeChannel(channelId, acceptStaleStateRisk = false) {
4342
+ async closeQuote(channelId, address, acceptStaleStateRisk = false) {
4186
4343
  const idBuf = requireChannelIdHex(channelId);
4344
+ const script = this._decodeSpliceDestination(address);
4345
+ if (!this.node.getChannel(idBuf))
4346
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.CHANNEL_NOT_FOUND, `Channel not found: ${channelId}`);
4347
+ const destination = script ?? (await this.cooperativeCloseDestination());
4348
+ try {
4349
+ return this.node.closeQuote(idBuf, destination, acceptStaleStateRisk);
4350
+ }
4351
+ catch (error) {
4352
+ throw new errors_1.BeignetError('CLOSE_UNAVAILABLE', error.message);
4353
+ }
4354
+ }
4355
+ async closeChannel(channelId, acceptStaleStateRisk = false, address) {
4356
+ const idBuf = requireChannelIdHex(channelId);
4357
+ const destination = this._decodeSpliceDestination(address);
4187
4358
  if (!this.node.getChannel(idBuf)) {
4188
4359
  throw new errors_1.BeignetError(errors_1.BeignetErrorCode.CHANNEL_NOT_FOUND, `Channel not found: ${idBuf.toString('hex')}`);
4189
4360
  }
4361
+ const scriptPubkey = destination ?? (await this.cooperativeCloseDestination());
4362
+ let externalSpendSats;
4363
+ if (destination) {
4364
+ let quote;
4365
+ try {
4366
+ quote = this.node.closeQuote(idBuf, destination, acceptStaleStateRisk);
4367
+ }
4368
+ catch (error) {
4369
+ throw new errors_1.BeignetError('CLOSE_UNAVAILABLE', error.message);
4370
+ }
4371
+ externalSpendSats = quote.amountSats + quote.feeSats;
4372
+ this._checkMaxPayment(externalSpendSats);
4373
+ this._checkSpendLimit(externalSpendSats);
4374
+ }
4375
+ const result = this.node.closeChannel(idBuf, scriptPubkey, acceptStaleStateRisk, destination !== undefined);
4376
+ if (result.ok && externalSpendSats !== undefined)
4377
+ this._recordSpend(externalSpendSats, 'onchain');
4378
+ return result;
4379
+ }
4380
+ async cooperativeCloseDestination() {
4190
4381
  let scriptPubkey = await this.boundedCurrentWalletAddressScript();
4191
4382
  if (!scriptPubkey)
4192
4383
  scriptPubkey = this.sweepDestinationScript;
@@ -4194,7 +4385,7 @@ class BeignetNode extends events_1.EventEmitter {
4194
4385
  const bitcoin = require('bitcoinjs-lib');
4195
4386
  scriptPubkey = bitcoin.address.toOutputScript(this.node.getFundingAddress(), this.getBitcoinNetwork());
4196
4387
  }
4197
- return this.node.closeChannel(idBuf, scriptPubkey, acceptStaleStateRisk);
4388
+ return scriptPubkey;
4198
4389
  }
4199
4390
  async boundedCurrentWalletAddressScript() {
4200
4391
  return this.raceWithTimeout(this.resolveCurrentWalletAddressScript().catch(() => undefined), this._closeAddressLookupTimeoutMs);
@@ -4814,6 +5005,12 @@ class BeignetNode extends events_1.EventEmitter {
4814
5005
  peerPubkey,
4815
5006
  state: ch.state,
4816
5007
  localBalanceSats: Number(ch.localBalanceMsat / 1000n),
5008
+ maxSendableSats: Number((ch.spendableOutboundMsat ?? 0n) / 1000n),
5009
+ localReserveWaived: ch.localReserveWaived === true,
5010
+ remoteReserveWaived: ch.remoteReserveWaived === true,
5011
+ isOpener: ch.isOpener,
5012
+ localReserveSats: Number((ch.localReserveMsat ?? 0n) / 1000n),
5013
+ remoteReserveSats: Number((ch.remoteReserveMsat ?? 0n) / 1000n),
4817
5014
  remoteBalanceSats: Number(ch.remoteBalanceMsat / 1000n),
4818
5015
  capacitySats: Number(ch.fundingSatoshis),
4819
5016
  isAnchor: (0, types_7.isAnchorChannel)(ch.channelType ?? null)
@@ -5680,7 +5877,7 @@ class BeignetNode extends events_1.EventEmitter {
5680
5877
  this._persistSpendState();
5681
5878
  return count;
5682
5879
  }
5683
- _persistSpendState() {
5880
+ _persistSpendState(strict = false) {
5684
5881
  if (this._dailySpendLimitSats === undefined)
5685
5882
  return;
5686
5883
  const storage = this.storage;
@@ -5702,6 +5899,8 @@ class BeignetNode extends events_1.EventEmitter {
5702
5899
  storage.saveMetadata(exports.DAILY_SPEND_STATE_KEY, JSON.stringify(state));
5703
5900
  }
5704
5901
  catch (err) {
5902
+ if (strict)
5903
+ throw err;
5705
5904
  this.log('warn', 'Could not persist the daily spend ledger', {
5706
5905
  error: err instanceof Error ? err.message : String(err)
5707
5906
  });
@@ -6161,6 +6360,39 @@ class BeignetNode extends events_1.EventEmitter {
6161
6360
  }
6162
6361
  return new errors_1.BeignetError(code, msg);
6163
6362
  }
6363
+ quotePayAll(bolt11, maxFeeMsat) {
6364
+ try {
6365
+ const quote = this.node.quotePayAll(bolt11, requireMsatValue(maxFeeMsat, 'maxFeeMsat'));
6366
+ return {
6367
+ ...quote,
6368
+ debitMsat: quote.debitMsat.toString(),
6369
+ maxFeeMsat: quote.maxFeeMsat.toString(),
6370
+ minRecipientMsat: quote.minRecipientMsat.toString(),
6371
+ remainderMsat: quote.remainderMsat.toString()
6372
+ };
6373
+ }
6374
+ catch (err) {
6375
+ throw this._toBeignetPaymentError(err);
6376
+ }
6377
+ }
6378
+ async payInvoiceAll(bolt11, debitMsat, maxFeeMsat, timeoutMs = 60000) {
6379
+ this._checkDraining();
6380
+ const decoded = decodeInvoiceInput(bolt11);
6381
+ const debit = requireMsatValue(debitMsat, 'debitMsat');
6382
+ const feeCap = requireMsatValue(maxFeeMsat, 'maxFeeMsat');
6383
+ if (debit <= 0n || debit > 0xffffffffffffffffn || feeCap >= debit) {
6384
+ throw new errors_1.BeignetError('INVALID_PARAMS', 'Pay-all requires a positive u64 debitMsat and maxFeeMsat below it');
6385
+ }
6386
+ if (!Number.isSafeInteger(timeoutMs) ||
6387
+ timeoutMs < 1 ||
6388
+ timeoutMs > 2147483647) {
6389
+ throw new errors_1.BeignetError('INVALID_PARAMS', 'timeoutMs must be a positive timer duration');
6390
+ }
6391
+ const claim = this._admitLightningSpend(decoded.paymentHash.toString('hex'), debit, 0n);
6392
+ return this.waitForInvoicePayment(decoded.paymentHash, timeoutMs, claim, () => {
6393
+ this.node.sendPayAll(bolt11, debit, feeCap);
6394
+ });
6395
+ }
6164
6396
  async payInvoice(bolt11, timeoutMs = 60000, maxFeeSats, amountSats, metadata, cltvLimit, maxFeeMsatCap) {
6165
6397
  this._checkDraining();
6166
6398
  const decoded = decodeInvoiceInput(bolt11);
@@ -6173,10 +6405,16 @@ class BeignetNode extends events_1.EventEmitter {
6173
6405
  const maxFeeMsat = resolveMaxFeeMsat(maxFeeSats, maxFeeMsatCap, payAmountMsat);
6174
6406
  const maxCltvExpiryHeight = this._cltvCeiling(cltvLimit);
6175
6407
  const claim = this._admitLightningSpend(paymentHashHex, payAmountMsat, maxFeeMsat);
6408
+ return this.waitForInvoicePayment(decoded.paymentHash, timeoutMs, claim, () => {
6409
+ this.node.sendPayment(bolt11, undefined, maxFeeMsat, amountMsat, maxCltvExpiryHeight, undefined, metadata);
6410
+ });
6411
+ }
6412
+ waitForInvoicePayment(paymentHash, timeoutMs, claim, dispatch) {
6413
+ const paymentHashHex = paymentHash.toString('hex');
6176
6414
  return new Promise((resolve, reject) => {
6177
6415
  const timer = setTimeout(() => {
6178
6416
  cleanup();
6179
- reject(this._paymentTimeout(decoded.paymentHash, 'Payment', timeoutMs, claim));
6417
+ reject(this._paymentTimeout(paymentHash, 'Payment', timeoutMs, claim));
6180
6418
  }, timeoutMs);
6181
6419
  const cleanup = () => {
6182
6420
  clearTimeout(timer);
@@ -6200,13 +6438,19 @@ class BeignetNode extends events_1.EventEmitter {
6200
6438
  };
6201
6439
  this.node.on('payment:sent', onSent);
6202
6440
  this.node.on('payment:failed', onFailed);
6441
+ const beforeDispatch = this.node.getPayment(paymentHash);
6203
6442
  try {
6204
- this.node.sendPayment(bolt11, undefined, maxFeeMsat, amountMsat, maxCltvExpiryHeight, undefined, metadata);
6443
+ dispatch();
6205
6444
  }
6206
6445
  catch (err) {
6207
6446
  cleanup();
6208
- if (claim)
6447
+ const dispatched = this.node.getPayment(paymentHash) !== beforeDispatch;
6448
+ if (dispatched && this.node.hasHtlcInFlight(paymentHash)) {
6449
+ this.node.failPaymentUnlessInFlight(paymentHash);
6450
+ }
6451
+ else if (claim) {
6209
6452
  this._closeAsyncSpendClaim(paymentHashHex, claim);
6453
+ }
6210
6454
  reject(this._toBeignetPaymentError(err));
6211
6455
  }
6212
6456
  });
@@ -6621,6 +6865,7 @@ class BeignetNode extends events_1.EventEmitter {
6621
6865
  return null;
6622
6866
  const feeMsat = paymentFeeMsat(record);
6623
6867
  return {
6868
+ ...(record.payAll ? { payAll: this.toPaymentInfo(record).payAll } : {}),
6624
6869
  paymentHash: proof.paymentHash.toString('hex'),
6625
6870
  preimage: proof.preimage.toString('hex'),
6626
6871
  amountSats: paymentAmountSats(record),
@@ -6711,6 +6956,17 @@ class BeignetNode extends events_1.EventEmitter {
6711
6956
  }
6712
6957
  toPaymentInfo(p) {
6713
6958
  const info = {
6959
+ ...(p.payAll
6960
+ ? {
6961
+ payAll: {
6962
+ debitMsat: p.payAll.debitMsat.toString(),
6963
+ maxFeeMsat: p.payAll.maxFeeMsat.toString(),
6964
+ deliveredMsat: p.payAll.deliveredMsat.toString(),
6965
+ feeMsat: p.payAll.feeMsat.toString(),
6966
+ remainderMsat: p.payAll.remainderMsat.toString()
6967
+ }
6968
+ }
6969
+ : {}),
6714
6970
  paymentHash: p.paymentHash.toString('hex'),
6715
6971
  amountSats: paymentAmountSats(p),
6716
6972
  status: p.status,
@@ -6853,10 +7109,10 @@ class BeignetNode extends events_1.EventEmitter {
6853
7109
  isAnchor: (0, types_7.isAnchorChannel)(state.channelType)
6854
7110
  };
6855
7111
  }
6856
- spliceQuote(channelId, direction, feeratePerkw) {
7112
+ spliceQuote(channelId, direction, feeratePerkw, destinationAddress) {
6857
7113
  const idBuf = requireChannelIdHex(channelId);
6858
7114
  requireU32(feeratePerkw, 'feeratePerkw', 1, splice_weight_1.MAX_SPLICE_FEERATE_PERKW);
6859
- return fundingOrRefuse(() => this.node.spliceQuote(idBuf, direction, feeratePerkw));
7115
+ return fundingOrRefuse(() => this.node.spliceQuote(idBuf, direction, feeratePerkw, this._decodeSpliceDestination(destinationAddress)));
6860
7116
  }
6861
7117
  spliceIn(channelId, amountSats, feeratePerkw, fundingUtxos) {
6862
7118
  const idBuf = requireChannelIdHex(channelId);
@@ -6865,10 +7121,7 @@ class BeignetNode extends events_1.EventEmitter {
6865
7121
  const result = fundingOrRefuse(() => this.node.spliceIn(idBuf, BigInt(amountSats), feeratePerkw, fundingUtxos));
6866
7122
  return result;
6867
7123
  }
6868
- spliceOut(channelId, amountSats, feeratePerkw, destinationAddress) {
6869
- const idBuf = requireChannelIdHex(channelId);
6870
- requirePositiveSafeInteger(amountSats, 'amountSats');
6871
- requireU32(feeratePerkw, 'feeratePerkw', 1, splice_weight_1.MAX_SPLICE_FEERATE_PERKW);
7124
+ _decodeSpliceDestination(destinationAddress) {
6872
7125
  let destinationScript;
6873
7126
  if (destinationAddress !== undefined) {
6874
7127
  if (typeof destinationAddress !== 'string' ||
@@ -6883,6 +7136,13 @@ class BeignetNode extends events_1.EventEmitter {
6883
7136
  throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, `destinationAddress is not a valid address for this network: ${err instanceof Error ? err.message : 'failed to decode'}`);
6884
7137
  }
6885
7138
  }
7139
+ return destinationScript;
7140
+ }
7141
+ spliceOut(channelId, amountSats, feeratePerkw, destinationAddress) {
7142
+ const idBuf = requireChannelIdHex(channelId);
7143
+ requirePositiveSafeInteger(amountSats, 'amountSats');
7144
+ requireU32(feeratePerkw, 'feeratePerkw', 1, splice_weight_1.MAX_SPLICE_FEERATE_PERKW);
7145
+ const destinationScript = this._decodeSpliceDestination(destinationAddress);
6886
7146
  let externalSpendSats;
6887
7147
  if (destinationScript !== undefined) {
6888
7148
  const feeSats = Number((0, splice_weight_1.spliceFeeSats)((0, splice_weight_1.estimateSpliceTxWeight)({
@@ -7327,7 +7587,9 @@ class BeignetNode extends events_1.EventEmitter {
7327
7587
  const snapshot = this.node.getLiquiditySnapshot();
7328
7588
  let reserveMsat = 0n;
7329
7589
  let sendableMsat = 0n;
7590
+ let maxSendableMsat = 0n;
7330
7591
  for (const ch of this.node.listChannels()) {
7592
+ maxSendableMsat += ch.spendableOutboundMsat ?? 0n;
7331
7593
  if (isHeldRestore(ch))
7332
7594
  continue;
7333
7595
  if (ch.state !== types_7.ChannelState.NORMAL && !ch.htlcUsable)
@@ -7351,6 +7613,7 @@ class BeignetNode extends events_1.EventEmitter {
7351
7613
  inboundLiquidityPct: snapshot.inboundLiquidityPct,
7352
7614
  reserveSats: Number(reserveMsat / 1000n),
7353
7615
  sendableSats: Number(sendableMsat / 1000n),
7616
+ maxSendableSats: Number(maxSendableMsat / 1000n),
7354
7617
  recommendations: snapshot.recommendations
7355
7618
  };
7356
7619
  }
@@ -7986,7 +8249,7 @@ class BeignetNode extends events_1.EventEmitter {
7986
8249
  }
7987
8250
  async backup(destPath) {
7988
8251
  await this.storage.backup(destPath);
7989
- await (0, backup_mac_1.writeBackupMac)((0, backup_mac_1.deriveBackupMacKey)(bip39.mnemonicToSeedSync(this.mnemonic)), destPath);
8252
+ await (0, backup_mac_1.writeBackupMac)((0, backup_mac_1.deriveBackupMacKey)(this.walletSeed()), destPath);
7990
8253
  }
7991
8254
  storageFiles() {
7992
8255
  const dbPath = fs.realpathSync.native(path.join(this.dataDir, `${this.networkName}.db`));
@@ -8037,14 +8300,14 @@ class BeignetNode extends events_1.EventEmitter {
8037
8300
  createdAt: Date.now(),
8038
8301
  channels: data.channels
8039
8302
  };
8040
- const seed = bip39.mnemonicToSeedSync(this.mnemonic);
8303
+ const seed = this.walletSeed();
8041
8304
  const encoded = (0, scb_1.encodeScb)(backup, seed);
8042
8305
  const scbPath = path.join(this.dataDir, 'channels.scb');
8043
8306
  (0, fs_utils_1.writeFileAtomic)(scbPath, encoded);
8044
8307
  return { encoded, channelCount: data.channels.length, path: scbPath };
8045
8308
  }
8046
8309
  async restoreFromScb(encoded) {
8047
- const seed = bip39.mnemonicToSeedSync(this.mnemonic);
8310
+ const seed = this.walletSeed();
8048
8311
  const backup = (0, scb_1.decodeScb)(encoded.trim(), seed);
8049
8312
  const expectedNetwork = this.toLnNetwork(this.networkName);
8050
8313
  if (backup.network !== expectedNetwork) {
@@ -8165,14 +8428,14 @@ class BeignetNode extends events_1.EventEmitter {
8165
8428
  });
8166
8429
  }
8167
8430
  }
8168
- const seed = bip39.mnemonicToSeedSync(this.mnemonic);
8431
+ const seed = this.walletSeed();
8169
8432
  this.offerRetrievedScb((0, scb_1.encodeScb)(embedded, seed), embedded, peerPubkey, 'capsule');
8170
8433
  return;
8171
8434
  }
8172
8435
  let backup;
8173
8436
  const encoded = blob.toString('utf8');
8174
8437
  try {
8175
- const seed = bip39.mnemonicToSeedSync(this.mnemonic);
8438
+ const seed = this.walletSeed();
8176
8439
  backup = (0, scb_1.decodeScb)(encoded, seed);
8177
8440
  }
8178
8441
  catch {