@bsv/wallet-toolbox-client 2.5.0 → 2.6.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.
@@ -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
  };
@@ -21447,31 +21622,45 @@ var ServiceCollection = class ServiceCollection {
21447
21622
  //#endregion
21448
21623
  //#region ../src/services/networkConfig.ts
21449
21624
  /**
21450
- * Runtime service-endpoint configuration for the `tstn` (Teranode Scaling Test Net) network.
21625
+ * Runtime service-endpoint configuration for Teranode networks that do not
21626
+ * have a public, operator-independent service endpoint.
21451
21627
  *
21452
- * Unlike `main`, `test`, and `ttn`, the tstn service endpoints are not public and must not be
21628
+ * Unlike `main`, `test`, and `ttn`, the stn/tstn service endpoints are not public and must not be
21453
21629
  * hardcoded in this (public) source tree. They are supplied at runtime through environment
21454
21630
  * variables:
21455
21631
  *
21632
+ * STN_ARCADE_URL STN Arcade broadcaster / ARC endpoint base.
21633
+ * STN_CHAINTRACKS_URL STN ChainTracks service URL.
21456
21634
  * TSTN_ARCADE_URL Arcade broadcaster / ARC endpoint base. Also the fallback host for
21457
21635
  * ChainTracks when TSTN_CHAINTRACKS_URL is unset
21458
21636
  * (`${TSTN_ARCADE_URL}/chaintracks/v1`, mirroring the ttn layout).
21459
21637
  * TSTN_CHAINTRACKS_URL ChainTracks service URL.
21460
21638
  *
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
21639
+ * stn/tstn run only operator-configured Arcade and ChainTracks services; there is no
21640
+ * documented WhatsOnChain service for them, so no WhatsOnChain endpoint is configured and
21463
21641
  * the WhatsOnChain-only lookups (raw tx, utxo status, txid status, script-hash history) are not
21464
- * available on tstn.
21642
+ * available on stn/tstn.
21465
21643
  *
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.
21644
+ * `process` is accessed defensively so importing this module remains safe in
21645
+ * browser bundles. Browser applications can still supply an explicit
21646
+ * ChaintracksClientApi without relying on environment variables.
21469
21647
  */
21470
21648
  function readEnv(name) {
21471
21649
  const value = (typeof process !== "undefined" ? process.env : void 0)?.[name];
21472
21650
  return value != null && value.trim() !== "" ? value.trim() : void 0;
21473
21651
  }
21474
- const stripTrailingSlash = (url) => {
21652
+ /** Credential-free public Arcade host for supported networks. */
21653
+ function publicArcadeUrl(chain) {
21654
+ switch (chain) {
21655
+ case "main": return "https://arcade-v2-us-1.bsvblockchain.tech";
21656
+ case "test": return "https://arcade-v2-testnet-us-1.bsvblockchain.tech";
21657
+ case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech";
21658
+ case "stn":
21659
+ case "tstn":
21660
+ case "mock": return;
21661
+ }
21662
+ }
21663
+ const stripTrailingSlash$1 = (url) => {
21475
21664
  let end = url.length;
21476
21665
  while (end > 0 && url[end - 1] === "/") end--;
21477
21666
  return url.slice(0, end);
@@ -21480,6 +21669,10 @@ const stripTrailingSlash = (url) => {
21480
21669
  function tstnArcadeUrl() {
21481
21670
  return readEnv("TSTN_ARCADE_URL");
21482
21671
  }
21672
+ /** Arcade broadcaster / ARC endpoint for stn, or `undefined` when unset. */
21673
+ function stnArcadeUrl() {
21674
+ return readEnv("STN_ARCADE_URL");
21675
+ }
21483
21676
  /**
21484
21677
  * ChainTracks service URL for tstn. Falls back to `${TSTN_ARCADE_URL}/chaintracks/v1` when
21485
21678
  * `TSTN_CHAINTRACKS_URL` is unset (mirrors the ttn layout). Throws when neither is configured.
@@ -21488,20 +21681,53 @@ function tstnChaintracksUrl() {
21488
21681
  const explicit = readEnv("TSTN_CHAINTRACKS_URL");
21489
21682
  if (explicit != null) return explicit;
21490
21683
  const arcade = tstnArcadeUrl();
21491
- if (arcade != null) return `${stripTrailingSlash(arcade)}/chaintracks/v1`;
21684
+ if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
21492
21685
  throw new Error("tstn chain requires a ChainTracks URL: set TSTN_CHAINTRACKS_URL (or TSTN_ARCADE_URL) in the environment.");
21493
21686
  }
21687
+ /**
21688
+ * ChainTracks service URL for stn. Falls back to the configured Arcade host's
21689
+ * legacy-compatible path when STN_CHAINTRACKS_URL is unset.
21690
+ */
21691
+ function stnChaintracksUrl() {
21692
+ const explicit = readEnv("STN_CHAINTRACKS_URL");
21693
+ if (explicit != null) return explicit;
21694
+ const arcade = stnArcadeUrl();
21695
+ if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
21696
+ throw new Error("stn chain requires a ChainTracks URL: set STN_CHAINTRACKS_URL (or STN_ARCADE_URL) in the environment.");
21697
+ }
21494
21698
  //#endregion
21495
21699
  //#region ../src/services/createDefaultWalletServicesOptions.ts
21700
+ function stripTrailingSlash(value) {
21701
+ let end = value.length;
21702
+ while (end > 0 && value[end - 1] === "/") end--;
21703
+ return value.slice(0, end);
21704
+ }
21705
+ function configuredChaintracksClient(chain, serviceUrl) {
21706
+ let path = "";
21707
+ try {
21708
+ path = stripTrailingSlash(new URL(serviceUrl).pathname);
21709
+ } catch {}
21710
+ if (path.endsWith("/v2")) return new GoChaintracksServiceClient(chain, serviceUrl);
21711
+ return new ChaintracksServiceClient(chain, serviceUrl);
21712
+ }
21713
+ /**
21714
+ * Returns the credential-free default ChainTracks client for a supported
21715
+ * public network, or an operator-configured client for stn/tstn.
21716
+ */
21717
+ function createDefaultChaintracksClient(chain) {
21718
+ switch (chain) {
21719
+ case "main":
21720
+ case "test":
21721
+ case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
21722
+ case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
21723
+ case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
21724
+ }
21725
+ }
21496
21726
  function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallbackToken, taalArcApiKey, gorillaPoolArcApiKey, bitailsApiKey, deploymentId, chaintracks, arcadeUrl, arcadeApiKey, arcadeCallbackToken]) {
21497
21727
  if (chain === "mock") throw new Error("createDefaultWalletServicesOptions does not support 'mock' chain. Use MockServices directly.");
21498
21728
  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
21729
  const chaintracksFiatExchangeRatesUrl = "https://mainnet-chaintracks.babbage.systems/getFiatExchangeRates";
21504
- chaintracks ||= new ChaintracksServiceClient(chain, chaintracksUrl);
21730
+ chaintracks ||= createDefaultChaintracksClient(chain);
21505
21731
  const o = {
21506
21732
  chain,
21507
21733
  taalApiKey: void 0,
@@ -21559,14 +21785,15 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
21559
21785
  }
21560
21786
  /**
21561
21787
  * 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).
21788
+ * Returns undefined when no public default is known for the chain.
21563
21789
  */
21564
21790
  function arcadeDefaultUrl(chain) {
21565
21791
  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";
21792
+ case "main":
21793
+ case "test":
21794
+ case "ttn": return publicArcadeUrl(chain);
21795
+ case "stn": return stnArcadeUrl();
21568
21796
  case "tstn": return tstnArcadeUrl();
21569
- case "test": return;
21570
21797
  case "mock": return;
21571
21798
  }
21572
21799
  }
@@ -21574,6 +21801,7 @@ function arcDefaultUrl(chain) {
21574
21801
  switch (chain) {
21575
21802
  case "main": return "https://arc.taal.com";
21576
21803
  case "test": return "https://arc-test.taal.com";
21804
+ case "stn": return stnArcadeUrl() ?? "";
21577
21805
  case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech/";
21578
21806
  case "tstn": return tstnArcadeUrl() ?? "";
21579
21807
  case "mock": return "";
@@ -22955,7 +23183,7 @@ var Services = class Services {
22955
23183
  telemetry;
22956
23184
  constructor(optionsOrChain) {
22957
23185
  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.");
23186
+ if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'stn', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
22959
23187
  this.options = typeof optionsOrChain === "string" ? Services.createDefaultOptions(this.chain) : optionsOrChain;
22960
23188
  this.telemetry = new _bsv_sdk.Telemetry(this.options.telemetry);
22961
23189
  this.whatsonchain = new WhatsOnChain(this.chain, { apiKey: this.options.whatsOnChainApiKey }, this);
@@ -22969,7 +23197,7 @@ var Services = class Services {
22969
23197
  if (this.options.arcGorillaPoolUrl != null && this.options.arcGorillaPoolUrl !== "") this.arcGorillaPool = new ARC(this.options.arcGorillaPoolUrl, this.options.arcGorillaPoolConfig, "arcGorillaPool");
22970
23198
  if (this.options.arcadeUrl != null && this.options.arcadeUrl !== "") this.arcade = new Arcade(this.options.arcadeUrl, this.options.arcadeConfig, "arcade");
22971
23199
  const hasBitails = this.chain === "main" || this.chain === "test";
22972
- const hasWhatsOnChain = this.chain !== "tstn";
23200
+ const hasWhatsOnChain = this.chain === "main" || this.chain === "test";
22973
23201
  if (hasBitails) this.bitails = new Bitails(this.chain, { apiKey: this.options.bitailsApiKey });
22974
23202
  return {
22975
23203
  hasBitails,
@@ -23683,9 +23911,22 @@ function classifyMerklePathResponse(status, statusText, retry) {
23683
23911
  //#endregion
23684
23912
  //#region ../src/services/providers/WhatsOnChain.ts
23685
23913
  var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
23914
+ requestGate;
23686
23915
  constructor(chain = "main", config = {}) {
23687
23916
  if (chain === "mock") throw new Error("WhatsOnChain does not support 'mock' chain. Use MockServices directly.");
23688
23917
  super(chain, config);
23918
+ this.requestGate = config.requestGate;
23919
+ }
23920
+ async requestWithAnonymousAuthFallback(url, requestOptions) {
23921
+ await this.requestGate?.();
23922
+ const response = await this.httpClient.request(url, requestOptions);
23923
+ if (response.status !== 401 && response.status !== 403 || this.apiKey.trim() === "") return response;
23924
+ if (this.requestGate != null) await this.requestGate();
23925
+ else await wait(350);
23926
+ return await this.httpClient.request(url, {
23927
+ method: "GET",
23928
+ headers: { Accept: "application/json" }
23929
+ });
23689
23930
  }
23690
23931
  /**
23691
23932
  * POST
@@ -24094,7 +24335,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
24094
24335
  };
24095
24336
  const url = `${this.URL}/block/${hash}/header`;
24096
24337
  for (let retry = 0; retry < 2; retry++) {
24097
- const response = await this.httpClient.request(url, requestOptions);
24338
+ const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
24098
24339
  if (response.statusText === "Too Many Requests" && retry < 2) {
24099
24340
  await wait(2e3);
24100
24341
  continue;
@@ -24112,7 +24353,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
24112
24353
  };
24113
24354
  const url = `${this.URL}/chain/info`;
24114
24355
  for (let retry = 0; retry < 2; retry++) {
24115
- const response = await this.httpClient.request(url, requestOptions);
24356
+ const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
24116
24357
  if (response.statusText === "Too Many Requests" && retry < 2) {
24117
24358
  await wait(2e3);
24118
24359
  continue;
@@ -24298,12 +24539,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24298
24539
  timeout: 3e4,
24299
24540
  userAgent: "BabbageWhatsOnChainServices",
24300
24541
  enableCache: true,
24301
- chainInfoMsecs: 5e3
24542
+ chainInfoMsecs: 5e3,
24543
+ minRequestIntervalMsecs: 350
24302
24544
  };
24303
24545
  }
24304
24546
  static chainInfo = [];
24305
24547
  static chainInfoTime = [];
24306
24548
  static chainInfoMsecs = [];
24549
+ static chainInfoPromise = {};
24550
+ static requestTail = Promise.resolve();
24551
+ static nextRequestMsecs = 0;
24307
24552
  chain;
24308
24553
  woc;
24309
24554
  constructor(options) {
@@ -24312,7 +24557,8 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24312
24557
  apiKey: this.options.apiKey,
24313
24558
  timeout: this.options.timeout,
24314
24559
  userAgent: this.options.userAgent,
24315
- enableCache: this.options.enableCache
24560
+ enableCache: this.options.enableCache,
24561
+ requestGate: async () => await this.waitForRateLimit()
24316
24562
  };
24317
24563
  this.chain = options.chain;
24318
24564
  const chainInfoMsecs = WhatsOnChainServices.chainInfoMsecs;
@@ -24330,7 +24576,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24330
24576
  let update = chainInfo[this.chain] === void 0;
24331
24577
  if (!update && chainInfoTime[this.chain] !== void 0) update = now.getTime() - chainInfoTime[this.chain].getTime() > chainInfoMsecs[this.chain];
24332
24578
  if (update) {
24333
- chainInfo[this.chain] = await this.woc.getChainInfo();
24579
+ let pending = WhatsOnChainServices.chainInfoPromise[this.chain];
24580
+ if (pending == null) {
24581
+ pending = this.woc.getChainInfo();
24582
+ WhatsOnChainServices.chainInfoPromise[this.chain] = pending;
24583
+ }
24584
+ try {
24585
+ chainInfo[this.chain] = await pending;
24586
+ } finally {
24587
+ if (WhatsOnChainServices.chainInfoPromise[this.chain] === pending) delete WhatsOnChainServices.chainInfoPromise[this.chain];
24588
+ }
24334
24589
  chainInfoTime[this.chain] = now;
24335
24590
  }
24336
24591
  if (!chainInfo[this.chain]) throw new Error("Unexpected failure to update chainInfo.");
@@ -24348,10 +24603,12 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24348
24603
  */
24349
24604
  async getHeaders(fetch) {
24350
24605
  fetch ||= new ChaintracksFetch();
24606
+ await this.waitForRateLimit();
24351
24607
  return await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers`);
24352
24608
  }
24353
24609
  async getHeaderByteFileLinks(neededRange, fetch) {
24354
24610
  fetch ||= new ChaintracksFetch();
24611
+ await this.waitForRateLimit();
24355
24612
  const files = await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers/resources`);
24356
24613
  const r = [];
24357
24614
  let range;
@@ -24364,6 +24621,21 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24364
24621
  }
24365
24622
  return r;
24366
24623
  }
24624
+ async waitForRateLimit() {
24625
+ let release;
24626
+ const previous = WhatsOnChainServices.requestTail;
24627
+ WhatsOnChainServices.requestTail = new Promise((resolve) => {
24628
+ release = resolve;
24629
+ });
24630
+ await previous;
24631
+ try {
24632
+ const delay = Math.max(0, WhatsOnChainServices.nextRequestMsecs - Date.now());
24633
+ if (delay > 0) await wait(delay);
24634
+ WhatsOnChainServices.nextRequestMsecs = Date.now() + (this.options.minRequestIntervalMsecs ?? 350);
24635
+ } finally {
24636
+ release();
24637
+ }
24638
+ }
24367
24639
  };
24368
24640
  function wocGetHeadersHeaderToBlockHeader(h) {
24369
24641
  const bits = typeof h.bits === "string" ? Number.parseInt(h.bits, 16) : h.bits;
@@ -24424,6 +24696,51 @@ var BulkIngestorWhatsOnChainCdn = class extends BulkIngestorBase {
24424
24696
  }
24425
24697
  };
24426
24698
  //#endregion
24699
+ //#region ../src/services/chaintracker/chaintracks/Ingest/BulkIngestorChaintracks.ts
24700
+ /**
24701
+ * Uses a go-chaintracks/Arcade-compatible service as a validated bulk source.
24702
+ * Retrieved bytes still pass through ChainTracks' local serialization, hash,
24703
+ * continuity, and genesis checks before storage.
24704
+ */
24705
+ var BulkIngestorChaintracks = class extends BulkIngestorBase {
24706
+ chaintracks;
24707
+ maxHeadersPerRequest;
24708
+ networkChecked = false;
24709
+ constructor(options) {
24710
+ super(options);
24711
+ this.chaintracks = options.chaintracks;
24712
+ this.maxHeadersPerRequest = options.maxHeadersPerRequest ?? 1e3;
24713
+ if (!Number.isInteger(this.maxHeadersPerRequest) || this.maxHeadersPerRequest < 1) throw new Error("maxHeadersPerRequest must be a positive integer.");
24714
+ }
24715
+ async getPresentHeight() {
24716
+ await this.ensureNetwork();
24717
+ return await this.chaintracks.getPresentHeight();
24718
+ }
24719
+ async fetchHeaders(_before, fetchRange, bulkRange, priorLiveHeaders) {
24720
+ if (fetchRange.isEmpty) return priorLiveHeaders;
24721
+ await this.ensureNetwork();
24722
+ let liveHeaders = priorLiveHeaders;
24723
+ let height = fetchRange.minHeight;
24724
+ while (height <= fetchRange.maxHeight) {
24725
+ const requested = Math.min(this.maxHeadersPerRequest, fetchRange.maxHeight - height + 1);
24726
+ const bytes = asUint8Array(await this.chaintracks.getHeaders(height, requested));
24727
+ if (bytes.length === 0) throw new Error(`ChainTracks upstream returned no headers at height ${height}.`);
24728
+ if (bytes.length % 80 !== 0 || bytes.length > requested * 80) throw new Error(`ChainTracks upstream returned ${bytes.length} bytes for ${requested} headers at height ${height}.`);
24729
+ const headers = deserializeBlockHeaders(height, bytes);
24730
+ liveHeaders = await this.storage().addBulkHeaders(headers, bulkRange, liveHeaders);
24731
+ height += headers.length;
24732
+ if (headers.length < requested && height <= fetchRange.maxHeight) throw new Error(`ChainTracks upstream returned ${headers.length} of ${requested} headers at height ${height - headers.length}.`);
24733
+ }
24734
+ return liveHeaders;
24735
+ }
24736
+ async ensureNetwork() {
24737
+ if (this.networkChecked) return;
24738
+ const actual = await this.chaintracks.getChain();
24739
+ if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
24740
+ this.networkChecked = true;
24741
+ }
24742
+ };
24743
+ //#endregion
24427
24744
  //#region ../src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainPoll.ts
24428
24745
  /**
24429
24746
  * Reports new headers by polling periodically.
@@ -24530,9 +24847,17 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
24530
24847
  }
24531
24848
  async startListening(liveHeaders) {
24532
24849
  this.stopped = false;
24533
- this.subscriptionId = await this.options.chaintracks.subscribeHeaders((header) => {
24850
+ const actual = await this.options.chaintracks.getChain();
24851
+ if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
24852
+ if (this.stopped) return;
24853
+ const subscriptionId = await this.options.chaintracks.subscribeHeaders((header) => {
24534
24854
  if (!this.stopped) liveHeaders.push(header);
24535
24855
  });
24856
+ if (this.stopped) {
24857
+ await this.options.chaintracks.unsubscribe(subscriptionId);
24858
+ return;
24859
+ }
24860
+ this.subscriptionId = subscriptionId;
24536
24861
  await new Promise((resolve) => {
24537
24862
  this.resolveStopped = resolve;
24538
24863
  if (this.stopped) resolve();
@@ -24545,7 +24870,9 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
24545
24870
  if (subscriptionId != null) this.options.chaintracks.unsubscribe(subscriptionId).catch((e) => {
24546
24871
  this.log(`LiveIngestorChaintracksSSE unsubscribe failed: ${e}`);
24547
24872
  });
24548
- this.resolveStopped?.();
24873
+ const resolveStopped = this.resolveStopped;
24874
+ this.resolveStopped = void 0;
24875
+ resolveStopped?.();
24549
24876
  }
24550
24877
  async shutdown() {
24551
24878
  this.stopListening();
@@ -25241,6 +25568,27 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
25241
25568
  tipHeaderId: 0,
25242
25569
  hashToHeaderId: /* @__PURE__ */ new Map()
25243
25570
  };
25571
+ static stnData = {
25572
+ chain: "stn",
25573
+ liveHeaders: /* @__PURE__ */ new Map(),
25574
+ maxHeaderId: 0,
25575
+ tipHeaderId: 0,
25576
+ hashToHeaderId: /* @__PURE__ */ new Map()
25577
+ };
25578
+ static ttnData = {
25579
+ chain: "ttn",
25580
+ liveHeaders: /* @__PURE__ */ new Map(),
25581
+ maxHeaderId: 0,
25582
+ tipHeaderId: 0,
25583
+ hashToHeaderId: /* @__PURE__ */ new Map()
25584
+ };
25585
+ static tstnData = {
25586
+ chain: "tstn",
25587
+ liveHeaders: /* @__PURE__ */ new Map(),
25588
+ maxHeaderId: 0,
25589
+ tipHeaderId: 0,
25590
+ hashToHeaderId: /* @__PURE__ */ new Map()
25591
+ };
25244
25592
  constructor(options) {
25245
25593
  super(options);
25246
25594
  }
@@ -25248,10 +25596,11 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
25248
25596
  async getData() {
25249
25597
  switch (this.chain) {
25250
25598
  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.`);
25599
+ case "test": return ChaintracksStorageNoDb.testData;
25600
+ case "stn": return ChaintracksStorageNoDb.stnData;
25601
+ case "ttn": return ChaintracksStorageNoDb.ttnData;
25602
+ case "tstn": return ChaintracksStorageNoDb.tstnData;
25603
+ default: throw new WERR_INVALID_PARAMETER("chain", `'main', 'test', 'stn', 'ttn', or 'tstn'. '${this.chain}' is unsupported.`);
25255
25604
  }
25256
25605
  }
25257
25606
  async deleteLiveBlockHeaders() {
@@ -25840,7 +26189,7 @@ var ChaintracksStorageIdb = class extends ChaintracksStorageBase {
25840
26189
  //#endregion
25841
26190
  //#region ../src/services/chaintracker/chaintracks/configureChaintracksIngestors.ts
25842
26191
  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;
26192
+ 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
26193
  return {
25845
26194
  chain,
25846
26195
  whatsonchainApiKey,
@@ -25852,11 +26201,12 @@ function resolveDefaultChaintracksArguments(args) {
25852
26201
  reorgHeightThreshold,
25853
26202
  bulkMigrationChunkSize,
25854
26203
  batchInsertLimit,
25855
- addLiveRecursionLimit
26204
+ addLiveRecursionLimit,
26205
+ sources
25856
26206
  };
25857
26207
  }
25858
26208
  function toDefaultChaintracksArguments(params) {
25859
- return [
26209
+ const args = [
25860
26210
  params.chain,
25861
26211
  params.whatsonchainApiKey,
25862
26212
  params.maxPerFile,
@@ -25869,6 +26219,8 @@ function toDefaultChaintracksArguments(params) {
25869
26219
  params.batchInsertLimit,
25870
26220
  params.addLiveRecursionLimit
25871
26221
  ];
26222
+ if (Object.keys(params.sources).length > 0) args.push(params.sources);
26223
+ return args;
25872
26224
  }
25873
26225
  function createDefaultBulkFileDataManager(params) {
25874
26226
  return new BulkFileDataManager({
@@ -25911,7 +26263,7 @@ function createAndStartDefaultChaintracks(args, createOptions) {
25911
26263
  * The caller is responsible for providing the storage implementation.
25912
26264
  */
25913
26265
  function buildChaintracksOptionsWithIngestors(params, storage) {
25914
- const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit } = params;
26266
+ const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit, sources } = params;
25915
26267
  const co = {
25916
26268
  chain,
25917
26269
  storage,
@@ -25922,35 +26274,58 @@ function buildChaintracksOptionsWithIngestors(params, storage) {
25922
26274
  readonly: false
25923
26275
  };
25924
26276
  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));
26277
+ if (!sources.disableCdn && cdnUrl !== "") {
26278
+ const bulkCdnOptions = {
26279
+ chain,
26280
+ jsonResource,
26281
+ fetch,
26282
+ cdnUrl,
26283
+ maxPerFile
26284
+ };
26285
+ co.bulkIngestors.push(new BulkIngestorCDNBabbage(bulkCdnOptions));
26286
+ }
26287
+ const chaintracksSource = sources.chaintracks ?? (sources.disableChaintracks ? void 0 : createPublicChaintracksSource(chain));
26288
+ if (chaintracksSource != null) {
26289
+ co.bulkIngestors.push(new BulkIngestorChaintracks({
26290
+ chain,
26291
+ jsonResource,
26292
+ chaintracks: chaintracksSource,
26293
+ maxHeadersPerRequest: sources.remoteMaxHeadersPerRequest
26294
+ }));
26295
+ co.liveIngestors.push(new LiveIngestorChaintracksSSE({
26296
+ chain,
26297
+ chaintracks: chaintracksSource
26298
+ }));
26299
+ }
26300
+ if ((chain === "main" || chain === "test") && !sources.disableWhatsOnChain) {
26301
+ const wocOptions = {
26302
+ chain,
26303
+ apiKey: whatsonchainApiKey,
26304
+ timeout: 3e4,
26305
+ userAgent: "BabbageWhatsOnChainServices",
26306
+ enableCache: true,
26307
+ chainInfoMsecs: 5e3
26308
+ };
26309
+ const bulkOptions = {
26310
+ ...wocOptions,
26311
+ jsonResource,
26312
+ idleWait: 5e3
26313
+ };
26314
+ co.bulkIngestors.push(new BulkIngestorWhatsOnChainCdn(bulkOptions));
26315
+ const liveOptions = {
26316
+ ...wocOptions,
26317
+ idleWait: 1e5
26318
+ };
26319
+ co.liveIngestors.push(new LiveIngestorWhatsOnChainPoll(liveOptions));
26320
+ }
26321
+ 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
26322
  return co;
25953
26323
  }
26324
+ function createPublicChaintracksSource(chain) {
26325
+ const serviceUrl = publicArcadeUrl(chain);
26326
+ if (serviceUrl == null) return void 0;
26327
+ return new GoChaintracksServiceClient(chain, serviceUrl, { apiPrefix: "/chaintracks/v2" });
26328
+ }
25954
26329
  //#endregion
25955
26330
  //#region ../src/services/chaintracker/chaintracks/createDefaultNoDbChaintracksOptions.ts
25956
26331
  function createDefaultNoDbChaintracksOptions(...args) {
@@ -34546,6 +34921,7 @@ exports.BulkFilesReaderStorage = BulkFilesReaderStorage;
34546
34921
  exports.BulkIngestorBase = BulkIngestorBase;
34547
34922
  exports.BulkIngestorCDN = BulkIngestorCDN;
34548
34923
  exports.BulkIngestorCDNBabbage = BulkIngestorCDNBabbage;
34924
+ exports.BulkIngestorChaintracks = BulkIngestorChaintracks;
34549
34925
  exports.BulkIngestorWhatsOnChainCdn = BulkIngestorWhatsOnChainCdn;
34550
34926
  exports.BulkStorageBase = BulkStorageBase;
34551
34927
  exports.CWIStyleWalletManager = CWIStyleWalletManager;
@@ -34624,7 +35000,12 @@ exports.asBsvSdkTx = asBsvSdkTx;
34624
35000
  exports.asString = asString;
34625
35001
  exports.asUint8Array = asUint8Array;
34626
35002
  exports.brc29ProtocolID = brc29ProtocolID;
35003
+ exports.buildChaintracksOptionsWithIngestors = buildChaintracksOptionsWithIngestors;
34627
35004
  exports.convertProofToMerklePath = convertProofToMerklePath;
35005
+ exports.createAndStartDefaultChaintracks = createAndStartDefaultChaintracks;
35006
+ exports.createDefaultBulkFileDataManager = createDefaultBulkFileDataManager;
35007
+ exports.createDefaultChaintracksClient = createDefaultChaintracksClient;
35008
+ exports.createDefaultChaintracksStorageOptions = createDefaultChaintracksStorageOptions;
34628
35009
  exports.createDefaultIdbChaintracksOptions = createDefaultIdbChaintracksOptions;
34629
35010
  exports.createDefaultNoDbChaintracksOptions = createDefaultNoDbChaintracksOptions;
34630
35011
  exports.createDefaultWalletServicesOptions = createDefaultWalletServicesOptions;
@@ -34666,6 +35047,7 @@ exports.partitionActionLabels = partitionActionLabels;
34666
35047
  exports.randomBytes = randomBytes;
34667
35048
  exports.randomBytesBase64 = randomBytesBase64;
34668
35049
  exports.randomBytesHex = randomBytesHex;
35050
+ exports.resolveDefaultChaintracksArguments = resolveDefaultChaintracksArguments;
34669
35051
  Object.defineProperty(exports, "sdk", {
34670
35052
  enumerable: true,
34671
35053
  get: function() {
@@ -34676,9 +35058,11 @@ exports.selectBulkHeaderFiles = selectBulkHeaderFiles;
34676
35058
  exports.sha256Hash = sha256Hash;
34677
35059
  exports.stampLog = stampLog;
34678
35060
  exports.stampLogFormat = stampLogFormat;
35061
+ exports.startChaintracks = startChaintracks;
34679
35062
  exports.tableAuthSessionToPeerSession = tableAuthSessionToPeerSession;
34680
35063
  exports.throwDummyReviewActions = throwDummyReviewActions;
34681
35064
  exports.toBinaryBaseBlockHeader = toBinaryBaseBlockHeader;
35065
+ exports.toDefaultChaintracksArguments = toDefaultChaintracksArguments;
34682
35066
  exports.toLookupNetworkPreset = toLookupNetworkPreset;
34683
35067
  exports.toWalletNetwork = toWalletNetwork;
34684
35068
  exports.transactionColumnsWithoutRawTx = transactionColumnsWithoutRawTx;