beignet 0.21.12 → 0.22.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/dist/cli/beignet-node.js +575 -143
- package/dist/cli/beignet-node.js.map +1 -1
- package/dist/cli/cli.js +10 -5
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/config.js +7 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/daemon-options.js +1 -0
- package/dist/cli/daemon-options.js.map +1 -1
- package/dist/cli/daemon.js +19 -6
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/node-storage-view.js +40 -0
- package/dist/cli/node-storage-view.js.map +1 -0
- package/dist/cli/payment-queue.js +215 -31
- package/dist/cli/payment-queue.js.map +1 -1
- package/dist/electrum/index.js +25 -6
- package/dist/electrum/index.js.map +1 -1
- package/dist/lightning/node/lightning-node.js +173 -17
- package/dist/lightning/node/lightning-node.js.map +1 -1
- package/dist/lightning/node/types.js.map +1 -1
- package/dist/lightning/transport/peer-manager.js +13 -7
- package/dist/lightning/transport/peer-manager.js.map +1 -1
- package/dist/lightning/watchtower/tower-connection.js +2 -4
- package/dist/lightning/watchtower/tower-connection.js.map +1 -1
- package/dist/lightning/watchtower/watchtower-client.js +3 -1
- package/dist/lightning/watchtower/watchtower-client.js.map +1 -1
- package/dist/types/cli/beignet-node.d.ts +29 -4
- package/dist/types/cli/index.d.ts +1 -1
- package/dist/types/cli/node-storage-view.d.ts +2 -0
- package/dist/types/cli/payment-queue.d.ts +27 -0
- package/dist/types/cli/types.d.ts +4 -0
- package/dist/types/electrum/index.d.ts +1 -0
- package/dist/types/lightning/node/lightning-node.d.ts +13 -1
- package/dist/types/lightning/node/types.d.ts +2 -0
- package/dist/types/lightning/transport/peer-manager.d.ts +14 -0
- package/dist/types/lightning/watchtower/tower-connection.d.ts +2 -0
- package/dist/types/lightning/watchtower/watchtower-client.d.ts +3 -0
- package/dist/types/wallet/index.d.ts +4 -0
- package/dist/wallet/index.js +125 -50
- package/dist/wallet/index.js.map +1 -1
- package/docs/AI_AGENT_GUIDE.md +9 -7
- 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.jitInvoiceError = exports.holdInvoiceEvent = exports.jsonSafeEvent = exports.jitReceiveRefusal = exports.leaseRatesRefusal = exports.clampDirectFundingMinimum = exports.MAX_ASYNC_SPEND_CLAIMS = exports.ASYNC_SPEND_CLAIM_TTL_MS = exports.jsonToRouteHops = exports.routeHopsToJson = exports.spliceRefusalError = exports.decodeOfferInput = exports.paymentSpendSats = exports.spendLimitSats = exports.decodeInvoiceInput = exports.parseScid = exports.formatScid = exports.gossipPrimeLatch = exports.defaultDataDirForMnemonic = exports.parseRecoveryMode = void 0;
|
|
26
|
+
exports.BeignetNode = exports.jitInvoiceError = exports.holdInvoiceEvent = exports.jsonSafeEvent = exports.jitReceiveRefusal = exports.leaseRatesRefusal = exports.clampDirectFundingMinimum = exports.DAILY_SPEND_STATE_KEY = exports.MAX_ASYNC_SPEND_CLAIMS = exports.ASYNC_SPEND_CLAIM_TTL_MS = exports.jsonToRouteHops = exports.routeHopsToJson = exports.spliceRefusalError = exports.decodeOfferInput = exports.paymentSpendSats = exports.spendLimitSats = exports.decodeInvoiceInput = exports.ENGINE_PAYMENT_ERROR_CODES = exports.parseScid = exports.formatScid = exports.gossipPrimeLatch = exports.defaultDataDirForMnemonic = exports.parseRecoveryMode = void 0;
|
|
27
27
|
const offline_receive_1 = require("./offline-receive");
|
|
28
28
|
const ffor_receive_1 = require("./ffor-receive");
|
|
29
29
|
const path = __importStar(require("path"));
|
|
@@ -41,6 +41,7 @@ const helpers_1 = require("../utils/helpers");
|
|
|
41
41
|
const conversion_1 = require("../utils/conversion");
|
|
42
42
|
const wallet_3 = require("../types/wallet");
|
|
43
43
|
const wallet_storage_1 = require("./wallet-storage");
|
|
44
|
+
const node_storage_view_1 = require("./node-storage-view");
|
|
44
45
|
const electrum_1 = require("../types/electrum");
|
|
45
46
|
const lightning_node_1 = require("../lightning/node/lightning-node");
|
|
46
47
|
const types_1 = require("../lightning/direct-funding/receiver/types");
|
|
@@ -69,6 +70,7 @@ const direct_funding_2 = require("./direct-funding");
|
|
|
69
70
|
const auth_1 = require("./auth");
|
|
70
71
|
const types_5 = require("../lightning/node/types");
|
|
71
72
|
const types_6 = require("../lightning/channel/types");
|
|
73
|
+
const channel_state_1 = require("../lightning/channel/channel-state");
|
|
72
74
|
const decode_1 = require("../lightning/invoice/decode");
|
|
73
75
|
const decode_2 = require("../lightning/offer/decode");
|
|
74
76
|
const errors_1 = require("./errors");
|
|
@@ -169,6 +171,17 @@ function parseScid(scid) {
|
|
|
169
171
|
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)`);
|
|
170
172
|
}
|
|
171
173
|
exports.parseScid = parseScid;
|
|
174
|
+
exports.ENGINE_PAYMENT_ERROR_CODES = {
|
|
175
|
+
NO_ROUTE: 'NO_ROUTE',
|
|
176
|
+
DUPLICATE_PAYMENT: 'DUPLICATE_PAYMENT',
|
|
177
|
+
NO_CHANNEL_TO_HOP: 'PEER_NOT_CONNECTED',
|
|
178
|
+
FEE_EXCEEDS_MAX: 'PAYMENT_FAILED',
|
|
179
|
+
CLTV_EXCEEDS_MAX: 'CLTV_EXCEEDS_MAX',
|
|
180
|
+
MISSING_AMOUNT: 'INVALID_PARAMS',
|
|
181
|
+
INVALID_INVOICE: 'INVALID_PARAMS',
|
|
182
|
+
INVALID_KEYSEND: 'INVALID_PARAMS',
|
|
183
|
+
INVOICE_EXPIRED: 'INVOICE_EXPIRED'
|
|
184
|
+
};
|
|
172
185
|
function decodeInvoiceInput(bolt11) {
|
|
173
186
|
try {
|
|
174
187
|
return (0, decode_1.decode)(bolt11);
|
|
@@ -279,6 +292,12 @@ exports.jsonToRouteHops = jsonToRouteHops;
|
|
|
279
292
|
const GRAPH_DESCRIBE_MAX_LIMIT = 500;
|
|
280
293
|
exports.ASYNC_SPEND_CLAIM_TTL_MS = 24 * 60 * 60 * 1000;
|
|
281
294
|
exports.MAX_ASYNC_SPEND_CLAIMS = 4096;
|
|
295
|
+
exports.DAILY_SPEND_STATE_KEY = 'daemon:daily-spend:v1';
|
|
296
|
+
const nextUtcMidnight = () => {
|
|
297
|
+
const next = new Date();
|
|
298
|
+
next.setUTCHours(24, 0, 0, 0);
|
|
299
|
+
return next.getTime();
|
|
300
|
+
};
|
|
282
301
|
const MAX_DIRECT_FUNDING_CHARGES = 1024;
|
|
283
302
|
const DEFAULT_L402_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
284
303
|
const DEFAULT_ELECTRUM = {
|
|
@@ -478,6 +497,21 @@ function isHeldRestore(ch) {
|
|
|
478
497
|
ch.reestablishSecretMissing === true ||
|
|
479
498
|
ch.restoreRevokedRisk === true);
|
|
480
499
|
}
|
|
500
|
+
const CLOSING_CHANNEL_STATES = new Set([
|
|
501
|
+
types_6.ChannelState.SHUTTING_DOWN,
|
|
502
|
+
types_6.ChannelState.NEGOTIATING_CLOSING,
|
|
503
|
+
types_6.ChannelState.CLOSED,
|
|
504
|
+
types_6.ChannelState.FORCE_CLOSED,
|
|
505
|
+
types_6.ChannelState.ERRORED
|
|
506
|
+
]);
|
|
507
|
+
function canBecomeUsable(channel) {
|
|
508
|
+
const st = channel.getFullState();
|
|
509
|
+
const effective = st.state === types_6.ChannelState.AWAITING_REESTABLISH && st.preReestablishState
|
|
510
|
+
? st.preReestablishState
|
|
511
|
+
: st.state;
|
|
512
|
+
return !CLOSING_CHANNEL_STATES.has(effective) && !(0, channel_state_1.isRecencyUnproven)(st);
|
|
513
|
+
}
|
|
514
|
+
const CHANNELS_CHANGED = Symbol('channels-changed');
|
|
481
515
|
const STALE_STATE_FORCE_CLOSE_REFUSAL = 'This channel was restored from a Recovery Capsule and its state ' +
|
|
482
516
|
'cannot be proven current, so the node will not broadcast its ' +
|
|
483
517
|
'commitment on its own initiative. If the peer holds a newer ' +
|
|
@@ -583,7 +617,6 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
583
617
|
this._dailySpendResetTime = 0;
|
|
584
618
|
this._pendingSpendSats = 0;
|
|
585
619
|
this._asyncSpendClaims = new Map();
|
|
586
|
-
this._blockingPaymentHashes = new Map();
|
|
587
620
|
this._l402Credentials = new l402_1.MemoryL402CredentialStore();
|
|
588
621
|
this._draining = false;
|
|
589
622
|
this.peerStorageEnabled = true;
|
|
@@ -706,6 +739,9 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
706
739
|
throw new errors_1.BeignetError('INVALID_PARAMS', `jitReceive: ${refusal}`);
|
|
707
740
|
}
|
|
708
741
|
}
|
|
742
|
+
if (opts.torProxyOnionOnly && !opts.torProxy) {
|
|
743
|
+
throw new errors_1.BeignetError('INVALID_PARAMS', 'torProxyOnionOnly needs torProxy: it only chooses which hosts use the configured proxy');
|
|
744
|
+
}
|
|
709
745
|
const networkName = this.networkName;
|
|
710
746
|
if (!opts.allowMultipleInstances) {
|
|
711
747
|
const lockPath = path.join(this.dataDir, `${networkName}.lock`);
|
|
@@ -897,10 +933,13 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
897
933
|
const [proxyHost, proxyPort] = opts.torProxy.split(':');
|
|
898
934
|
const port = parseInt(proxyPort, 10);
|
|
899
935
|
if (!proxyHost || !Number.isFinite(port)) {
|
|
900
|
-
throw new errors_1.BeignetError('INVALID_PARAMS', `Invalid torProxy "${opts.torProxy}"
|
|
936
|
+
throw new errors_1.BeignetError('INVALID_PARAMS', `Invalid torProxy "${opts.torProxy}": expected "host:port"`);
|
|
901
937
|
}
|
|
902
938
|
socks5Proxy = { host: proxyHost, port };
|
|
903
939
|
}
|
|
940
|
+
const socks5ProxyScope = opts.torProxyOnionOnly
|
|
941
|
+
? 'onion'
|
|
942
|
+
: 'all';
|
|
904
943
|
let announcedAddresses;
|
|
905
944
|
if (opts.announceAddresses && opts.announceAddresses.length > 0) {
|
|
906
945
|
announcedAddresses = opts.announceAddresses.map((addr) => {
|
|
@@ -920,10 +959,11 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
920
959
|
this._initialGossipPrime = null;
|
|
921
960
|
});
|
|
922
961
|
}
|
|
962
|
+
this._nodeStorageView = (0, node_storage_view_1.nodeStorageView)(this.storage);
|
|
923
963
|
this.node = lightning_node_1.LightningNode.fromMnemonic(this.mnemonic, {
|
|
924
964
|
coinType,
|
|
925
965
|
network: lnNetwork,
|
|
926
|
-
storage: this.
|
|
966
|
+
storage: this._nodeStorageView,
|
|
927
967
|
newChannelsRefused: () => this.newChannelRefusal(),
|
|
928
968
|
enableNetworking: true,
|
|
929
969
|
autoReconnect: opts.autoReconnect ?? true,
|
|
@@ -1088,6 +1128,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
1088
1128
|
logger: this.logger,
|
|
1089
1129
|
sweepDestinationScript,
|
|
1090
1130
|
socks5Proxy,
|
|
1131
|
+
socks5ProxyScope,
|
|
1091
1132
|
...(opts.guardianServe
|
|
1092
1133
|
? {
|
|
1093
1134
|
guardianHost: {
|
|
@@ -1168,6 +1209,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
1168
1209
|
});
|
|
1169
1210
|
this.node.on('payment:failed', (info) => {
|
|
1170
1211
|
const pi = this.toPaymentInfo(info);
|
|
1212
|
+
this._releaseAsyncSpendClaimsUnlessInFlight(info.paymentHash);
|
|
1171
1213
|
this.log('warn', 'Payment failed', {
|
|
1172
1214
|
paymentHash: pi.paymentHash,
|
|
1173
1215
|
failureCode: pi.failureCode,
|
|
@@ -1182,6 +1224,61 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
1182
1224
|
this.refreshStaticChannelBackup();
|
|
1183
1225
|
this.emit('channel:ready', { channelId });
|
|
1184
1226
|
});
|
|
1227
|
+
const usableNode = this.node;
|
|
1228
|
+
const deferEmit = (emit) => {
|
|
1229
|
+
setImmediate(() => {
|
|
1230
|
+
if (this.destroyed || this._resuming || this.node !== usableNode) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
emit();
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
try {
|
|
1238
|
+
this.log('warn', 'A payment-readiness listener failed', {
|
|
1239
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
};
|
|
1247
|
+
const announceUsable = (channelId) => deferEmit(() => {
|
|
1248
|
+
const channel = usableNode.getChannelManager().getChannel(channelId);
|
|
1249
|
+
if (!channel?.acceptsNewHtlcs())
|
|
1250
|
+
return;
|
|
1251
|
+
this.emit('channel:usable', { channelId: channelId.toString('hex') });
|
|
1252
|
+
});
|
|
1253
|
+
this.node.on('channel:ready', (data) => announceUsable(data.channelId));
|
|
1254
|
+
this.node
|
|
1255
|
+
.getChannelManager()
|
|
1256
|
+
.on('channel:reestablished', (channelId) => announceUsable(channelId));
|
|
1257
|
+
for (const event of [
|
|
1258
|
+
'splice:complete',
|
|
1259
|
+
'splice:aborted',
|
|
1260
|
+
'splice:reverted'
|
|
1261
|
+
]) {
|
|
1262
|
+
this.node.on(event, (data) => announceUsable(data.channelId));
|
|
1263
|
+
}
|
|
1264
|
+
this.node.on('log', (entry) => {
|
|
1265
|
+
if (entry?.action !== 'funding_missing_quarantine_lifted')
|
|
1266
|
+
return;
|
|
1267
|
+
const idHex = entry.data?.channelId;
|
|
1268
|
+
if (typeof idHex === 'string')
|
|
1269
|
+
announceUsable(Buffer.from(idHex, 'hex'));
|
|
1270
|
+
});
|
|
1271
|
+
const channelsChanged = () => deferEmit(() => this.emit(CHANNELS_CHANGED));
|
|
1272
|
+
for (const event of [
|
|
1273
|
+
'channel:pending-close',
|
|
1274
|
+
'channel:force-closing',
|
|
1275
|
+
'channel:closed',
|
|
1276
|
+
'channel:voided',
|
|
1277
|
+
'channel:aborted'
|
|
1278
|
+
]) {
|
|
1279
|
+
this.node.on(event, channelsChanged);
|
|
1280
|
+
}
|
|
1281
|
+
this.node.getChannelManager().on('channel:errored', channelsChanged);
|
|
1185
1282
|
this.node.on('channel:closed', (data) => {
|
|
1186
1283
|
const channelId = data.channelId.toString('hex');
|
|
1187
1284
|
this.log('info', 'Channel closed', { channelId });
|
|
@@ -1531,7 +1628,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
1531
1628
|
if (opts.dailySpendLimitSats !== undefined &&
|
|
1532
1629
|
opts.dailySpendLimitSats > 0) {
|
|
1533
1630
|
this._dailySpendLimitSats = opts.dailySpendLimitSats;
|
|
1534
|
-
this.
|
|
1631
|
+
this._loadSpendState();
|
|
1535
1632
|
}
|
|
1536
1633
|
if (opts.maxPaymentSats !== undefined && opts.maxPaymentSats > 0) {
|
|
1537
1634
|
this._maxPaymentSats = opts.maxPaymentSats;
|
|
@@ -2207,6 +2304,13 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
2207
2304
|
}
|
|
2208
2305
|
this._resuming = false;
|
|
2209
2306
|
this._bootTargetEmpty = false;
|
|
2307
|
+
if (this.paymentQueue) {
|
|
2308
|
+
this.whenReadyToPay(() => {
|
|
2309
|
+
this.paymentQueue?.start();
|
|
2310
|
+
this.paymentQueue?.resettle();
|
|
2311
|
+
this.paymentQueue?.poke();
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2210
2314
|
this._peerRetrievedCapsules.clear();
|
|
2211
2315
|
const uncovered = uncoveredEntries.map((e) => e.channelId);
|
|
2212
2316
|
if (uncovered.length > 0) {
|
|
@@ -2426,6 +2530,11 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
2426
2530
|
}
|
|
2427
2531
|
this.paymentQueue?.removeAllListeners();
|
|
2428
2532
|
this.node.destroy();
|
|
2533
|
+
try {
|
|
2534
|
+
this.storage.close();
|
|
2535
|
+
}
|
|
2536
|
+
catch {
|
|
2537
|
+
}
|
|
2429
2538
|
void this.wallet?.stop().catch(() => {
|
|
2430
2539
|
});
|
|
2431
2540
|
}
|
|
@@ -2447,6 +2556,19 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
2447
2556
|
error: err instanceof Error ? err.message : String(err)
|
|
2448
2557
|
});
|
|
2449
2558
|
}
|
|
2559
|
+
try {
|
|
2560
|
+
for (const row of from.loadAllQueueEntries()) {
|
|
2561
|
+
to.saveQueueEntry(row);
|
|
2562
|
+
if (row.error !== undefined || row.completedAt !== undefined) {
|
|
2563
|
+
to.updateQueueEntryStatus(row.id, row.status, row.error, row.completedAt);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
catch (err) {
|
|
2568
|
+
this.log('warn', 'Could not carry the payment queue into the restore', {
|
|
2569
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2450
2572
|
try {
|
|
2451
2573
|
for (const peer of from.loadAllPeerAddresses()) {
|
|
2452
2574
|
to.savePeerAddress(peer.pubkey, peer.host, peer.port);
|
|
@@ -2457,6 +2579,14 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
2457
2579
|
error: err instanceof Error ? err.message : String(err)
|
|
2458
2580
|
});
|
|
2459
2581
|
}
|
|
2582
|
+
try {
|
|
2583
|
+
const spend = from.loadMetadata(exports.DAILY_SPEND_STATE_KEY);
|
|
2584
|
+
if (spend !== null)
|
|
2585
|
+
to.saveMetadata(exports.DAILY_SPEND_STATE_KEY, spend);
|
|
2586
|
+
}
|
|
2587
|
+
catch (err) {
|
|
2588
|
+
this.log('warn', 'Could not carry the daily spend ledger into the restore', { error: err instanceof Error ? err.message : String(err) });
|
|
2589
|
+
}
|
|
2460
2590
|
}
|
|
2461
2591
|
finishStagedCapsuleRestore(dbPath) {
|
|
2462
2592
|
const markerPath = this.capsuleRestoreMarkerPath();
|
|
@@ -4937,15 +5067,14 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4937
5067
|
}
|
|
4938
5068
|
return result;
|
|
4939
5069
|
}
|
|
4940
|
-
_resetDailySpendIfNeeded() {
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
const tomorrow = new Date();
|
|
4944
|
-
tomorrow.setUTCHours(24, 0, 0, 0);
|
|
4945
|
-
this._dailySpendResetTime = tomorrow.getTime();
|
|
5070
|
+
_resetDailySpendIfNeeded(persist = true) {
|
|
5071
|
+
if (Date.now() >= this._dailySpendResetTime) {
|
|
5072
|
+
this._dailySpendResetTime = nextUtcMidnight();
|
|
4946
5073
|
this._dailySpentSats = 0;
|
|
4947
5074
|
this._dailySpentLightningSats = 0;
|
|
4948
5075
|
this._dailySpentOnchainSats = 0;
|
|
5076
|
+
if (persist)
|
|
5077
|
+
this._persistSpendState();
|
|
4949
5078
|
}
|
|
4950
5079
|
}
|
|
4951
5080
|
_checkSpendLimit(amountSats) {
|
|
@@ -4969,7 +5098,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4969
5098
|
_recordSpend(amountSats, source = 'lightning') {
|
|
4970
5099
|
if (this._dailySpendLimitSats === undefined)
|
|
4971
5100
|
return;
|
|
4972
|
-
this._resetDailySpendIfNeeded();
|
|
5101
|
+
this._resetDailySpendIfNeeded(false);
|
|
4973
5102
|
this._dailySpentSats += amountSats;
|
|
4974
5103
|
if (source === 'onchain') {
|
|
4975
5104
|
this._dailySpentOnchainSats += amountSats;
|
|
@@ -4977,6 +5106,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4977
5106
|
else {
|
|
4978
5107
|
this._dailySpentLightningSats += amountSats;
|
|
4979
5108
|
}
|
|
5109
|
+
this._persistSpendState();
|
|
4980
5110
|
}
|
|
4981
5111
|
_openAsyncSpendClaim(paymentHashHex, amountSats) {
|
|
4982
5112
|
if (this._dailySpendLimitSats === undefined)
|
|
@@ -5001,8 +5131,25 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5001
5131
|
else
|
|
5002
5132
|
this._asyncSpendClaims.set(paymentHashHex, [claim]);
|
|
5003
5133
|
this._pendingSpendSats += amountSats;
|
|
5134
|
+
this._persistSpendState();
|
|
5004
5135
|
return claim;
|
|
5005
5136
|
}
|
|
5137
|
+
_rekeyAsyncSpendClaim(fromKey, toKey, claim) {
|
|
5138
|
+
const from = this._asyncSpendClaims.get(fromKey);
|
|
5139
|
+
if (from) {
|
|
5140
|
+
const index = from.indexOf(claim);
|
|
5141
|
+
if (index !== -1)
|
|
5142
|
+
from.splice(index, 1);
|
|
5143
|
+
if (from.length === 0)
|
|
5144
|
+
this._asyncSpendClaims.delete(fromKey);
|
|
5145
|
+
}
|
|
5146
|
+
const to = this._asyncSpendClaims.get(toKey);
|
|
5147
|
+
if (to)
|
|
5148
|
+
to.push(claim);
|
|
5149
|
+
else
|
|
5150
|
+
this._asyncSpendClaims.set(toKey, [claim]);
|
|
5151
|
+
this._persistSpendState();
|
|
5152
|
+
}
|
|
5006
5153
|
_closeAsyncSpendClaim(paymentHashHex, claim) {
|
|
5007
5154
|
const claims = this._asyncSpendClaims.get(paymentHashHex);
|
|
5008
5155
|
if (!claims)
|
|
@@ -5013,49 +5160,74 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5013
5160
|
claims.splice(index, 1);
|
|
5014
5161
|
if (claims.length === 0)
|
|
5015
5162
|
this._asyncSpendClaims.delete(paymentHashHex);
|
|
5016
|
-
this.
|
|
5163
|
+
this._releaseClaimReservation(claim);
|
|
5164
|
+
this._persistSpendState();
|
|
5017
5165
|
}
|
|
5018
5166
|
_releaseAsyncSpendClaim(claim) {
|
|
5167
|
+
if (this._releaseClaimReservation(claim))
|
|
5168
|
+
this._persistSpendState();
|
|
5169
|
+
}
|
|
5170
|
+
_releaseClaimReservation(claim) {
|
|
5019
5171
|
if (!claim.reserved)
|
|
5020
|
-
return;
|
|
5172
|
+
return false;
|
|
5021
5173
|
claim.reserved = false;
|
|
5022
5174
|
this._pendingSpendSats -= claim.sats;
|
|
5175
|
+
return true;
|
|
5023
5176
|
}
|
|
5024
|
-
|
|
5025
|
-
|
|
5177
|
+
_releaseAsyncSpendClaimsUnlessInFlight(paymentHash) {
|
|
5178
|
+
const claims = this._asyncSpendClaims.get(paymentHash.toString('hex'));
|
|
5179
|
+
if (!claims || !claims.some((claim) => claim.reserved))
|
|
5026
5180
|
return;
|
|
5181
|
+
if (this.node.hasHtlcInFlight(paymentHash))
|
|
5182
|
+
return;
|
|
5183
|
+
let released = false;
|
|
5184
|
+
for (const claim of claims) {
|
|
5185
|
+
released = this._releaseClaimReservation(claim) || released;
|
|
5186
|
+
}
|
|
5187
|
+
if (released)
|
|
5188
|
+
this._persistSpendState();
|
|
5189
|
+
}
|
|
5190
|
+
_chargeAsyncSpendClaim(paymentHashHex, claim) {
|
|
5027
5191
|
this._expireAsyncSpendClaims();
|
|
5028
5192
|
const claims = this._asyncSpendClaims.get(paymentHashHex);
|
|
5029
5193
|
if (!claims || claims.length === 0)
|
|
5030
5194
|
return;
|
|
5031
|
-
|
|
5032
|
-
|
|
5195
|
+
let index;
|
|
5196
|
+
if (claim) {
|
|
5197
|
+
index = claims.indexOf(claim);
|
|
5198
|
+
if (index === -1)
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
else {
|
|
5202
|
+
const rank = (entry) => (entry.reserved ? 0 : 2) + (entry.settled ? 1 : 0);
|
|
5203
|
+
index = 0;
|
|
5204
|
+
for (let i = 1; i < claims.length; i++) {
|
|
5205
|
+
if (rank(claims[i]) < rank(claims[index]))
|
|
5206
|
+
index = i;
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
const [charged] = claims.splice(index, 1);
|
|
5210
|
+
for (const rest of claims) {
|
|
5211
|
+
rest.settled = true;
|
|
5212
|
+
this._releaseClaimReservation(rest);
|
|
5213
|
+
}
|
|
5033
5214
|
if (claims.length === 0)
|
|
5034
5215
|
this._asyncSpendClaims.delete(paymentHashHex);
|
|
5035
|
-
this.
|
|
5036
|
-
this._recordSpend(
|
|
5037
|
-
}
|
|
5038
|
-
_acquireBlockingPayment(paymentHashHex) {
|
|
5039
|
-
this._blockingPaymentHashes.set(paymentHashHex, (this._blockingPaymentHashes.get(paymentHashHex) ?? 0) + 1);
|
|
5040
|
-
}
|
|
5041
|
-
_releaseBlockingPayment(paymentHashHex) {
|
|
5042
|
-
const held = this._blockingPaymentHashes.get(paymentHashHex);
|
|
5043
|
-
if (held === undefined)
|
|
5044
|
-
return;
|
|
5045
|
-
if (held > 1)
|
|
5046
|
-
this._blockingPaymentHashes.set(paymentHashHex, held - 1);
|
|
5047
|
-
else
|
|
5048
|
-
this._blockingPaymentHashes.delete(paymentHashHex);
|
|
5216
|
+
this._releaseClaimReservation(charged);
|
|
5217
|
+
this._recordSpend(charged.sats);
|
|
5049
5218
|
}
|
|
5050
|
-
_expireAsyncSpendClaims() {
|
|
5219
|
+
_expireAsyncSpendClaims(persist = true) {
|
|
5051
5220
|
const now = Date.now();
|
|
5221
|
+
let released = false;
|
|
5052
5222
|
for (const claims of this._asyncSpendClaims.values()) {
|
|
5053
5223
|
for (const claim of claims) {
|
|
5054
5224
|
if (claim.reserved && claim.expiresAt <= now) {
|
|
5055
|
-
this.
|
|
5225
|
+
released = this._releaseClaimReservation(claim) || released;
|
|
5056
5226
|
}
|
|
5057
5227
|
}
|
|
5058
5228
|
}
|
|
5229
|
+
if (released && persist)
|
|
5230
|
+
this._persistSpendState();
|
|
5059
5231
|
}
|
|
5060
5232
|
_countAsyncSpendClaims() {
|
|
5061
5233
|
let total = 0;
|
|
@@ -5067,6 +5239,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5067
5239
|
let count = this._countAsyncSpendClaims();
|
|
5068
5240
|
if (count <= target)
|
|
5069
5241
|
return count;
|
|
5242
|
+
const before = count;
|
|
5070
5243
|
for (const [paymentHashHex, claims] of this._asyncSpendClaims) {
|
|
5071
5244
|
for (let i = 0; i < claims.length && count > target;) {
|
|
5072
5245
|
if (claims[i].reserved) {
|
|
@@ -5081,8 +5254,164 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5081
5254
|
if (count <= target)
|
|
5082
5255
|
break;
|
|
5083
5256
|
}
|
|
5257
|
+
if (count < before)
|
|
5258
|
+
this._persistSpendState();
|
|
5084
5259
|
return count;
|
|
5085
5260
|
}
|
|
5261
|
+
_persistSpendState() {
|
|
5262
|
+
if (this._dailySpendLimitSats === undefined)
|
|
5263
|
+
return;
|
|
5264
|
+
const storage = this.storage;
|
|
5265
|
+
if (!storage)
|
|
5266
|
+
return;
|
|
5267
|
+
const claims = {};
|
|
5268
|
+
for (const [paymentHashHex, list] of this._asyncSpendClaims) {
|
|
5269
|
+
if (list.length > 0)
|
|
5270
|
+
claims[paymentHashHex] = list;
|
|
5271
|
+
}
|
|
5272
|
+
const state = {
|
|
5273
|
+
resetTime: this._dailySpendResetTime,
|
|
5274
|
+
totalSats: this._dailySpentSats,
|
|
5275
|
+
lightningSats: this._dailySpentLightningSats,
|
|
5276
|
+
onchainSats: this._dailySpentOnchainSats,
|
|
5277
|
+
claims
|
|
5278
|
+
};
|
|
5279
|
+
try {
|
|
5280
|
+
storage.saveMetadata(exports.DAILY_SPEND_STATE_KEY, JSON.stringify(state));
|
|
5281
|
+
}
|
|
5282
|
+
catch (err) {
|
|
5283
|
+
this.log('warn', 'Could not persist the daily spend ledger', {
|
|
5284
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5285
|
+
});
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
_loadSpendState() {
|
|
5289
|
+
for (const claims of this._asyncSpendClaims.values()) {
|
|
5290
|
+
for (const claim of claims)
|
|
5291
|
+
this._releaseClaimReservation(claim);
|
|
5292
|
+
}
|
|
5293
|
+
this._asyncSpendClaims.clear();
|
|
5294
|
+
this._dailySpendResetTime = 0;
|
|
5295
|
+
this._dailySpentSats = 0;
|
|
5296
|
+
this._dailySpentLightningSats = 0;
|
|
5297
|
+
this._dailySpentOnchainSats = 0;
|
|
5298
|
+
let stored;
|
|
5299
|
+
try {
|
|
5300
|
+
const raw = this.storage.loadMetadata(exports.DAILY_SPEND_STATE_KEY);
|
|
5301
|
+
if (raw)
|
|
5302
|
+
stored = JSON.parse(raw);
|
|
5303
|
+
}
|
|
5304
|
+
catch (err) {
|
|
5305
|
+
this.log('warn', 'Could not read the persisted daily spend ledger; the day starts at zero', { error: err instanceof Error ? err.message : String(err) });
|
|
5306
|
+
}
|
|
5307
|
+
const nonNegative = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
5308
|
+
if (stored &&
|
|
5309
|
+
nonNegative(stored.resetTime) &&
|
|
5310
|
+
Date.now() < stored.resetTime &&
|
|
5311
|
+
stored.resetTime <= nextUtcMidnight() &&
|
|
5312
|
+
nonNegative(stored.totalSats) &&
|
|
5313
|
+
nonNegative(stored.lightningSats) &&
|
|
5314
|
+
nonNegative(stored.onchainSats)) {
|
|
5315
|
+
this._dailySpendResetTime = stored.resetTime;
|
|
5316
|
+
this._dailySpentSats = stored.totalSats;
|
|
5317
|
+
this._dailySpentLightningSats = stored.lightningSats;
|
|
5318
|
+
this._dailySpentOnchainSats = stored.onchainSats;
|
|
5319
|
+
}
|
|
5320
|
+
else {
|
|
5321
|
+
this._resetDailySpendIfNeeded(false);
|
|
5322
|
+
}
|
|
5323
|
+
const isClaim = (value) => {
|
|
5324
|
+
const entry = value;
|
|
5325
|
+
return (typeof entry === 'object' &&
|
|
5326
|
+
entry !== null &&
|
|
5327
|
+
nonNegative(entry.sats) &&
|
|
5328
|
+
typeof entry.expiresAt === 'number' &&
|
|
5329
|
+
Number.isFinite(entry.expiresAt) &&
|
|
5330
|
+
typeof entry.reserved === 'boolean' &&
|
|
5331
|
+
(entry.settled === undefined || typeof entry.settled === 'boolean'));
|
|
5332
|
+
};
|
|
5333
|
+
let restored = 0;
|
|
5334
|
+
let charged = 0;
|
|
5335
|
+
let dropped = 0;
|
|
5336
|
+
const toCharge = [];
|
|
5337
|
+
const rows = stored?.claims && typeof stored.claims === 'object'
|
|
5338
|
+
? Object.entries(stored.claims)
|
|
5339
|
+
: [];
|
|
5340
|
+
for (const [paymentHashHex, list] of rows) {
|
|
5341
|
+
const total = Array.isArray(list) ? list.length : 0;
|
|
5342
|
+
const claims = Array.isArray(list) ? list.filter(isClaim) : [];
|
|
5343
|
+
dropped += total - claims.length;
|
|
5344
|
+
if (claims.length === 0)
|
|
5345
|
+
continue;
|
|
5346
|
+
if (!/^[0-9a-f]{64}$/.test(paymentHashHex)) {
|
|
5347
|
+
dropped += claims.length;
|
|
5348
|
+
continue;
|
|
5349
|
+
}
|
|
5350
|
+
const paymentHash = Buffer.from(paymentHashHex, 'hex');
|
|
5351
|
+
const outcome = this._spendClaimOutcomeAtBoot(paymentHash, paymentHashHex);
|
|
5352
|
+
if (outcome === 'gone') {
|
|
5353
|
+
dropped += claims.length;
|
|
5354
|
+
continue;
|
|
5355
|
+
}
|
|
5356
|
+
const kept = claims.map((entry) => ({
|
|
5357
|
+
sats: entry.sats,
|
|
5358
|
+
expiresAt: entry.expiresAt,
|
|
5359
|
+
reserved: entry.reserved && outcome !== 'settled',
|
|
5360
|
+
...(entry.settled ? { settled: true } : {})
|
|
5361
|
+
}));
|
|
5362
|
+
this._asyncSpendClaims.set(paymentHashHex, kept);
|
|
5363
|
+
for (const entry of kept) {
|
|
5364
|
+
if (entry.reserved)
|
|
5365
|
+
this._pendingSpendSats += entry.sats;
|
|
5366
|
+
}
|
|
5367
|
+
restored += kept.length;
|
|
5368
|
+
if (outcome === 'settled' && kept.some((entry) => !entry.settled)) {
|
|
5369
|
+
toCharge.push(paymentHashHex);
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
for (const paymentHashHex of toCharge) {
|
|
5373
|
+
this._chargeAsyncSpendClaim(paymentHashHex);
|
|
5374
|
+
charged++;
|
|
5375
|
+
}
|
|
5376
|
+
this._expireAsyncSpendClaims(false);
|
|
5377
|
+
this._persistSpendState();
|
|
5378
|
+
if (stored) {
|
|
5379
|
+
this.log('info', 'Daily spend ledger restored', {
|
|
5380
|
+
spentSats: this._dailySpentSats,
|
|
5381
|
+
resetsAt: this._dailySpendResetTime,
|
|
5382
|
+
claimsRestored: restored,
|
|
5383
|
+
settledWhileDown: charged,
|
|
5384
|
+
claimsDropped: dropped
|
|
5385
|
+
});
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
_spendClaimOutcomeAtBoot(paymentHash, paymentHashHex) {
|
|
5389
|
+
const view = this.node.getOutgoingHtlcs(paymentHash);
|
|
5390
|
+
if (view.preimage || view.status === types_5.PaymentStatus.COMPLETED) {
|
|
5391
|
+
return 'settled';
|
|
5392
|
+
}
|
|
5393
|
+
try {
|
|
5394
|
+
const durable = this.storage.loadPayment(paymentHashHex);
|
|
5395
|
+
if (durable?.direction === types_5.PaymentDirection.OUTGOING &&
|
|
5396
|
+
(durable.status === types_5.PaymentStatus.COMPLETED ||
|
|
5397
|
+
durable.preimage !== undefined ||
|
|
5398
|
+
this.storage.loadPreimage(paymentHashHex) !== null)) {
|
|
5399
|
+
return 'settled';
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5402
|
+
catch (err) {
|
|
5403
|
+
this.log('warn', 'Could not read a payment record for the daily spend ledger; keeping its claim', {
|
|
5404
|
+
paymentHash: paymentHashHex,
|
|
5405
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5406
|
+
});
|
|
5407
|
+
return 'live';
|
|
5408
|
+
}
|
|
5409
|
+
if (view.status === types_5.PaymentStatus.PENDING ||
|
|
5410
|
+
this.node.hasHtlcInFlight(paymentHash)) {
|
|
5411
|
+
return 'live';
|
|
5412
|
+
}
|
|
5413
|
+
return 'gone';
|
|
5414
|
+
}
|
|
5086
5415
|
_builtOnchainTotalSats(amountSats) {
|
|
5087
5416
|
const feeSats = this.wallet.transaction?.data?.fee;
|
|
5088
5417
|
if (!Number.isFinite(feeSats) || feeSats < 0) {
|
|
@@ -5095,11 +5424,16 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5095
5424
|
}
|
|
5096
5425
|
getDailySpendInfo() {
|
|
5097
5426
|
this._resetDailySpendIfNeeded();
|
|
5427
|
+
this._expireAsyncSpendClaims();
|
|
5098
5428
|
const limit = this._dailySpendLimitSats ?? null;
|
|
5429
|
+
const pendingSats = this._pendingSpendSats;
|
|
5099
5430
|
return {
|
|
5100
5431
|
limitSats: limit,
|
|
5101
5432
|
spentSats: this._dailySpentSats,
|
|
5102
|
-
remainingSats: limit !== null
|
|
5433
|
+
remainingSats: limit !== null
|
|
5434
|
+
? Math.max(0, limit - this._dailySpentSats - pendingSats)
|
|
5435
|
+
: Infinity,
|
|
5436
|
+
pendingSats,
|
|
5103
5437
|
resetsAt: this._dailySpendResetTime,
|
|
5104
5438
|
totalSats: this._dailySpentSats,
|
|
5105
5439
|
lightningSats: this._dailySpentLightningSats,
|
|
@@ -5326,6 +5660,35 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5326
5660
|
}
|
|
5327
5661
|
return height + cltvLimit;
|
|
5328
5662
|
}
|
|
5663
|
+
_paymentTimeout(paymentHash, what, timeoutMs, claim) {
|
|
5664
|
+
const failed = this.node.failPaymentUnlessInFlight(paymentHash);
|
|
5665
|
+
if (failed && claim)
|
|
5666
|
+
this._releaseAsyncSpendClaim(claim);
|
|
5667
|
+
return new errors_1.BeignetError('PAYMENT_TIMEOUT', failed
|
|
5668
|
+
? `${what} timed out after ${timeoutMs}ms`
|
|
5669
|
+
: `${what} timed out after ${timeoutMs}ms; an HTLC is still in flight and the payment stays PENDING until it resolves; no further route is tried after the timeout`);
|
|
5670
|
+
}
|
|
5671
|
+
_toBeignetPaymentError(err) {
|
|
5672
|
+
if (err instanceof errors_1.BeignetError)
|
|
5673
|
+
return err;
|
|
5674
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5675
|
+
let code = 'PAYMENT_FAILED';
|
|
5676
|
+
if (err instanceof Error && 'code' in err) {
|
|
5677
|
+
const lpErr = err;
|
|
5678
|
+
code = exports.ENGINE_PAYMENT_ERROR_CODES[lpErr.code] || 'PAYMENT_FAILED';
|
|
5679
|
+
}
|
|
5680
|
+
else if (msg.includes('No route found')) {
|
|
5681
|
+
code = 'NO_ROUTE';
|
|
5682
|
+
}
|
|
5683
|
+
else if (msg.includes('already in flight')) {
|
|
5684
|
+
code = 'DUPLICATE_PAYMENT';
|
|
5685
|
+
}
|
|
5686
|
+
else if (msg.includes('No channel to first hop') ||
|
|
5687
|
+
msg.includes('Peer not found')) {
|
|
5688
|
+
code = 'PEER_NOT_CONNECTED';
|
|
5689
|
+
}
|
|
5690
|
+
return new errors_1.BeignetError(code, msg);
|
|
5691
|
+
}
|
|
5329
5692
|
async payInvoice(bolt11, timeoutMs = 60000, maxFeeSats, amountSats, metadata, cltvLimit) {
|
|
5330
5693
|
this._checkDraining();
|
|
5331
5694
|
const decoded = decodeInvoiceInput(bolt11);
|
|
@@ -5340,62 +5703,41 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5340
5703
|
1000n
|
|
5341
5704
|
: undefined;
|
|
5342
5705
|
const maxCltvExpiryHeight = this._cltvCeiling(cltvLimit);
|
|
5706
|
+
let claim;
|
|
5343
5707
|
if (spendAmountSats > 0) {
|
|
5344
5708
|
this._checkMaxPayment(spendAmountSats);
|
|
5345
5709
|
this._checkSpendLimit(spendAmountSats);
|
|
5346
|
-
this.
|
|
5710
|
+
claim = this._openAsyncSpendClaim(paymentHashHex, spendAmountSats);
|
|
5347
5711
|
}
|
|
5348
|
-
this._acquireBlockingPayment(paymentHashHex);
|
|
5349
|
-
let blockingReleased = false;
|
|
5350
|
-
const releaseBlockingPayment = () => {
|
|
5351
|
-
if (blockingReleased)
|
|
5352
|
-
return;
|
|
5353
|
-
blockingReleased = true;
|
|
5354
|
-
this._releaseBlockingPayment(paymentHashHex);
|
|
5355
|
-
};
|
|
5356
|
-
let released = false;
|
|
5357
|
-
const releaseReservation = () => {
|
|
5358
|
-
if (spendAmountSats <= 0 || released)
|
|
5359
|
-
return;
|
|
5360
|
-
released = true;
|
|
5361
|
-
this._pendingSpendSats -= spendAmountSats;
|
|
5362
|
-
};
|
|
5363
5712
|
try {
|
|
5364
5713
|
if (metadata) {
|
|
5365
5714
|
this.node.setPaymentMetadata(decoded.paymentHash, metadata);
|
|
5366
5715
|
}
|
|
5367
5716
|
}
|
|
5368
5717
|
catch (err) {
|
|
5369
|
-
|
|
5370
|
-
|
|
5718
|
+
if (claim)
|
|
5719
|
+
this._closeAsyncSpendClaim(paymentHashHex, claim);
|
|
5371
5720
|
throw err;
|
|
5372
5721
|
}
|
|
5373
5722
|
return new Promise((resolve, reject) => {
|
|
5374
5723
|
const timer = setTimeout(() => {
|
|
5375
5724
|
cleanup();
|
|
5376
|
-
|
|
5377
|
-
this.node.failPayment(decoded.paymentHash);
|
|
5378
|
-
reject(new errors_1.BeignetError('PAYMENT_TIMEOUT', `Payment timed out after ${timeoutMs}ms`));
|
|
5725
|
+
reject(this._paymentTimeout(decoded.paymentHash, 'Payment', timeoutMs, claim));
|
|
5379
5726
|
}, timeoutMs);
|
|
5380
5727
|
const cleanup = () => {
|
|
5381
5728
|
clearTimeout(timer);
|
|
5382
5729
|
this.node.removeListener('payment:sent', onSent);
|
|
5383
5730
|
this.node.removeListener('payment:failed', onFailed);
|
|
5384
|
-
releaseBlockingPayment();
|
|
5385
5731
|
};
|
|
5386
5732
|
const onSent = (info) => {
|
|
5387
5733
|
if (info.paymentHash.toString('hex') === paymentHashHex) {
|
|
5388
5734
|
cleanup();
|
|
5389
|
-
releaseReservation();
|
|
5390
|
-
if (spendAmountSats > 0)
|
|
5391
|
-
this._recordSpend(spendAmountSats);
|
|
5392
5735
|
resolve(this.toPaymentInfo(info));
|
|
5393
5736
|
}
|
|
5394
5737
|
};
|
|
5395
5738
|
const onFailed = (info) => {
|
|
5396
5739
|
if (info.paymentHash.toString('hex') === paymentHashHex) {
|
|
5397
5740
|
cleanup();
|
|
5398
|
-
releaseReservation();
|
|
5399
5741
|
const failDesc = info.failureCode !== undefined
|
|
5400
5742
|
? (0, errors_1.describeFailureCode)(info.failureCode)
|
|
5401
5743
|
: 'unknown';
|
|
@@ -5409,33 +5751,9 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5409
5751
|
}
|
|
5410
5752
|
catch (err) {
|
|
5411
5753
|
cleanup();
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
if (err instanceof Error && 'code' in err) {
|
|
5416
|
-
const lpErr = err;
|
|
5417
|
-
const codeMap = {
|
|
5418
|
-
NO_ROUTE: 'NO_ROUTE',
|
|
5419
|
-
DUPLICATE_PAYMENT: 'DUPLICATE_PAYMENT',
|
|
5420
|
-
NO_CHANNEL_TO_HOP: 'PEER_NOT_CONNECTED',
|
|
5421
|
-
FEE_EXCEEDS_MAX: 'PAYMENT_FAILED',
|
|
5422
|
-
CLTV_EXCEEDS_MAX: 'CLTV_EXCEEDS_MAX',
|
|
5423
|
-
MISSING_AMOUNT: 'INVALID_PARAMS',
|
|
5424
|
-
INVALID_INVOICE: 'INVALID_PARAMS',
|
|
5425
|
-
INVOICE_EXPIRED: 'INVOICE_EXPIRED'
|
|
5426
|
-
};
|
|
5427
|
-
code = codeMap[lpErr.code] || 'PAYMENT_FAILED';
|
|
5428
|
-
}
|
|
5429
|
-
else {
|
|
5430
|
-
if (msg.includes('No route found'))
|
|
5431
|
-
code = 'NO_ROUTE';
|
|
5432
|
-
else if (msg.includes('already in flight'))
|
|
5433
|
-
code = 'DUPLICATE_PAYMENT';
|
|
5434
|
-
else if (msg.includes('No channel to first hop') ||
|
|
5435
|
-
msg.includes('Peer not found'))
|
|
5436
|
-
code = 'PEER_NOT_CONNECTED';
|
|
5437
|
-
}
|
|
5438
|
-
reject(new errors_1.BeignetError(code, msg));
|
|
5754
|
+
if (claim)
|
|
5755
|
+
this._closeAsyncSpendClaim(paymentHashHex, claim);
|
|
5756
|
+
reject(this._toBeignetPaymentError(err));
|
|
5439
5757
|
}
|
|
5440
5758
|
});
|
|
5441
5759
|
}
|
|
@@ -5457,7 +5775,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5457
5775
|
catch {
|
|
5458
5776
|
}
|
|
5459
5777
|
if (hashHex !== 'unknown') {
|
|
5460
|
-
const existing = this.getPayment(hashHex);
|
|
5778
|
+
const existing = this.getPayment(hashHex) ?? this.durablePaymentFor(err, hashHex);
|
|
5461
5779
|
if (existing)
|
|
5462
5780
|
return existing;
|
|
5463
5781
|
}
|
|
@@ -5489,7 +5807,8 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5489
5807
|
throw err;
|
|
5490
5808
|
lastError = err;
|
|
5491
5809
|
if (!(0, errors_1.isRetryableError)(err)) {
|
|
5492
|
-
const pi = this.getPayment(paymentHashHex)
|
|
5810
|
+
const pi = this.getPayment(paymentHashHex) ??
|
|
5811
|
+
this.durablePaymentFor(err, paymentHashHex);
|
|
5493
5812
|
if (pi)
|
|
5494
5813
|
return { ...pi, attempts: attempt };
|
|
5495
5814
|
return {
|
|
@@ -5601,11 +5920,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5601
5920
|
catch (err) {
|
|
5602
5921
|
if (claim)
|
|
5603
5922
|
this._closeAsyncSpendClaim(paymentHashHex, claim);
|
|
5604
|
-
|
|
5605
|
-
err.code === 'CLTV_EXCEEDS_MAX') {
|
|
5606
|
-
throw new errors_1.BeignetError('CLTV_EXCEEDS_MAX', err.message);
|
|
5607
|
-
}
|
|
5608
|
-
throw err;
|
|
5923
|
+
throw this._toBeignetPaymentError(err);
|
|
5609
5924
|
}
|
|
5610
5925
|
if (claim && result.status === 'FAILED') {
|
|
5611
5926
|
this._releaseAsyncSpendClaim(claim);
|
|
@@ -5624,14 +5939,8 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5624
5939
|
: undefined;
|
|
5625
5940
|
this._checkMaxPayment(amountSats);
|
|
5626
5941
|
this._checkSpendLimit(amountSats);
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
const releaseReservation = () => {
|
|
5630
|
-
if (released)
|
|
5631
|
-
return;
|
|
5632
|
-
released = true;
|
|
5633
|
-
this._pendingSpendSats -= amountSats;
|
|
5634
|
-
};
|
|
5942
|
+
const provisionalKey = `keysend:${crypto.randomBytes(8).toString('hex')}`;
|
|
5943
|
+
const claim = this._openAsyncSpendClaim(provisionalKey, amountSats);
|
|
5635
5944
|
const destination = Buffer.from(pubkey, 'hex');
|
|
5636
5945
|
let result;
|
|
5637
5946
|
try {
|
|
@@ -5643,22 +5952,29 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5643
5952
|
});
|
|
5644
5953
|
}
|
|
5645
5954
|
catch (err) {
|
|
5646
|
-
|
|
5647
|
-
|
|
5955
|
+
if (claim)
|
|
5956
|
+
this._closeAsyncSpendClaim(provisionalKey, claim);
|
|
5957
|
+
throw this._toBeignetPaymentError(err);
|
|
5648
5958
|
}
|
|
5649
5959
|
const paymentHashHex = result.paymentHash.toString('hex');
|
|
5960
|
+
if (claim) {
|
|
5961
|
+
this._rekeyAsyncSpendClaim(provisionalKey, paymentHashHex, claim);
|
|
5962
|
+
}
|
|
5650
5963
|
if (result.status !== 'PENDING') {
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5964
|
+
if (claim) {
|
|
5965
|
+
if (result.status === 'COMPLETED') {
|
|
5966
|
+
this._chargeAsyncSpendClaim(paymentHashHex, claim);
|
|
5967
|
+
}
|
|
5968
|
+
else {
|
|
5969
|
+
this._releaseAsyncSpendClaim(claim);
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5654
5972
|
return this.toPaymentInfo(result);
|
|
5655
5973
|
}
|
|
5656
5974
|
return new Promise((resolve, reject) => {
|
|
5657
5975
|
const timer = setTimeout(() => {
|
|
5658
5976
|
cleanup();
|
|
5659
|
-
|
|
5660
|
-
this.node.failPayment(result.paymentHash);
|
|
5661
|
-
reject(new errors_1.BeignetError('PAYMENT_TIMEOUT', `Keysend timed out after ${timeoutMs}ms`));
|
|
5977
|
+
reject(this._paymentTimeout(result.paymentHash, 'Keysend', timeoutMs, claim));
|
|
5662
5978
|
}, timeoutMs);
|
|
5663
5979
|
const cleanup = () => {
|
|
5664
5980
|
clearTimeout(timer);
|
|
@@ -5668,15 +5984,12 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5668
5984
|
const onSent = (info) => {
|
|
5669
5985
|
if (info.paymentHash.toString('hex') === paymentHashHex) {
|
|
5670
5986
|
cleanup();
|
|
5671
|
-
releaseReservation();
|
|
5672
|
-
this._recordSpend(amountSats);
|
|
5673
5987
|
resolve(this.toPaymentInfo(info));
|
|
5674
5988
|
}
|
|
5675
5989
|
};
|
|
5676
5990
|
const onFailed = (info) => {
|
|
5677
5991
|
if (info.paymentHash.toString('hex') === paymentHashHex) {
|
|
5678
5992
|
cleanup();
|
|
5679
|
-
releaseReservation();
|
|
5680
5993
|
const failDesc = info.failureCode !== undefined
|
|
5681
5994
|
? (0, errors_1.describeFailureCode)(info.failureCode)
|
|
5682
5995
|
: 'unknown';
|
|
@@ -5740,6 +6053,19 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
5740
6053
|
return null;
|
|
5741
6054
|
return this.toPaymentInfo(p);
|
|
5742
6055
|
}
|
|
6056
|
+
durablePaymentFor(err, hashHex) {
|
|
6057
|
+
if (!(err instanceof errors_1.BeignetError) ||
|
|
6058
|
+
err.code !== errors_1.BeignetErrorCode.DUPLICATE_PAYMENT) {
|
|
6059
|
+
return null;
|
|
6060
|
+
}
|
|
6061
|
+
try {
|
|
6062
|
+
const durable = this.storage.loadPayment(hashHex);
|
|
6063
|
+
return durable ? this.toPaymentInfo(durable) : null;
|
|
6064
|
+
}
|
|
6065
|
+
catch {
|
|
6066
|
+
return null;
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
5743
6069
|
listForwards(filter) {
|
|
5744
6070
|
return this.node.listForwards(filter).map((f) => ({
|
|
5745
6071
|
id: f.id,
|
|
@@ -6166,45 +6492,31 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
6166
6492
|
this._checkDraining();
|
|
6167
6493
|
const paymentHashHex = bolt12Invoice.paymentHash.toString('hex');
|
|
6168
6494
|
const spendAmountSats = paymentSpendSats(bolt12Invoice.amount, amountSats);
|
|
6495
|
+
let claim;
|
|
6169
6496
|
if (spendAmountSats > 0) {
|
|
6170
6497
|
this._checkMaxPayment(spendAmountSats);
|
|
6171
6498
|
this._checkSpendLimit(spendAmountSats);
|
|
6172
|
-
this.
|
|
6499
|
+
claim = this._openAsyncSpendClaim(paymentHashHex, spendAmountSats);
|
|
6173
6500
|
}
|
|
6174
|
-
this._acquireBlockingPayment(paymentHashHex);
|
|
6175
|
-
let released = false;
|
|
6176
|
-
const releaseReservation = () => {
|
|
6177
|
-
if (spendAmountSats <= 0 || released)
|
|
6178
|
-
return;
|
|
6179
|
-
released = true;
|
|
6180
|
-
this._pendingSpendSats -= spendAmountSats;
|
|
6181
|
-
};
|
|
6182
6501
|
return new Promise((resolve, reject) => {
|
|
6183
6502
|
const timer = setTimeout(() => {
|
|
6184
6503
|
cleanup();
|
|
6185
|
-
|
|
6186
|
-
this.node.failPayment(bolt12Invoice.paymentHash);
|
|
6187
|
-
reject(new errors_1.BeignetError('PAYMENT_TIMEOUT', `Payment timed out after ${timeoutMs}ms`));
|
|
6504
|
+
reject(this._paymentTimeout(bolt12Invoice.paymentHash, 'Payment', timeoutMs, claim));
|
|
6188
6505
|
}, timeoutMs);
|
|
6189
6506
|
const cleanup = () => {
|
|
6190
6507
|
clearTimeout(timer);
|
|
6191
6508
|
this.node.removeListener('payment:sent', onSent);
|
|
6192
6509
|
this.node.removeListener('payment:failed', onFailed);
|
|
6193
|
-
this._releaseBlockingPayment(paymentHashHex);
|
|
6194
6510
|
};
|
|
6195
6511
|
const onSent = (info) => {
|
|
6196
6512
|
if (info.paymentHash.toString('hex') === paymentHashHex) {
|
|
6197
6513
|
cleanup();
|
|
6198
|
-
releaseReservation();
|
|
6199
|
-
if (spendAmountSats > 0)
|
|
6200
|
-
this._recordSpend(spendAmountSats);
|
|
6201
6514
|
resolve(this.toPaymentInfo(info));
|
|
6202
6515
|
}
|
|
6203
6516
|
};
|
|
6204
6517
|
const onFailed = (info) => {
|
|
6205
6518
|
if (info.paymentHash.toString('hex') === paymentHashHex) {
|
|
6206
6519
|
cleanup();
|
|
6207
|
-
releaseReservation();
|
|
6208
6520
|
const failDesc = info.failureCode !== undefined
|
|
6209
6521
|
? (0, errors_1.describeFailureCode)(info.failureCode)
|
|
6210
6522
|
: 'unknown';
|
|
@@ -6218,9 +6530,9 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
6218
6530
|
}
|
|
6219
6531
|
catch (err) {
|
|
6220
6532
|
cleanup();
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
reject(
|
|
6533
|
+
if (claim)
|
|
6534
|
+
this._closeAsyncSpendClaim(paymentHashHex, claim);
|
|
6535
|
+
reject(this._toBeignetPaymentError(err));
|
|
6224
6536
|
}
|
|
6225
6537
|
});
|
|
6226
6538
|
}
|
|
@@ -6873,12 +7185,121 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
6873
7185
|
setPaymentMetadata(paymentHash, metadata) {
|
|
6874
7186
|
this.node.setPaymentMetadata(Buffer.from(paymentHash, 'hex'), metadata);
|
|
6875
7187
|
}
|
|
7188
|
+
async resolveInterruptedPayment(bolt11) {
|
|
7189
|
+
if (this.destroyed) {
|
|
7190
|
+
throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NODE_DESTROYED, 'Node is shut down; an interrupted payment is settled at the next start');
|
|
7191
|
+
}
|
|
7192
|
+
if (this._restorePending || this._resuming) {
|
|
7193
|
+
throw new errors_1.BeignetError('NODE_RESTORE_PENDING', 'No node is running yet to settle an interrupted payment against');
|
|
7194
|
+
}
|
|
7195
|
+
if (this._restartRequired) {
|
|
7196
|
+
throw new errors_1.BeignetError('NODE_RESTART_REQUIRED', 'A capsule restore replaced this database; an interrupted payment ' +
|
|
7197
|
+
'is settled after the restart');
|
|
7198
|
+
}
|
|
7199
|
+
let paymentHash;
|
|
7200
|
+
try {
|
|
7201
|
+
paymentHash = (0, decode_1.decode)(bolt11).paymentHash;
|
|
7202
|
+
}
|
|
7203
|
+
catch {
|
|
7204
|
+
return { status: 'unpaid' };
|
|
7205
|
+
}
|
|
7206
|
+
const hashHex = paymentHash.toString('hex');
|
|
7207
|
+
const view = await this.node.awaitPaymentResolution(paymentHash);
|
|
7208
|
+
if (view.preimage || view.status === types_5.PaymentStatus.COMPLETED) {
|
|
7209
|
+
return { status: 'completed', paymentHash: hashHex };
|
|
7210
|
+
}
|
|
7211
|
+
const durable = this.storage.loadPayment(hashHex);
|
|
7212
|
+
if (durable?.direction === types_5.PaymentDirection.OUTGOING &&
|
|
7213
|
+
(durable.status === types_5.PaymentStatus.COMPLETED ||
|
|
7214
|
+
durable.preimage !== undefined)) {
|
|
7215
|
+
return { status: 'completed', paymentHash: hashHex };
|
|
7216
|
+
}
|
|
7217
|
+
return { status: 'unpaid' };
|
|
7218
|
+
}
|
|
7219
|
+
canCarryHtlc() {
|
|
7220
|
+
const live = this.node
|
|
7221
|
+
.getChannelManager()
|
|
7222
|
+
.listChannels()
|
|
7223
|
+
.filter(canBecomeUsable);
|
|
7224
|
+
return live.length === 0 || live.some((ch) => ch.acceptsNewHtlcs());
|
|
7225
|
+
}
|
|
7226
|
+
whenReadyToPay(run) {
|
|
7227
|
+
if (this.destroyed)
|
|
7228
|
+
return;
|
|
7229
|
+
if (this._restorePending) {
|
|
7230
|
+
this.once('recovery:restored', () => this.whenReadyToPay(run));
|
|
7231
|
+
return;
|
|
7232
|
+
}
|
|
7233
|
+
if (this._resuming || this._restartRequired)
|
|
7234
|
+
return;
|
|
7235
|
+
const node = this.node;
|
|
7236
|
+
const live = () => !this.destroyed &&
|
|
7237
|
+
!this._resuming &&
|
|
7238
|
+
!this._restartRequired &&
|
|
7239
|
+
this.node === node;
|
|
7240
|
+
let ready;
|
|
7241
|
+
try {
|
|
7242
|
+
ready = node.waitForReady(BeignetNode.MAX_TIMER_MS);
|
|
7243
|
+
}
|
|
7244
|
+
catch {
|
|
7245
|
+
return;
|
|
7246
|
+
}
|
|
7247
|
+
ready
|
|
7248
|
+
.then(() => {
|
|
7249
|
+
if (!live())
|
|
7250
|
+
return;
|
|
7251
|
+
if (this.canCarryHtlc()) {
|
|
7252
|
+
run();
|
|
7253
|
+
return;
|
|
7254
|
+
}
|
|
7255
|
+
const recheck = () => {
|
|
7256
|
+
if (live() && !this.canCarryHtlc())
|
|
7257
|
+
return;
|
|
7258
|
+
this.removeListener('channel:usable', recheck);
|
|
7259
|
+
this.removeListener(CHANNELS_CHANGED, recheck);
|
|
7260
|
+
if (live())
|
|
7261
|
+
run();
|
|
7262
|
+
};
|
|
7263
|
+
this.on('channel:usable', recheck);
|
|
7264
|
+
this.on(CHANNELS_CHANGED, recheck);
|
|
7265
|
+
}, () => {
|
|
7266
|
+
})
|
|
7267
|
+
.catch((err) => {
|
|
7268
|
+
try {
|
|
7269
|
+
this.log('warn', 'Starting the payment queue failed', {
|
|
7270
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7271
|
+
});
|
|
7272
|
+
}
|
|
7273
|
+
catch {
|
|
7274
|
+
}
|
|
7275
|
+
});
|
|
7276
|
+
}
|
|
6876
7277
|
getPaymentQueue() {
|
|
6877
7278
|
if (!this.paymentQueue) {
|
|
6878
|
-
this.paymentQueue = new payment_queue_1.PaymentQueue((bolt11, timeout, maxFee, amount, meta) => this.payInvoiceSafe(bolt11, timeout, maxFee, amount, meta), (amount) => this.
|
|
7279
|
+
this.paymentQueue = new payment_queue_1.PaymentQueue((bolt11, timeout, maxFee, amount, meta) => this.payInvoiceSafe(bolt11, timeout, maxFee, amount, meta), (amount) => this.node
|
|
7280
|
+
? this.canSend(amount)
|
|
7281
|
+
: { canSend: false, availableSats: 0 }, {
|
|
7282
|
+
resolveInterrupted: (b) => this.resolveInterruptedPayment(b),
|
|
7283
|
+
invoiceAmountSats: (b) => paymentSpendSats(decodeInvoiceInput(b).amountMsat)
|
|
7284
|
+
}, this.liveQueueStorage());
|
|
7285
|
+
if (this.destroyed) {
|
|
7286
|
+
this.paymentQueue.stop();
|
|
7287
|
+
}
|
|
7288
|
+
else {
|
|
7289
|
+
this.whenReadyToPay(() => this.paymentQueue?.start());
|
|
7290
|
+
this.on('channel:usable', () => this.paymentQueue?.poke());
|
|
7291
|
+
}
|
|
6879
7292
|
}
|
|
6880
7293
|
return this.paymentQueue;
|
|
6881
7294
|
}
|
|
7295
|
+
liveQueueStorage() {
|
|
7296
|
+
return {
|
|
7297
|
+
saveQueueEntry: (entry) => this.storage.saveQueueEntry(entry),
|
|
7298
|
+
updateQueueEntryStatus: (id, status, error, completedAt) => this.storage.updateQueueEntryStatus(id, status, error, completedAt),
|
|
7299
|
+
deleteQueueEntry: (id) => this.storage.deleteQueueEntry(id),
|
|
7300
|
+
loadAllQueueEntries: () => this.storage.loadAllQueueEntries()
|
|
7301
|
+
};
|
|
7302
|
+
}
|
|
6882
7303
|
enqueuePayment(bolt11, priority, opts) {
|
|
6883
7304
|
return this.getPaymentQueue().enqueue(bolt11, priority, opts);
|
|
6884
7305
|
}
|
|
@@ -7274,6 +7695,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
7274
7695
|
clearInterval(this._fallbackRecoveryTimer);
|
|
7275
7696
|
this._fallbackRecoveryTimer = undefined;
|
|
7276
7697
|
}
|
|
7698
|
+
this.paymentQueue?.stop();
|
|
7277
7699
|
this.paymentQueue?.removeAllListeners();
|
|
7278
7700
|
this.directFundingSender?.stop();
|
|
7279
7701
|
if (this._confirmTimer) {
|
|
@@ -7286,15 +7708,12 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
7286
7708
|
await this._backupPromise.catch(() => {
|
|
7287
7709
|
});
|
|
7288
7710
|
}
|
|
7289
|
-
await this.node?.gracefulShutdown(timeoutMs);
|
|
7290
|
-
this.storage.close();
|
|
7291
|
-
this.removeAllListeners();
|
|
7292
7711
|
try {
|
|
7293
|
-
await this.
|
|
7712
|
+
await this.node?.gracefulShutdown(timeoutMs);
|
|
7294
7713
|
}
|
|
7295
|
-
|
|
7714
|
+
finally {
|
|
7715
|
+
await this.stopWalletAndCloseStorage();
|
|
7296
7716
|
}
|
|
7297
|
-
this.releaseLock();
|
|
7298
7717
|
}
|
|
7299
7718
|
async destroy() {
|
|
7300
7719
|
this.stopOfflineReceive();
|
|
@@ -7326,16 +7745,29 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
7326
7745
|
}
|
|
7327
7746
|
this.stopRecoveryLeaseCheck();
|
|
7328
7747
|
this.clearAutoApplyTimers();
|
|
7748
|
+
this.paymentQueue?.stop();
|
|
7329
7749
|
this.paymentQueue?.removeAllListeners();
|
|
7330
7750
|
this.directFundingSender?.stop();
|
|
7331
|
-
|
|
7332
|
-
|
|
7751
|
+
try {
|
|
7752
|
+
this.node?.destroy();
|
|
7753
|
+
}
|
|
7754
|
+
finally {
|
|
7755
|
+
await this.stopWalletAndCloseStorage();
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
async stopWalletAndCloseStorage() {
|
|
7759
|
+
this._nodeStorageView?.close();
|
|
7333
7760
|
this.removeAllListeners();
|
|
7334
7761
|
try {
|
|
7335
7762
|
await this.wallet?.stop();
|
|
7336
7763
|
}
|
|
7337
7764
|
catch {
|
|
7338
7765
|
}
|
|
7766
|
+
try {
|
|
7767
|
+
this.storage.close();
|
|
7768
|
+
}
|
|
7769
|
+
catch {
|
|
7770
|
+
}
|
|
7339
7771
|
this.releaseLock();
|
|
7340
7772
|
}
|
|
7341
7773
|
releaseLock() {
|