@bsv/wallet-toolbox-client 2.8.0 → 2.9.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/out/index.client.cjs +259 -74
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +171 -5
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +171 -5
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +258 -75
- package/out/index.client.mjs.map +1 -1
- package/package.json +2 -2
package/out/index.client.cjs
CHANGED
|
@@ -19597,6 +19597,24 @@ function isLiveBlockHeader(header) {
|
|
|
19597
19597
|
return "chainWork" in header && typeof header.previousHash === "string";
|
|
19598
19598
|
}
|
|
19599
19599
|
//#endregion
|
|
19600
|
+
//#region ../src/services/chaintracker/chaintracks/Api/BulkFileDataValidatorApi.ts
|
|
19601
|
+
/**
|
|
19602
|
+
* Identifies deterministic rejection of the supplied immutable bytes.
|
|
19603
|
+
* Operational failures such as worker crashes and queue saturation deliberately
|
|
19604
|
+
* use ordinary errors so callers preserve the cache entry and avoid downloading
|
|
19605
|
+
* a replacement that cannot be validated.
|
|
19606
|
+
*
|
|
19607
|
+
* @public
|
|
19608
|
+
*/
|
|
19609
|
+
var BulkFileDataValidationError = class extends Error {
|
|
19610
|
+
data;
|
|
19611
|
+
constructor(message, data) {
|
|
19612
|
+
super(message);
|
|
19613
|
+
this.data = data;
|
|
19614
|
+
this.name = "BulkFileDataValidationError";
|
|
19615
|
+
}
|
|
19616
|
+
};
|
|
19617
|
+
//#endregion
|
|
19600
19618
|
//#region ../src/services/chaintracker/chaintracks/util/HeightRange.ts
|
|
19601
19619
|
/**
|
|
19602
19620
|
* Represents a range of block heights.
|
|
@@ -19815,6 +19833,8 @@ var Chaintracks = class {
|
|
|
19815
19833
|
lastPresentHeight = -1;
|
|
19816
19834
|
lastPresentHeightMsecs = 0;
|
|
19817
19835
|
lastPresentHeightMaxAge = 60 * 1e3;
|
|
19836
|
+
presentHeightRefresh;
|
|
19837
|
+
mainLoopHeartbeatMsecs = 0;
|
|
19818
19838
|
lock = new SingleWriterMultiReaderLock();
|
|
19819
19839
|
sourceStatus = /* @__PURE__ */ new Map();
|
|
19820
19840
|
constructor(options) {
|
|
@@ -19852,12 +19872,31 @@ var Chaintracks = class {
|
|
|
19852
19872
|
return this.chain;
|
|
19853
19873
|
}
|
|
19854
19874
|
/**
|
|
19855
|
-
*
|
|
19856
|
-
*
|
|
19875
|
+
* Returns the last known valid height immediately and refreshes stale state
|
|
19876
|
+
* once in the background. Cold start waits for the single shared refresh.
|
|
19857
19877
|
*/
|
|
19858
19878
|
async getPresentHeight() {
|
|
19859
19879
|
const now = Date.now();
|
|
19860
19880
|
if (this.lastPresentHeight >= 0 && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
|
|
19881
|
+
if (this.lastPresentHeight >= 0) {
|
|
19882
|
+
this.refreshPresentHeight().catch((error) => {
|
|
19883
|
+
this.log(`Background present-height refresh failed: ${WalletError.fromUnknown(error).message}`);
|
|
19884
|
+
});
|
|
19885
|
+
return this.lastPresentHeight;
|
|
19886
|
+
}
|
|
19887
|
+
return await this.refreshPresentHeight();
|
|
19888
|
+
}
|
|
19889
|
+
async refreshPresentHeight() {
|
|
19890
|
+
if (this.presentHeightRefresh != null) return await this.presentHeightRefresh;
|
|
19891
|
+
const refresh = this.loadPresentHeight();
|
|
19892
|
+
this.presentHeightRefresh = refresh;
|
|
19893
|
+
try {
|
|
19894
|
+
return await refresh;
|
|
19895
|
+
} finally {
|
|
19896
|
+
if (this.presentHeightRefresh === refresh) this.presentHeightRefresh = void 0;
|
|
19897
|
+
}
|
|
19898
|
+
}
|
|
19899
|
+
async loadPresentHeight() {
|
|
19861
19900
|
for (const [index, bulk] of this.bulkIngestors.entries()) {
|
|
19862
19901
|
const source = this.sourceName("bulk", index, bulk);
|
|
19863
19902
|
try {
|
|
@@ -19865,7 +19904,7 @@ var Chaintracks = class {
|
|
|
19865
19904
|
if (presentHeight != null && Number.isInteger(presentHeight) && presentHeight >= 0) {
|
|
19866
19905
|
this.markSourceSuccess(source, "bulk");
|
|
19867
19906
|
this.lastPresentHeight = presentHeight;
|
|
19868
|
-
this.lastPresentHeightMsecs = now;
|
|
19907
|
+
this.lastPresentHeightMsecs = Date.now();
|
|
19869
19908
|
return presentHeight;
|
|
19870
19909
|
}
|
|
19871
19910
|
} catch (uerr) {
|
|
@@ -19880,7 +19919,7 @@ var Chaintracks = class {
|
|
|
19880
19919
|
const localHeight = Math.max(ranges.bulk.maxHeight, ranges.live.maxHeight);
|
|
19881
19920
|
if (localHeight >= 0) {
|
|
19882
19921
|
this.lastPresentHeight = localHeight;
|
|
19883
|
-
this.lastPresentHeightMsecs = now;
|
|
19922
|
+
this.lastPresentHeightMsecs = Date.now();
|
|
19884
19923
|
return localHeight;
|
|
19885
19924
|
}
|
|
19886
19925
|
} catch (error) {
|
|
@@ -19891,6 +19930,19 @@ var Chaintracks = class {
|
|
|
19891
19930
|
async currentHeight() {
|
|
19892
19931
|
return await this.getPresentHeight();
|
|
19893
19932
|
}
|
|
19933
|
+
/** Returns local process state without locks, storage reads, or network I/O. */
|
|
19934
|
+
getAvailabilitySnapshot() {
|
|
19935
|
+
return {
|
|
19936
|
+
available: this.available,
|
|
19937
|
+
startupError: this.startupError?.message,
|
|
19938
|
+
presentHeight: this.lastPresentHeight >= 0 ? this.lastPresentHeight : void 0,
|
|
19939
|
+
presentHeightUpdatedAt: this.lastPresentHeightMsecs > 0 ? new Date(this.lastPresentHeightMsecs).toISOString() : void 0,
|
|
19940
|
+
presentHeightRefreshInFlight: this.presentHeightRefresh != null,
|
|
19941
|
+
mainLoopHeartbeatAt: this.mainLoopHeartbeatMsecs > 0 ? new Date(this.mainLoopHeartbeatMsecs).toISOString() : void 0,
|
|
19942
|
+
sources: Array.from(this.sourceStatus.values()).map((status) => ({ ...status })),
|
|
19943
|
+
bulkData: this.storage.bulkManager.getStats()
|
|
19944
|
+
};
|
|
19945
|
+
}
|
|
19894
19946
|
async subscribeHeaders(listener) {
|
|
19895
19947
|
const ID = randomBytesBase64(8);
|
|
19896
19948
|
this.callbacks.header[ID] = listener;
|
|
@@ -19955,6 +20007,7 @@ var Chaintracks = class {
|
|
|
19955
20007
|
for (const liveIn of this.liveIngestors) await liveIn.shutdown();
|
|
19956
20008
|
for (const bulkIn of this.bulkIngestors) await bulkIn.shutdown();
|
|
19957
20009
|
await Promise.all(this.promises);
|
|
20010
|
+
await this.storage.bulkManager.destroy();
|
|
19958
20011
|
await this.storage.destroy();
|
|
19959
20012
|
this.available = false;
|
|
19960
20013
|
this.stopMainThread = false;
|
|
@@ -20236,9 +20289,11 @@ var Chaintracks = class {
|
|
|
20236
20289
|
const syncCheckRepeatMsecs = 1800 * 1e3;
|
|
20237
20290
|
while (!this.stopMainThread) try {
|
|
20238
20291
|
const now = Date.now();
|
|
20292
|
+
this.mainLoopHeartbeatMsecs = now;
|
|
20239
20293
|
lastSyncCheck = now;
|
|
20240
20294
|
lastBulkSync = await this.runBulkSyncIfNeeded(now, lastBulkSync, cdnSyncRepeatMsecs);
|
|
20241
20295
|
await this.processLiveHeaderQueue(lastSyncCheck, syncCheckRepeatMsecs);
|
|
20296
|
+
this.mainLoopHeartbeatMsecs = Date.now();
|
|
20242
20297
|
} catch (error_) {
|
|
20243
20298
|
const e = WalletError.fromUnknown(error_);
|
|
20244
20299
|
if (this.available) this.log(`Error occurred during chaintracks main thread processing: ${e.stack || e.message}`);
|
|
@@ -20250,7 +20305,7 @@ var Chaintracks = class {
|
|
|
20250
20305
|
}
|
|
20251
20306
|
/** Returns (potentially updated) lastBulkSync timestamp. */
|
|
20252
20307
|
async runBulkSyncIfNeeded(now, lastBulkSync, cdnSyncRepeatMsecs) {
|
|
20253
|
-
const presentHeight = await this.
|
|
20308
|
+
const presentHeight = await this.refreshPresentHeight();
|
|
20254
20309
|
const before = await this.storage.getAvailableHeightRanges();
|
|
20255
20310
|
let skipBulkSync = !before.live.isEmpty && before.live.maxHeight >= presentHeight - this.addLiveRecursionLimit / 2;
|
|
20256
20311
|
if (skipBulkSync && now - lastBulkSync > cdnSyncRepeatMsecs) skipBulkSync = false;
|
|
@@ -21399,12 +21454,12 @@ var ChaintracksFetch = class {
|
|
|
21399
21454
|
this.maxRetryMsecs = positiveSafeInteger(options.maxRetryMsecs, DEFAULT_MAX_RETRY_MSECS, "maxRetryMsecs");
|
|
21400
21455
|
this.random = options.random ?? Math.random;
|
|
21401
21456
|
}
|
|
21402
|
-
async download(url, maxResponseBytes) {
|
|
21457
|
+
async download(url, maxResponseBytes, options) {
|
|
21403
21458
|
const responseLimit = maxResponseBytes == null ? this.maxResponseBytes : Math.min(this.maxResponseBytes, positiveSafeInteger(maxResponseBytes, this.maxResponseBytes, "maxResponseBytes"));
|
|
21404
21459
|
return await this.requestBytes(url, {
|
|
21405
21460
|
method: "GET",
|
|
21406
21461
|
headers: { Accept: "application/octet-stream" }
|
|
21407
|
-
}, "download", responseLimit);
|
|
21462
|
+
}, "download", responseLimit, options);
|
|
21408
21463
|
}
|
|
21409
21464
|
async fetchJson(url) {
|
|
21410
21465
|
const bytes = await this.requestBytes(url, {
|
|
@@ -21413,8 +21468,9 @@ var ChaintracksFetch = class {
|
|
|
21413
21468
|
}, "fetch JSON", this.maxResponseBytes);
|
|
21414
21469
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
21415
21470
|
}
|
|
21416
|
-
async requestBytes(url, init, kind, maxResponseBytes) {
|
|
21471
|
+
async requestBytes(url, init, kind, maxResponseBytes, downloadOptions) {
|
|
21417
21472
|
for (let retry = 0;; retry++) {
|
|
21473
|
+
if (retry > 0) await downloadOptions?.beforeRetry?.(retry + 1);
|
|
21418
21474
|
const controller = new AbortController();
|
|
21419
21475
|
const timeout = setTimeout(() => controller.abort(), this.timeoutMsecs);
|
|
21420
21476
|
try {
|
|
@@ -21490,6 +21546,38 @@ var ChaintracksFetch = class {
|
|
|
21490
21546
|
}
|
|
21491
21547
|
};
|
|
21492
21548
|
//#endregion
|
|
21549
|
+
//#region ../src/services/chaintracker/chaintracks/util/InlineBulkFileDataValidator.ts
|
|
21550
|
+
/**
|
|
21551
|
+
* Portable complete-object validator. Node services should normally inject
|
|
21552
|
+
* `NodeBulkFileDataValidator`; browser and mobile consumers retain this
|
|
21553
|
+
* dependency-free fallback.
|
|
21554
|
+
*
|
|
21555
|
+
* @public
|
|
21556
|
+
*/
|
|
21557
|
+
var InlineBulkFileDataValidator = class {
|
|
21558
|
+
async validate(request) {
|
|
21559
|
+
try {
|
|
21560
|
+
const expectedLength = request.count * 80;
|
|
21561
|
+
if (request.data.length !== expectedLength) throw new WERR_INVALID_PARAMETER("file.data", `bulk file ${request.fileName} data length ${request.data.length} does not match expected count ${request.count}`);
|
|
21562
|
+
const fileHash = asString(_bsv_sdk.Hash.sha256(asArray(request.data)), "base64");
|
|
21563
|
+
if (request.fileHash != null && fileHash !== request.fileHash) throw new WERR_INVALID_PARAMETER("fileHash", `a match for retrieved data for ${request.fileName}`);
|
|
21564
|
+
const { lastHeaderHash, lastChainWork } = validateBufferOfHeaders(request.data, request.prevHash, 0, request.count, request.prevChainWork);
|
|
21565
|
+
if (request.lastHash && request.lastHash !== lastHeaderHash) throw new WERR_INVALID_PARAMETER("file.lastHash", `expected ${request.lastHash} but got ${lastHeaderHash}`);
|
|
21566
|
+
if (request.lastChainWork && request.lastChainWork !== lastChainWork) throw new WERR_INVALID_PARAMETER("file.lastChainWork", `expected ${request.lastChainWork} but got ${lastChainWork}`);
|
|
21567
|
+
if (request.firstHeight === 0 && request.chain != null) validateGenesisHeader(request.data, request.chain);
|
|
21568
|
+
return {
|
|
21569
|
+
data: request.data,
|
|
21570
|
+
fileHash,
|
|
21571
|
+
lastHeaderHash,
|
|
21572
|
+
lastChainWork
|
|
21573
|
+
};
|
|
21574
|
+
} catch (error) {
|
|
21575
|
+
if (error instanceof BulkFileDataValidationError) throw error;
|
|
21576
|
+
throw new BulkFileDataValidationError(error instanceof Error ? error.message : String(error), request.data);
|
|
21577
|
+
}
|
|
21578
|
+
}
|
|
21579
|
+
};
|
|
21580
|
+
//#endregion
|
|
21493
21581
|
//#region ../src/services/chaintracker/chaintracks/util/BulkFileDataManager.ts
|
|
21494
21582
|
/**
|
|
21495
21583
|
* Manages bulk file data (typically 8MB chunks of 100,000 headers each).
|
|
@@ -21515,6 +21603,7 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
21515
21603
|
fileHashToIndex = {};
|
|
21516
21604
|
lock = new SingleWriterMultiReaderLock();
|
|
21517
21605
|
inFlightLoads = /* @__PURE__ */ new Map();
|
|
21606
|
+
failedLoads = /* @__PURE__ */ new Map();
|
|
21518
21607
|
storage;
|
|
21519
21608
|
stats = {
|
|
21520
21609
|
memoryHits: 0,
|
|
@@ -21524,7 +21613,8 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
21524
21613
|
persistentCacheRejects: 0,
|
|
21525
21614
|
coalescedLoads: 0,
|
|
21526
21615
|
downloads: 0,
|
|
21527
|
-
downloadedBytes: 0
|
|
21616
|
+
downloadedBytes: 0,
|
|
21617
|
+
loadBackoffs: 0
|
|
21528
21618
|
};
|
|
21529
21619
|
chain;
|
|
21530
21620
|
maxPerFile;
|
|
@@ -21533,6 +21623,8 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
21533
21623
|
fromKnownSourceUrl;
|
|
21534
21624
|
cache;
|
|
21535
21625
|
downloadBudget;
|
|
21626
|
+
validator;
|
|
21627
|
+
failedLoadRetryMsecs;
|
|
21536
21628
|
constructor(options) {
|
|
21537
21629
|
const resolvedOptions = typeof options === "object" ? options : BulkFileDataManager.createDefaultOptions(options);
|
|
21538
21630
|
this.chain = resolvedOptions.chain;
|
|
@@ -21542,10 +21634,17 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
21542
21634
|
this.fetch = resolvedOptions.fetch;
|
|
21543
21635
|
this.cache = resolvedOptions.cache;
|
|
21544
21636
|
this.downloadBudget = resolvedOptions.downloadBudget;
|
|
21637
|
+
this.validator = resolvedOptions.validator ?? new InlineBulkFileDataValidator();
|
|
21638
|
+
this.failedLoadRetryMsecs = resolvedOptions.failedLoadRetryMsecs ?? 30 * 1e3;
|
|
21639
|
+
if (!Number.isSafeInteger(this.failedLoadRetryMsecs) || this.failedLoadRetryMsecs < 0) throw new WERR_INVALID_PARAMETER("failedLoadRetryMsecs", "a non-negative safe integer");
|
|
21545
21640
|
this.deleteBulkFilesNoLock();
|
|
21546
21641
|
}
|
|
21547
21642
|
getStats() {
|
|
21548
|
-
return {
|
|
21643
|
+
return {
|
|
21644
|
+
...this.stats,
|
|
21645
|
+
validation: this.validator.getStats?.(),
|
|
21646
|
+
downloadBudget: this.downloadBudget?.snapshot?.()
|
|
21647
|
+
};
|
|
21549
21648
|
}
|
|
21550
21649
|
async deleteBulkFiles() {
|
|
21551
21650
|
return await this.lock.withWriteLock(async () => this.deleteBulkFilesNoLock());
|
|
@@ -21764,9 +21863,29 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
21764
21863
|
});
|
|
21765
21864
|
}
|
|
21766
21865
|
async getDataFromFile(file, offset, length) {
|
|
21767
|
-
const
|
|
21768
|
-
|
|
21769
|
-
|
|
21866
|
+
const resolved = await this.lock.withReadLock(async () => {
|
|
21867
|
+
const resolved = this.getBfdForHeight(file.firstHeight);
|
|
21868
|
+
if (resolved == null || resolved.count < file.count) throw new WERR_INVALID_PARAMETER("file", `a match for ${file.firstHeight}, ${file.count} in the BulkFileDataManager.`);
|
|
21869
|
+
return {
|
|
21870
|
+
current: resolved,
|
|
21871
|
+
snapshot: snapshotBfd(resolved)
|
|
21872
|
+
};
|
|
21873
|
+
});
|
|
21874
|
+
return await this.getDataFromSnapshot(resolved.current, resolved.snapshot, offset, length);
|
|
21875
|
+
}
|
|
21876
|
+
async getDataFromSnapshot(original, snapshot, offset, length) {
|
|
21877
|
+
const data = await this.getDataFromFileNoLock(snapshot, offset, length);
|
|
21878
|
+
if (snapshot.data != null) await this.lock.withWriteLock(async () => {
|
|
21879
|
+
if (this.bfds.includes(original) && original.fileHash === snapshot.fileHash && original.firstHeight === snapshot.firstHeight && original.count === snapshot.count) {
|
|
21880
|
+
original.data = snapshot.data;
|
|
21881
|
+
original.validated = true;
|
|
21882
|
+
original.lastHash = snapshot.lastHash;
|
|
21883
|
+
original.lastChainWork = snapshot.lastChainWork;
|
|
21884
|
+
original.mru = Date.now();
|
|
21885
|
+
this.ensureMaxRetained();
|
|
21886
|
+
}
|
|
21887
|
+
});
|
|
21888
|
+
return data;
|
|
21770
21889
|
}
|
|
21771
21890
|
async getDataFromFileNoLock(bfd, offset, length) {
|
|
21772
21891
|
const fileLength = bfd.count * 80;
|
|
@@ -21777,15 +21896,19 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
21777
21896
|
return (await this.ensureData(bfd)).slice(offset, offset + length);
|
|
21778
21897
|
}
|
|
21779
21898
|
async findHeaderForHeightOrUndefined(height) {
|
|
21780
|
-
|
|
21899
|
+
const resolved = await this.lock.withReadLock(async () => {
|
|
21781
21900
|
if (!Number.isInteger(height) || height < 0) throw new WERR_INVALID_PARAMETER("height", `a non-negative integer (${height}).`);
|
|
21782
21901
|
const file = this.bfds.find((f) => f.firstHeight <= height && f.firstHeight + f.count > height);
|
|
21783
21902
|
if (file == null) return void 0;
|
|
21784
|
-
|
|
21785
|
-
|
|
21786
|
-
|
|
21787
|
-
|
|
21903
|
+
return {
|
|
21904
|
+
current: file,
|
|
21905
|
+
snapshot: snapshotBfd(file),
|
|
21906
|
+
offset: (height - file.firstHeight) * 80
|
|
21907
|
+
};
|
|
21788
21908
|
});
|
|
21909
|
+
if (resolved == null) return void 0;
|
|
21910
|
+
const data = await this.getDataFromSnapshot(resolved.current, resolved.snapshot, resolved.offset, 80);
|
|
21911
|
+
return data == null ? void 0 : deserializeBlockHeader(data, height, 0);
|
|
21789
21912
|
}
|
|
21790
21913
|
async getFileForHeight(height) {
|
|
21791
21914
|
return await this.lock.withReadLock(async () => {
|
|
@@ -21840,22 +21963,30 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
21840
21963
|
return bfd;
|
|
21841
21964
|
}
|
|
21842
21965
|
async validateBfdData(bfd, expectedFileHash) {
|
|
21843
|
-
await this.ensureData(bfd);
|
|
21844
|
-
|
|
21845
|
-
bfd.fileHash = asString(_bsv_sdk.Hash.sha256(asArray(bfd.data)), "base64");
|
|
21846
|
-
if (expectedFileHash && expectedFileHash !== bfd.fileHash) throw new WERR_INVALID_PARAMETER("file.fileHash", `expected ${expectedFileHash} but got ${bfd.fileHash}`);
|
|
21847
|
-
this.validateBfdHeaders(bfd);
|
|
21966
|
+
const data = await this.ensureData(bfd);
|
|
21967
|
+
bfd.data = await this.validateRetrievedData(bfd, data, expectedFileHash);
|
|
21848
21968
|
}
|
|
21849
|
-
validateBfdHeaders(bfd) {
|
|
21969
|
+
async validateBfdHeaders(bfd, expectedFileHash = bfd.fileHash) {
|
|
21850
21970
|
const pbf = bfd.firstHeight > 0 ? this.getBfdForHeight(bfd.firstHeight - 1) : void 0;
|
|
21851
21971
|
const prevHash = pbf?.lastHash ?? "00".repeat(32);
|
|
21852
21972
|
const prevChainWork = pbf?.lastChainWork ?? "00".repeat(32);
|
|
21853
|
-
const
|
|
21854
|
-
|
|
21855
|
-
|
|
21856
|
-
|
|
21857
|
-
|
|
21858
|
-
|
|
21973
|
+
const result = await this.validator.validate({
|
|
21974
|
+
fileName: bfd.fileName,
|
|
21975
|
+
data: bfd.data,
|
|
21976
|
+
count: bfd.count,
|
|
21977
|
+
fileHash: expectedFileHash,
|
|
21978
|
+
firstHeight: bfd.firstHeight,
|
|
21979
|
+
prevHash,
|
|
21980
|
+
prevChainWork,
|
|
21981
|
+
lastHash: bfd.lastHash,
|
|
21982
|
+
lastChainWork: bfd.lastChainWork,
|
|
21983
|
+
chain: bfd.chain
|
|
21984
|
+
});
|
|
21985
|
+
bfd.data = result.data;
|
|
21986
|
+
bfd.fileHash = result.fileHash;
|
|
21987
|
+
bfd.lastHash = result.lastHeaderHash;
|
|
21988
|
+
bfd.lastChainWork = result.lastChainWork;
|
|
21989
|
+
return result.data;
|
|
21859
21990
|
}
|
|
21860
21991
|
async ReValidate() {
|
|
21861
21992
|
return await this.lock.withReadLock(async () => await this.ReValidateNoLock());
|
|
@@ -22046,62 +22177,96 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
22046
22177
|
this.ensureMaxRetained();
|
|
22047
22178
|
return data;
|
|
22048
22179
|
}
|
|
22180
|
+
const failed = this.failedLoads.get(key);
|
|
22181
|
+
if (failed != null) {
|
|
22182
|
+
if (Date.now() < failed.retryAt) {
|
|
22183
|
+
this.stats.loadBackoffs++;
|
|
22184
|
+
throw failed.error;
|
|
22185
|
+
}
|
|
22186
|
+
this.failedLoads.delete(key);
|
|
22187
|
+
}
|
|
22049
22188
|
const load = this.loadAndValidateData(bfd);
|
|
22050
22189
|
this.inFlightLoads.set(key, load);
|
|
22051
22190
|
try {
|
|
22052
22191
|
const data = await load;
|
|
22053
22192
|
bfd.data = data;
|
|
22054
22193
|
bfd.validated = true;
|
|
22194
|
+
this.failedLoads.delete(key);
|
|
22055
22195
|
bfd.mru = Date.now();
|
|
22056
22196
|
this.ensureMaxRetained();
|
|
22057
22197
|
return data;
|
|
22198
|
+
} catch (error) {
|
|
22199
|
+
const resolved = error instanceof Error ? error : new Error(String(error));
|
|
22200
|
+
this.failedLoads.set(key, {
|
|
22201
|
+
retryAt: Date.now() + this.failedLoadRetryMsecs,
|
|
22202
|
+
error: resolved
|
|
22203
|
+
});
|
|
22204
|
+
throw resolved;
|
|
22058
22205
|
} finally {
|
|
22059
22206
|
if (this.inFlightLoads.get(key) === load) this.inFlightLoads.delete(key);
|
|
22060
22207
|
}
|
|
22061
22208
|
}
|
|
22062
22209
|
async loadAndValidateData(bfd) {
|
|
22063
|
-
|
|
22064
|
-
|
|
22065
|
-
|
|
22066
|
-
|
|
22067
|
-
|
|
22068
|
-
|
|
22069
|
-
}
|
|
22070
|
-
if (this.cache != null) {
|
|
22071
|
-
const cached = await this.cache.get(bfd);
|
|
22072
|
-
if (cached != null) try {
|
|
22073
|
-
this.validateRetrievedData(bfd, cached);
|
|
22074
|
-
this.stats.persistentCacheHits++;
|
|
22075
|
-
return cached;
|
|
22076
|
-
} catch (error) {
|
|
22077
|
-
this.stats.persistentCacheRejects++;
|
|
22078
|
-
await this.cache.delete?.(bfd);
|
|
22079
|
-
this.log(`Rejected corrupt bulk-header cache entry ${bfd.fileName}: ${String(error)}`);
|
|
22080
|
-
}
|
|
22081
|
-
else this.stats.persistentCacheMisses++;
|
|
22082
|
-
}
|
|
22083
|
-
if (this.fetch != null && bfd.sourceUrl) {
|
|
22084
|
-
const expectedBytes = bfd.count * 80;
|
|
22085
|
-
await this.downloadBudget?.consume(expectedBytes);
|
|
22086
|
-
const url = this.fetch.pathJoin(bfd.sourceUrl, bfd.fileName);
|
|
22087
|
-
const downloaded = await this.fetch.download(url, expectedBytes);
|
|
22088
|
-
if (downloaded == null) throw new WERR_INVALID_PARAMETER("sourceUrl", `data not found for sourceUrl ${url}`);
|
|
22089
|
-
this.validateRetrievedData(bfd, downloaded);
|
|
22090
|
-
this.stats.downloads++;
|
|
22091
|
-
this.stats.downloadedBytes += downloaded.length;
|
|
22092
|
-
await this.cache?.set(bfd, downloaded);
|
|
22093
|
-
return downloaded;
|
|
22094
|
-
}
|
|
22210
|
+
const stored = await this.loadFromStorage(bfd);
|
|
22211
|
+
if (stored != null) return stored;
|
|
22212
|
+
const cached = await this.loadFromCache(bfd);
|
|
22213
|
+
if (cached != null) return cached;
|
|
22214
|
+
const downloaded = await this.loadFromRemote(bfd);
|
|
22215
|
+
if (downloaded != null) return downloaded;
|
|
22095
22216
|
throw new WERR_INVALID_PARAMETER("data", `defined. Unable to retrieve data for ${bfd.fileName}`);
|
|
22096
22217
|
}
|
|
22097
|
-
|
|
22098
|
-
if (
|
|
22099
|
-
|
|
22218
|
+
async loadFromStorage(bfd) {
|
|
22219
|
+
if (this.storage == null || !bfd.fileId) return void 0;
|
|
22220
|
+
const stored = await this.storage.getBulkFileData(bfd.fileId);
|
|
22221
|
+
if (stored == null) throw new WERR_INVALID_PARAMETER("fileId", `valid, data not found for fileId ${bfd.fileId}`);
|
|
22222
|
+
const validated = await this.validateRetrievedData(bfd, stored);
|
|
22223
|
+
this.stats.storageHits++;
|
|
22224
|
+
return validated;
|
|
22225
|
+
}
|
|
22226
|
+
async loadFromCache(bfd) {
|
|
22227
|
+
if (this.cache == null) return void 0;
|
|
22228
|
+
const cached = await this.cache.get(bfd);
|
|
22229
|
+
if (cached == null) {
|
|
22230
|
+
this.stats.persistentCacheMisses++;
|
|
22231
|
+
return;
|
|
22232
|
+
}
|
|
22233
|
+
try {
|
|
22234
|
+
const validated = await this.validateRetrievedData(bfd, cached);
|
|
22235
|
+
this.stats.persistentCacheHits++;
|
|
22236
|
+
await this.cache.promoteValidated?.(bfd, validated);
|
|
22237
|
+
return validated;
|
|
22238
|
+
} catch (error) {
|
|
22239
|
+
if (!(error instanceof BulkFileDataValidationError)) throw error;
|
|
22240
|
+
this.stats.persistentCacheRejects++;
|
|
22241
|
+
let rejectedData = error.data;
|
|
22242
|
+
if (!(rejectedData instanceof Uint8Array) && cached.byteLength > 0) rejectedData = cached;
|
|
22243
|
+
await this.cache.quarantine?.(bfd, String(error), rejectedData);
|
|
22244
|
+
this.log(`Rejected corrupt bulk-header cache entry ${bfd.fileName}: ${String(error)}`);
|
|
22245
|
+
return;
|
|
22246
|
+
}
|
|
22247
|
+
}
|
|
22248
|
+
async loadFromRemote(bfd) {
|
|
22249
|
+
if (this.fetch == null || !bfd.sourceUrl) return void 0;
|
|
22250
|
+
const expectedBytes = bfd.count * 80;
|
|
22251
|
+
await this.downloadBudget?.consume(expectedBytes);
|
|
22252
|
+
const url = this.fetch.pathJoin(bfd.sourceUrl, bfd.fileName);
|
|
22253
|
+
const downloaded = await this.fetch.download(url, expectedBytes, { beforeRetry: async () => await this.downloadBudget?.consume(expectedBytes) });
|
|
22254
|
+
if (downloaded == null) throw new WERR_INVALID_PARAMETER("sourceUrl", `data not found for sourceUrl ${url}`);
|
|
22255
|
+
const validated = await this.validateRetrievedData(bfd, downloaded);
|
|
22256
|
+
this.stats.downloads++;
|
|
22257
|
+
this.stats.downloadedBytes += validated.length;
|
|
22258
|
+
await this.cache?.set(bfd, validated);
|
|
22259
|
+
return validated;
|
|
22260
|
+
}
|
|
22261
|
+
async validateRetrievedData(bfd, data, expectedFileHash = bfd.fileHash) {
|
|
22100
22262
|
const candidate = {
|
|
22101
22263
|
...bfd,
|
|
22102
22264
|
data
|
|
22103
22265
|
};
|
|
22104
|
-
this.validateBfdHeaders(candidate);
|
|
22266
|
+
const validated = await this.validateBfdHeaders(candidate, expectedFileHash);
|
|
22267
|
+
bfd.lastHash = candidate.lastHash;
|
|
22268
|
+
bfd.lastChainWork = candidate.lastChainWork;
|
|
22269
|
+
return validated;
|
|
22105
22270
|
}
|
|
22106
22271
|
ensureMaxRetained() {
|
|
22107
22272
|
if (this.maxRetained === void 0) return;
|
|
@@ -22137,17 +22302,24 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
22137
22302
|
i++;
|
|
22138
22303
|
const data = await reader.read();
|
|
22139
22304
|
if (data == null || data.length === 0) break;
|
|
22140
|
-
const
|
|
22141
|
-
|
|
22142
|
-
|
|
22305
|
+
const validated = await this.validator.validate({
|
|
22306
|
+
fileName: toFileName(i),
|
|
22307
|
+
data,
|
|
22308
|
+
count: data.length / 80,
|
|
22309
|
+
firstHeight,
|
|
22310
|
+
prevHash: lastHeaderHash,
|
|
22311
|
+
prevChainWork: lastChainWork,
|
|
22312
|
+
chain
|
|
22313
|
+
});
|
|
22314
|
+
await toFs.writeFile(toPath(i), validated.data);
|
|
22143
22315
|
const file = {
|
|
22144
22316
|
chain,
|
|
22145
|
-
count: data.length / 80,
|
|
22146
|
-
fileHash,
|
|
22317
|
+
count: validated.data.length / 80,
|
|
22318
|
+
fileHash: validated.fileHash,
|
|
22147
22319
|
fileName: toFileName(i),
|
|
22148
22320
|
firstHeight,
|
|
22149
|
-
lastChainWork:
|
|
22150
|
-
lastHash:
|
|
22321
|
+
lastChainWork: validated.lastChainWork,
|
|
22322
|
+
lastHash: validated.lastHeaderHash,
|
|
22151
22323
|
prevChainWork: lastChainWork,
|
|
22152
22324
|
prevHash: lastHeaderHash,
|
|
22153
22325
|
sourceUrl
|
|
@@ -22159,7 +22331,16 @@ var BulkFileDataManager = class BulkFileDataManager {
|
|
|
22159
22331
|
}
|
|
22160
22332
|
await toFs.writeFile(toJsonPath(), asUint8Array(JSON.stringify(toBulkFiles), "utf8"));
|
|
22161
22333
|
}
|
|
22334
|
+
async destroy() {
|
|
22335
|
+
await this.validator.destroy?.();
|
|
22336
|
+
}
|
|
22162
22337
|
};
|
|
22338
|
+
function snapshotBfd(file) {
|
|
22339
|
+
return {
|
|
22340
|
+
...file,
|
|
22341
|
+
data: file.data
|
|
22342
|
+
};
|
|
22343
|
+
}
|
|
22163
22344
|
function selectBulkHeaderFiles(files, chain, maxPerFile) {
|
|
22164
22345
|
const r = [];
|
|
22165
22346
|
let height = 0;
|
|
@@ -27028,7 +27209,8 @@ function createDefaultBulkFileDataManager(params) {
|
|
|
27028
27209
|
maxRetained: params.maxRetained,
|
|
27029
27210
|
fromKnownSourceUrl: params.cdnUrl,
|
|
27030
27211
|
cache: params.sources.bulkFileCache,
|
|
27031
|
-
downloadBudget: params.sources.bulkFileDownloadBudget
|
|
27212
|
+
downloadBudget: params.sources.bulkFileDownloadBudget,
|
|
27213
|
+
validator: params.sources.bulkFileDataValidator
|
|
27032
27214
|
});
|
|
27033
27215
|
}
|
|
27034
27216
|
function createDefaultChaintracksStorageOptions(params) {
|
|
@@ -27553,6 +27735,7 @@ var FixedWindowBulkFileDownloadBudget = class {
|
|
|
27553
27735
|
return {
|
|
27554
27736
|
maxBytes: this.maxBytes,
|
|
27555
27737
|
consumedBytes: this.consumedBytes,
|
|
27738
|
+
remainingBytes: this.maxBytes - this.consumedBytes,
|
|
27556
27739
|
windowStartedAt: this.windowStartedAt,
|
|
27557
27740
|
windowMsecs: this.windowMsecs
|
|
27558
27741
|
};
|
|
@@ -36828,6 +37011,7 @@ exports.BHServiceClient = BHServiceClient;
|
|
|
36828
37011
|
exports.BRC153_REFERENCE_PREFIX = BRC153_REFERENCE_PREFIX;
|
|
36829
37012
|
exports.BulkFileDataManager = BulkFileDataManager;
|
|
36830
37013
|
exports.BulkFileDataReader = BulkFileDataReader;
|
|
37014
|
+
exports.BulkFileDataValidationError = BulkFileDataValidationError;
|
|
36831
37015
|
exports.BulkFilesReader = BulkFilesReader;
|
|
36832
37016
|
exports.BulkFilesReaderFs = BulkFilesReaderFs;
|
|
36833
37017
|
exports.BulkFilesReaderStorage = BulkFilesReaderStorage;
|
|
@@ -36876,6 +37060,7 @@ exports.EntityUser = EntityUser;
|
|
|
36876
37060
|
exports.FixedWindowBulkFileDownloadBudget = FixedWindowBulkFileDownloadBudget;
|
|
36877
37061
|
exports.GoChaintracksServiceClient = GoChaintracksServiceClient;
|
|
36878
37062
|
exports.HeightRange = HeightRange;
|
|
37063
|
+
exports.InlineBulkFileDataValidator = InlineBulkFileDataValidator;
|
|
36879
37064
|
exports.KDF_MAX_HASH_LENGTH = KDF_MAX_HASH_LENGTH;
|
|
36880
37065
|
exports.LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS = LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS;
|
|
36881
37066
|
exports.LiveIngestorBase = LiveIngestorBase;
|