@bsv/wallet-toolbox-client 2.5.0 → 2.6.1

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.
@@ -1005,6 +1005,7 @@ function toWalletNetwork(chain) {
1005
1005
  switch (chain) {
1006
1006
  case "main": return "mainnet";
1007
1007
  case "test":
1008
+ case "stn":
1008
1009
  case "ttn":
1009
1010
  case "tstn":
1010
1011
  case "mock": return "testnet";
@@ -1018,6 +1019,7 @@ function toLookupNetworkPreset(chain) {
1018
1019
  switch (chain) {
1019
1020
  case "main": return "mainnet";
1020
1021
  case "test": return "testnet";
1022
+ case "stn":
1021
1023
  case "ttn":
1022
1024
  case "tstn":
1023
1025
  case "mock": return "local";
@@ -11767,9 +11769,7 @@ function genesisHeader(chain) {
11767
11769
  height: 0,
11768
11770
  hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
11769
11771
  };
11770
- case "test":
11771
- case "ttn":
11772
- case "tstn": return {
11772
+ case "test": return {
11773
11773
  version: 1,
11774
11774
  previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
11775
11775
  merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
@@ -11779,6 +11779,36 @@ function genesisHeader(chain) {
11779
11779
  height: 0,
11780
11780
  hash: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
11781
11781
  };
11782
+ case "stn": return {
11783
+ version: 1,
11784
+ previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
11785
+ merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
11786
+ time: 1296688602,
11787
+ bits: 486604799,
11788
+ nonce: 173779992,
11789
+ height: 0,
11790
+ hash: "6b38bdbcd73a19f7889d23e1fa6166a9de71affceca60ca3bb1b28af8135c594"
11791
+ };
11792
+ case "ttn": return {
11793
+ version: 1,
11794
+ previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
11795
+ merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
11796
+ time: 1755606836,
11797
+ bits: 486604799,
11798
+ nonce: 1092578460,
11799
+ height: 0,
11800
+ hash: "000000000499eabba0a88f5b3747231c74b9191c1a4a04b2c2ea817976b7776d"
11801
+ };
11802
+ case "tstn": return {
11803
+ version: 1,
11804
+ previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
11805
+ merkleRoot: "64452e5b25c65e492ad6a4f5ce9f427ca986626c28315d88de920d66e28cc98f",
11806
+ time: 1782864e3,
11807
+ bits: 486604799,
11808
+ nonce: 1780488216,
11809
+ height: 0,
11810
+ hash: "000000005d221c0e023cb56b5682cf094f32cd959958b40bc931e5797cae706c"
11811
+ };
11782
11812
  case "mock": throw new Error("genesisHeader does not support 'mock' chain. Mock chain generates its own genesis block.");
11783
11813
  }
11784
11814
  }
@@ -19306,10 +19336,11 @@ var Chaintracks = class {
19306
19336
  startupError = null;
19307
19337
  subscriberCallbacksEnabled = false;
19308
19338
  stopMainThread = true;
19309
- lastPresentHeight = 0;
19339
+ lastPresentHeight = -1;
19310
19340
  lastPresentHeightMsecs = 0;
19311
19341
  lastPresentHeightMaxAge = 60 * 1e3;
19312
19342
  lock = new SingleWriterMultiReaderLock();
19343
+ sourceStatus = /* @__PURE__ */ new Map();
19313
19344
  constructor(options) {
19314
19345
  this.options = options;
19315
19346
  if (options.storage == null) throw new Error("storage is required.");
@@ -19320,6 +19351,22 @@ var Chaintracks = class {
19320
19351
  this.storage = options.storage;
19321
19352
  this.bulkIngestors = options.bulkIngestors;
19322
19353
  this.liveIngestors = options.liveIngestors;
19354
+ for (const [index, source] of this.bulkIngestors.entries()) {
19355
+ const name = this.sourceName("bulk", index, source);
19356
+ this.sourceStatus.set(name, {
19357
+ name,
19358
+ role: "bulk",
19359
+ state: "unknown"
19360
+ });
19361
+ }
19362
+ for (const [index, source] of this.liveIngestors.entries()) {
19363
+ const name = this.sourceName("live", index, source);
19364
+ this.sourceStatus.set(name, {
19365
+ name,
19366
+ role: "live",
19367
+ state: "unknown"
19368
+ });
19369
+ }
19323
19370
  this.addLiveRecursionLimit = options.addLiveRecursionLimit;
19324
19371
  if (options.logging != null) this.log = options.logging;
19325
19372
  this.storage.log = this.log;
@@ -19334,19 +19381,36 @@ var Chaintracks = class {
19334
19381
  */
19335
19382
  async getPresentHeight() {
19336
19383
  const now = Date.now();
19337
- if (this.lastPresentHeight && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
19338
- const presentHeights = [];
19339
- for (const bulk of this.bulkIngestors) try {
19340
- const presentHeight = await bulk.getPresentHeight();
19341
- if (presentHeight) presentHeights.push(presentHeight);
19342
- } catch (uerr) {
19343
- console.error(uerr);
19344
- }
19345
- const presentHeight = presentHeights.length > 0 ? Math.max(...presentHeights) : void 0;
19346
- if (!presentHeight) throw new Error("At least one bulk ingestor must implement getPresentHeight.");
19347
- this.lastPresentHeight = presentHeight;
19348
- this.lastPresentHeightMsecs = now;
19349
- return presentHeight;
19384
+ if (this.lastPresentHeight >= 0 && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
19385
+ for (const [index, bulk] of this.bulkIngestors.entries()) {
19386
+ const source = this.sourceName("bulk", index, bulk);
19387
+ try {
19388
+ const presentHeight = await bulk.getPresentHeight();
19389
+ if (presentHeight != null && Number.isInteger(presentHeight) && presentHeight >= 0) {
19390
+ this.markSourceSuccess(source, "bulk");
19391
+ this.lastPresentHeight = presentHeight;
19392
+ this.lastPresentHeightMsecs = now;
19393
+ return presentHeight;
19394
+ }
19395
+ } catch (uerr) {
19396
+ const error = WalletError.fromUnknown(uerr);
19397
+ this.markSourceFailure(source, "bulk", error);
19398
+ this.log(`Present-height source ${source} failed: ${error.message}`);
19399
+ }
19400
+ }
19401
+ if (this.lastPresentHeight >= 0) return this.lastPresentHeight;
19402
+ try {
19403
+ const ranges = await this.storage.getAvailableHeightRanges();
19404
+ const localHeight = Math.max(ranges.bulk.maxHeight, ranges.live.maxHeight);
19405
+ if (localHeight >= 0) {
19406
+ this.lastPresentHeight = localHeight;
19407
+ this.lastPresentHeightMsecs = now;
19408
+ return localHeight;
19409
+ }
19410
+ } catch (error) {
19411
+ this.log(`Unable to read the locally validated ChainTracks height: ${WalletError.fromUnknown(error).message}`);
19412
+ }
19413
+ throw new Error("No present-height source or locally validated headers are available.");
19350
19414
  }
19351
19415
  async currentHeight() {
19352
19416
  return await this.getPresentHeight();
@@ -19397,7 +19461,7 @@ var Chaintracks = class {
19397
19461
  for (const bulkIn of this.bulkIngestors) await bulkIn.setStorage(this.storage, this.log);
19398
19462
  for (const liveIn of this.liveIngestors) await liveIn.setStorage(this.storage, this.log);
19399
19463
  this.stopMainThread = false;
19400
- for (const liveIngestor of this.liveIngestors) this.promises.push(this.runLiveIngestor(liveIngestor));
19464
+ for (const [index, liveIngestor] of this.liveIngestors.entries()) this.promises.push(this.runLiveIngestor(liveIngestor, index));
19401
19465
  this.promises.push(this.mainThreadShiftLiveHeaders());
19402
19466
  while (!this.available && this.startupError == null) await wait(100);
19403
19467
  if (this.startupError != null) throw this.startupError;
@@ -19424,10 +19488,12 @@ var Chaintracks = class {
19424
19488
  async listening() {
19425
19489
  return await this.makeAvailable();
19426
19490
  }
19427
- async runLiveIngestor(liveIngestor) {
19491
+ async runLiveIngestor(liveIngestor, index) {
19428
19492
  let restartCount = 0;
19429
19493
  const name = liveIngestor.constructor.name;
19494
+ const source = this.sourceName("live", index, liveIngestor);
19430
19495
  while (!this.stopMainThread) try {
19496
+ this.markSourceSuccess(source, "live");
19431
19497
  await liveIngestor.startListening(this.liveHeaders);
19432
19498
  if (this.stopMainThread) return;
19433
19499
  restartCount++;
@@ -19438,6 +19504,7 @@ var Chaintracks = class {
19438
19504
  if (this.stopMainThread) return;
19439
19505
  restartCount++;
19440
19506
  const e = WalletError.fromUnknown(error_);
19507
+ this.markSourceFailure(source, "live", e);
19441
19508
  const waitMsecs = this.liveIngestorRestartWaitMsecs(restartCount);
19442
19509
  this.log(`Live ingestor ${name} failed restart=${restartCount} retryMsecs=${waitMsecs}: ${e.stack ?? e.message}`);
19443
19510
  await wait(waitMsecs);
@@ -19485,7 +19552,8 @@ var Chaintracks = class {
19485
19552
  storage: this.storage.constructor.name,
19486
19553
  bulkIngestors: this.bulkIngestors.map((bulkIngestor) => bulkIngestor.constructor.name),
19487
19554
  liveIngestors: this.liveIngestors.map((liveIngestor) => liveIngestor.constructor.name),
19488
- packages: []
19555
+ packages: [],
19556
+ sources: Array.from(this.sourceStatus.values()).map((status) => ({ ...status }))
19489
19557
  };
19490
19558
  }
19491
19559
  async getHeaders(height, count) {
@@ -19568,26 +19636,30 @@ var Chaintracks = class {
19568
19636
  let madeProgress = false;
19569
19637
  let hadSuccess = false;
19570
19638
  let done = false;
19571
- for (const bulk of this.bulkIngestors) try {
19572
- const beforeBulkMax = before.bulk.maxHeight;
19573
- const beforeLiveRange = HeightRange.from(newLiveHeaders);
19574
- const r = await bulk.synchronize(presentHeight, before, newLiveHeaders);
19575
- hadSuccess = true;
19576
- newLiveHeaders = r.liveHeaders;
19577
- after = await this.storage.getAvailableHeightRanges();
19578
- const added = after.bulk.above(before.bulk);
19579
- const afterLiveRange = HeightRange.from(newLiveHeaders);
19580
- if (after.bulk.maxHeight > beforeBulkMax || afterLiveRange.maxHeight > beforeLiveRange.maxHeight) madeProgress = true;
19581
- before = after;
19582
- this.log(`Bulk Ingestor: ${added.length} added with ${newLiveHeaders.length} live headers from ${bulk.constructor.name}`);
19583
- if (r.done) {
19584
- done = true;
19585
- break;
19639
+ for (const [index, bulk] of this.bulkIngestors.entries()) {
19640
+ const source = this.sourceName("bulk", index, bulk);
19641
+ try {
19642
+ const beforeBulkMax = before.bulk.maxHeight;
19643
+ const beforeLiveRange = HeightRange.from(newLiveHeaders);
19644
+ const r = await bulk.synchronize(presentHeight, before, newLiveHeaders);
19645
+ hadSuccess = true;
19646
+ this.markSourceSuccess(source, "bulk");
19647
+ newLiveHeaders = r.liveHeaders;
19648
+ after = await this.storage.getAvailableHeightRanges();
19649
+ const added = after.bulk.above(before.bulk);
19650
+ const afterLiveRange = HeightRange.from(newLiveHeaders);
19651
+ if (after.bulk.maxHeight > beforeBulkMax || afterLiveRange.maxHeight > beforeLiveRange.maxHeight) madeProgress = true;
19652
+ before = after;
19653
+ this.log(`Bulk Ingestor: ${added.length} added with ${newLiveHeaders.length} live headers from ${bulk.constructor.name}`);
19654
+ if (r.done) {
19655
+ done = true;
19656
+ break;
19657
+ }
19658
+ } catch (error_) {
19659
+ const e = bulkSyncError = WalletError.fromUnknown(error_);
19660
+ this.markSourceFailure(source, "bulk", e);
19661
+ this.log(`bulk sync error: ${e.message}`);
19586
19662
  }
19587
- } catch (error_) {
19588
- const e = bulkSyncError = WalletError.fromUnknown(error_);
19589
- this.log(`bulk sync error: ${e.message}`);
19590
- if (!this.available) break;
19591
19663
  }
19592
19664
  if (!this.available && bulkSyncError != null && !hadSuccess) this.startupError = bulkSyncError;
19593
19665
  return {
@@ -19597,10 +19669,41 @@ var Chaintracks = class {
19597
19669
  madeProgress
19598
19670
  };
19599
19671
  }
19672
+ sourceName(role, index, source) {
19673
+ return `${role}[${index}]:${source.constructor.name}`;
19674
+ }
19675
+ markSourceSuccess(name, role) {
19676
+ this.sourceStatus.set(name, {
19677
+ ...this.sourceStatus.get(name),
19678
+ name,
19679
+ role,
19680
+ state: "healthy",
19681
+ lastSuccess: (/* @__PURE__ */ new Date()).toISOString(),
19682
+ error: void 0
19683
+ });
19684
+ }
19685
+ markSourceFailure(name, role, error) {
19686
+ this.sourceStatus.set(name, {
19687
+ ...this.sourceStatus.get(name),
19688
+ name,
19689
+ role,
19690
+ state: "degraded",
19691
+ lastFailure: (/* @__PURE__ */ new Date()).toISOString(),
19692
+ error: error.message
19693
+ });
19694
+ }
19600
19695
  async getMissingBlockHeader(hash) {
19601
- for (const live of this.liveIngestors) {
19602
- const header = await live.getHeaderByHash(hash);
19603
- if (header != null) return header;
19696
+ for (const [index, live] of this.liveIngestors.entries()) {
19697
+ const source = this.sourceName("live", index, live);
19698
+ try {
19699
+ const header = await live.getHeaderByHash(hash);
19700
+ this.markSourceSuccess(source, "live");
19701
+ if (header != null) return header;
19702
+ } catch (error) {
19703
+ const resolved = WalletError.fromUnknown(error);
19704
+ this.markSourceFailure(source, "live", resolved);
19705
+ this.log(`Header lookup source ${source} failed: ${resolved.message}`);
19706
+ }
19604
19707
  }
19605
19708
  }
19606
19709
  invalidInsertHeaderResult(ihr) {
@@ -19933,6 +20036,9 @@ var GoChaintracksServiceClient = class {
19933
20036
  chain;
19934
20037
  baseUrl;
19935
20038
  fetcher;
20039
+ requestTimeoutMsecs;
20040
+ reconnectWaitMsecs;
20041
+ reconnectWaitMaxMsecs;
19936
20042
  subscriptions = /* @__PURE__ */ new Map();
19937
20043
  nextSubscriptionId = 1;
19938
20044
  constructor(chain, serviceUrl, options = {}) {
@@ -19946,6 +20052,15 @@ var GoChaintracksServiceClient = class {
19946
20052
  }
19947
20053
  this.baseUrl = `${base}${prefix}`;
19948
20054
  this.fetcher = options.fetch ?? fetch;
20055
+ this.requestTimeoutMsecs = options.requestTimeoutMsecs ?? 3e4;
20056
+ this.reconnectWaitMsecs = options.reconnectWaitMsecs ?? 1e3;
20057
+ this.reconnectWaitMaxMsecs = options.reconnectWaitMaxMsecs ?? 6e4;
20058
+ for (const [name, value] of [
20059
+ ["requestTimeoutMsecs", this.requestTimeoutMsecs],
20060
+ ["reconnectWaitMsecs", this.reconnectWaitMsecs],
20061
+ ["reconnectWaitMaxMsecs", this.reconnectWaitMaxMsecs]
20062
+ ]) if (!Number.isSafeInteger(value) || value < 1) throw new Error(`${name} must be a positive integer.`);
20063
+ if (this.reconnectWaitMaxMsecs < this.reconnectWaitMsecs) throw new Error("reconnectWaitMaxMsecs must be greater than or equal to reconnectWaitMsecs.");
19949
20064
  }
19950
20065
  async currentHeight() {
19951
20066
  return await this.getPresentHeight();
@@ -19955,12 +20070,8 @@ var GoChaintracksServiceClient = class {
19955
20070
  return h != null && root === asString(h.merkleRoot);
19956
20071
  }
19957
20072
  async getChain() {
19958
- try {
19959
- const r = await this.getJson("/network");
19960
- return this.normalizeChain(r.network);
19961
- } catch {
19962
- return this.chain;
19963
- }
20073
+ const r = await this.getJson("/network");
20074
+ return this.normalizeChain(typeof r === "string" ? r : r.network);
19964
20075
  }
19965
20076
  async getInfo() {
19966
20077
  const tip = await this.findChainTipHeader();
@@ -19975,11 +20086,11 @@ var GoChaintracksServiceClient = class {
19975
20086
  };
19976
20087
  }
19977
20088
  async getPresentHeight() {
19978
- return (await this.getJson("/height")).height;
20089
+ const result = await this.getJson("/height");
20090
+ return typeof result === "number" ? result : result.height;
19979
20091
  }
19980
20092
  async getHeaders(height, count) {
19981
- const bytes = await this.getBinary(`/headers.bin?height=${height}&count=${count}`);
19982
- return Buffer.from(bytes).toString("hex");
20093
+ return asString(await this.getBinary(`/headers.bin?height=${height}&count=${count}`));
19983
20094
  }
19984
20095
  async findChainTipHeader() {
19985
20096
  return await this.getJson("/tip");
@@ -20035,7 +20146,7 @@ var GoChaintracksServiceClient = class {
20035
20146
  async subscribe(type, path, onPayload) {
20036
20147
  const id = `${type}-${this.nextSubscriptionId++}`;
20037
20148
  const abort = new AbortController();
20038
- const done = this.runSse(path, abort.signal, onPayload);
20149
+ const done = this.runSseWithReconnect(path, abort.signal, onPayload);
20039
20150
  this.subscriptions.set(id, {
20040
20151
  id,
20041
20152
  type,
@@ -20047,30 +20158,75 @@ var GoChaintracksServiceClient = class {
20047
20158
  });
20048
20159
  return id;
20049
20160
  }
20050
- async runSse(path, signal, onPayload) {
20051
- const response = await this.fetcher(this.url(path), {
20052
- headers: { Accept: "text/event-stream" },
20053
- signal
20161
+ async runSseWithReconnect(path, signal, onPayload) {
20162
+ let failures = 0;
20163
+ while (!signal.aborted) {
20164
+ try {
20165
+ failures = await this.runSse(path, signal, onPayload) ? 0 : failures + 1;
20166
+ } catch {
20167
+ if (signal.aborted) return;
20168
+ failures++;
20169
+ }
20170
+ const multiplier = Math.min(2 ** Math.max(0, failures - 1), 64);
20171
+ const delay = Math.min(this.reconnectWaitMsecs * multiplier, this.reconnectWaitMaxMsecs);
20172
+ await this.waitForReconnect(delay, signal);
20173
+ }
20174
+ }
20175
+ async waitForReconnect(msecs, signal) {
20176
+ if (signal.aborted || msecs <= 0) return;
20177
+ await new Promise((resolve) => {
20178
+ let timeout;
20179
+ const onAbort = () => done();
20180
+ const done = () => {
20181
+ clearTimeout(timeout);
20182
+ signal.removeEventListener("abort", onAbort);
20183
+ resolve();
20184
+ };
20185
+ timeout = setTimeout(done, msecs);
20186
+ signal.addEventListener("abort", onAbort, { once: true });
20054
20187
  });
20055
- if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
20056
- if (response.body == null) throw new Error(`GET ${this.url(path)} returned no response body`);
20057
- const reader = response.body.getReader();
20058
- const decoder = new TextDecoder();
20059
- let buffer = "";
20188
+ }
20189
+ async runSse(path, signal, onPayload) {
20190
+ const controller = new AbortController();
20191
+ const onAbort = () => controller.abort();
20192
+ signal.addEventListener("abort", onAbort, { once: true });
20193
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMsecs);
20194
+ let receivedEvent = false;
20195
+ const observePayload = (payload) => {
20196
+ receivedEvent = true;
20197
+ onPayload(payload);
20198
+ };
20060
20199
  try {
20061
- for (;;) {
20062
- const { done, value } = await reader.read();
20063
- if (done) break;
20064
- buffer += decoder.decode(value, { stream: true });
20065
- buffer = this.processSseBuffer(buffer, onPayload);
20200
+ const response = await this.fetcher(this.url(path), {
20201
+ headers: { Accept: "text/event-stream" },
20202
+ signal: controller.signal
20203
+ });
20204
+ clearTimeout(timeout);
20205
+ if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
20206
+ if (response.body == null) throw new Error(`GET ${this.url(path)} returned no response body`);
20207
+ const reader = response.body.getReader();
20208
+ const decoder = new TextDecoder();
20209
+ let buffer = "";
20210
+ try {
20211
+ for (;;) {
20212
+ const { done, value } = await reader.read();
20213
+ if (done) break;
20214
+ buffer += decoder.decode(value, { stream: true });
20215
+ buffer = this.processSseBuffer(buffer, observePayload);
20216
+ }
20217
+ buffer += decoder.decode();
20218
+ this.processSseBuffer(`${buffer}\n\n`, observePayload);
20219
+ } finally {
20220
+ reader.releaseLock();
20066
20221
  }
20067
- buffer += decoder.decode();
20068
- this.processSseBuffer(`${buffer}\n\n`, onPayload);
20069
20222
  } finally {
20070
- reader.releaseLock();
20223
+ clearTimeout(timeout);
20224
+ signal.removeEventListener("abort", onAbort);
20071
20225
  }
20226
+ return receivedEvent;
20072
20227
  }
20073
20228
  processSseBuffer(buffer, onPayload) {
20229
+ buffer = buffer.replaceAll("\r\n", "\n");
20074
20230
  for (;;) {
20075
20231
  const boundary = buffer.indexOf("\n\n");
20076
20232
  if (boundary < 0) return buffer;
@@ -20089,32 +20245,51 @@ var GoChaintracksServiceClient = class {
20089
20245
  return r;
20090
20246
  }
20091
20247
  async getJsonOrUndefined(path) {
20092
- const response = await this.fetcher(this.url(path), { headers: { Accept: "application/json" } });
20248
+ const response = await this.fetchWithTimeout(this.url(path), { headers: { Accept: "application/json" } });
20093
20249
  if (response.status === 404) return void 0;
20094
20250
  if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
20095
- return await response.json();
20251
+ const value = await response.json();
20252
+ if (value != null && typeof value === "object" && "status" in value) {
20253
+ const envelope = value;
20254
+ if (envelope.status === "success") return envelope.value;
20255
+ if (envelope.status === "error") throw new Error(envelope.description ?? `GET ${this.url(path)} failed`);
20256
+ }
20257
+ return value;
20096
20258
  }
20097
20259
  async getBinary(path) {
20098
- const response = await this.fetcher(this.url(path), { headers: { Accept: "application/octet-stream" } });
20260
+ const response = await this.fetchWithTimeout(this.url(path), { headers: { Accept: "application/octet-stream" } });
20099
20261
  if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
20100
20262
  return new Uint8Array(await response.arrayBuffer());
20101
20263
  }
20264
+ async fetchWithTimeout(url, init) {
20265
+ const controller = new AbortController();
20266
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMsecs);
20267
+ try {
20268
+ return await this.fetcher(url, {
20269
+ ...init,
20270
+ signal: controller.signal
20271
+ });
20272
+ } finally {
20273
+ clearTimeout(timeout);
20274
+ }
20275
+ }
20102
20276
  url(path) {
20103
20277
  return `${this.baseUrl}${path}`;
20104
20278
  }
20105
20279
  normalizeChain(network) {
20106
- switch (network) {
20280
+ switch (network.trim().toLowerCase()) {
20107
20281
  case "main":
20108
20282
  case "mainnet": return "main";
20109
20283
  case "test":
20110
20284
  case "testnet": return "test";
20285
+ case "stn":
20286
+ case "scalingtestnet": return "stn";
20111
20287
  case "ttn":
20112
20288
  case "teratest":
20113
20289
  case "teratestnet": return "ttn";
20114
20290
  case "tstn":
20115
- case "teranodescalingtestnet":
20116
- case "scalingtestnet": return "tstn";
20117
- default: return this.chain;
20291
+ case "teranodescalingtestnet": return "tstn";
20292
+ default: throw new Error(`Unsupported ChainTracks upstream network '${network}'.`);
20118
20293
  }
20119
20294
  }
20120
20295
  };
@@ -20609,9 +20784,11 @@ var BulkFileDataManager = class BulkFileDataManager {
20609
20784
  const nextHeight = lbf != null ? lbf.firstHeight + lbf.count : 0;
20610
20785
  ({headers: newBulkHeaders, incrementalChainWork} = trimAlreadyStoredHeaders(newBulkHeaders, nextHeight, incrementalChainWork));
20611
20786
  if (newBulkHeaders.length === 0) return;
20612
- if (lbf == null || nextHeight !== newBulkHeaders[0].height) throw new WERR_INVALID_PARAMETER("newBulkHeaders", "an extension of existing bulk headers");
20613
- if (!lbf.lastHash) throw new WERR_INTERNAL(`lastHash is not defined for the last bulk file ${lbf.fileName}`);
20614
- const lastChainWork = incrementalChainWork ? addWork(incrementalChainWork, lbf.lastChainWork) : computeChainWorkFromHeaders(newBulkHeaders, lbf);
20787
+ if (nextHeight !== newBulkHeaders[0].height) throw new WERR_INVALID_PARAMETER("newBulkHeaders", "an extension of existing bulk headers");
20788
+ if (lbf != null && !lbf.lastHash) throw new WERR_INTERNAL(`lastHash is not defined for the last bulk file ${lbf.fileName}`);
20789
+ const prevChainWork = lbf?.lastChainWork ?? "00".repeat(32);
20790
+ const prevHash = lbf?.lastHash ?? "00".repeat(32);
20791
+ const lastChainWork = incrementalChainWork ? addWork(incrementalChainWork, prevChainWork) : computeChainWorkFromHeaders(newBulkHeaders, lbf);
20615
20792
  const data = serializeBaseBlockHeaders(newBulkHeaders);
20616
20793
  const fileHash = asString(_bsv_sdk.Hash.sha256(asArray(data)), "base64");
20617
20794
  const bf = {
@@ -20621,9 +20798,9 @@ var BulkFileDataManager = class BulkFileDataManager {
20621
20798
  fileName: "incremental",
20622
20799
  firstHeight: newBulkHeaders[0].height,
20623
20800
  count: newBulkHeaders.length,
20624
- prevChainWork: lbf.lastChainWork,
20801
+ prevChainWork,
20625
20802
  lastChainWork,
20626
- prevHash: lbf.lastHash,
20803
+ prevHash,
20627
20804
  lastHash: newBulkHeaders.at(-1).hash,
20628
20805
  fileHash,
20629
20806
  data
@@ -21053,12 +21230,13 @@ function trimAlreadyStoredHeaders(headers, nextHeight, incrementalChainWork) {
21053
21230
  }
21054
21231
  /**
21055
21232
  * Computes `lastChainWork` for a sequence of new bulk headers extending `lbf`,
21056
- * validating that the sequence is contiguous.
21233
+ * or beginning at genesis when bulk storage is empty, validating that the
21234
+ * sequence is contiguous.
21057
21235
  */
21058
21236
  function computeChainWorkFromHeaders(headers, lbf) {
21059
- let lastHeight = lbf.firstHeight + lbf.count - 1;
21060
- let lastHash = lbf.lastHash;
21061
- let lastChainWork = lbf.lastChainWork;
21237
+ let lastHeight = lbf != null ? lbf.firstHeight + lbf.count - 1 : -1;
21238
+ let lastHash = lbf?.lastHash ?? "00".repeat(32);
21239
+ let lastChainWork = lbf?.lastChainWork ?? "00".repeat(32);
21062
21240
  for (const h of headers) {
21063
21241
  if (h.height !== lastHeight + 1 || h.previousHash !== lastHash) throw new WERR_INVALID_PARAMETER("headers", `an extension of existing bulk headers, header with height ${h.height} is non-sequential`);
21064
21242
  lastChainWork = addWork(lastChainWork, convertBitsToWork(h.bits));
@@ -21447,31 +21625,45 @@ var ServiceCollection = class ServiceCollection {
21447
21625
  //#endregion
21448
21626
  //#region ../src/services/networkConfig.ts
21449
21627
  /**
21450
- * Runtime service-endpoint configuration for the `tstn` (Teranode Scaling Test Net) network.
21628
+ * Runtime service-endpoint configuration for Teranode networks that do not
21629
+ * have a public, operator-independent service endpoint.
21451
21630
  *
21452
- * Unlike `main`, `test`, and `ttn`, the tstn service endpoints are not public and must not be
21631
+ * Unlike `main`, `test`, and `ttn`, the stn/tstn service endpoints are not public and must not be
21453
21632
  * hardcoded in this (public) source tree. They are supplied at runtime through environment
21454
21633
  * variables:
21455
21634
  *
21635
+ * STN_ARCADE_URL STN Arcade broadcaster / ARC endpoint base.
21636
+ * STN_CHAINTRACKS_URL STN ChainTracks service URL.
21456
21637
  * TSTN_ARCADE_URL Arcade broadcaster / ARC endpoint base. Also the fallback host for
21457
21638
  * ChainTracks when TSTN_CHAINTRACKS_URL is unset
21458
21639
  * (`${TSTN_ARCADE_URL}/chaintracks/v1`, mirroring the ttn layout).
21459
21640
  * TSTN_CHAINTRACKS_URL ChainTracks service URL.
21460
21641
  *
21461
- * tstn runs only Arcade (broadcast + merkle proofs) and ChainTracks (headers); there is no
21462
- * WhatsOnChain / block-explorer service for tstn, so no WhatsOnChain endpoint is configured and
21642
+ * stn/tstn run only operator-configured Arcade and ChainTracks services; there is no
21643
+ * documented WhatsOnChain service for them, so no WhatsOnChain endpoint is configured and
21463
21644
  * the WhatsOnChain-only lookups (raw tx, utxo status, txid status, script-hash history) are not
21464
- * available on tstn.
21645
+ * available on stn/tstn.
21465
21646
  *
21466
- * `process` is accessed defensively so importing this module remains safe in browser bundles;
21467
- * tstn is a server-side network and these variables are only read when the selected chain is
21468
- * tstn.
21647
+ * `process` is accessed defensively so importing this module remains safe in
21648
+ * browser bundles. Browser applications can still supply an explicit
21649
+ * ChaintracksClientApi without relying on environment variables.
21469
21650
  */
21470
21651
  function readEnv(name) {
21471
21652
  const value = (typeof process !== "undefined" ? process.env : void 0)?.[name];
21472
21653
  return value != null && value.trim() !== "" ? value.trim() : void 0;
21473
21654
  }
21474
- const stripTrailingSlash = (url) => {
21655
+ /** Credential-free public Arcade host for supported networks. */
21656
+ function publicArcadeUrl(chain) {
21657
+ switch (chain) {
21658
+ case "main": return "https://arcade-v2-us-1.bsvblockchain.tech";
21659
+ case "test": return "https://arcade-v2-testnet-us-1.bsvblockchain.tech";
21660
+ case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech";
21661
+ case "stn":
21662
+ case "tstn":
21663
+ case "mock": return;
21664
+ }
21665
+ }
21666
+ const stripTrailingSlash$1 = (url) => {
21475
21667
  let end = url.length;
21476
21668
  while (end > 0 && url[end - 1] === "/") end--;
21477
21669
  return url.slice(0, end);
@@ -21480,6 +21672,10 @@ const stripTrailingSlash = (url) => {
21480
21672
  function tstnArcadeUrl() {
21481
21673
  return readEnv("TSTN_ARCADE_URL");
21482
21674
  }
21675
+ /** Arcade broadcaster / ARC endpoint for stn, or `undefined` when unset. */
21676
+ function stnArcadeUrl() {
21677
+ return readEnv("STN_ARCADE_URL");
21678
+ }
21483
21679
  /**
21484
21680
  * ChainTracks service URL for tstn. Falls back to `${TSTN_ARCADE_URL}/chaintracks/v1` when
21485
21681
  * `TSTN_CHAINTRACKS_URL` is unset (mirrors the ttn layout). Throws when neither is configured.
@@ -21488,20 +21684,53 @@ function tstnChaintracksUrl() {
21488
21684
  const explicit = readEnv("TSTN_CHAINTRACKS_URL");
21489
21685
  if (explicit != null) return explicit;
21490
21686
  const arcade = tstnArcadeUrl();
21491
- if (arcade != null) return `${stripTrailingSlash(arcade)}/chaintracks/v1`;
21687
+ if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
21492
21688
  throw new Error("tstn chain requires a ChainTracks URL: set TSTN_CHAINTRACKS_URL (or TSTN_ARCADE_URL) in the environment.");
21493
21689
  }
21690
+ /**
21691
+ * ChainTracks service URL for stn. Falls back to the configured Arcade host's
21692
+ * legacy-compatible path when STN_CHAINTRACKS_URL is unset.
21693
+ */
21694
+ function stnChaintracksUrl() {
21695
+ const explicit = readEnv("STN_CHAINTRACKS_URL");
21696
+ if (explicit != null) return explicit;
21697
+ const arcade = stnArcadeUrl();
21698
+ if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
21699
+ throw new Error("stn chain requires a ChainTracks URL: set STN_CHAINTRACKS_URL (or STN_ARCADE_URL) in the environment.");
21700
+ }
21494
21701
  //#endregion
21495
21702
  //#region ../src/services/createDefaultWalletServicesOptions.ts
21703
+ function stripTrailingSlash(value) {
21704
+ let end = value.length;
21705
+ while (end > 0 && value[end - 1] === "/") end--;
21706
+ return value.slice(0, end);
21707
+ }
21708
+ function configuredChaintracksClient(chain, serviceUrl) {
21709
+ let path = "";
21710
+ try {
21711
+ path = stripTrailingSlash(new URL(serviceUrl).pathname);
21712
+ } catch {}
21713
+ if (path.endsWith("/v2")) return new GoChaintracksServiceClient(chain, serviceUrl);
21714
+ return new ChaintracksServiceClient(chain, serviceUrl);
21715
+ }
21716
+ /**
21717
+ * Returns the credential-free default ChainTracks client for a supported
21718
+ * public network, or an operator-configured client for stn/tstn.
21719
+ */
21720
+ function createDefaultChaintracksClient(chain) {
21721
+ switch (chain) {
21722
+ case "main":
21723
+ case "test":
21724
+ case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
21725
+ case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
21726
+ case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
21727
+ }
21728
+ }
21496
21729
  function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallbackToken, taalArcApiKey, gorillaPoolArcApiKey, bitailsApiKey, deploymentId, chaintracks, arcadeUrl, arcadeApiKey, arcadeCallbackToken]) {
21497
21730
  if (chain === "mock") throw new Error("createDefaultWalletServicesOptions does not support 'mock' chain. Use MockServices directly.");
21498
21731
  deploymentId ||= `wallet-toolbox-${randomBytesHex(16)}`;
21499
- let chaintracksUrl;
21500
- if (chain === "ttn") chaintracksUrl = "https://arcade-v2-ttn-us-1.bsvblockchain.tech/chaintracks/v1";
21501
- else if (chain === "tstn") chaintracksUrl = tstnChaintracksUrl();
21502
- else chaintracksUrl = `https://${chain}net-chaintracks.babbage.systems`;
21503
21732
  const chaintracksFiatExchangeRatesUrl = "https://mainnet-chaintracks.babbage.systems/getFiatExchangeRates";
21504
- chaintracks ||= new ChaintracksServiceClient(chain, chaintracksUrl);
21733
+ chaintracks ||= createDefaultChaintracksClient(chain);
21505
21734
  const o = {
21506
21735
  chain,
21507
21736
  taalApiKey: void 0,
@@ -21559,14 +21788,15 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
21559
21788
  }
21560
21789
  /**
21561
21790
  * Default Arcade (bsv-blockchain/arcade) endpoint per chain.
21562
- * Returns undefined when no public default is known for the chain (e.g. testnet not yet deployed).
21791
+ * Returns undefined when no public default is known for the chain.
21563
21792
  */
21564
21793
  function arcadeDefaultUrl(chain) {
21565
21794
  switch (chain) {
21566
- case "main": return "https://arcade-v2-us-1.bsvblockchain.tech";
21567
- case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech";
21795
+ case "main":
21796
+ case "test":
21797
+ case "ttn": return publicArcadeUrl(chain);
21798
+ case "stn": return stnArcadeUrl();
21568
21799
  case "tstn": return tstnArcadeUrl();
21569
- case "test": return;
21570
21800
  case "mock": return;
21571
21801
  }
21572
21802
  }
@@ -21574,6 +21804,7 @@ function arcDefaultUrl(chain) {
21574
21804
  switch (chain) {
21575
21805
  case "main": return "https://arc.taal.com";
21576
21806
  case "test": return "https://arc-test.taal.com";
21807
+ case "stn": return stnArcadeUrl() ?? "";
21577
21808
  case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech/";
21578
21809
  case "tstn": return tstnArcadeUrl() ?? "";
21579
21810
  case "mock": return "";
@@ -22955,7 +23186,7 @@ var Services = class Services {
22955
23186
  telemetry;
22956
23187
  constructor(optionsOrChain) {
22957
23188
  this.chain = typeof optionsOrChain === "string" ? optionsOrChain : optionsOrChain.chain;
22958
- if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
23189
+ if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'stn', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
22959
23190
  this.options = typeof optionsOrChain === "string" ? Services.createDefaultOptions(this.chain) : optionsOrChain;
22960
23191
  this.telemetry = new _bsv_sdk.Telemetry(this.options.telemetry);
22961
23192
  this.whatsonchain = new WhatsOnChain(this.chain, { apiKey: this.options.whatsOnChainApiKey }, this);
@@ -22969,7 +23200,7 @@ var Services = class Services {
22969
23200
  if (this.options.arcGorillaPoolUrl != null && this.options.arcGorillaPoolUrl !== "") this.arcGorillaPool = new ARC(this.options.arcGorillaPoolUrl, this.options.arcGorillaPoolConfig, "arcGorillaPool");
22970
23201
  if (this.options.arcadeUrl != null && this.options.arcadeUrl !== "") this.arcade = new Arcade(this.options.arcadeUrl, this.options.arcadeConfig, "arcade");
22971
23202
  const hasBitails = this.chain === "main" || this.chain === "test";
22972
- const hasWhatsOnChain = this.chain !== "tstn";
23203
+ const hasWhatsOnChain = this.chain === "main" || this.chain === "test";
22973
23204
  if (hasBitails) this.bitails = new Bitails(this.chain, { apiKey: this.options.bitailsApiKey });
22974
23205
  return {
22975
23206
  hasBitails,
@@ -23683,9 +23914,22 @@ function classifyMerklePathResponse(status, statusText, retry) {
23683
23914
  //#endregion
23684
23915
  //#region ../src/services/providers/WhatsOnChain.ts
23685
23916
  var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
23917
+ requestGate;
23686
23918
  constructor(chain = "main", config = {}) {
23687
23919
  if (chain === "mock") throw new Error("WhatsOnChain does not support 'mock' chain. Use MockServices directly.");
23688
23920
  super(chain, config);
23921
+ this.requestGate = config.requestGate;
23922
+ }
23923
+ async requestWithAnonymousAuthFallback(url, requestOptions) {
23924
+ await this.requestGate?.();
23925
+ const response = await this.httpClient.request(url, requestOptions);
23926
+ if (response.status !== 401 && response.status !== 403 || this.apiKey.trim() === "") return response;
23927
+ if (this.requestGate != null) await this.requestGate();
23928
+ else await wait(350);
23929
+ return await this.httpClient.request(url, {
23930
+ method: "GET",
23931
+ headers: { Accept: "application/json" }
23932
+ });
23689
23933
  }
23690
23934
  /**
23691
23935
  * POST
@@ -24094,7 +24338,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
24094
24338
  };
24095
24339
  const url = `${this.URL}/block/${hash}/header`;
24096
24340
  for (let retry = 0; retry < 2; retry++) {
24097
- const response = await this.httpClient.request(url, requestOptions);
24341
+ const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
24098
24342
  if (response.statusText === "Too Many Requests" && retry < 2) {
24099
24343
  await wait(2e3);
24100
24344
  continue;
@@ -24112,7 +24356,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
24112
24356
  };
24113
24357
  const url = `${this.URL}/chain/info`;
24114
24358
  for (let retry = 0; retry < 2; retry++) {
24115
- const response = await this.httpClient.request(url, requestOptions);
24359
+ const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
24116
24360
  if (response.statusText === "Too Many Requests" && retry < 2) {
24117
24361
  await wait(2e3);
24118
24362
  continue;
@@ -24298,12 +24542,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24298
24542
  timeout: 3e4,
24299
24543
  userAgent: "BabbageWhatsOnChainServices",
24300
24544
  enableCache: true,
24301
- chainInfoMsecs: 5e3
24545
+ chainInfoMsecs: 5e3,
24546
+ minRequestIntervalMsecs: 350
24302
24547
  };
24303
24548
  }
24304
24549
  static chainInfo = [];
24305
24550
  static chainInfoTime = [];
24306
24551
  static chainInfoMsecs = [];
24552
+ static chainInfoPromise = {};
24553
+ static requestTail = Promise.resolve();
24554
+ static nextRequestMsecs = 0;
24307
24555
  chain;
24308
24556
  woc;
24309
24557
  constructor(options) {
@@ -24312,7 +24560,8 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24312
24560
  apiKey: this.options.apiKey,
24313
24561
  timeout: this.options.timeout,
24314
24562
  userAgent: this.options.userAgent,
24315
- enableCache: this.options.enableCache
24563
+ enableCache: this.options.enableCache,
24564
+ requestGate: async () => await this.waitForRateLimit()
24316
24565
  };
24317
24566
  this.chain = options.chain;
24318
24567
  const chainInfoMsecs = WhatsOnChainServices.chainInfoMsecs;
@@ -24330,7 +24579,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24330
24579
  let update = chainInfo[this.chain] === void 0;
24331
24580
  if (!update && chainInfoTime[this.chain] !== void 0) update = now.getTime() - chainInfoTime[this.chain].getTime() > chainInfoMsecs[this.chain];
24332
24581
  if (update) {
24333
- chainInfo[this.chain] = await this.woc.getChainInfo();
24582
+ let pending = WhatsOnChainServices.chainInfoPromise[this.chain];
24583
+ if (pending == null) {
24584
+ pending = this.woc.getChainInfo();
24585
+ WhatsOnChainServices.chainInfoPromise[this.chain] = pending;
24586
+ }
24587
+ try {
24588
+ chainInfo[this.chain] = await pending;
24589
+ } finally {
24590
+ if (WhatsOnChainServices.chainInfoPromise[this.chain] === pending) delete WhatsOnChainServices.chainInfoPromise[this.chain];
24591
+ }
24334
24592
  chainInfoTime[this.chain] = now;
24335
24593
  }
24336
24594
  if (!chainInfo[this.chain]) throw new Error("Unexpected failure to update chainInfo.");
@@ -24348,10 +24606,12 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24348
24606
  */
24349
24607
  async getHeaders(fetch) {
24350
24608
  fetch ||= new ChaintracksFetch();
24609
+ await this.waitForRateLimit();
24351
24610
  return await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers`);
24352
24611
  }
24353
24612
  async getHeaderByteFileLinks(neededRange, fetch) {
24354
24613
  fetch ||= new ChaintracksFetch();
24614
+ await this.waitForRateLimit();
24355
24615
  const files = await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers/resources`);
24356
24616
  const r = [];
24357
24617
  let range;
@@ -24364,6 +24624,21 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24364
24624
  }
24365
24625
  return r;
24366
24626
  }
24627
+ async waitForRateLimit() {
24628
+ let release;
24629
+ const previous = WhatsOnChainServices.requestTail;
24630
+ WhatsOnChainServices.requestTail = new Promise((resolve) => {
24631
+ release = resolve;
24632
+ });
24633
+ await previous;
24634
+ try {
24635
+ const delay = Math.max(0, WhatsOnChainServices.nextRequestMsecs - Date.now());
24636
+ if (delay > 0) await wait(delay);
24637
+ WhatsOnChainServices.nextRequestMsecs = Date.now() + (this.options.minRequestIntervalMsecs ?? 350);
24638
+ } finally {
24639
+ release();
24640
+ }
24641
+ }
24367
24642
  };
24368
24643
  function wocGetHeadersHeaderToBlockHeader(h) {
24369
24644
  const bits = typeof h.bits === "string" ? Number.parseInt(h.bits, 16) : h.bits;
@@ -24424,6 +24699,51 @@ var BulkIngestorWhatsOnChainCdn = class extends BulkIngestorBase {
24424
24699
  }
24425
24700
  };
24426
24701
  //#endregion
24702
+ //#region ../src/services/chaintracker/chaintracks/Ingest/BulkIngestorChaintracks.ts
24703
+ /**
24704
+ * Uses a go-chaintracks/Arcade-compatible service as a validated bulk source.
24705
+ * Retrieved bytes still pass through ChainTracks' local serialization, hash,
24706
+ * continuity, and genesis checks before storage.
24707
+ */
24708
+ var BulkIngestorChaintracks = class extends BulkIngestorBase {
24709
+ chaintracks;
24710
+ maxHeadersPerRequest;
24711
+ networkChecked = false;
24712
+ constructor(options) {
24713
+ super(options);
24714
+ this.chaintracks = options.chaintracks;
24715
+ this.maxHeadersPerRequest = options.maxHeadersPerRequest ?? 1e3;
24716
+ if (!Number.isInteger(this.maxHeadersPerRequest) || this.maxHeadersPerRequest < 1) throw new Error("maxHeadersPerRequest must be a positive integer.");
24717
+ }
24718
+ async getPresentHeight() {
24719
+ await this.ensureNetwork();
24720
+ return await this.chaintracks.getPresentHeight();
24721
+ }
24722
+ async fetchHeaders(_before, fetchRange, bulkRange, priorLiveHeaders) {
24723
+ if (fetchRange.isEmpty) return priorLiveHeaders;
24724
+ await this.ensureNetwork();
24725
+ let liveHeaders = priorLiveHeaders;
24726
+ let height = fetchRange.minHeight;
24727
+ while (height <= fetchRange.maxHeight) {
24728
+ const requested = Math.min(this.maxHeadersPerRequest, fetchRange.maxHeight - height + 1);
24729
+ const bytes = asUint8Array(await this.chaintracks.getHeaders(height, requested));
24730
+ if (bytes.length === 0) throw new Error(`ChainTracks upstream returned no headers at height ${height}.`);
24731
+ if (bytes.length % 80 !== 0 || bytes.length > requested * 80) throw new Error(`ChainTracks upstream returned ${bytes.length} bytes for ${requested} headers at height ${height}.`);
24732
+ const headers = deserializeBlockHeaders(height, bytes);
24733
+ liveHeaders = await this.storage().addBulkHeaders(headers, bulkRange, liveHeaders);
24734
+ height += headers.length;
24735
+ if (headers.length < requested && height <= fetchRange.maxHeight) throw new Error(`ChainTracks upstream returned ${headers.length} of ${requested} headers at height ${height - headers.length}.`);
24736
+ }
24737
+ return liveHeaders;
24738
+ }
24739
+ async ensureNetwork() {
24740
+ if (this.networkChecked) return;
24741
+ const actual = await this.chaintracks.getChain();
24742
+ if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
24743
+ this.networkChecked = true;
24744
+ }
24745
+ };
24746
+ //#endregion
24427
24747
  //#region ../src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainPoll.ts
24428
24748
  /**
24429
24749
  * Reports new headers by polling periodically.
@@ -24530,9 +24850,17 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
24530
24850
  }
24531
24851
  async startListening(liveHeaders) {
24532
24852
  this.stopped = false;
24533
- this.subscriptionId = await this.options.chaintracks.subscribeHeaders((header) => {
24853
+ const actual = await this.options.chaintracks.getChain();
24854
+ if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
24855
+ if (this.stopped) return;
24856
+ const subscriptionId = await this.options.chaintracks.subscribeHeaders((header) => {
24534
24857
  if (!this.stopped) liveHeaders.push(header);
24535
24858
  });
24859
+ if (this.stopped) {
24860
+ await this.options.chaintracks.unsubscribe(subscriptionId);
24861
+ return;
24862
+ }
24863
+ this.subscriptionId = subscriptionId;
24536
24864
  await new Promise((resolve) => {
24537
24865
  this.resolveStopped = resolve;
24538
24866
  if (this.stopped) resolve();
@@ -24545,7 +24873,9 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
24545
24873
  if (subscriptionId != null) this.options.chaintracks.unsubscribe(subscriptionId).catch((e) => {
24546
24874
  this.log(`LiveIngestorChaintracksSSE unsubscribe failed: ${e}`);
24547
24875
  });
24548
- this.resolveStopped?.();
24876
+ const resolveStopped = this.resolveStopped;
24877
+ this.resolveStopped = void 0;
24878
+ resolveStopped?.();
24549
24879
  }
24550
24880
  async shutdown() {
24551
24881
  this.stopListening();
@@ -25241,6 +25571,27 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
25241
25571
  tipHeaderId: 0,
25242
25572
  hashToHeaderId: /* @__PURE__ */ new Map()
25243
25573
  };
25574
+ static stnData = {
25575
+ chain: "stn",
25576
+ liveHeaders: /* @__PURE__ */ new Map(),
25577
+ maxHeaderId: 0,
25578
+ tipHeaderId: 0,
25579
+ hashToHeaderId: /* @__PURE__ */ new Map()
25580
+ };
25581
+ static ttnData = {
25582
+ chain: "ttn",
25583
+ liveHeaders: /* @__PURE__ */ new Map(),
25584
+ maxHeaderId: 0,
25585
+ tipHeaderId: 0,
25586
+ hashToHeaderId: /* @__PURE__ */ new Map()
25587
+ };
25588
+ static tstnData = {
25589
+ chain: "tstn",
25590
+ liveHeaders: /* @__PURE__ */ new Map(),
25591
+ maxHeaderId: 0,
25592
+ tipHeaderId: 0,
25593
+ hashToHeaderId: /* @__PURE__ */ new Map()
25594
+ };
25244
25595
  constructor(options) {
25245
25596
  super(options);
25246
25597
  }
@@ -25248,10 +25599,11 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
25248
25599
  async getData() {
25249
25600
  switch (this.chain) {
25250
25601
  case "main": return ChaintracksStorageNoDb.mainData;
25251
- case "test":
25252
- case "ttn":
25253
- case "tstn": return ChaintracksStorageNoDb.testData;
25254
- default: throw new WERR_INVALID_PARAMETER("chain", `'main', 'test', 'ttn', or 'tstn'. '${this.chain}' is unsupported.`);
25602
+ case "test": return ChaintracksStorageNoDb.testData;
25603
+ case "stn": return ChaintracksStorageNoDb.stnData;
25604
+ case "ttn": return ChaintracksStorageNoDb.ttnData;
25605
+ case "tstn": return ChaintracksStorageNoDb.tstnData;
25606
+ default: throw new WERR_INVALID_PARAMETER("chain", `'main', 'test', 'stn', 'ttn', or 'tstn'. '${this.chain}' is unsupported.`);
25255
25607
  }
25256
25608
  }
25257
25609
  async deleteLiveBlockHeaders() {
@@ -25840,7 +26192,7 @@ var ChaintracksStorageIdb = class extends ChaintracksStorageBase {
25840
26192
  //#endregion
25841
26193
  //#region ../src/services/chaintracker/chaintracks/configureChaintracksIngestors.ts
25842
26194
  function resolveDefaultChaintracksArguments(args) {
25843
- const [chain, whatsonchainApiKey = "", maxPerFile = 1e5, maxRetained = 2, fetch = new ChaintracksFetch(), cdnUrl = "https://cdn.projectbabbage.com/blockheaders/", liveHeightThreshold = 2e3, reorgHeightThreshold = 400, bulkMigrationChunkSize = 500, batchInsertLimit = 400, addLiveRecursionLimit = 36] = args;
26195
+ const [chain, whatsonchainApiKey = "", maxPerFile = 1e5, maxRetained = 2, fetch = new ChaintracksFetch(), cdnUrl = chain === "main" || chain === "test" ? "https://cdn.projectbabbage.com/blockheaders/" : "", liveHeightThreshold = 2e3, reorgHeightThreshold = 400, bulkMigrationChunkSize = 500, batchInsertLimit = 400, addLiveRecursionLimit = 36, sources = {}] = args;
25844
26196
  return {
25845
26197
  chain,
25846
26198
  whatsonchainApiKey,
@@ -25852,11 +26204,12 @@ function resolveDefaultChaintracksArguments(args) {
25852
26204
  reorgHeightThreshold,
25853
26205
  bulkMigrationChunkSize,
25854
26206
  batchInsertLimit,
25855
- addLiveRecursionLimit
26207
+ addLiveRecursionLimit,
26208
+ sources
25856
26209
  };
25857
26210
  }
25858
26211
  function toDefaultChaintracksArguments(params) {
25859
- return [
26212
+ const args = [
25860
26213
  params.chain,
25861
26214
  params.whatsonchainApiKey,
25862
26215
  params.maxPerFile,
@@ -25869,6 +26222,8 @@ function toDefaultChaintracksArguments(params) {
25869
26222
  params.batchInsertLimit,
25870
26223
  params.addLiveRecursionLimit
25871
26224
  ];
26225
+ if (Object.keys(params.sources).length > 0) args.push(params.sources);
26226
+ return args;
25872
26227
  }
25873
26228
  function createDefaultBulkFileDataManager(params) {
25874
26229
  return new BulkFileDataManager({
@@ -25911,7 +26266,7 @@ function createAndStartDefaultChaintracks(args, createOptions) {
25911
26266
  * The caller is responsible for providing the storage implementation.
25912
26267
  */
25913
26268
  function buildChaintracksOptionsWithIngestors(params, storage) {
25914
- const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit } = params;
26269
+ const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit, sources } = params;
25915
26270
  const co = {
25916
26271
  chain,
25917
26272
  storage,
@@ -25922,35 +26277,58 @@ function buildChaintracksOptionsWithIngestors(params, storage) {
25922
26277
  readonly: false
25923
26278
  };
25924
26279
  const jsonResource = `${chain}NetBlockHeaders.json`;
25925
- const bulkCdnOptions = {
25926
- chain,
25927
- jsonResource,
25928
- fetch,
25929
- cdnUrl,
25930
- maxPerFile
25931
- };
25932
- co.bulkIngestors.push(new BulkIngestorCDNBabbage(bulkCdnOptions));
25933
- const wocOptions = {
25934
- chain,
25935
- apiKey: whatsonchainApiKey,
25936
- timeout: 3e4,
25937
- userAgent: "BabbageWhatsOnChainServices",
25938
- enableCache: true,
25939
- chainInfoMsecs: 5e3
25940
- };
25941
- const bulkOptions = {
25942
- ...wocOptions,
25943
- jsonResource,
25944
- idleWait: 5e3
25945
- };
25946
- co.bulkIngestors.push(new BulkIngestorWhatsOnChainCdn(bulkOptions));
25947
- const liveOptions = {
25948
- ...wocOptions,
25949
- idleWait: 1e5
25950
- };
25951
- co.liveIngestors.push(new LiveIngestorWhatsOnChainPoll(liveOptions));
26280
+ if (!sources.disableCdn && cdnUrl !== "") {
26281
+ const bulkCdnOptions = {
26282
+ chain,
26283
+ jsonResource,
26284
+ fetch,
26285
+ cdnUrl,
26286
+ maxPerFile
26287
+ };
26288
+ co.bulkIngestors.push(new BulkIngestorCDNBabbage(bulkCdnOptions));
26289
+ }
26290
+ const chaintracksSource = sources.chaintracks ?? (sources.disableChaintracks ? void 0 : createPublicChaintracksSource(chain));
26291
+ if (chaintracksSource != null) {
26292
+ co.bulkIngestors.push(new BulkIngestorChaintracks({
26293
+ chain,
26294
+ jsonResource,
26295
+ chaintracks: chaintracksSource,
26296
+ maxHeadersPerRequest: sources.remoteMaxHeadersPerRequest
26297
+ }));
26298
+ co.liveIngestors.push(new LiveIngestorChaintracksSSE({
26299
+ chain,
26300
+ chaintracks: chaintracksSource
26301
+ }));
26302
+ }
26303
+ if ((chain === "main" || chain === "test") && !sources.disableWhatsOnChain) {
26304
+ const wocOptions = {
26305
+ chain,
26306
+ apiKey: whatsonchainApiKey,
26307
+ timeout: 3e4,
26308
+ userAgent: "BabbageWhatsOnChainServices",
26309
+ enableCache: true,
26310
+ chainInfoMsecs: 5e3
26311
+ };
26312
+ const bulkOptions = {
26313
+ ...wocOptions,
26314
+ jsonResource,
26315
+ idleWait: 5e3
26316
+ };
26317
+ co.bulkIngestors.push(new BulkIngestorWhatsOnChainCdn(bulkOptions));
26318
+ const liveOptions = {
26319
+ ...wocOptions,
26320
+ idleWait: 1e5
26321
+ };
26322
+ co.liveIngestors.push(new LiveIngestorWhatsOnChainPoll(liveOptions));
26323
+ }
26324
+ if (co.bulkIngestors.length === 0 || co.liveIngestors.length === 0) throw new Error(`ChainTracks ${chain} requires at least one bulk and live source. Configure sources.chaintracks for Teranode networks.`);
25952
26325
  return co;
25953
26326
  }
26327
+ function createPublicChaintracksSource(chain) {
26328
+ const serviceUrl = publicArcadeUrl(chain);
26329
+ if (serviceUrl == null) return void 0;
26330
+ return new GoChaintracksServiceClient(chain, serviceUrl, { apiPrefix: "/chaintracks/v2" });
26331
+ }
25954
26332
  //#endregion
25955
26333
  //#region ../src/services/chaintracker/chaintracks/createDefaultNoDbChaintracksOptions.ts
25956
26334
  function createDefaultNoDbChaintracksOptions(...args) {
@@ -34546,6 +34924,7 @@ exports.BulkFilesReaderStorage = BulkFilesReaderStorage;
34546
34924
  exports.BulkIngestorBase = BulkIngestorBase;
34547
34925
  exports.BulkIngestorCDN = BulkIngestorCDN;
34548
34926
  exports.BulkIngestorCDNBabbage = BulkIngestorCDNBabbage;
34927
+ exports.BulkIngestorChaintracks = BulkIngestorChaintracks;
34549
34928
  exports.BulkIngestorWhatsOnChainCdn = BulkIngestorWhatsOnChainCdn;
34550
34929
  exports.BulkStorageBase = BulkStorageBase;
34551
34930
  exports.CWIStyleWalletManager = CWIStyleWalletManager;
@@ -34624,7 +35003,12 @@ exports.asBsvSdkTx = asBsvSdkTx;
34624
35003
  exports.asString = asString;
34625
35004
  exports.asUint8Array = asUint8Array;
34626
35005
  exports.brc29ProtocolID = brc29ProtocolID;
35006
+ exports.buildChaintracksOptionsWithIngestors = buildChaintracksOptionsWithIngestors;
34627
35007
  exports.convertProofToMerklePath = convertProofToMerklePath;
35008
+ exports.createAndStartDefaultChaintracks = createAndStartDefaultChaintracks;
35009
+ exports.createDefaultBulkFileDataManager = createDefaultBulkFileDataManager;
35010
+ exports.createDefaultChaintracksClient = createDefaultChaintracksClient;
35011
+ exports.createDefaultChaintracksStorageOptions = createDefaultChaintracksStorageOptions;
34628
35012
  exports.createDefaultIdbChaintracksOptions = createDefaultIdbChaintracksOptions;
34629
35013
  exports.createDefaultNoDbChaintracksOptions = createDefaultNoDbChaintracksOptions;
34630
35014
  exports.createDefaultWalletServicesOptions = createDefaultWalletServicesOptions;
@@ -34666,6 +35050,7 @@ exports.partitionActionLabels = partitionActionLabels;
34666
35050
  exports.randomBytes = randomBytes;
34667
35051
  exports.randomBytesBase64 = randomBytesBase64;
34668
35052
  exports.randomBytesHex = randomBytesHex;
35053
+ exports.resolveDefaultChaintracksArguments = resolveDefaultChaintracksArguments;
34669
35054
  Object.defineProperty(exports, "sdk", {
34670
35055
  enumerable: true,
34671
35056
  get: function() {
@@ -34676,9 +35061,11 @@ exports.selectBulkHeaderFiles = selectBulkHeaderFiles;
34676
35061
  exports.sha256Hash = sha256Hash;
34677
35062
  exports.stampLog = stampLog;
34678
35063
  exports.stampLogFormat = stampLogFormat;
35064
+ exports.startChaintracks = startChaintracks;
34679
35065
  exports.tableAuthSessionToPeerSession = tableAuthSessionToPeerSession;
34680
35066
  exports.throwDummyReviewActions = throwDummyReviewActions;
34681
35067
  exports.toBinaryBaseBlockHeader = toBinaryBaseBlockHeader;
35068
+ exports.toDefaultChaintracksArguments = toDefaultChaintracksArguments;
34682
35069
  exports.toLookupNetworkPreset = toLookupNetworkPreset;
34683
35070
  exports.toWalletNetwork = toWalletNetwork;
34684
35071
  exports.transactionColumnsWithoutRawTx = transactionColumnsWithoutRawTx;