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.
- package/README.md +284 -1
- package/dist/cli/beignet-node.js +1019 -47
- package/dist/cli/beignet-node.js.map +1 -1
- package/dist/cli/cli.js +1201 -16
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/config.js +21 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/daemon.js +342 -16
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/errors.js +2 -0
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/openapi.js +1207 -14
- package/dist/cli/openapi.js.map +1 -1
- package/dist/cli/restore.js +89 -0
- package/dist/cli/restore.js.map +1 -0
- package/dist/cli/wallet-storage.js +34 -0
- package/dist/cli/wallet-storage.js.map +1 -0
- package/dist/electrum/index.js +105 -10
- package/dist/electrum/index.js.map +1 -1
- package/dist/lightning/advisor/fee-tuner.js +42 -0
- package/dist/lightning/advisor/fee-tuner.js.map +1 -0
- package/dist/lightning/advisor/index.js +9 -1
- package/dist/lightning/advisor/index.js.map +1 -1
- package/dist/lightning/advisor/rebalance-planner.js +55 -0
- package/dist/lightning/advisor/rebalance-planner.js.map +1 -0
- package/dist/lightning/backup/index.js +18 -0
- package/dist/lightning/backup/index.js.map +1 -0
- package/dist/lightning/backup/scb.js +42 -0
- package/dist/lightning/backup/scb.js.map +1 -0
- package/dist/lightning/chain/chain-monitor.js +27 -0
- package/dist/lightning/chain/chain-monitor.js.map +1 -1
- package/dist/lightning/chain/output-resolver.js +55 -6
- package/dist/lightning/chain/output-resolver.js.map +1 -1
- package/dist/lightning/chain/types.js +1 -0
- package/dist/lightning/chain/types.js.map +1 -1
- package/dist/lightning/channel/channel-manager.js +64 -0
- package/dist/lightning/channel/channel-manager.js.map +1 -1
- package/dist/lightning/channel/channel-state.js.map +1 -1
- package/dist/lightning/channel/channel.js +124 -4
- package/dist/lightning/channel/channel.js.map +1 -1
- package/dist/lightning/channel/dual-funding.js +6 -5
- package/dist/lightning/channel/dual-funding.js.map +1 -1
- package/dist/lightning/channel/types.js +3 -1
- package/dist/lightning/channel/types.js.map +1 -1
- package/dist/lightning/channel/validation.js +3 -3
- package/dist/lightning/channel/validation.js.map +1 -1
- package/dist/lightning/crypto/index.js +1 -0
- package/dist/lightning/crypto/index.js.map +1 -1
- package/dist/lightning/crypto/message-signing.js +116 -0
- package/dist/lightning/crypto/message-signing.js.map +1 -0
- package/dist/lightning/crypto/xchacha20poly1305.js +77 -0
- package/dist/lightning/crypto/xchacha20poly1305.js.map +1 -0
- package/dist/lightning/features/flags.js +1 -0
- package/dist/lightning/features/flags.js.map +1 -1
- package/dist/lightning/gossip/messages.js +37 -1
- package/dist/lightning/gossip/messages.js.map +1 -1
- package/dist/lightning/index.js +2 -1
- package/dist/lightning/index.js.map +1 -1
- package/dist/lightning/keys/wallet-keys.js +1 -0
- package/dist/lightning/keys/wallet-keys.js.map +1 -1
- package/dist/lightning/message/index.js +1 -0
- package/dist/lightning/message/index.js.map +1 -1
- package/dist/lightning/message/peer-storage.js +43 -0
- package/dist/lightning/message/peer-storage.js.map +1 -0
- package/dist/lightning/message/types.js +2 -0
- package/dist/lightning/message/types.js.map +1 -1
- package/dist/lightning/node/lightning-node.js +1583 -235
- package/dist/lightning/node/lightning-node.js.map +1 -1
- package/dist/lightning/node/types.js +18 -1
- package/dist/lightning/node/types.js.map +1 -1
- package/dist/lightning/offer/offer-manager.js +2 -5
- package/dist/lightning/offer/offer-manager.js.map +1 -1
- package/dist/lightning/offer/tlv.js +3 -1
- package/dist/lightning/offer/tlv.js.map +1 -1
- package/dist/lightning/storage/encryption.js +67 -0
- package/dist/lightning/storage/encryption.js.map +1 -0
- package/dist/lightning/storage/index.js +1 -0
- package/dist/lightning/storage/index.js.map +1 -1
- package/dist/lightning/storage/serialization.js +6 -2
- package/dist/lightning/storage/serialization.js.map +1 -1
- package/dist/lightning/storage/sqlite-storage.js +387 -33
- package/dist/lightning/storage/sqlite-storage.js.map +1 -1
- package/dist/lightning/watchtower/blob.js +135 -0
- package/dist/lightning/watchtower/blob.js.map +1 -0
- package/dist/lightning/watchtower/index.js +55 -0
- package/dist/lightning/watchtower/index.js.map +1 -0
- package/dist/lightning/watchtower/justice.js +182 -0
- package/dist/lightning/watchtower/justice.js.map +1 -0
- package/dist/lightning/watchtower/tower-connection.js +208 -0
- package/dist/lightning/watchtower/tower-connection.js.map +1 -0
- package/dist/lightning/watchtower/types.js +3 -0
- package/dist/lightning/watchtower/types.js.map +1 -0
- package/dist/lightning/watchtower/watchtower-client.js +402 -0
- package/dist/lightning/watchtower/watchtower-client.js.map +1 -0
- package/dist/lightning/watchtower/wtwire.js +201 -0
- package/dist/lightning/watchtower/wtwire.js.map +1 -0
- package/dist/shapes/electrum.js +32 -1
- package/dist/shapes/electrum.js.map +1 -1
- package/dist/shapes/wallet.js +11 -1
- package/dist/shapes/wallet.js.map +1 -1
- package/dist/transaction/index.js +163 -15
- package/dist/transaction/index.js.map +1 -1
- package/dist/types/cli/beignet-node.d.ts +144 -3
- package/dist/types/cli/daemon.d.ts +2 -0
- package/dist/types/cli/errors.d.ts +2 -0
- package/dist/types/cli/restore.d.ts +10 -0
- package/dist/types/cli/types.d.ts +228 -1
- package/dist/types/cli/wallet-storage.d.ts +3 -0
- package/dist/types/electrum/index.d.ts +12 -0
- package/dist/types/electrum.js +1 -0
- package/dist/types/electrum.js.map +1 -1
- package/dist/types/lightning/advisor/fee-tuner.d.ts +20 -0
- package/dist/types/lightning/advisor/index.d.ts +4 -0
- package/dist/types/lightning/advisor/rebalance-planner.d.ts +12 -0
- package/dist/types/lightning/backup/index.d.ts +1 -0
- package/dist/types/lightning/backup/scb.d.ts +24 -0
- package/dist/types/lightning/chain/chain-monitor.d.ts +1 -0
- package/dist/types/lightning/chain/types.d.ts +1 -0
- package/dist/types/lightning/channel/channel-manager.d.ts +7 -0
- package/dist/types/lightning/channel/channel-state.d.ts +2 -0
- package/dist/types/lightning/channel/channel.d.ts +6 -0
- package/dist/types/lightning/channel/dual-funding.d.ts +2 -1
- package/dist/types/lightning/channel/types.d.ts +2 -0
- package/dist/types/lightning/channel/validation.d.ts +1 -1
- package/dist/types/lightning/crypto/index.d.ts +1 -0
- package/dist/types/lightning/crypto/message-signing.d.ts +9 -0
- package/dist/types/lightning/crypto/xchacha20poly1305.d.ts +3 -0
- package/dist/types/lightning/features/flags.d.ts +1 -0
- package/dist/types/lightning/gossip/messages.d.ts +4 -0
- package/dist/types/lightning/index.d.ts +1 -0
- package/dist/types/lightning/keys/wallet-keys.d.ts +2 -1
- package/dist/types/lightning/message/index.d.ts +1 -0
- package/dist/types/lightning/message/peer-storage.d.ts +9 -0
- package/dist/types/lightning/message/types.d.ts +2 -0
- package/dist/types/lightning/node/lightning-node.d.ts +116 -4
- package/dist/types/lightning/node/types.d.ts +62 -1
- package/dist/types/lightning/storage/encryption.d.ts +11 -0
- package/dist/types/lightning/storage/index.d.ts +1 -0
- package/dist/types/lightning/storage/serialization.d.ts +2 -0
- package/dist/types/lightning/storage/sqlite-storage.d.ts +43 -3
- package/dist/types/lightning/storage/types.d.ts +62 -0
- package/dist/types/lightning/watchtower/blob.d.ts +32 -0
- package/dist/types/lightning/watchtower/index.d.ts +9 -0
- package/dist/types/lightning/watchtower/justice.d.ts +29 -0
- package/dist/types/lightning/watchtower/tower-connection.d.ts +34 -0
- package/dist/types/lightning/watchtower/types.d.ts +44 -0
- package/dist/types/lightning/watchtower/watchtower-client.d.ts +67 -0
- package/dist/types/lightning/watchtower/wtwire.d.ts +90 -0
- package/dist/types/shapes/electrum.d.ts +3 -1
- package/dist/types/transaction/index.d.ts +6 -0
- package/dist/types/transaction.js.map +1 -1
- package/dist/types/types/electrum.d.ts +3 -2
- package/dist/types/types/transaction.d.ts +1 -1
- package/dist/types/types/wallet.d.ts +78 -4
- package/dist/types/utils/descriptors.d.ts +2 -0
- package/dist/types/utils/fees.d.ts +3 -0
- package/dist/types/utils/helpers.d.ts +3 -1
- package/dist/types/utils/index.d.ts +5 -0
- package/dist/types/utils/multisig.d.ts +29 -0
- package/dist/types/utils/transaction.d.ts +7 -0
- package/dist/types/utils/wallet-storage-encryption.d.ts +3 -0
- package/dist/types/utils/wallet.d.ts +1 -1
- package/dist/types/utils/watch-only.d.ts +12 -0
- package/dist/types/wallet/constants.d.ts +1 -0
- package/dist/types/wallet/index.d.ts +61 -4
- package/dist/types/wallet.js +3 -0
- package/dist/types/wallet.js.map +1 -1
- package/dist/utils/derivation-path.js +8 -7
- package/dist/utils/derivation-path.js.map +1 -1
- package/dist/utils/descriptors.js +56 -0
- package/dist/utils/descriptors.js.map +1 -0
- package/dist/utils/electrum.js +10 -2
- package/dist/utils/electrum.js.map +1 -1
- package/dist/utils/fees.js +19 -0
- package/dist/utils/fees.js.map +1 -0
- package/dist/utils/helpers.js +22 -5
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/multisig.js +90 -0
- package/dist/utils/multisig.js.map +1 -0
- package/dist/utils/transaction.js +30 -3
- package/dist/utils/transaction.js.map +1 -1
- package/dist/utils/wallet-storage-encryption.js +45 -0
- package/dist/utils/wallet-storage-encryption.js.map +1 -0
- package/dist/utils/wallet.js +3 -1
- package/dist/utils/wallet.js.map +1 -1
- package/dist/utils/watch-only.js +102 -0
- package/dist/utils/watch-only.js.map +1 -0
- package/dist/wallet/constants.js +2 -1
- package/dist/wallet/constants.js.map +1 -1
- package/dist/wallet/index.js +787 -30
- package/dist/wallet/index.js.map +1 -1
- package/docs/ROADMAP.md +292 -0
- package/package.json +1 -1
|
@@ -30,6 +30,7 @@ exports.LightningNode = void 0;
|
|
|
30
30
|
const events_1 = require("events");
|
|
31
31
|
const crypto_1 = __importDefault(require("crypto"));
|
|
32
32
|
const ecdh_1 = require("../crypto/ecdh");
|
|
33
|
+
const message_signing_1 = require("../crypto/message-signing");
|
|
33
34
|
const blinded_path_1 = require("../onion/blinded-path");
|
|
34
35
|
const channel_manager_1 = require("../channel/channel-manager");
|
|
35
36
|
const channel_1 = require("../channel/channel");
|
|
@@ -40,6 +41,7 @@ const network_graph_1 = require("../gossip/network-graph");
|
|
|
40
41
|
const pathfinding_1 = require("../gossip/pathfinding");
|
|
41
42
|
const rapid_sync_1 = require("../gossip/rapid-sync");
|
|
42
43
|
const mission_control_1 = require("../gossip/mission-control");
|
|
44
|
+
const types_2 = require("../gossip/types");
|
|
43
45
|
const messages_1 = require("../gossip/messages");
|
|
44
46
|
const gossip_queries_1 = require("../gossip/gossip-queries");
|
|
45
47
|
const gossip_sync_1 = require("../gossip/gossip-sync");
|
|
@@ -48,20 +50,22 @@ const construct_1 = require("../onion/construct");
|
|
|
48
50
|
const process_1 = require("../onion/process");
|
|
49
51
|
const sphinx_crypto_1 = require("../onion/sphinx-crypto");
|
|
50
52
|
const failures_1 = require("../onion/failures");
|
|
51
|
-
const
|
|
53
|
+
const types_3 = require("../onion/types");
|
|
52
54
|
const encode_1 = require("../invoice/encode");
|
|
53
55
|
const decode_1 = require("../invoice/decode");
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
56
|
+
const types_4 = require("../invoice/types");
|
|
57
|
+
const types_5 = require("../message/types");
|
|
58
|
+
const peer_storage_1 = require("../message/peer-storage");
|
|
59
|
+
const types_6 = require("./types");
|
|
57
60
|
const validation_2 = require("../validation");
|
|
58
61
|
const bitcoin = __importStar(require("bitcoinjs-lib"));
|
|
59
62
|
const ecc = __importStar(require("@bitcoinerlab/secp256k1"));
|
|
60
63
|
const flags_1 = require("../features/flags");
|
|
61
64
|
const chain_watcher_1 = require("../chain/chain-watcher");
|
|
62
65
|
const sweep_1 = require("../chain/sweep");
|
|
63
|
-
const
|
|
66
|
+
const types_7 = require("../chain/types");
|
|
64
67
|
const chain_monitor_1 = require("../chain/chain-monitor");
|
|
68
|
+
const watchtower_1 = require("../watchtower");
|
|
65
69
|
const wallet_keys_1 = require("../keys/wallet-keys");
|
|
66
70
|
const bip32Lib = __importStar(require("bip32"));
|
|
67
71
|
const bip39 = __importStar(require("bip39"));
|
|
@@ -69,7 +73,8 @@ const shachain_1 = require("../keys/shachain");
|
|
|
69
73
|
const derivation_1 = require("../keys/derivation");
|
|
70
74
|
const funding_1 = require("../script/funding");
|
|
71
75
|
const funding_taproot_1 = require("../script/funding-taproot");
|
|
72
|
-
const
|
|
76
|
+
const types_8 = require("../channel/types");
|
|
77
|
+
const channel_state_1 = require("../channel/channel-state");
|
|
73
78
|
const commitment_builder_1 = require("../channel/commitment-builder");
|
|
74
79
|
const signer_1 = require("../keys/signer");
|
|
75
80
|
const bootstrap_1 = require("../bootstrap");
|
|
@@ -80,6 +85,8 @@ const rate_limiter_1 = require("./rate-limiter");
|
|
|
80
85
|
const liquidity_advisor_1 = require("../advisor/liquidity-advisor");
|
|
81
86
|
const fee_advisor_1 = require("../advisor/fee-advisor");
|
|
82
87
|
const channel_suggestions_1 = require("../advisor/channel-suggestions");
|
|
88
|
+
const rebalance_planner_1 = require("../advisor/rebalance-planner");
|
|
89
|
+
const fee_tuner_1 = require("../advisor/fee-tuner");
|
|
83
90
|
bitcoin.initEccLib(ecc);
|
|
84
91
|
const GOSSIP_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
85
92
|
const HELD_HTLC_EXPIRY_MARGIN = 18;
|
|
@@ -100,6 +107,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
100
107
|
this.chainWatcher = null;
|
|
101
108
|
this._chainWatcherEventsWired = false;
|
|
102
109
|
this.currentBlockHeight = 0;
|
|
110
|
+
this.channelPolicies = new Map();
|
|
103
111
|
this.gossipSyncManagers = new Map();
|
|
104
112
|
this._ownChannelGossip = new Map();
|
|
105
113
|
this.pendingMppPayments = new Map();
|
|
@@ -131,10 +139,19 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
131
139
|
this.liquidityAdvisor = new liquidity_advisor_1.LiquidityAdvisor();
|
|
132
140
|
this.feeAdvisor = new fee_advisor_1.FeeAdvisor();
|
|
133
141
|
this.channelSuggestions = new channel_suggestions_1.ChannelSuggestions();
|
|
142
|
+
this.autoRebalanceTimer = null;
|
|
143
|
+
this.autoTuneFeesTimer = null;
|
|
144
|
+
this.rebalanceBudgetDay = null;
|
|
145
|
+
this.rebalanceRunInFlight = false;
|
|
146
|
+
this.watchtowerClient = null;
|
|
147
|
+
this.peerStorageBlobs = new Map();
|
|
148
|
+
this.peerStorageLastAccepted = new Map();
|
|
149
|
+
this.ourPeerStorageBlob = null;
|
|
150
|
+
this.retrievedPeerStorage = new Map();
|
|
134
151
|
this.setMaxListeners(50);
|
|
135
152
|
this.nodePrivkey = config.nodePrivateKey;
|
|
136
153
|
this.nodeId = (0, ecdh_1.getPublicKey)(config.nodePrivateKey).toString('hex');
|
|
137
|
-
this.network = config.network ||
|
|
154
|
+
this.network = config.network || types_4.Network.REGTEST;
|
|
138
155
|
this.acceptableChainHashes = config.chainHashes ?? [];
|
|
139
156
|
this.storage = config.storage || null;
|
|
140
157
|
this.resourceConfig = {
|
|
@@ -158,6 +175,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
158
175
|
this.revocationBasepointSecret = config.revocationBasepointSecret;
|
|
159
176
|
this.paymentBasepointSecret = config.paymentBasepointSecret;
|
|
160
177
|
this.feeEstimator = config.feeEstimator || null;
|
|
178
|
+
this.socks5Proxy = config.socks5Proxy ?? null;
|
|
179
|
+
this.initWatchtowerClient(config.watchtowers ?? []);
|
|
161
180
|
this.missionControl = new mission_control_1.MissionControl();
|
|
162
181
|
this.maxPaymentRetries = config.maxPaymentRetries ?? 3;
|
|
163
182
|
this.maxTotalInFlightHtlcs = config.maxTotalInFlightHtlcs ?? 1000;
|
|
@@ -170,6 +189,15 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
170
189
|
!localFeatures.hasFeature(flags_1.Feature.ANCHOR_ZERO_FEE_HTLC)) {
|
|
171
190
|
localFeatures.setOptional(flags_1.Feature.ANCHOR_ZERO_FEE_HTLC);
|
|
172
191
|
}
|
|
192
|
+
this.largeChannels = config.largeChannels ?? false;
|
|
193
|
+
if (this.largeChannels) {
|
|
194
|
+
localFeatures.setOptional(flags_1.Feature.LARGE_CHANNELS);
|
|
195
|
+
}
|
|
196
|
+
this.peerStorageEnabled = config.peerStorageEnabled ?? true;
|
|
197
|
+
if (!this.peerStorageEnabled) {
|
|
198
|
+
localFeatures.clearBit(flags_1.Feature.PROVIDE_STORAGE);
|
|
199
|
+
localFeatures.clearBit(flags_1.Feature.PROVIDE_STORAGE + 1);
|
|
200
|
+
}
|
|
173
201
|
this.channelManager = new channel_manager_1.ChannelManager({
|
|
174
202
|
localFeatures,
|
|
175
203
|
localConfig: config.channelConfig,
|
|
@@ -184,7 +212,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
184
212
|
preferTaproot: config.preferTaproot,
|
|
185
213
|
chainHash: config.chainHashes?.[0],
|
|
186
214
|
nodePrivateKey: config.nodePrivateKey,
|
|
187
|
-
channelKeyDeriver: config.channelKeyDeriver
|
|
215
|
+
channelKeyDeriver: config.channelKeyDeriver,
|
|
216
|
+
largeChannels: this.largeChannels
|
|
188
217
|
});
|
|
189
218
|
this.channelManager.setFundingProvider(this.fundingProvider);
|
|
190
219
|
this.graph = new network_graph_1.NetworkGraph();
|
|
@@ -211,6 +240,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
211
240
|
this.channelManager.attachToPeerManager(this.peerManager);
|
|
212
241
|
this.registerGossipHandlers();
|
|
213
242
|
this.registerOnionMessageHandler();
|
|
243
|
+
this.registerPeerStorageHandlers();
|
|
214
244
|
this.wirePeerManagerEvents();
|
|
215
245
|
}
|
|
216
246
|
if (config.chainBackend) {
|
|
@@ -269,6 +299,35 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
269
299
|
this.missionControlTimer.unref();
|
|
270
300
|
}
|
|
271
301
|
}
|
|
302
|
+
this.autoRebalanceConfig = config.autoRebalance ?? {};
|
|
303
|
+
this.autoTuneFeesConfig = config.autoTuneFees ?? {};
|
|
304
|
+
if (this.autoRebalanceConfig.enabled === true) {
|
|
305
|
+
this.autoRebalanceTimer = setInterval(() => {
|
|
306
|
+
this.executeRebalanceRecommendations().catch((err) => {
|
|
307
|
+
this.emitStructuredLog('payment', 'auto_rebalance_failed', {
|
|
308
|
+
error: err instanceof Error ? err.message : String(err)
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}, this.autoRebalanceConfig.intervalMs ?? 3600000);
|
|
312
|
+
if (this.autoRebalanceTimer.unref) {
|
|
313
|
+
this.autoRebalanceTimer.unref();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (this.autoTuneFeesConfig.enabled === true) {
|
|
317
|
+
this.autoTuneFeesTimer = setInterval(() => {
|
|
318
|
+
try {
|
|
319
|
+
this.runFeeTuneOnce();
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
this.emitStructuredLog('fee', 'auto_tune_failed', {
|
|
323
|
+
error: err instanceof Error ? err.message : String(err)
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}, this.autoTuneFeesConfig.intervalMs ?? 21600000);
|
|
327
|
+
if (this.autoTuneFeesTimer.unref) {
|
|
328
|
+
this.autoTuneFeesTimer.unref();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
272
331
|
this.graphPruneTimer = setInterval(() => {
|
|
273
332
|
this.pruneStaleGossipWithStorage();
|
|
274
333
|
}, 3600000);
|
|
@@ -332,13 +391,35 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
332
391
|
for (const { key, secret } of this.storage.loadAllHtlcSharedSecrets()) {
|
|
333
392
|
this.receivedHtlcSharedSecrets.set(key, secret);
|
|
334
393
|
}
|
|
394
|
+
if (this.storage.loadAllChannelPolicies) {
|
|
395
|
+
for (const { channelId, policy } of this.storage.loadAllChannelPolicies()) {
|
|
396
|
+
const override = {};
|
|
397
|
+
if (policy.feeBaseMsat !== undefined)
|
|
398
|
+
override.feeBaseMsat = policy.feeBaseMsat;
|
|
399
|
+
if (policy.feeProportionalMillionths !== undefined)
|
|
400
|
+
override.feeProportionalMillionths = policy.feeProportionalMillionths;
|
|
401
|
+
if (policy.cltvExpiryDelta !== undefined)
|
|
402
|
+
override.cltvExpiryDelta = policy.cltvExpiryDelta;
|
|
403
|
+
if (policy.htlcMinimumMsat !== undefined)
|
|
404
|
+
override.htlcMinimumMsat = BigInt(policy.htlcMinimumMsat);
|
|
405
|
+
if (policy.htlcMaximumMsat !== undefined)
|
|
406
|
+
override.htlcMaximumMsat = BigInt(policy.htlcMaximumMsat);
|
|
407
|
+
this.channelPolicies.set(channelId, override);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
335
410
|
for (const { paymentHashHex, invoice } of this.storage.loadAllInvoices()) {
|
|
336
411
|
if (invoice.createdAt > 10000000000) {
|
|
337
412
|
invoice.createdAt = Math.floor(invoice.createdAt / 1000);
|
|
338
413
|
}
|
|
339
414
|
this.invoices.set(paymentHashHex, invoice);
|
|
340
415
|
if (invoice.hold) {
|
|
341
|
-
|
|
416
|
+
if (invoice.cancelledAt) {
|
|
417
|
+
this.preimages.delete(paymentHashHex);
|
|
418
|
+
this.paymentSecrets.delete(paymentHashHex);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
this.heldInvoiceHashes.add(paymentHashHex);
|
|
422
|
+
}
|
|
342
423
|
}
|
|
343
424
|
}
|
|
344
425
|
const heldJson = this.storage.loadMetadata('held_htlcs');
|
|
@@ -408,10 +489,11 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
408
489
|
if (this.feeEstimator) {
|
|
409
490
|
this.feeEstimator
|
|
410
491
|
.estimateFee(6)
|
|
411
|
-
.then((
|
|
492
|
+
.then((rawSatPerVbyte) => {
|
|
493
|
+
const satPerVbyte = this.clampEstimatedFeeRate(rawSatPerVbyte);
|
|
412
494
|
if (satPerVbyte > 0) {
|
|
413
495
|
this.feeAdvisor.recordSample(satPerVbyte);
|
|
414
|
-
const feeratePerKw = Math.max((0,
|
|
496
|
+
const feeratePerKw = Math.max((0, types_7.satPerVbyteToSatPerKw)(satPerVbyte), types_7.MIN_FEERATE_PER_KW);
|
|
415
497
|
for (const { channelId: monitorChannelId } of monitors) {
|
|
416
498
|
const m = this.channelManager.getMonitor(Buffer.from(monitorChannelId, 'hex'));
|
|
417
499
|
if (m && typeof m.updateFeeRate === 'function') {
|
|
@@ -503,6 +585,9 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
503
585
|
}
|
|
504
586
|
}
|
|
505
587
|
wireChannelManagerEvents() {
|
|
588
|
+
this.channelManager.on('watchtower:backup', (channelId, _peerPubkey, perCommitmentSecret, revokedTx) => {
|
|
589
|
+
this.backupRevokedStateToTowers(channelId, perCommitmentSecret, revokedTx);
|
|
590
|
+
});
|
|
506
591
|
this.channelManager.on('channel:ready', (channelId) => {
|
|
507
592
|
this.registerChannelAliases(channelId);
|
|
508
593
|
this.persistChannel(channelId);
|
|
@@ -514,6 +599,27 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
514
599
|
this.emitReady();
|
|
515
600
|
this.triggerPendingAnnouncementSigning(channelId);
|
|
516
601
|
});
|
|
602
|
+
this.channelManager.on('channel:opening', (channelId, fundingTxid) => {
|
|
603
|
+
this.emit('channel:opening', { channelId, fundingTxid });
|
|
604
|
+
this.emitStructuredLog('channel', 'opening', {
|
|
605
|
+
channelId: channelId.toString('hex'),
|
|
606
|
+
fundingTxid: fundingTxid.toString('hex')
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
this.channelManager.on('channel:pending-close', (channelId, initiator) => {
|
|
610
|
+
this.emit('channel:pending-close', { channelId, initiator });
|
|
611
|
+
this.emitStructuredLog('channel', 'pending_close', {
|
|
612
|
+
channelId: channelId.toString('hex'),
|
|
613
|
+
initiator
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
this.channelManager.on('channel:force-closing', (channelId, initiator) => {
|
|
617
|
+
this.emit('channel:force-closing', { channelId, initiator });
|
|
618
|
+
this.emitStructuredLog('channel', 'force_closing', {
|
|
619
|
+
channelId: channelId.toString('hex'),
|
|
620
|
+
initiator
|
|
621
|
+
});
|
|
622
|
+
});
|
|
517
623
|
this.channelManager.on('channel:closed', (channelId) => {
|
|
518
624
|
this.persistChannel(channelId);
|
|
519
625
|
this.emit('channel:closed', { channelId });
|
|
@@ -558,9 +664,11 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
558
664
|
});
|
|
559
665
|
this.channelManager.on('htlc:fulfilled', (channelId, htlcId, preimage) => {
|
|
560
666
|
this.handleHtlcFulfilled(channelId, htlcId, preimage);
|
|
667
|
+
this.emit('htlc:fulfilled', { channelId, htlcId });
|
|
561
668
|
});
|
|
562
669
|
this.channelManager.on('htlc:failed', (channelId, htlcId, reason) => {
|
|
563
670
|
this.handleHtlcFailed(channelId, htlcId, reason);
|
|
671
|
+
this.emit('htlc:failed', { channelId, htlcId });
|
|
564
672
|
});
|
|
565
673
|
this.channelManager.on('error', (channelId, message) => {
|
|
566
674
|
const err = {
|
|
@@ -594,13 +702,16 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
594
702
|
this.safeStorage(() => this.storage.saveChainMonitor(channelIdHex, monitor.getFullState()), 'saveChainMonitor');
|
|
595
703
|
});
|
|
596
704
|
this.channelManager.on('announcement:ready', (channelId, channelAnnouncement, channelUpdate) => {
|
|
597
|
-
let signedChannelUpdate = channelUpdate;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
705
|
+
let signedChannelUpdate = this.refreshChannelUpdate(channelUpdate, Math.floor(Date.now() / 1000), channelId);
|
|
706
|
+
if (!signedChannelUpdate) {
|
|
707
|
+
signedChannelUpdate = channelUpdate;
|
|
708
|
+
try {
|
|
709
|
+
const sig = (0, validation_1.signChannelUpdate)(channelUpdate, this.nodePrivkey);
|
|
710
|
+
signedChannelUpdate = Buffer.from(channelUpdate);
|
|
711
|
+
sig.copy(signedChannelUpdate, 0);
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
}
|
|
604
715
|
}
|
|
605
716
|
try {
|
|
606
717
|
const annMsg = (0, messages_1.decodeChannelAnnouncementMessage)(channelAnnouncement);
|
|
@@ -697,14 +808,14 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
697
808
|
if (!this.peerManager)
|
|
698
809
|
return;
|
|
699
810
|
const gossipTypes = [
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
811
|
+
types_5.MessageType.CHANNEL_ANNOUNCEMENT,
|
|
812
|
+
types_5.MessageType.NODE_ANNOUNCEMENT,
|
|
813
|
+
types_5.MessageType.CHANNEL_UPDATE,
|
|
814
|
+
types_5.MessageType.QUERY_CHANNEL_RANGE,
|
|
815
|
+
types_5.MessageType.REPLY_CHANNEL_RANGE,
|
|
816
|
+
types_5.MessageType.QUERY_SHORT_CHANNEL_IDS,
|
|
817
|
+
types_5.MessageType.REPLY_SHORT_CHANNEL_IDS_END,
|
|
818
|
+
types_5.MessageType.GOSSIP_TIMESTAMP_FILTER
|
|
708
819
|
];
|
|
709
820
|
for (const type of gossipTypes) {
|
|
710
821
|
this.peerManager.onMessage(type, (pubkey, msgType, payload) => {
|
|
@@ -712,10 +823,149 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
712
823
|
});
|
|
713
824
|
}
|
|
714
825
|
}
|
|
826
|
+
registerPeerStorageHandlers() {
|
|
827
|
+
if (!this.peerManager)
|
|
828
|
+
return;
|
|
829
|
+
this.peerManager.onMessage(types_5.MessageType.PEER_STORAGE, (pubkey, _t, payload) => {
|
|
830
|
+
this.handlePeerStorageMessage(pubkey, payload);
|
|
831
|
+
});
|
|
832
|
+
this.peerManager.onMessage(types_5.MessageType.PEER_STORAGE_RETRIEVAL, (pubkey, _t, payload) => {
|
|
833
|
+
this.handlePeerStorageRetrievalMessage(pubkey, payload);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
handlePeerStorageMessage(pubkey, payload) {
|
|
837
|
+
if (!this.peerStorageEnabled)
|
|
838
|
+
return;
|
|
839
|
+
let blob;
|
|
840
|
+
try {
|
|
841
|
+
blob = (0, peer_storage_1.decodePeerStorageMessage)(payload).blob;
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
this.emitStructuredLog('peer', 'peer_storage_invalid', {
|
|
845
|
+
pubkey,
|
|
846
|
+
error: err instanceof Error ? err.message : String(err)
|
|
847
|
+
});
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (!this.peerQualifiesForStorage(pubkey)) {
|
|
851
|
+
this.emitStructuredLog('peer', 'peer_storage_rejected', {
|
|
852
|
+
pubkey,
|
|
853
|
+
reason: 'no channel and not trusted'
|
|
854
|
+
});
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const now = Date.now();
|
|
858
|
+
const last = this.peerStorageLastAccepted.get(pubkey);
|
|
859
|
+
if (last !== undefined &&
|
|
860
|
+
now - last < LightningNode.PEER_STORAGE_MIN_INTERVAL_MS) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
this.peerStorageLastAccepted.set(pubkey, now);
|
|
864
|
+
this.peerStorageBlobs.set(pubkey, { blob, receivedAt: now });
|
|
865
|
+
if (this.storage?.savePeerStorageBlob) {
|
|
866
|
+
this.safeStorage(() => this.storage.savePeerStorageBlob(pubkey, blob, now), 'savePeerStorageBlob');
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
handlePeerStorageRetrievalMessage(pubkey, payload) {
|
|
870
|
+
if (!this.peerStorageEnabled)
|
|
871
|
+
return;
|
|
872
|
+
let blob;
|
|
873
|
+
try {
|
|
874
|
+
blob = (0, peer_storage_1.decodePeerStorageRetrievalMessage)(payload).blob;
|
|
875
|
+
}
|
|
876
|
+
catch (err) {
|
|
877
|
+
this.emitStructuredLog('peer', 'peer_storage_retrieval_invalid', {
|
|
878
|
+
pubkey,
|
|
879
|
+
error: err instanceof Error ? err.message : String(err)
|
|
880
|
+
});
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.retrievedPeerStorage.set(pubkey, { blob, receivedAt: Date.now() });
|
|
884
|
+
this.emit('peer_storage:retrieved', pubkey, blob);
|
|
885
|
+
}
|
|
886
|
+
peerQualifiesForStorage(pubkey) {
|
|
887
|
+
if (this.channelManager.isTrustedPeer(pubkey))
|
|
888
|
+
return true;
|
|
889
|
+
for (const channel of this.channelManager.listChannels()) {
|
|
890
|
+
const channelId = channel.getChannelId();
|
|
891
|
+
if (!channelId)
|
|
892
|
+
continue;
|
|
893
|
+
if (this.channelManager.getPeerForChannel(channelId) !== pubkey)
|
|
894
|
+
continue;
|
|
895
|
+
if (channel.getState() !== types_1.ChannelState.CLOSED)
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
sendPeerStorageOnConnect(pubkey) {
|
|
901
|
+
if (!this.peerStorageEnabled || !this.peerManager)
|
|
902
|
+
return;
|
|
903
|
+
let held = this.peerStorageBlobs.get(pubkey);
|
|
904
|
+
if (!held && this.storage?.loadPeerStorageBlob) {
|
|
905
|
+
try {
|
|
906
|
+
const loaded = this.storage.loadPeerStorageBlob(pubkey);
|
|
907
|
+
if (loaded) {
|
|
908
|
+
held = loaded;
|
|
909
|
+
this.peerStorageBlobs.set(pubkey, loaded);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch (err) {
|
|
913
|
+
this.emitStructuredLog('peer', 'peer_storage_load_failed', {
|
|
914
|
+
pubkey,
|
|
915
|
+
error: err instanceof Error ? err.message : String(err)
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
if (held) {
|
|
921
|
+
this.peerManager.sendToPeer(pubkey, types_5.MessageType.PEER_STORAGE_RETRIEVAL, (0, peer_storage_1.encodePeerStorageRetrievalMessage)({ blob: held.blob }));
|
|
922
|
+
}
|
|
923
|
+
if (this.ourPeerStorageBlob && this.peerAdvertisesPeerStorage(pubkey)) {
|
|
924
|
+
this.peerManager.sendToPeer(pubkey, types_5.MessageType.PEER_STORAGE, (0, peer_storage_1.encodePeerStorageMessage)({ blob: this.ourPeerStorageBlob }));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
peerAdvertisesPeerStorage(pubkey) {
|
|
931
|
+
const init = this.peerManager?.getPeer(pubkey)?.getRemoteInit();
|
|
932
|
+
return init ? init.features.hasFeature(flags_1.Feature.PROVIDE_STORAGE) : false;
|
|
933
|
+
}
|
|
934
|
+
distributePeerStorage(blob) {
|
|
935
|
+
if (blob.length > peer_storage_1.PEER_STORAGE_MAX_BYTES) {
|
|
936
|
+
throw new Error(`peer storage blob too large: ${blob.length} > ${peer_storage_1.PEER_STORAGE_MAX_BYTES} bytes`);
|
|
937
|
+
}
|
|
938
|
+
if (!this.peerStorageEnabled)
|
|
939
|
+
return 0;
|
|
940
|
+
this.ourPeerStorageBlob = Buffer.from(blob);
|
|
941
|
+
if (!this.peerManager)
|
|
942
|
+
return 0;
|
|
943
|
+
const payload = (0, peer_storage_1.encodePeerStorageMessage)({ blob: this.ourPeerStorageBlob });
|
|
944
|
+
let sent = 0;
|
|
945
|
+
for (const peer of this.peerManager.listPeers()) {
|
|
946
|
+
if (!this.peerAdvertisesPeerStorage(peer.pubkey))
|
|
947
|
+
continue;
|
|
948
|
+
try {
|
|
949
|
+
this.peerManager.sendToPeer(peer.pubkey, types_5.MessageType.PEER_STORAGE, payload);
|
|
950
|
+
sent++;
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return sent;
|
|
956
|
+
}
|
|
957
|
+
getRetrievedPeerStorage() {
|
|
958
|
+
return [...this.retrievedPeerStorage.entries()].map(([peerPubkey, { blob, receivedAt }]) => ({
|
|
959
|
+
peerPubkey,
|
|
960
|
+
blob: Buffer.from(blob),
|
|
961
|
+
receivedAt
|
|
962
|
+
}));
|
|
963
|
+
}
|
|
715
964
|
wirePeerManagerEvents() {
|
|
716
965
|
if (!this.peerManager)
|
|
717
966
|
return;
|
|
718
967
|
this.peerManager.on('peer:connect', (pubkey) => {
|
|
968
|
+
this.sendPeerStorageOnConnect(pubkey);
|
|
719
969
|
this.channelManager.handlePeerReconnected(pubkey);
|
|
720
970
|
this.sendOwnGossipTo(pubkey);
|
|
721
971
|
if (this.peerManager) {
|
|
@@ -802,17 +1052,19 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
802
1052
|
if (!state.remoteBasepoints)
|
|
803
1053
|
return;
|
|
804
1054
|
const networkMap = {
|
|
805
|
-
[
|
|
806
|
-
[
|
|
807
|
-
[
|
|
808
|
-
[
|
|
1055
|
+
[types_4.Network.MAINNET]: bitcoin.networks.bitcoin,
|
|
1056
|
+
[types_4.Network.TESTNET]: bitcoin.networks.testnet,
|
|
1057
|
+
[types_4.Network.REGTEST]: bitcoin.networks.regtest,
|
|
1058
|
+
[types_4.Network.SIGNET]: bitcoin.networks.testnet
|
|
809
1059
|
};
|
|
810
1060
|
const btcNetwork = networkMap[this.network] || bitcoin.networks.regtest;
|
|
811
|
-
const { address } = (0,
|
|
1061
|
+
const { address } = (0, types_8.isTaprootChannel)(state.channelType)
|
|
812
1062
|
? (0, funding_taproot_1.createTaprootFundingScript)(state.localBasepoints.fundingPubkey, state.remoteBasepoints.fundingPubkey, btcNetwork)
|
|
813
1063
|
: (0, funding_1.createFundingScript)(state.localBasepoints.fundingPubkey, state.remoteBasepoints.fundingPubkey, btcNetwork);
|
|
814
1064
|
const feePromise = this.feeEstimator
|
|
815
|
-
? this.feeEstimator
|
|
1065
|
+
? this.feeEstimator
|
|
1066
|
+
.estimateFee(6)
|
|
1067
|
+
.then((f) => (f > 0 ? this.clampEstimatedFeeRate(f) : undefined))
|
|
816
1068
|
: Promise.resolve(undefined);
|
|
817
1069
|
feePromise
|
|
818
1070
|
.then((satsPerByte) => this.fundingProvider.buildFundingTransaction(address, state.fundingSatoshis, satsPerByte))
|
|
@@ -836,6 +1088,21 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
836
1088
|
getNodeId() {
|
|
837
1089
|
return this.nodeId;
|
|
838
1090
|
}
|
|
1091
|
+
signMessage(message) {
|
|
1092
|
+
return (0, message_signing_1.signMessageWithKey)(message, this.nodePrivkey);
|
|
1093
|
+
}
|
|
1094
|
+
verifyMessage(message, signature) {
|
|
1095
|
+
const result = (0, message_signing_1.verifyMessageSignature)(message, signature);
|
|
1096
|
+
if (!result.valid || !result.pubkey) {
|
|
1097
|
+
return { valid: false, pubkey: null, knownNode: false };
|
|
1098
|
+
}
|
|
1099
|
+
const knownNode = this.graph.getNode(result.pubkey) !== undefined;
|
|
1100
|
+
return {
|
|
1101
|
+
valid: true,
|
|
1102
|
+
pubkey: result.pubkey.toString('hex'),
|
|
1103
|
+
knownNode
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
839
1106
|
getNodeInfo() {
|
|
840
1107
|
return {
|
|
841
1108
|
nodeId: this.nodeId,
|
|
@@ -846,6 +1113,148 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
846
1113
|
alias: this.alias
|
|
847
1114
|
};
|
|
848
1115
|
}
|
|
1116
|
+
buildStaticChannelBackupData() {
|
|
1117
|
+
const peerAddresses = new Map();
|
|
1118
|
+
if (this.storage) {
|
|
1119
|
+
for (const addr of this.storage.loadAllPeerAddresses()) {
|
|
1120
|
+
const list = peerAddresses.get(addr.pubkey) ?? [];
|
|
1121
|
+
list.push(`${addr.host}:${addr.port}`);
|
|
1122
|
+
peerAddresses.set(addr.pubkey, list);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const channels = [];
|
|
1126
|
+
const seen = new Set();
|
|
1127
|
+
for (const channel of this.channelManager.listChannels()) {
|
|
1128
|
+
const state = channel.getFullState();
|
|
1129
|
+
if (!state.fundingTxid || !state.channelId)
|
|
1130
|
+
continue;
|
|
1131
|
+
if (state.state === types_1.ChannelState.CLOSED)
|
|
1132
|
+
continue;
|
|
1133
|
+
const idHex = state.channelId.toString('hex');
|
|
1134
|
+
if (seen.has(idHex))
|
|
1135
|
+
continue;
|
|
1136
|
+
const peer = this.channelManager.getPeerForChannel(state.channelId);
|
|
1137
|
+
if (!peer)
|
|
1138
|
+
continue;
|
|
1139
|
+
seen.add(idHex);
|
|
1140
|
+
channels.push({
|
|
1141
|
+
channelId: idHex,
|
|
1142
|
+
peerNodeId: peer,
|
|
1143
|
+
peerAddresses: peerAddresses.get(peer) ?? [],
|
|
1144
|
+
fundingTxid: state.fundingTxid.toString('hex'),
|
|
1145
|
+
fundingOutputIndex: state.fundingOutputIndex,
|
|
1146
|
+
fundingSatoshis: state.fundingSatoshis.toString(),
|
|
1147
|
+
channelKeyIndex: channel.channelKeyIndex,
|
|
1148
|
+
channelType: state.channelType ? state.channelType.toString('hex') : '',
|
|
1149
|
+
role: state.role === types_1.ChannelRole.OPENER ? 'OPENER' : 'ACCEPTOR',
|
|
1150
|
+
isTaproot: (0, types_8.isTaprootChannel)(state.channelType),
|
|
1151
|
+
isAnchor: (0, types_8.isAnchorChannel)(state.channelType)
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
return { network: this.network, channels };
|
|
1155
|
+
}
|
|
1156
|
+
async recoverFromStaticChannelBackup(entries) {
|
|
1157
|
+
const recovering = [];
|
|
1158
|
+
const skipped = [];
|
|
1159
|
+
for (const entry of entries) {
|
|
1160
|
+
const channelId = Buffer.from(entry.channelId, 'hex');
|
|
1161
|
+
if (channelId.length !== 32 ||
|
|
1162
|
+
channelId.toString('hex') !== entry.channelId.toLowerCase()) {
|
|
1163
|
+
skipped.push({
|
|
1164
|
+
channelId: entry.channelId,
|
|
1165
|
+
reason: 'invalid channelId (expected 32-byte hex)'
|
|
1166
|
+
});
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
if (this.channelManager.getChannel(channelId)) {
|
|
1170
|
+
skipped.push({
|
|
1171
|
+
channelId: entry.channelId,
|
|
1172
|
+
reason: 'channel already exists'
|
|
1173
|
+
});
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const material = this.channelManager.getRecoveryChannelMaterial(entry.channelKeyIndex);
|
|
1177
|
+
const stateParams = {
|
|
1178
|
+
temporaryChannelId: Buffer.from(channelId),
|
|
1179
|
+
fundingSatoshis: BigInt(entry.fundingSatoshis),
|
|
1180
|
+
pushMsat: 0n,
|
|
1181
|
+
localConfig: material.localConfig,
|
|
1182
|
+
localBasepoints: material.basepoints,
|
|
1183
|
+
localPerCommitmentSeed: material.perCommitmentSeed
|
|
1184
|
+
};
|
|
1185
|
+
const state = entry.role === 'ACCEPTOR'
|
|
1186
|
+
? (0, channel_state_1.createAcceptorState)({
|
|
1187
|
+
...stateParams,
|
|
1188
|
+
remoteBasepoints: material.basepoints,
|
|
1189
|
+
remoteConfig: { ...types_1.DEFAULT_CHANNEL_CONFIG }
|
|
1190
|
+
})
|
|
1191
|
+
: (0, channel_state_1.createOpenerState)(stateParams);
|
|
1192
|
+
state.remoteBasepoints = null;
|
|
1193
|
+
state.channelId = channelId;
|
|
1194
|
+
state.fundingTxid = Buffer.from(entry.fundingTxid, 'hex');
|
|
1195
|
+
state.fundingOutputIndex = entry.fundingOutputIndex;
|
|
1196
|
+
state.channelType = entry.channelType
|
|
1197
|
+
? Buffer.from(entry.channelType, 'hex')
|
|
1198
|
+
: null;
|
|
1199
|
+
state.localCommitmentNumber = 0n;
|
|
1200
|
+
state.remoteCommitmentNumber = 0n;
|
|
1201
|
+
state.localBalanceMsat = 0n;
|
|
1202
|
+
state.remoteBalanceMsat = 0n;
|
|
1203
|
+
state.announceChannel = false;
|
|
1204
|
+
state.state = types_1.ChannelState.ERRORED;
|
|
1205
|
+
state.dataLossDetected = true;
|
|
1206
|
+
const channel = new channel_1.Channel(state);
|
|
1207
|
+
channel.channelKeyIndex = entry.channelKeyIndex;
|
|
1208
|
+
this.channelManager.restoreChannel(channel, entry.peerNodeId, entry.channelKeyIndex);
|
|
1209
|
+
this.persistChannel(channelId);
|
|
1210
|
+
recovering.push(entry.channelId);
|
|
1211
|
+
this.emitStructuredLog('channel', 'recovery_started', {
|
|
1212
|
+
channelId: entry.channelId,
|
|
1213
|
+
peerNodeId: entry.peerNodeId,
|
|
1214
|
+
fundingTxid: entry.fundingTxid,
|
|
1215
|
+
fundingOutputIndex: entry.fundingOutputIndex,
|
|
1216
|
+
channelKeyIndex: entry.channelKeyIndex
|
|
1217
|
+
});
|
|
1218
|
+
if (this.chainWatcher) {
|
|
1219
|
+
try {
|
|
1220
|
+
await this.watchRecoveredFundingOutput(channelId, state);
|
|
1221
|
+
}
|
|
1222
|
+
catch (err) {
|
|
1223
|
+
this.emit('node:error', {
|
|
1224
|
+
code: 'RECOVERY_WATCH_FAILED',
|
|
1225
|
+
channelId,
|
|
1226
|
+
message: `Failed to watch recovered funding output: ${err.message}`,
|
|
1227
|
+
timestamp: Date.now()
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (this.peerManager && entry.peerAddresses.length > 0) {
|
|
1232
|
+
void this.contactRecoveryPeer(entry.peerNodeId, entry.peerAddresses);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return { recovering, skipped };
|
|
1236
|
+
}
|
|
1237
|
+
async contactRecoveryPeer(peerNodeId, addresses) {
|
|
1238
|
+
for (const address of addresses) {
|
|
1239
|
+
const sep = address.lastIndexOf(':');
|
|
1240
|
+
if (sep <= 0)
|
|
1241
|
+
continue;
|
|
1242
|
+
const host = address.slice(0, sep).replace(/^\[|\]$/g, '');
|
|
1243
|
+
const port = parseInt(address.slice(sep + 1), 10);
|
|
1244
|
+
if (!Number.isFinite(port) || port <= 0)
|
|
1245
|
+
continue;
|
|
1246
|
+
try {
|
|
1247
|
+
await this.connectPeer(peerNodeId, host, port);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
this.emitStructuredLog('peer', 'recovery_connect_failed', {
|
|
1254
|
+
peerNodeId,
|
|
1255
|
+
addresses
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
849
1258
|
getSweepDestinationScript() {
|
|
850
1259
|
if (this.sweepDestinationScript) {
|
|
851
1260
|
return this.sweepDestinationScript;
|
|
@@ -857,6 +1266,92 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
857
1266
|
return Buffer.alloc(22);
|
|
858
1267
|
}
|
|
859
1268
|
}
|
|
1269
|
+
getBitcoinNetwork() {
|
|
1270
|
+
if (this.network === types_4.Network.MAINNET)
|
|
1271
|
+
return bitcoin.networks.bitcoin;
|
|
1272
|
+
if (this.network === types_4.Network.REGTEST)
|
|
1273
|
+
return bitcoin.networks.regtest;
|
|
1274
|
+
return bitcoin.networks.testnet;
|
|
1275
|
+
}
|
|
1276
|
+
initWatchtowerClient(towers) {
|
|
1277
|
+
if (towers.length === 0) {
|
|
1278
|
+
this.watchtowerClient = null;
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const btcNetwork = this.getBitcoinNetwork();
|
|
1282
|
+
const store = this.storage &&
|
|
1283
|
+
typeof this.storage.saveWatchtowerSession ===
|
|
1284
|
+
'function'
|
|
1285
|
+
? this.storage
|
|
1286
|
+
: undefined;
|
|
1287
|
+
this.watchtowerClient = new watchtower_1.WatchtowerClient({
|
|
1288
|
+
localPrivateKey: this.nodePrivkey,
|
|
1289
|
+
chainHash: (0, watchtower_1.chainHashForNetwork)(btcNetwork),
|
|
1290
|
+
network: btcNetwork,
|
|
1291
|
+
towers,
|
|
1292
|
+
store,
|
|
1293
|
+
socks5Proxy: this.socks5Proxy ?? undefined
|
|
1294
|
+
});
|
|
1295
|
+
this.watchtowerClient.on('log', (entry) => {
|
|
1296
|
+
const event = String(entry.event ?? 'log');
|
|
1297
|
+
this.emitStructuredLog('watchtower', event, entry);
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
backupRevokedStateToTowers(channelId, perCommitmentSecret, revokedTx) {
|
|
1301
|
+
const client = this.watchtowerClient;
|
|
1302
|
+
if (!client || !client.enabled)
|
|
1303
|
+
return;
|
|
1304
|
+
try {
|
|
1305
|
+
const channel = this.channelManager.getChannel(channelId);
|
|
1306
|
+
if (!channel)
|
|
1307
|
+
return;
|
|
1308
|
+
const state = channel.getFullState();
|
|
1309
|
+
if (!state.remoteBasepoints)
|
|
1310
|
+
return;
|
|
1311
|
+
const perCh = this.channelManager.getMonitorSigningKeys(channelId);
|
|
1312
|
+
const revocationBasepointSecret = perCh?.revocationBasepointSecret ?? this.revocationBasepointSecret;
|
|
1313
|
+
if (!revocationBasepointSecret)
|
|
1314
|
+
return;
|
|
1315
|
+
const paymentBasepointSecret = perCh?.paymentBasepointSecret ?? this.paymentBasepointSecret;
|
|
1316
|
+
const btcNetwork = this.getBitcoinNetwork();
|
|
1317
|
+
const ctx = {
|
|
1318
|
+
channelId: channelId.toString('hex'),
|
|
1319
|
+
revokedTx: bitcoin.Transaction.fromBuffer(revokedTx),
|
|
1320
|
+
perCommitmentSecret,
|
|
1321
|
+
revocationBasepoint: state.localBasepoints.revocationBasepoint,
|
|
1322
|
+
revocationBasepointSecret,
|
|
1323
|
+
remoteDelayedBasepoint: state.remoteBasepoints.delayedPaymentBasepoint,
|
|
1324
|
+
toSelfDelay: state.localConfig.toSelfDelay,
|
|
1325
|
+
isAnchor: (0, types_8.isAnchorChannel)(state.channelType),
|
|
1326
|
+
localPaymentPubkey: state.localBasepoints.paymentBasepoint,
|
|
1327
|
+
paymentBasepointSecret,
|
|
1328
|
+
sweepScript: this.getSweepDestinationScript(),
|
|
1329
|
+
network: btcNetwork
|
|
1330
|
+
};
|
|
1331
|
+
client.backupRevokedState(ctx);
|
|
1332
|
+
}
|
|
1333
|
+
catch (err) {
|
|
1334
|
+
this.emitStructuredLog('watchtower', 'backup_context_failed', {
|
|
1335
|
+
channelId: channelId.toString('hex'),
|
|
1336
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
addWatchtower(uri) {
|
|
1341
|
+
if (!this.watchtowerClient) {
|
|
1342
|
+
this.initWatchtowerClient([uri]);
|
|
1343
|
+
const client = this.watchtowerClient;
|
|
1344
|
+
void client?.start();
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
this.watchtowerClient.addTower(uri);
|
|
1348
|
+
}
|
|
1349
|
+
removeWatchtower(uri) {
|
|
1350
|
+
this.watchtowerClient?.removeTower(uri);
|
|
1351
|
+
}
|
|
1352
|
+
getWatchtowers() {
|
|
1353
|
+
return this.watchtowerClient ? this.watchtowerClient.getHealth() : [];
|
|
1354
|
+
}
|
|
860
1355
|
setSweepDestinationScript(destinationScript) {
|
|
861
1356
|
this.sweepDestinationScript = destinationScript;
|
|
862
1357
|
this.chainWatcher?.setDestinationScript(destinationScript);
|
|
@@ -884,7 +1379,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
884
1379
|
let feeRatePerVbyte = opts?.feeRatePerVbyte ?? 0;
|
|
885
1380
|
if (feeRatePerVbyte <= 0 && this.feeEstimator) {
|
|
886
1381
|
try {
|
|
887
|
-
feeRatePerVbyte = await this.feeEstimator.estimateFee(6);
|
|
1382
|
+
feeRatePerVbyte = this.clampEstimatedFeeRate(await this.feeEstimator.estimateFee(6));
|
|
888
1383
|
}
|
|
889
1384
|
catch {
|
|
890
1385
|
}
|
|
@@ -918,10 +1413,10 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
918
1413
|
}
|
|
919
1414
|
getFundingAddress() {
|
|
920
1415
|
const networkMap = {
|
|
921
|
-
[
|
|
922
|
-
[
|
|
923
|
-
[
|
|
924
|
-
[
|
|
1416
|
+
[types_4.Network.MAINNET]: bitcoin.networks.bitcoin,
|
|
1417
|
+
[types_4.Network.TESTNET]: bitcoin.networks.testnet,
|
|
1418
|
+
[types_4.Network.REGTEST]: bitcoin.networks.regtest,
|
|
1419
|
+
[types_4.Network.SIGNET]: bitcoin.networks.testnet
|
|
925
1420
|
};
|
|
926
1421
|
const btcNetwork = networkMap[this.network] || bitcoin.networks.regtest;
|
|
927
1422
|
const { address } = bitcoin.payments.p2wpkh({
|
|
@@ -949,6 +1444,13 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
949
1444
|
const pubkeyErr = (0, validation_2.validateHexPubkey)(pubkey, 'pubkey');
|
|
950
1445
|
if (pubkeyErr)
|
|
951
1446
|
throw new Error(pubkeyErr);
|
|
1447
|
+
if (host === undefined && port === undefined) {
|
|
1448
|
+
await this.connectPeerById(pubkey);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
if (host === undefined || port === undefined) {
|
|
1452
|
+
throw new Error('host and port must be provided together (omit both to resolve from gossip/DNS)');
|
|
1453
|
+
}
|
|
952
1454
|
const hostErr = (0, validation_2.validateHost)(host);
|
|
953
1455
|
if (hostErr)
|
|
954
1456
|
throw new Error(hostErr);
|
|
@@ -957,6 +1459,58 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
957
1459
|
throw new Error(portErr);
|
|
958
1460
|
await this.peerManager.connectPeer(pubkey, host, port);
|
|
959
1461
|
}
|
|
1462
|
+
async connectPeerById(pubkey) {
|
|
1463
|
+
const attempts = [];
|
|
1464
|
+
const isTor = (a) => a.type === types_2.ADDRESS_TYPE_TORV2 || a.type === types_2.ADDRESS_TYPE_TORV3;
|
|
1465
|
+
const announced = this.graph.getNode(Buffer.from(pubkey, 'hex'))?.announcement?.addresses ??
|
|
1466
|
+
[];
|
|
1467
|
+
let skippedTor = 0;
|
|
1468
|
+
const candidates = [];
|
|
1469
|
+
for (const addr of announced) {
|
|
1470
|
+
if (isTor(addr) && !this.socks5Proxy) {
|
|
1471
|
+
skippedTor++;
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
const dialable = (0, messages_1.nodeAddressToHostPort)(addr);
|
|
1475
|
+
if (dialable)
|
|
1476
|
+
candidates.push(dialable);
|
|
1477
|
+
}
|
|
1478
|
+
for (const { host, port } of candidates) {
|
|
1479
|
+
try {
|
|
1480
|
+
await this.peerManager.connectPeer(pubkey, host, port);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
catch (err) {
|
|
1484
|
+
attempts.push(`graph ${host}:${port} (${err instanceof Error ? err.message : String(err)})`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (skippedTor > 0) {
|
|
1488
|
+
attempts.push(`skipped ${skippedTor} Tor address(es): no socks5Proxy configured`);
|
|
1489
|
+
}
|
|
1490
|
+
if (candidates.length === 0) {
|
|
1491
|
+
let seedPeers = [];
|
|
1492
|
+
try {
|
|
1493
|
+
seedPeers = await this.bootstrapPeers();
|
|
1494
|
+
}
|
|
1495
|
+
catch (err) {
|
|
1496
|
+
attempts.push(`DNS bootstrap failed (${err instanceof Error ? err.message : String(err)})`);
|
|
1497
|
+
}
|
|
1498
|
+
const matches = seedPeers.filter((p) => p.pubkey.toString('hex') === pubkey);
|
|
1499
|
+
if (matches.length === 0) {
|
|
1500
|
+
attempts.push('DNS bootstrap returned no address for this node id');
|
|
1501
|
+
}
|
|
1502
|
+
for (const peer of matches) {
|
|
1503
|
+
try {
|
|
1504
|
+
await this.peerManager.connectPeer(pubkey, peer.host, peer.port);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
catch (err) {
|
|
1508
|
+
attempts.push(`dns ${peer.host}:${peer.port} (${err instanceof Error ? err.message : String(err)})`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
throw new Error(`Unable to resolve a connection to ${pubkey}: ${attempts.join('; ')}`);
|
|
1513
|
+
}
|
|
960
1514
|
disconnectPeer(pubkey) {
|
|
961
1515
|
if (!this.peerManager) {
|
|
962
1516
|
throw new Error('Networking is not enabled');
|
|
@@ -1033,6 +1587,13 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1033
1587
|
});
|
|
1034
1588
|
}
|
|
1035
1589
|
async startChainWatcher() {
|
|
1590
|
+
if (this.watchtowerClient) {
|
|
1591
|
+
this.watchtowerClient.start().catch((err) => {
|
|
1592
|
+
this.emitStructuredLog('watchtower', 'start_failed', {
|
|
1593
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1594
|
+
});
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1036
1597
|
if (this.chainWatcher) {
|
|
1037
1598
|
this.wireChainWatcherEvents();
|
|
1038
1599
|
await this.chainWatcher.start();
|
|
@@ -1050,10 +1611,10 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1050
1611
|
if (!this.chainWatcher)
|
|
1051
1612
|
return;
|
|
1052
1613
|
const networkMap = {
|
|
1053
|
-
[
|
|
1054
|
-
[
|
|
1055
|
-
[
|
|
1056
|
-
[
|
|
1614
|
+
[types_4.Network.MAINNET]: bitcoin.networks.bitcoin,
|
|
1615
|
+
[types_4.Network.TESTNET]: bitcoin.networks.testnet,
|
|
1616
|
+
[types_4.Network.REGTEST]: bitcoin.networks.regtest,
|
|
1617
|
+
[types_4.Network.SIGNET]: bitcoin.networks.testnet
|
|
1057
1618
|
};
|
|
1058
1619
|
const btcNetwork = networkMap[this.network] || bitcoin.networks.regtest;
|
|
1059
1620
|
for (const channel of this.channelManager.listChannels()) {
|
|
@@ -1066,10 +1627,10 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1066
1627
|
continue;
|
|
1067
1628
|
if (monitor) {
|
|
1068
1629
|
for (const output of monitor.getTrackedOutputs()) {
|
|
1069
|
-
if (output.status ===
|
|
1630
|
+
if (output.status === types_7.OutputStatus.IRREVOCABLY_RESOLVED)
|
|
1070
1631
|
continue;
|
|
1071
1632
|
try {
|
|
1072
|
-
const seedTxid = output.status ===
|
|
1633
|
+
const seedTxid = output.status === types_7.OutputStatus.SPEND_CONFIRMED
|
|
1073
1634
|
? output.resolutionTxid
|
|
1074
1635
|
: undefined;
|
|
1075
1636
|
const seedHeight = seedTxid !== undefined ? output.confirmationHeight : undefined;
|
|
@@ -1093,10 +1654,10 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1093
1654
|
continue;
|
|
1094
1655
|
if (monitor) {
|
|
1095
1656
|
for (const output of monitor.getTrackedOutputs()) {
|
|
1096
|
-
if (output.status ===
|
|
1657
|
+
if (output.status === types_7.OutputStatus.IRREVOCABLY_RESOLVED)
|
|
1097
1658
|
continue;
|
|
1098
1659
|
try {
|
|
1099
|
-
const seedTxid = output.status ===
|
|
1660
|
+
const seedTxid = output.status === types_7.OutputStatus.SPEND_CONFIRMED
|
|
1100
1661
|
? output.resolutionTxid
|
|
1101
1662
|
: undefined;
|
|
1102
1663
|
const seedHeight = seedTxid !== undefined ? output.confirmationHeight : undefined;
|
|
@@ -1107,8 +1668,22 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1107
1668
|
}
|
|
1108
1669
|
}
|
|
1109
1670
|
}
|
|
1110
|
-
if (!state.remoteBasepoints)
|
|
1671
|
+
if (!state.remoteBasepoints) {
|
|
1672
|
+
if (state.dataLossDetected && state.fundingTxid) {
|
|
1673
|
+
try {
|
|
1674
|
+
await this.watchRecoveredFundingOutput(state.channelId || state.temporaryChannelId, state);
|
|
1675
|
+
}
|
|
1676
|
+
catch (err) {
|
|
1677
|
+
this.emit('node:error', {
|
|
1678
|
+
code: 'RECOVERY_WATCH_FAILED',
|
|
1679
|
+
channelId: state.channelId || state.temporaryChannelId,
|
|
1680
|
+
message: `Failed to watch recovered funding output: ${err.message}`,
|
|
1681
|
+
timestamp: Date.now()
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1111
1685
|
continue;
|
|
1686
|
+
}
|
|
1112
1687
|
const { p2wshOutput } = (0, funding_1.createFundingScript)(state.localBasepoints.fundingPubkey, state.remoteBasepoints.fundingPubkey, btcNetwork);
|
|
1113
1688
|
const inflight = state.spliceInFlight;
|
|
1114
1689
|
if (inflight) {
|
|
@@ -1130,6 +1705,21 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1130
1705
|
await this.chainWatcher.watchFundingOutput(state.channelId || state.temporaryChannelId, txidHex, state.fundingOutputIndex, state.minimumDepth ?? 3, p2wshOutput);
|
|
1131
1706
|
}
|
|
1132
1707
|
}
|
|
1708
|
+
async watchRecoveredFundingOutput(channelId, state) {
|
|
1709
|
+
if (!this.chainWatcher || !this._chainBackend) {
|
|
1710
|
+
throw new Error('Chain backend is not available');
|
|
1711
|
+
}
|
|
1712
|
+
if (!state.fundingTxid) {
|
|
1713
|
+
throw new Error('Recovered channel has no funding txid');
|
|
1714
|
+
}
|
|
1715
|
+
const txidHex = Buffer.from(state.fundingTxid).reverse().toString('hex');
|
|
1716
|
+
const rawTx = await this._chainBackend.getTransaction(txidHex);
|
|
1717
|
+
const fundingTx = bitcoin.Transaction.fromBuffer(rawTx);
|
|
1718
|
+
if (state.fundingOutputIndex >= fundingTx.outs.length) {
|
|
1719
|
+
throw new Error(`Funding output index ${state.fundingOutputIndex} out of range for tx ${txidHex}`);
|
|
1720
|
+
}
|
|
1721
|
+
await this.chainWatcher.watchFundingOutput(channelId, txidHex, state.fundingOutputIndex, state.minimumDepth || 1, Buffer.from(fundingTx.outs[state.fundingOutputIndex].script));
|
|
1722
|
+
}
|
|
1133
1723
|
async bootstrapPeers(config) {
|
|
1134
1724
|
return (0, bootstrap_1.bootstrapPeers)(config);
|
|
1135
1725
|
}
|
|
@@ -1198,6 +1788,14 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1198
1788
|
clearInterval(this._gossipRefreshTimer);
|
|
1199
1789
|
this._gossipRefreshTimer = undefined;
|
|
1200
1790
|
}
|
|
1791
|
+
if (this.autoRebalanceTimer) {
|
|
1792
|
+
clearInterval(this.autoRebalanceTimer);
|
|
1793
|
+
this.autoRebalanceTimer = null;
|
|
1794
|
+
}
|
|
1795
|
+
if (this.autoTuneFeesTimer) {
|
|
1796
|
+
clearInterval(this.autoTuneFeesTimer);
|
|
1797
|
+
this.autoTuneFeesTimer = null;
|
|
1798
|
+
}
|
|
1201
1799
|
for (const t of this._reconnectTimers) {
|
|
1202
1800
|
clearTimeout(t);
|
|
1203
1801
|
}
|
|
@@ -1214,6 +1812,9 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1214
1812
|
if (this.chainWatcher) {
|
|
1215
1813
|
this.chainWatcher.stop();
|
|
1216
1814
|
}
|
|
1815
|
+
if (this.watchtowerClient) {
|
|
1816
|
+
this.watchtowerClient.stop();
|
|
1817
|
+
}
|
|
1217
1818
|
if (this.peerManager) {
|
|
1218
1819
|
this.peerManager.destroy();
|
|
1219
1820
|
}
|
|
@@ -1315,8 +1916,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1315
1916
|
const max = this.resourceConfig.maxCompletedPayments;
|
|
1316
1917
|
let pruned = 0;
|
|
1317
1918
|
for (const [hash, payment] of this.payments) {
|
|
1318
|
-
if (payment.status ===
|
|
1319
|
-
payment.status ===
|
|
1919
|
+
if (payment.status === types_6.PaymentStatus.COMPLETED ||
|
|
1920
|
+
payment.status === types_6.PaymentStatus.FAILED) {
|
|
1320
1921
|
const age = now - (payment.completedAt || payment.createdAt);
|
|
1321
1922
|
if (age > ttl) {
|
|
1322
1923
|
this.payments.delete(hash);
|
|
@@ -1327,8 +1928,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1327
1928
|
}
|
|
1328
1929
|
const completed = [];
|
|
1329
1930
|
for (const entry of this.payments) {
|
|
1330
|
-
if (entry[1].status ===
|
|
1331
|
-
entry[1].status ===
|
|
1931
|
+
if (entry[1].status === types_6.PaymentStatus.COMPLETED ||
|
|
1932
|
+
entry[1].status === types_6.PaymentStatus.FAILED) {
|
|
1332
1933
|
completed.push(entry);
|
|
1333
1934
|
}
|
|
1334
1935
|
}
|
|
@@ -1444,6 +2045,217 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1444
2045
|
}
|
|
1445
2046
|
return { ok: true };
|
|
1446
2047
|
}
|
|
2048
|
+
setChannelPolicy(channelId, policy) {
|
|
2049
|
+
this.validateChannelPolicyFields(policy);
|
|
2050
|
+
let targets;
|
|
2051
|
+
if (channelId === 'all') {
|
|
2052
|
+
targets = this.channelManager
|
|
2053
|
+
.listChannels()
|
|
2054
|
+
.map((ch) => ch.getChannelId())
|
|
2055
|
+
.filter((id) => id !== null);
|
|
2056
|
+
}
|
|
2057
|
+
else {
|
|
2058
|
+
const cidErr = (0, validation_2.validateBuffer)(channelId, 32, 'channelId');
|
|
2059
|
+
if (cidErr)
|
|
2060
|
+
throw new Error(cidErr);
|
|
2061
|
+
if (!this.channelManager.getChannel(channelId)) {
|
|
2062
|
+
throw new Error(`Channel not found: ${channelId.toString('hex')}`);
|
|
2063
|
+
}
|
|
2064
|
+
targets = [channelId];
|
|
2065
|
+
}
|
|
2066
|
+
for (const target of targets) {
|
|
2067
|
+
const hex = target.toString('hex');
|
|
2068
|
+
const merged = {
|
|
2069
|
+
...this.channelPolicies.get(hex),
|
|
2070
|
+
...policy
|
|
2071
|
+
};
|
|
2072
|
+
if (merged.htlcMinimumMsat !== undefined &&
|
|
2073
|
+
merged.htlcMaximumMsat !== undefined &&
|
|
2074
|
+
merged.htlcMinimumMsat > merged.htlcMaximumMsat) {
|
|
2075
|
+
throw new Error(`htlcMinimumMsat (${merged.htlcMinimumMsat}) exceeds htlcMaximumMsat (${merged.htlcMaximumMsat})`);
|
|
2076
|
+
}
|
|
2077
|
+
this.channelPolicies.set(hex, merged);
|
|
2078
|
+
this.safeStorage(() => this.storage.saveChannelPolicy?.(hex, LightningNode.serializeChannelPolicy(merged)), 'saveChannelPolicy');
|
|
2079
|
+
this.regenerateChannelUpdateForPolicy(target);
|
|
2080
|
+
this.emitStructuredLog('channel', 'policy_updated', {
|
|
2081
|
+
channelId: hex,
|
|
2082
|
+
...LightningNode.serializeChannelPolicy(merged)
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
getChannelPolicy(channelId) {
|
|
2087
|
+
const channel = this.channelManager.getChannel(channelId);
|
|
2088
|
+
if (!channel)
|
|
2089
|
+
return null;
|
|
2090
|
+
const state = channel.getFullState();
|
|
2091
|
+
const override = this.channelPolicies.get(channelId.toString('hex'));
|
|
2092
|
+
const capacityMsat = state.fundingSatoshis * 1000n;
|
|
2093
|
+
const defaultHtlcMax = state.localConfig.maxHtlcValueInFlightMsat > capacityMsat
|
|
2094
|
+
? capacityMsat
|
|
2095
|
+
: state.localConfig.maxHtlcValueInFlightMsat;
|
|
2096
|
+
return {
|
|
2097
|
+
feeBaseMsat: override?.feeBaseMsat ?? this.forwardingFeeBaseMsat,
|
|
2098
|
+
feeProportionalMillionths: override?.feeProportionalMillionths ?? this.forwardingFeePropMillionths,
|
|
2099
|
+
cltvExpiryDelta: override?.cltvExpiryDelta ?? this.forwardingCltvDelta,
|
|
2100
|
+
htlcMinimumMsat: override?.htlcMinimumMsat ?? state.localConfig.htlcMinimumMsat,
|
|
2101
|
+
htlcMaximumMsat: override?.htlcMaximumMsat ?? defaultHtlcMax,
|
|
2102
|
+
source: override && Object.keys(override).length > 0 ? 'override' : 'default'
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
getForwardingPolicyForChannel(channelId) {
|
|
2106
|
+
const override = channelId
|
|
2107
|
+
? this.channelPolicies.get(channelId.toString('hex'))
|
|
2108
|
+
: undefined;
|
|
2109
|
+
return {
|
|
2110
|
+
feeBaseMsat: override?.feeBaseMsat ?? this.forwardingFeeBaseMsat,
|
|
2111
|
+
feeProportionalMillionths: override?.feeProportionalMillionths ?? this.forwardingFeePropMillionths,
|
|
2112
|
+
cltvExpiryDelta: override?.cltvExpiryDelta ?? this.forwardingCltvDelta
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
validateChannelPolicyFields(policy) {
|
|
2116
|
+
if (policy.feeBaseMsat === undefined &&
|
|
2117
|
+
policy.feeProportionalMillionths === undefined &&
|
|
2118
|
+
policy.cltvExpiryDelta === undefined &&
|
|
2119
|
+
policy.htlcMinimumMsat === undefined &&
|
|
2120
|
+
policy.htlcMaximumMsat === undefined) {
|
|
2121
|
+
throw new Error('policy must set at least one field');
|
|
2122
|
+
}
|
|
2123
|
+
if (policy.feeBaseMsat !== undefined) {
|
|
2124
|
+
if (!Number.isInteger(policy.feeBaseMsat) ||
|
|
2125
|
+
policy.feeBaseMsat < 0 ||
|
|
2126
|
+
policy.feeBaseMsat > 0xffffffff) {
|
|
2127
|
+
throw new Error(`feeBaseMsat must be an integer in [0, 4294967295], got ${policy.feeBaseMsat}`);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
if (policy.feeProportionalMillionths !== undefined) {
|
|
2131
|
+
if (!Number.isInteger(policy.feeProportionalMillionths) ||
|
|
2132
|
+
policy.feeProportionalMillionths < 0 ||
|
|
2133
|
+
policy.feeProportionalMillionths > 0xffffffff) {
|
|
2134
|
+
throw new Error(`feeProportionalMillionths must be an integer in [0, 4294967295], got ${policy.feeProportionalMillionths}`);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (policy.cltvExpiryDelta !== undefined) {
|
|
2138
|
+
if (!Number.isInteger(policy.cltvExpiryDelta) ||
|
|
2139
|
+
policy.cltvExpiryDelta < 1 ||
|
|
2140
|
+
policy.cltvExpiryDelta > 0xffff) {
|
|
2141
|
+
throw new Error(`cltvExpiryDelta must be an integer in [1, 65535] (>= 18 recommended), got ${policy.cltvExpiryDelta}`);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
if (policy.htlcMinimumMsat !== undefined &&
|
|
2145
|
+
(typeof policy.htlcMinimumMsat !== 'bigint' ||
|
|
2146
|
+
policy.htlcMinimumMsat < 0n)) {
|
|
2147
|
+
throw new Error(`htlcMinimumMsat must be a non-negative bigint, got ${policy.htlcMinimumMsat}`);
|
|
2148
|
+
}
|
|
2149
|
+
if (policy.htlcMaximumMsat !== undefined &&
|
|
2150
|
+
(typeof policy.htlcMaximumMsat !== 'bigint' ||
|
|
2151
|
+
policy.htlcMaximumMsat < 0n)) {
|
|
2152
|
+
throw new Error(`htlcMaximumMsat must be a non-negative bigint, got ${policy.htlcMaximumMsat}`);
|
|
2153
|
+
}
|
|
2154
|
+
if (policy.htlcMinimumMsat !== undefined &&
|
|
2155
|
+
policy.htlcMaximumMsat !== undefined &&
|
|
2156
|
+
policy.htlcMinimumMsat > policy.htlcMaximumMsat) {
|
|
2157
|
+
throw new Error(`htlcMinimumMsat (${policy.htlcMinimumMsat}) exceeds htlcMaximumMsat (${policy.htlcMaximumMsat})`);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
static serializeChannelPolicy(policy) {
|
|
2161
|
+
const out = {};
|
|
2162
|
+
if (policy.feeBaseMsat !== undefined)
|
|
2163
|
+
out.feeBaseMsat = policy.feeBaseMsat;
|
|
2164
|
+
if (policy.feeProportionalMillionths !== undefined)
|
|
2165
|
+
out.feeProportionalMillionths = policy.feeProportionalMillionths;
|
|
2166
|
+
if (policy.cltvExpiryDelta !== undefined)
|
|
2167
|
+
out.cltvExpiryDelta = policy.cltvExpiryDelta;
|
|
2168
|
+
if (policy.htlcMinimumMsat !== undefined)
|
|
2169
|
+
out.htlcMinimumMsat = policy.htlcMinimumMsat.toString();
|
|
2170
|
+
if (policy.htlcMaximumMsat !== undefined)
|
|
2171
|
+
out.htlcMaximumMsat = policy.htlcMaximumMsat.toString();
|
|
2172
|
+
return out;
|
|
2173
|
+
}
|
|
2174
|
+
regenerateChannelUpdateForPolicy(channelId) {
|
|
2175
|
+
const hex = channelId.toString('hex');
|
|
2176
|
+
const gossip = this._ownChannelGossip.get(hex);
|
|
2177
|
+
if (gossip) {
|
|
2178
|
+
let timestamp = Math.floor(Date.now() / 1000);
|
|
2179
|
+
try {
|
|
2180
|
+
timestamp = Math.max(timestamp, (0, messages_1.decodeChannelUpdateMessage)(gossip.update).timestamp + 1);
|
|
2181
|
+
}
|
|
2182
|
+
catch {
|
|
2183
|
+
}
|
|
2184
|
+
const refreshed = this.refreshChannelUpdate(gossip.update, timestamp, channelId);
|
|
2185
|
+
if (!refreshed)
|
|
2186
|
+
return;
|
|
2187
|
+
this._ownChannelGossip.set(hex, {
|
|
2188
|
+
announcement: gossip.announcement,
|
|
2189
|
+
update: refreshed
|
|
2190
|
+
});
|
|
2191
|
+
try {
|
|
2192
|
+
this.graph.applyChannelUpdate((0, messages_1.decodeChannelUpdateMessage)(refreshed));
|
|
2193
|
+
}
|
|
2194
|
+
catch {
|
|
2195
|
+
}
|
|
2196
|
+
this.broadcastOwnGossip();
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
const payload = this.buildDirectChannelUpdate(channelId);
|
|
2200
|
+
if (!payload)
|
|
2201
|
+
return;
|
|
2202
|
+
const peer = this.channelManager.getPeerForChannel(channelId);
|
|
2203
|
+
if (!peer)
|
|
2204
|
+
return;
|
|
2205
|
+
if (this.peerManager) {
|
|
2206
|
+
try {
|
|
2207
|
+
this.peerManager.sendToPeer(peer, types_5.MessageType.CHANNEL_UPDATE, payload);
|
|
2208
|
+
}
|
|
2209
|
+
catch {
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
else {
|
|
2213
|
+
this.emit('message:outbound', peer, types_5.MessageType.CHANNEL_UPDATE, payload);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
buildDirectChannelUpdate(channelId) {
|
|
2217
|
+
const channel = this.channelManager.getChannel(channelId);
|
|
2218
|
+
if (!channel)
|
|
2219
|
+
return null;
|
|
2220
|
+
const state = channel.getFullState();
|
|
2221
|
+
const scid = state.shortChannelId ?? state.scidAlias;
|
|
2222
|
+
if (!scid)
|
|
2223
|
+
return null;
|
|
2224
|
+
const peerHex = this.channelManager.getPeerForChannel(channelId);
|
|
2225
|
+
if (!peerHex)
|
|
2226
|
+
return null;
|
|
2227
|
+
const policy = this.getChannelPolicy(channelId);
|
|
2228
|
+
if (!policy)
|
|
2229
|
+
return null;
|
|
2230
|
+
try {
|
|
2231
|
+
const { encodeChannelUpdateMessage } = require('../gossip/messages');
|
|
2232
|
+
const ourNodeId = (0, ecdh_1.getPublicKey)(this.nodePrivkey);
|
|
2233
|
+
const peerNodeId = Buffer.from(peerHex, 'hex');
|
|
2234
|
+
const capacityMsat = state.fundingSatoshis * 1000n;
|
|
2235
|
+
const htlcMaxMsat = policy.htlcMaximumMsat > capacityMsat
|
|
2236
|
+
? capacityMsat
|
|
2237
|
+
: policy.htlcMaximumMsat;
|
|
2238
|
+
const payload = encodeChannelUpdateMessage({
|
|
2239
|
+
signature: Buffer.alloc(64),
|
|
2240
|
+
chainHash: this.acceptableChainHashes[0] ?? types_1.BITCOIN_CHAIN_HASH,
|
|
2241
|
+
shortChannelId: scid,
|
|
2242
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
2243
|
+
messageFlags: 0x01,
|
|
2244
|
+
channelFlags: Buffer.compare(ourNodeId, peerNodeId) < 0 ? 0 : 1,
|
|
2245
|
+
cltvExpiryDelta: policy.cltvExpiryDelta,
|
|
2246
|
+
htlcMinimumMsat: policy.htlcMinimumMsat,
|
|
2247
|
+
feeBaseMsat: policy.feeBaseMsat,
|
|
2248
|
+
feeProportionalMillionths: policy.feeProportionalMillionths,
|
|
2249
|
+
htlcMaximumMsat: htlcMaxMsat
|
|
2250
|
+
});
|
|
2251
|
+
const sig = (0, validation_1.signChannelUpdate)(payload, this.nodePrivkey);
|
|
2252
|
+
sig.copy(payload, 0);
|
|
2253
|
+
return payload;
|
|
2254
|
+
}
|
|
2255
|
+
catch {
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
1447
2259
|
forceCloseChannel(channelId, destinationScript) {
|
|
1448
2260
|
const result = this.channelManager.forceClose(channelId, destinationScript, this.resolveForceCloseFeeRatePerVbyte());
|
|
1449
2261
|
if (!result.ok) {
|
|
@@ -1661,6 +2473,316 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1661
2473
|
});
|
|
1662
2474
|
return this.liquidityAdvisor.analyze(snapshots);
|
|
1663
2475
|
}
|
|
2476
|
+
planRebalanceRecommendations(minImbalancePct) {
|
|
2477
|
+
const snapshots = this.listChannels().map((ch) => ({
|
|
2478
|
+
channelId: ch.channelId.toString('hex'),
|
|
2479
|
+
state: ch.state,
|
|
2480
|
+
localBalanceMsat: ch.localBalanceMsat,
|
|
2481
|
+
remoteBalanceMsat: ch.remoteBalanceMsat,
|
|
2482
|
+
capacitySats: Number(ch.fundingSatoshis),
|
|
2483
|
+
peerPubkey: ch.peerPubkey
|
|
2484
|
+
}));
|
|
2485
|
+
const plans = (0, rebalance_planner_1.planRebalances)(snapshots, {
|
|
2486
|
+
minImbalancePct: minImbalancePct ?? this.autoRebalanceConfig.minImbalancePct
|
|
2487
|
+
});
|
|
2488
|
+
const clamped = [];
|
|
2489
|
+
for (const plan of plans) {
|
|
2490
|
+
const fromCh = this.channelManager.getChannel(Buffer.from(plan.fromChannelId, 'hex'));
|
|
2491
|
+
const toCh = this.channelManager.getChannel(Buffer.from(plan.toChannelId, 'hex'));
|
|
2492
|
+
if (!fromCh || !toCh)
|
|
2493
|
+
continue;
|
|
2494
|
+
const outCapMsat = fromCh.getFullState().remoteConfig.maxHtlcValueInFlightMsat;
|
|
2495
|
+
const inCapMsat = toCh.getFullState().localConfig.maxHtlcValueInFlightMsat;
|
|
2496
|
+
const capMsat = outCapMsat < inCapMsat ? outCapMsat : inCapMsat;
|
|
2497
|
+
const maxAmountSats = (capMsat * 99n) / 100n / 1000n;
|
|
2498
|
+
const amountSats = plan.amountSats < maxAmountSats ? plan.amountSats : maxAmountSats;
|
|
2499
|
+
if (amountSats < rebalance_planner_1.MIN_REBALANCE_SATS)
|
|
2500
|
+
continue;
|
|
2501
|
+
clamped.push({ ...plan, amountSats });
|
|
2502
|
+
}
|
|
2503
|
+
return clamped;
|
|
2504
|
+
}
|
|
2505
|
+
async rebalanceChannel(options) {
|
|
2506
|
+
const { fromChannelId, toChannelId, amountSats, maxFeeSats } = options;
|
|
2507
|
+
const cidErr = (0, validation_2.validateBuffer)(fromChannelId, 32, 'fromChannelId') ||
|
|
2508
|
+
(0, validation_2.validateBuffer)(toChannelId, 32, 'toChannelId');
|
|
2509
|
+
if (cidErr)
|
|
2510
|
+
throw new Error(cidErr);
|
|
2511
|
+
if (fromChannelId.equals(toChannelId)) {
|
|
2512
|
+
throw new Error('fromChannelId and toChannelId must differ');
|
|
2513
|
+
}
|
|
2514
|
+
if (amountSats <= 0n)
|
|
2515
|
+
throw new Error('amountSats must be positive');
|
|
2516
|
+
if (maxFeeSats < 0n)
|
|
2517
|
+
throw new Error('maxFeeSats must be non-negative');
|
|
2518
|
+
const fromChannel = this.channelManager.getChannel(fromChannelId);
|
|
2519
|
+
if (!fromChannel || fromChannel.getState() !== types_1.ChannelState.NORMAL) {
|
|
2520
|
+
throw new Error(`from channel not found or not NORMAL: ${fromChannelId.toString('hex')}`);
|
|
2521
|
+
}
|
|
2522
|
+
const toChannel = this.channelManager.getChannel(toChannelId);
|
|
2523
|
+
if (!toChannel || toChannel.getState() !== types_1.ChannelState.NORMAL) {
|
|
2524
|
+
throw new Error(`to channel not found or not NORMAL: ${toChannelId.toString('hex')}`);
|
|
2525
|
+
}
|
|
2526
|
+
const amountMsat = amountSats * 1000n;
|
|
2527
|
+
const maxFeeMsat = maxFeeSats * 1000n;
|
|
2528
|
+
const fromState = fromChannel.getFullState();
|
|
2529
|
+
const toState = toChannel.getFullState();
|
|
2530
|
+
if (fromState.localBalanceMsat < amountMsat) {
|
|
2531
|
+
throw new Error('insufficient local balance on from channel');
|
|
2532
|
+
}
|
|
2533
|
+
if (toState.remoteBalanceMsat < amountMsat) {
|
|
2534
|
+
throw new Error('insufficient inbound capacity on to channel');
|
|
2535
|
+
}
|
|
2536
|
+
if (amountMsat > toState.localConfig.maxHtlcValueInFlightMsat) {
|
|
2537
|
+
throw new Error('amount exceeds our max_htlc_value_in_flight on the to channel');
|
|
2538
|
+
}
|
|
2539
|
+
const fromScid = fromState.shortChannelId ?? fromState.scidAlias;
|
|
2540
|
+
if (!fromScid) {
|
|
2541
|
+
throw new Error('from channel has no SCID or alias yet');
|
|
2542
|
+
}
|
|
2543
|
+
const fromPeerHex = this.channelManager.getPeerForChannel(fromChannelId);
|
|
2544
|
+
if (!fromPeerHex)
|
|
2545
|
+
throw new Error('from channel has no known peer');
|
|
2546
|
+
const toHint = this.buildRoutingHintForChannel(toChannel);
|
|
2547
|
+
if (!toHint) {
|
|
2548
|
+
throw new Error('to channel has no usable routing hint (SCID/alias)');
|
|
2549
|
+
}
|
|
2550
|
+
const excluded = new Set();
|
|
2551
|
+
for (const channel of this.channelManager.listChannels()) {
|
|
2552
|
+
const id = channel.getChannelId();
|
|
2553
|
+
if (id && id.equals(fromChannelId))
|
|
2554
|
+
continue;
|
|
2555
|
+
const st = channel.getFullState();
|
|
2556
|
+
if (st.shortChannelId)
|
|
2557
|
+
excluded.add(st.shortChannelId.toString('hex'));
|
|
2558
|
+
if (st.scidAlias)
|
|
2559
|
+
excluded.add(st.scidAlias.toString('hex'));
|
|
2560
|
+
if (st.remoteScidAlias) {
|
|
2561
|
+
excluded.add(st.remoteScidAlias.toString('hex'));
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
const ourNodeId = (0, ecdh_1.getPublicKey)(this.nodePrivkey);
|
|
2565
|
+
const finalCltvExpiry = types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY;
|
|
2566
|
+
const toPeerFeeMsat = (0, pathfinding_1.calculateFee)(amountMsat, toHint.feeBaseMsat, toHint.feeProportionalMillionths);
|
|
2567
|
+
const subRoute = (0, pathfinding_1.findRoute)(this.graph, ourNodeId, toHint.pubkey, amountMsat + toPeerFeeMsat, finalCltvExpiry + toHint.cltvExpiryDelta, undefined, excluded, this.missionControl, undefined, undefined, undefined, [
|
|
2568
|
+
{
|
|
2569
|
+
shortChannelId: fromScid,
|
|
2570
|
+
peer: Buffer.from(fromPeerHex, 'hex'),
|
|
2571
|
+
outboundMsat: fromState.localBalanceMsat
|
|
2572
|
+
}
|
|
2573
|
+
]);
|
|
2574
|
+
if (!subRoute) {
|
|
2575
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.NO_ROUTE, 'No circular route from fromChannel back to toChannel');
|
|
2576
|
+
}
|
|
2577
|
+
if (!subRoute.hops[0].shortChannelId.equals(fromScid)) {
|
|
2578
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.NO_ROUTE, 'Route does not leave over the requested from channel');
|
|
2579
|
+
}
|
|
2580
|
+
const totalFeeMsat = subRoute.totalAmountMsat - amountMsat;
|
|
2581
|
+
if (totalFeeMsat > maxFeeMsat) {
|
|
2582
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.FEE_EXCEEDS_MAX, `Rebalance fee ${totalFeeMsat} msat exceeds cap ${maxFeeMsat} msat`);
|
|
2583
|
+
}
|
|
2584
|
+
if (subRoute.totalAmountMsat > fromState.remoteConfig.maxHtlcValueInFlightMsat) {
|
|
2585
|
+
throw new Error('amount plus fees exceeds the peer max_htlc_value_in_flight on the from channel');
|
|
2586
|
+
}
|
|
2587
|
+
const invoice = this.createInvoice({
|
|
2588
|
+
amountMsat,
|
|
2589
|
+
description: 'beignet circular rebalance',
|
|
2590
|
+
minFinalCltvExpiry: finalCltvExpiry
|
|
2591
|
+
});
|
|
2592
|
+
const hops = [
|
|
2593
|
+
...subRoute.hops,
|
|
2594
|
+
{
|
|
2595
|
+
pubkey: ourNodeId,
|
|
2596
|
+
shortChannelId: toHint.shortChannelId,
|
|
2597
|
+
amountToForwardMsat: amountMsat,
|
|
2598
|
+
outgoingCltvValue: finalCltvExpiry,
|
|
2599
|
+
cltvExpiryDelta: toHint.cltvExpiryDelta,
|
|
2600
|
+
feeBaseMsat: 0,
|
|
2601
|
+
feeProportionalMillionths: 0
|
|
2602
|
+
}
|
|
2603
|
+
];
|
|
2604
|
+
this.emitStructuredLog('payment', 'rebalance_started', {
|
|
2605
|
+
fromChannelId: fromChannelId.toString('hex'),
|
|
2606
|
+
toChannelId: toChannelId.toString('hex'),
|
|
2607
|
+
amountMsat: amountMsat.toString(),
|
|
2608
|
+
feeMsat: totalFeeMsat.toString(),
|
|
2609
|
+
hops: hops.length
|
|
2610
|
+
});
|
|
2611
|
+
this.sendPaymentToRoute({ hops }, invoice.paymentHash, finalCltvExpiry, invoice.paymentSecret, amountMsat);
|
|
2612
|
+
await this.waitForPayment(invoice.paymentHash, options.timeoutMs ?? 60000);
|
|
2613
|
+
this.emitStructuredLog('payment', 'rebalance_succeeded', {
|
|
2614
|
+
fromChannelId: fromChannelId.toString('hex'),
|
|
2615
|
+
toChannelId: toChannelId.toString('hex'),
|
|
2616
|
+
amountMsat: amountMsat.toString(),
|
|
2617
|
+
feeMsat: totalFeeMsat.toString()
|
|
2618
|
+
});
|
|
2619
|
+
return {
|
|
2620
|
+
paymentHash: invoice.paymentHash,
|
|
2621
|
+
amountMsat,
|
|
2622
|
+
feeMsat: totalFeeMsat,
|
|
2623
|
+
hops: hops.length
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
static currentUtcDay() {
|
|
2627
|
+
return new Date().toISOString().slice(0, 10);
|
|
2628
|
+
}
|
|
2629
|
+
loadRebalanceSpentMsat() {
|
|
2630
|
+
const day = LightningNode.currentUtcDay();
|
|
2631
|
+
if (!this.rebalanceBudgetDay || this.rebalanceBudgetDay.day !== day) {
|
|
2632
|
+
let spent = 0n;
|
|
2633
|
+
if (this.storage) {
|
|
2634
|
+
try {
|
|
2635
|
+
const raw = this.storage.loadMetadata(LightningNode.REBALANCE_BUDGET_KEY);
|
|
2636
|
+
if (raw) {
|
|
2637
|
+
const parsed = JSON.parse(raw);
|
|
2638
|
+
if (parsed.day === day && parsed.spentFeeMsat) {
|
|
2639
|
+
spent = BigInt(parsed.spentFeeMsat);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
catch {
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
this.rebalanceBudgetDay = { day, spentFeeMsat: spent };
|
|
2647
|
+
}
|
|
2648
|
+
return this.rebalanceBudgetDay.spentFeeMsat;
|
|
2649
|
+
}
|
|
2650
|
+
recordRebalanceSpend(feeMsat) {
|
|
2651
|
+
const spent = this.loadRebalanceSpentMsat() + feeMsat;
|
|
2652
|
+
this.rebalanceBudgetDay = {
|
|
2653
|
+
day: LightningNode.currentUtcDay(),
|
|
2654
|
+
spentFeeMsat: spent
|
|
2655
|
+
};
|
|
2656
|
+
this.safeStorage(() => this.storage.saveMetadata(LightningNode.REBALANCE_BUDGET_KEY, JSON.stringify({
|
|
2657
|
+
day: this.rebalanceBudgetDay.day,
|
|
2658
|
+
spentFeeMsat: spent.toString()
|
|
2659
|
+
})), 'saveRebalanceBudget');
|
|
2660
|
+
}
|
|
2661
|
+
async executeRebalanceRecommendations(options) {
|
|
2662
|
+
if (this.rebalanceRunInFlight) {
|
|
2663
|
+
throw new Error('a rebalance execution run is already in progress');
|
|
2664
|
+
}
|
|
2665
|
+
this.rebalanceRunInFlight = true;
|
|
2666
|
+
try {
|
|
2667
|
+
const budgetSats = options?.budgetSatsPerDay ??
|
|
2668
|
+
this.autoRebalanceConfig.budgetSatsPerDay ??
|
|
2669
|
+
1000;
|
|
2670
|
+
if (budgetSats < 0)
|
|
2671
|
+
throw new Error('budgetSatsPerDay must be >= 0');
|
|
2672
|
+
const budgetMsat = BigInt(budgetSats) * 1000n;
|
|
2673
|
+
const plans = this.planRebalanceRecommendations(options?.minImbalancePct);
|
|
2674
|
+
const attempts = [];
|
|
2675
|
+
let feeSpentThisRunMsat = 0n;
|
|
2676
|
+
for (const plan of plans) {
|
|
2677
|
+
const remainingMsat = budgetMsat - this.loadRebalanceSpentMsat();
|
|
2678
|
+
const proportionalCapMsat = (plan.amountSats * 1000n * 5000n) / 1000000n;
|
|
2679
|
+
const perPairCapMsat = proportionalCapMsat > 1000n ? proportionalCapMsat : 1000n;
|
|
2680
|
+
const feeCapMsat = remainingMsat < perPairCapMsat ? remainingMsat : perPairCapMsat;
|
|
2681
|
+
if (feeCapMsat < 1000n) {
|
|
2682
|
+
attempts.push({
|
|
2683
|
+
fromChannelId: plan.fromChannelId,
|
|
2684
|
+
toChannelId: plan.toChannelId,
|
|
2685
|
+
amountSats: plan.amountSats,
|
|
2686
|
+
status: 'SKIPPED_BUDGET'
|
|
2687
|
+
});
|
|
2688
|
+
this.emitStructuredLog('payment', 'rebalance_budget_exhausted', {
|
|
2689
|
+
remainingMsat: remainingMsat.toString(),
|
|
2690
|
+
budgetMsat: budgetMsat.toString()
|
|
2691
|
+
});
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
try {
|
|
2695
|
+
const result = await this.rebalanceChannel({
|
|
2696
|
+
fromChannelId: Buffer.from(plan.fromChannelId, 'hex'),
|
|
2697
|
+
toChannelId: Buffer.from(plan.toChannelId, 'hex'),
|
|
2698
|
+
amountSats: plan.amountSats,
|
|
2699
|
+
maxFeeSats: feeCapMsat / 1000n
|
|
2700
|
+
});
|
|
2701
|
+
this.recordRebalanceSpend(result.feeMsat);
|
|
2702
|
+
feeSpentThisRunMsat += result.feeMsat;
|
|
2703
|
+
attempts.push({
|
|
2704
|
+
fromChannelId: plan.fromChannelId,
|
|
2705
|
+
toChannelId: plan.toChannelId,
|
|
2706
|
+
amountSats: plan.amountSats,
|
|
2707
|
+
status: 'SUCCEEDED',
|
|
2708
|
+
feeMsat: result.feeMsat
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
catch (err) {
|
|
2712
|
+
attempts.push({
|
|
2713
|
+
fromChannelId: plan.fromChannelId,
|
|
2714
|
+
toChannelId: plan.toChannelId,
|
|
2715
|
+
amountSats: plan.amountSats,
|
|
2716
|
+
status: 'FAILED',
|
|
2717
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
const spent = this.loadRebalanceSpentMsat();
|
|
2722
|
+
return {
|
|
2723
|
+
attempts,
|
|
2724
|
+
succeeded: attempts.filter((a) => a.status === 'SUCCEEDED').length,
|
|
2725
|
+
failed: attempts.filter((a) => a.status === 'FAILED').length,
|
|
2726
|
+
skippedBudget: attempts.filter((a) => a.status === 'SKIPPED_BUDGET')
|
|
2727
|
+
.length,
|
|
2728
|
+
feeSpentMsat: feeSpentThisRunMsat,
|
|
2729
|
+
budgetRemainingMsat: budgetMsat > spent ? budgetMsat - spent : 0n
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
finally {
|
|
2733
|
+
this.rebalanceRunInFlight = false;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
runFeeTuneOnce(now = Date.now()) {
|
|
2737
|
+
const intervalMs = this.autoTuneFeesConfig.intervalMs ?? 21600000;
|
|
2738
|
+
const floorPpm = this.autoTuneFeesConfig.floorPpm ?? fee_tuner_1.DEFAULT_FEE_TUNE_FLOOR_PPM;
|
|
2739
|
+
const ceilPpm = this.autoTuneFeesConfig.ceilPpm ?? fee_tuner_1.DEFAULT_FEE_TUNE_CEIL_PPM;
|
|
2740
|
+
if (floorPpm < 0 || ceilPpm < floorPpm) {
|
|
2741
|
+
throw new Error('autoTuneFees requires 0 <= floorPpm <= ceilPpm');
|
|
2742
|
+
}
|
|
2743
|
+
const since = now - intervalMs;
|
|
2744
|
+
const inputs = [];
|
|
2745
|
+
for (const channel of this.channelManager.listChannels()) {
|
|
2746
|
+
if (channel.getState() !== types_1.ChannelState.NORMAL)
|
|
2747
|
+
continue;
|
|
2748
|
+
const channelId = channel.getChannelId();
|
|
2749
|
+
if (!channelId)
|
|
2750
|
+
continue;
|
|
2751
|
+
const hex = channelId.toString('hex');
|
|
2752
|
+
const policy = this.getChannelPolicy(channelId);
|
|
2753
|
+
if (!policy)
|
|
2754
|
+
continue;
|
|
2755
|
+
const st = channel.getFullState();
|
|
2756
|
+
const capacityMsat = st.fundingSatoshis * 1000n;
|
|
2757
|
+
if (capacityMsat <= 0n)
|
|
2758
|
+
continue;
|
|
2759
|
+
const forwards = this.listForwards({ since, until: now, channelId: hex });
|
|
2760
|
+
inputs.push({
|
|
2761
|
+
channelId: hex,
|
|
2762
|
+
currentPpm: policy.feeProportionalMillionths,
|
|
2763
|
+
localBalanceFraction: Number(st.localBalanceMsat) / Number(capacityMsat),
|
|
2764
|
+
outboundForwards: forwards.filter((f) => f.outChannelId === hex).length,
|
|
2765
|
+
totalForwards: forwards.length
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
const adjustments = (0, fee_tuner_1.computeFeeTuneAdjustments)(inputs, {
|
|
2769
|
+
floorPpm,
|
|
2770
|
+
ceilPpm
|
|
2771
|
+
});
|
|
2772
|
+
for (const adj of adjustments) {
|
|
2773
|
+
this.setChannelPolicy(Buffer.from(adj.channelId, 'hex'), {
|
|
2774
|
+
feeProportionalMillionths: adj.newPpm
|
|
2775
|
+
});
|
|
2776
|
+
this.emitStructuredLog('fee', 'auto_tune_adjusted', {
|
|
2777
|
+
channelId: adj.channelId,
|
|
2778
|
+
oldPpm: adj.oldPpm,
|
|
2779
|
+
newPpm: adj.newPpm,
|
|
2780
|
+
reason: adj.reason,
|
|
2781
|
+
windowMs: intervalMs
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
return adjustments;
|
|
2785
|
+
}
|
|
1664
2786
|
getFeeSnapshot() {
|
|
1665
2787
|
return this.feeAdvisor.getSnapshot();
|
|
1666
2788
|
}
|
|
@@ -1717,6 +2839,14 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1717
2839
|
info.localReserveMsat = state.remoteConfig.channelReserveSatoshis * 1000n;
|
|
1718
2840
|
info.remoteReserveMsat = state.localConfig.channelReserveSatoshis * 1000n;
|
|
1719
2841
|
info.isPrivate = !state.announceChannel;
|
|
2842
|
+
const policy = this.getChannelPolicy(channelId);
|
|
2843
|
+
if (policy) {
|
|
2844
|
+
info.feeBaseMsat = policy.feeBaseMsat;
|
|
2845
|
+
info.feeProportionalMillionths = policy.feeProportionalMillionths;
|
|
2846
|
+
info.cltvExpiryDelta = policy.cltvExpiryDelta;
|
|
2847
|
+
info.htlcMinimumMsat = policy.htlcMinimumMsat;
|
|
2848
|
+
info.htlcMaximumMsat = policy.htlcMaximumMsat;
|
|
2849
|
+
}
|
|
1720
2850
|
return info;
|
|
1721
2851
|
}
|
|
1722
2852
|
registerChannelScid(channelId, scid) {
|
|
@@ -1744,9 +2874,13 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1744
2874
|
if (this.alias) {
|
|
1745
2875
|
Buffer.from(this.alias, 'utf8').copy(aliasBuffer, 0, 0, Math.min(32, Buffer.byteLength(this.alias, 'utf8')));
|
|
1746
2876
|
}
|
|
2877
|
+
const announcedFeatures = flags_1.FeatureFlags.empty();
|
|
2878
|
+
if (this.largeChannels) {
|
|
2879
|
+
announcedFeatures.setOptional(flags_1.Feature.LARGE_CHANNELS);
|
|
2880
|
+
}
|
|
1747
2881
|
const payload = encodeNodeAnnouncementMessage({
|
|
1748
2882
|
signature: Buffer.alloc(64),
|
|
1749
|
-
features:
|
|
2883
|
+
features: announcedFeatures.toBuffer(),
|
|
1750
2884
|
timestamp,
|
|
1751
2885
|
nodeId,
|
|
1752
2886
|
rgbColor: Buffer.from([0, 0, 0]),
|
|
@@ -1761,11 +2895,24 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1761
2895
|
return null;
|
|
1762
2896
|
}
|
|
1763
2897
|
}
|
|
1764
|
-
refreshChannelUpdate(cachedUpdate, timestamp) {
|
|
2898
|
+
refreshChannelUpdate(cachedUpdate, timestamp, channelId) {
|
|
1765
2899
|
try {
|
|
1766
2900
|
const { encodeChannelUpdateMessage } = require('../gossip/messages');
|
|
1767
2901
|
const msg = (0, messages_1.decodeChannelUpdateMessage)(cachedUpdate);
|
|
1768
2902
|
msg.timestamp = timestamp;
|
|
2903
|
+
const policy = channelId ? this.getChannelPolicy(channelId) : null;
|
|
2904
|
+
if (policy && channelId) {
|
|
2905
|
+
msg.cltvExpiryDelta = policy.cltvExpiryDelta;
|
|
2906
|
+
msg.feeBaseMsat = policy.feeBaseMsat;
|
|
2907
|
+
msg.feeProportionalMillionths = policy.feeProportionalMillionths;
|
|
2908
|
+
msg.htlcMinimumMsat = policy.htlcMinimumMsat;
|
|
2909
|
+
const capacityMsat = this.channelManager.getChannel(channelId).getFullState()
|
|
2910
|
+
.fundingSatoshis * 1000n;
|
|
2911
|
+
msg.htlcMaximumMsat =
|
|
2912
|
+
policy.htlcMaximumMsat > capacityMsat
|
|
2913
|
+
? capacityMsat
|
|
2914
|
+
: policy.htlcMaximumMsat;
|
|
2915
|
+
}
|
|
1769
2916
|
const payload = encodeChannelUpdateMessage(msg);
|
|
1770
2917
|
const sig = (0, validation_1.signChannelUpdate)(payload, this.nodePrivkey);
|
|
1771
2918
|
sig.copy(payload, 0);
|
|
@@ -1780,11 +2927,11 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1780
2927
|
return;
|
|
1781
2928
|
try {
|
|
1782
2929
|
for (const { announcement, update } of this._ownChannelGossip.values()) {
|
|
1783
|
-
this.peerManager.sendToPeer(pubkey,
|
|
1784
|
-
this.peerManager.sendToPeer(pubkey,
|
|
2930
|
+
this.peerManager.sendToPeer(pubkey, types_5.MessageType.CHANNEL_ANNOUNCEMENT, announcement);
|
|
2931
|
+
this.peerManager.sendToPeer(pubkey, types_5.MessageType.CHANNEL_UPDATE, update);
|
|
1785
2932
|
}
|
|
1786
2933
|
if (this._ownNodeAnnouncement) {
|
|
1787
|
-
this.peerManager.sendToPeer(pubkey,
|
|
2934
|
+
this.peerManager.sendToPeer(pubkey, types_5.MessageType.NODE_ANNOUNCEMENT, this._ownNodeAnnouncement);
|
|
1788
2935
|
}
|
|
1789
2936
|
}
|
|
1790
2937
|
catch {
|
|
@@ -1807,7 +2954,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1807
2954
|
this._ownNodeAnnouncement = refreshed;
|
|
1808
2955
|
}
|
|
1809
2956
|
for (const [channelIdHex, gossip] of this._ownChannelGossip) {
|
|
1810
|
-
const refreshedUpdate = this.refreshChannelUpdate(gossip.update, now);
|
|
2957
|
+
const refreshedUpdate = this.refreshChannelUpdate(gossip.update, now, Buffer.from(channelIdHex, 'hex'));
|
|
1811
2958
|
if (refreshedUpdate) {
|
|
1812
2959
|
this._ownChannelGossip.set(channelIdHex, {
|
|
1813
2960
|
announcement: gossip.announcement,
|
|
@@ -1822,56 +2969,59 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
1822
2969
|
}
|
|
1823
2970
|
getPrivateChannelRoutingHints() {
|
|
1824
2971
|
const hints = [];
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
if (effectiveState !== types_1.ChannelState.NORMAL)
|
|
1830
|
-
continue;
|
|
1831
|
-
const channelId = channel.getChannelId();
|
|
1832
|
-
if (!channelId)
|
|
1833
|
-
continue;
|
|
1834
|
-
const peerPubkeyHex = this.channelManager.getPeerForChannel(channelId);
|
|
1835
|
-
if (!peerPubkeyHex)
|
|
1836
|
-
continue;
|
|
1837
|
-
const scid = state.shortChannelId || state.scidAlias;
|
|
1838
|
-
if (!scid)
|
|
1839
|
-
continue;
|
|
1840
|
-
const peerPubkey = Buffer.from(peerPubkeyHex, 'hex');
|
|
1841
|
-
let feeBaseMsat = this.forwardingFeeBaseMsat;
|
|
1842
|
-
let feeProportionalMillionths = this.forwardingFeePropMillionths;
|
|
1843
|
-
let cltvExpiryDelta = this.forwardingCltvDelta;
|
|
1844
|
-
const directPolicy = state.remoteForwardingPolicy;
|
|
1845
|
-
if (directPolicy) {
|
|
1846
|
-
feeBaseMsat = directPolicy.feeBaseMsat;
|
|
1847
|
-
feeProportionalMillionths = directPolicy.feeProportionalMillionths;
|
|
1848
|
-
cltvExpiryDelta = directPolicy.cltvExpiryDelta;
|
|
1849
|
-
}
|
|
1850
|
-
if (state.shortChannelId) {
|
|
1851
|
-
const graphChannel = this.graph.getChannel(state.shortChannelId);
|
|
1852
|
-
const peerUpdate = graphChannel?.nodeId1.equals(peerPubkey)
|
|
1853
|
-
? graphChannel.update1
|
|
1854
|
-
: graphChannel?.nodeId2.equals(peerPubkey)
|
|
1855
|
-
? graphChannel.update2
|
|
1856
|
-
: undefined;
|
|
1857
|
-
if (peerUpdate &&
|
|
1858
|
-
(!directPolicy || peerUpdate.timestamp >= directPolicy.timestamp)) {
|
|
1859
|
-
feeBaseMsat = peerUpdate.feeBaseMsat;
|
|
1860
|
-
feeProportionalMillionths = peerUpdate.feeProportionalMillionths;
|
|
1861
|
-
cltvExpiryDelta = peerUpdate.cltvExpiryDelta;
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
const hop = {
|
|
1865
|
-
pubkey: peerPubkey,
|
|
1866
|
-
shortChannelId: scid,
|
|
1867
|
-
feeBaseMsat,
|
|
1868
|
-
feeProportionalMillionths,
|
|
1869
|
-
cltvExpiryDelta
|
|
1870
|
-
};
|
|
1871
|
-
hints.push([hop]);
|
|
2972
|
+
for (const channel of this.channelManager.listChannels()) {
|
|
2973
|
+
const hop = this.buildRoutingHintForChannel(channel);
|
|
2974
|
+
if (hop)
|
|
2975
|
+
hints.push([hop]);
|
|
1872
2976
|
}
|
|
1873
2977
|
return hints;
|
|
1874
2978
|
}
|
|
2979
|
+
buildRoutingHintForChannel(channel) {
|
|
2980
|
+
const state = channel.getFullState();
|
|
2981
|
+
const effectiveState = state.preReestablishState ?? channel.getState();
|
|
2982
|
+
if (effectiveState !== types_1.ChannelState.NORMAL)
|
|
2983
|
+
return null;
|
|
2984
|
+
const channelId = channel.getChannelId();
|
|
2985
|
+
if (!channelId)
|
|
2986
|
+
return null;
|
|
2987
|
+
const peerPubkeyHex = this.channelManager.getPeerForChannel(channelId);
|
|
2988
|
+
if (!peerPubkeyHex)
|
|
2989
|
+
return null;
|
|
2990
|
+
const scid = state.shortChannelId || state.scidAlias;
|
|
2991
|
+
if (!scid)
|
|
2992
|
+
return null;
|
|
2993
|
+
const peerPubkey = Buffer.from(peerPubkeyHex, 'hex');
|
|
2994
|
+
let feeBaseMsat = this.forwardingFeeBaseMsat;
|
|
2995
|
+
let feeProportionalMillionths = this.forwardingFeePropMillionths;
|
|
2996
|
+
let cltvExpiryDelta = this.forwardingCltvDelta;
|
|
2997
|
+
const directPolicy = state.remoteForwardingPolicy;
|
|
2998
|
+
if (directPolicy) {
|
|
2999
|
+
feeBaseMsat = directPolicy.feeBaseMsat;
|
|
3000
|
+
feeProportionalMillionths = directPolicy.feeProportionalMillionths;
|
|
3001
|
+
cltvExpiryDelta = directPolicy.cltvExpiryDelta;
|
|
3002
|
+
}
|
|
3003
|
+
if (state.shortChannelId) {
|
|
3004
|
+
const graphChannel = this.graph.getChannel(state.shortChannelId);
|
|
3005
|
+
const peerUpdate = graphChannel?.nodeId1.equals(peerPubkey)
|
|
3006
|
+
? graphChannel.update1
|
|
3007
|
+
: graphChannel?.nodeId2.equals(peerPubkey)
|
|
3008
|
+
? graphChannel.update2
|
|
3009
|
+
: undefined;
|
|
3010
|
+
if (peerUpdate &&
|
|
3011
|
+
(!directPolicy || peerUpdate.timestamp >= directPolicy.timestamp)) {
|
|
3012
|
+
feeBaseMsat = peerUpdate.feeBaseMsat;
|
|
3013
|
+
feeProportionalMillionths = peerUpdate.feeProportionalMillionths;
|
|
3014
|
+
cltvExpiryDelta = peerUpdate.cltvExpiryDelta;
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
return {
|
|
3018
|
+
pubkey: peerPubkey,
|
|
3019
|
+
shortChannelId: scid,
|
|
3020
|
+
feeBaseMsat,
|
|
3021
|
+
feeProportionalMillionths,
|
|
3022
|
+
cltvExpiryDelta
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
1875
3025
|
findBlindedIntroExtension(peerPubkey) {
|
|
1876
3026
|
const ourNodeId = (0, ecdh_1.getPublicKey)(this.nodePrivkey);
|
|
1877
3027
|
for (const edge of this.graph.getNodeChannels(peerPubkey)) {
|
|
@@ -2006,16 +3156,16 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2006
3156
|
}
|
|
2007
3157
|
handleGossipMessage(pubkey, type, payload) {
|
|
2008
3158
|
switch (type) {
|
|
2009
|
-
case
|
|
3159
|
+
case types_5.MessageType.CHANNEL_ANNOUNCEMENT:
|
|
2010
3160
|
this.handleChannelAnnouncement(payload);
|
|
2011
3161
|
break;
|
|
2012
|
-
case
|
|
3162
|
+
case types_5.MessageType.NODE_ANNOUNCEMENT:
|
|
2013
3163
|
this.handleNodeAnnouncement(payload);
|
|
2014
3164
|
break;
|
|
2015
|
-
case
|
|
3165
|
+
case types_5.MessageType.CHANNEL_UPDATE:
|
|
2016
3166
|
this.handleChannelUpdate(payload);
|
|
2017
3167
|
break;
|
|
2018
|
-
case
|
|
3168
|
+
case types_5.MessageType.REPLY_CHANNEL_RANGE: {
|
|
2019
3169
|
const syncMgr = this.gossipSyncManagers.get(pubkey);
|
|
2020
3170
|
if (syncMgr) {
|
|
2021
3171
|
const msg = (0, gossip_queries_1.decodeReplyChannelRangeMessage)(payload);
|
|
@@ -2026,7 +3176,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2026
3176
|
}
|
|
2027
3177
|
break;
|
|
2028
3178
|
}
|
|
2029
|
-
case
|
|
3179
|
+
case types_5.MessageType.REPLY_SHORT_CHANNEL_IDS_END: {
|
|
2030
3180
|
const syncMgr = this.gossipSyncManagers.get(pubkey);
|
|
2031
3181
|
if (syncMgr) {
|
|
2032
3182
|
const msg = (0, gossip_queries_1.decodeReplyShortChannelIdsEndMessage)(payload);
|
|
@@ -2037,7 +3187,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2037
3187
|
}
|
|
2038
3188
|
break;
|
|
2039
3189
|
}
|
|
2040
|
-
case
|
|
3190
|
+
case types_5.MessageType.QUERY_CHANNEL_RANGE: {
|
|
2041
3191
|
const syncMgr = this.getOrCreateSyncManager(pubkey);
|
|
2042
3192
|
const msg = (0, gossip_queries_1.decodeQueryChannelRangeMessage)(payload);
|
|
2043
3193
|
const responses = syncMgr.handleQueryChannelRange(msg);
|
|
@@ -2046,7 +3196,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2046
3196
|
}
|
|
2047
3197
|
break;
|
|
2048
3198
|
}
|
|
2049
|
-
case
|
|
3199
|
+
case types_5.MessageType.QUERY_SHORT_CHANNEL_IDS: {
|
|
2050
3200
|
const syncMgr = this.getOrCreateSyncManager(pubkey);
|
|
2051
3201
|
const msg = (0, gossip_queries_1.decodeQueryShortChannelIdsMessage)(payload);
|
|
2052
3202
|
const responses = syncMgr.handleQueryShortChannelIds(msg);
|
|
@@ -2055,7 +3205,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2055
3205
|
}
|
|
2056
3206
|
break;
|
|
2057
3207
|
}
|
|
2058
|
-
case
|
|
3208
|
+
case types_5.MessageType.GOSSIP_TIMESTAMP_FILTER:
|
|
2059
3209
|
(0, gossip_queries_1.decodeGossipTimestampFilterMessage)(payload);
|
|
2060
3210
|
this.sendOwnGossipTo(pubkey);
|
|
2061
3211
|
break;
|
|
@@ -2237,8 +3387,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2237
3387
|
descriptionHash: options.descriptionHash,
|
|
2238
3388
|
paymentHash,
|
|
2239
3389
|
paymentSecret,
|
|
2240
|
-
expiry: options.expiry ??
|
|
2241
|
-
minFinalCltvExpiry: options.minFinalCltvExpiry ??
|
|
3390
|
+
expiry: options.expiry ?? types_4.DEFAULT_EXPIRY,
|
|
3391
|
+
minFinalCltvExpiry: options.minFinalCltvExpiry ?? types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY,
|
|
2242
3392
|
privateKey: this.nodePrivkey,
|
|
2243
3393
|
payeeNodeKey: (0, ecdh_1.getPublicKey)(this.nodePrivkey),
|
|
2244
3394
|
routingHints: !useBlinded && routingHints.length > 0 ? routingHints : undefined,
|
|
@@ -2249,8 +3399,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2249
3399
|
paymentHash,
|
|
2250
3400
|
preimage,
|
|
2251
3401
|
amountMsat: options.amountMsat || 0n,
|
|
2252
|
-
status:
|
|
2253
|
-
direction:
|
|
3402
|
+
status: types_6.PaymentStatus.PENDING,
|
|
3403
|
+
direction: types_6.PaymentDirection.INCOMING,
|
|
2254
3404
|
createdAt: Date.now()
|
|
2255
3405
|
};
|
|
2256
3406
|
this.payments.set(paymentHash.toString('hex'), payment);
|
|
@@ -2265,7 +3415,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2265
3415
|
bolt11: invoiceStr,
|
|
2266
3416
|
amountMsat: options.amountMsat,
|
|
2267
3417
|
description: options.description,
|
|
2268
|
-
expiry: options.expiry ??
|
|
3418
|
+
expiry: options.expiry ?? types_4.DEFAULT_EXPIRY,
|
|
2269
3419
|
createdAt: createdAtSecs,
|
|
2270
3420
|
hold: options.hold
|
|
2271
3421
|
});
|
|
@@ -2276,7 +3426,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2276
3426
|
bolt11: invoiceStr,
|
|
2277
3427
|
amountMsat: options.amountMsat,
|
|
2278
3428
|
description: options.description,
|
|
2279
|
-
expiry: options.expiry ??
|
|
3429
|
+
expiry: options.expiry ?? types_4.DEFAULT_EXPIRY,
|
|
2280
3430
|
createdAt: createdAtSecs,
|
|
2281
3431
|
hold: options.hold
|
|
2282
3432
|
});
|
|
@@ -2311,27 +3461,27 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2311
3461
|
const invoice = (0, decode_1.decode)(invoiceStr);
|
|
2312
3462
|
const dedupHashHex = invoice.paymentHash.toString('hex');
|
|
2313
3463
|
const existingPayment = this.payments.get(dedupHashHex);
|
|
2314
|
-
if (existingPayment && existingPayment.status ===
|
|
2315
|
-
throw new
|
|
3464
|
+
if (existingPayment && existingPayment.status === types_6.PaymentStatus.PENDING) {
|
|
3465
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.DUPLICATE_PAYMENT, 'Payment already in flight for this invoice');
|
|
2316
3466
|
}
|
|
2317
3467
|
const destination = invoice.payeeNodeKey || invoice.recoveredPubkey;
|
|
2318
3468
|
if (!destination) {
|
|
2319
|
-
throw new
|
|
3469
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.INVALID_INVOICE, 'Cannot determine payee from invoice');
|
|
2320
3470
|
}
|
|
2321
3471
|
let paymentAmountMsat = invoice.amountMsat;
|
|
2322
3472
|
if (paymentAmountMsat === undefined) {
|
|
2323
3473
|
if (amountMsat === undefined) {
|
|
2324
|
-
throw new
|
|
3474
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.MISSING_AMOUNT, 'Invoice has no amount and no amountMsat provided');
|
|
2325
3475
|
}
|
|
2326
3476
|
paymentAmountMsat = amountMsat;
|
|
2327
3477
|
}
|
|
2328
|
-
const expiryTimestamp = invoice.timestamp + (invoice.expiry ??
|
|
3478
|
+
const expiryTimestamp = invoice.timestamp + (invoice.expiry ?? types_4.DEFAULT_EXPIRY);
|
|
2329
3479
|
if (Math.floor(Date.now() / 1000) > expiryTimestamp) {
|
|
2330
3480
|
const payment = {
|
|
2331
3481
|
paymentHash: invoice.paymentHash,
|
|
2332
3482
|
amountMsat: paymentAmountMsat,
|
|
2333
|
-
status:
|
|
2334
|
-
direction:
|
|
3483
|
+
status: types_6.PaymentStatus.FAILED,
|
|
3484
|
+
direction: types_6.PaymentDirection.OUTGOING,
|
|
2335
3485
|
createdAt: Date.now(),
|
|
2336
3486
|
completedAt: Date.now()
|
|
2337
3487
|
};
|
|
@@ -2339,16 +3489,16 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2339
3489
|
this.emit('payment:failed', payment);
|
|
2340
3490
|
return payment;
|
|
2341
3491
|
}
|
|
2342
|
-
const finalCltvExpiry = invoice.minFinalCltvExpiry ??
|
|
3492
|
+
const finalCltvExpiry = invoice.minFinalCltvExpiry ?? types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY;
|
|
2343
3493
|
const sourceNodeId = (0, ecdh_1.getPublicKey)(this.nodePrivkey);
|
|
2344
3494
|
if (invoice.blindedPaths && invoice.blindedPaths.length > 0) {
|
|
2345
3495
|
const blinded = invoice.blindedPaths[0];
|
|
2346
3496
|
const blindedRoute = (0, pathfinding_1.findRouteToBlindedPath)(this.graph, sourceNodeId, blinded.path, blinded.payInfo, paymentAmountMsat, finalCltvExpiry, undefined, excludedChannels, this.missionControl, this.getLocalChannelEdges());
|
|
2347
3497
|
if (!blindedRoute) {
|
|
2348
|
-
throw new
|
|
3498
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.NO_ROUTE, 'No route to blinded path introduction node');
|
|
2349
3499
|
}
|
|
2350
3500
|
if (maxFeeMsat !== undefined && blindedRoute.totalFeeMsat > maxFeeMsat) {
|
|
2351
|
-
throw new
|
|
3501
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.FEE_EXCEEDS_MAX, 'Route fee exceeds maximum');
|
|
2352
3502
|
}
|
|
2353
3503
|
const bHashHex = invoice.paymentHash.toString('hex');
|
|
2354
3504
|
if (!this.paymentRetryContexts.has(bHashHex)) {
|
|
@@ -2369,16 +3519,16 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2369
3519
|
const multiRoute = (0, pathfinding_1.findMultiPathRoute)(this.graph, sourceNodeId, destination, paymentAmountMsat, finalCltvExpiry, undefined, undefined, this.missionControl, invoice.routingHints, undefined, localChannels);
|
|
2370
3520
|
if (multiRoute) {
|
|
2371
3521
|
if (maxFeeMsat !== undefined && multiRoute.totalFeeMsat > maxFeeMsat) {
|
|
2372
|
-
throw new
|
|
3522
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.FEE_EXCEEDS_MAX, 'Route fee exceeds maximum');
|
|
2373
3523
|
}
|
|
2374
3524
|
return this.sendPaymentMpp(invoiceStr, invoice, multiRoute, finalCltvExpiry);
|
|
2375
3525
|
}
|
|
2376
3526
|
}
|
|
2377
3527
|
if (!route) {
|
|
2378
|
-
throw new
|
|
3528
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.NO_ROUTE, 'No route found to destination');
|
|
2379
3529
|
}
|
|
2380
3530
|
if (maxFeeMsat !== undefined && route.totalFeeMsat > maxFeeMsat) {
|
|
2381
|
-
throw new
|
|
3531
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.FEE_EXCEEDS_MAX, 'Route fee exceeds maximum');
|
|
2382
3532
|
}
|
|
2383
3533
|
const hashHex = invoice.paymentHash.toString('hex');
|
|
2384
3534
|
if (!this.paymentRetryContexts.has(hashHex)) {
|
|
@@ -2433,9 +3583,10 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2433
3583
|
const onionPacket = (0, construct_1.constructOnionPacket)(sessionKey, onionHops, paymentHash);
|
|
2434
3584
|
const onionBuf = (0, construct_1.encodeOnionPacket)(onionPacket);
|
|
2435
3585
|
const firstHopPubkey = hops[0].pubkey.toString('hex');
|
|
2436
|
-
const outChannel = this.
|
|
3586
|
+
const outChannel = this.findLocalChannelByScid(hops[0].shortChannelId, firstHopPubkey) ??
|
|
3587
|
+
this.findChannelForPeer(firstHopPubkey, hops[0].amountToForwardMsat);
|
|
2437
3588
|
if (!outChannel) {
|
|
2438
|
-
throw new
|
|
3589
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.NO_CHANNEL_TO_HOP, `No channel to first hop ${firstHopPubkey}`);
|
|
2439
3590
|
}
|
|
2440
3591
|
const channelId = outChannel.getChannelId();
|
|
2441
3592
|
const cltvExpiry = hops[0].outgoingCltvValue + baseHeight;
|
|
@@ -2443,8 +3594,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2443
3594
|
const payment = {
|
|
2444
3595
|
paymentHash,
|
|
2445
3596
|
amountMsat: amount,
|
|
2446
|
-
status:
|
|
2447
|
-
direction:
|
|
3597
|
+
status: types_6.PaymentStatus.PENDING,
|
|
3598
|
+
direction: types_6.PaymentDirection.OUTGOING,
|
|
2448
3599
|
route: route,
|
|
2449
3600
|
sharedSecrets,
|
|
2450
3601
|
createdAt: Date.now()
|
|
@@ -2461,7 +3612,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2461
3612
|
}
|
|
2462
3613
|
const result = this.channelManager.addHtlc(channelId, amount, paymentHash, cltvExpiry, onionBuf);
|
|
2463
3614
|
if (!result.ok) {
|
|
2464
|
-
payment.status =
|
|
3615
|
+
payment.status = types_6.PaymentStatus.FAILED;
|
|
2465
3616
|
payment.completedAt = Date.now();
|
|
2466
3617
|
this.emit('payment:failed', payment);
|
|
2467
3618
|
}
|
|
@@ -2470,31 +3621,31 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2470
3621
|
sendKeysend(options) {
|
|
2471
3622
|
const { destination, amountMsat, maxFeeMsat, customRecords: extraRecords, metadata } = options;
|
|
2472
3623
|
if (!destination || destination.length !== 33) {
|
|
2473
|
-
throw new
|
|
3624
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.INVALID_KEYSEND, 'destination must be a 33-byte compressed public key');
|
|
2474
3625
|
}
|
|
2475
3626
|
if (amountMsat <= 0n) {
|
|
2476
|
-
throw new
|
|
3627
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.INVALID_KEYSEND, 'amountMsat must be positive');
|
|
2477
3628
|
}
|
|
2478
3629
|
const preimage = crypto_1.default.randomBytes(32);
|
|
2479
3630
|
const paymentHash = crypto_1.default.createHash('sha256').update(preimage).digest();
|
|
2480
3631
|
const hashHex = paymentHash.toString('hex');
|
|
2481
3632
|
const existingPayment = this.payments.get(hashHex);
|
|
2482
|
-
if (existingPayment && existingPayment.status ===
|
|
2483
|
-
throw new
|
|
3633
|
+
if (existingPayment && existingPayment.status === types_6.PaymentStatus.PENDING) {
|
|
3634
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.DUPLICATE_PAYMENT, 'Payment already in flight');
|
|
2484
3635
|
}
|
|
2485
|
-
const finalCltvExpiry =
|
|
3636
|
+
const finalCltvExpiry = types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY;
|
|
2486
3637
|
const sourceNodeId = (0, ecdh_1.getPublicKey)(this.nodePrivkey);
|
|
2487
3638
|
const route = (0, pathfinding_1.findRoute)(this.graph, sourceNodeId, destination, amountMsat, finalCltvExpiry, undefined, undefined, this.missionControl, undefined, undefined, undefined, this.getLocalChannelEdges());
|
|
2488
3639
|
if (!route) {
|
|
2489
|
-
throw new
|
|
3640
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.NO_ROUTE, 'No route found to destination');
|
|
2490
3641
|
}
|
|
2491
3642
|
if (maxFeeMsat !== undefined && route.totalFeeMsat > maxFeeMsat) {
|
|
2492
|
-
throw new
|
|
3643
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.FEE_EXCEEDS_MAX, 'Route fee exceeds maximum');
|
|
2493
3644
|
}
|
|
2494
3645
|
const hops = route.hops;
|
|
2495
3646
|
const baseHeight = this.currentBlockHeight;
|
|
2496
3647
|
const keysendRecords = new Map();
|
|
2497
|
-
keysendRecords.set(
|
|
3648
|
+
keysendRecords.set(types_3.KEYSEND_TLV_TYPE, preimage);
|
|
2498
3649
|
if (extraRecords) {
|
|
2499
3650
|
for (const [type, value] of extraRecords) {
|
|
2500
3651
|
keysendRecords.set(type, value);
|
|
@@ -2523,7 +3674,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2523
3674
|
const firstHopPubkey = hops[0].pubkey.toString('hex');
|
|
2524
3675
|
const outChannel = this.findChannelForPeer(firstHopPubkey, hops[0].amountToForwardMsat);
|
|
2525
3676
|
if (!outChannel) {
|
|
2526
|
-
throw new
|
|
3677
|
+
throw new types_6.LightningPaymentError(types_6.LightningErrorCode.NO_CHANNEL_TO_HOP, `No channel to first hop ${firstHopPubkey}`);
|
|
2527
3678
|
}
|
|
2528
3679
|
const channelId = outChannel.getChannelId();
|
|
2529
3680
|
const cltvExpiry = hops[0].outgoingCltvValue + baseHeight;
|
|
@@ -2532,8 +3683,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2532
3683
|
paymentHash,
|
|
2533
3684
|
preimage,
|
|
2534
3685
|
amountMsat: amount,
|
|
2535
|
-
status:
|
|
2536
|
-
direction:
|
|
3686
|
+
status: types_6.PaymentStatus.PENDING,
|
|
3687
|
+
direction: types_6.PaymentDirection.OUTGOING,
|
|
2537
3688
|
route: route,
|
|
2538
3689
|
sharedSecrets,
|
|
2539
3690
|
createdAt: Date.now(),
|
|
@@ -2551,7 +3702,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2551
3702
|
}
|
|
2552
3703
|
const result = this.channelManager.addHtlc(channelId, amount, paymentHash, cltvExpiry, onionBuf);
|
|
2553
3704
|
if (!result.ok) {
|
|
2554
|
-
payment.status =
|
|
3705
|
+
payment.status = types_6.PaymentStatus.FAILED;
|
|
2555
3706
|
payment.completedAt = Date.now();
|
|
2556
3707
|
this.emit('payment:failed', payment);
|
|
2557
3708
|
}
|
|
@@ -2564,8 +3715,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2564
3715
|
const payment = {
|
|
2565
3716
|
paymentHash,
|
|
2566
3717
|
amountMsat: totalMsat,
|
|
2567
|
-
status:
|
|
2568
|
-
direction:
|
|
3718
|
+
status: types_6.PaymentStatus.PENDING,
|
|
3719
|
+
direction: types_6.PaymentDirection.OUTGOING,
|
|
2569
3720
|
createdAt: Date.now()
|
|
2570
3721
|
};
|
|
2571
3722
|
this.payments.set(hashHex, payment);
|
|
@@ -2632,16 +3783,16 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2632
3783
|
channelId,
|
|
2633
3784
|
htlcId,
|
|
2634
3785
|
amountMsat: amount,
|
|
2635
|
-
status:
|
|
3786
|
+
status: types_6.PaymentStatus.PENDING
|
|
2636
3787
|
});
|
|
2637
3788
|
const result = this.channelManager.addHtlc(channelId, amount, paymentHash, cltvExpiry, onionBuf);
|
|
2638
3789
|
if (!result.ok) {
|
|
2639
3790
|
for (const dispatched of mppState.parts) {
|
|
2640
|
-
if (dispatched.status ===
|
|
2641
|
-
this.channelManager.failHtlc(dispatched.channelId, dispatched.htlcId, (0, failures_1.createFailureMessage)(Buffer.alloc(32),
|
|
3791
|
+
if (dispatched.status === types_6.PaymentStatus.PENDING) {
|
|
3792
|
+
this.channelManager.failHtlc(dispatched.channelId, dispatched.htlcId, (0, failures_1.createFailureMessage)(Buffer.alloc(32), types_3.TEMPORARY_CHANNEL_FAILURE));
|
|
2642
3793
|
}
|
|
2643
3794
|
}
|
|
2644
|
-
payment.status =
|
|
3795
|
+
payment.status = types_6.PaymentStatus.FAILED;
|
|
2645
3796
|
payment.completedAt = Date.now();
|
|
2646
3797
|
this.outboundMppPayments.delete(hashHex);
|
|
2647
3798
|
this.emit('payment:failed', payment);
|
|
@@ -2661,12 +3812,12 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2661
3812
|
if (!channel)
|
|
2662
3813
|
return;
|
|
2663
3814
|
if (this.getTotalInFlightHtlcCount() > this.maxTotalInFlightHtlcs) {
|
|
2664
|
-
this.channelManager.failHtlc(channelId, htlcId, (0, failures_1.createFailureMessage)(Buffer.alloc(32),
|
|
3815
|
+
this.channelManager.failHtlc(channelId, htlcId, (0, failures_1.createFailureMessage)(Buffer.alloc(32), types_3.TEMPORARY_NODE_FAILURE));
|
|
2665
3816
|
return;
|
|
2666
3817
|
}
|
|
2667
3818
|
const peerPubkey = this.channelManager.getPeerForChannel(channelId);
|
|
2668
3819
|
if (peerPubkey && !this.rateLimiter.tryConsume(peerPubkey)) {
|
|
2669
|
-
this.channelManager.failHtlc(channelId, htlcId, (0, failures_1.createFailureMessage)(Buffer.alloc(32),
|
|
3820
|
+
this.channelManager.failHtlc(channelId, htlcId, (0, failures_1.createFailureMessage)(Buffer.alloc(32), types_3.TEMPORARY_NODE_FAILURE));
|
|
2670
3821
|
return;
|
|
2671
3822
|
}
|
|
2672
3823
|
const htlcEntry = channel.getFullState().htlcs.get(`received-${htlcId}`);
|
|
@@ -2689,7 +3840,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2689
3840
|
message: `Onion processing failed for HTLC ${htlcId} on channel ${channelId.toString('hex')}: ${err.message || 'unknown'}`,
|
|
2690
3841
|
timestamp: Date.now()
|
|
2691
3842
|
});
|
|
2692
|
-
this.channelManager.failHtlc(channelId, htlcId, (0, failures_1.createFailureMessage)(Buffer.alloc(32),
|
|
3843
|
+
this.channelManager.failHtlc(channelId, htlcId, (0, failures_1.createFailureMessage)(Buffer.alloc(32), types_3.INVALID_ONION_HMAC));
|
|
2693
3844
|
return;
|
|
2694
3845
|
}
|
|
2695
3846
|
const htlcSecretKey = `${channelId.toString('hex')}:${htlcId}`;
|
|
@@ -2712,11 +3863,11 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2712
3863
|
const hashHex = paymentHash.toString('hex');
|
|
2713
3864
|
const htlcSecretKey = `${channelId.toString('hex')}:${htlcId}`;
|
|
2714
3865
|
const sharedSecret = this.receivedHtlcSharedSecrets.get(htlcSecretKey);
|
|
2715
|
-
const keysendPreimage = hopPayload?.customRecords?.get(
|
|
3866
|
+
const keysendPreimage = hopPayload?.customRecords?.get(types_3.KEYSEND_TLV_TYPE);
|
|
2716
3867
|
if (keysendPreimage) {
|
|
2717
3868
|
if (keysendPreimage.length !== 32) {
|
|
2718
3869
|
const reason = sharedSecret
|
|
2719
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
3870
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
|
|
2720
3871
|
: Buffer.alloc(290);
|
|
2721
3872
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
2722
3873
|
this.channelManager.failHtlc(channelId, htlcId, reason);
|
|
@@ -2728,7 +3879,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2728
3879
|
.digest();
|
|
2729
3880
|
if (!expectedHash.equals(paymentHash)) {
|
|
2730
3881
|
const reason = sharedSecret
|
|
2731
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
3882
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
|
|
2732
3883
|
: Buffer.alloc(290);
|
|
2733
3884
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
2734
3885
|
this.channelManager.failHtlc(channelId, htlcId, reason);
|
|
@@ -2739,8 +3890,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2739
3890
|
paymentHash,
|
|
2740
3891
|
preimage: keysendPreimage,
|
|
2741
3892
|
amountMsat,
|
|
2742
|
-
status:
|
|
2743
|
-
direction:
|
|
3893
|
+
status: types_6.PaymentStatus.PENDING,
|
|
3894
|
+
direction: types_6.PaymentDirection.INCOMING,
|
|
2744
3895
|
createdAt: Date.now(),
|
|
2745
3896
|
metadata: { _keysend: 'true' }
|
|
2746
3897
|
};
|
|
@@ -2765,7 +3916,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2765
3916
|
paymentHash: hashHex
|
|
2766
3917
|
});
|
|
2767
3918
|
const reason = sharedSecret
|
|
2768
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
3919
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
|
|
2769
3920
|
: Buffer.alloc(290);
|
|
2770
3921
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
2771
3922
|
this.channelManager.failHtlc(channelId, htlcId, reason);
|
|
@@ -2779,7 +3930,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2779
3930
|
paymentHash: hashHex
|
|
2780
3931
|
});
|
|
2781
3932
|
const reason = sharedSecret
|
|
2782
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
3933
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
|
|
2783
3934
|
: Buffer.alloc(290);
|
|
2784
3935
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
2785
3936
|
this.channelManager.failHtlc(channelId, htlcId, reason);
|
|
@@ -2795,7 +3946,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2795
3946
|
onionCltv: hopPayload.outgoingCltvValue
|
|
2796
3947
|
});
|
|
2797
3948
|
const reason = sharedSecret
|
|
2798
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
3949
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.FINAL_INCORRECT_CLTV_EXPIRY)
|
|
2799
3950
|
: Buffer.alloc(290);
|
|
2800
3951
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
2801
3952
|
this.channelManager.failHtlc(channelId, htlcId, reason);
|
|
@@ -2803,14 +3954,14 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2803
3954
|
}
|
|
2804
3955
|
if (this.currentBlockHeight > 0 &&
|
|
2805
3956
|
incomingCltvExpiry <
|
|
2806
|
-
this.currentBlockHeight +
|
|
3957
|
+
this.currentBlockHeight + types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY) {
|
|
2807
3958
|
this.emitStructuredLog('htlc', 'final_expiry_too_soon', {
|
|
2808
3959
|
paymentHash: hashHex,
|
|
2809
3960
|
htlcCltv: incomingCltvExpiry,
|
|
2810
3961
|
height: this.currentBlockHeight
|
|
2811
3962
|
});
|
|
2812
3963
|
const reason = sharedSecret
|
|
2813
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
3964
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
|
|
2814
3965
|
: Buffer.alloc(290);
|
|
2815
3966
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
2816
3967
|
this.channelManager.failHtlc(channelId, htlcId, reason);
|
|
@@ -2831,7 +3982,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2831
3982
|
invoiced: finalInvoice.amountMsat.toString()
|
|
2832
3983
|
});
|
|
2833
3984
|
const reason = sharedSecret
|
|
2834
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
3985
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
|
|
2835
3986
|
: Buffer.alloc(290);
|
|
2836
3987
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
2837
3988
|
this.channelManager.failHtlc(channelId, htlcId, reason);
|
|
@@ -2891,11 +4042,12 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2891
4042
|
this.persistHeldHtlcs();
|
|
2892
4043
|
const payment = this.payments.get(hashHex);
|
|
2893
4044
|
if (payment) {
|
|
2894
|
-
payment.status =
|
|
4045
|
+
payment.status = types_6.PaymentStatus.COMPLETED;
|
|
2895
4046
|
payment.preimage = pre;
|
|
2896
4047
|
payment.completedAt = Date.now();
|
|
2897
4048
|
this.safeStorage(() => this.persistPayment(paymentHash), 'persistPayment');
|
|
2898
4049
|
this.emit('payment:received', payment);
|
|
4050
|
+
this.emitInvoiceSettled(paymentHash, payment);
|
|
2899
4051
|
}
|
|
2900
4052
|
this.emitStructuredLog('payment', 'received', {
|
|
2901
4053
|
paymentHash: hashHex,
|
|
@@ -2903,7 +4055,21 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2903
4055
|
});
|
|
2904
4056
|
return true;
|
|
2905
4057
|
}
|
|
2906
|
-
|
|
4058
|
+
emitInvoiceSettled(paymentHash, payment) {
|
|
4059
|
+
const invoice = this.invoices.get(paymentHash.toString('hex'));
|
|
4060
|
+
if (!invoice)
|
|
4061
|
+
return;
|
|
4062
|
+
this.emit('invoice:settled', {
|
|
4063
|
+
paymentHash,
|
|
4064
|
+
bolt11: invoice.bolt11,
|
|
4065
|
+
amountMsat: payment.amountMsat
|
|
4066
|
+
});
|
|
4067
|
+
this.emitStructuredLog('payment', 'invoice_settled', {
|
|
4068
|
+
paymentHash: paymentHash.toString('hex'),
|
|
4069
|
+
amountMsat: payment.amountMsat.toString()
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
cancelHeldHtlc(paymentHash, failureCode = types_3.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS) {
|
|
2907
4073
|
const hashHex = paymentHash.toString('hex');
|
|
2908
4074
|
const held = this.heldHtlcs.get(hashHex);
|
|
2909
4075
|
if (!held || held.length === 0)
|
|
@@ -2920,9 +4086,80 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
2920
4086
|
this.heldHtlcs.delete(hashHex);
|
|
2921
4087
|
this.heldInvoiceHashes.delete(hashHex);
|
|
2922
4088
|
this.persistHeldHtlcs();
|
|
4089
|
+
this.markHoldInvoiceCancelled(hashHex);
|
|
2923
4090
|
this.emitStructuredLog('htlc', 'held_cancelled', { paymentHash: hashHex });
|
|
2924
4091
|
return true;
|
|
2925
4092
|
}
|
|
4093
|
+
markHoldInvoiceCancelled(hashHex) {
|
|
4094
|
+
this.preimages.delete(hashHex);
|
|
4095
|
+
this.paymentSecrets.delete(hashHex);
|
|
4096
|
+
this.safeStorage(() => this.storage.deletePaymentSecret(hashHex), 'deletePaymentSecret');
|
|
4097
|
+
const invoice = this.invoices.get(hashHex);
|
|
4098
|
+
if (invoice && !invoice.cancelledAt) {
|
|
4099
|
+
invoice.cancelledAt = Date.now();
|
|
4100
|
+
this.safeStorage(() => this.storage.saveInvoice(hashHex, invoice), 'saveInvoice');
|
|
4101
|
+
}
|
|
4102
|
+
const payment = this.payments.get(hashHex);
|
|
4103
|
+
if (payment &&
|
|
4104
|
+
payment.direction === types_6.PaymentDirection.INCOMING &&
|
|
4105
|
+
payment.status !== types_6.PaymentStatus.COMPLETED) {
|
|
4106
|
+
payment.status = types_6.PaymentStatus.FAILED;
|
|
4107
|
+
this.safeStorage(() => this.persistPayment(Buffer.from(hashHex, 'hex')), 'persistPayment');
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
cancelHoldInvoice(paymentHash) {
|
|
4111
|
+
const hashHex = paymentHash.toString('hex');
|
|
4112
|
+
const held = this.heldHtlcs.get(hashHex);
|
|
4113
|
+
if (held && held.length > 0) {
|
|
4114
|
+
const count = held.length;
|
|
4115
|
+
this.cancelHeldHtlc(paymentHash);
|
|
4116
|
+
return { htlcsFailed: count };
|
|
4117
|
+
}
|
|
4118
|
+
if (!this.heldInvoiceHashes.has(hashHex))
|
|
4119
|
+
return null;
|
|
4120
|
+
this.heldInvoiceHashes.delete(hashHex);
|
|
4121
|
+
this.markHoldInvoiceCancelled(hashHex);
|
|
4122
|
+
this.emitStructuredLog('htlc', 'held_cancelled', { paymentHash: hashHex });
|
|
4123
|
+
return { htlcsFailed: 0 };
|
|
4124
|
+
}
|
|
4125
|
+
listHoldInvoices() {
|
|
4126
|
+
const out = [];
|
|
4127
|
+
for (const [hashHex, invoice] of this.invoices) {
|
|
4128
|
+
if (!invoice.hold)
|
|
4129
|
+
continue;
|
|
4130
|
+
const held = this.heldHtlcs.get(hashHex) ?? [];
|
|
4131
|
+
let heldAmountMsat = 0n;
|
|
4132
|
+
for (const h of held)
|
|
4133
|
+
heldAmountMsat += h.amountMsat;
|
|
4134
|
+
const payment = this.payments.get(hashHex);
|
|
4135
|
+
let state;
|
|
4136
|
+
if (held.length > 0) {
|
|
4137
|
+
state = 'ACCEPTED';
|
|
4138
|
+
}
|
|
4139
|
+
else if (payment?.status === types_6.PaymentStatus.COMPLETED &&
|
|
4140
|
+
payment.direction === types_6.PaymentDirection.INCOMING) {
|
|
4141
|
+
state = 'SETTLED';
|
|
4142
|
+
}
|
|
4143
|
+
else if (invoice.cancelledAt || !this.heldInvoiceHashes.has(hashHex)) {
|
|
4144
|
+
state = 'CANCELLED';
|
|
4145
|
+
}
|
|
4146
|
+
else {
|
|
4147
|
+
state = 'OPEN';
|
|
4148
|
+
}
|
|
4149
|
+
out.push({
|
|
4150
|
+
paymentHash: hashHex,
|
|
4151
|
+
bolt11: invoice.bolt11,
|
|
4152
|
+
amountMsat: invoice.amountMsat,
|
|
4153
|
+
description: invoice.description,
|
|
4154
|
+
expiry: invoice.expiry,
|
|
4155
|
+
createdAt: invoice.createdAt,
|
|
4156
|
+
state,
|
|
4157
|
+
heldAmountMsat,
|
|
4158
|
+
htlcCount: held.length
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
return out;
|
|
4162
|
+
}
|
|
2926
4163
|
scanExpiringHeldHtlcs(height) {
|
|
2927
4164
|
if (height <= 0)
|
|
2928
4165
|
return;
|
|
@@ -3006,7 +4243,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3006
4243
|
channelId,
|
|
3007
4244
|
htlcId,
|
|
3008
4245
|
amountMsat,
|
|
3009
|
-
status:
|
|
4246
|
+
status: types_6.PaymentStatus.PENDING
|
|
3010
4247
|
};
|
|
3011
4248
|
pending.receivedParts.push(part);
|
|
3012
4249
|
let totalReceived = 0n;
|
|
@@ -3016,16 +4253,17 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3016
4253
|
if (totalReceived >= pending.totalMsat) {
|
|
3017
4254
|
this.channelManager.recordPreimage(paymentHash, preimage);
|
|
3018
4255
|
for (const p of pending.receivedParts) {
|
|
3019
|
-
p.status =
|
|
4256
|
+
p.status = types_6.PaymentStatus.COMPLETED;
|
|
3020
4257
|
this.channelManager.fulfillHtlc(p.channelId, p.htlcId, preimage);
|
|
3021
4258
|
}
|
|
3022
4259
|
this.pendingMppPayments.delete(hashHex);
|
|
3023
4260
|
const payment = this.payments.get(hashHex);
|
|
3024
4261
|
if (payment) {
|
|
3025
|
-
payment.status =
|
|
4262
|
+
payment.status = types_6.PaymentStatus.COMPLETED;
|
|
3026
4263
|
payment.completedAt = Date.now();
|
|
3027
4264
|
this.persistPayment(paymentHash);
|
|
3028
4265
|
this.emit('payment:received', payment);
|
|
4266
|
+
this.emitInvoiceSettled(paymentHash, payment);
|
|
3029
4267
|
}
|
|
3030
4268
|
}
|
|
3031
4269
|
}
|
|
@@ -3034,12 +4272,12 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3034
4272
|
for (const [hashHex, pending] of this.pendingMppPayments) {
|
|
3035
4273
|
if (now - pending.createdAt > this.mppTimeoutMs) {
|
|
3036
4274
|
for (const part of pending.receivedParts) {
|
|
3037
|
-
if (part.status ===
|
|
3038
|
-
part.status =
|
|
4275
|
+
if (part.status === types_6.PaymentStatus.PENDING) {
|
|
4276
|
+
part.status = types_6.PaymentStatus.FAILED;
|
|
3039
4277
|
const htlcSecretKey = `${part.channelId.toString('hex')}:${part.htlcId}`;
|
|
3040
4278
|
const sharedSecret = this.receivedHtlcSharedSecrets.get(htlcSecretKey);
|
|
3041
4279
|
const reason = sharedSecret
|
|
3042
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
4280
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.MPP_TIMEOUT)
|
|
3043
4281
|
: Buffer.alloc(290);
|
|
3044
4282
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
3045
4283
|
this.channelManager.failHtlc(part.channelId, part.htlcId, reason);
|
|
@@ -3056,7 +4294,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3056
4294
|
this.paymentSecrets.delete(hashHex);
|
|
3057
4295
|
const payment = this.payments.get(hashHex);
|
|
3058
4296
|
if (payment) {
|
|
3059
|
-
payment.status =
|
|
4297
|
+
payment.status = types_6.PaymentStatus.COMPLETED;
|
|
3060
4298
|
payment.completedAt = Date.now();
|
|
3061
4299
|
}
|
|
3062
4300
|
if (this.storage) {
|
|
@@ -3075,6 +4313,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3075
4313
|
}
|
|
3076
4314
|
if (payment) {
|
|
3077
4315
|
this.emit('payment:received', payment);
|
|
4316
|
+
this.emitInvoiceSettled(paymentHash, payment);
|
|
3078
4317
|
this.emitStructuredLog('payment', 'received', {
|
|
3079
4318
|
paymentHash: hashHex,
|
|
3080
4319
|
amountMsat: Number(payment.amountMsat),
|
|
@@ -3117,39 +4356,41 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3117
4356
|
const forwardCltv = blindedOutCltv ?? hopPayload.outgoingCltvValue;
|
|
3118
4357
|
if (!outgoingScid) {
|
|
3119
4358
|
this.cleanupHtlcSharedSecret(inHtlcSecretKey);
|
|
3120
|
-
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret,
|
|
4359
|
+
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret, types_3.UNKNOWN_NEXT_PEER));
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
const scidHex = outgoingScid.toString('hex');
|
|
4363
|
+
const outChannelId = this.scidToChannelId.get(scidHex);
|
|
4364
|
+
if (!outChannelId) {
|
|
4365
|
+
this.cleanupHtlcSharedSecret(inHtlcSecretKey);
|
|
4366
|
+
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret, types_3.UNKNOWN_NEXT_PEER));
|
|
3121
4367
|
return;
|
|
3122
4368
|
}
|
|
4369
|
+
const outPolicy = this.getForwardingPolicyForChannel(outChannelId);
|
|
3123
4370
|
if (isBlindedForward) {
|
|
3124
4371
|
if (forwardAmount <= 0n ||
|
|
3125
|
-
incomingCltvExpiry - forwardCltv <
|
|
4372
|
+
incomingCltvExpiry - forwardCltv < outPolicy.cltvExpiryDelta ||
|
|
3126
4373
|
(blindedMaxCltv !== undefined && incomingCltvExpiry > blindedMaxCltv)) {
|
|
3127
4374
|
this.cleanupHtlcSharedSecret(inHtlcSecretKey);
|
|
3128
|
-
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret,
|
|
4375
|
+
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret, types_3.INCORRECT_CLTV_EXPIRY));
|
|
3129
4376
|
return;
|
|
3130
4377
|
}
|
|
3131
4378
|
}
|
|
3132
4379
|
else {
|
|
3133
|
-
if (incomingCltvExpiry < forwardCltv +
|
|
4380
|
+
if (incomingCltvExpiry < forwardCltv + outPolicy.cltvExpiryDelta) {
|
|
3134
4381
|
this.cleanupHtlcSharedSecret(inHtlcSecretKey);
|
|
3135
|
-
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret,
|
|
4382
|
+
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret, types_3.INCORRECT_CLTV_EXPIRY));
|
|
3136
4383
|
return;
|
|
3137
4384
|
}
|
|
3138
|
-
const requiredFee = BigInt(
|
|
3139
|
-
(forwardAmount * BigInt(
|
|
4385
|
+
const requiredFee = BigInt(outPolicy.feeBaseMsat) +
|
|
4386
|
+
(forwardAmount * BigInt(outPolicy.feeProportionalMillionths)) /
|
|
4387
|
+
1000000n;
|
|
3140
4388
|
if (incomingAmountMsat < forwardAmount + requiredFee) {
|
|
3141
4389
|
this.cleanupHtlcSharedSecret(inHtlcSecretKey);
|
|
3142
|
-
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret,
|
|
4390
|
+
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret, types_3.FEE_INSUFFICIENT));
|
|
3143
4391
|
return;
|
|
3144
4392
|
}
|
|
3145
4393
|
}
|
|
3146
|
-
const scidHex = outgoingScid.toString('hex');
|
|
3147
|
-
const outChannelId = this.scidToChannelId.get(scidHex);
|
|
3148
|
-
if (!outChannelId) {
|
|
3149
|
-
this.cleanupHtlcSharedSecret(inHtlcSecretKey);
|
|
3150
|
-
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret, types_2.UNKNOWN_NEXT_PEER));
|
|
3151
|
-
return;
|
|
3152
|
-
}
|
|
3153
4394
|
const performForward = () => {
|
|
3154
4395
|
const nextOnionBuf = (0, construct_1.encodeOnionPacket)(nextPacket);
|
|
3155
4396
|
const outChannel = this.channelManager.getChannel(outChannelId);
|
|
@@ -3163,7 +4404,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3163
4404
|
if (!result.ok) {
|
|
3164
4405
|
this.forwardedHtlcs.delete(outKey);
|
|
3165
4406
|
this.cleanupHtlcSharedSecret(inHtlcSecretKey);
|
|
3166
|
-
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret,
|
|
4407
|
+
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret, types_3.TEMPORARY_CHANNEL_FAILURE));
|
|
3167
4408
|
return;
|
|
3168
4409
|
}
|
|
3169
4410
|
this.emit('htlc:forward', inChannelId, outChannelId, forwardAmount, paymentHash);
|
|
@@ -3184,7 +4425,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3184
4425
|
fail: () => {
|
|
3185
4426
|
this.heldForwards.delete(hashHex);
|
|
3186
4427
|
this.cleanupHtlcSharedSecret(inHtlcSecretKey);
|
|
3187
|
-
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret,
|
|
4428
|
+
this.channelManager.failHtlc(inChannelId, inHtlcId, (0, failures_1.createFailureMessage)(sharedSecret, types_3.UNKNOWN_NEXT_PEER));
|
|
3188
4429
|
}
|
|
3189
4430
|
});
|
|
3190
4431
|
this.emit('htlc:held-forward', {
|
|
@@ -3235,6 +4476,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3235
4476
|
if (forward) {
|
|
3236
4477
|
this.cleanupHtlcSharedSecret(`${forward.inChannelId.toString('hex')}:${forward.inHtlcId}`);
|
|
3237
4478
|
this.channelManager.recordPreimage(preimageHash, preimage);
|
|
4479
|
+
this.recordForwardingEvent(channelId, htlcId, forward);
|
|
3238
4480
|
this.safeStorage(() => this.storage.deleteForwardedHtlc(outKey), 'deleteForwardedHtlc');
|
|
3239
4481
|
this.channelManager.fulfillHtlc(forward.inChannelId, forward.inHtlcId, preimage);
|
|
3240
4482
|
this.forwardedHtlcs.delete(outKey);
|
|
@@ -3245,10 +4487,10 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3245
4487
|
const hashHex = paymentHash.toString('hex');
|
|
3246
4488
|
const payment = this.payments.get(hashHex);
|
|
3247
4489
|
if (payment &&
|
|
3248
|
-
payment.direction ===
|
|
3249
|
-
(payment.status ===
|
|
3250
|
-
payment.status ===
|
|
3251
|
-
payment.status =
|
|
4490
|
+
payment.direction === types_6.PaymentDirection.OUTGOING &&
|
|
4491
|
+
(payment.status === types_6.PaymentStatus.PENDING ||
|
|
4492
|
+
payment.status === types_6.PaymentStatus.FAILED)) {
|
|
4493
|
+
payment.status = types_6.PaymentStatus.COMPLETED;
|
|
3252
4494
|
payment.preimage = preimage;
|
|
3253
4495
|
payment.completedAt = Date.now();
|
|
3254
4496
|
const retryCtx = this.paymentRetryContexts.get(hashHex);
|
|
@@ -3285,6 +4527,41 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3285
4527
|
});
|
|
3286
4528
|
}
|
|
3287
4529
|
}
|
|
4530
|
+
recordForwardingEvent(outChannelId, outHtlcId, forward) {
|
|
4531
|
+
const outState = this.channelManager
|
|
4532
|
+
.getChannel(outChannelId)
|
|
4533
|
+
?.getFullState();
|
|
4534
|
+
const inState = this.channelManager
|
|
4535
|
+
.getChannel(forward.inChannelId)
|
|
4536
|
+
?.getFullState();
|
|
4537
|
+
const outHtlc = outState?.htlcs.get(`offered-${outHtlcId}`);
|
|
4538
|
+
const inHtlc = inState?.htlcs.get(`received-${forward.inHtlcId}`);
|
|
4539
|
+
if (!outHtlc || !inHtlc)
|
|
4540
|
+
return;
|
|
4541
|
+
const amountInMsat = inHtlc.amountMsat;
|
|
4542
|
+
const amountOutMsat = outHtlc.amountMsat;
|
|
4543
|
+
this.emit('htlc:forwarded', {
|
|
4544
|
+
inChannelId: forward.inChannelId,
|
|
4545
|
+
outChannelId,
|
|
4546
|
+
amountInMsat,
|
|
4547
|
+
amountOutMsat,
|
|
4548
|
+
feeMsat: amountInMsat - amountOutMsat
|
|
4549
|
+
});
|
|
4550
|
+
if (!this.storage ||
|
|
4551
|
+
typeof this.storage.saveForwardingEvent !== 'function') {
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
this.safeStorage(() => this.storage.saveForwardingEvent({
|
|
4555
|
+
settledAt: Date.now(),
|
|
4556
|
+
inChannelId: forward.inChannelId.toString('hex'),
|
|
4557
|
+
outChannelId: outChannelId.toString('hex'),
|
|
4558
|
+
inScid: inState?.shortChannelId?.toString('hex'),
|
|
4559
|
+
outScid: outState?.shortChannelId?.toString('hex'),
|
|
4560
|
+
amountInMsat,
|
|
4561
|
+
amountOutMsat,
|
|
4562
|
+
feeMsat: amountInMsat - amountOutMsat
|
|
4563
|
+
}), 'saveForwardingEvent');
|
|
4564
|
+
}
|
|
3288
4565
|
handleHtlcFailed(channelId, htlcId, reason) {
|
|
3289
4566
|
const outKey = `${channelId.toString('hex')}:offered-${htlcId}`;
|
|
3290
4567
|
const forward = this.forwardedHtlcs.get(outKey);
|
|
@@ -3306,7 +4583,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3306
4583
|
if (!hashHex)
|
|
3307
4584
|
return;
|
|
3308
4585
|
const payment = this.payments.get(hashHex);
|
|
3309
|
-
if (!payment || payment.direction !==
|
|
4586
|
+
if (!payment || payment.direction !== types_6.PaymentDirection.OUTGOING)
|
|
3310
4587
|
return;
|
|
3311
4588
|
let failureData;
|
|
3312
4589
|
if (payment.sharedSecrets && reason.length > 0) {
|
|
@@ -3356,7 +4633,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3356
4633
|
}
|
|
3357
4634
|
retryCtx.retryCount++;
|
|
3358
4635
|
payment.retryCount = retryCtx.retryCount;
|
|
3359
|
-
payment.status =
|
|
4636
|
+
payment.status = types_6.PaymentStatus.PENDING;
|
|
3360
4637
|
payment.failureCode = undefined;
|
|
3361
4638
|
payment.failureSourceIndex = undefined;
|
|
3362
4639
|
payment.completedAt = undefined;
|
|
@@ -3368,7 +4645,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3368
4645
|
}
|
|
3369
4646
|
}
|
|
3370
4647
|
this.paymentRetryContexts.delete(hashHex);
|
|
3371
|
-
payment.status =
|
|
4648
|
+
payment.status = types_6.PaymentStatus.FAILED;
|
|
3372
4649
|
payment.completedAt = Date.now();
|
|
3373
4650
|
this.htlcPaymentMap.delete(key);
|
|
3374
4651
|
if (this.storage) {
|
|
@@ -3391,7 +4668,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3391
4668
|
return true;
|
|
3392
4669
|
if (failureCode & 0x8000)
|
|
3393
4670
|
return true;
|
|
3394
|
-
if (failureCode ===
|
|
4671
|
+
if (failureCode === types_3.EXPIRY_TOO_FAR)
|
|
3395
4672
|
return true;
|
|
3396
4673
|
return false;
|
|
3397
4674
|
}
|
|
@@ -3401,12 +4678,36 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3401
4678
|
listPayments() {
|
|
3402
4679
|
return [...this.payments.values()];
|
|
3403
4680
|
}
|
|
4681
|
+
listForwards(filter) {
|
|
4682
|
+
if (!this.storage ||
|
|
4683
|
+
typeof this.storage.listForwardingEvents !== 'function') {
|
|
4684
|
+
return [];
|
|
4685
|
+
}
|
|
4686
|
+
try {
|
|
4687
|
+
return this.storage.listForwardingEvents(filter);
|
|
4688
|
+
}
|
|
4689
|
+
catch {
|
|
4690
|
+
return [];
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
getForwardingSummary(options) {
|
|
4694
|
+
if (!this.storage ||
|
|
4695
|
+
typeof this.storage.getForwardingSummary !== 'function') {
|
|
4696
|
+
return { count: 0, volumeOutMsat: 0n, feesEarnedMsat: 0n };
|
|
4697
|
+
}
|
|
4698
|
+
try {
|
|
4699
|
+
return this.storage.getForwardingSummary(options);
|
|
4700
|
+
}
|
|
4701
|
+
catch {
|
|
4702
|
+
return { count: 0, volumeOutMsat: 0n, feesEarnedMsat: 0n };
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
3404
4705
|
getPaymentProof(paymentHash) {
|
|
3405
4706
|
const hashHex = paymentHash.toString('hex');
|
|
3406
4707
|
const payment = this.payments.get(hashHex);
|
|
3407
4708
|
if (!payment)
|
|
3408
4709
|
return null;
|
|
3409
|
-
if (payment.status !==
|
|
4710
|
+
if (payment.status !== types_6.PaymentStatus.COMPLETED)
|
|
3410
4711
|
return null;
|
|
3411
4712
|
if (!payment.preimage)
|
|
3412
4713
|
return null;
|
|
@@ -3444,7 +4745,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3444
4745
|
if (!destination)
|
|
3445
4746
|
return null;
|
|
3446
4747
|
const sourceBuf = Buffer.from(this.nodeId, 'hex');
|
|
3447
|
-
const route = (0, pathfinding_1.findRoute)(this.graph, sourceBuf, destination, amountMsat, decoded.minFinalCltvExpiry ||
|
|
4748
|
+
const route = (0, pathfinding_1.findRoute)(this.graph, sourceBuf, destination, amountMsat, decoded.minFinalCltvExpiry || types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY, 20, undefined, this.missionControl, undefined, decoded.routingHints, undefined, this.getLocalChannelEdges());
|
|
3448
4749
|
if (!route)
|
|
3449
4750
|
return null;
|
|
3450
4751
|
return {
|
|
@@ -3469,7 +4770,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3469
4770
|
if (!destination)
|
|
3470
4771
|
return null;
|
|
3471
4772
|
const sourceBuf = Buffer.from(this.nodeId, 'hex');
|
|
3472
|
-
const route = (0, pathfinding_1.findRoute)(this.graph, sourceBuf, destination, amountMsat, decoded.minFinalCltvExpiry ||
|
|
4773
|
+
const route = (0, pathfinding_1.findRoute)(this.graph, sourceBuf, destination, amountMsat, decoded.minFinalCltvExpiry || types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY, 20, undefined, this.missionControl, undefined, decoded.routingHints, undefined, this.getLocalChannelEdges());
|
|
3473
4774
|
if (!route)
|
|
3474
4775
|
return null;
|
|
3475
4776
|
let successProbability = 1.0;
|
|
@@ -3492,7 +4793,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3492
4793
|
const estimatedTimeMs = hopCount * 2000;
|
|
3493
4794
|
let alternativeAvailable = false;
|
|
3494
4795
|
try {
|
|
3495
|
-
const altRoute = (0, pathfinding_1.findMultiPathRoute)(this.graph, sourceBuf, destination, amountMsat, decoded.minFinalCltvExpiry ||
|
|
4796
|
+
const altRoute = (0, pathfinding_1.findMultiPathRoute)(this.graph, sourceBuf, destination, amountMsat, decoded.minFinalCltvExpiry || types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY, undefined, undefined, this.missionControl, decoded.routingHints, undefined, this.getLocalChannelEdges());
|
|
3496
4797
|
alternativeAvailable = altRoute !== null && altRoute.parts.length > 1;
|
|
3497
4798
|
}
|
|
3498
4799
|
catch {
|
|
@@ -3526,7 +4827,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3526
4827
|
const amountMsat = BigInt(amountSats) * 1000n;
|
|
3527
4828
|
const destBuf = Buffer.from(destination, 'hex');
|
|
3528
4829
|
const sourceBuf = Buffer.from(this.nodeId, 'hex');
|
|
3529
|
-
const route = (0, pathfinding_1.findRoute)(this.graph, sourceBuf, destBuf, amountMsat,
|
|
4830
|
+
const route = (0, pathfinding_1.findRoute)(this.graph, sourceBuf, destBuf, amountMsat, types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY, 20, undefined, this.missionControl, undefined, undefined, undefined, this.getLocalChannelEdges());
|
|
3530
4831
|
if (!route)
|
|
3531
4832
|
return { success: false };
|
|
3532
4833
|
if (route.hops.length > 0) {
|
|
@@ -3542,6 +4843,10 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3542
4843
|
return { success: false };
|
|
3543
4844
|
}
|
|
3544
4845
|
}
|
|
4846
|
+
queryRoute(destination, amountMsat, finalCltvExpiry = types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY) {
|
|
4847
|
+
const sourceBuf = Buffer.from(this.nodeId, 'hex');
|
|
4848
|
+
return (0, pathfinding_1.findRoute)(this.graph, sourceBuf, destination, amountMsat, finalCltvExpiry, undefined, undefined, this.missionControl, undefined, undefined, undefined, this.getLocalChannelEdges());
|
|
4849
|
+
}
|
|
3545
4850
|
handlePeerMessage(pubkey, type, payload) {
|
|
3546
4851
|
if (Buffer.isBuffer(payload) && payload.length > validation_2.MAX_MESSAGE_SIZE) {
|
|
3547
4852
|
this.emit('node:error', {
|
|
@@ -3551,17 +4856,17 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3551
4856
|
});
|
|
3552
4857
|
return;
|
|
3553
4858
|
}
|
|
3554
|
-
if (type ===
|
|
3555
|
-
type ===
|
|
3556
|
-
type ===
|
|
3557
|
-
type ===
|
|
3558
|
-
type ===
|
|
3559
|
-
type ===
|
|
3560
|
-
type ===
|
|
3561
|
-
type ===
|
|
4859
|
+
if (type === types_5.MessageType.CHANNEL_ANNOUNCEMENT ||
|
|
4860
|
+
type === types_5.MessageType.NODE_ANNOUNCEMENT ||
|
|
4861
|
+
type === types_5.MessageType.CHANNEL_UPDATE ||
|
|
4862
|
+
type === types_5.MessageType.QUERY_CHANNEL_RANGE ||
|
|
4863
|
+
type === types_5.MessageType.REPLY_CHANNEL_RANGE ||
|
|
4864
|
+
type === types_5.MessageType.QUERY_SHORT_CHANNEL_IDS ||
|
|
4865
|
+
type === types_5.MessageType.REPLY_SHORT_CHANNEL_IDS_END ||
|
|
4866
|
+
type === types_5.MessageType.GOSSIP_TIMESTAMP_FILTER) {
|
|
3562
4867
|
this.handleGossipMessage(pubkey, type, payload);
|
|
3563
4868
|
}
|
|
3564
|
-
if (type ===
|
|
4869
|
+
if (type === types_5.MessageType.ONION_MESSAGE) {
|
|
3565
4870
|
this.onionMessageManager.handleMessage(pubkey, payload);
|
|
3566
4871
|
}
|
|
3567
4872
|
this.channelManager.handleMessage(pubkey, type, payload);
|
|
@@ -3593,7 +4898,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3593
4898
|
if (this.feeEstimator) {
|
|
3594
4899
|
this.feeEstimator
|
|
3595
4900
|
.estimateFee(6)
|
|
3596
|
-
.then((
|
|
4901
|
+
.then((rawSatPerVbyte) => {
|
|
4902
|
+
const satPerVbyte = this.clampEstimatedFeeRate(rawSatPerVbyte);
|
|
3597
4903
|
if (satPerVbyte > 0) {
|
|
3598
4904
|
this.feeAdvisor.recordSample(satPerVbyte);
|
|
3599
4905
|
for (const monitor of this.channelManager.getMonitors().values()) {
|
|
@@ -3614,6 +4920,14 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3614
4920
|
return FORCE_CLOSE_DEFAULT_SAT_PER_VBYTE;
|
|
3615
4921
|
return Math.max(Math.ceil(live * FORCE_CLOSE_FEE_MULTIPLIER), FORCE_CLOSE_DEFAULT_SAT_PER_VBYTE);
|
|
3616
4922
|
}
|
|
4923
|
+
clampEstimatedFeeRate(satPerVbyte) {
|
|
4924
|
+
return (0, types_6.clampFeeRateSatPerVbyte)(satPerVbyte, (original, clamped) => {
|
|
4925
|
+
this.emitStructuredLog('fee', 'estimate_clamped', {
|
|
4926
|
+
original,
|
|
4927
|
+
clamped
|
|
4928
|
+
});
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
3617
4931
|
scanExpiringHtlcs(blockHeight) {
|
|
3618
4932
|
const claimBuffer = Math.max(LightningNode.INBOUND_HTLC_CLAIM_FORCE_CLOSE_BUFFER, this.htlcSafetyMargin);
|
|
3619
4933
|
const channels = this.channelManager.listChannels();
|
|
@@ -3626,8 +4940,13 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3626
4940
|
if (!key.startsWith('received-'))
|
|
3627
4941
|
continue;
|
|
3628
4942
|
const paymentHashHex = htlc.paymentHash?.toString('hex');
|
|
3629
|
-
const
|
|
3630
|
-
|
|
4943
|
+
const parkedHold = htlc.state !== types_1.HtlcState.FULFILLED &&
|
|
4944
|
+
paymentHashHex !== undefined &&
|
|
4945
|
+
this.heldInvoiceHashes.has(paymentHashHex);
|
|
4946
|
+
const haveClaim = !parkedHold &&
|
|
4947
|
+
(htlc.state === types_1.HtlcState.FULFILLED ||
|
|
4948
|
+
(paymentHashHex !== undefined &&
|
|
4949
|
+
this.preimages.has(paymentHashHex)));
|
|
3631
4950
|
if (haveClaim && htlc.cltvExpiry - blockHeight <= claimBuffer) {
|
|
3632
4951
|
const channelId = state.channelId || state.temporaryChannelId;
|
|
3633
4952
|
this.emit('node:error', {
|
|
@@ -3647,7 +4966,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3647
4966
|
const htlcSecretKey = `${channelId.toString('hex')}:${htlc.id}`;
|
|
3648
4967
|
const htlcSharedSecret = this.receivedHtlcSharedSecrets.get(htlcSecretKey);
|
|
3649
4968
|
const reason = htlcSharedSecret
|
|
3650
|
-
? (0, failures_1.createFailureMessage)(htlcSharedSecret,
|
|
4969
|
+
? (0, failures_1.createFailureMessage)(htlcSharedSecret, types_3.EXPIRY_TOO_SOON)
|
|
3651
4970
|
: Buffer.alloc(290);
|
|
3652
4971
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
3653
4972
|
this.channelManager.failHtlc(channelId, htlc.id, reason);
|
|
@@ -3689,7 +5008,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3689
5008
|
const htlcSecretKey = `${channelId.toString('hex')}:${htlc.id}`;
|
|
3690
5009
|
const sharedSecret = this.receivedHtlcSharedSecrets.get(htlcSecretKey);
|
|
3691
5010
|
const reason = sharedSecret
|
|
3692
|
-
? (0, failures_1.createFailureMessage)(sharedSecret,
|
|
5011
|
+
? (0, failures_1.createFailureMessage)(sharedSecret, types_3.EXPIRY_TOO_SOON)
|
|
3693
5012
|
: Buffer.alloc(290);
|
|
3694
5013
|
this.cleanupHtlcSharedSecret(htlcSecretKey);
|
|
3695
5014
|
this.channelManager.failHtlc(channelId, htlc.id, reason);
|
|
@@ -3775,8 +5094,13 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3775
5094
|
feeEstimator: options?.feeEstimator,
|
|
3776
5095
|
socks5Proxy: options?.socks5Proxy,
|
|
3777
5096
|
preferAnchors: options?.preferAnchors,
|
|
5097
|
+
largeChannels: options?.largeChannels,
|
|
3778
5098
|
chainBackend: options?.chainBackend,
|
|
3779
5099
|
sweepDestinationScript: options?.sweepDestinationScript,
|
|
5100
|
+
peerStorageEnabled: options?.peerStorageEnabled,
|
|
5101
|
+
autoRebalance: options?.autoRebalance,
|
|
5102
|
+
autoTuneFees: options?.autoTuneFees,
|
|
5103
|
+
watchtowers: options?.watchtowers,
|
|
3780
5104
|
channelKeyDeriver
|
|
3781
5105
|
});
|
|
3782
5106
|
}
|
|
@@ -3797,6 +5121,9 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3797
5121
|
flags.setOptional(flags_1.Feature.ANCHOR_ZERO_FEE_HTLC);
|
|
3798
5122
|
flags.setOptional(flags_1.Feature.SHUTDOWN_ANY_SEGWIT);
|
|
3799
5123
|
flags.setOptional(flags_1.Feature.SIMPLE_CLOSE);
|
|
5124
|
+
flags.setOptional(flags_1.Feature.DUAL_FUND);
|
|
5125
|
+
flags.setOptional(flags_1.Feature.ZERO_CONF);
|
|
5126
|
+
flags.setOptional(flags_1.Feature.PROVIDE_STORAGE);
|
|
3800
5127
|
return flags;
|
|
3801
5128
|
}
|
|
3802
5129
|
sendOnionMessage(destination, messageData, options) {
|
|
@@ -3829,7 +5156,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3829
5156
|
}
|
|
3830
5157
|
}
|
|
3831
5158
|
});
|
|
3832
|
-
this.peerManager.onMessage(
|
|
5159
|
+
this.peerManager.onMessage(types_5.MessageType.ONION_MESSAGE, (pubkey, _type, payload) => {
|
|
3833
5160
|
this.onionMessageManager.handleMessage(pubkey, payload);
|
|
3834
5161
|
});
|
|
3835
5162
|
}
|
|
@@ -3859,7 +5186,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3859
5186
|
}
|
|
3860
5187
|
const destination = invoice.nodeId;
|
|
3861
5188
|
const amountMsat = invoice.amount;
|
|
3862
|
-
const finalCltvExpiry =
|
|
5189
|
+
const finalCltvExpiry = types_4.DEFAULT_MIN_FINAL_CLTV_EXPIRY;
|
|
3863
5190
|
const sourceNodeId = (0, ecdh_1.getPublicKey)(this.nodePrivkey);
|
|
3864
5191
|
if (invoice.paths && invoice.paths.length > 0) {
|
|
3865
5192
|
const payInfo = invoice.blindedPayInfo?.[0] ?? {
|
|
@@ -3902,7 +5229,7 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3902
5229
|
bolt11: '',
|
|
3903
5230
|
amountMsat: invoice.amount,
|
|
3904
5231
|
description: invoice.description,
|
|
3905
|
-
expiry: invoice.relativeExpiry ??
|
|
5232
|
+
expiry: invoice.relativeExpiry ?? types_4.DEFAULT_EXPIRY,
|
|
3906
5233
|
createdAt: Number(invoice.createdAt)
|
|
3907
5234
|
};
|
|
3908
5235
|
this.invoices.set(hashHex, invoiceInfo);
|
|
@@ -3911,8 +5238,8 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3911
5238
|
paymentHash: invoice.paymentHash,
|
|
3912
5239
|
preimage,
|
|
3913
5240
|
amountMsat: invoice.amount,
|
|
3914
|
-
status:
|
|
3915
|
-
direction:
|
|
5241
|
+
status: types_6.PaymentStatus.PENDING,
|
|
5242
|
+
direction: types_6.PaymentDirection.INCOMING,
|
|
3916
5243
|
createdAt: Date.now()
|
|
3917
5244
|
});
|
|
3918
5245
|
}
|
|
@@ -3972,9 +5299,9 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
3972
5299
|
failPayment(paymentHash) {
|
|
3973
5300
|
const hashHex = paymentHash.toString('hex');
|
|
3974
5301
|
const payment = this.payments.get(hashHex);
|
|
3975
|
-
if (!payment || payment.status !==
|
|
5302
|
+
if (!payment || payment.status !== types_6.PaymentStatus.PENDING)
|
|
3976
5303
|
return;
|
|
3977
|
-
payment.status =
|
|
5304
|
+
payment.status = types_6.PaymentStatus.FAILED;
|
|
3978
5305
|
payment.completedAt = Date.now();
|
|
3979
5306
|
this.paymentRetryContexts.delete(hashHex);
|
|
3980
5307
|
this.outboundMppPayments.delete(hashHex);
|
|
@@ -4008,9 +5335,9 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
4008
5335
|
}
|
|
4009
5336
|
}
|
|
4010
5337
|
for (const [hashHex, payment] of this.payments) {
|
|
4011
|
-
if (payment.status !==
|
|
5338
|
+
if (payment.status !== types_6.PaymentStatus.PENDING)
|
|
4012
5339
|
continue;
|
|
4013
|
-
if (payment.direction !==
|
|
5340
|
+
if (payment.direction !== types_6.PaymentDirection.OUTGOING)
|
|
4014
5341
|
continue;
|
|
4015
5342
|
if (now - payment.createdAt < TEN_MINUTES)
|
|
4016
5343
|
continue;
|
|
@@ -4022,9 +5349,9 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
4022
5349
|
scanExpiredPendingPayments() {
|
|
4023
5350
|
const now = Math.floor(Date.now() / 1000);
|
|
4024
5351
|
for (const [hashHex, payment] of this.payments) {
|
|
4025
|
-
if (payment.status !==
|
|
5352
|
+
if (payment.status !== types_6.PaymentStatus.PENDING)
|
|
4026
5353
|
continue;
|
|
4027
|
-
if (payment.direction !==
|
|
5354
|
+
if (payment.direction !== types_6.PaymentDirection.OUTGOING)
|
|
4028
5355
|
continue;
|
|
4029
5356
|
const retryCtx = this.paymentRetryContexts.get(hashHex);
|
|
4030
5357
|
if (!retryCtx)
|
|
@@ -4162,10 +5489,10 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
4162
5489
|
const hashHex = paymentHash.toString('hex');
|
|
4163
5490
|
const existing = this.payments.get(hashHex);
|
|
4164
5491
|
if (existing) {
|
|
4165
|
-
if (existing.status ===
|
|
5492
|
+
if (existing.status === types_6.PaymentStatus.COMPLETED) {
|
|
4166
5493
|
return Promise.resolve(existing);
|
|
4167
5494
|
}
|
|
4168
|
-
if (existing.status ===
|
|
5495
|
+
if (existing.status === types_6.PaymentStatus.FAILED) {
|
|
4169
5496
|
return Promise.reject(new Error(`Payment already failed${existing.failureCode !== undefined
|
|
4170
5497
|
? ` (code ${existing.failureCode})`
|
|
4171
5498
|
: ''}`));
|
|
@@ -4232,6 +5559,9 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
4232
5559
|
for (const channel of channels) {
|
|
4233
5560
|
const state = channel.getFullState();
|
|
4234
5561
|
const channelId = state.channelId || state.temporaryChannelId;
|
|
5562
|
+
if (state.dataLossDetected) {
|
|
5563
|
+
continue;
|
|
5564
|
+
}
|
|
4235
5565
|
const effectiveState = state.state === types_1.ChannelState.AWAITING_REESTABLISH
|
|
4236
5566
|
? state.preReestablishState || state.state
|
|
4237
5567
|
: state.state;
|
|
@@ -4307,11 +5637,11 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
4307
5637
|
async checkAndUpdateFees() {
|
|
4308
5638
|
if (!this.feeEstimator)
|
|
4309
5639
|
return;
|
|
4310
|
-
const satPerVbyte = await this.feeEstimator.estimateFee(6);
|
|
5640
|
+
const satPerVbyte = this.clampEstimatedFeeRate(await this.feeEstimator.estimateFee(6));
|
|
4311
5641
|
if (satPerVbyte <= 0)
|
|
4312
5642
|
return;
|
|
4313
5643
|
this.feeAdvisor.recordSample(satPerVbyte);
|
|
4314
|
-
const newFeeratePerKw = Math.max((0,
|
|
5644
|
+
const newFeeratePerKw = Math.max((0, types_7.satPerVbyteToSatPerKw)(satPerVbyte), types_7.MIN_FEERATE_PER_KW);
|
|
4315
5645
|
if (this.lastKnownFeeratePerKw > 0) {
|
|
4316
5646
|
const ratio = newFeeratePerKw / this.lastKnownFeeratePerKw;
|
|
4317
5647
|
if (ratio > 0.8 && ratio < 1.2)
|
|
@@ -4405,6 +5735,22 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
4405
5735
|
}
|
|
4406
5736
|
}
|
|
4407
5737
|
}
|
|
5738
|
+
findLocalChannelByScid(scid, peerPubkeyHex) {
|
|
5739
|
+
if (!scid || scid.length === 0 || scid.equals(Buffer.alloc(scid.length))) {
|
|
5740
|
+
return undefined;
|
|
5741
|
+
}
|
|
5742
|
+
for (const channel of this.channelManager.getChannelsByPeer(peerPubkeyHex)) {
|
|
5743
|
+
if (channel.getState() !== types_1.ChannelState.NORMAL)
|
|
5744
|
+
continue;
|
|
5745
|
+
const st = channel.getFullState();
|
|
5746
|
+
if ((st.shortChannelId && st.shortChannelId.equals(scid)) ||
|
|
5747
|
+
(st.scidAlias && st.scidAlias.equals(scid)) ||
|
|
5748
|
+
(st.remoteScidAlias && st.remoteScidAlias.equals(scid))) {
|
|
5749
|
+
return channel;
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
return undefined;
|
|
5753
|
+
}
|
|
4408
5754
|
findChannelForPeer(peerPubkeyHex, amountMsat) {
|
|
4409
5755
|
const channels = this.channelManager.getChannelsByPeer(peerPubkeyHex);
|
|
4410
5756
|
const normalChannels = channels.filter((ch) => ch.getState() === types_1.ChannelState.NORMAL);
|
|
@@ -4430,7 +5776,9 @@ class LightningNode extends events_1.EventEmitter {
|
|
|
4430
5776
|
}
|
|
4431
5777
|
}
|
|
4432
5778
|
exports.LightningNode = LightningNode;
|
|
5779
|
+
LightningNode.PEER_STORAGE_MIN_INTERVAL_MS = 60000;
|
|
4433
5780
|
LightningNode.SPLICE_MIN_AMOUNT_SATS = 546n;
|
|
5781
|
+
LightningNode.REBALANCE_BUDGET_KEY = 'advisor:rebalance-budget';
|
|
4434
5782
|
LightningNode.INBOUND_HTLC_CLAIM_FORCE_CLOSE_BUFFER = 18;
|
|
4435
5783
|
LightningNode.OFFERED_HTLC_FORCE_CLOSE_GRACE_BLOCKS = 6;
|
|
4436
5784
|
//# sourceMappingURL=lightning-node.js.map
|