@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.
@@ -19562,6 +19562,24 @@ function isLiveBlockHeader(header) {
19562
19562
  return "chainWork" in header && typeof header.previousHash === "string";
19563
19563
  }
19564
19564
  //#endregion
19565
+ //#region ../src/services/chaintracker/chaintracks/Api/BulkFileDataValidatorApi.ts
19566
+ /**
19567
+ * Identifies deterministic rejection of the supplied immutable bytes.
19568
+ * Operational failures such as worker crashes and queue saturation deliberately
19569
+ * use ordinary errors so callers preserve the cache entry and avoid downloading
19570
+ * a replacement that cannot be validated.
19571
+ *
19572
+ * @public
19573
+ */
19574
+ var BulkFileDataValidationError = class extends Error {
19575
+ data;
19576
+ constructor(message, data) {
19577
+ super(message);
19578
+ this.data = data;
19579
+ this.name = "BulkFileDataValidationError";
19580
+ }
19581
+ };
19582
+ //#endregion
19565
19583
  //#region ../src/services/chaintracker/chaintracks/util/HeightRange.ts
19566
19584
  /**
19567
19585
  * Represents a range of block heights.
@@ -19780,6 +19798,8 @@ var Chaintracks = class {
19780
19798
  lastPresentHeight = -1;
19781
19799
  lastPresentHeightMsecs = 0;
19782
19800
  lastPresentHeightMaxAge = 60 * 1e3;
19801
+ presentHeightRefresh;
19802
+ mainLoopHeartbeatMsecs = 0;
19783
19803
  lock = new SingleWriterMultiReaderLock();
19784
19804
  sourceStatus = /* @__PURE__ */ new Map();
19785
19805
  constructor(options) {
@@ -19817,12 +19837,31 @@ var Chaintracks = class {
19817
19837
  return this.chain;
19818
19838
  }
19819
19839
  /**
19820
- * Caches and returns most recently sourced value if less than one minute old.
19821
- * @returns the current externally available chain height (via bulk ingestors).
19840
+ * Returns the last known valid height immediately and refreshes stale state
19841
+ * once in the background. Cold start waits for the single shared refresh.
19822
19842
  */
19823
19843
  async getPresentHeight() {
19824
19844
  const now = Date.now();
19825
19845
  if (this.lastPresentHeight >= 0 && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
19846
+ if (this.lastPresentHeight >= 0) {
19847
+ this.refreshPresentHeight().catch((error) => {
19848
+ this.log(`Background present-height refresh failed: ${WalletError.fromUnknown(error).message}`);
19849
+ });
19850
+ return this.lastPresentHeight;
19851
+ }
19852
+ return await this.refreshPresentHeight();
19853
+ }
19854
+ async refreshPresentHeight() {
19855
+ if (this.presentHeightRefresh != null) return await this.presentHeightRefresh;
19856
+ const refresh = this.loadPresentHeight();
19857
+ this.presentHeightRefresh = refresh;
19858
+ try {
19859
+ return await refresh;
19860
+ } finally {
19861
+ if (this.presentHeightRefresh === refresh) this.presentHeightRefresh = void 0;
19862
+ }
19863
+ }
19864
+ async loadPresentHeight() {
19826
19865
  for (const [index, bulk] of this.bulkIngestors.entries()) {
19827
19866
  const source = this.sourceName("bulk", index, bulk);
19828
19867
  try {
@@ -19830,7 +19869,7 @@ var Chaintracks = class {
19830
19869
  if (presentHeight != null && Number.isInteger(presentHeight) && presentHeight >= 0) {
19831
19870
  this.markSourceSuccess(source, "bulk");
19832
19871
  this.lastPresentHeight = presentHeight;
19833
- this.lastPresentHeightMsecs = now;
19872
+ this.lastPresentHeightMsecs = Date.now();
19834
19873
  return presentHeight;
19835
19874
  }
19836
19875
  } catch (uerr) {
@@ -19845,7 +19884,7 @@ var Chaintracks = class {
19845
19884
  const localHeight = Math.max(ranges.bulk.maxHeight, ranges.live.maxHeight);
19846
19885
  if (localHeight >= 0) {
19847
19886
  this.lastPresentHeight = localHeight;
19848
- this.lastPresentHeightMsecs = now;
19887
+ this.lastPresentHeightMsecs = Date.now();
19849
19888
  return localHeight;
19850
19889
  }
19851
19890
  } catch (error) {
@@ -19856,6 +19895,19 @@ var Chaintracks = class {
19856
19895
  async currentHeight() {
19857
19896
  return await this.getPresentHeight();
19858
19897
  }
19898
+ /** Returns local process state without locks, storage reads, or network I/O. */
19899
+ getAvailabilitySnapshot() {
19900
+ return {
19901
+ available: this.available,
19902
+ startupError: this.startupError?.message,
19903
+ presentHeight: this.lastPresentHeight >= 0 ? this.lastPresentHeight : void 0,
19904
+ presentHeightUpdatedAt: this.lastPresentHeightMsecs > 0 ? new Date(this.lastPresentHeightMsecs).toISOString() : void 0,
19905
+ presentHeightRefreshInFlight: this.presentHeightRefresh != null,
19906
+ mainLoopHeartbeatAt: this.mainLoopHeartbeatMsecs > 0 ? new Date(this.mainLoopHeartbeatMsecs).toISOString() : void 0,
19907
+ sources: Array.from(this.sourceStatus.values()).map((status) => ({ ...status })),
19908
+ bulkData: this.storage.bulkManager.getStats()
19909
+ };
19910
+ }
19859
19911
  async subscribeHeaders(listener) {
19860
19912
  const ID = randomBytesBase64(8);
19861
19913
  this.callbacks.header[ID] = listener;
@@ -19920,6 +19972,7 @@ var Chaintracks = class {
19920
19972
  for (const liveIn of this.liveIngestors) await liveIn.shutdown();
19921
19973
  for (const bulkIn of this.bulkIngestors) await bulkIn.shutdown();
19922
19974
  await Promise.all(this.promises);
19975
+ await this.storage.bulkManager.destroy();
19923
19976
  await this.storage.destroy();
19924
19977
  this.available = false;
19925
19978
  this.stopMainThread = false;
@@ -20201,9 +20254,11 @@ var Chaintracks = class {
20201
20254
  const syncCheckRepeatMsecs = 1800 * 1e3;
20202
20255
  while (!this.stopMainThread) try {
20203
20256
  const now = Date.now();
20257
+ this.mainLoopHeartbeatMsecs = now;
20204
20258
  lastSyncCheck = now;
20205
20259
  lastBulkSync = await this.runBulkSyncIfNeeded(now, lastBulkSync, cdnSyncRepeatMsecs);
20206
20260
  await this.processLiveHeaderQueue(lastSyncCheck, syncCheckRepeatMsecs);
20261
+ this.mainLoopHeartbeatMsecs = Date.now();
20207
20262
  } catch (error_) {
20208
20263
  const e = WalletError.fromUnknown(error_);
20209
20264
  if (this.available) this.log(`Error occurred during chaintracks main thread processing: ${e.stack || e.message}`);
@@ -20215,7 +20270,7 @@ var Chaintracks = class {
20215
20270
  }
20216
20271
  /** Returns (potentially updated) lastBulkSync timestamp. */
20217
20272
  async runBulkSyncIfNeeded(now, lastBulkSync, cdnSyncRepeatMsecs) {
20218
- const presentHeight = await this.getPresentHeight();
20273
+ const presentHeight = await this.refreshPresentHeight();
20219
20274
  const before = await this.storage.getAvailableHeightRanges();
20220
20275
  let skipBulkSync = !before.live.isEmpty && before.live.maxHeight >= presentHeight - this.addLiveRecursionLimit / 2;
20221
20276
  if (skipBulkSync && now - lastBulkSync > cdnSyncRepeatMsecs) skipBulkSync = false;
@@ -21364,12 +21419,12 @@ var ChaintracksFetch = class {
21364
21419
  this.maxRetryMsecs = positiveSafeInteger(options.maxRetryMsecs, DEFAULT_MAX_RETRY_MSECS, "maxRetryMsecs");
21365
21420
  this.random = options.random ?? Math.random;
21366
21421
  }
21367
- async download(url, maxResponseBytes) {
21422
+ async download(url, maxResponseBytes, options) {
21368
21423
  const responseLimit = maxResponseBytes == null ? this.maxResponseBytes : Math.min(this.maxResponseBytes, positiveSafeInteger(maxResponseBytes, this.maxResponseBytes, "maxResponseBytes"));
21369
21424
  return await this.requestBytes(url, {
21370
21425
  method: "GET",
21371
21426
  headers: { Accept: "application/octet-stream" }
21372
- }, "download", responseLimit);
21427
+ }, "download", responseLimit, options);
21373
21428
  }
21374
21429
  async fetchJson(url) {
21375
21430
  const bytes = await this.requestBytes(url, {
@@ -21378,8 +21433,9 @@ var ChaintracksFetch = class {
21378
21433
  }, "fetch JSON", this.maxResponseBytes);
21379
21434
  return JSON.parse(new TextDecoder().decode(bytes));
21380
21435
  }
21381
- async requestBytes(url, init, kind, maxResponseBytes) {
21436
+ async requestBytes(url, init, kind, maxResponseBytes, downloadOptions) {
21382
21437
  for (let retry = 0;; retry++) {
21438
+ if (retry > 0) await downloadOptions?.beforeRetry?.(retry + 1);
21383
21439
  const controller = new AbortController();
21384
21440
  const timeout = setTimeout(() => controller.abort(), this.timeoutMsecs);
21385
21441
  try {
@@ -21455,6 +21511,38 @@ var ChaintracksFetch = class {
21455
21511
  }
21456
21512
  };
21457
21513
  //#endregion
21514
+ //#region ../src/services/chaintracker/chaintracks/util/InlineBulkFileDataValidator.ts
21515
+ /**
21516
+ * Portable complete-object validator. Node services should normally inject
21517
+ * `NodeBulkFileDataValidator`; browser and mobile consumers retain this
21518
+ * dependency-free fallback.
21519
+ *
21520
+ * @public
21521
+ */
21522
+ var InlineBulkFileDataValidator = class {
21523
+ async validate(request) {
21524
+ try {
21525
+ const expectedLength = request.count * 80;
21526
+ 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}`);
21527
+ const fileHash = asString(Hash.sha256(asArray(request.data)), "base64");
21528
+ if (request.fileHash != null && fileHash !== request.fileHash) throw new WERR_INVALID_PARAMETER("fileHash", `a match for retrieved data for ${request.fileName}`);
21529
+ const { lastHeaderHash, lastChainWork } = validateBufferOfHeaders(request.data, request.prevHash, 0, request.count, request.prevChainWork);
21530
+ if (request.lastHash && request.lastHash !== lastHeaderHash) throw new WERR_INVALID_PARAMETER("file.lastHash", `expected ${request.lastHash} but got ${lastHeaderHash}`);
21531
+ if (request.lastChainWork && request.lastChainWork !== lastChainWork) throw new WERR_INVALID_PARAMETER("file.lastChainWork", `expected ${request.lastChainWork} but got ${lastChainWork}`);
21532
+ if (request.firstHeight === 0 && request.chain != null) validateGenesisHeader(request.data, request.chain);
21533
+ return {
21534
+ data: request.data,
21535
+ fileHash,
21536
+ lastHeaderHash,
21537
+ lastChainWork
21538
+ };
21539
+ } catch (error) {
21540
+ if (error instanceof BulkFileDataValidationError) throw error;
21541
+ throw new BulkFileDataValidationError(error instanceof Error ? error.message : String(error), request.data);
21542
+ }
21543
+ }
21544
+ };
21545
+ //#endregion
21458
21546
  //#region ../src/services/chaintracker/chaintracks/util/BulkFileDataManager.ts
21459
21547
  /**
21460
21548
  * Manages bulk file data (typically 8MB chunks of 100,000 headers each).
@@ -21480,6 +21568,7 @@ var BulkFileDataManager = class BulkFileDataManager {
21480
21568
  fileHashToIndex = {};
21481
21569
  lock = new SingleWriterMultiReaderLock();
21482
21570
  inFlightLoads = /* @__PURE__ */ new Map();
21571
+ failedLoads = /* @__PURE__ */ new Map();
21483
21572
  storage;
21484
21573
  stats = {
21485
21574
  memoryHits: 0,
@@ -21489,7 +21578,8 @@ var BulkFileDataManager = class BulkFileDataManager {
21489
21578
  persistentCacheRejects: 0,
21490
21579
  coalescedLoads: 0,
21491
21580
  downloads: 0,
21492
- downloadedBytes: 0
21581
+ downloadedBytes: 0,
21582
+ loadBackoffs: 0
21493
21583
  };
21494
21584
  chain;
21495
21585
  maxPerFile;
@@ -21498,6 +21588,8 @@ var BulkFileDataManager = class BulkFileDataManager {
21498
21588
  fromKnownSourceUrl;
21499
21589
  cache;
21500
21590
  downloadBudget;
21591
+ validator;
21592
+ failedLoadRetryMsecs;
21501
21593
  constructor(options) {
21502
21594
  const resolvedOptions = typeof options === "object" ? options : BulkFileDataManager.createDefaultOptions(options);
21503
21595
  this.chain = resolvedOptions.chain;
@@ -21507,10 +21599,17 @@ var BulkFileDataManager = class BulkFileDataManager {
21507
21599
  this.fetch = resolvedOptions.fetch;
21508
21600
  this.cache = resolvedOptions.cache;
21509
21601
  this.downloadBudget = resolvedOptions.downloadBudget;
21602
+ this.validator = resolvedOptions.validator ?? new InlineBulkFileDataValidator();
21603
+ this.failedLoadRetryMsecs = resolvedOptions.failedLoadRetryMsecs ?? 30 * 1e3;
21604
+ if (!Number.isSafeInteger(this.failedLoadRetryMsecs) || this.failedLoadRetryMsecs < 0) throw new WERR_INVALID_PARAMETER("failedLoadRetryMsecs", "a non-negative safe integer");
21510
21605
  this.deleteBulkFilesNoLock();
21511
21606
  }
21512
21607
  getStats() {
21513
- return { ...this.stats };
21608
+ return {
21609
+ ...this.stats,
21610
+ validation: this.validator.getStats?.(),
21611
+ downloadBudget: this.downloadBudget?.snapshot?.()
21612
+ };
21514
21613
  }
21515
21614
  async deleteBulkFiles() {
21516
21615
  return await this.lock.withWriteLock(async () => this.deleteBulkFilesNoLock());
@@ -21729,9 +21828,29 @@ var BulkFileDataManager = class BulkFileDataManager {
21729
21828
  });
21730
21829
  }
21731
21830
  async getDataFromFile(file, offset, length) {
21732
- const bfd = this.getBfdForHeight(file.firstHeight);
21733
- if (bfd == null || bfd.count < file.count) throw new WERR_INVALID_PARAMETER("file", `a match for ${file.firstHeight}, ${file.count} in the BulkFileDataManager.`);
21734
- return await this.lock.withReadLock(async () => await this.getDataFromFileNoLock(bfd, offset, length));
21831
+ const resolved = await this.lock.withReadLock(async () => {
21832
+ const resolved = this.getBfdForHeight(file.firstHeight);
21833
+ if (resolved == null || resolved.count < file.count) throw new WERR_INVALID_PARAMETER("file", `a match for ${file.firstHeight}, ${file.count} in the BulkFileDataManager.`);
21834
+ return {
21835
+ current: resolved,
21836
+ snapshot: snapshotBfd(resolved)
21837
+ };
21838
+ });
21839
+ return await this.getDataFromSnapshot(resolved.current, resolved.snapshot, offset, length);
21840
+ }
21841
+ async getDataFromSnapshot(original, snapshot, offset, length) {
21842
+ const data = await this.getDataFromFileNoLock(snapshot, offset, length);
21843
+ if (snapshot.data != null) await this.lock.withWriteLock(async () => {
21844
+ if (this.bfds.includes(original) && original.fileHash === snapshot.fileHash && original.firstHeight === snapshot.firstHeight && original.count === snapshot.count) {
21845
+ original.data = snapshot.data;
21846
+ original.validated = true;
21847
+ original.lastHash = snapshot.lastHash;
21848
+ original.lastChainWork = snapshot.lastChainWork;
21849
+ original.mru = Date.now();
21850
+ this.ensureMaxRetained();
21851
+ }
21852
+ });
21853
+ return data;
21735
21854
  }
21736
21855
  async getDataFromFileNoLock(bfd, offset, length) {
21737
21856
  const fileLength = bfd.count * 80;
@@ -21742,15 +21861,19 @@ var BulkFileDataManager = class BulkFileDataManager {
21742
21861
  return (await this.ensureData(bfd)).slice(offset, offset + length);
21743
21862
  }
21744
21863
  async findHeaderForHeightOrUndefined(height) {
21745
- return await this.lock.withReadLock(async () => {
21864
+ const resolved = await this.lock.withReadLock(async () => {
21746
21865
  if (!Number.isInteger(height) || height < 0) throw new WERR_INVALID_PARAMETER("height", `a non-negative integer (${height}).`);
21747
21866
  const file = this.bfds.find((f) => f.firstHeight <= height && f.firstHeight + f.count > height);
21748
21867
  if (file == null) return void 0;
21749
- const offset = (height - file.firstHeight) * 80;
21750
- const data = await this.getDataFromFileNoLock(file, offset, 80);
21751
- if (data == null) return void 0;
21752
- return deserializeBlockHeader(data, height, 0);
21868
+ return {
21869
+ current: file,
21870
+ snapshot: snapshotBfd(file),
21871
+ offset: (height - file.firstHeight) * 80
21872
+ };
21753
21873
  });
21874
+ if (resolved == null) return void 0;
21875
+ const data = await this.getDataFromSnapshot(resolved.current, resolved.snapshot, resolved.offset, 80);
21876
+ return data == null ? void 0 : deserializeBlockHeader(data, height, 0);
21754
21877
  }
21755
21878
  async getFileForHeight(height) {
21756
21879
  return await this.lock.withReadLock(async () => {
@@ -21805,22 +21928,30 @@ var BulkFileDataManager = class BulkFileDataManager {
21805
21928
  return bfd;
21806
21929
  }
21807
21930
  async validateBfdData(bfd, expectedFileHash) {
21808
- await this.ensureData(bfd);
21809
- 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}`);
21810
- bfd.fileHash = asString(Hash.sha256(asArray(bfd.data)), "base64");
21811
- if (expectedFileHash && expectedFileHash !== bfd.fileHash) throw new WERR_INVALID_PARAMETER("file.fileHash", `expected ${expectedFileHash} but got ${bfd.fileHash}`);
21812
- this.validateBfdHeaders(bfd);
21931
+ const data = await this.ensureData(bfd);
21932
+ bfd.data = await this.validateRetrievedData(bfd, data, expectedFileHash);
21813
21933
  }
21814
- validateBfdHeaders(bfd) {
21934
+ async validateBfdHeaders(bfd, expectedFileHash = bfd.fileHash) {
21815
21935
  const pbf = bfd.firstHeight > 0 ? this.getBfdForHeight(bfd.firstHeight - 1) : void 0;
21816
21936
  const prevHash = pbf?.lastHash ?? "00".repeat(32);
21817
21937
  const prevChainWork = pbf?.lastChainWork ?? "00".repeat(32);
21818
- const { lastHeaderHash, lastChainWork } = validateBufferOfHeaders(bfd.data, prevHash, 0, void 0, prevChainWork);
21819
- if (bfd.lastHash && bfd.lastHash !== lastHeaderHash) throw new WERR_INVALID_PARAMETER("file.lastHash", `expected ${bfd.lastHash} but got ${lastHeaderHash}`);
21820
- if (bfd.lastChainWork && bfd.lastChainWork !== lastChainWork) throw new WERR_INVALID_PARAMETER("file.lastChainWork", `expected ${bfd.lastChainWork} but got ${lastChainWork}`);
21821
- bfd.lastHash = lastHeaderHash;
21822
- bfd.lastChainWork = lastChainWork;
21823
- if (bfd.firstHeight === 0) validateGenesisHeader(bfd.data, bfd.chain);
21938
+ const result = await this.validator.validate({
21939
+ fileName: bfd.fileName,
21940
+ data: bfd.data,
21941
+ count: bfd.count,
21942
+ fileHash: expectedFileHash,
21943
+ firstHeight: bfd.firstHeight,
21944
+ prevHash,
21945
+ prevChainWork,
21946
+ lastHash: bfd.lastHash,
21947
+ lastChainWork: bfd.lastChainWork,
21948
+ chain: bfd.chain
21949
+ });
21950
+ bfd.data = result.data;
21951
+ bfd.fileHash = result.fileHash;
21952
+ bfd.lastHash = result.lastHeaderHash;
21953
+ bfd.lastChainWork = result.lastChainWork;
21954
+ return result.data;
21824
21955
  }
21825
21956
  async ReValidate() {
21826
21957
  return await this.lock.withReadLock(async () => await this.ReValidateNoLock());
@@ -22011,62 +22142,96 @@ var BulkFileDataManager = class BulkFileDataManager {
22011
22142
  this.ensureMaxRetained();
22012
22143
  return data;
22013
22144
  }
22145
+ const failed = this.failedLoads.get(key);
22146
+ if (failed != null) {
22147
+ if (Date.now() < failed.retryAt) {
22148
+ this.stats.loadBackoffs++;
22149
+ throw failed.error;
22150
+ }
22151
+ this.failedLoads.delete(key);
22152
+ }
22014
22153
  const load = this.loadAndValidateData(bfd);
22015
22154
  this.inFlightLoads.set(key, load);
22016
22155
  try {
22017
22156
  const data = await load;
22018
22157
  bfd.data = data;
22019
22158
  bfd.validated = true;
22159
+ this.failedLoads.delete(key);
22020
22160
  bfd.mru = Date.now();
22021
22161
  this.ensureMaxRetained();
22022
22162
  return data;
22163
+ } catch (error) {
22164
+ const resolved = error instanceof Error ? error : new Error(String(error));
22165
+ this.failedLoads.set(key, {
22166
+ retryAt: Date.now() + this.failedLoadRetryMsecs,
22167
+ error: resolved
22168
+ });
22169
+ throw resolved;
22023
22170
  } finally {
22024
22171
  if (this.inFlightLoads.get(key) === load) this.inFlightLoads.delete(key);
22025
22172
  }
22026
22173
  }
22027
22174
  async loadAndValidateData(bfd) {
22028
- if (this.storage != null && bfd.fileId) {
22029
- const stored = await this.storage.getBulkFileData(bfd.fileId);
22030
- if (stored == null) throw new WERR_INVALID_PARAMETER("fileId", `valid, data not found for fileId ${bfd.fileId}`);
22031
- this.validateRetrievedData(bfd, stored);
22032
- this.stats.storageHits++;
22033
- return stored;
22034
- }
22035
- if (this.cache != null) {
22036
- const cached = await this.cache.get(bfd);
22037
- if (cached != null) try {
22038
- this.validateRetrievedData(bfd, cached);
22039
- this.stats.persistentCacheHits++;
22040
- return cached;
22041
- } catch (error) {
22042
- this.stats.persistentCacheRejects++;
22043
- await this.cache.delete?.(bfd);
22044
- this.log(`Rejected corrupt bulk-header cache entry ${bfd.fileName}: ${String(error)}`);
22045
- }
22046
- else this.stats.persistentCacheMisses++;
22047
- }
22048
- if (this.fetch != null && bfd.sourceUrl) {
22049
- const expectedBytes = bfd.count * 80;
22050
- await this.downloadBudget?.consume(expectedBytes);
22051
- const url = this.fetch.pathJoin(bfd.sourceUrl, bfd.fileName);
22052
- const downloaded = await this.fetch.download(url, expectedBytes);
22053
- if (downloaded == null) throw new WERR_INVALID_PARAMETER("sourceUrl", `data not found for sourceUrl ${url}`);
22054
- this.validateRetrievedData(bfd, downloaded);
22055
- this.stats.downloads++;
22056
- this.stats.downloadedBytes += downloaded.length;
22057
- await this.cache?.set(bfd, downloaded);
22058
- return downloaded;
22059
- }
22175
+ const stored = await this.loadFromStorage(bfd);
22176
+ if (stored != null) return stored;
22177
+ const cached = await this.loadFromCache(bfd);
22178
+ if (cached != null) return cached;
22179
+ const downloaded = await this.loadFromRemote(bfd);
22180
+ if (downloaded != null) return downloaded;
22060
22181
  throw new WERR_INVALID_PARAMETER("data", `defined. Unable to retrieve data for ${bfd.fileName}`);
22061
22182
  }
22062
- validateRetrievedData(bfd, data) {
22063
- 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}`);
22064
- if (asString(Hash.sha256(asArray(data)), "base64") !== bfd.fileHash) throw new WERR_INVALID_PARAMETER("fileHash", `a match for retrieved data for ${bfd.fileName}`);
22183
+ async loadFromStorage(bfd) {
22184
+ if (this.storage == null || !bfd.fileId) return void 0;
22185
+ const stored = await this.storage.getBulkFileData(bfd.fileId);
22186
+ if (stored == null) throw new WERR_INVALID_PARAMETER("fileId", `valid, data not found for fileId ${bfd.fileId}`);
22187
+ const validated = await this.validateRetrievedData(bfd, stored);
22188
+ this.stats.storageHits++;
22189
+ return validated;
22190
+ }
22191
+ async loadFromCache(bfd) {
22192
+ if (this.cache == null) return void 0;
22193
+ const cached = await this.cache.get(bfd);
22194
+ if (cached == null) {
22195
+ this.stats.persistentCacheMisses++;
22196
+ return;
22197
+ }
22198
+ try {
22199
+ const validated = await this.validateRetrievedData(bfd, cached);
22200
+ this.stats.persistentCacheHits++;
22201
+ await this.cache.promoteValidated?.(bfd, validated);
22202
+ return validated;
22203
+ } catch (error) {
22204
+ if (!(error instanceof BulkFileDataValidationError)) throw error;
22205
+ this.stats.persistentCacheRejects++;
22206
+ let rejectedData = error.data;
22207
+ if (!(rejectedData instanceof Uint8Array) && cached.byteLength > 0) rejectedData = cached;
22208
+ await this.cache.quarantine?.(bfd, String(error), rejectedData);
22209
+ this.log(`Rejected corrupt bulk-header cache entry ${bfd.fileName}: ${String(error)}`);
22210
+ return;
22211
+ }
22212
+ }
22213
+ async loadFromRemote(bfd) {
22214
+ if (this.fetch == null || !bfd.sourceUrl) return void 0;
22215
+ const expectedBytes = bfd.count * 80;
22216
+ await this.downloadBudget?.consume(expectedBytes);
22217
+ const url = this.fetch.pathJoin(bfd.sourceUrl, bfd.fileName);
22218
+ const downloaded = await this.fetch.download(url, expectedBytes, { beforeRetry: async () => await this.downloadBudget?.consume(expectedBytes) });
22219
+ if (downloaded == null) throw new WERR_INVALID_PARAMETER("sourceUrl", `data not found for sourceUrl ${url}`);
22220
+ const validated = await this.validateRetrievedData(bfd, downloaded);
22221
+ this.stats.downloads++;
22222
+ this.stats.downloadedBytes += validated.length;
22223
+ await this.cache?.set(bfd, validated);
22224
+ return validated;
22225
+ }
22226
+ async validateRetrievedData(bfd, data, expectedFileHash = bfd.fileHash) {
22065
22227
  const candidate = {
22066
22228
  ...bfd,
22067
22229
  data
22068
22230
  };
22069
- this.validateBfdHeaders(candidate);
22231
+ const validated = await this.validateBfdHeaders(candidate, expectedFileHash);
22232
+ bfd.lastHash = candidate.lastHash;
22233
+ bfd.lastChainWork = candidate.lastChainWork;
22234
+ return validated;
22070
22235
  }
22071
22236
  ensureMaxRetained() {
22072
22237
  if (this.maxRetained === void 0) return;
@@ -22102,17 +22267,24 @@ var BulkFileDataManager = class BulkFileDataManager {
22102
22267
  i++;
22103
22268
  const data = await reader.read();
22104
22269
  if (data == null || data.length === 0) break;
22105
- const last = validateBufferOfHeaders(data, lastHeaderHash, 0, void 0, lastChainWork);
22106
- await toFs.writeFile(toPath(i), data);
22107
- const fileHash = asString(Hash.sha256(asArray(data)), "base64");
22270
+ const validated = await this.validator.validate({
22271
+ fileName: toFileName(i),
22272
+ data,
22273
+ count: data.length / 80,
22274
+ firstHeight,
22275
+ prevHash: lastHeaderHash,
22276
+ prevChainWork: lastChainWork,
22277
+ chain
22278
+ });
22279
+ await toFs.writeFile(toPath(i), validated.data);
22108
22280
  const file = {
22109
22281
  chain,
22110
- count: data.length / 80,
22111
- fileHash,
22282
+ count: validated.data.length / 80,
22283
+ fileHash: validated.fileHash,
22112
22284
  fileName: toFileName(i),
22113
22285
  firstHeight,
22114
- lastChainWork: last.lastChainWork,
22115
- lastHash: last.lastHeaderHash,
22286
+ lastChainWork: validated.lastChainWork,
22287
+ lastHash: validated.lastHeaderHash,
22116
22288
  prevChainWork: lastChainWork,
22117
22289
  prevHash: lastHeaderHash,
22118
22290
  sourceUrl
@@ -22124,7 +22296,16 @@ var BulkFileDataManager = class BulkFileDataManager {
22124
22296
  }
22125
22297
  await toFs.writeFile(toJsonPath(), asUint8Array(JSON.stringify(toBulkFiles), "utf8"));
22126
22298
  }
22299
+ async destroy() {
22300
+ await this.validator.destroy?.();
22301
+ }
22127
22302
  };
22303
+ function snapshotBfd(file) {
22304
+ return {
22305
+ ...file,
22306
+ data: file.data
22307
+ };
22308
+ }
22128
22309
  function selectBulkHeaderFiles(files, chain, maxPerFile) {
22129
22310
  const r = [];
22130
22311
  let height = 0;
@@ -26993,7 +27174,8 @@ function createDefaultBulkFileDataManager(params) {
26993
27174
  maxRetained: params.maxRetained,
26994
27175
  fromKnownSourceUrl: params.cdnUrl,
26995
27176
  cache: params.sources.bulkFileCache,
26996
- downloadBudget: params.sources.bulkFileDownloadBudget
27177
+ downloadBudget: params.sources.bulkFileDownloadBudget,
27178
+ validator: params.sources.bulkFileDataValidator
26997
27179
  });
26998
27180
  }
26999
27181
  function createDefaultChaintracksStorageOptions(params) {
@@ -27518,6 +27700,7 @@ var FixedWindowBulkFileDownloadBudget = class {
27518
27700
  return {
27519
27701
  maxBytes: this.maxBytes,
27520
27702
  consumedBytes: this.consumedBytes,
27703
+ remainingBytes: this.maxBytes - this.consumedBytes,
27521
27704
  windowStartedAt: this.windowStartedAt,
27522
27705
  windowMsecs: this.windowMsecs
27523
27706
  };
@@ -36781,6 +36964,6 @@ var WalletPermissionsManager = class WalletPermissionsManager {
36781
36964
  }
36782
36965
  };
36783
36966
  //#endregion
36784
- export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BRC153_REFERENCE_PREFIX, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkHeaderFile, BulkHeaderFileFs, BulkHeaderFileStorage, BulkHeaderFiles, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorChaintracks, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION, DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION, DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS, DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS, DEFAULT_MANAGED_CHANGE_TARGET_UTXOS, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, FixedWindowBulkFileDownloadBudget, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, LocalChainTracker, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, applyBrc153ReferenceLabel, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, buildChaintracksOptionsWithIngestors, convertProofToMerklePath, createAndStartDefaultChaintracks, createDefaultBulkFileDataManager, createDefaultChaintracksClient, createDefaultChaintracksStorageOptions, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, defaultManagedChangePolicy, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isBrc153ReferenceLabel, isLegacyManagedChangeBasketDefault, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, makeBrc153ReferenceLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseBrc153ReferenceLabel, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, resolveDefaultChaintracksArguments, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, startChaintracks, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toDefaultChaintracksArguments, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, upgradeLegacyManagedChangeBasketDefault, blockHeaderUtilities_exports as utils, validateManagedChangePolicy, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
36967
+ export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BRC153_REFERENCE_PREFIX, BulkFileDataManager, BulkFileDataReader, BulkFileDataValidationError, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkHeaderFile, BulkHeaderFileFs, BulkHeaderFileStorage, BulkHeaderFiles, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorChaintracks, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION, DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION, DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS, DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS, DEFAULT_MANAGED_CHANGE_TARGET_UTXOS, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, FixedWindowBulkFileDownloadBudget, GoChaintracksServiceClient, HeightRange, InlineBulkFileDataValidator, KDF_MAX_HASH_LENGTH, LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, LocalChainTracker, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, applyBrc153ReferenceLabel, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, buildChaintracksOptionsWithIngestors, convertProofToMerklePath, createAndStartDefaultChaintracks, createDefaultBulkFileDataManager, createDefaultChaintracksClient, createDefaultChaintracksStorageOptions, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, defaultManagedChangePolicy, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isBrc153ReferenceLabel, isLegacyManagedChangeBasketDefault, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, makeBrc153ReferenceLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseBrc153ReferenceLabel, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, resolveDefaultChaintracksArguments, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, startChaintracks, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toDefaultChaintracksArguments, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, upgradeLegacyManagedChangeBasketDefault, blockHeaderUtilities_exports as utils, validateManagedChangePolicy, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
36785
36968
 
36786
36969
  //# sourceMappingURL=index.client.mjs.map