@bsv/overlay 2.2.0 → 2.2.7
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 -266
- 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 +46 -18
- 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 +377 -0
- package/dist/cjs/src/Engine.d.ts.map +1 -0
- package/dist/cjs/src/Engine.js +430 -436
- 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 +34 -31
- 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 +36 -12
- 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 +408 -414
- 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 +23 -19
- 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/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js +36 -12
- package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.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 +15 -0
- 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/dist/types/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts.map +1 -1
- package/package.json +87 -30
- 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 -2199
- package/src/GASP/OverlayGASPRemote.ts +0 -108
- package/src/GASP/OverlayGASPStorage.ts +0 -383
- 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 -290
- package/src/__tests/Engine.test.ts +0 -1345
- package/src/__tests/OverlayGASPRemote.test.ts +0 -135
- package/src/__tests/OverlayGASPStorage.test.ts +0 -171
- 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 -40
- 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
|
*/
|
|
@@ -36,8 +44,8 @@ class Engine {
|
|
|
36
44
|
* @param {number} unprovenEvictionBlocks - Default block age for opt-in unproven state eviction.
|
|
37
45
|
*/
|
|
38
46
|
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
|
+
var _a, _b;
|
|
48
|
+
var _c;
|
|
41
49
|
this.managers = managers;
|
|
42
50
|
this.lookupServices = lookupServices;
|
|
43
51
|
this.storage = storage;
|
|
@@ -59,12 +67,7 @@ class Engine {
|
|
|
59
67
|
this.unprovenEvictionBlocks = unprovenEvictionBlocks;
|
|
60
68
|
// 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
69
|
// 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
|
-
}
|
|
70
|
+
(_a = this.syncConfiguration) !== null && _a !== void 0 ? _a : (this.syncConfiguration = {});
|
|
68
71
|
for (const managerName of Object.keys(managers)) {
|
|
69
72
|
if (managerName === 'tm_ship' && this.shipTrackers !== undefined && this.syncConfiguration[managerName] !== false) {
|
|
70
73
|
// Combine tm_ship trackers with preexisting entries if any
|
|
@@ -84,7 +87,7 @@ class Engine {
|
|
|
84
87
|
}
|
|
85
88
|
else {
|
|
86
89
|
// Set undefined managers to 'SHIP' by default
|
|
87
|
-
(
|
|
90
|
+
(_b = (_c = this.syncConfiguration)[managerName]) !== null && _b !== void 0 ? _b : (_c[managerName] = 'SHIP');
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
}
|
|
@@ -124,7 +127,7 @@ class Engine {
|
|
|
124
127
|
return header.blockHash;
|
|
125
128
|
}
|
|
126
129
|
catch (error) {
|
|
127
|
-
this.logger.warn(`Unable to resolve BASM block hash
|
|
130
|
+
this.logger.warn(`Unable to resolve BASM block hash: height=${(0, SafeLog_js_1.serializeLogValue)(blockHeight)} error=${(0, SafeLog_js_1.serializeErrorForLog)(error)}`);
|
|
128
131
|
return undefined;
|
|
129
132
|
}
|
|
130
133
|
}
|
|
@@ -232,7 +235,7 @@ class Engine {
|
|
|
232
235
|
}
|
|
233
236
|
if (toHeight - fromHeight + 1 > DEFAULT_BASM_RANGE_LIMIT) {
|
|
234
237
|
// Bound the work per pass; the next trigger resumes from the new tip.
|
|
235
|
-
this.logger.warn(`[BASM] capping anchor chain extension
|
|
238
|
+
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
239
|
toHeight = fromHeight + DEFAULT_BASM_RANGE_LIMIT - 1;
|
|
237
240
|
}
|
|
238
241
|
const previousAnchor = fromHeight > 0
|
|
@@ -246,7 +249,7 @@ class Engine {
|
|
|
246
249
|
// canonical re-resolution from the header resolver instead of reusing it.
|
|
247
250
|
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
251
|
if (blockHash === undefined) {
|
|
249
|
-
this.logger.warn(`[BASM] unable to resolve block hash
|
|
252
|
+
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
253
|
return;
|
|
251
254
|
}
|
|
252
255
|
const basmRoot = (0, BASM_js_1.computeBasmRoot)(admitted);
|
|
@@ -375,386 +378,331 @@ class Engine {
|
|
|
375
378
|
newTipHeight: tip
|
|
376
379
|
});
|
|
377
380
|
}
|
|
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);
|
|
381
|
+
async validateTopicSubmission(topic, context) {
|
|
382
|
+
const { tx, txid, beef, offChainValues, mode, dupeTopics, failedTopics } = context;
|
|
383
|
+
try {
|
|
384
|
+
if (this.managers[topic] === undefined || this.managers[topic] === null) {
|
|
385
|
+
throw new Error(`This server does not support this topic: ${topic}`);
|
|
386
|
+
}
|
|
387
|
+
this.startTime(`dupCheck_${txid.substring(0, 10)}`);
|
|
388
|
+
const isDupe = await this.storage.doesAppliedTransactionExist({ txid, topic });
|
|
389
|
+
this.endTime(`dupCheck_${txid.substring(0, 10)}`);
|
|
390
|
+
if (isDupe) {
|
|
391
|
+
dupeTopics.add(topic);
|
|
462
392
|
return {
|
|
463
393
|
topic,
|
|
464
|
-
isDupe:
|
|
394
|
+
isDupe: true,
|
|
465
395
|
previousCoins: [],
|
|
466
396
|
previousOutputs: [],
|
|
467
397
|
admissibleOutputs: { outputsToAdmit: [], coinsToRetain: [] }
|
|
468
398
|
};
|
|
469
399
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
400
|
+
const previousCoins = [];
|
|
401
|
+
const outputPromises = tx.inputs.map(async (input, inputIndex) => {
|
|
402
|
+
var _a;
|
|
403
|
+
var _b;
|
|
404
|
+
const previousTXID = (_b = input.sourceTXID) !== null && _b !== void 0 ? _b : (_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex');
|
|
405
|
+
if (previousTXID === undefined)
|
|
406
|
+
return null;
|
|
407
|
+
const output = await this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
|
|
408
|
+
if (output !== undefined && output !== null)
|
|
409
|
+
previousCoins.push(inputIndex);
|
|
410
|
+
return output !== null && output !== void 0 ? output : null;
|
|
411
|
+
});
|
|
412
|
+
this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`);
|
|
413
|
+
const previousOutputs = await Promise.all(outputPromises);
|
|
414
|
+
this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`);
|
|
415
|
+
this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
|
|
416
|
+
const admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(beef, previousCoins, offChainValues, mode);
|
|
417
|
+
this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
|
|
418
|
+
return {
|
|
419
|
+
topic,
|
|
420
|
+
isDupe: false,
|
|
421
|
+
previousCoins,
|
|
422
|
+
previousOutputs,
|
|
423
|
+
admissibleOutputs
|
|
424
|
+
};
|
|
475
425
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
426
|
+
catch (error) {
|
|
427
|
+
this.logger.error(`Error validating topic during submit: topic=${(0, SafeLog_js_1.serializeLogValue)(topic)} error=${(0, SafeLog_js_1.serializeErrorForLog)(error)}`);
|
|
428
|
+
failedTopics.add(topic);
|
|
429
|
+
return {
|
|
430
|
+
topic,
|
|
431
|
+
isDupe: false,
|
|
432
|
+
previousCoins: [],
|
|
433
|
+
previousOutputs: [],
|
|
434
|
+
admissibleOutputs: { outputsToAdmit: [], coinsToRetain: [] }
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
isTopicSubmissionAccepted(validation, failedTopics) {
|
|
439
|
+
return !failedTopics.has(validation.topic) && (validation.isDupe ||
|
|
440
|
+
validation.admissibleOutputs.outputsToAdmit.length > 0 ||
|
|
441
|
+
validation.admissibleOutputs.coinsToRetain.length > 0 ||
|
|
442
|
+
validation.previousCoins.length > 0);
|
|
443
|
+
}
|
|
444
|
+
async broadcastAcceptedSubmission(tx, txid, mode, anyTopicAccepted) {
|
|
493
445
|
this.startTime(`broadcast_${txid.substring(0, 10)}`);
|
|
494
446
|
if (mode !== 'historical-tx' && this.broadcaster !== undefined && anyTopicAccepted) {
|
|
495
447
|
try {
|
|
496
448
|
let response;
|
|
497
449
|
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
450
|
const mp = tx.merklePath;
|
|
501
451
|
const leaf = mp.path[0].find(leaf => leaf.hash === txid);
|
|
502
|
-
|
|
452
|
+
response = {
|
|
503
453
|
status: 'success',
|
|
504
|
-
txid
|
|
505
|
-
message: `In block at height ${mp.blockHeight} index ${leaf === null || leaf === void 0 ? void 0 : leaf.offset}
|
|
454
|
+
txid,
|
|
455
|
+
message: `In block at height ${mp.blockHeight} index ${leaf === null || leaf === void 0 ? void 0 : leaf.offset}`
|
|
506
456
|
};
|
|
507
|
-
response = r;
|
|
508
457
|
}
|
|
509
458
|
else {
|
|
510
459
|
response = await this.broadcaster.broadcast(tx);
|
|
511
460
|
}
|
|
512
461
|
if ((0, sdk_1.isBroadcastFailure)(response) && this.throwOnBroadcastFailure) {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
throw
|
|
462
|
+
const error = new Error(`Failed to broadcast transaction! Error: ${response.description}`);
|
|
463
|
+
error.more = response.more;
|
|
464
|
+
throw error;
|
|
516
465
|
}
|
|
517
466
|
}
|
|
518
467
|
catch (error) {
|
|
519
|
-
if (this.throwOnBroadcastFailure)
|
|
468
|
+
if (this.throwOnBroadcastFailure)
|
|
520
469
|
throw error;
|
|
521
|
-
}
|
|
522
470
|
this.logger.error('Error broadcasting transaction:', error);
|
|
523
471
|
}
|
|
524
472
|
}
|
|
525
473
|
this.endTime(`broadcast_${txid.substring(0, 10)}`);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
474
|
+
}
|
|
475
|
+
async notifyOutputSpent(lookupService, tx, txid, output, topic, offChainValues) {
|
|
476
|
+
var _a;
|
|
477
|
+
if (typeof lookupService.outputSpent !== 'function')
|
|
478
|
+
return;
|
|
479
|
+
if (lookupService.spendNotificationMode === 'txid') {
|
|
480
|
+
await lookupService.outputSpent({
|
|
481
|
+
mode: 'txid',
|
|
482
|
+
spendingTxid: txid,
|
|
483
|
+
txid: output.txid,
|
|
484
|
+
outputIndex: output.outputIndex,
|
|
485
|
+
topic
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
529
488
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
489
|
+
if (lookupService.spendNotificationMode === 'script') {
|
|
490
|
+
const inputIndex = findSpendingInputIndex(tx, output);
|
|
491
|
+
if (inputIndex === -1)
|
|
492
|
+
throw new Error('Could not find input index');
|
|
493
|
+
await lookupService.outputSpent({
|
|
494
|
+
mode: 'script',
|
|
495
|
+
spendingTxid: txid,
|
|
496
|
+
inputIndex,
|
|
497
|
+
sequenceNumber: (_a = tx.inputs[inputIndex].sequence) !== null && _a !== void 0 ? _a : 0xffffffff,
|
|
498
|
+
unlockingScript: tx.inputs[inputIndex].unlockingScript,
|
|
499
|
+
txid: output.txid,
|
|
500
|
+
outputIndex: output.outputIndex,
|
|
501
|
+
topic,
|
|
502
|
+
offChainValues
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (lookupService.spendNotificationMode === 'whole-tx') {
|
|
507
|
+
await lookupService.outputSpent({
|
|
508
|
+
mode: 'whole-tx',
|
|
509
|
+
spendingAtomicBEEF: tx.toAtomicBEEF(),
|
|
510
|
+
txid: output.txid,
|
|
511
|
+
outputIndex: output.outputIndex,
|
|
512
|
+
topic,
|
|
513
|
+
offChainValues
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
await lookupService.outputSpent({
|
|
518
|
+
mode: 'none',
|
|
519
|
+
txid: output.txid,
|
|
520
|
+
outputIndex: output.outputIndex,
|
|
521
|
+
topic
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
async markPreviousOutputSpent(output, topic, tx, txid, offChainValues) {
|
|
525
|
+
if (output === null)
|
|
526
|
+
return;
|
|
527
|
+
try {
|
|
528
|
+
await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic);
|
|
529
|
+
await Promise.all(Object.values(this.lookupServices).map(async (lookupService) => {
|
|
530
|
+
try {
|
|
531
|
+
await this.notifyOutputSpent(lookupService, tx, txid, output, topic, offChainValues);
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
this.logger.error('Error in lookup service for outputSpent:', error);
|
|
535
|
+
}
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
this.logger.error('Error marking UTXO as spent:', error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async markPreviousOutputsSpent(validations, failedTopics, tx, txid, offChainValues) {
|
|
534
543
|
await Promise.all(validations.map(async (validation) => {
|
|
535
|
-
if (validation.isDupe || failedTopics.has(validation.topic))
|
|
544
|
+
if (validation.isDupe || failedTopics.has(validation.topic))
|
|
536
545
|
return;
|
|
546
|
+
await Promise.all(validation.previousOutputs.map(async (output) => {
|
|
547
|
+
await this.markPreviousOutputSpent(output, validation.topic, tx, txid, offChainValues);
|
|
548
|
+
}));
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
classifyPreviousCoins(tx, validation) {
|
|
552
|
+
var _a;
|
|
553
|
+
var _b;
|
|
554
|
+
const outputsConsumed = [];
|
|
555
|
+
const outputsToMarkStale = [];
|
|
556
|
+
for (const inputIndex of validation.previousCoins) {
|
|
557
|
+
const input = tx.inputs[inputIndex];
|
|
558
|
+
const previousTXID = (_b = input.sourceTXID) !== null && _b !== void 0 ? _b : (_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex');
|
|
559
|
+
if (typeof previousTXID !== 'string')
|
|
560
|
+
continue;
|
|
561
|
+
if (validation.admissibleOutputs.coinsToRetain.includes(inputIndex)) {
|
|
562
|
+
outputsConsumed.push({
|
|
563
|
+
txid: previousTXID,
|
|
564
|
+
outputIndex: input.sourceOutputIndex
|
|
565
|
+
});
|
|
537
566
|
}
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
567
|
+
else {
|
|
568
|
+
outputsToMarkStale.push({
|
|
569
|
+
txid: previousTXID,
|
|
570
|
+
previousOutputIndex: input.sourceOutputIndex,
|
|
571
|
+
inputIndex
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return { outputsConsumed, outputsToMarkStale };
|
|
576
|
+
}
|
|
577
|
+
async removeStaleOutputs(outputs, topic, txid) {
|
|
578
|
+
this.startTime(`lookForStaleOutputs_${txid.substring(0, 10)}`);
|
|
579
|
+
await Promise.all(outputs.map(async (coin) => {
|
|
580
|
+
const output = await this.storage.findOutput(coin.txid, coin.previousOutputIndex, topic);
|
|
581
|
+
if (output !== undefined && output !== null)
|
|
582
|
+
await this.deleteUTXODeep(output);
|
|
583
|
+
}));
|
|
584
|
+
this.endTime(`lookForStaleOutputs_${txid.substring(0, 10)}`);
|
|
585
|
+
}
|
|
586
|
+
async notifyOutputAdmitted(lookupService, tx, txid, outputIndex, topic, offChainValues) {
|
|
587
|
+
if (lookupService.admissionMode === 'locking-script') {
|
|
588
|
+
if (typeof tx.outputs[outputIndex].lockingScript !== 'object' ||
|
|
589
|
+
typeof tx.outputs[outputIndex].satoshis !== 'number')
|
|
590
|
+
return;
|
|
591
|
+
await lookupService.outputAdmittedByTopic({
|
|
592
|
+
mode: 'locking-script',
|
|
593
|
+
txid,
|
|
594
|
+
outputIndex,
|
|
595
|
+
lockingScript: tx.outputs[outputIndex].lockingScript,
|
|
596
|
+
satoshis: tx.outputs[outputIndex].satoshis,
|
|
597
|
+
topic,
|
|
598
|
+
offChainValues
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
await lookupService.outputAdmittedByTopic({
|
|
603
|
+
mode: 'whole-tx',
|
|
604
|
+
atomicBEEF: tx.toAtomicBEEF(),
|
|
605
|
+
outputIndex,
|
|
606
|
+
topic,
|
|
607
|
+
offChainValues
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
async admitOutput(outputIndex, context) {
|
|
611
|
+
var _a;
|
|
612
|
+
const { tx, txid, beef, topic, outputsConsumed, newUTXOs, offChainValues } = context;
|
|
613
|
+
if (typeof tx.outputs[outputIndex].satoshis !== 'number')
|
|
614
|
+
return;
|
|
615
|
+
this.startTime(`insertNewOutput_${txid.substring(0, 10)}`);
|
|
616
|
+
await this.storage.insertOutput({
|
|
617
|
+
txid,
|
|
618
|
+
outputIndex,
|
|
619
|
+
outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
|
|
620
|
+
satoshis: tx.outputs[outputIndex].satoshis,
|
|
621
|
+
topic,
|
|
622
|
+
spent: false,
|
|
623
|
+
beef: this.compactBEEFForStorage(tx, beef),
|
|
624
|
+
consumedBy: [],
|
|
625
|
+
outputsConsumed,
|
|
626
|
+
score: Date.now(),
|
|
627
|
+
blockHeight: (_a = (0, BASM_js_1.extractMerkleProofMetadata)(txid, tx.merklePath)) === null || _a === void 0 ? void 0 : _a.blockHeight
|
|
628
|
+
});
|
|
629
|
+
this.endTime(`insertNewOutput_${txid.substring(0, 10)}`);
|
|
630
|
+
newUTXOs.push({ txid, outputIndex });
|
|
631
|
+
this.startTime(`notifyLookupService${txid.substring(0, 10)}`);
|
|
632
|
+
await Promise.all(Object.values(this.lookupServices).map(async (lookupService) => {
|
|
633
|
+
try {
|
|
634
|
+
await this.notifyOutputAdmitted(lookupService, tx, txid, outputIndex, topic, offChainValues);
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
this.logger.error('Error in lookup service for outputAdmittedByTopic:', error);
|
|
638
|
+
}
|
|
639
|
+
}));
|
|
640
|
+
this.endTime(`notifyLookupService${txid.substring(0, 10)}`);
|
|
641
|
+
}
|
|
642
|
+
async updateConsumedOutput(output, newUTXOs, topic) {
|
|
643
|
+
const storedOutput = await this.storage.findOutput(output.txid, output.outputIndex, topic);
|
|
644
|
+
if (storedOutput === undefined || storedOutput === null)
|
|
645
|
+
return;
|
|
646
|
+
const consumedBy = [...new Set([...newUTXOs, ...storedOutput.consumedBy])];
|
|
647
|
+
await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, consumedBy);
|
|
648
|
+
}
|
|
649
|
+
async applyTopicStorageMutation(validation, steak, tx, txid, beef, offChainValues) {
|
|
650
|
+
const topic = validation.topic;
|
|
651
|
+
const { outputsConsumed, outputsToMarkStale } = this.classifyPreviousCoins(tx, validation);
|
|
652
|
+
await this.removeStaleOutputs(outputsToMarkStale, topic, txid);
|
|
653
|
+
steak[topic].coinsRemoved = outputsToMarkStale.map(output => output.inputIndex);
|
|
654
|
+
const newUTXOs = [];
|
|
655
|
+
await Promise.all(validation.admissibleOutputs.outputsToAdmit.map(async (outputIndex) => {
|
|
656
|
+
await this.admitOutput(outputIndex, {
|
|
657
|
+
tx,
|
|
658
|
+
txid,
|
|
659
|
+
beef,
|
|
660
|
+
topic,
|
|
661
|
+
outputsConsumed,
|
|
662
|
+
newUTXOs,
|
|
663
|
+
offChainValues
|
|
611
664
|
});
|
|
612
|
-
await Promise.all(markSpentPromises);
|
|
613
665
|
}));
|
|
614
|
-
|
|
666
|
+
this.startTime(`outputConsumed_${txid.substring(0, 10)}`);
|
|
667
|
+
const appliedRecord = await this.buildAppliedTransactionRecord(tx);
|
|
668
|
+
await this.recordTransactionData(tx, beef, appliedRecord.blockHash);
|
|
669
|
+
await Promise.all([
|
|
670
|
+
...outputsConsumed.map(async (output) => {
|
|
671
|
+
await this.updateConsumedOutput(output, newUTXOs, topic);
|
|
672
|
+
}),
|
|
673
|
+
this.storage.insertAppliedTransaction({ txid, topic, ...appliedRecord })
|
|
674
|
+
]);
|
|
675
|
+
if (appliedRecord.blockHeight !== undefined && appliedRecord.blockHash !== undefined) {
|
|
676
|
+
await this.recomputeTopicBlockAnchor(topic, appliedRecord.blockHeight, appliedRecord.blockHash);
|
|
677
|
+
}
|
|
678
|
+
this.endTime(`outputConsumed_${txid.substring(0, 10)}`);
|
|
679
|
+
}
|
|
680
|
+
async applyStorageMutations(validations, context) {
|
|
681
|
+
const { dupeTopics, failedTopics, steak, tx, txid, beef, offChainValues } = context;
|
|
615
682
|
for (const validation of validations) {
|
|
616
683
|
const topic = validation.topic;
|
|
617
|
-
if (dupeTopics.has(topic))
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
if (failedTopics.has(topic)) {
|
|
684
|
+
if (dupeTopics.has(topic) || failedTopics.has(topic))
|
|
621
685
|
continue;
|
|
622
|
-
}
|
|
623
686
|
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)}`);
|
|
687
|
+
await this.applyTopicStorageMutation(validation, steak, tx, txid, beef, offChainValues);
|
|
739
688
|
}
|
|
740
689
|
catch (error) {
|
|
741
690
|
this.logger.error('Error updating storage and notifying lookup services for topic', topic, error);
|
|
742
691
|
}
|
|
743
692
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return steak;
|
|
747
|
-
}
|
|
693
|
+
}
|
|
694
|
+
async propagateSubmission(taggedBEEF, steak, dupeTopics, tx, txid) {
|
|
748
695
|
this.startTime(`transactionPropagation_${txid.substring(0, 10)}`);
|
|
749
|
-
const relevantTopics = taggedBEEF.topics.filter(topic => { var _a; return steak[topic] !== undefined &&
|
|
696
|
+
const relevantTopics = taggedBEEF.topics.filter(topic => { var _a; return steak[topic] !== undefined &&
|
|
697
|
+
!dupeTopics.has(topic) &&
|
|
698
|
+
(steak[topic].outputsToAdmit.length !== 0 ||
|
|
699
|
+
((_a = steak[topic].coinsRemoved) === null || _a === void 0 ? void 0 : _a.length) !== 0); });
|
|
750
700
|
if (relevantTopics.length === 0) {
|
|
751
701
|
this.endTime(`transactionPropagation_${txid.substring(0, 10)}`);
|
|
752
|
-
return
|
|
702
|
+
return;
|
|
753
703
|
}
|
|
754
|
-
// Create a SHIPBroadcaster instance
|
|
755
704
|
let customBroadcasterConfig;
|
|
756
705
|
if (Array.isArray(this.slapTrackers)) {
|
|
757
|
-
// Custom SLAP trackers warrant a custom broadcaster config
|
|
758
706
|
const resolverConfig = {
|
|
759
707
|
slapTrackers: this.slapTrackers
|
|
760
708
|
};
|
|
@@ -770,6 +718,93 @@ class Engine {
|
|
|
770
718
|
this.logger.error('Error during propagation to other nodes:', error);
|
|
771
719
|
}
|
|
772
720
|
this.endTime(`transactionPropagation_${txid.substring(0, 10)}`);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Submits a transaction for processing by Overlay Services.
|
|
724
|
+
* @param {TaggedBEEF} taggedBEEF - The transaction to process
|
|
725
|
+
* @param {function(STEAK): void} [onSTEAKReady] - Optional callback function invoked when the STEAK is ready.
|
|
726
|
+
* @param {string} mode — Indicates the submission behavior, whether historical or current. Historical transactions are not broadcast or propagated.
|
|
727
|
+
* @param {number[]} offChainValues — Values necessary to evaluate topical admittance that are not stored on-chain.
|
|
728
|
+
*
|
|
729
|
+
* The optional callback function should be used to get STEAK when ready, and avoid waiting for broadcast and transaction propagation to complete.
|
|
730
|
+
*
|
|
731
|
+
* @returns {Promise<STEAK>} The submitted transaction execution acknowledgement
|
|
732
|
+
*/
|
|
733
|
+
async submit(taggedBEEF, onSteakReady, mode = 'current-tx', offChainValues) {
|
|
734
|
+
for (const t of taggedBEEF.topics) {
|
|
735
|
+
if (this.managers[t] === undefined || this.managers[t] === null) {
|
|
736
|
+
throw new Error(`This server does not support this topic: ${t}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Validate the transaction SPV information
|
|
740
|
+
const tx = sdk_1.Transaction.fromBEEF(taggedBEEF.beef);
|
|
741
|
+
const txid = tx.id('hex');
|
|
742
|
+
this.startTime(`submit_${txid}`);
|
|
743
|
+
if (mode !== 'historical-tx-no-spv') {
|
|
744
|
+
this.startTime(`chainTracker_${txid.substring(0, 10)}`);
|
|
745
|
+
const txValid = await tx.verify(this.chainTracker);
|
|
746
|
+
if (!txValid)
|
|
747
|
+
throw new Error('Unable to verify SPV information.');
|
|
748
|
+
this.endTime(`chainTracker_${txid.substring(0, 10)}`);
|
|
749
|
+
}
|
|
750
|
+
const steak = {};
|
|
751
|
+
const dupeTopics = new Set();
|
|
752
|
+
const failedTopics = new Set();
|
|
753
|
+
// ===================================================================
|
|
754
|
+
// PHASE 1: VALIDATE (read-only, no mutations)
|
|
755
|
+
// ===================================================================
|
|
756
|
+
const topicValidations = taggedBEEF.topics.map(async (topic) => await this.validateTopicSubmission(topic, {
|
|
757
|
+
tx,
|
|
758
|
+
txid,
|
|
759
|
+
beef: taggedBEEF.beef,
|
|
760
|
+
offChainValues,
|
|
761
|
+
mode,
|
|
762
|
+
dupeTopics,
|
|
763
|
+
failedTopics
|
|
764
|
+
}));
|
|
765
|
+
const validations = await Promise.all(topicValidations);
|
|
766
|
+
// Build preliminary STEAK from validation results
|
|
767
|
+
for (const validation of validations) {
|
|
768
|
+
steak[validation.topic] = validation.admissibleOutputs;
|
|
769
|
+
}
|
|
770
|
+
// ===================================================================
|
|
771
|
+
// PHASE 2: BROADCAST (before any mutations)
|
|
772
|
+
// ===================================================================
|
|
773
|
+
// Only broadcast when at least one topic actually accepted the
|
|
774
|
+
// transaction. For a non-failed topic, acceptance means: previously
|
|
775
|
+
// accepted (dupe / client retry), outputs admitted, coins retained, or
|
|
776
|
+
// previously-admitted coins consumed (e.g. a consume-only deletion such
|
|
777
|
+
// as a KVStore remove, even one that retains nothing). A topic manager
|
|
778
|
+
// REJECTS by throwing from identifyAdmissibleOutputs (tracked in
|
|
779
|
+
// failedTopics). A transaction every topic rejected must never reach the
|
|
780
|
+
// network: submitters treat an empty STEAK as a rejection and
|
|
781
|
+
// abort/release their held inputs, so broadcasting it anyway would
|
|
782
|
+
// desync their wallets from the chain.
|
|
783
|
+
const anyTopicAccepted = validations.some(validation => this.isTopicSubmissionAccepted(validation, failedTopics));
|
|
784
|
+
await this.broadcastAcceptedSubmission(tx, txid, mode, anyTopicAccepted);
|
|
785
|
+
// Call the callback function with STEAK if it is provided (before storage mutations)
|
|
786
|
+
if (onSteakReady !== undefined) {
|
|
787
|
+
onSteakReady(steak);
|
|
788
|
+
}
|
|
789
|
+
// ===================================================================
|
|
790
|
+
// PHASE 3: MUTATE STORAGE (only after broadcast succeeded)
|
|
791
|
+
// ===================================================================
|
|
792
|
+
// Mark previous outputs as spent and notify lookup services
|
|
793
|
+
await this.markPreviousOutputsSpent(validations, failedTopics, tx, txid, offChainValues);
|
|
794
|
+
await this.applyStorageMutations(validations, {
|
|
795
|
+
dupeTopics,
|
|
796
|
+
failedTopics,
|
|
797
|
+
steak,
|
|
798
|
+
tx,
|
|
799
|
+
txid,
|
|
800
|
+
beef: taggedBEEF.beef,
|
|
801
|
+
offChainValues
|
|
802
|
+
});
|
|
803
|
+
// If we don't have an advertiser or we are dealing with historical transactions, just return the steak
|
|
804
|
+
if (this.advertiser === undefined || mode === 'historical-tx' || mode === 'historical-tx-no-spv') {
|
|
805
|
+
return steak;
|
|
806
|
+
}
|
|
807
|
+
await this.propagateSubmission(taggedBEEF, steak, dupeTopics, tx, txid);
|
|
773
808
|
// Immediately return from the function without waiting for the promises to resolve.
|
|
774
809
|
return steak;
|
|
775
810
|
}
|
|
@@ -782,7 +817,7 @@ class Engine {
|
|
|
782
817
|
// Validate a lookup service for the provider is found
|
|
783
818
|
const lookupService = this.lookupServices[lookupQuestion.service];
|
|
784
819
|
if (lookupService === undefined || lookupService === null)
|
|
785
|
-
throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}
|
|
820
|
+
throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}`);
|
|
786
821
|
const lookupResult = await lookupService.lookup(lookupQuestion);
|
|
787
822
|
const hydrationContext = this.createUTXOHistoryHydrationContext();
|
|
788
823
|
await this.preloadOutputsWithBEEF(lookupResult.map(({ txid, outputIndex }) => ({ txid, outputIndex })), hydrationContext);
|
|
@@ -1003,72 +1038,37 @@ class Engine {
|
|
|
1003
1038
|
throw new Error('Overlay Service Engine not configured for topical synchronization!');
|
|
1004
1039
|
}
|
|
1005
1040
|
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) {
|
|
1041
|
+
const configuredEndpoints = this.syncConfiguration[topic];
|
|
1042
|
+
if (configuredEndpoints === false)
|
|
1010
1043
|
continue;
|
|
1044
|
+
if (!Array.isArray(configuredEndpoints) && configuredEndpoints !== 'SHIP')
|
|
1045
|
+
continue;
|
|
1046
|
+
const syncEndpoints = await this.resolveSyncEndpointsForTopic(topic, 'Failed to parse advertisement output:');
|
|
1047
|
+
this.logger.info(`[GASP SYNC] Will attempt to sync with ${syncEndpoints.length} peer${syncEndpoints.length === 1 ? '' : 's'}`);
|
|
1048
|
+
// Sync with each endpoint sequentially to avoid parallel locks while
|
|
1049
|
+
// keeping peer failures isolated.
|
|
1050
|
+
for (const endpoint of syncEndpoints) {
|
|
1051
|
+
await this.syncGASPWithPeer(topic, endpoint);
|
|
1011
1052
|
}
|
|
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
|
-
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
async syncGASPWithPeer(topic, endpoint) {
|
|
1056
|
+
this.logger.info(`[GASP SYNC] Starting sync for topic "${topic}" with peer "${endpoint}"`);
|
|
1057
|
+
try {
|
|
1058
|
+
const lastInteraction = await this.storage.getLastInteraction(endpoint, topic);
|
|
1059
|
+
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);
|
|
1060
|
+
await gasp.sync(endpoint, DEFAULT_GASP_SYNC_LIMIT);
|
|
1061
|
+
if (gasp.lastInteraction > lastInteraction) {
|
|
1062
|
+
await this.storage.updateLastInteraction(endpoint, topic, gasp.lastInteraction);
|
|
1068
1063
|
}
|
|
1064
|
+
this.logger.info(`[GASP SYNC] Sync successful for topic "${topic}" with peer "${endpoint}"`);
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
this.logger.error(`[GASP SYNC] Sync failed for topic "${topic}" with peer "${endpoint}"`, error);
|
|
1068
|
+
// Continue on to the next endpoint without throwing.
|
|
1069
1069
|
}
|
|
1070
1070
|
}
|
|
1071
|
-
async resolveSyncEndpointsForTopic(topic) {
|
|
1071
|
+
async resolveSyncEndpointsForTopic(topic, advertisementErrorMessage = 'Failed to parse BASM advertisement output:') {
|
|
1072
1072
|
if (this.syncConfiguration === undefined) {
|
|
1073
1073
|
return [];
|
|
1074
1074
|
}
|
|
@@ -1099,7 +1099,7 @@ class Engine {
|
|
|
1099
1099
|
}
|
|
1100
1100
|
}
|
|
1101
1101
|
catch (error) {
|
|
1102
|
-
this.logger.error(
|
|
1102
|
+
this.logger.error(advertisementErrorMessage, error);
|
|
1103
1103
|
}
|
|
1104
1104
|
});
|
|
1105
1105
|
}
|
|
@@ -1259,7 +1259,8 @@ class Engine {
|
|
|
1259
1259
|
}
|
|
1260
1260
|
}
|
|
1261
1261
|
async reconcileRemoteAnchor(topic, remote, remoteAnchor, report) {
|
|
1262
|
-
var _a, _b, _c, _d
|
|
1262
|
+
var _a, _b, _c, _d;
|
|
1263
|
+
var _e;
|
|
1263
1264
|
report.checkedHeights.push(remoteAnchor.blockHeight);
|
|
1264
1265
|
const localAnchor = await ((_b = (_a = this.storage).findTopicBlockAnchor) === null || _b === void 0 ? void 0 : _b.call(_a, topic, remoteAnchor.blockHeight, remoteAnchor.blockHash));
|
|
1265
1266
|
if ((localAnchor === null || localAnchor === void 0 ? void 0 : localAnchor.tac) === remoteAnchor.tac) {
|
|
@@ -1271,7 +1272,7 @@ class Engine {
|
|
|
1271
1272
|
admittedResponse.admitted.length !== remoteAnchor.admittedCount) {
|
|
1272
1273
|
throw new Error(`Peer ${report.endpoint} supplied an admitted list inconsistent with its anchor at height ${remoteAnchor.blockHeight}`);
|
|
1273
1274
|
}
|
|
1274
|
-
const localAdmitted = (
|
|
1275
|
+
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
1276
|
const localTxids = new Set(localAdmitted.map(item => item.txid));
|
|
1276
1277
|
const missingTxids = admittedResponse.admitted
|
|
1277
1278
|
.map(item => item.txid)
|
|
@@ -1315,14 +1316,15 @@ class Engine {
|
|
|
1315
1316
|
}
|
|
1316
1317
|
}
|
|
1317
1318
|
async evictUnprovenTransactions(options = {}) {
|
|
1318
|
-
var _a, _b
|
|
1319
|
+
var _a, _b;
|
|
1320
|
+
var _c;
|
|
1319
1321
|
if (typeof this.storage.findUnprovenAppliedTransactions !== 'function') {
|
|
1320
1322
|
throw new TypeError('Storage does not support unproven transaction eviction');
|
|
1321
1323
|
}
|
|
1322
1324
|
if (this.chainTracker === 'scripts only') {
|
|
1323
1325
|
throw new Error('Unproven eviction requires a ChainTracker to determine block age');
|
|
1324
1326
|
}
|
|
1325
|
-
const thresholdBlocks = (
|
|
1327
|
+
const thresholdBlocks = (_c = options.thresholdBlocks) !== null && _c !== void 0 ? _c : this.unprovenEvictionBlocks;
|
|
1326
1328
|
const currentHeight = await this.chainTracker.currentHeight();
|
|
1327
1329
|
const cutoffHeight = currentHeight - thresholdBlocks;
|
|
1328
1330
|
const candidates = await this.storage.findUnprovenAppliedTransactions(cutoffHeight, options.topic);
|
|
@@ -1340,7 +1342,7 @@ class Engine {
|
|
|
1340
1342
|
await this.storage.deleteOutput(output.txid, output.outputIndex, candidate.topic);
|
|
1341
1343
|
evictedOutputs++;
|
|
1342
1344
|
}
|
|
1343
|
-
await ((
|
|
1345
|
+
await ((_b = (_a = this.storage).deleteAppliedTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, candidate.txid, candidate.topic));
|
|
1344
1346
|
}
|
|
1345
1347
|
return {
|
|
1346
1348
|
cutoffHeight,
|
|
@@ -1447,14 +1449,11 @@ class Engine {
|
|
|
1447
1449
|
async provideForeignSyncResponse(initialRequest, topic) {
|
|
1448
1450
|
const outputs = await this.storage.findUTXOsForTopic(topic, initialRequest.since, initialRequest.limit);
|
|
1449
1451
|
return {
|
|
1450
|
-
UTXOList: outputs.map(output => {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
score: (_a = output.score) !== null && _a !== void 0 ? _a : 0
|
|
1456
|
-
});
|
|
1457
|
-
}),
|
|
1452
|
+
UTXOList: outputs.map(output => { var _a; return ({
|
|
1453
|
+
txid: output.txid,
|
|
1454
|
+
outputIndex: output.outputIndex,
|
|
1455
|
+
score: (_a = output.score) !== null && _a !== void 0 ? _a : 0
|
|
1456
|
+
}); }),
|
|
1458
1457
|
since: initialRequest.since
|
|
1459
1458
|
};
|
|
1460
1459
|
}
|
|
@@ -1627,22 +1626,21 @@ class Engine {
|
|
|
1627
1626
|
* @param proof for txid
|
|
1628
1627
|
*/
|
|
1629
1628
|
updateInputProofs(tx, txid, proof) {
|
|
1630
|
-
if (tx.
|
|
1629
|
+
if (tx.id('hex') === txid) {
|
|
1631
1630
|
// Update the merkle path to handle potential reorgs
|
|
1632
1631
|
tx.merklePath = proof;
|
|
1633
1632
|
return;
|
|
1634
1633
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1634
|
+
// A mined transaction's source graph is no longer part of its BEEF. Do not
|
|
1635
|
+
// replace an unrelated transaction's proof with the proof for an ancestor.
|
|
1636
|
+
if (tx.merklePath !== undefined)
|
|
1637
|
+
return;
|
|
1638
|
+
for (const input of tx.inputs) {
|
|
1639
|
+
// All inputs must have sourceTransactions
|
|
1640
|
+
const stx = input.sourceTransaction;
|
|
1641
|
+
if (typeof stx !== 'object')
|
|
1642
|
+
continue;
|
|
1643
|
+
this.updateInputProofs(stx, txid, proof);
|
|
1646
1644
|
}
|
|
1647
1645
|
}
|
|
1648
1646
|
/**
|
|
@@ -1657,12 +1655,8 @@ class Engine {
|
|
|
1657
1655
|
throw new Error('Output must have associated transaction BEEF!');
|
|
1658
1656
|
}
|
|
1659
1657
|
const tx = sdk_1.Transaction.fromBEEF(output.beef);
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
tx.merklePath = proof;
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
// recursively update all sourceTransactions proven by (txid,proof)
|
|
1658
|
+
// Update this transaction, or recursively update the matching source
|
|
1659
|
+
// transaction. This also persists replacement proofs after a reorg.
|
|
1666
1660
|
this.updateInputProofs(tx, txid, proof);
|
|
1667
1661
|
// Update the output's BEEF in the storage DB
|
|
1668
1662
|
await this.storage.updateTransactionBEEF(output.txid, tx.toBEEF());
|
|
@@ -1823,7 +1817,7 @@ class Engine {
|
|
|
1823
1817
|
// If none of the disallowed conditions matched, the URL is valid
|
|
1824
1818
|
return true;
|
|
1825
1819
|
}
|
|
1826
|
-
catch
|
|
1820
|
+
catch {
|
|
1827
1821
|
// URL constructor throws on malformed input — not a valid URL, return false
|
|
1828
1822
|
return false;
|
|
1829
1823
|
}
|