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