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