beignet 0.2.0 → 0.3.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 (195) hide show
  1. package/README.md +284 -1
  2. package/dist/cli/beignet-node.js +1019 -47
  3. package/dist/cli/beignet-node.js.map +1 -1
  4. package/dist/cli/cli.js +1201 -16
  5. package/dist/cli/cli.js.map +1 -1
  6. package/dist/cli/config.js +21 -1
  7. package/dist/cli/config.js.map +1 -1
  8. package/dist/cli/daemon.js +342 -16
  9. package/dist/cli/daemon.js.map +1 -1
  10. package/dist/cli/errors.js +2 -0
  11. package/dist/cli/errors.js.map +1 -1
  12. package/dist/cli/openapi.js +1207 -14
  13. package/dist/cli/openapi.js.map +1 -1
  14. package/dist/cli/restore.js +89 -0
  15. package/dist/cli/restore.js.map +1 -0
  16. package/dist/cli/wallet-storage.js +34 -0
  17. package/dist/cli/wallet-storage.js.map +1 -0
  18. package/dist/electrum/index.js +105 -10
  19. package/dist/electrum/index.js.map +1 -1
  20. package/dist/lightning/advisor/fee-tuner.js +42 -0
  21. package/dist/lightning/advisor/fee-tuner.js.map +1 -0
  22. package/dist/lightning/advisor/index.js +9 -1
  23. package/dist/lightning/advisor/index.js.map +1 -1
  24. package/dist/lightning/advisor/rebalance-planner.js +55 -0
  25. package/dist/lightning/advisor/rebalance-planner.js.map +1 -0
  26. package/dist/lightning/backup/index.js +18 -0
  27. package/dist/lightning/backup/index.js.map +1 -0
  28. package/dist/lightning/backup/scb.js +42 -0
  29. package/dist/lightning/backup/scb.js.map +1 -0
  30. package/dist/lightning/chain/chain-monitor.js +27 -0
  31. package/dist/lightning/chain/chain-monitor.js.map +1 -1
  32. package/dist/lightning/chain/output-resolver.js +55 -6
  33. package/dist/lightning/chain/output-resolver.js.map +1 -1
  34. package/dist/lightning/chain/types.js +1 -0
  35. package/dist/lightning/chain/types.js.map +1 -1
  36. package/dist/lightning/channel/channel-manager.js +64 -0
  37. package/dist/lightning/channel/channel-manager.js.map +1 -1
  38. package/dist/lightning/channel/channel-state.js.map +1 -1
  39. package/dist/lightning/channel/channel.js +124 -4
  40. package/dist/lightning/channel/channel.js.map +1 -1
  41. package/dist/lightning/channel/dual-funding.js +6 -5
  42. package/dist/lightning/channel/dual-funding.js.map +1 -1
  43. package/dist/lightning/channel/types.js +3 -1
  44. package/dist/lightning/channel/types.js.map +1 -1
  45. package/dist/lightning/channel/validation.js +3 -3
  46. package/dist/lightning/channel/validation.js.map +1 -1
  47. package/dist/lightning/crypto/index.js +1 -0
  48. package/dist/lightning/crypto/index.js.map +1 -1
  49. package/dist/lightning/crypto/message-signing.js +116 -0
  50. package/dist/lightning/crypto/message-signing.js.map +1 -0
  51. package/dist/lightning/crypto/xchacha20poly1305.js +77 -0
  52. package/dist/lightning/crypto/xchacha20poly1305.js.map +1 -0
  53. package/dist/lightning/features/flags.js +1 -0
  54. package/dist/lightning/features/flags.js.map +1 -1
  55. package/dist/lightning/gossip/messages.js +37 -1
  56. package/dist/lightning/gossip/messages.js.map +1 -1
  57. package/dist/lightning/index.js +2 -1
  58. package/dist/lightning/index.js.map +1 -1
  59. package/dist/lightning/keys/wallet-keys.js +1 -0
  60. package/dist/lightning/keys/wallet-keys.js.map +1 -1
  61. package/dist/lightning/message/index.js +1 -0
  62. package/dist/lightning/message/index.js.map +1 -1
  63. package/dist/lightning/message/peer-storage.js +43 -0
  64. package/dist/lightning/message/peer-storage.js.map +1 -0
  65. package/dist/lightning/message/types.js +2 -0
  66. package/dist/lightning/message/types.js.map +1 -1
  67. package/dist/lightning/node/lightning-node.js +1583 -235
  68. package/dist/lightning/node/lightning-node.js.map +1 -1
  69. package/dist/lightning/node/types.js +18 -1
  70. package/dist/lightning/node/types.js.map +1 -1
  71. package/dist/lightning/offer/offer-manager.js +2 -5
  72. package/dist/lightning/offer/offer-manager.js.map +1 -1
  73. package/dist/lightning/offer/tlv.js +3 -1
  74. package/dist/lightning/offer/tlv.js.map +1 -1
  75. package/dist/lightning/storage/encryption.js +67 -0
  76. package/dist/lightning/storage/encryption.js.map +1 -0
  77. package/dist/lightning/storage/index.js +1 -0
  78. package/dist/lightning/storage/index.js.map +1 -1
  79. package/dist/lightning/storage/serialization.js +6 -2
  80. package/dist/lightning/storage/serialization.js.map +1 -1
  81. package/dist/lightning/storage/sqlite-storage.js +387 -33
  82. package/dist/lightning/storage/sqlite-storage.js.map +1 -1
  83. package/dist/lightning/watchtower/blob.js +135 -0
  84. package/dist/lightning/watchtower/blob.js.map +1 -0
  85. package/dist/lightning/watchtower/index.js +55 -0
  86. package/dist/lightning/watchtower/index.js.map +1 -0
  87. package/dist/lightning/watchtower/justice.js +182 -0
  88. package/dist/lightning/watchtower/justice.js.map +1 -0
  89. package/dist/lightning/watchtower/tower-connection.js +208 -0
  90. package/dist/lightning/watchtower/tower-connection.js.map +1 -0
  91. package/dist/lightning/watchtower/types.js +3 -0
  92. package/dist/lightning/watchtower/types.js.map +1 -0
  93. package/dist/lightning/watchtower/watchtower-client.js +402 -0
  94. package/dist/lightning/watchtower/watchtower-client.js.map +1 -0
  95. package/dist/lightning/watchtower/wtwire.js +201 -0
  96. package/dist/lightning/watchtower/wtwire.js.map +1 -0
  97. package/dist/shapes/electrum.js +32 -1
  98. package/dist/shapes/electrum.js.map +1 -1
  99. package/dist/shapes/wallet.js +11 -1
  100. package/dist/shapes/wallet.js.map +1 -1
  101. package/dist/transaction/index.js +163 -15
  102. package/dist/transaction/index.js.map +1 -1
  103. package/dist/types/cli/beignet-node.d.ts +144 -3
  104. package/dist/types/cli/daemon.d.ts +2 -0
  105. package/dist/types/cli/errors.d.ts +2 -0
  106. package/dist/types/cli/restore.d.ts +10 -0
  107. package/dist/types/cli/types.d.ts +228 -1
  108. package/dist/types/cli/wallet-storage.d.ts +3 -0
  109. package/dist/types/electrum/index.d.ts +12 -0
  110. package/dist/types/electrum.js +1 -0
  111. package/dist/types/electrum.js.map +1 -1
  112. package/dist/types/lightning/advisor/fee-tuner.d.ts +20 -0
  113. package/dist/types/lightning/advisor/index.d.ts +4 -0
  114. package/dist/types/lightning/advisor/rebalance-planner.d.ts +12 -0
  115. package/dist/types/lightning/backup/index.d.ts +1 -0
  116. package/dist/types/lightning/backup/scb.d.ts +24 -0
  117. package/dist/types/lightning/chain/chain-monitor.d.ts +1 -0
  118. package/dist/types/lightning/chain/types.d.ts +1 -0
  119. package/dist/types/lightning/channel/channel-manager.d.ts +7 -0
  120. package/dist/types/lightning/channel/channel-state.d.ts +2 -0
  121. package/dist/types/lightning/channel/channel.d.ts +6 -0
  122. package/dist/types/lightning/channel/dual-funding.d.ts +2 -1
  123. package/dist/types/lightning/channel/types.d.ts +2 -0
  124. package/dist/types/lightning/channel/validation.d.ts +1 -1
  125. package/dist/types/lightning/crypto/index.d.ts +1 -0
  126. package/dist/types/lightning/crypto/message-signing.d.ts +9 -0
  127. package/dist/types/lightning/crypto/xchacha20poly1305.d.ts +3 -0
  128. package/dist/types/lightning/features/flags.d.ts +1 -0
  129. package/dist/types/lightning/gossip/messages.d.ts +4 -0
  130. package/dist/types/lightning/index.d.ts +1 -0
  131. package/dist/types/lightning/keys/wallet-keys.d.ts +2 -1
  132. package/dist/types/lightning/message/index.d.ts +1 -0
  133. package/dist/types/lightning/message/peer-storage.d.ts +9 -0
  134. package/dist/types/lightning/message/types.d.ts +2 -0
  135. package/dist/types/lightning/node/lightning-node.d.ts +116 -4
  136. package/dist/types/lightning/node/types.d.ts +62 -1
  137. package/dist/types/lightning/storage/encryption.d.ts +11 -0
  138. package/dist/types/lightning/storage/index.d.ts +1 -0
  139. package/dist/types/lightning/storage/serialization.d.ts +2 -0
  140. package/dist/types/lightning/storage/sqlite-storage.d.ts +43 -3
  141. package/dist/types/lightning/storage/types.d.ts +62 -0
  142. package/dist/types/lightning/watchtower/blob.d.ts +32 -0
  143. package/dist/types/lightning/watchtower/index.d.ts +9 -0
  144. package/dist/types/lightning/watchtower/justice.d.ts +29 -0
  145. package/dist/types/lightning/watchtower/tower-connection.d.ts +34 -0
  146. package/dist/types/lightning/watchtower/types.d.ts +44 -0
  147. package/dist/types/lightning/watchtower/watchtower-client.d.ts +67 -0
  148. package/dist/types/lightning/watchtower/wtwire.d.ts +90 -0
  149. package/dist/types/shapes/electrum.d.ts +3 -1
  150. package/dist/types/transaction/index.d.ts +6 -0
  151. package/dist/types/transaction.js.map +1 -1
  152. package/dist/types/types/electrum.d.ts +3 -2
  153. package/dist/types/types/transaction.d.ts +1 -1
  154. package/dist/types/types/wallet.d.ts +78 -4
  155. package/dist/types/utils/descriptors.d.ts +2 -0
  156. package/dist/types/utils/fees.d.ts +3 -0
  157. package/dist/types/utils/helpers.d.ts +3 -1
  158. package/dist/types/utils/index.d.ts +5 -0
  159. package/dist/types/utils/multisig.d.ts +29 -0
  160. package/dist/types/utils/transaction.d.ts +7 -0
  161. package/dist/types/utils/wallet-storage-encryption.d.ts +3 -0
  162. package/dist/types/utils/wallet.d.ts +1 -1
  163. package/dist/types/utils/watch-only.d.ts +12 -0
  164. package/dist/types/wallet/constants.d.ts +1 -0
  165. package/dist/types/wallet/index.d.ts +61 -4
  166. package/dist/types/wallet.js +3 -0
  167. package/dist/types/wallet.js.map +1 -1
  168. package/dist/utils/derivation-path.js +8 -7
  169. package/dist/utils/derivation-path.js.map +1 -1
  170. package/dist/utils/descriptors.js +56 -0
  171. package/dist/utils/descriptors.js.map +1 -0
  172. package/dist/utils/electrum.js +10 -2
  173. package/dist/utils/electrum.js.map +1 -1
  174. package/dist/utils/fees.js +19 -0
  175. package/dist/utils/fees.js.map +1 -0
  176. package/dist/utils/helpers.js +22 -5
  177. package/dist/utils/helpers.js.map +1 -1
  178. package/dist/utils/index.js +5 -0
  179. package/dist/utils/index.js.map +1 -1
  180. package/dist/utils/multisig.js +90 -0
  181. package/dist/utils/multisig.js.map +1 -0
  182. package/dist/utils/transaction.js +30 -3
  183. package/dist/utils/transaction.js.map +1 -1
  184. package/dist/utils/wallet-storage-encryption.js +45 -0
  185. package/dist/utils/wallet-storage-encryption.js.map +1 -0
  186. package/dist/utils/wallet.js +3 -1
  187. package/dist/utils/wallet.js.map +1 -1
  188. package/dist/utils/watch-only.js +102 -0
  189. package/dist/utils/watch-only.js.map +1 -0
  190. package/dist/wallet/constants.js +2 -1
  191. package/dist/wallet/constants.js.map +1 -1
  192. package/dist/wallet/index.js +787 -30
  193. package/dist/wallet/index.js.map +1 -1
  194. package/docs/ROADMAP.md +292 -0
  195. 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.defaultDataDirForMnemonic = void 0;
26
+ exports.BeignetNode = exports.jsonToRouteHops = exports.routeHopsToJson = exports.parseScid = exports.formatScid = exports.defaultDataDirForMnemonic = void 0;
27
27
  const path = __importStar(require("path"));
28
28
  const fs = __importStar(require("fs"));
29
29
  const net = __importStar(require("net"));
@@ -36,16 +36,21 @@ const wallet_1 = require("../wallet");
36
36
  const helpers_1 = require("../utils/helpers");
37
37
  const conversion_1 = require("../utils/conversion");
38
38
  const wallet_2 = require("../types/wallet");
39
+ const wallet_storage_1 = require("./wallet-storage");
39
40
  const electrum_1 = require("../types/electrum");
40
41
  const lightning_node_1 = require("../lightning/node/lightning-node");
41
42
  const wallet_funding_provider_1 = require("../lightning/wallet/wallet-funding-provider");
42
43
  const sqlite_storage_1 = require("../lightning/storage/sqlite-storage");
44
+ const encryption_1 = require("../lightning/storage/encryption");
45
+ const scb_1 = require("../lightning/backup/scb");
46
+ const bip39 = __importStar(require("bip39"));
43
47
  const rapid_sync_1 = require("../lightning/gossip/rapid-sync");
44
48
  const messages_1 = require("../lightning/gossip/messages");
49
+ const types_1 = require("../lightning/gossip/types");
45
50
  const electrum_backend_1 = require("../lightning/chain/electrum-backend");
46
- const types_1 = require("../lightning/invoice/types");
51
+ const types_2 = require("../lightning/invoice/types");
47
52
  const wallet_keys_1 = require("../lightning/keys/wallet-keys");
48
- const types_2 = require("../lightning/channel/types");
53
+ const types_3 = require("../lightning/channel/types");
49
54
  const decode_1 = require("../lightning/invoice/decode");
50
55
  const decode_2 = require("../lightning/offer/decode");
51
56
  const errors_1 = require("./errors");
@@ -60,9 +65,73 @@ function defaultDataDirForMnemonic(mnemonic, baseDir = DEFAULT_DATA_DIR) {
60
65
  return path.join(baseDir, walletTag);
61
66
  }
62
67
  exports.defaultDataDirForMnemonic = defaultDataDirForMnemonic;
68
+ function formatScid(scid) {
69
+ const { block, txIndex, outputIndex } = (0, types_1.decodeShortChannelId)(scid);
70
+ return `${block}x${txIndex}x${outputIndex}`;
71
+ }
72
+ exports.formatScid = formatScid;
73
+ function parseScid(scid) {
74
+ const human = scid.match(/^(\d+)x(\d+)x(\d+)$/);
75
+ if (human) {
76
+ return (0, types_1.encodeShortChannelId)({
77
+ block: parseInt(human[1], 10),
78
+ txIndex: parseInt(human[2], 10),
79
+ outputIndex: parseInt(human[3], 10)
80
+ });
81
+ }
82
+ if (/^[0-9a-fA-F]{16}$/.test(scid)) {
83
+ return Buffer.from(scid, 'hex');
84
+ }
85
+ 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)`);
86
+ }
87
+ exports.parseScid = parseScid;
88
+ function routeHopsToJson(route) {
89
+ return route.hops.map((hop, idx) => {
90
+ const feeMsat = idx < route.hops.length - 1
91
+ ? hop.amountToForwardMsat - route.hops[idx + 1].amountToForwardMsat
92
+ : 0n;
93
+ return {
94
+ pubkey: hop.pubkey.toString('hex'),
95
+ shortChannelId: formatScid(hop.shortChannelId),
96
+ amountToForwardMsat: hop.amountToForwardMsat.toString(),
97
+ outgoingCltvValue: hop.outgoingCltvValue,
98
+ feeMsat: feeMsat.toString(),
99
+ cltvExpiryDelta: hop.cltvExpiryDelta
100
+ };
101
+ });
102
+ }
103
+ exports.routeHopsToJson = routeHopsToJson;
104
+ function jsonToRouteHops(hops) {
105
+ return hops.map((hop, idx) => {
106
+ if (!hop.pubkey || !/^[0-9a-fA-F]{66}$/.test(hop.pubkey)) {
107
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, `Route hop ${idx}: pubkey must be a 33-byte hex string`);
108
+ }
109
+ let amountToForwardMsat;
110
+ try {
111
+ amountToForwardMsat = BigInt(hop.amountToForwardMsat);
112
+ }
113
+ catch {
114
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, `Route hop ${idx}: amountToForwardMsat must be a decimal string`);
115
+ }
116
+ if (typeof hop.outgoingCltvValue !== 'number' ||
117
+ !Number.isInteger(hop.outgoingCltvValue) ||
118
+ hop.outgoingCltvValue < 0) {
119
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, `Route hop ${idx}: outgoingCltvValue must be a non-negative integer`);
120
+ }
121
+ return {
122
+ pubkey: Buffer.from(hop.pubkey, 'hex'),
123
+ shortChannelId: parseScid(hop.shortChannelId),
124
+ amountToForwardMsat,
125
+ outgoingCltvValue: hop.outgoingCltvValue
126
+ };
127
+ });
128
+ }
129
+ exports.jsonToRouteHops = jsonToRouteHops;
130
+ const GRAPH_DESCRIBE_MAX_LIMIT = 500;
63
131
  const DEFAULT_ELECTRUM = {
64
132
  mainnet: { host: 'fulcrum.bitkit.blocktank.to', port: 8900, useTls: true },
65
133
  testnet: { host: 'electrum.blockstream.info', port: 60002, useTls: true },
134
+ signet: { host: 'mempool.space', port: 60602, useTls: true },
66
135
  regtest: { host: '34.65.252.32', port: 18483, useTls: false }
67
136
  };
68
137
  async function resolveHostToIPv4(host) {
@@ -106,9 +175,13 @@ class BeignetNode extends events_1.EventEmitter {
106
175
  this._failoverInProgress = false;
107
176
  this._connectTimeoutMs = 15000;
108
177
  this._dailySpentSats = 0;
178
+ this._dailySpentLightningSats = 0;
179
+ this._dailySpentOnchainSats = 0;
109
180
  this._dailySpendResetTime = 0;
110
181
  this._pendingSpendSats = 0;
111
182
  this._draining = false;
183
+ this.peerStorageEnabled = true;
184
+ this._peerRetrievedScb = null;
112
185
  this.mnemonic = mnemonic;
113
186
  this.networkName = networkName;
114
187
  this.dataDir = dataDir;
@@ -132,6 +205,7 @@ class BeignetNode extends events_1.EventEmitter {
132
205
  if (opts.logLevel)
133
206
  this.logLevel = opts.logLevel;
134
207
  this.autoGossipSync = opts.autoGossipSync ?? true;
208
+ this.peerStorageEnabled = opts.peerStorageEnabled ?? true;
135
209
  this.rapidGossipSync = opts.rapidGossipSync ?? true;
136
210
  this.rapidGossipSyncUrl = opts.rapidGossipSyncUrl;
137
211
  const networkName = this.networkName;
@@ -149,7 +223,11 @@ class BeignetNode extends events_1.EventEmitter {
149
223
  const beignetNetwork = this.toBeignetNetwork(networkName);
150
224
  const lnNetwork = this.toLnNetwork(networkName);
151
225
  const coinType = this.toCoinType(networkName);
152
- const chainHash = networkName === 'regtest' ? types_2.REGTEST_CHAIN_HASH : types_2.BITCOIN_CHAIN_HASH;
226
+ let chainHash = types_3.BITCOIN_CHAIN_HASH;
227
+ if (networkName === 'regtest')
228
+ chainHash = types_3.REGTEST_CHAIN_HASH;
229
+ if (networkName === 'signet')
230
+ chainHash = types_3.SIGNET_CHAIN_HASH;
153
231
  if (!opts.allowMultipleInstances) {
154
232
  const lockPath = path.join(this.dataDir, `${networkName}.lock`);
155
233
  try {
@@ -174,11 +252,15 @@ class BeignetNode extends events_1.EventEmitter {
174
252
  `If it held this wallet's channels, re-run with dataDir set to "${DEFAULT_DATA_DIR}" to use it.`);
175
253
  }
176
254
  }
255
+ let encryptionKey;
256
+ if (opts.storageEncryption ?? true) {
257
+ encryptionKey = (0, encryption_1.deriveStorageKey)(bip39.mnemonicToSeedSync(this.mnemonic));
258
+ }
177
259
  this.storage = new sqlite_storage_1.SqliteStorage(dbPath, (err) => {
178
260
  this.log('warn', 'Skipped corrupted storage row during load', {
179
261
  error: err instanceof Error ? err.message : String(err)
180
262
  });
181
- });
263
+ }, encryptionKey ? { encryptionKey } : undefined);
182
264
  this.storage.open();
183
265
  const electrumServer = {
184
266
  host: electrumHost,
@@ -189,12 +271,17 @@ class BeignetNode extends events_1.EventEmitter {
189
271
  const walletResult = await wallet_1.Wallet.create({
190
272
  mnemonic: this.mnemonic,
191
273
  network: beignetNetwork,
274
+ ...(opts.feeEstimationSource
275
+ ? { feeEstimationSource: opts.feeEstimationSource }
276
+ : {}),
192
277
  electrumOptions: {
193
278
  net,
194
279
  tls,
195
280
  servers: electrumServer
196
281
  },
197
- disableMessagesOnCreate: true
282
+ disableMessagesOnCreate: true,
283
+ rbf: true,
284
+ storage: (0, wallet_storage_1.createWalletStorage)(this.storage)
198
285
  });
199
286
  if (walletResult.isErr()) {
200
287
  throw new errors_1.BeignetError('WALLET_CREATE_FAILED', walletResult.error.message);
@@ -293,10 +380,15 @@ class BeignetNode extends events_1.EventEmitter {
293
380
  announcedAddresses,
294
381
  fundingProvider,
295
382
  preferAnchors: opts.preferAnchors,
383
+ largeChannels: opts.largeChannels,
296
384
  chainBackend: electrumBackend,
297
385
  feeEstimator: electrumBackend,
298
386
  sweepDestinationScript,
299
- socks5Proxy
387
+ socks5Proxy,
388
+ peerStorageEnabled: this.peerStorageEnabled,
389
+ autoRebalance: opts.autoRebalance,
390
+ autoTuneFees: opts.autoTuneFees,
391
+ watchtowers: opts.watchtowers
300
392
  });
301
393
  if (!sweepDestinationScript) {
302
394
  this.scheduleSweepAddressRefresh();
@@ -344,13 +436,79 @@ class BeignetNode extends events_1.EventEmitter {
344
436
  this.node.on('channel:ready', (data) => {
345
437
  const channelId = data.channelId.toString('hex');
346
438
  this.log('info', 'Channel ready', { channelId });
439
+ this.refreshStaticChannelBackup();
347
440
  this.emit('channel:ready', { channelId });
348
441
  });
349
442
  this.node.on('channel:closed', (data) => {
350
443
  const channelId = data.channelId.toString('hex');
351
444
  this.log('info', 'Channel closed', { channelId });
445
+ this.refreshStaticChannelBackup();
352
446
  this.emit('channel:closed', { channelId });
353
447
  });
448
+ this.node.on('channel:resolved', () => {
449
+ this.refreshStaticChannelBackup();
450
+ });
451
+ this.node.on('channel:opening', (data) => {
452
+ const channelId = data.channelId.toString('hex');
453
+ const fundingTxid = data.fundingTxid.toString('hex');
454
+ this.log('info', 'Channel opening', { channelId, fundingTxid });
455
+ this.emit('channel:opening', { channelId, fundingTxid });
456
+ });
457
+ this.node.on('channel:pending-close', (data) => {
458
+ const channelId = data.channelId.toString('hex');
459
+ this.log('info', 'Channel pending close', {
460
+ channelId,
461
+ initiator: data.initiator
462
+ });
463
+ this.emit('channel:pending-close', {
464
+ channelId,
465
+ initiator: data.initiator
466
+ });
467
+ });
468
+ this.node.on('channel:force-closing', (data) => {
469
+ const channelId = data.channelId.toString('hex');
470
+ this.log('warn', 'Channel force-closing', {
471
+ channelId,
472
+ initiator: data.initiator
473
+ });
474
+ this.emit('channel:force-closing', {
475
+ channelId,
476
+ initiator: data.initiator
477
+ });
478
+ });
479
+ this.node.on('invoice:settled', (data) => {
480
+ const info = {
481
+ paymentHash: data.paymentHash.toString('hex'),
482
+ bolt11: data.bolt11,
483
+ amountSats: Number(data.amountMsat / 1000n)
484
+ };
485
+ this.log('info', 'Invoice settled', {
486
+ paymentHash: info.paymentHash,
487
+ amountSats: info.amountSats
488
+ });
489
+ this.emit('invoice:settled', info);
490
+ });
491
+ this.node.on('htlc:forwarded', (data) => {
492
+ this.emit('htlc:forwarded', {
493
+ inChannelId: data.inChannelId.toString('hex'),
494
+ outChannelId: data.outChannelId.toString('hex'),
495
+ amountInMsat: data.amountInMsat.toString(),
496
+ amountOutMsat: data.amountOutMsat.toString(),
497
+ feeMsat: data.feeMsat.toString()
498
+ });
499
+ });
500
+ this.node.on('htlc:fulfilled', (data) => {
501
+ this.emit('htlc:fulfilled', {
502
+ channelId: data.channelId.toString('hex'),
503
+ htlcId: data.htlcId.toString()
504
+ });
505
+ });
506
+ this.node.on('htlc:failed', (data) => {
507
+ this.emit('htlc:failed', {
508
+ channelId: data.channelId.toString('hex'),
509
+ htlcId: data.htlcId.toString()
510
+ });
511
+ });
354
512
  this.node.on('peer:connect', (pubkey) => {
355
513
  this.log('debug', 'Peer connected', { pubkey });
356
514
  if (this.autoGossipSync) {
@@ -378,6 +536,10 @@ class BeignetNode extends events_1.EventEmitter {
378
536
  this.log('info', 'Node ready');
379
537
  this.emit('node:ready');
380
538
  });
539
+ this.node.on('peer_storage:retrieved', (peerPubkey, blob) => this.handleRetrievedPeerStorage(peerPubkey, blob));
540
+ if (this.peerStorageEnabled) {
541
+ this.refreshStaticChannelBackup();
542
+ }
381
543
  try {
382
544
  await electrumBackend.estimateFee(6);
383
545
  }
@@ -489,6 +651,21 @@ class BeignetNode extends events_1.EventEmitter {
489
651
  getMnemonic() {
490
652
  return this.mnemonic;
491
653
  }
654
+ signMessage(message) {
655
+ if (typeof message !== 'string' || message.length === 0) {
656
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'message required');
657
+ }
658
+ return {
659
+ signature: this.node.signMessage(message),
660
+ pubkey: this.node.getNodeId()
661
+ };
662
+ }
663
+ verifyMessage(message, signature) {
664
+ if (typeof message !== 'string' || typeof signature !== 'string') {
665
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'message and signature required');
666
+ }
667
+ return this.node.verifyMessage(message, signature);
668
+ }
492
669
  getBalance() {
493
670
  const onchain = this.wallet.getBalance();
494
671
  const lnBalance = this.node.getBalance();
@@ -498,9 +675,9 @@ class BeignetNode extends events_1.EventEmitter {
498
675
  }
499
676
  getPendingCloseBalanceSats() {
500
677
  const recovering = new Set([
501
- types_2.ChannelState.FORCE_CLOSED,
502
- types_2.ChannelState.SHUTTING_DOWN,
503
- types_2.ChannelState.NEGOTIATING_CLOSING
678
+ types_3.ChannelState.FORCE_CLOSED,
679
+ types_3.ChannelState.SHUTTING_DOWN,
680
+ types_3.ChannelState.NEGOTIATING_CLOSING
504
681
  ]);
505
682
  let totalMsat = 0n;
506
683
  for (const ch of this.node.listChannels()) {
@@ -513,7 +690,7 @@ class BeignetNode extends events_1.EventEmitter {
513
690
  getErroredBalanceSats() {
514
691
  let totalMsat = 0n;
515
692
  for (const ch of this.node.listChannels()) {
516
- if (ch.state === types_2.ChannelState.ERRORED) {
693
+ if (ch.state === types_3.ChannelState.ERRORED) {
517
694
  totalMsat += ch.localBalanceMsat;
518
695
  }
519
696
  }
@@ -584,16 +761,96 @@ class BeignetNode extends events_1.EventEmitter {
584
761
  return result.value.addressIndex.address;
585
762
  }
586
763
  async sendOnchain(address, amountSats, satsPerVbyte) {
764
+ this._checkSpendLimit(amountSats);
587
765
  const result = await this.wallet.send({
588
766
  address,
589
767
  amount: amountSats,
590
768
  broadcast: false,
769
+ rbf: this.wallet.rbf,
591
770
  ...(satsPerVbyte !== undefined ? { satsPerByte: satsPerVbyte } : {})
592
771
  });
593
772
  if (result.isErr()) {
594
773
  throw new errors_1.BeignetError('SEND_FAILED', result.error.message);
595
774
  }
596
- const hex = result.value;
775
+ if (this._dailySpendLimitSats === undefined) {
776
+ return this._broadcastRawTx(result.value);
777
+ }
778
+ const totalSats = this._builtOnchainTotalSats(amountSats);
779
+ try {
780
+ this._checkSpendLimit(totalSats);
781
+ }
782
+ catch (e) {
783
+ await this.wallet.resetSendTransaction();
784
+ throw e;
785
+ }
786
+ this._pendingSpendSats += totalSats;
787
+ try {
788
+ const info = await this._broadcastRawTx(result.value);
789
+ this._recordSpend(totalSats, 'onchain');
790
+ return info;
791
+ }
792
+ finally {
793
+ this._pendingSpendSats -= totalSats;
794
+ }
795
+ }
796
+ async buildPsbt(outputs, satsPerVbyte) {
797
+ if (!Array.isArray(outputs) || outputs.length === 0) {
798
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'outputs array required');
799
+ }
800
+ for (const output of outputs) {
801
+ if (!output?.address || typeof output.amountSats !== 'number') {
802
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'each output requires address and amountSats');
803
+ }
804
+ }
805
+ this._validateFeeRate(satsPerVbyte);
806
+ const result = await this.wallet.buildPsbt({
807
+ txs: outputs.map((o) => ({ address: o.address, amount: o.amountSats })),
808
+ rbf: this.wallet.rbf,
809
+ ...(satsPerVbyte !== undefined ? { satsPerByte: satsPerVbyte } : {})
810
+ });
811
+ if (result.isErr()) {
812
+ throw new errors_1.BeignetError('PSBT_BUILD_FAILED', result.error.message);
813
+ }
814
+ const built = result.value;
815
+ return {
816
+ psbtBase64: built.psbtBase64,
817
+ feeSats: built.fee,
818
+ vsizeEstimate: built.vsizeEstimate,
819
+ satsPerVbyte: built.satsPerByte,
820
+ inputs: built.inputs.map((input) => ({
821
+ txid: input.tx_hash,
822
+ vout: input.tx_pos,
823
+ address: input.address,
824
+ valueSats: input.value,
825
+ path: input.path
826
+ })),
827
+ outputs: built.outputs.map((output) => ({
828
+ address: output.address,
829
+ valueSats: output.value
830
+ }))
831
+ };
832
+ }
833
+ importSignedPsbt(psbtBase64) {
834
+ if (!psbtBase64 || typeof psbtBase64 !== 'string') {
835
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'psbtBase64 required');
836
+ }
837
+ const result = this.wallet.importSignedPsbt(psbtBase64);
838
+ if (result.isErr()) {
839
+ throw new errors_1.BeignetError('PSBT_IMPORT_FAILED', result.error.message);
840
+ }
841
+ return { txid: result.value.txid, txHex: result.value.txHex };
842
+ }
843
+ combinePsbts(psbts) {
844
+ if (!Array.isArray(psbts) || psbts.length < 2) {
845
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'psbts array with at least two entries required');
846
+ }
847
+ const result = this.wallet.combinePsbts(psbts);
848
+ if (result.isErr()) {
849
+ throw new errors_1.BeignetError('PSBT_COMBINE_FAILED', result.error.message);
850
+ }
851
+ return { psbtBase64: result.value };
852
+ }
853
+ async _broadcastRawTx(hex) {
597
854
  const bitcoin = await Promise.resolve().then(() => __importStar(require('bitcoinjs-lib')));
598
855
  const tx = bitcoin.Transaction.fromHex(hex);
599
856
  const broadcastRes = await this.wallet.electrum.broadcastTransaction({
@@ -604,15 +861,217 @@ class BeignetNode extends events_1.EventEmitter {
604
861
  }
605
862
  return { txid: tx.getId(), hex };
606
863
  }
864
+ _validateFeeRate(satsPerVbyte, required = false) {
865
+ if (satsPerVbyte === undefined) {
866
+ if (required) {
867
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'satsPerVbyte required');
868
+ }
869
+ return;
870
+ }
871
+ if (typeof satsPerVbyte !== 'number' ||
872
+ !Number.isFinite(satsPerVbyte) ||
873
+ satsPerVbyte <= 0) {
874
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'satsPerVbyte must be a positive number');
875
+ }
876
+ }
877
+ _validateTxid(txid) {
878
+ if (typeof txid !== 'string' || !/^[0-9a-fA-F]{64}$/.test(txid)) {
879
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'txid must be a 64-character hex string');
880
+ }
881
+ }
882
+ async sendMaxOnchain(address, satsPerVbyte) {
883
+ if (!address ||
884
+ typeof address !== 'string' ||
885
+ !this.wallet.validateAddress(address)) {
886
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, `Invalid ${this.networkName} address: ${address}`);
887
+ }
888
+ this._validateFeeRate(satsPerVbyte);
889
+ const result = await this.wallet.sendMax({
890
+ address,
891
+ satsPerByte: satsPerVbyte ?? this.wallet.feeEstimates.normal,
892
+ rbf: this.wallet.rbf,
893
+ broadcast: false
894
+ });
895
+ if (result.isErr()) {
896
+ throw new errors_1.BeignetError('SEND_FAILED', result.error.message);
897
+ }
898
+ if (this._dailySpendLimitSats === undefined) {
899
+ return this._broadcastRawTx(result.value);
900
+ }
901
+ const totalSats = this.wallet.transaction.getTransactionInputValue({
902
+ inputs: this.wallet.transaction.data.inputs
903
+ });
904
+ if (totalSats <= 0) {
905
+ await this.wallet.resetSendTransaction();
906
+ throw new errors_1.BeignetError('SPENDING_LIMIT_EXCEEDED', 'Unable to determine the swept amount for the daily spend limit check; refusing to send');
907
+ }
908
+ try {
909
+ this._checkSpendLimit(totalSats);
910
+ }
911
+ catch (e) {
912
+ await this.wallet.resetSendTransaction();
913
+ throw e;
914
+ }
915
+ this._pendingSpendSats += totalSats;
916
+ try {
917
+ const info = await this._broadcastRawTx(result.value);
918
+ this._recordSpend(totalSats, 'onchain');
919
+ return info;
920
+ }
921
+ finally {
922
+ this._pendingSpendSats -= totalSats;
923
+ }
924
+ }
925
+ async bumpFeeOnchain(txid, satsPerVbyte) {
926
+ this._validateTxid(txid);
927
+ this._validateFeeRate(satsPerVbyte, true);
928
+ const can = this.wallet.canBoost(txid);
929
+ if (!can.rbf) {
930
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NOT_BOOSTABLE, can.cpfp
931
+ ? `Transaction ${txid} cannot be replaced via RBF; use POST /tx/boost (CPFP) instead`
932
+ : `Transaction ${txid} is not boostable (unknown, already confirmed, or balance too low)`);
933
+ }
934
+ return this._boostRbf(txid, satsPerVbyte);
935
+ }
936
+ async boostOnchain(txid, satsPerVbyte) {
937
+ this._validateTxid(txid);
938
+ this._validateFeeRate(satsPerVbyte);
939
+ const can = this.wallet.canBoost(txid);
940
+ if (!can.canBoost) {
941
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NOT_BOOSTABLE, `Transaction ${txid} is not boostable (unknown, already confirmed, or balance too low)`);
942
+ }
943
+ if (can.rbf) {
944
+ try {
945
+ return await this._boostRbf(txid, satsPerVbyte);
946
+ }
947
+ catch (e) {
948
+ const rbfImpossible = e instanceof errors_1.BeignetError &&
949
+ e.code === errors_1.BeignetErrorCode.NOT_BOOSTABLE;
950
+ if (!(rbfImpossible && can.cpfp))
951
+ throw e;
952
+ }
953
+ }
954
+ return this._boostCpfp(txid, satsPerVbyte);
955
+ }
956
+ async _boostRbf(txid, satsPerVbyte) {
957
+ const setup = await this.wallet.transaction.setupRbf({ txid });
958
+ if (setup.isErr()) {
959
+ await this.wallet.resetSendTransaction();
960
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NOT_BOOSTABLE, setup.error.message);
961
+ }
962
+ try {
963
+ if (satsPerVbyte !== undefined) {
964
+ const feeRes = this.wallet.transaction.updateFee({
965
+ satsPerByte: satsPerVbyte
966
+ });
967
+ if (feeRes.isErr()) {
968
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, feeRes.error.message);
969
+ }
970
+ }
971
+ const original = this.wallet.unconfirmedTransactions[txid] ??
972
+ this.wallet.transactions[txid];
973
+ const originalFeeSats = original ? (0, conversion_1.btcToSats)(original.fee) : 0;
974
+ const newFeeSats = this.wallet.transaction.data.fee;
975
+ if (newFeeSats <= originalFeeSats) {
976
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, `Replacement fee ${newFeeSats} sats does not exceed the original fee ${originalFeeSats} sats; raise satsPerVbyte`);
977
+ }
978
+ const createRes = await this.wallet.transaction.createTransaction();
979
+ if (createRes.isErr()) {
980
+ throw new errors_1.BeignetError('SEND_FAILED', createRes.error.message);
981
+ }
982
+ const info = await this._broadcastRawTx(createRes.value.hex);
983
+ await this._recordBoost(txid, info.txid, wallet_2.EBoostType.rbf, newFeeSats);
984
+ return {
985
+ ...info,
986
+ boostType: 'rbf',
987
+ feeSats: newFeeSats,
988
+ originalTxid: txid
989
+ };
990
+ }
991
+ finally {
992
+ await this.wallet.resetSendTransaction();
993
+ }
994
+ }
995
+ async _boostCpfp(txid, satsPerVbyte) {
996
+ const setup = await this.wallet.transaction.setupCpfp({
997
+ txid,
998
+ ...(satsPerVbyte !== undefined ? { satsPerByte: satsPerVbyte } : {})
999
+ });
1000
+ if (setup.isErr()) {
1001
+ await this.wallet.resetSendTransaction();
1002
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NOT_BOOSTABLE, setup.error.message);
1003
+ }
1004
+ try {
1005
+ const feeSats = setup.value.fee;
1006
+ const createRes = await this.wallet.transaction.createTransaction();
1007
+ if (createRes.isErr()) {
1008
+ throw new errors_1.BeignetError('SEND_FAILED', createRes.error.message);
1009
+ }
1010
+ const info = await this._broadcastRawTx(createRes.value.hex);
1011
+ await this._recordBoost(txid, info.txid, wallet_2.EBoostType.cpfp, feeSats);
1012
+ return {
1013
+ ...info,
1014
+ boostType: 'cpfp',
1015
+ feeSats,
1016
+ originalTxid: txid
1017
+ };
1018
+ }
1019
+ finally {
1020
+ await this.wallet.resetSendTransaction();
1021
+ }
1022
+ }
1023
+ async _recordBoost(oldTxId, newTxId, type, fee) {
1024
+ const res = await this.wallet.addBoostedTransaction({
1025
+ oldTxId,
1026
+ newTxId,
1027
+ type,
1028
+ fee
1029
+ });
1030
+ if (res.isErr()) {
1031
+ this.log('warn', 'Failed to record boosted transaction', {
1032
+ oldTxId,
1033
+ newTxId,
1034
+ error: res.error.message
1035
+ });
1036
+ }
1037
+ }
1038
+ listBoostableTransactions() {
1039
+ const { rbf, cpfp } = this.wallet.getBoostableTransactions();
1040
+ return {
1041
+ rbf: rbf.map((tx) => this.toOnchainTxInfo(tx)),
1042
+ cpfp: cpfp.map((tx) => this.toOnchainTxInfo(tx))
1043
+ };
1044
+ }
1045
+ async consolidateUtxos(satsPerVbyte) {
1046
+ this._validateFeeRate(satsPerVbyte);
1047
+ const utxoCount = this.wallet
1048
+ .listUtxos()
1049
+ .filter((u) => !this.wallet.isUtxoFrozen(u.tx_hash, u.tx_pos)).length;
1050
+ if (utxoCount < 2) {
1051
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NOTHING_TO_CONSOLIDATE, `Nothing to consolidate: wallet has ${utxoCount} spendable UTXO(s), need at least 2`);
1052
+ }
1053
+ const address = await this.getNewAddress();
1054
+ const result = await this.wallet.sendMax({
1055
+ address,
1056
+ satsPerByte: satsPerVbyte ?? this.wallet.feeEstimates.normal,
1057
+ rbf: this.wallet.rbf,
1058
+ broadcast: false
1059
+ });
1060
+ if (result.isErr()) {
1061
+ throw new errors_1.BeignetError('SEND_FAILED', result.error.message);
1062
+ }
1063
+ const feeSats = this.wallet.transaction.data.fee;
1064
+ const info = await this._broadcastRawTx(result.value);
1065
+ return { ...info, utxosConsolidated: utxoCount, address, feeSats };
1066
+ }
607
1067
  async refreshWallet() {
608
1068
  const result = await this.wallet.refreshWallet({});
609
1069
  if (result.isErr()) {
610
1070
  throw new errors_1.BeignetError('REFRESH_FAILED', result.error.message);
611
1071
  }
612
1072
  }
613
- listOnchainTransactions() {
614
- return Object.values(this.wallet.transactions)
615
- .map((tx) => ({
1073
+ toOnchainTxInfo(tx) {
1074
+ return {
616
1075
  txid: tx.txid,
617
1076
  type: tx.type === wallet_2.EPaymentType.sent
618
1077
  ? 'sent'
@@ -627,7 +1086,11 @@ class BeignetNode extends events_1.EventEmitter {
627
1086
  ...(tx.confirmTimestamp !== undefined
628
1087
  ? { confirmTimestamp: tx.confirmTimestamp }
629
1088
  : {})
630
- }))
1089
+ };
1090
+ }
1091
+ listOnchainTransactions() {
1092
+ return Object.values(this.wallet.transactions)
1093
+ .map((tx) => this.toOnchainTxInfo(tx))
631
1094
  .sort((a, b) => b.timestamp - a.timestamp);
632
1095
  }
633
1096
  listUtxos() {
@@ -636,9 +1099,43 @@ class BeignetNode extends events_1.EventEmitter {
636
1099
  vout: utxo.tx_pos,
637
1100
  address: utxo.address,
638
1101
  valueSats: utxo.value,
639
- height: utxo.height
1102
+ height: utxo.height,
1103
+ frozen: this.wallet.isUtxoFrozen(utxo.tx_hash, utxo.tx_pos)
640
1104
  }));
641
1105
  }
1106
+ async freezeUtxo(txid, index) {
1107
+ this._validateTxid(txid);
1108
+ const res = await this.wallet.freezeUtxo({ txid, index });
1109
+ if (res.isErr()) {
1110
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, res.error.message);
1111
+ }
1112
+ return { frozen: `${txid}:${index}` };
1113
+ }
1114
+ async unfreezeUtxo(txid, index) {
1115
+ this._validateTxid(txid);
1116
+ const res = await this.wallet.unfreezeUtxo({ txid, index });
1117
+ if (res.isErr()) {
1118
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, res.error.message);
1119
+ }
1120
+ return { unfrozen: `${txid}:${index}` };
1121
+ }
1122
+ async setAddressLabel(address, label) {
1123
+ const res = await this.wallet.setAddressLabel(address, label);
1124
+ if (res.isErr()) {
1125
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, res.error.message);
1126
+ }
1127
+ return { address, label };
1128
+ }
1129
+ listAddressLabels() {
1130
+ return this.wallet.listAddressLabels();
1131
+ }
1132
+ exportDescriptors() {
1133
+ const res = this.wallet.exportDescriptors();
1134
+ if (res.isErr()) {
1135
+ throw new errors_1.BeignetError('DESCRIPTOR_EXPORT_FAILED', res.error.message);
1136
+ }
1137
+ return res.value;
1138
+ }
642
1139
  async getFeeEstimates() {
643
1140
  try {
644
1141
  return await this.wallet.getFeeEstimates();
@@ -654,10 +1151,13 @@ class BeignetNode extends events_1.EventEmitter {
654
1151
  return this.wallet;
655
1152
  }
656
1153
  async connectPeer(pubkey, host, port) {
1154
+ const target = host !== undefined && port !== undefined
1155
+ ? `${host}:${port}`
1156
+ : 'resolved address (gossip graph / DNS bootstrap)';
657
1157
  let timer;
658
1158
  const connectPromise = this.node.connectPeer(pubkey, host, port);
659
1159
  const timeoutPromise = new Promise((_, reject) => {
660
- timer = setTimeout(() => reject(new errors_1.BeignetError('CONNECT_TIMEOUT', `connectPeer timed out after ${this._connectTimeoutMs}ms (is ${host}:${port} the peer's P2P address?)`)), this._connectTimeoutMs);
1160
+ timer = setTimeout(() => reject(new errors_1.BeignetError('CONNECT_TIMEOUT', `connectPeer timed out after ${this._connectTimeoutMs}ms (is ${target} the peer's P2P address?)`)), this._connectTimeoutMs);
661
1161
  });
662
1162
  try {
663
1163
  await Promise.race([connectPromise, timeoutPromise]);
@@ -665,13 +1165,19 @@ class BeignetNode extends events_1.EventEmitter {
665
1165
  catch (err) {
666
1166
  if (err instanceof errors_1.BeignetError)
667
1167
  throw err;
668
- throw new errors_1.BeignetError('CONNECT_FAILED', `Failed to connect to ${pubkey.slice(0, 16)}…@${host}:${port}: ${err.message}`);
1168
+ throw new errors_1.BeignetError('CONNECT_FAILED', `Failed to connect to ${pubkey.slice(0, 16)}…@${target}: ${err.message}`);
669
1169
  }
670
1170
  finally {
671
1171
  if (timer)
672
1172
  clearTimeout(timer);
673
1173
  }
674
- return { pubkey, host, port, state: 'connected' };
1174
+ const connected = this.node.listPeers().find((p) => p.pubkey === pubkey);
1175
+ return {
1176
+ pubkey,
1177
+ host: host ?? connected?.host ?? '',
1178
+ port: port ?? connected?.port ?? 0,
1179
+ state: 'connected'
1180
+ };
675
1181
  }
676
1182
  disconnectPeer(pubkey) {
677
1183
  this.node.disconnectPeer(pubkey);
@@ -701,6 +1207,8 @@ class BeignetNode extends events_1.EventEmitter {
701
1207
  });
702
1208
  }
703
1209
  }
1210
+ if (synced.length > 0)
1211
+ this._lastGraphSyncAt = Date.now();
704
1212
  return synced;
705
1213
  }
706
1214
  async syncRapidGossip() {
@@ -712,6 +1220,7 @@ class BeignetNode extends events_1.EventEmitter {
712
1220
  this.log('info', 'Rapid gossip sync: downloading snapshot', { url });
713
1221
  const data = await (0, rapid_sync_1.fetchRapidGossipSnapshot)(url);
714
1222
  const result = this.node.loadRapidGossipSnapshot(data);
1223
+ this._lastGraphSyncAt = Date.now();
715
1224
  this.log('info', 'Rapid gossip sync complete', {
716
1225
  channelsAdded: result.channelsAdded,
717
1226
  updatesApplied: result.updatesApplied,
@@ -733,6 +1242,7 @@ class BeignetNode extends events_1.EventEmitter {
733
1242
  const state = channel.getFullState();
734
1243
  const balances = channel.getBalances();
735
1244
  const channelId = state.channelId || state.temporaryChannelId;
1245
+ this.refreshStaticChannelBackup();
736
1246
  return {
737
1247
  channelId: channelId.toString('hex'),
738
1248
  peerPubkey: pubkey,
@@ -740,7 +1250,7 @@ class BeignetNode extends events_1.EventEmitter {
740
1250
  localBalanceSats: Number(balances.localMsat / 1000n),
741
1251
  remoteBalanceSats: Number(balances.remoteMsat / 1000n),
742
1252
  capacitySats: amountSats,
743
- isAnchor: (0, types_2.isAnchorChannel)(state.channelType)
1253
+ isAnchor: (0, types_3.isAnchorChannel)(state.channelType)
744
1254
  };
745
1255
  }
746
1256
  async openChannelAndWait(pubkey, amountSats, opts) {
@@ -882,7 +1392,7 @@ class BeignetNode extends events_1.EventEmitter {
882
1392
  localBalanceSats: Number(ch.localBalanceMsat / 1000n),
883
1393
  remoteBalanceSats: Number(ch.remoteBalanceMsat / 1000n),
884
1394
  capacitySats: Number(ch.fundingSatoshis),
885
- isAnchor: (0, types_2.isAnchorChannel)(ch.channelType ?? null)
1395
+ isAnchor: (0, types_3.isAnchorChannel)(ch.channelType ?? null)
886
1396
  };
887
1397
  if (ch.fundingTxid)
888
1398
  info.fundingTxid = ch.fundingTxid;
@@ -894,6 +1404,16 @@ class BeignetNode extends events_1.EventEmitter {
894
1404
  info.htlcCount = ch.htlcCount;
895
1405
  if (ch.isPrivate !== undefined)
896
1406
  info.isPrivate = ch.isPrivate;
1407
+ if (ch.feeBaseMsat !== undefined)
1408
+ info.feeBaseMsat = ch.feeBaseMsat;
1409
+ if (ch.feeProportionalMillionths !== undefined)
1410
+ info.feeProportionalMillionths = ch.feeProportionalMillionths;
1411
+ if (ch.cltvExpiryDelta !== undefined)
1412
+ info.cltvExpiryDelta = ch.cltvExpiryDelta;
1413
+ if (ch.htlcMinimumMsat !== undefined)
1414
+ info.htlcMinimumMsat = ch.htlcMinimumMsat.toString();
1415
+ if (ch.htlcMaximumMsat !== undefined)
1416
+ info.htlcMaximumMsat = ch.htlcMaximumMsat.toString();
897
1417
  return info;
898
1418
  }
899
1419
  createInvoice(amountSats, description, expirySecs, descriptionHash) {
@@ -916,6 +1436,76 @@ class BeignetNode extends events_1.EventEmitter {
916
1436
  info.expiry = expirySecs;
917
1437
  return info;
918
1438
  }
1439
+ createHoldInvoice(opts) {
1440
+ if (!/^[0-9a-fA-F]{64}$/.test(opts.paymentHash)) {
1441
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'paymentHash must be 32 bytes hex (64 hex chars)');
1442
+ }
1443
+ const amountMsat = opts.amountMsat ??
1444
+ (opts.amountSats !== undefined && opts.amountSats !== 0
1445
+ ? BigInt(opts.amountSats) * 1000n
1446
+ : undefined);
1447
+ const result = this.node.createInvoice({
1448
+ amountMsat,
1449
+ description: opts.description || '',
1450
+ expiry: opts.expiry,
1451
+ hold: true,
1452
+ paymentHash: Buffer.from(opts.paymentHash, 'hex')
1453
+ });
1454
+ const info = {
1455
+ bolt11: result.bolt11,
1456
+ paymentHash: result.paymentHash.toString('hex'),
1457
+ paymentSecret: result.paymentSecret.toString('hex')
1458
+ };
1459
+ if (amountMsat !== undefined)
1460
+ info.amountSats = Number(amountMsat / 1000n);
1461
+ if (opts.expiry !== undefined)
1462
+ info.expiry = opts.expiry;
1463
+ return info;
1464
+ }
1465
+ settleHoldInvoice(preimage) {
1466
+ if (!/^[0-9a-fA-F]{64}$/.test(preimage)) {
1467
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'preimage must be 32 bytes hex (64 hex chars)');
1468
+ }
1469
+ const preimageBuf = Buffer.from(preimage, 'hex');
1470
+ const paymentHash = crypto
1471
+ .createHash('sha256')
1472
+ .update(preimageBuf)
1473
+ .digest();
1474
+ const settled = this.node.settleHeldHtlc(paymentHash, preimageBuf);
1475
+ if (!settled) {
1476
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NOT_FOUND, 'No parked HTLCs for this preimage (invoice unknown, not yet paid, or already resolved)');
1477
+ }
1478
+ return { paymentHash: paymentHash.toString('hex') };
1479
+ }
1480
+ cancelHoldInvoice(paymentHash) {
1481
+ if (!/^[0-9a-fA-F]{64}$/.test(paymentHash)) {
1482
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'paymentHash must be 32 bytes hex (64 hex chars)');
1483
+ }
1484
+ const result = this.node.cancelHoldInvoice(Buffer.from(paymentHash, 'hex'));
1485
+ if (!result) {
1486
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NOT_FOUND, 'No open hold invoice for this payment hash');
1487
+ }
1488
+ return { paymentHash, htlcsFailed: result.htlcsFailed };
1489
+ }
1490
+ listHoldInvoices() {
1491
+ return this.node.listHoldInvoices().map((inv) => {
1492
+ const info = {
1493
+ paymentHash: inv.paymentHash,
1494
+ bolt11: inv.bolt11,
1495
+ state: inv.state,
1496
+ heldAmountMsat: inv.heldAmountMsat.toString(),
1497
+ htlcCount: inv.htlcCount,
1498
+ expiry: inv.expiry,
1499
+ createdAt: inv.createdAt
1500
+ };
1501
+ if (inv.amountMsat !== undefined) {
1502
+ info.amountSats = Number(inv.amountMsat / 1000n);
1503
+ }
1504
+ if (inv.description)
1505
+ info.description = inv.description;
1506
+ return info;
1507
+ });
1508
+ }
919
1509
  decodeInvoice(bolt11) {
920
1510
  const inv = (0, decode_1.decode)(bolt11);
921
1511
  const result = {
@@ -967,6 +1557,8 @@ class BeignetNode extends events_1.EventEmitter {
967
1557
  tomorrow.setUTCHours(24, 0, 0, 0);
968
1558
  this._dailySpendResetTime = tomorrow.getTime();
969
1559
  this._dailySpentSats = 0;
1560
+ this._dailySpentLightningSats = 0;
1561
+ this._dailySpentOnchainSats = 0;
970
1562
  }
971
1563
  }
972
1564
  _checkSpendLimit(amountSats) {
@@ -986,10 +1578,26 @@ class BeignetNode extends events_1.EventEmitter {
986
1578
  throw new errors_1.BeignetError('SPENDING_LIMIT_EXCEEDED', `Payment amount ${amountSats} sats exceeds per-payment limit of ${this._maxPaymentSats} sats`);
987
1579
  }
988
1580
  }
989
- _recordSpend(amountSats) {
1581
+ _recordSpend(amountSats, source = 'lightning') {
990
1582
  if (this._dailySpendLimitSats === undefined)
991
1583
  return;
992
1584
  this._dailySpentSats += amountSats;
1585
+ if (source === 'onchain') {
1586
+ this._dailySpentOnchainSats += amountSats;
1587
+ }
1588
+ else {
1589
+ this._dailySpentLightningSats += amountSats;
1590
+ }
1591
+ }
1592
+ _builtOnchainTotalSats(amountSats) {
1593
+ const feeSats = this.wallet.transaction?.data?.fee;
1594
+ if (!Number.isFinite(feeSats) || feeSats < 0) {
1595
+ if (this._dailySpendLimitSats !== undefined) {
1596
+ throw new errors_1.BeignetError('SPENDING_LIMIT_EXCEEDED', 'Unable to determine the transaction fee for the daily spend limit check; refusing to send');
1597
+ }
1598
+ return amountSats;
1599
+ }
1600
+ return amountSats + feeSats;
993
1601
  }
994
1602
  getDailySpendInfo() {
995
1603
  this._resetDailySpendIfNeeded();
@@ -998,7 +1606,10 @@ class BeignetNode extends events_1.EventEmitter {
998
1606
  limitSats: limit,
999
1607
  spentSats: this._dailySpentSats,
1000
1608
  remainingSats: limit !== null ? Math.max(0, limit - this._dailySpentSats) : Infinity,
1001
- resetsAt: this._dailySpendResetTime
1609
+ resetsAt: this._dailySpendResetTime,
1610
+ totalSats: this._dailySpentSats,
1611
+ lightningSats: this._dailySpentLightningSats,
1612
+ onchainSats: this._dailySpentOnchainSats
1002
1613
  };
1003
1614
  }
1004
1615
  setDraining(enabled) {
@@ -1543,6 +2154,36 @@ class BeignetNode extends events_1.EventEmitter {
1543
2154
  return null;
1544
2155
  return this.toPaymentInfo(p);
1545
2156
  }
2157
+ listForwards(filter) {
2158
+ return this.node.listForwards(filter).map((f) => ({
2159
+ id: f.id,
2160
+ settledAt: f.settledAt,
2161
+ inChannelId: f.inChannelId,
2162
+ outChannelId: f.outChannelId,
2163
+ inScid: f.inScid,
2164
+ outScid: f.outScid,
2165
+ amountInMsat: f.amountInMsat.toString(),
2166
+ amountOutMsat: f.amountOutMsat.toString(),
2167
+ feeMsat: f.feeMsat.toString()
2168
+ }));
2169
+ }
2170
+ getForwardingSummary(since) {
2171
+ const summary = this.node.getForwardingSummary(since !== undefined ? { since } : undefined);
2172
+ return {
2173
+ count: summary.count,
2174
+ volumeOutMsat: summary.volumeOutMsat.toString(),
2175
+ feesEarnedMsat: summary.feesEarnedMsat.toString()
2176
+ };
2177
+ }
2178
+ listWatchtowers() {
2179
+ return this.node.getWatchtowers();
2180
+ }
2181
+ addWatchtower(uri) {
2182
+ this.node.addWatchtower(uri);
2183
+ }
2184
+ removeWatchtower(uri) {
2185
+ this.node.removeWatchtower(uri);
2186
+ }
1546
2187
  getPaymentProof(paymentHash) {
1547
2188
  const proof = this.node.getPaymentProof(Buffer.from(paymentHash, 'hex'));
1548
2189
  if (!proof)
@@ -1579,6 +2220,41 @@ class BeignetNode extends events_1.EventEmitter {
1579
2220
  updateChannelFee(channelId, feeratePerKw) {
1580
2221
  return this.node.updateChannelFee(Buffer.from(channelId, 'hex'), feeratePerKw);
1581
2222
  }
2223
+ updateChannelPolicy(channelId, policy) {
2224
+ const update = {};
2225
+ if (policy.feeBaseMsat !== undefined)
2226
+ update.feeBaseMsat = policy.feeBaseMsat;
2227
+ if (policy.feeProportionalMillionths !== undefined)
2228
+ update.feeProportionalMillionths = policy.feeProportionalMillionths;
2229
+ if (policy.cltvExpiryDelta !== undefined)
2230
+ update.cltvExpiryDelta = policy.cltvExpiryDelta;
2231
+ if (policy.htlcMinimumMsat !== undefined)
2232
+ update.htlcMinimumMsat = BigInt(policy.htlcMinimumMsat);
2233
+ if (policy.htlcMaximumMsat !== undefined)
2234
+ update.htlcMaximumMsat = BigInt(policy.htlcMaximumMsat);
2235
+ const targets = channelId === 'all'
2236
+ ? this.node.listChannels().map((ch) => ch.channelId.toString('hex'))
2237
+ : [channelId];
2238
+ this.node.setChannelPolicy(channelId === 'all' ? 'all' : Buffer.from(channelId, 'hex'), update);
2239
+ const policies = targets
2240
+ .map((id) => this.getChannelPolicy(id))
2241
+ .filter((p) => p !== null);
2242
+ return { updated: policies.length, policies };
2243
+ }
2244
+ getChannelPolicy(channelId) {
2245
+ const policy = this.node.getChannelPolicy(Buffer.from(channelId, 'hex'));
2246
+ if (!policy)
2247
+ return null;
2248
+ return {
2249
+ channelId,
2250
+ feeBaseMsat: policy.feeBaseMsat,
2251
+ feeProportionalMillionths: policy.feeProportionalMillionths,
2252
+ cltvExpiryDelta: policy.cltvExpiryDelta,
2253
+ htlcMinimumMsat: policy.htlcMinimumMsat.toString(),
2254
+ htlcMaximumMsat: policy.htlcMaximumMsat.toString(),
2255
+ source: policy.source
2256
+ };
2257
+ }
1582
2258
  cancelPayment(paymentHash) {
1583
2259
  this.node.failPayment(Buffer.from(paymentHash, 'hex'));
1584
2260
  return { ok: true };
@@ -1662,6 +2338,7 @@ class BeignetNode extends events_1.EventEmitter {
1662
2338
  const state = channel.getFullState();
1663
2339
  const balances = channel.getBalances();
1664
2340
  const channelId = state.channelId || state.temporaryChannelId;
2341
+ this.refreshStaticChannelBackup();
1665
2342
  return {
1666
2343
  channelId: channelId.toString('hex'),
1667
2344
  peerPubkey,
@@ -1669,7 +2346,7 @@ class BeignetNode extends events_1.EventEmitter {
1669
2346
  localBalanceSats: Number(balances.localMsat / 1000n),
1670
2347
  remoteBalanceSats: Number(balances.remoteMsat / 1000n),
1671
2348
  capacitySats: amountSats,
1672
- isAnchor: (0, types_2.isAnchorChannel)(state.channelType)
2349
+ isAnchor: (0, types_3.isAnchorChannel)(state.channelType)
1673
2350
  };
1674
2351
  }
1675
2352
  openChannelV2(peerPubkey, params) {
@@ -1682,6 +2359,7 @@ class BeignetNode extends events_1.EventEmitter {
1682
2359
  const state = channel.getFullState();
1683
2360
  const balances = channel.getBalances();
1684
2361
  const channelId = state.channelId || state.temporaryChannelId;
2362
+ this.refreshStaticChannelBackup();
1685
2363
  return {
1686
2364
  channelId: channelId.toString('hex'),
1687
2365
  peerPubkey,
@@ -1689,16 +2367,20 @@ class BeignetNode extends events_1.EventEmitter {
1689
2367
  localBalanceSats: Number(balances.localMsat / 1000n),
1690
2368
  remoteBalanceSats: Number(balances.remoteMsat / 1000n),
1691
2369
  capacitySats: params.amountSats,
1692
- isAnchor: (0, types_2.isAnchorChannel)(state.channelType)
2370
+ isAnchor: (0, types_3.isAnchorChannel)(state.channelType)
1693
2371
  };
1694
2372
  }
1695
2373
  spliceIn(channelId, amountSats, feeratePerkw) {
1696
2374
  const idBuf = Buffer.from(channelId, 'hex');
1697
- return this.node.spliceIn(idBuf, BigInt(amountSats), feeratePerkw);
2375
+ const result = this.node.spliceIn(idBuf, BigInt(amountSats), feeratePerkw);
2376
+ this.refreshStaticChannelBackup();
2377
+ return result;
1698
2378
  }
1699
2379
  spliceOut(channelId, amountSats, feeratePerkw) {
1700
2380
  const idBuf = Buffer.from(channelId, 'hex');
1701
- return this.node.spliceOut(idBuf, BigInt(amountSats), feeratePerkw);
2381
+ const result = this.node.spliceOut(idBuf, BigInt(amountSats), feeratePerkw);
2382
+ this.refreshStaticChannelBackup();
2383
+ return result;
1702
2384
  }
1703
2385
  decodeOfferString(offerStr) {
1704
2386
  const offer = (0, decode_2.decodeOffer)(offerStr);
@@ -1915,7 +2597,7 @@ class BeignetNode extends events_1.EventEmitter {
1915
2597
  ? 'Networking and auto-reconnect enabled'
1916
2598
  : 'Networking disabled — node cannot reconnect to peers'
1917
2599
  });
1918
- const hasAnchor = channels.some((ch) => ch.channelType != null && (0, types_2.isAnchorChannel)(ch.channelType));
2600
+ const hasAnchor = channels.some((ch) => ch.channelType != null && (0, types_3.isAnchorChannel)(ch.channelType));
1919
2601
  checks.push({
1920
2602
  name: 'ANCHOR_CHANNELS_PREFERRED',
1921
2603
  status: hasAnchor || channels.length === 0 ? 'PASS' : 'WARN',
@@ -2032,6 +2714,70 @@ class BeignetNode extends events_1.EventEmitter {
2032
2714
  recommendations: snapshot.recommendations
2033
2715
  };
2034
2716
  }
2717
+ getAdvisorRecommendations() {
2718
+ const plan = this.node
2719
+ .planRebalanceRecommendations()
2720
+ .map((p) => ({
2721
+ fromChannelId: p.fromChannelId,
2722
+ toChannelId: p.toChannelId,
2723
+ amountSats: Number(p.amountSats),
2724
+ reason: p.reason
2725
+ }));
2726
+ return { ...this.getLiquiditySnapshot(), rebalancePlan: plan };
2727
+ }
2728
+ async rebalanceChannel(fromChannelId, toChannelId, amountSats, maxFeeSats) {
2729
+ if (!/^[0-9a-fA-F]{64}$/.test(fromChannelId))
2730
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'fromChannelId must be 64 hex chars');
2731
+ if (!/^[0-9a-fA-F]{64}$/.test(toChannelId))
2732
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'toChannelId must be 64 hex chars');
2733
+ if (!Number.isInteger(amountSats) || amountSats <= 0)
2734
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'amountSats must be a positive integer');
2735
+ if (!Number.isInteger(maxFeeSats) || maxFeeSats < 0)
2736
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'maxFeeSats must be a non-negative integer');
2737
+ const result = await this.node.rebalanceChannel({
2738
+ fromChannelId: Buffer.from(fromChannelId, 'hex'),
2739
+ toChannelId: Buffer.from(toChannelId, 'hex'),
2740
+ amountSats: BigInt(amountSats),
2741
+ maxFeeSats: BigInt(maxFeeSats)
2742
+ });
2743
+ this.log('info', 'Rebalance completed', {
2744
+ fromChannelId,
2745
+ toChannelId,
2746
+ amountSats,
2747
+ feeMsat: result.feeMsat.toString()
2748
+ });
2749
+ return {
2750
+ paymentHash: result.paymentHash.toString('hex'),
2751
+ amountSats,
2752
+ feeMsat: result.feeMsat.toString(),
2753
+ feeSats: Number(result.feeMsat / 1000n),
2754
+ hops: result.hops
2755
+ };
2756
+ }
2757
+ async executeRebalances(budgetSatsPerDay) {
2758
+ if (budgetSatsPerDay !== undefined &&
2759
+ (!Number.isInteger(budgetSatsPerDay) || budgetSatsPerDay < 0)) {
2760
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'budgetSatsPerDay must be a non-negative integer');
2761
+ }
2762
+ const summary = await this.node.executeRebalanceRecommendations({
2763
+ budgetSatsPerDay
2764
+ });
2765
+ return {
2766
+ attempts: summary.attempts.map((a) => ({
2767
+ fromChannelId: a.fromChannelId,
2768
+ toChannelId: a.toChannelId,
2769
+ amountSats: Number(a.amountSats),
2770
+ status: a.status,
2771
+ feeMsat: a.feeMsat?.toString(),
2772
+ error: a.error
2773
+ })),
2774
+ succeeded: summary.succeeded,
2775
+ failed: summary.failed,
2776
+ skippedBudget: summary.skippedBudget,
2777
+ feeSpentMsat: summary.feeSpentMsat.toString(),
2778
+ budgetRemainingMsat: summary.budgetRemainingMsat.toString()
2779
+ };
2780
+ }
2035
2781
  getFeeSnapshot() {
2036
2782
  return this.node.getFeeSnapshot();
2037
2783
  }
@@ -2047,10 +2793,160 @@ class BeignetNode extends events_1.EventEmitter {
2047
2793
  probeRoute(destination, amountSats) {
2048
2794
  return this.node.probeRoute(destination, amountSats);
2049
2795
  }
2796
+ getGraphInfo() {
2797
+ const graph = this.node.getGraph();
2798
+ const info = {
2799
+ nodeCount: graph.getNodeCount(),
2800
+ channelCount: graph.getChannelCount()
2801
+ };
2802
+ if (this._lastGraphSyncAt !== undefined) {
2803
+ info.lastSyncAt = this._lastGraphSyncAt;
2804
+ }
2805
+ return info;
2806
+ }
2807
+ getGraphNode(pubkey) {
2808
+ if (!/^[0-9a-fA-F]{66}$/.test(pubkey)) {
2809
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'pubkey must be a 33-byte hex string');
2810
+ }
2811
+ const graph = this.node.getGraph();
2812
+ const node = graph.getNode(Buffer.from(pubkey, 'hex'));
2813
+ if (!node)
2814
+ return null;
2815
+ const info = {
2816
+ pubkey: node.nodeId.toString('hex'),
2817
+ channelCount: node.channels.size,
2818
+ channels: graph
2819
+ .getNodeChannels(node.nodeId)
2820
+ .map((ch) => formatScid(ch.shortChannelId))
2821
+ };
2822
+ const ann = node.announcement;
2823
+ if (ann) {
2824
+ const alias = ann.alias
2825
+ .subarray(0, ann.alias.indexOf(0) === -1 ? 32 : ann.alias.indexOf(0))
2826
+ .toString('utf8');
2827
+ if (alias.length > 0)
2828
+ info.alias = alias;
2829
+ info.color = ann.rgbColor.toString('hex');
2830
+ info.addresses = ann.addresses.map((a) => ({
2831
+ type: a.type,
2832
+ host: a.host,
2833
+ port: a.port
2834
+ }));
2835
+ info.featuresHex = ann.features.toString('hex');
2836
+ info.lastUpdate = ann.timestamp;
2837
+ }
2838
+ return info;
2839
+ }
2840
+ getGraphChannel(scid) {
2841
+ const scidBuf = parseScid(scid);
2842
+ const channel = this.node.getGraph().getChannel(scidBuf);
2843
+ if (!channel)
2844
+ return null;
2845
+ return this.toGraphChannelInfo(channel);
2846
+ }
2847
+ describeGraph(limit, offset) {
2848
+ const cappedLimit = Math.min(Math.max(1, Math.floor(limit ?? GRAPH_DESCRIBE_MAX_LIMIT)), GRAPH_DESCRIBE_MAX_LIMIT);
2849
+ const safeOffset = Math.max(0, Math.floor(offset ?? 0));
2850
+ const all = this.node.getGraph().getAllChannels();
2851
+ return {
2852
+ totalChannels: all.length,
2853
+ limit: cappedLimit,
2854
+ offset: safeOffset,
2855
+ channels: all
2856
+ .slice(safeOffset, safeOffset + cappedLimit)
2857
+ .map((ch) => this.toGraphChannelInfo(ch))
2858
+ };
2859
+ }
2860
+ toGraphChannelInfo(channel) {
2861
+ const info = {
2862
+ shortChannelId: formatScid(channel.shortChannelId),
2863
+ node1Pubkey: channel.nodeId1.toString('hex'),
2864
+ node2Pubkey: channel.nodeId2.toString('hex')
2865
+ };
2866
+ const max1 = channel.update1?.htlcMaximumMsat;
2867
+ const max2 = channel.update2?.htlcMaximumMsat;
2868
+ const best = (max1 ?? 0n) > (max2 ?? 0n) ? max1 : max2;
2869
+ if (best !== undefined)
2870
+ info.capacitySats = Number(best / 1000n);
2871
+ if (channel.update1) {
2872
+ info.node1Policy = this.toGraphChannelPolicy(channel.update1);
2873
+ }
2874
+ if (channel.update2) {
2875
+ info.node2Policy = this.toGraphChannelPolicy(channel.update2);
2876
+ }
2877
+ return info;
2878
+ }
2879
+ toGraphChannelPolicy(update) {
2880
+ const policy = {
2881
+ feeBaseMsat: update.feeBaseMsat,
2882
+ feeProportionalMillionths: update.feeProportionalMillionths,
2883
+ cltvExpiryDelta: update.cltvExpiryDelta,
2884
+ htlcMinimumMsat: update.htlcMinimumMsat.toString(),
2885
+ disabled: (update.channelFlags & types_1.CHANNEL_FLAG_DISABLED) !== 0,
2886
+ lastUpdate: update.timestamp
2887
+ };
2888
+ if (update.htlcMaximumMsat !== undefined) {
2889
+ policy.htlcMaximumMsat = update.htlcMaximumMsat.toString();
2890
+ }
2891
+ return policy;
2892
+ }
2893
+ queryRoute(destination, amountSats, maxFeeSats) {
2894
+ if (!/^[0-9a-fA-F]{66}$/.test(destination)) {
2895
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'destination must be a 33-byte hex string');
2896
+ }
2897
+ if (!Number.isInteger(amountSats) || amountSats <= 0) {
2898
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'amountSats must be a positive integer');
2899
+ }
2900
+ const amountMsat = BigInt(amountSats) * 1000n;
2901
+ const route = this.node.queryRoute(Buffer.from(destination, 'hex'), amountMsat, types_2.DEFAULT_MIN_FINAL_CLTV_EXPIRY);
2902
+ if (!route) {
2903
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NO_ROUTE, 'No route found to destination');
2904
+ }
2905
+ if (maxFeeSats !== undefined &&
2906
+ route.totalFeeMsat > BigInt(maxFeeSats) * 1000n) {
2907
+ throw new errors_1.BeignetError('FEE_EXCEEDS_MAX', `Route fee ${route.totalFeeMsat} msat exceeds maximum ${maxFeeSats} sats`);
2908
+ }
2909
+ return {
2910
+ destination,
2911
+ amountSats,
2912
+ hops: routeHopsToJson(route),
2913
+ totalAmountMsat: route.totalAmountMsat.toString(),
2914
+ totalFeeMsat: route.totalFeeMsat.toString(),
2915
+ totalCltvDelta: route.totalCltvDelta,
2916
+ finalCltvExpiry: types_2.DEFAULT_MIN_FINAL_CLTV_EXPIRY
2917
+ };
2918
+ }
2919
+ sendToRoute(paymentHash, route, paymentSecret) {
2920
+ if (!/^[0-9a-fA-F]{64}$/.test(paymentHash)) {
2921
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'paymentHash must be a 32-byte hex string');
2922
+ }
2923
+ if (!route || !Array.isArray(route.hops) || route.hops.length === 0) {
2924
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'route.hops must be a non-empty array');
2925
+ }
2926
+ if (paymentSecret !== undefined &&
2927
+ !/^[0-9a-fA-F]{64}$/.test(paymentSecret)) {
2928
+ throw new errors_1.BeignetError(errors_1.BeignetErrorCode.INVALID_PARAMS, 'paymentSecret must be a 32-byte hex string');
2929
+ }
2930
+ this._checkDraining();
2931
+ const hops = jsonToRouteHops(route.hops);
2932
+ const finalHop = hops[hops.length - 1];
2933
+ try {
2934
+ const payment = this.node.sendPaymentToRoute({ hops }, Buffer.from(paymentHash, 'hex'), finalHop.outgoingCltvValue, paymentSecret ? Buffer.from(paymentSecret, 'hex') : undefined, finalHop.amountToForwardMsat);
2935
+ return this.toPaymentInfo(payment);
2936
+ }
2937
+ catch (err) {
2938
+ if (err instanceof errors_1.BeignetError)
2939
+ throw err;
2940
+ const code = err !== null && typeof err === 'object' && 'code' in err
2941
+ ? String(err.code)
2942
+ : 'PAYMENT_FAILED';
2943
+ throw new errors_1.BeignetError(code, err instanceof Error ? err.message : String(err));
2944
+ }
2945
+ }
2050
2946
  getReadyChannels() {
2051
2947
  return this.node
2052
2948
  .listChannels()
2053
- .filter((ch) => ch.state === types_2.ChannelState.NORMAL)
2949
+ .filter((ch) => ch.state === types_3.ChannelState.NORMAL)
2054
2950
  .map((ch) => this.toChannelInfo(ch));
2055
2951
  }
2056
2952
  canSend(amountSats) {
@@ -2059,7 +2955,7 @@ class BeignetNode extends events_1.EventEmitter {
2059
2955
  let bestAvailableMsat = 0n;
2060
2956
  let totalAvailableMsat = 0n;
2061
2957
  for (const ch of this.node.listChannels()) {
2062
- if (ch.state !== types_2.ChannelState.NORMAL)
2958
+ if (ch.state !== types_3.ChannelState.NORMAL)
2063
2959
  continue;
2064
2960
  const reserveMsat = ch.localReserveMsat ?? 0n;
2065
2961
  const available = ch.localBalanceMsat > reserveMsat
@@ -2083,7 +2979,7 @@ class BeignetNode extends events_1.EventEmitter {
2083
2979
  let bestAvailableMsat = 0n;
2084
2980
  let totalAvailableMsat = 0n;
2085
2981
  for (const ch of this.node.listChannels()) {
2086
- if (ch.state !== types_2.ChannelState.NORMAL)
2982
+ if (ch.state !== types_3.ChannelState.NORMAL)
2087
2983
  continue;
2088
2984
  const reserveMsat = ch.remoteReserveMsat ?? 0n;
2089
2985
  const available = ch.remoteBalanceMsat > reserveMsat
@@ -2277,6 +3173,86 @@ class BeignetNode extends events_1.EventEmitter {
2277
3173
  triggerBackup() {
2278
3174
  this.performScheduledBackup();
2279
3175
  }
3176
+ exportStaticChannelBackup() {
3177
+ const data = this.node.buildStaticChannelBackupData();
3178
+ const backup = {
3179
+ version: 1,
3180
+ network: data.network,
3181
+ createdAt: Date.now(),
3182
+ channels: data.channels
3183
+ };
3184
+ const seed = bip39.mnemonicToSeedSync(this.mnemonic);
3185
+ const encoded = (0, scb_1.encodeScb)(backup, seed);
3186
+ const scbPath = path.join(this.dataDir, 'channels.scb');
3187
+ const tmpPath = `${scbPath}.tmp`;
3188
+ fs.writeFileSync(tmpPath, encoded);
3189
+ fs.renameSync(tmpPath, scbPath);
3190
+ return { encoded, channelCount: data.channels.length, path: scbPath };
3191
+ }
3192
+ async restoreFromScb(encoded) {
3193
+ const seed = bip39.mnemonicToSeedSync(this.mnemonic);
3194
+ const backup = (0, scb_1.decodeScb)(encoded.trim(), seed);
3195
+ const expectedNetwork = this.toLnNetwork(this.networkName);
3196
+ if (backup.network !== expectedNetwork) {
3197
+ throw new errors_1.BeignetError('INVALID_PARAMS', `SCB network "${backup.network}" does not match this node's network "${expectedNetwork}"`);
3198
+ }
3199
+ const { recovering, skipped } = await this.node.recoverFromStaticChannelBackup(backup.channels);
3200
+ this.log('info', 'SCB restore processed', {
3201
+ channelCount: backup.channels.length,
3202
+ recovering: recovering.length,
3203
+ skipped: skipped.length
3204
+ });
3205
+ return { recovering, skipped, channelCount: backup.channels.length };
3206
+ }
3207
+ refreshStaticChannelBackup() {
3208
+ if (this.destroyed)
3209
+ return;
3210
+ try {
3211
+ const { encoded } = this.exportStaticChannelBackup();
3212
+ if (this.peerStorageEnabled) {
3213
+ const sent = this.node.distributePeerStorage(Buffer.from(encoded, 'utf8'));
3214
+ if (sent > 0) {
3215
+ this.log('debug', 'Pushed SCB to peer storage', { peers: sent });
3216
+ }
3217
+ }
3218
+ }
3219
+ catch (err) {
3220
+ this.log('warn', 'Static channel backup refresh failed', {
3221
+ error: err instanceof Error ? err.message : String(err)
3222
+ });
3223
+ }
3224
+ }
3225
+ handleRetrievedPeerStorage(peerPubkey, blob) {
3226
+ let backup;
3227
+ const encoded = blob.toString('utf8');
3228
+ try {
3229
+ const seed = bip39.mnemonicToSeedSync(this.mnemonic);
3230
+ backup = (0, scb_1.decodeScb)(encoded, seed);
3231
+ }
3232
+ catch {
3233
+ this.log('debug', 'Ignoring peer storage blob that is not our SCB', {
3234
+ fromPeer: peerPubkey
3235
+ });
3236
+ return;
3237
+ }
3238
+ if (this._peerRetrievedScb &&
3239
+ this._peerRetrievedScb.createdAt >= backup.createdAt) {
3240
+ return;
3241
+ }
3242
+ this._peerRetrievedScb = {
3243
+ encoded,
3244
+ createdAt: backup.createdAt,
3245
+ fromPeer: peerPubkey
3246
+ };
3247
+ this.log('info', 'Recovered SCB from peer storage', {
3248
+ fromPeer: peerPubkey,
3249
+ createdAt: backup.createdAt,
3250
+ channelCount: backup.channels.length
3251
+ });
3252
+ }
3253
+ getPeerRetrievedBackup() {
3254
+ return this._peerRetrievedScb;
3255
+ }
2280
3256
  getNodeUri(externalHost) {
2281
3257
  if (!this._listenPort)
2282
3258
  return null;
@@ -2366,6 +3342,8 @@ class BeignetNode extends events_1.EventEmitter {
2366
3342
  return wallet_2.EAvailableNetworks.testnet;
2367
3343
  case 'regtest':
2368
3344
  return wallet_2.EAvailableNetworks.regtest;
3345
+ case 'signet':
3346
+ return wallet_2.EAvailableNetworks.signet;
2369
3347
  default:
2370
3348
  return wallet_2.EAvailableNetworks.bitcoin;
2371
3349
  }
@@ -2373,13 +3351,15 @@ class BeignetNode extends events_1.EventEmitter {
2373
3351
  toLnNetwork(network) {
2374
3352
  switch (network) {
2375
3353
  case 'mainnet':
2376
- return types_1.Network.MAINNET;
3354
+ return types_2.Network.MAINNET;
2377
3355
  case 'testnet':
2378
- return types_1.Network.TESTNET;
3356
+ return types_2.Network.TESTNET;
2379
3357
  case 'regtest':
2380
- return types_1.Network.REGTEST;
3358
+ return types_2.Network.REGTEST;
3359
+ case 'signet':
3360
+ return types_2.Network.SIGNET;
2381
3361
  default:
2382
- return types_1.Network.MAINNET;
3362
+ return types_2.Network.MAINNET;
2383
3363
  }
2384
3364
  }
2385
3365
  toCoinType(network) {
@@ -2390,22 +3370,14 @@ class BeignetNode extends events_1.EventEmitter {
2390
3370
  return wallet_keys_1.LnCoinType.TESTNET;
2391
3371
  case 'regtest':
2392
3372
  return wallet_keys_1.LnCoinType.REGTEST;
3373
+ case 'signet':
3374
+ return wallet_keys_1.LnCoinType.SIGNET;
2393
3375
  default:
2394
3376
  return wallet_keys_1.LnCoinType.BITCOIN;
2395
3377
  }
2396
3378
  }
2397
3379
  getBitcoinNetwork() {
2398
- const bitcoin = require('bitcoinjs-lib');
2399
- switch (this.networkName) {
2400
- case 'mainnet':
2401
- return bitcoin.networks.bitcoin;
2402
- case 'testnet':
2403
- return bitcoin.networks.testnet;
2404
- case 'regtest':
2405
- return bitcoin.networks.regtest;
2406
- default:
2407
- return bitcoin.networks.bitcoin;
2408
- }
3380
+ return (0, helpers_1.getBitcoinJsNetwork)(this.toBeignetNetwork(this.networkName));
2409
3381
  }
2410
3382
  }
2411
3383
  exports.BeignetNode = BeignetNode;