@bsv/overlay 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +57 -27
- package/README.md +102 -269
- package/dist/cjs/mod.d.ts +15 -0
- package/dist/cjs/mod.d.ts.map +1 -0
- package/dist/cjs/mod.js +4 -1
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +41 -13
- package/dist/cjs/src/Advertisement.d.ts +9 -0
- package/dist/cjs/src/Advertisement.d.ts.map +1 -0
- package/dist/cjs/src/Advertiser.d.ts +36 -0
- package/dist/cjs/src/Advertiser.d.ts.map +1 -0
- package/dist/cjs/src/BASM.d.ts +110 -0
- package/dist/cjs/src/BASM.d.ts.map +1 -0
- package/dist/cjs/src/BASM.js.map +1 -1
- package/dist/cjs/src/BASMRemote.d.ts +14 -0
- package/dist/cjs/src/BASMRemote.d.ts.map +1 -0
- package/dist/cjs/src/BASMRemote.js.map +1 -1
- package/dist/cjs/src/Engine.d.ts +379 -0
- package/dist/cjs/src/Engine.d.ts.map +1 -0
- package/dist/cjs/src/Engine.js +425 -418
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPRemote.d.ts +24 -0
- package/dist/cjs/src/GASP/OverlayGASPRemote.d.ts.map +1 -0
- package/dist/cjs/src/GASP/OverlayGASPRemote.js +7 -10
- package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.d.ts +103 -0
- package/dist/cjs/src/GASP/OverlayGASPStorage.d.ts.map +1 -0
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +32 -30
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/LookupFormula.d.ts +29 -0
- package/dist/cjs/src/LookupFormula.d.ts.map +1 -0
- package/dist/cjs/src/LookupService.d.ts +85 -0
- package/dist/cjs/src/LookupService.d.ts.map +1 -0
- package/dist/cjs/src/Output.d.ts +32 -0
- package/dist/cjs/src/Output.d.ts.map +1 -0
- package/dist/cjs/src/SafeLog.d.ts +14 -0
- package/dist/cjs/src/SafeLog.d.ts.map +1 -0
- package/dist/cjs/src/SafeLog.js +44 -0
- package/dist/cjs/src/SafeLog.js.map +1 -0
- package/{src/SyncConfiguration.ts → dist/cjs/src/SyncConfiguration.d.ts} +2 -1
- package/dist/cjs/src/SyncConfiguration.d.ts.map +1 -0
- package/dist/cjs/src/TopicManager.d.ts +35 -0
- package/dist/cjs/src/TopicManager.d.ts.map +1 -0
- package/dist/cjs/src/storage/Storage.d.ts +237 -0
- package/dist/cjs/src/storage/Storage.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/KnexStorage.d.ts +69 -0
- package/dist/cjs/src/storage/knex/KnexStorage.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/KnexStorage.js +7 -10
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/all-migrations.d.ts +11 -0
- package/dist/cjs/src/storage/knex/all-migrations.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts +9 -0
- package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -1
- package/dist/esm/mod.js +2 -1
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/BASMRemote.js.map +1 -1
- package/dist/esm/src/Engine.js +404 -396
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
- package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +21 -18
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/SafeLog.js +40 -0
- package/dist/esm/src/SafeLog.js.map +1 -0
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/types/mod.d.ts +5 -4
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/BASMRemote.d.ts.map +1 -1
- package/dist/types/src/Engine.d.ts +18 -1
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts +2 -2
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts +3 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/src/SafeLog.d.ts +14 -0
- package/dist/types/src/SafeLog.d.ts.map +1 -0
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
- package/package.json +85 -28
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +0 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +0 -1
- package/dist/types/tsconfig.types.tsbuildinfo +0 -1
- package/docs/API.md +0 -944
- package/docs/BRC-136-BASM.md +0 -327
- package/docs/README.md +0 -16
- package/docs/Synchronization.md +0 -212
- package/docs/concepts/00-overview.md +0 -85
- package/docs/concepts/01-best-practices.md +0 -202
- package/docs/concepts/02-query-performance.md +0 -345
- package/docs/concepts/03-database-monitoring.md +0 -211
- package/docs/concepts/04-pagination-example.md +0 -186
- package/docs/concepts/05-recommendations-summary.md +0 -158
- package/docs/concepts/README.md +0 -20
- package/docs/examples/README.md +0 -15
- package/docs/examples/gs-wip.md +0 -111
- package/docs/internal/README.md +0 -11
- package/mod.ts +0 -40
- package/src/Advertisement.ts +0 -8
- package/src/Advertiser.ts +0 -41
- package/src/BASM.ts +0 -208
- package/src/BASMRemote.ts +0 -54
- package/src/Engine.ts +0 -2194
- package/src/GASP/OverlayGASPRemote.ts +0 -108
- package/src/GASP/OverlayGASPStorage.ts +0 -384
- package/src/LookupFormula.ts +0 -31
- package/src/LookupService.ts +0 -136
- package/src/Output.ts +0 -31
- package/src/TopicManager.ts +0 -40
- package/src/__tests/BASM.test.ts +0 -44
- package/src/__tests/BASMChain.test.ts +0 -295
- package/src/__tests/Engine.test.ts +0 -1381
- package/src/__tests/OverlayGASPRemote.test.ts +0 -135
- package/src/__tests/OverlayGASPStorage.test.ts +0 -205
- package/src/storage/Storage.ts +0 -260
- package/src/storage/knex/KnexStorage.ts +0 -651
- package/src/storage/knex/all-migrations.ts +0 -32
- package/src/storage/knex/migrations/2024-05-18-001-initial.ts +0 -30
- package/src/storage/knex/migrations/2024-07-10-001-block-height.ts +0 -15
- package/src/storage/knex/migrations/2024-07-17-001-transactions.ts +0 -64
- package/src/storage/knex/migrations/2024-07-18-001-indexes.ts +0 -30
- package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +0 -42
- package/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.ts +0 -46
- package/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.ts +0 -24
- package/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.ts +0 -18
- package/src/storage/knex/migrations/2026-05-29-001-brc136-basm.ts +0 -123
package/dist/cjs/src/Engine.js
CHANGED
|
@@ -7,8 +7,16 @@ const OverlayGASPRemote_js_1 = require("./GASP/OverlayGASPRemote.js");
|
|
|
7
7
|
const OverlayGASPStorage_js_1 = require("./GASP/OverlayGASPStorage.js");
|
|
8
8
|
const BASM_js_1 = require("./BASM.js");
|
|
9
9
|
const BASMRemote_js_1 = require("./BASMRemote.js");
|
|
10
|
+
const SafeLog_js_1 = require("./SafeLog.js");
|
|
10
11
|
const DEFAULT_GASP_SYNC_LIMIT = 10000;
|
|
11
12
|
const DEFAULT_BASM_RANGE_LIMIT = 1024;
|
|
13
|
+
function findSpendingInputIndex(tx, output) {
|
|
14
|
+
return tx.inputs.findIndex(input => {
|
|
15
|
+
var _a;
|
|
16
|
+
const realSource = input.sourceTXID || ((_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex'));
|
|
17
|
+
return realSource === output.txid && input.sourceOutputIndex === output.outputIndex;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
12
20
|
/**
|
|
13
21
|
* An engine for running BSV Overlay Services (topic managers and lookup services).
|
|
14
22
|
*/
|
|
@@ -34,10 +42,11 @@ class Engine {
|
|
|
34
42
|
* @param {TopicAnchorHeaderResolver} topicAnchorHeaderResolver - Resolves block hashes for BASM anchors.
|
|
35
43
|
* @param {boolean} basmSyncEnabled - Whether BASM sync should run automatically.
|
|
36
44
|
* @param {number} unprovenEvictionBlocks - Default block age for opt-in unproven state eviction.
|
|
45
|
+
* @param {number} maxLookupResults - Maximum lookup formulas hydrated per request. Use -1 to opt out.
|
|
37
46
|
*/
|
|
38
|
-
constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration, logTime = false, logPrefix = '[OVERLAY_ENGINE] ', throwOnBroadcastFailure = false, overlayBroadcastFacilitator = new sdk_1.HTTPSOverlayBroadcastFacilitator(), logger = console, suppressDefaultSyncAdvertisements = true, topicAnchorHeaderResolver, basmSyncEnabled = false, unprovenEvictionBlocks = 144) {
|
|
39
|
-
var _a;
|
|
40
|
-
var
|
|
47
|
+
constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration, logTime = false, logPrefix = '[OVERLAY_ENGINE] ', throwOnBroadcastFailure = false, overlayBroadcastFacilitator = new sdk_1.HTTPSOverlayBroadcastFacilitator(), logger = console, suppressDefaultSyncAdvertisements = true, topicAnchorHeaderResolver, basmSyncEnabled = false, unprovenEvictionBlocks = 144, maxLookupResults = 1000) {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
var _c;
|
|
41
50
|
this.managers = managers;
|
|
42
51
|
this.lookupServices = lookupServices;
|
|
43
52
|
this.storage = storage;
|
|
@@ -57,14 +66,13 @@ class Engine {
|
|
|
57
66
|
this.topicAnchorHeaderResolver = topicAnchorHeaderResolver;
|
|
58
67
|
this.basmSyncEnabled = basmSyncEnabled;
|
|
59
68
|
this.unprovenEvictionBlocks = unprovenEvictionBlocks;
|
|
69
|
+
this.maxLookupResults = maxLookupResults;
|
|
70
|
+
if (maxLookupResults !== -1 && (!Number.isSafeInteger(maxLookupResults) || maxLookupResults < 1)) {
|
|
71
|
+
throw new TypeError('maxLookupResults must be -1 or a positive safe integer');
|
|
72
|
+
}
|
|
60
73
|
// To encourage synchronization of overlay services, the SHIP sync strategy is used by default for all overlay topics, except for 'tm_ship' and 'tm_slap'.
|
|
61
74
|
// For these two topics, any existing trackers are combined with the provided shipTrackers and slapTrackers omitting any duplicates.
|
|
62
|
-
|
|
63
|
-
this.syncConfiguration = {};
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
this.syncConfiguration = syncConfiguration;
|
|
67
|
-
}
|
|
75
|
+
(_a = this.syncConfiguration) !== null && _a !== void 0 ? _a : (this.syncConfiguration = {});
|
|
68
76
|
for (const managerName of Object.keys(managers)) {
|
|
69
77
|
if (managerName === 'tm_ship' && this.shipTrackers !== undefined && this.syncConfiguration[managerName] !== false) {
|
|
70
78
|
// Combine tm_ship trackers with preexisting entries if any
|
|
@@ -84,7 +92,7 @@ class Engine {
|
|
|
84
92
|
}
|
|
85
93
|
else {
|
|
86
94
|
// Set undefined managers to 'SHIP' by default
|
|
87
|
-
(
|
|
95
|
+
(_b = (_c = this.syncConfiguration)[managerName]) !== null && _b !== void 0 ? _b : (_c[managerName] = 'SHIP');
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
}
|
|
@@ -124,7 +132,7 @@ class Engine {
|
|
|
124
132
|
return header.blockHash;
|
|
125
133
|
}
|
|
126
134
|
catch (error) {
|
|
127
|
-
this.logger.warn(`Unable to resolve BASM block hash
|
|
135
|
+
this.logger.warn(`Unable to resolve BASM block hash: height=${(0, SafeLog_js_1.serializeLogValue)(blockHeight)} error=${(0, SafeLog_js_1.serializeErrorForLog)(error)}`);
|
|
128
136
|
return undefined;
|
|
129
137
|
}
|
|
130
138
|
}
|
|
@@ -232,7 +240,7 @@ class Engine {
|
|
|
232
240
|
}
|
|
233
241
|
if (toHeight - fromHeight + 1 > DEFAULT_BASM_RANGE_LIMIT) {
|
|
234
242
|
// Bound the work per pass; the next trigger resumes from the new tip.
|
|
235
|
-
this.logger.warn(`[BASM] capping anchor chain extension
|
|
243
|
+
this.logger.warn(`[BASM] capping anchor chain extension: topic=${(0, SafeLog_js_1.serializeLogValue)(topic)} limit=${(0, SafeLog_js_1.serializeLogValue)(DEFAULT_BASM_RANGE_LIMIT)} requestedFrom=${(0, SafeLog_js_1.serializeLogValue)(fromHeight)} requestedTo=${(0, SafeLog_js_1.serializeLogValue)(toHeight)}; will continue on the next pass`);
|
|
236
244
|
toHeight = fromHeight + DEFAULT_BASM_RANGE_LIMIT - 1;
|
|
237
245
|
}
|
|
238
246
|
const previousAnchor = fromHeight > 0
|
|
@@ -246,7 +254,7 @@ class Engine {
|
|
|
246
254
|
// canonical re-resolution from the header resolver instead of reusing it.
|
|
247
255
|
const blockHash = (_c = (_b = blockHashHints.get(height)) !== null && _b !== void 0 ? _b : (forceResolve ? undefined : existing === null || existing === void 0 ? void 0 : existing.blockHash)) !== null && _c !== void 0 ? _c : await this.resolveBlockHash(height);
|
|
248
256
|
if (blockHash === undefined) {
|
|
249
|
-
this.logger.warn(`[BASM] unable to resolve block hash
|
|
257
|
+
this.logger.warn(`[BASM] unable to resolve block hash: topic=${(0, SafeLog_js_1.serializeLogValue)(topic)} height=${(0, SafeLog_js_1.serializeLogValue)(height)}; halting chain extension`);
|
|
250
258
|
return;
|
|
251
259
|
}
|
|
252
260
|
const basmRoot = (0, BASM_js_1.computeBasmRoot)(admitted);
|
|
@@ -375,386 +383,331 @@ class Engine {
|
|
|
375
383
|
newTipHeight: tip
|
|
376
384
|
});
|
|
377
385
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
async submit(taggedBEEF, onSteakReady, mode = 'current-tx', offChainValues) {
|
|
390
|
-
var _a, _b;
|
|
391
|
-
for (const t of taggedBEEF.topics) {
|
|
392
|
-
if (this.managers[t] === undefined || this.managers[t] === null) {
|
|
393
|
-
throw new Error(`This server does not support this topic: ${t}`);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// Validate the transaction SPV information
|
|
397
|
-
const tx = sdk_1.Transaction.fromBEEF(taggedBEEF.beef);
|
|
398
|
-
const txid = tx.id('hex');
|
|
399
|
-
this.startTime(`submit_${txid}`);
|
|
400
|
-
if (mode !== 'historical-tx-no-spv') {
|
|
401
|
-
this.startTime(`chainTracker_${txid.substring(0, 10)}`);
|
|
402
|
-
const txValid = await tx.verify(this.chainTracker);
|
|
403
|
-
if (!txValid)
|
|
404
|
-
throw new Error('Unable to verify SPV information.');
|
|
405
|
-
this.endTime(`chainTracker_${txid.substring(0, 10)}`);
|
|
406
|
-
}
|
|
407
|
-
const steak = {};
|
|
408
|
-
const dupeTopics = new Set();
|
|
409
|
-
const failedTopics = new Set();
|
|
410
|
-
const topicValidations = taggedBEEF.topics.map(async (topic) => {
|
|
411
|
-
try {
|
|
412
|
-
if (this.managers[topic] === undefined || this.managers[topic] === null) {
|
|
413
|
-
throw new Error(`This server does not support this topic: ${topic}`);
|
|
414
|
-
}
|
|
415
|
-
// Check for duplicate transactions
|
|
416
|
-
this.startTime(`dupCheck_${txid.substring(0, 10)}`);
|
|
417
|
-
const dupeCheck = await this.storage.doesAppliedTransactionExist({ txid, topic });
|
|
418
|
-
this.endTime(`dupCheck_${txid.substring(0, 10)}`);
|
|
419
|
-
if (dupeCheck) {
|
|
420
|
-
dupeTopics.add(topic);
|
|
421
|
-
return {
|
|
422
|
-
topic,
|
|
423
|
-
isDupe: true,
|
|
424
|
-
previousCoins: [],
|
|
425
|
-
previousOutputs: [],
|
|
426
|
-
admissibleOutputs: { outputsToAdmit: [], coinsToRetain: [] }
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
// Identify previous coins admitted to this specific topic
|
|
430
|
-
const previousCoins = [];
|
|
431
|
-
const outputPromises = tx.inputs.map(async (input, i) => {
|
|
432
|
-
var _a, _b;
|
|
433
|
-
const previousTXID = (_a = input.sourceTXID) !== null && _a !== void 0 ? _a : (_b = input.sourceTransaction) === null || _b === void 0 ? void 0 : _b.id('hex');
|
|
434
|
-
if (previousTXID !== undefined) {
|
|
435
|
-
// Check if the previous output was admitted to this specific topic
|
|
436
|
-
const output = await this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
|
|
437
|
-
if (output !== undefined && output !== null) {
|
|
438
|
-
previousCoins.push(i);
|
|
439
|
-
return output;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return null;
|
|
443
|
-
});
|
|
444
|
-
this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`);
|
|
445
|
-
const previousOutputs = await Promise.all(outputPromises);
|
|
446
|
-
this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`);
|
|
447
|
-
// Determine which outputs are admissible for this topic (validation only)
|
|
448
|
-
this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
|
|
449
|
-
const admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins, offChainValues, mode);
|
|
450
|
-
this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
|
|
451
|
-
return {
|
|
452
|
-
topic,
|
|
453
|
-
isDupe: false,
|
|
454
|
-
previousCoins,
|
|
455
|
-
previousOutputs,
|
|
456
|
-
admissibleOutputs
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
catch (error) {
|
|
460
|
-
this.logger.error('Error validating topic during submit:', error);
|
|
461
|
-
failedTopics.add(topic);
|
|
386
|
+
async validateTopicSubmission(topic, context) {
|
|
387
|
+
const { tx, txid, beef, offChainValues, mode, dupeTopics, failedTopics } = context;
|
|
388
|
+
try {
|
|
389
|
+
if (this.managers[topic] === undefined || this.managers[topic] === null) {
|
|
390
|
+
throw new Error(`This server does not support this topic: ${topic}`);
|
|
391
|
+
}
|
|
392
|
+
this.startTime(`dupCheck_${txid.substring(0, 10)}`);
|
|
393
|
+
const isDupe = await this.storage.doesAppliedTransactionExist({ txid, topic });
|
|
394
|
+
this.endTime(`dupCheck_${txid.substring(0, 10)}`);
|
|
395
|
+
if (isDupe) {
|
|
396
|
+
dupeTopics.add(topic);
|
|
462
397
|
return {
|
|
463
398
|
topic,
|
|
464
|
-
isDupe:
|
|
399
|
+
isDupe: true,
|
|
465
400
|
previousCoins: [],
|
|
466
401
|
previousOutputs: [],
|
|
467
402
|
admissibleOutputs: { outputsToAdmit: [], coinsToRetain: [] }
|
|
468
403
|
};
|
|
469
404
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
405
|
+
const previousCoins = [];
|
|
406
|
+
const outputPromises = tx.inputs.map(async (input, inputIndex) => {
|
|
407
|
+
var _a;
|
|
408
|
+
var _b;
|
|
409
|
+
const previousTXID = (_b = input.sourceTXID) !== null && _b !== void 0 ? _b : (_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex');
|
|
410
|
+
if (previousTXID === undefined)
|
|
411
|
+
return null;
|
|
412
|
+
const output = await this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
|
|
413
|
+
if (output !== undefined && output !== null)
|
|
414
|
+
previousCoins.push(inputIndex);
|
|
415
|
+
return output !== null && output !== void 0 ? output : null;
|
|
416
|
+
});
|
|
417
|
+
this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`);
|
|
418
|
+
const previousOutputs = await Promise.all(outputPromises);
|
|
419
|
+
this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`);
|
|
420
|
+
this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
|
|
421
|
+
const admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(beef, previousCoins, offChainValues, mode);
|
|
422
|
+
this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
|
|
423
|
+
return {
|
|
424
|
+
topic,
|
|
425
|
+
isDupe: false,
|
|
426
|
+
previousCoins,
|
|
427
|
+
previousOutputs,
|
|
428
|
+
admissibleOutputs
|
|
429
|
+
};
|
|
475
430
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
431
|
+
catch (error) {
|
|
432
|
+
this.logger.error(`Error validating topic during submit: topic=${(0, SafeLog_js_1.serializeLogValue)(topic)} error=${(0, SafeLog_js_1.serializeErrorForLog)(error)}`);
|
|
433
|
+
failedTopics.add(topic);
|
|
434
|
+
return {
|
|
435
|
+
topic,
|
|
436
|
+
isDupe: false,
|
|
437
|
+
previousCoins: [],
|
|
438
|
+
previousOutputs: [],
|
|
439
|
+
admissibleOutputs: { outputsToAdmit: [], coinsToRetain: [] }
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
isTopicSubmissionAccepted(validation, failedTopics) {
|
|
444
|
+
return !failedTopics.has(validation.topic) && (validation.isDupe ||
|
|
445
|
+
validation.admissibleOutputs.outputsToAdmit.length > 0 ||
|
|
446
|
+
validation.admissibleOutputs.coinsToRetain.length > 0 ||
|
|
447
|
+
validation.previousCoins.length > 0);
|
|
448
|
+
}
|
|
449
|
+
async broadcastAcceptedSubmission(tx, txid, mode, anyTopicAccepted) {
|
|
493
450
|
this.startTime(`broadcast_${txid.substring(0, 10)}`);
|
|
494
451
|
if (mode !== 'historical-tx' && this.broadcaster !== undefined && anyTopicAccepted) {
|
|
495
452
|
try {
|
|
496
453
|
let response;
|
|
497
454
|
if (tx.merklePath !== undefined) {
|
|
498
|
-
// tx has been verified, thus if there is a merklePath, the transaction is already on-chain...skip broadcast.
|
|
499
|
-
const txid = tx.id('hex');
|
|
500
455
|
const mp = tx.merklePath;
|
|
501
456
|
const leaf = mp.path[0].find(leaf => leaf.hash === txid);
|
|
502
|
-
|
|
457
|
+
response = {
|
|
503
458
|
status: 'success',
|
|
504
|
-
txid
|
|
505
|
-
message: `In block at height ${mp.blockHeight} index ${leaf === null || leaf === void 0 ? void 0 : leaf.offset}
|
|
459
|
+
txid,
|
|
460
|
+
message: `In block at height ${mp.blockHeight} index ${leaf === null || leaf === void 0 ? void 0 : leaf.offset}`
|
|
506
461
|
};
|
|
507
|
-
response = r;
|
|
508
462
|
}
|
|
509
463
|
else {
|
|
510
464
|
response = await this.broadcaster.broadcast(tx);
|
|
511
465
|
}
|
|
512
466
|
if ((0, sdk_1.isBroadcastFailure)(response) && this.throwOnBroadcastFailure) {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
throw
|
|
467
|
+
const error = new Error(`Failed to broadcast transaction! Error: ${response.description}`);
|
|
468
|
+
error.more = response.more;
|
|
469
|
+
throw error;
|
|
516
470
|
}
|
|
517
471
|
}
|
|
518
472
|
catch (error) {
|
|
519
|
-
if (this.throwOnBroadcastFailure)
|
|
473
|
+
if (this.throwOnBroadcastFailure)
|
|
520
474
|
throw error;
|
|
521
|
-
}
|
|
522
475
|
this.logger.error('Error broadcasting transaction:', error);
|
|
523
476
|
}
|
|
524
477
|
}
|
|
525
478
|
this.endTime(`broadcast_${txid.substring(0, 10)}`);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
479
|
+
}
|
|
480
|
+
async notifyOutputSpent(lookupService, tx, txid, output, topic, offChainValues) {
|
|
481
|
+
var _a;
|
|
482
|
+
if (typeof lookupService.outputSpent !== 'function')
|
|
483
|
+
return;
|
|
484
|
+
if (lookupService.spendNotificationMode === 'txid') {
|
|
485
|
+
await lookupService.outputSpent({
|
|
486
|
+
mode: 'txid',
|
|
487
|
+
spendingTxid: txid,
|
|
488
|
+
txid: output.txid,
|
|
489
|
+
outputIndex: output.outputIndex,
|
|
490
|
+
topic
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
529
493
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
494
|
+
if (lookupService.spendNotificationMode === 'script') {
|
|
495
|
+
const inputIndex = findSpendingInputIndex(tx, output);
|
|
496
|
+
if (inputIndex === -1)
|
|
497
|
+
throw new Error('Could not find input index');
|
|
498
|
+
await lookupService.outputSpent({
|
|
499
|
+
mode: 'script',
|
|
500
|
+
spendingTxid: txid,
|
|
501
|
+
inputIndex,
|
|
502
|
+
sequenceNumber: (_a = tx.inputs[inputIndex].sequence) !== null && _a !== void 0 ? _a : 0xffffffff,
|
|
503
|
+
unlockingScript: tx.inputs[inputIndex].unlockingScript,
|
|
504
|
+
txid: output.txid,
|
|
505
|
+
outputIndex: output.outputIndex,
|
|
506
|
+
topic,
|
|
507
|
+
offChainValues
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (lookupService.spendNotificationMode === 'whole-tx') {
|
|
512
|
+
await lookupService.outputSpent({
|
|
513
|
+
mode: 'whole-tx',
|
|
514
|
+
spendingAtomicBEEF: tx.toAtomicBEEF(),
|
|
515
|
+
txid: output.txid,
|
|
516
|
+
outputIndex: output.outputIndex,
|
|
517
|
+
topic,
|
|
518
|
+
offChainValues
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
await lookupService.outputSpent({
|
|
523
|
+
mode: 'none',
|
|
524
|
+
txid: output.txid,
|
|
525
|
+
outputIndex: output.outputIndex,
|
|
526
|
+
topic
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async markPreviousOutputSpent(output, topic, tx, txid, offChainValues) {
|
|
530
|
+
if (output === null)
|
|
531
|
+
return;
|
|
532
|
+
try {
|
|
533
|
+
await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic);
|
|
534
|
+
await Promise.all(Object.values(this.lookupServices).map(async (lookupService) => {
|
|
535
|
+
try {
|
|
536
|
+
await this.notifyOutputSpent(lookupService, tx, txid, output, topic, offChainValues);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
this.logger.error('Error in lookup service for outputSpent:', error);
|
|
540
|
+
}
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
this.logger.error('Error marking UTXO as spent:', error);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async markPreviousOutputsSpent(validations, failedTopics, tx, txid, offChainValues) {
|
|
534
548
|
await Promise.all(validations.map(async (validation) => {
|
|
535
|
-
if (validation.isDupe || failedTopics.has(validation.topic))
|
|
549
|
+
if (validation.isDupe || failedTopics.has(validation.topic))
|
|
536
550
|
return;
|
|
551
|
+
await Promise.all(validation.previousOutputs.map(async (output) => {
|
|
552
|
+
await this.markPreviousOutputSpent(output, validation.topic, tx, txid, offChainValues);
|
|
553
|
+
}));
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
classifyPreviousCoins(tx, validation) {
|
|
557
|
+
var _a;
|
|
558
|
+
var _b;
|
|
559
|
+
const outputsConsumed = [];
|
|
560
|
+
const outputsToMarkStale = [];
|
|
561
|
+
for (const inputIndex of validation.previousCoins) {
|
|
562
|
+
const input = tx.inputs[inputIndex];
|
|
563
|
+
const previousTXID = (_b = input.sourceTXID) !== null && _b !== void 0 ? _b : (_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex');
|
|
564
|
+
if (typeof previousTXID !== 'string')
|
|
565
|
+
continue;
|
|
566
|
+
if (validation.admissibleOutputs.coinsToRetain.includes(inputIndex)) {
|
|
567
|
+
outputsConsumed.push({
|
|
568
|
+
txid: previousTXID,
|
|
569
|
+
outputIndex: input.sourceOutputIndex
|
|
570
|
+
});
|
|
537
571
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
await l.outputSpent({
|
|
571
|
-
mode: 'script',
|
|
572
|
-
spendingTxid: txid,
|
|
573
|
-
inputIndex,
|
|
574
|
-
sequenceNumber: (_a = tx.inputs[inputIndex].sequence) !== null && _a !== void 0 ? _a : 0xffffffff,
|
|
575
|
-
unlockingScript: tx.inputs[inputIndex].unlockingScript,
|
|
576
|
-
txid: output.txid,
|
|
577
|
-
outputIndex: output.outputIndex,
|
|
578
|
-
topic,
|
|
579
|
-
offChainValues
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
else if (l.spendNotificationMode === 'whole-tx') {
|
|
583
|
-
await l.outputSpent({
|
|
584
|
-
mode: 'whole-tx',
|
|
585
|
-
spendingAtomicBEEF: tx.toAtomicBEEF(),
|
|
586
|
-
txid: output.txid,
|
|
587
|
-
outputIndex: output.outputIndex,
|
|
588
|
-
topic,
|
|
589
|
-
offChainValues
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
else { // none
|
|
593
|
-
await l.outputSpent({
|
|
594
|
-
mode: 'none',
|
|
595
|
-
txid: output.txid,
|
|
596
|
-
outputIndex: output.outputIndex,
|
|
597
|
-
topic
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
this.logger.error('Error in lookup service for outputSpent:', error);
|
|
604
|
-
}
|
|
605
|
-
}));
|
|
606
|
-
}
|
|
607
|
-
catch (error) {
|
|
608
|
-
this.logger.error('Error marking UTXO as spent:', error);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
572
|
+
else {
|
|
573
|
+
outputsToMarkStale.push({
|
|
574
|
+
txid: previousTXID,
|
|
575
|
+
previousOutputIndex: input.sourceOutputIndex,
|
|
576
|
+
inputIndex
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return { outputsConsumed, outputsToMarkStale };
|
|
581
|
+
}
|
|
582
|
+
async removeStaleOutputs(outputs, topic, txid) {
|
|
583
|
+
this.startTime(`lookForStaleOutputs_${txid.substring(0, 10)}`);
|
|
584
|
+
await Promise.all(outputs.map(async (coin) => {
|
|
585
|
+
const output = await this.storage.findOutput(coin.txid, coin.previousOutputIndex, topic);
|
|
586
|
+
if (output !== undefined && output !== null)
|
|
587
|
+
await this.deleteUTXODeep(output);
|
|
588
|
+
}));
|
|
589
|
+
this.endTime(`lookForStaleOutputs_${txid.substring(0, 10)}`);
|
|
590
|
+
}
|
|
591
|
+
async notifyOutputAdmitted(lookupService, tx, txid, outputIndex, topic, offChainValues) {
|
|
592
|
+
if (lookupService.admissionMode === 'locking-script') {
|
|
593
|
+
if (typeof tx.outputs[outputIndex].lockingScript !== 'object' ||
|
|
594
|
+
typeof tx.outputs[outputIndex].satoshis !== 'number')
|
|
595
|
+
return;
|
|
596
|
+
await lookupService.outputAdmittedByTopic({
|
|
597
|
+
mode: 'locking-script',
|
|
598
|
+
txid,
|
|
599
|
+
outputIndex,
|
|
600
|
+
lockingScript: tx.outputs[outputIndex].lockingScript,
|
|
601
|
+
satoshis: tx.outputs[outputIndex].satoshis,
|
|
602
|
+
topic,
|
|
603
|
+
offChainValues
|
|
611
604
|
});
|
|
612
|
-
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
await lookupService.outputAdmittedByTopic({
|
|
608
|
+
mode: 'whole-tx',
|
|
609
|
+
atomicBEEF: tx.toAtomicBEEF(),
|
|
610
|
+
outputIndex,
|
|
611
|
+
topic,
|
|
612
|
+
offChainValues
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async admitOutput(outputIndex, context) {
|
|
616
|
+
var _a;
|
|
617
|
+
const { tx, txid, beef, topic, outputsConsumed, newUTXOs, offChainValues } = context;
|
|
618
|
+
if (typeof tx.outputs[outputIndex].satoshis !== 'number')
|
|
619
|
+
return;
|
|
620
|
+
this.startTime(`insertNewOutput_${txid.substring(0, 10)}`);
|
|
621
|
+
await this.storage.insertOutput({
|
|
622
|
+
txid,
|
|
623
|
+
outputIndex,
|
|
624
|
+
outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
|
|
625
|
+
satoshis: tx.outputs[outputIndex].satoshis,
|
|
626
|
+
topic,
|
|
627
|
+
spent: false,
|
|
628
|
+
beef: this.compactBEEFForStorage(tx, beef),
|
|
629
|
+
consumedBy: [],
|
|
630
|
+
outputsConsumed,
|
|
631
|
+
score: Date.now(),
|
|
632
|
+
blockHeight: (_a = (0, BASM_js_1.extractMerkleProofMetadata)(txid, tx.merklePath)) === null || _a === void 0 ? void 0 : _a.blockHeight
|
|
633
|
+
});
|
|
634
|
+
this.endTime(`insertNewOutput_${txid.substring(0, 10)}`);
|
|
635
|
+
newUTXOs.push({ txid, outputIndex });
|
|
636
|
+
this.startTime(`notifyLookupService${txid.substring(0, 10)}`);
|
|
637
|
+
await Promise.all(Object.values(this.lookupServices).map(async (lookupService) => {
|
|
638
|
+
try {
|
|
639
|
+
await this.notifyOutputAdmitted(lookupService, tx, txid, outputIndex, topic, offChainValues);
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
this.logger.error('Error in lookup service for outputAdmittedByTopic:', error);
|
|
643
|
+
}
|
|
613
644
|
}));
|
|
614
|
-
|
|
645
|
+
this.endTime(`notifyLookupService${txid.substring(0, 10)}`);
|
|
646
|
+
}
|
|
647
|
+
async updateConsumedOutput(output, newUTXOs, topic) {
|
|
648
|
+
const storedOutput = await this.storage.findOutput(output.txid, output.outputIndex, topic);
|
|
649
|
+
if (storedOutput === undefined || storedOutput === null)
|
|
650
|
+
return;
|
|
651
|
+
const consumedBy = [...new Set([...newUTXOs, ...storedOutput.consumedBy])];
|
|
652
|
+
await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, consumedBy);
|
|
653
|
+
}
|
|
654
|
+
async applyTopicStorageMutation(validation, steak, tx, txid, beef, offChainValues) {
|
|
655
|
+
const topic = validation.topic;
|
|
656
|
+
const { outputsConsumed, outputsToMarkStale } = this.classifyPreviousCoins(tx, validation);
|
|
657
|
+
await this.removeStaleOutputs(outputsToMarkStale, topic, txid);
|
|
658
|
+
steak[topic].coinsRemoved = outputsToMarkStale.map(output => output.inputIndex);
|
|
659
|
+
const newUTXOs = [];
|
|
660
|
+
await Promise.all(validation.admissibleOutputs.outputsToAdmit.map(async (outputIndex) => {
|
|
661
|
+
await this.admitOutput(outputIndex, {
|
|
662
|
+
tx,
|
|
663
|
+
txid,
|
|
664
|
+
beef,
|
|
665
|
+
topic,
|
|
666
|
+
outputsConsumed,
|
|
667
|
+
newUTXOs,
|
|
668
|
+
offChainValues
|
|
669
|
+
});
|
|
670
|
+
}));
|
|
671
|
+
this.startTime(`outputConsumed_${txid.substring(0, 10)}`);
|
|
672
|
+
const appliedRecord = await this.buildAppliedTransactionRecord(tx);
|
|
673
|
+
await this.recordTransactionData(tx, beef, appliedRecord.blockHash);
|
|
674
|
+
await Promise.all([
|
|
675
|
+
...outputsConsumed.map(async (output) => {
|
|
676
|
+
await this.updateConsumedOutput(output, newUTXOs, topic);
|
|
677
|
+
}),
|
|
678
|
+
this.storage.insertAppliedTransaction({ txid, topic, ...appliedRecord })
|
|
679
|
+
]);
|
|
680
|
+
if (appliedRecord.blockHeight !== undefined && appliedRecord.blockHash !== undefined) {
|
|
681
|
+
await this.recomputeTopicBlockAnchor(topic, appliedRecord.blockHeight, appliedRecord.blockHash);
|
|
682
|
+
}
|
|
683
|
+
this.endTime(`outputConsumed_${txid.substring(0, 10)}`);
|
|
684
|
+
}
|
|
685
|
+
async applyStorageMutations(validations, context) {
|
|
686
|
+
const { dupeTopics, failedTopics, steak, tx, txid, beef, offChainValues } = context;
|
|
615
687
|
for (const validation of validations) {
|
|
616
688
|
const topic = validation.topic;
|
|
617
|
-
if (dupeTopics.has(topic))
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
if (failedTopics.has(topic)) {
|
|
689
|
+
if (dupeTopics.has(topic) || failedTopics.has(topic))
|
|
621
690
|
continue;
|
|
622
|
-
}
|
|
623
691
|
try {
|
|
624
|
-
|
|
625
|
-
const outputsToAdmit = admissibleOutputs.outputsToAdmit;
|
|
626
|
-
const outputsConsumed = [];
|
|
627
|
-
const outputsToMarkStale = [];
|
|
628
|
-
// Use previousCoins from validation
|
|
629
|
-
const previousCoins = validation.previousCoins;
|
|
630
|
-
// For each of the previous UTXOs for this topic, if the UTXO was not included in the list of UTXOs identified for retention, then it will be marked as stale.
|
|
631
|
-
for (const inputIndex of previousCoins) {
|
|
632
|
-
const previousTXID = (_a = tx.inputs[inputIndex].sourceTXID) !== null && _a !== void 0 ? _a : (_b = tx.inputs[inputIndex].sourceTransaction) === null || _b === void 0 ? void 0 : _b.id('hex');
|
|
633
|
-
if (typeof previousTXID !== 'string')
|
|
634
|
-
continue;
|
|
635
|
-
const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex;
|
|
636
|
-
if (admissibleOutputs.coinsToRetain.includes(inputIndex)) {
|
|
637
|
-
outputsConsumed.push({
|
|
638
|
-
txid: previousTXID,
|
|
639
|
-
outputIndex: previousOutputIndex
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
else {
|
|
643
|
-
outputsToMarkStale.push({
|
|
644
|
-
txid: previousTXID,
|
|
645
|
-
previousOutputIndex,
|
|
646
|
-
inputIndex
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
// Remove stale outputs recursively
|
|
651
|
-
this.startTime(`lookForStaleOutputs_${txid.substring(0, 10)}`);
|
|
652
|
-
await Promise.all(outputsToMarkStale.map(async (coin) => {
|
|
653
|
-
const output = await this.storage.findOutput(coin.txid, coin.previousOutputIndex, topic);
|
|
654
|
-
if (output !== undefined && output !== null) {
|
|
655
|
-
await this.deleteUTXODeep(output);
|
|
656
|
-
}
|
|
657
|
-
}));
|
|
658
|
-
this.endTime(`lookForStaleOutputs_${txid.substring(0, 10)}`);
|
|
659
|
-
// Update the STEAK to indicate which coins were removed
|
|
660
|
-
steak[topic].coinsRemoved = outputsToMarkStale.map(x => x.inputIndex);
|
|
661
|
-
// Handle admittance and notification of incoming UTXOs
|
|
662
|
-
const newUTXOs = [];
|
|
663
|
-
await Promise.all(outputsToAdmit.map(async (outputIndex) => {
|
|
664
|
-
var _a;
|
|
665
|
-
if (typeof tx.outputs[outputIndex].satoshis !== 'number')
|
|
666
|
-
return;
|
|
667
|
-
this.startTime(`insertNewOutput_${txid.substring(0, 10)}`);
|
|
668
|
-
await this.storage.insertOutput({
|
|
669
|
-
txid,
|
|
670
|
-
outputIndex,
|
|
671
|
-
outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
|
|
672
|
-
satoshis: tx.outputs[outputIndex].satoshis,
|
|
673
|
-
topic,
|
|
674
|
-
spent: false,
|
|
675
|
-
beef: this.compactBEEFForStorage(tx, taggedBEEF.beef),
|
|
676
|
-
consumedBy: [],
|
|
677
|
-
outputsConsumed,
|
|
678
|
-
score: Date.now(),
|
|
679
|
-
blockHeight: (_a = (0, BASM_js_1.extractMerkleProofMetadata)(txid, tx.merklePath)) === null || _a === void 0 ? void 0 : _a.blockHeight
|
|
680
|
-
});
|
|
681
|
-
this.endTime(`insertNewOutput_${txid.substring(0, 10)}`);
|
|
682
|
-
newUTXOs.push({ txid, outputIndex });
|
|
683
|
-
this.startTime(`notifyLookupService${txid.substring(0, 10)}`);
|
|
684
|
-
await Promise.all(Object.values(this.lookupServices).map(async (l) => {
|
|
685
|
-
try {
|
|
686
|
-
if (l.admissionMode === 'locking-script') {
|
|
687
|
-
if (typeof tx.outputs[outputIndex].lockingScript !== 'object' ||
|
|
688
|
-
typeof tx.outputs[outputIndex].satoshis !== 'number') {
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
await l.outputAdmittedByTopic({
|
|
692
|
-
mode: 'locking-script',
|
|
693
|
-
txid,
|
|
694
|
-
outputIndex,
|
|
695
|
-
lockingScript: tx.outputs[outputIndex].lockingScript,
|
|
696
|
-
satoshis: tx.outputs[outputIndex].satoshis,
|
|
697
|
-
topic,
|
|
698
|
-
offChainValues
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
await l.outputAdmittedByTopic({
|
|
703
|
-
mode: 'whole-tx',
|
|
704
|
-
atomicBEEF: tx.toAtomicBEEF(),
|
|
705
|
-
outputIndex,
|
|
706
|
-
topic,
|
|
707
|
-
offChainValues
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
catch (error) {
|
|
712
|
-
this.logger.error('Error in lookup service for outputAdmittedByTopic:', error);
|
|
713
|
-
}
|
|
714
|
-
}));
|
|
715
|
-
this.endTime(`notifyLookupService${txid.substring(0, 10)}`);
|
|
716
|
-
}));
|
|
717
|
-
this.startTime(`outputConsumed_${txid.substring(0, 10)}`);
|
|
718
|
-
// Update each output consumed to know who consumed it and insert applied transaction in parallel
|
|
719
|
-
const appliedRecord = await this.buildAppliedTransactionRecord(tx);
|
|
720
|
-
await this.recordTransactionData(tx, taggedBEEF.beef, appliedRecord.blockHash);
|
|
721
|
-
await Promise.all([
|
|
722
|
-
...outputsConsumed.map(async (output) => {
|
|
723
|
-
const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic);
|
|
724
|
-
if (outputToUpdate !== undefined && outputToUpdate !== null) {
|
|
725
|
-
const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])];
|
|
726
|
-
await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy);
|
|
727
|
-
}
|
|
728
|
-
}),
|
|
729
|
-
this.storage.insertAppliedTransaction({
|
|
730
|
-
txid,
|
|
731
|
-
topic,
|
|
732
|
-
...appliedRecord
|
|
733
|
-
})
|
|
734
|
-
]);
|
|
735
|
-
if (appliedRecord.blockHeight !== undefined && appliedRecord.blockHash !== undefined) {
|
|
736
|
-
await this.recomputeTopicBlockAnchor(topic, appliedRecord.blockHeight, appliedRecord.blockHash);
|
|
737
|
-
}
|
|
738
|
-
this.endTime(`outputConsumed_${txid.substring(0, 10)}`);
|
|
692
|
+
await this.applyTopicStorageMutation(validation, steak, tx, txid, beef, offChainValues);
|
|
739
693
|
}
|
|
740
694
|
catch (error) {
|
|
741
695
|
this.logger.error('Error updating storage and notifying lookup services for topic', topic, error);
|
|
742
696
|
}
|
|
743
697
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return steak;
|
|
747
|
-
}
|
|
698
|
+
}
|
|
699
|
+
async propagateSubmission(taggedBEEF, steak, dupeTopics, tx, txid) {
|
|
748
700
|
this.startTime(`transactionPropagation_${txid.substring(0, 10)}`);
|
|
749
|
-
const relevantTopics = taggedBEEF.topics.filter(topic => { var _a; return steak[topic] !== undefined &&
|
|
701
|
+
const relevantTopics = taggedBEEF.topics.filter(topic => { var _a; return steak[topic] !== undefined &&
|
|
702
|
+
!dupeTopics.has(topic) &&
|
|
703
|
+
(steak[topic].outputsToAdmit.length !== 0 ||
|
|
704
|
+
((_a = steak[topic].coinsRemoved) === null || _a === void 0 ? void 0 : _a.length) !== 0); });
|
|
750
705
|
if (relevantTopics.length === 0) {
|
|
751
706
|
this.endTime(`transactionPropagation_${txid.substring(0, 10)}`);
|
|
752
|
-
return
|
|
707
|
+
return;
|
|
753
708
|
}
|
|
754
|
-
// Create a SHIPBroadcaster instance
|
|
755
709
|
let customBroadcasterConfig;
|
|
756
710
|
if (Array.isArray(this.slapTrackers)) {
|
|
757
|
-
// Custom SLAP trackers warrant a custom broadcaster config
|
|
758
711
|
const resolverConfig = {
|
|
759
712
|
slapTrackers: this.slapTrackers
|
|
760
713
|
};
|
|
@@ -770,6 +723,93 @@ class Engine {
|
|
|
770
723
|
this.logger.error('Error during propagation to other nodes:', error);
|
|
771
724
|
}
|
|
772
725
|
this.endTime(`transactionPropagation_${txid.substring(0, 10)}`);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Submits a transaction for processing by Overlay Services.
|
|
729
|
+
* @param {TaggedBEEF} taggedBEEF - The transaction to process
|
|
730
|
+
* @param {function(STEAK): void} [onSTEAKReady] - Optional callback function invoked when the STEAK is ready.
|
|
731
|
+
* @param {string} mode — Indicates the submission behavior, whether historical or current. Historical transactions are not broadcast or propagated.
|
|
732
|
+
* @param {number[]} offChainValues — Values necessary to evaluate topical admittance that are not stored on-chain.
|
|
733
|
+
*
|
|
734
|
+
* The optional callback function should be used to get STEAK when ready, and avoid waiting for broadcast and transaction propagation to complete.
|
|
735
|
+
*
|
|
736
|
+
* @returns {Promise<STEAK>} The submitted transaction execution acknowledgement
|
|
737
|
+
*/
|
|
738
|
+
async submit(taggedBEEF, onSteakReady, mode = 'current-tx', offChainValues) {
|
|
739
|
+
for (const t of taggedBEEF.topics) {
|
|
740
|
+
if (this.managers[t] === undefined || this.managers[t] === null) {
|
|
741
|
+
throw new Error(`This server does not support this topic: ${t}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// Validate the transaction SPV information
|
|
745
|
+
const tx = sdk_1.Transaction.fromBEEF(taggedBEEF.beef);
|
|
746
|
+
const txid = tx.id('hex');
|
|
747
|
+
this.startTime(`submit_${txid}`);
|
|
748
|
+
if (mode !== 'historical-tx-no-spv') {
|
|
749
|
+
this.startTime(`chainTracker_${txid.substring(0, 10)}`);
|
|
750
|
+
const txValid = await tx.verify(this.chainTracker);
|
|
751
|
+
if (!txValid)
|
|
752
|
+
throw new Error('Unable to verify SPV information.');
|
|
753
|
+
this.endTime(`chainTracker_${txid.substring(0, 10)}`);
|
|
754
|
+
}
|
|
755
|
+
const steak = {};
|
|
756
|
+
const dupeTopics = new Set();
|
|
757
|
+
const failedTopics = new Set();
|
|
758
|
+
// ===================================================================
|
|
759
|
+
// PHASE 1: VALIDATE (read-only, no mutations)
|
|
760
|
+
// ===================================================================
|
|
761
|
+
const topicValidations = taggedBEEF.topics.map(async (topic) => await this.validateTopicSubmission(topic, {
|
|
762
|
+
tx,
|
|
763
|
+
txid,
|
|
764
|
+
beef: taggedBEEF.beef,
|
|
765
|
+
offChainValues,
|
|
766
|
+
mode,
|
|
767
|
+
dupeTopics,
|
|
768
|
+
failedTopics
|
|
769
|
+
}));
|
|
770
|
+
const validations = await Promise.all(topicValidations);
|
|
771
|
+
// Build preliminary STEAK from validation results
|
|
772
|
+
for (const validation of validations) {
|
|
773
|
+
steak[validation.topic] = validation.admissibleOutputs;
|
|
774
|
+
}
|
|
775
|
+
// ===================================================================
|
|
776
|
+
// PHASE 2: BROADCAST (before any mutations)
|
|
777
|
+
// ===================================================================
|
|
778
|
+
// Only broadcast when at least one topic actually accepted the
|
|
779
|
+
// transaction. For a non-failed topic, acceptance means: previously
|
|
780
|
+
// accepted (dupe / client retry), outputs admitted, coins retained, or
|
|
781
|
+
// previously-admitted coins consumed (e.g. a consume-only deletion such
|
|
782
|
+
// as a KVStore remove, even one that retains nothing). A topic manager
|
|
783
|
+
// REJECTS by throwing from identifyAdmissibleOutputs (tracked in
|
|
784
|
+
// failedTopics). A transaction every topic rejected must never reach the
|
|
785
|
+
// network: submitters treat an empty STEAK as a rejection and
|
|
786
|
+
// abort/release their held inputs, so broadcasting it anyway would
|
|
787
|
+
// desync their wallets from the chain.
|
|
788
|
+
const anyTopicAccepted = validations.some(validation => this.isTopicSubmissionAccepted(validation, failedTopics));
|
|
789
|
+
await this.broadcastAcceptedSubmission(tx, txid, mode, anyTopicAccepted);
|
|
790
|
+
// Call the callback function with STEAK if it is provided (before storage mutations)
|
|
791
|
+
if (onSteakReady !== undefined) {
|
|
792
|
+
onSteakReady(steak);
|
|
793
|
+
}
|
|
794
|
+
// ===================================================================
|
|
795
|
+
// PHASE 3: MUTATE STORAGE (only after broadcast succeeded)
|
|
796
|
+
// ===================================================================
|
|
797
|
+
// Mark previous outputs as spent and notify lookup services
|
|
798
|
+
await this.markPreviousOutputsSpent(validations, failedTopics, tx, txid, offChainValues);
|
|
799
|
+
await this.applyStorageMutations(validations, {
|
|
800
|
+
dupeTopics,
|
|
801
|
+
failedTopics,
|
|
802
|
+
steak,
|
|
803
|
+
tx,
|
|
804
|
+
txid,
|
|
805
|
+
beef: taggedBEEF.beef,
|
|
806
|
+
offChainValues
|
|
807
|
+
});
|
|
808
|
+
// If we don't have an advertiser or we are dealing with historical transactions, just return the steak
|
|
809
|
+
if (this.advertiser === undefined || mode === 'historical-tx' || mode === 'historical-tx-no-spv') {
|
|
810
|
+
return steak;
|
|
811
|
+
}
|
|
812
|
+
await this.propagateSubmission(taggedBEEF, steak, dupeTopics, tx, txid);
|
|
773
813
|
// Immediately return from the function without waiting for the promises to resolve.
|
|
774
814
|
return steak;
|
|
775
815
|
}
|
|
@@ -784,6 +824,9 @@ class Engine {
|
|
|
784
824
|
if (lookupService === undefined || lookupService === null)
|
|
785
825
|
throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}`);
|
|
786
826
|
const lookupResult = await lookupService.lookup(lookupQuestion);
|
|
827
|
+
if (this.maxLookupResults !== -1 && lookupResult.length > this.maxLookupResults) {
|
|
828
|
+
throw new RangeError(`Lookup returned ${lookupResult.length} results; maximum is ${this.maxLookupResults}`);
|
|
829
|
+
}
|
|
787
830
|
const hydrationContext = this.createUTXOHistoryHydrationContext();
|
|
788
831
|
await this.preloadOutputsWithBEEF(lookupResult.map(({ txid, outputIndex }) => ({ txid, outputIndex })), hydrationContext);
|
|
789
832
|
const hydratedOutputs = (await Promise.all(lookupResult.map(async ({ txid, outputIndex, history, context }) => {
|
|
@@ -1003,72 +1046,37 @@ class Engine {
|
|
|
1003
1046
|
throw new Error('Overlay Service Engine not configured for topical synchronization!');
|
|
1004
1047
|
}
|
|
1005
1048
|
for (const topic of Object.keys(this.syncConfiguration)) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
// Check if this topic has been configured NOT to sync
|
|
1009
|
-
if (syncEndpoints === false) {
|
|
1049
|
+
const configuredEndpoints = this.syncConfiguration[topic];
|
|
1050
|
+
if (configuredEndpoints === false)
|
|
1010
1051
|
continue;
|
|
1052
|
+
if (!Array.isArray(configuredEndpoints) && configuredEndpoints !== 'SHIP')
|
|
1053
|
+
continue;
|
|
1054
|
+
const syncEndpoints = await this.resolveSyncEndpointsForTopic(topic, 'Failed to parse advertisement output:');
|
|
1055
|
+
this.logger.info(`[GASP SYNC] Will attempt to sync with ${syncEndpoints.length} peer${syncEndpoints.length === 1 ? '' : 's'}`);
|
|
1056
|
+
// Sync with each endpoint sequentially to avoid parallel locks while
|
|
1057
|
+
// keeping peer failures isolated.
|
|
1058
|
+
for (const endpoint of syncEndpoints) {
|
|
1059
|
+
await this.syncGASPWithPeer(topic, endpoint);
|
|
1011
1060
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
// Lookup will currently always return type output-list
|
|
1025
|
-
if (lookupAnswer.type === 'output-list') {
|
|
1026
|
-
const endpointSet = new Set();
|
|
1027
|
-
lookupAnswer.outputs.forEach(output => {
|
|
1028
|
-
var _a;
|
|
1029
|
-
try {
|
|
1030
|
-
// Parse out the advertisements using the provided parser
|
|
1031
|
-
const tx = sdk_1.Transaction.fromBEEF(output.beef);
|
|
1032
|
-
const advertisement = (_a = this.advertiser) === null || _a === void 0 ? void 0 : _a.parseAdvertisement(tx.outputs[output.outputIndex].lockingScript);
|
|
1033
|
-
if ((advertisement === null || advertisement === void 0 ? void 0 : advertisement.protocol) === 'SHIP') {
|
|
1034
|
-
endpointSet.add(advertisement.domain);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
catch (error) {
|
|
1038
|
-
this.logger.error('Failed to parse advertisement output:', error);
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
syncEndpoints = Array.from(endpointSet);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
// Now syncEndpoints is guaranteed to be an array of strings without duplicates
|
|
1045
|
-
if (Array.isArray(syncEndpoints)) {
|
|
1046
|
-
// Remove our own hosting URL so we don't sync with ourselves
|
|
1047
|
-
syncEndpoints = syncEndpoints.filter((endpoint) => endpoint !== this.hostingURL);
|
|
1048
|
-
this.logger.info(`[GASP SYNC] Will attempt to sync with ${syncEndpoints.length} peer${syncEndpoints.length === 1 ? '' : 's'}`);
|
|
1049
|
-
// Sync with each endpoint individually to avoid parallel locks and let failures be isolated
|
|
1050
|
-
for (const endpoint of syncEndpoints) {
|
|
1051
|
-
this.logger.info(`[GASP SYNC] Starting sync for topic "${topic}" with peer "${endpoint}"`);
|
|
1052
|
-
try {
|
|
1053
|
-
// Read the last interaction score from storage
|
|
1054
|
-
const lastInteraction = await this.storage.getLastInteraction(endpoint, topic);
|
|
1055
|
-
const gasp = new gasp_1.GASP(new OverlayGASPStorage_js_1.OverlayGASPStorage(topic, this), new OverlayGASPRemote_js_1.OverlayGASPRemote(endpoint, topic), lastInteraction, `[GASP Sync of ${topic} with ${endpoint}]`, true, true);
|
|
1056
|
-
await gasp.sync(endpoint, DEFAULT_GASP_SYNC_LIMIT);
|
|
1057
|
-
// Save the updated last interaction score
|
|
1058
|
-
if (gasp.lastInteraction > lastInteraction) {
|
|
1059
|
-
await this.storage.updateLastInteraction(endpoint, topic, gasp.lastInteraction);
|
|
1060
|
-
}
|
|
1061
|
-
this.logger.info(`[GASP SYNC] Sync successful for topic "${topic}" with peer "${endpoint}"`);
|
|
1062
|
-
}
|
|
1063
|
-
catch (err) {
|
|
1064
|
-
this.logger.error(`[GASP SYNC] Sync failed for topic "${topic}" with peer "${endpoint}"`, err);
|
|
1065
|
-
// Continue on to the next endpoint without throwing
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async syncGASPWithPeer(topic, endpoint) {
|
|
1064
|
+
this.logger.info(`[GASP SYNC] Starting sync for topic "${topic}" with peer "${endpoint}"`);
|
|
1065
|
+
try {
|
|
1066
|
+
const lastInteraction = await this.storage.getLastInteraction(endpoint, topic);
|
|
1067
|
+
const gasp = new gasp_1.GASP(new OverlayGASPStorage_js_1.OverlayGASPStorage(topic, this), new OverlayGASPRemote_js_1.OverlayGASPRemote(endpoint, topic), lastInteraction, `[GASP Sync of ${topic} with ${endpoint}]`, true, true);
|
|
1068
|
+
await gasp.sync(endpoint, DEFAULT_GASP_SYNC_LIMIT);
|
|
1069
|
+
if (gasp.lastInteraction > lastInteraction) {
|
|
1070
|
+
await this.storage.updateLastInteraction(endpoint, topic, gasp.lastInteraction);
|
|
1068
1071
|
}
|
|
1072
|
+
this.logger.info(`[GASP SYNC] Sync successful for topic "${topic}" with peer "${endpoint}"`);
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
this.logger.error(`[GASP SYNC] Sync failed for topic "${topic}" with peer "${endpoint}"`, error);
|
|
1076
|
+
// Continue on to the next endpoint without throwing.
|
|
1069
1077
|
}
|
|
1070
1078
|
}
|
|
1071
|
-
async resolveSyncEndpointsForTopic(topic) {
|
|
1079
|
+
async resolveSyncEndpointsForTopic(topic, advertisementErrorMessage = 'Failed to parse BASM advertisement output:') {
|
|
1072
1080
|
if (this.syncConfiguration === undefined) {
|
|
1073
1081
|
return [];
|
|
1074
1082
|
}
|
|
@@ -1099,7 +1107,7 @@ class Engine {
|
|
|
1099
1107
|
}
|
|
1100
1108
|
}
|
|
1101
1109
|
catch (error) {
|
|
1102
|
-
this.logger.error(
|
|
1110
|
+
this.logger.error(advertisementErrorMessage, error);
|
|
1103
1111
|
}
|
|
1104
1112
|
});
|
|
1105
1113
|
}
|
|
@@ -1259,7 +1267,8 @@ class Engine {
|
|
|
1259
1267
|
}
|
|
1260
1268
|
}
|
|
1261
1269
|
async reconcileRemoteAnchor(topic, remote, remoteAnchor, report) {
|
|
1262
|
-
var _a, _b, _c, _d
|
|
1270
|
+
var _a, _b, _c, _d;
|
|
1271
|
+
var _e;
|
|
1263
1272
|
report.checkedHeights.push(remoteAnchor.blockHeight);
|
|
1264
1273
|
const localAnchor = await ((_b = (_a = this.storage).findTopicBlockAnchor) === null || _b === void 0 ? void 0 : _b.call(_a, topic, remoteAnchor.blockHeight, remoteAnchor.blockHash));
|
|
1265
1274
|
if ((localAnchor === null || localAnchor === void 0 ? void 0 : localAnchor.tac) === remoteAnchor.tac) {
|
|
@@ -1271,7 +1280,7 @@ class Engine {
|
|
|
1271
1280
|
admittedResponse.admitted.length !== remoteAnchor.admittedCount) {
|
|
1272
1281
|
throw new Error(`Peer ${report.endpoint} supplied an admitted list inconsistent with its anchor at height ${remoteAnchor.blockHeight}`);
|
|
1273
1282
|
}
|
|
1274
|
-
const localAdmitted = (
|
|
1283
|
+
const localAdmitted = (_e = await ((_d = (_c = this.storage).findAdmittedTransactionsForBlock) === null || _d === void 0 ? void 0 : _d.call(_c, topic, remoteAnchor.blockHeight, remoteAnchor.blockHash))) !== null && _e !== void 0 ? _e : [];
|
|
1275
1284
|
const localTxids = new Set(localAdmitted.map(item => item.txid));
|
|
1276
1285
|
const missingTxids = admittedResponse.admitted
|
|
1277
1286
|
.map(item => item.txid)
|
|
@@ -1315,14 +1324,15 @@ class Engine {
|
|
|
1315
1324
|
}
|
|
1316
1325
|
}
|
|
1317
1326
|
async evictUnprovenTransactions(options = {}) {
|
|
1318
|
-
var _a, _b
|
|
1327
|
+
var _a, _b;
|
|
1328
|
+
var _c;
|
|
1319
1329
|
if (typeof this.storage.findUnprovenAppliedTransactions !== 'function') {
|
|
1320
1330
|
throw new TypeError('Storage does not support unproven transaction eviction');
|
|
1321
1331
|
}
|
|
1322
1332
|
if (this.chainTracker === 'scripts only') {
|
|
1323
1333
|
throw new Error('Unproven eviction requires a ChainTracker to determine block age');
|
|
1324
1334
|
}
|
|
1325
|
-
const thresholdBlocks = (
|
|
1335
|
+
const thresholdBlocks = (_c = options.thresholdBlocks) !== null && _c !== void 0 ? _c : this.unprovenEvictionBlocks;
|
|
1326
1336
|
const currentHeight = await this.chainTracker.currentHeight();
|
|
1327
1337
|
const cutoffHeight = currentHeight - thresholdBlocks;
|
|
1328
1338
|
const candidates = await this.storage.findUnprovenAppliedTransactions(cutoffHeight, options.topic);
|
|
@@ -1340,7 +1350,7 @@ class Engine {
|
|
|
1340
1350
|
await this.storage.deleteOutput(output.txid, output.outputIndex, candidate.topic);
|
|
1341
1351
|
evictedOutputs++;
|
|
1342
1352
|
}
|
|
1343
|
-
await ((
|
|
1353
|
+
await ((_b = (_a = this.storage).deleteAppliedTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, candidate.txid, candidate.topic));
|
|
1344
1354
|
}
|
|
1345
1355
|
return {
|
|
1346
1356
|
cutoffHeight,
|
|
@@ -1447,14 +1457,11 @@ class Engine {
|
|
|
1447
1457
|
async provideForeignSyncResponse(initialRequest, topic) {
|
|
1448
1458
|
const outputs = await this.storage.findUTXOsForTopic(topic, initialRequest.since, initialRequest.limit);
|
|
1449
1459
|
return {
|
|
1450
|
-
UTXOList: outputs.map(output => {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
score: (_a = output.score) !== null && _a !== void 0 ? _a : 0
|
|
1456
|
-
});
|
|
1457
|
-
}),
|
|
1460
|
+
UTXOList: outputs.map(output => { var _a; return ({
|
|
1461
|
+
txid: output.txid,
|
|
1462
|
+
outputIndex: output.outputIndex,
|
|
1463
|
+
score: (_a = output.score) !== null && _a !== void 0 ? _a : 0
|
|
1464
|
+
}); }),
|
|
1458
1465
|
since: initialRequest.since
|
|
1459
1466
|
};
|
|
1460
1467
|
}
|
|
@@ -1818,7 +1825,7 @@ class Engine {
|
|
|
1818
1825
|
// If none of the disallowed conditions matched, the URL is valid
|
|
1819
1826
|
return true;
|
|
1820
1827
|
}
|
|
1821
|
-
catch
|
|
1828
|
+
catch {
|
|
1822
1829
|
// URL constructor throws on malformed input — not a valid URL, return false
|
|
1823
1830
|
return false;
|
|
1824
1831
|
}
|