@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.
@@ -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
- * Caches and returns most recently sourced value if less than one minute old.
19856
- * @returns the current externally available chain height (via bulk ingestors).
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.getPresentHeight();
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 { ...this.stats };
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 bfd = this.getBfdForHeight(file.firstHeight);
21768
- if (bfd == null || bfd.count < file.count) throw new WERR_INVALID_PARAMETER("file", `a match for ${file.firstHeight}, ${file.count} in the BulkFileDataManager.`);
21769
- return await this.lock.withReadLock(async () => await this.getDataFromFileNoLock(bfd, offset, length));
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
- return await this.lock.withReadLock(async () => {
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
- const offset = (height - file.firstHeight) * 80;
21785
- const data = await this.getDataFromFileNoLock(file, offset, 80);
21786
- if (data == null) return void 0;
21787
- return deserializeBlockHeader(data, height, 0);
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
- if (bfd.data?.length !== bfd.count * 80) throw new WERR_INVALID_PARAMETER("file.data", `bulk file ${bfd.fileName} data length ${bfd.data?.length} does not match expected count ${bfd.count}`);
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 { lastHeaderHash, lastChainWork } = validateBufferOfHeaders(bfd.data, prevHash, 0, void 0, prevChainWork);
21854
- if (bfd.lastHash && bfd.lastHash !== lastHeaderHash) throw new WERR_INVALID_PARAMETER("file.lastHash", `expected ${bfd.lastHash} but got ${lastHeaderHash}`);
21855
- if (bfd.lastChainWork && bfd.lastChainWork !== lastChainWork) throw new WERR_INVALID_PARAMETER("file.lastChainWork", `expected ${bfd.lastChainWork} but got ${lastChainWork}`);
21856
- bfd.lastHash = lastHeaderHash;
21857
- bfd.lastChainWork = lastChainWork;
21858
- if (bfd.firstHeight === 0) validateGenesisHeader(bfd.data, bfd.chain);
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
- if (this.storage != null && bfd.fileId) {
22064
- const stored = await this.storage.getBulkFileData(bfd.fileId);
22065
- if (stored == null) throw new WERR_INVALID_PARAMETER("fileId", `valid, data not found for fileId ${bfd.fileId}`);
22066
- this.validateRetrievedData(bfd, stored);
22067
- this.stats.storageHits++;
22068
- return stored;
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
- validateRetrievedData(bfd, data) {
22098
- if (data.length !== bfd.count * 80) throw new WERR_INVALID_PARAMETER("file.data", `bulk file ${bfd.fileName} data length ${data.length} does not match expected count ${bfd.count}`);
22099
- if (asString(_bsv_sdk.Hash.sha256(asArray(data)), "base64") !== bfd.fileHash) throw new WERR_INVALID_PARAMETER("fileHash", `a match for retrieved data for ${bfd.fileName}`);
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 last = validateBufferOfHeaders(data, lastHeaderHash, 0, void 0, lastChainWork);
22141
- await toFs.writeFile(toPath(i), data);
22142
- const fileHash = asString(_bsv_sdk.Hash.sha256(asArray(data)), "base64");
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: last.lastChainWork,
22150
- lastHash: last.lastHeaderHash,
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;