@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.
@@ -970,6 +970,7 @@ function toWalletNetwork(chain) {
970
970
  switch (chain) {
971
971
  case "main": return "mainnet";
972
972
  case "test":
973
+ case "stn":
973
974
  case "ttn":
974
975
  case "tstn":
975
976
  case "mock": return "testnet";
@@ -983,6 +984,7 @@ function toLookupNetworkPreset(chain) {
983
984
  switch (chain) {
984
985
  case "main": return "mainnet";
985
986
  case "test": return "testnet";
987
+ case "stn":
986
988
  case "ttn":
987
989
  case "tstn":
988
990
  case "mock": return "local";
@@ -11732,9 +11734,7 @@ function genesisHeader(chain) {
11732
11734
  height: 0,
11733
11735
  hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
11734
11736
  };
11735
- case "test":
11736
- case "ttn":
11737
- case "tstn": return {
11737
+ case "test": return {
11738
11738
  version: 1,
11739
11739
  previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
11740
11740
  merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
@@ -11744,6 +11744,36 @@ function genesisHeader(chain) {
11744
11744
  height: 0,
11745
11745
  hash: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
11746
11746
  };
11747
+ case "stn": return {
11748
+ version: 1,
11749
+ previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
11750
+ merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
11751
+ time: 1296688602,
11752
+ bits: 486604799,
11753
+ nonce: 173779992,
11754
+ height: 0,
11755
+ hash: "6b38bdbcd73a19f7889d23e1fa6166a9de71affceca60ca3bb1b28af8135c594"
11756
+ };
11757
+ case "ttn": return {
11758
+ version: 1,
11759
+ previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
11760
+ merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
11761
+ time: 1755606836,
11762
+ bits: 486604799,
11763
+ nonce: 1092578460,
11764
+ height: 0,
11765
+ hash: "000000000499eabba0a88f5b3747231c74b9191c1a4a04b2c2ea817976b7776d"
11766
+ };
11767
+ case "tstn": return {
11768
+ version: 1,
11769
+ previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
11770
+ merkleRoot: "64452e5b25c65e492ad6a4f5ce9f427ca986626c28315d88de920d66e28cc98f",
11771
+ time: 1782864e3,
11772
+ bits: 486604799,
11773
+ nonce: 1780488216,
11774
+ height: 0,
11775
+ hash: "000000005d221c0e023cb56b5682cf094f32cd959958b40bc931e5797cae706c"
11776
+ };
11747
11777
  case "mock": throw new Error("genesisHeader does not support 'mock' chain. Mock chain generates its own genesis block.");
11748
11778
  }
11749
11779
  }
@@ -19271,10 +19301,11 @@ var Chaintracks = class {
19271
19301
  startupError = null;
19272
19302
  subscriberCallbacksEnabled = false;
19273
19303
  stopMainThread = true;
19274
- lastPresentHeight = 0;
19304
+ lastPresentHeight = -1;
19275
19305
  lastPresentHeightMsecs = 0;
19276
19306
  lastPresentHeightMaxAge = 60 * 1e3;
19277
19307
  lock = new SingleWriterMultiReaderLock();
19308
+ sourceStatus = /* @__PURE__ */ new Map();
19278
19309
  constructor(options) {
19279
19310
  this.options = options;
19280
19311
  if (options.storage == null) throw new Error("storage is required.");
@@ -19285,6 +19316,22 @@ var Chaintracks = class {
19285
19316
  this.storage = options.storage;
19286
19317
  this.bulkIngestors = options.bulkIngestors;
19287
19318
  this.liveIngestors = options.liveIngestors;
19319
+ for (const [index, source] of this.bulkIngestors.entries()) {
19320
+ const name = this.sourceName("bulk", index, source);
19321
+ this.sourceStatus.set(name, {
19322
+ name,
19323
+ role: "bulk",
19324
+ state: "unknown"
19325
+ });
19326
+ }
19327
+ for (const [index, source] of this.liveIngestors.entries()) {
19328
+ const name = this.sourceName("live", index, source);
19329
+ this.sourceStatus.set(name, {
19330
+ name,
19331
+ role: "live",
19332
+ state: "unknown"
19333
+ });
19334
+ }
19288
19335
  this.addLiveRecursionLimit = options.addLiveRecursionLimit;
19289
19336
  if (options.logging != null) this.log = options.logging;
19290
19337
  this.storage.log = this.log;
@@ -19299,19 +19346,36 @@ var Chaintracks = class {
19299
19346
  */
19300
19347
  async getPresentHeight() {
19301
19348
  const now = Date.now();
19302
- if (this.lastPresentHeight && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
19303
- const presentHeights = [];
19304
- for (const bulk of this.bulkIngestors) try {
19305
- const presentHeight = await bulk.getPresentHeight();
19306
- if (presentHeight) presentHeights.push(presentHeight);
19307
- } catch (uerr) {
19308
- console.error(uerr);
19309
- }
19310
- const presentHeight = presentHeights.length > 0 ? Math.max(...presentHeights) : void 0;
19311
- if (!presentHeight) throw new Error("At least one bulk ingestor must implement getPresentHeight.");
19312
- this.lastPresentHeight = presentHeight;
19313
- this.lastPresentHeightMsecs = now;
19314
- return presentHeight;
19349
+ if (this.lastPresentHeight >= 0 && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
19350
+ for (const [index, bulk] of this.bulkIngestors.entries()) {
19351
+ const source = this.sourceName("bulk", index, bulk);
19352
+ try {
19353
+ const presentHeight = await bulk.getPresentHeight();
19354
+ if (presentHeight != null && Number.isInteger(presentHeight) && presentHeight >= 0) {
19355
+ this.markSourceSuccess(source, "bulk");
19356
+ this.lastPresentHeight = presentHeight;
19357
+ this.lastPresentHeightMsecs = now;
19358
+ return presentHeight;
19359
+ }
19360
+ } catch (uerr) {
19361
+ const error = WalletError.fromUnknown(uerr);
19362
+ this.markSourceFailure(source, "bulk", error);
19363
+ this.log(`Present-height source ${source} failed: ${error.message}`);
19364
+ }
19365
+ }
19366
+ if (this.lastPresentHeight >= 0) return this.lastPresentHeight;
19367
+ try {
19368
+ const ranges = await this.storage.getAvailableHeightRanges();
19369
+ const localHeight = Math.max(ranges.bulk.maxHeight, ranges.live.maxHeight);
19370
+ if (localHeight >= 0) {
19371
+ this.lastPresentHeight = localHeight;
19372
+ this.lastPresentHeightMsecs = now;
19373
+ return localHeight;
19374
+ }
19375
+ } catch (error) {
19376
+ this.log(`Unable to read the locally validated ChainTracks height: ${WalletError.fromUnknown(error).message}`);
19377
+ }
19378
+ throw new Error("No present-height source or locally validated headers are available.");
19315
19379
  }
19316
19380
  async currentHeight() {
19317
19381
  return await this.getPresentHeight();
@@ -19362,7 +19426,7 @@ var Chaintracks = class {
19362
19426
  for (const bulkIn of this.bulkIngestors) await bulkIn.setStorage(this.storage, this.log);
19363
19427
  for (const liveIn of this.liveIngestors) await liveIn.setStorage(this.storage, this.log);
19364
19428
  this.stopMainThread = false;
19365
- for (const liveIngestor of this.liveIngestors) this.promises.push(this.runLiveIngestor(liveIngestor));
19429
+ for (const [index, liveIngestor] of this.liveIngestors.entries()) this.promises.push(this.runLiveIngestor(liveIngestor, index));
19366
19430
  this.promises.push(this.mainThreadShiftLiveHeaders());
19367
19431
  while (!this.available && this.startupError == null) await wait(100);
19368
19432
  if (this.startupError != null) throw this.startupError;
@@ -19389,10 +19453,12 @@ var Chaintracks = class {
19389
19453
  async listening() {
19390
19454
  return await this.makeAvailable();
19391
19455
  }
19392
- async runLiveIngestor(liveIngestor) {
19456
+ async runLiveIngestor(liveIngestor, index) {
19393
19457
  let restartCount = 0;
19394
19458
  const name = liveIngestor.constructor.name;
19459
+ const source = this.sourceName("live", index, liveIngestor);
19395
19460
  while (!this.stopMainThread) try {
19461
+ this.markSourceSuccess(source, "live");
19396
19462
  await liveIngestor.startListening(this.liveHeaders);
19397
19463
  if (this.stopMainThread) return;
19398
19464
  restartCount++;
@@ -19403,6 +19469,7 @@ var Chaintracks = class {
19403
19469
  if (this.stopMainThread) return;
19404
19470
  restartCount++;
19405
19471
  const e = WalletError.fromUnknown(error_);
19472
+ this.markSourceFailure(source, "live", e);
19406
19473
  const waitMsecs = this.liveIngestorRestartWaitMsecs(restartCount);
19407
19474
  this.log(`Live ingestor ${name} failed restart=${restartCount} retryMsecs=${waitMsecs}: ${e.stack ?? e.message}`);
19408
19475
  await wait(waitMsecs);
@@ -19450,7 +19517,8 @@ var Chaintracks = class {
19450
19517
  storage: this.storage.constructor.name,
19451
19518
  bulkIngestors: this.bulkIngestors.map((bulkIngestor) => bulkIngestor.constructor.name),
19452
19519
  liveIngestors: this.liveIngestors.map((liveIngestor) => liveIngestor.constructor.name),
19453
- packages: []
19520
+ packages: [],
19521
+ sources: Array.from(this.sourceStatus.values()).map((status) => ({ ...status }))
19454
19522
  };
19455
19523
  }
19456
19524
  async getHeaders(height, count) {
@@ -19533,26 +19601,30 @@ var Chaintracks = class {
19533
19601
  let madeProgress = false;
19534
19602
  let hadSuccess = false;
19535
19603
  let done = false;
19536
- for (const bulk of this.bulkIngestors) try {
19537
- const beforeBulkMax = before.bulk.maxHeight;
19538
- const beforeLiveRange = HeightRange.from(newLiveHeaders);
19539
- const r = await bulk.synchronize(presentHeight, before, newLiveHeaders);
19540
- hadSuccess = true;
19541
- newLiveHeaders = r.liveHeaders;
19542
- after = await this.storage.getAvailableHeightRanges();
19543
- const added = after.bulk.above(before.bulk);
19544
- const afterLiveRange = HeightRange.from(newLiveHeaders);
19545
- if (after.bulk.maxHeight > beforeBulkMax || afterLiveRange.maxHeight > beforeLiveRange.maxHeight) madeProgress = true;
19546
- before = after;
19547
- this.log(`Bulk Ingestor: ${added.length} added with ${newLiveHeaders.length} live headers from ${bulk.constructor.name}`);
19548
- if (r.done) {
19549
- done = true;
19550
- break;
19604
+ for (const [index, bulk] of this.bulkIngestors.entries()) {
19605
+ const source = this.sourceName("bulk", index, bulk);
19606
+ try {
19607
+ const beforeBulkMax = before.bulk.maxHeight;
19608
+ const beforeLiveRange = HeightRange.from(newLiveHeaders);
19609
+ const r = await bulk.synchronize(presentHeight, before, newLiveHeaders);
19610
+ hadSuccess = true;
19611
+ this.markSourceSuccess(source, "bulk");
19612
+ newLiveHeaders = r.liveHeaders;
19613
+ after = await this.storage.getAvailableHeightRanges();
19614
+ const added = after.bulk.above(before.bulk);
19615
+ const afterLiveRange = HeightRange.from(newLiveHeaders);
19616
+ if (after.bulk.maxHeight > beforeBulkMax || afterLiveRange.maxHeight > beforeLiveRange.maxHeight) madeProgress = true;
19617
+ before = after;
19618
+ this.log(`Bulk Ingestor: ${added.length} added with ${newLiveHeaders.length} live headers from ${bulk.constructor.name}`);
19619
+ if (r.done) {
19620
+ done = true;
19621
+ break;
19622
+ }
19623
+ } catch (error_) {
19624
+ const e = bulkSyncError = WalletError.fromUnknown(error_);
19625
+ this.markSourceFailure(source, "bulk", e);
19626
+ this.log(`bulk sync error: ${e.message}`);
19551
19627
  }
19552
- } catch (error_) {
19553
- const e = bulkSyncError = WalletError.fromUnknown(error_);
19554
- this.log(`bulk sync error: ${e.message}`);
19555
- if (!this.available) break;
19556
19628
  }
19557
19629
  if (!this.available && bulkSyncError != null && !hadSuccess) this.startupError = bulkSyncError;
19558
19630
  return {
@@ -19562,10 +19634,41 @@ var Chaintracks = class {
19562
19634
  madeProgress
19563
19635
  };
19564
19636
  }
19637
+ sourceName(role, index, source) {
19638
+ return `${role}[${index}]:${source.constructor.name}`;
19639
+ }
19640
+ markSourceSuccess(name, role) {
19641
+ this.sourceStatus.set(name, {
19642
+ ...this.sourceStatus.get(name),
19643
+ name,
19644
+ role,
19645
+ state: "healthy",
19646
+ lastSuccess: (/* @__PURE__ */ new Date()).toISOString(),
19647
+ error: void 0
19648
+ });
19649
+ }
19650
+ markSourceFailure(name, role, error) {
19651
+ this.sourceStatus.set(name, {
19652
+ ...this.sourceStatus.get(name),
19653
+ name,
19654
+ role,
19655
+ state: "degraded",
19656
+ lastFailure: (/* @__PURE__ */ new Date()).toISOString(),
19657
+ error: error.message
19658
+ });
19659
+ }
19565
19660
  async getMissingBlockHeader(hash) {
19566
- for (const live of this.liveIngestors) {
19567
- const header = await live.getHeaderByHash(hash);
19568
- if (header != null) return header;
19661
+ for (const [index, live] of this.liveIngestors.entries()) {
19662
+ const source = this.sourceName("live", index, live);
19663
+ try {
19664
+ const header = await live.getHeaderByHash(hash);
19665
+ this.markSourceSuccess(source, "live");
19666
+ if (header != null) return header;
19667
+ } catch (error) {
19668
+ const resolved = WalletError.fromUnknown(error);
19669
+ this.markSourceFailure(source, "live", resolved);
19670
+ this.log(`Header lookup source ${source} failed: ${resolved.message}`);
19671
+ }
19569
19672
  }
19570
19673
  }
19571
19674
  invalidInsertHeaderResult(ihr) {
@@ -19898,6 +20001,9 @@ var GoChaintracksServiceClient = class {
19898
20001
  chain;
19899
20002
  baseUrl;
19900
20003
  fetcher;
20004
+ requestTimeoutMsecs;
20005
+ reconnectWaitMsecs;
20006
+ reconnectWaitMaxMsecs;
19901
20007
  subscriptions = /* @__PURE__ */ new Map();
19902
20008
  nextSubscriptionId = 1;
19903
20009
  constructor(chain, serviceUrl, options = {}) {
@@ -19911,6 +20017,15 @@ var GoChaintracksServiceClient = class {
19911
20017
  }
19912
20018
  this.baseUrl = `${base}${prefix}`;
19913
20019
  this.fetcher = options.fetch ?? fetch;
20020
+ this.requestTimeoutMsecs = options.requestTimeoutMsecs ?? 3e4;
20021
+ this.reconnectWaitMsecs = options.reconnectWaitMsecs ?? 1e3;
20022
+ this.reconnectWaitMaxMsecs = options.reconnectWaitMaxMsecs ?? 6e4;
20023
+ for (const [name, value] of [
20024
+ ["requestTimeoutMsecs", this.requestTimeoutMsecs],
20025
+ ["reconnectWaitMsecs", this.reconnectWaitMsecs],
20026
+ ["reconnectWaitMaxMsecs", this.reconnectWaitMaxMsecs]
20027
+ ]) if (!Number.isSafeInteger(value) || value < 1) throw new Error(`${name} must be a positive integer.`);
20028
+ if (this.reconnectWaitMaxMsecs < this.reconnectWaitMsecs) throw new Error("reconnectWaitMaxMsecs must be greater than or equal to reconnectWaitMsecs.");
19914
20029
  }
19915
20030
  async currentHeight() {
19916
20031
  return await this.getPresentHeight();
@@ -19920,12 +20035,8 @@ var GoChaintracksServiceClient = class {
19920
20035
  return h != null && root === asString(h.merkleRoot);
19921
20036
  }
19922
20037
  async getChain() {
19923
- try {
19924
- const r = await this.getJson("/network");
19925
- return this.normalizeChain(r.network);
19926
- } catch {
19927
- return this.chain;
19928
- }
20038
+ const r = await this.getJson("/network");
20039
+ return this.normalizeChain(typeof r === "string" ? r : r.network);
19929
20040
  }
19930
20041
  async getInfo() {
19931
20042
  const tip = await this.findChainTipHeader();
@@ -19940,11 +20051,11 @@ var GoChaintracksServiceClient = class {
19940
20051
  };
19941
20052
  }
19942
20053
  async getPresentHeight() {
19943
- return (await this.getJson("/height")).height;
20054
+ const result = await this.getJson("/height");
20055
+ return typeof result === "number" ? result : result.height;
19944
20056
  }
19945
20057
  async getHeaders(height, count) {
19946
- const bytes = await this.getBinary(`/headers.bin?height=${height}&count=${count}`);
19947
- return Buffer.from(bytes).toString("hex");
20058
+ return asString(await this.getBinary(`/headers.bin?height=${height}&count=${count}`));
19948
20059
  }
19949
20060
  async findChainTipHeader() {
19950
20061
  return await this.getJson("/tip");
@@ -20000,7 +20111,7 @@ var GoChaintracksServiceClient = class {
20000
20111
  async subscribe(type, path, onPayload) {
20001
20112
  const id = `${type}-${this.nextSubscriptionId++}`;
20002
20113
  const abort = new AbortController();
20003
- const done = this.runSse(path, abort.signal, onPayload);
20114
+ const done = this.runSseWithReconnect(path, abort.signal, onPayload);
20004
20115
  this.subscriptions.set(id, {
20005
20116
  id,
20006
20117
  type,
@@ -20012,30 +20123,75 @@ var GoChaintracksServiceClient = class {
20012
20123
  });
20013
20124
  return id;
20014
20125
  }
20015
- async runSse(path, signal, onPayload) {
20016
- const response = await this.fetcher(this.url(path), {
20017
- headers: { Accept: "text/event-stream" },
20018
- signal
20126
+ async runSseWithReconnect(path, signal, onPayload) {
20127
+ let failures = 0;
20128
+ while (!signal.aborted) {
20129
+ try {
20130
+ failures = await this.runSse(path, signal, onPayload) ? 0 : failures + 1;
20131
+ } catch {
20132
+ if (signal.aborted) return;
20133
+ failures++;
20134
+ }
20135
+ const multiplier = Math.min(2 ** Math.max(0, failures - 1), 64);
20136
+ const delay = Math.min(this.reconnectWaitMsecs * multiplier, this.reconnectWaitMaxMsecs);
20137
+ await this.waitForReconnect(delay, signal);
20138
+ }
20139
+ }
20140
+ async waitForReconnect(msecs, signal) {
20141
+ if (signal.aborted || msecs <= 0) return;
20142
+ await new Promise((resolve) => {
20143
+ let timeout;
20144
+ const onAbort = () => done();
20145
+ const done = () => {
20146
+ clearTimeout(timeout);
20147
+ signal.removeEventListener("abort", onAbort);
20148
+ resolve();
20149
+ };
20150
+ timeout = setTimeout(done, msecs);
20151
+ signal.addEventListener("abort", onAbort, { once: true });
20019
20152
  });
20020
- if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
20021
- if (response.body == null) throw new Error(`GET ${this.url(path)} returned no response body`);
20022
- const reader = response.body.getReader();
20023
- const decoder = new TextDecoder();
20024
- let buffer = "";
20153
+ }
20154
+ async runSse(path, signal, onPayload) {
20155
+ const controller = new AbortController();
20156
+ const onAbort = () => controller.abort();
20157
+ signal.addEventListener("abort", onAbort, { once: true });
20158
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMsecs);
20159
+ let receivedEvent = false;
20160
+ const observePayload = (payload) => {
20161
+ receivedEvent = true;
20162
+ onPayload(payload);
20163
+ };
20025
20164
  try {
20026
- for (;;) {
20027
- const { done, value } = await reader.read();
20028
- if (done) break;
20029
- buffer += decoder.decode(value, { stream: true });
20030
- buffer = this.processSseBuffer(buffer, onPayload);
20165
+ const response = await this.fetcher(this.url(path), {
20166
+ headers: { Accept: "text/event-stream" },
20167
+ signal: controller.signal
20168
+ });
20169
+ clearTimeout(timeout);
20170
+ if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
20171
+ if (response.body == null) throw new Error(`GET ${this.url(path)} returned no response body`);
20172
+ const reader = response.body.getReader();
20173
+ const decoder = new TextDecoder();
20174
+ let buffer = "";
20175
+ try {
20176
+ for (;;) {
20177
+ const { done, value } = await reader.read();
20178
+ if (done) break;
20179
+ buffer += decoder.decode(value, { stream: true });
20180
+ buffer = this.processSseBuffer(buffer, observePayload);
20181
+ }
20182
+ buffer += decoder.decode();
20183
+ this.processSseBuffer(`${buffer}\n\n`, observePayload);
20184
+ } finally {
20185
+ reader.releaseLock();
20031
20186
  }
20032
- buffer += decoder.decode();
20033
- this.processSseBuffer(`${buffer}\n\n`, onPayload);
20034
20187
  } finally {
20035
- reader.releaseLock();
20188
+ clearTimeout(timeout);
20189
+ signal.removeEventListener("abort", onAbort);
20036
20190
  }
20191
+ return receivedEvent;
20037
20192
  }
20038
20193
  processSseBuffer(buffer, onPayload) {
20194
+ buffer = buffer.replaceAll("\r\n", "\n");
20039
20195
  for (;;) {
20040
20196
  const boundary = buffer.indexOf("\n\n");
20041
20197
  if (boundary < 0) return buffer;
@@ -20054,32 +20210,51 @@ var GoChaintracksServiceClient = class {
20054
20210
  return r;
20055
20211
  }
20056
20212
  async getJsonOrUndefined(path) {
20057
- const response = await this.fetcher(this.url(path), { headers: { Accept: "application/json" } });
20213
+ const response = await this.fetchWithTimeout(this.url(path), { headers: { Accept: "application/json" } });
20058
20214
  if (response.status === 404) return void 0;
20059
20215
  if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
20060
- return await response.json();
20216
+ const value = await response.json();
20217
+ if (value != null && typeof value === "object" && "status" in value) {
20218
+ const envelope = value;
20219
+ if (envelope.status === "success") return envelope.value;
20220
+ if (envelope.status === "error") throw new Error(envelope.description ?? `GET ${this.url(path)} failed`);
20221
+ }
20222
+ return value;
20061
20223
  }
20062
20224
  async getBinary(path) {
20063
- const response = await this.fetcher(this.url(path), { headers: { Accept: "application/octet-stream" } });
20225
+ const response = await this.fetchWithTimeout(this.url(path), { headers: { Accept: "application/octet-stream" } });
20064
20226
  if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
20065
20227
  return new Uint8Array(await response.arrayBuffer());
20066
20228
  }
20229
+ async fetchWithTimeout(url, init) {
20230
+ const controller = new AbortController();
20231
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMsecs);
20232
+ try {
20233
+ return await this.fetcher(url, {
20234
+ ...init,
20235
+ signal: controller.signal
20236
+ });
20237
+ } finally {
20238
+ clearTimeout(timeout);
20239
+ }
20240
+ }
20067
20241
  url(path) {
20068
20242
  return `${this.baseUrl}${path}`;
20069
20243
  }
20070
20244
  normalizeChain(network) {
20071
- switch (network) {
20245
+ switch (network.trim().toLowerCase()) {
20072
20246
  case "main":
20073
20247
  case "mainnet": return "main";
20074
20248
  case "test":
20075
20249
  case "testnet": return "test";
20250
+ case "stn":
20251
+ case "scalingtestnet": return "stn";
20076
20252
  case "ttn":
20077
20253
  case "teratest":
20078
20254
  case "teratestnet": return "ttn";
20079
20255
  case "tstn":
20080
- case "teranodescalingtestnet":
20081
- case "scalingtestnet": return "tstn";
20082
- default: return this.chain;
20256
+ case "teranodescalingtestnet": return "tstn";
20257
+ default: throw new Error(`Unsupported ChainTracks upstream network '${network}'.`);
20083
20258
  }
20084
20259
  }
20085
20260
  };
@@ -20574,9 +20749,11 @@ var BulkFileDataManager = class BulkFileDataManager {
20574
20749
  const nextHeight = lbf != null ? lbf.firstHeight + lbf.count : 0;
20575
20750
  ({headers: newBulkHeaders, incrementalChainWork} = trimAlreadyStoredHeaders(newBulkHeaders, nextHeight, incrementalChainWork));
20576
20751
  if (newBulkHeaders.length === 0) return;
20577
- if (lbf == null || nextHeight !== newBulkHeaders[0].height) throw new WERR_INVALID_PARAMETER("newBulkHeaders", "an extension of existing bulk headers");
20578
- if (!lbf.lastHash) throw new WERR_INTERNAL(`lastHash is not defined for the last bulk file ${lbf.fileName}`);
20579
- const lastChainWork = incrementalChainWork ? addWork(incrementalChainWork, lbf.lastChainWork) : computeChainWorkFromHeaders(newBulkHeaders, lbf);
20752
+ if (nextHeight !== newBulkHeaders[0].height) throw new WERR_INVALID_PARAMETER("newBulkHeaders", "an extension of existing bulk headers");
20753
+ if (lbf != null && !lbf.lastHash) throw new WERR_INTERNAL(`lastHash is not defined for the last bulk file ${lbf.fileName}`);
20754
+ const prevChainWork = lbf?.lastChainWork ?? "00".repeat(32);
20755
+ const prevHash = lbf?.lastHash ?? "00".repeat(32);
20756
+ const lastChainWork = incrementalChainWork ? addWork(incrementalChainWork, prevChainWork) : computeChainWorkFromHeaders(newBulkHeaders, lbf);
20580
20757
  const data = serializeBaseBlockHeaders(newBulkHeaders);
20581
20758
  const fileHash = asString(Hash.sha256(asArray(data)), "base64");
20582
20759
  const bf = {
@@ -20586,9 +20763,9 @@ var BulkFileDataManager = class BulkFileDataManager {
20586
20763
  fileName: "incremental",
20587
20764
  firstHeight: newBulkHeaders[0].height,
20588
20765
  count: newBulkHeaders.length,
20589
- prevChainWork: lbf.lastChainWork,
20766
+ prevChainWork,
20590
20767
  lastChainWork,
20591
- prevHash: lbf.lastHash,
20768
+ prevHash,
20592
20769
  lastHash: newBulkHeaders.at(-1).hash,
20593
20770
  fileHash,
20594
20771
  data
@@ -21018,12 +21195,13 @@ function trimAlreadyStoredHeaders(headers, nextHeight, incrementalChainWork) {
21018
21195
  }
21019
21196
  /**
21020
21197
  * Computes `lastChainWork` for a sequence of new bulk headers extending `lbf`,
21021
- * validating that the sequence is contiguous.
21198
+ * or beginning at genesis when bulk storage is empty, validating that the
21199
+ * sequence is contiguous.
21022
21200
  */
21023
21201
  function computeChainWorkFromHeaders(headers, lbf) {
21024
- let lastHeight = lbf.firstHeight + lbf.count - 1;
21025
- let lastHash = lbf.lastHash;
21026
- let lastChainWork = lbf.lastChainWork;
21202
+ let lastHeight = lbf != null ? lbf.firstHeight + lbf.count - 1 : -1;
21203
+ let lastHash = lbf?.lastHash ?? "00".repeat(32);
21204
+ let lastChainWork = lbf?.lastChainWork ?? "00".repeat(32);
21027
21205
  for (const h of headers) {
21028
21206
  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`);
21029
21207
  lastChainWork = addWork(lastChainWork, convertBitsToWork(h.bits));
@@ -21412,31 +21590,45 @@ var ServiceCollection = class ServiceCollection {
21412
21590
  //#endregion
21413
21591
  //#region ../src/services/networkConfig.ts
21414
21592
  /**
21415
- * Runtime service-endpoint configuration for the `tstn` (Teranode Scaling Test Net) network.
21593
+ * Runtime service-endpoint configuration for Teranode networks that do not
21594
+ * have a public, operator-independent service endpoint.
21416
21595
  *
21417
- * Unlike `main`, `test`, and `ttn`, the tstn service endpoints are not public and must not be
21596
+ * Unlike `main`, `test`, and `ttn`, the stn/tstn service endpoints are not public and must not be
21418
21597
  * hardcoded in this (public) source tree. They are supplied at runtime through environment
21419
21598
  * variables:
21420
21599
  *
21600
+ * STN_ARCADE_URL STN Arcade broadcaster / ARC endpoint base.
21601
+ * STN_CHAINTRACKS_URL STN ChainTracks service URL.
21421
21602
  * TSTN_ARCADE_URL Arcade broadcaster / ARC endpoint base. Also the fallback host for
21422
21603
  * ChainTracks when TSTN_CHAINTRACKS_URL is unset
21423
21604
  * (`${TSTN_ARCADE_URL}/chaintracks/v1`, mirroring the ttn layout).
21424
21605
  * TSTN_CHAINTRACKS_URL ChainTracks service URL.
21425
21606
  *
21426
- * tstn runs only Arcade (broadcast + merkle proofs) and ChainTracks (headers); there is no
21427
- * WhatsOnChain / block-explorer service for tstn, so no WhatsOnChain endpoint is configured and
21607
+ * stn/tstn run only operator-configured Arcade and ChainTracks services; there is no
21608
+ * documented WhatsOnChain service for them, so no WhatsOnChain endpoint is configured and
21428
21609
  * the WhatsOnChain-only lookups (raw tx, utxo status, txid status, script-hash history) are not
21429
- * available on tstn.
21610
+ * available on stn/tstn.
21430
21611
  *
21431
- * `process` is accessed defensively so importing this module remains safe in browser bundles;
21432
- * tstn is a server-side network and these variables are only read when the selected chain is
21433
- * tstn.
21612
+ * `process` is accessed defensively so importing this module remains safe in
21613
+ * browser bundles. Browser applications can still supply an explicit
21614
+ * ChaintracksClientApi without relying on environment variables.
21434
21615
  */
21435
21616
  function readEnv(name) {
21436
21617
  const value = (typeof process !== "undefined" ? process.env : void 0)?.[name];
21437
21618
  return value != null && value.trim() !== "" ? value.trim() : void 0;
21438
21619
  }
21439
- const stripTrailingSlash = (url) => {
21620
+ /** Credential-free public Arcade host for supported networks. */
21621
+ function publicArcadeUrl(chain) {
21622
+ switch (chain) {
21623
+ case "main": return "https://arcade-v2-us-1.bsvblockchain.tech";
21624
+ case "test": return "https://arcade-v2-testnet-us-1.bsvblockchain.tech";
21625
+ case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech";
21626
+ case "stn":
21627
+ case "tstn":
21628
+ case "mock": return;
21629
+ }
21630
+ }
21631
+ const stripTrailingSlash$1 = (url) => {
21440
21632
  let end = url.length;
21441
21633
  while (end > 0 && url[end - 1] === "/") end--;
21442
21634
  return url.slice(0, end);
@@ -21445,6 +21637,10 @@ const stripTrailingSlash = (url) => {
21445
21637
  function tstnArcadeUrl() {
21446
21638
  return readEnv("TSTN_ARCADE_URL");
21447
21639
  }
21640
+ /** Arcade broadcaster / ARC endpoint for stn, or `undefined` when unset. */
21641
+ function stnArcadeUrl() {
21642
+ return readEnv("STN_ARCADE_URL");
21643
+ }
21448
21644
  /**
21449
21645
  * ChainTracks service URL for tstn. Falls back to `${TSTN_ARCADE_URL}/chaintracks/v1` when
21450
21646
  * `TSTN_CHAINTRACKS_URL` is unset (mirrors the ttn layout). Throws when neither is configured.
@@ -21453,20 +21649,53 @@ function tstnChaintracksUrl() {
21453
21649
  const explicit = readEnv("TSTN_CHAINTRACKS_URL");
21454
21650
  if (explicit != null) return explicit;
21455
21651
  const arcade = tstnArcadeUrl();
21456
- if (arcade != null) return `${stripTrailingSlash(arcade)}/chaintracks/v1`;
21652
+ if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
21457
21653
  throw new Error("tstn chain requires a ChainTracks URL: set TSTN_CHAINTRACKS_URL (or TSTN_ARCADE_URL) in the environment.");
21458
21654
  }
21655
+ /**
21656
+ * ChainTracks service URL for stn. Falls back to the configured Arcade host's
21657
+ * legacy-compatible path when STN_CHAINTRACKS_URL is unset.
21658
+ */
21659
+ function stnChaintracksUrl() {
21660
+ const explicit = readEnv("STN_CHAINTRACKS_URL");
21661
+ if (explicit != null) return explicit;
21662
+ const arcade = stnArcadeUrl();
21663
+ if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
21664
+ throw new Error("stn chain requires a ChainTracks URL: set STN_CHAINTRACKS_URL (or STN_ARCADE_URL) in the environment.");
21665
+ }
21459
21666
  //#endregion
21460
21667
  //#region ../src/services/createDefaultWalletServicesOptions.ts
21668
+ function stripTrailingSlash(value) {
21669
+ let end = value.length;
21670
+ while (end > 0 && value[end - 1] === "/") end--;
21671
+ return value.slice(0, end);
21672
+ }
21673
+ function configuredChaintracksClient(chain, serviceUrl) {
21674
+ let path = "";
21675
+ try {
21676
+ path = stripTrailingSlash(new URL(serviceUrl).pathname);
21677
+ } catch {}
21678
+ if (path.endsWith("/v2")) return new GoChaintracksServiceClient(chain, serviceUrl);
21679
+ return new ChaintracksServiceClient(chain, serviceUrl);
21680
+ }
21681
+ /**
21682
+ * Returns the credential-free default ChainTracks client for a supported
21683
+ * public network, or an operator-configured client for stn/tstn.
21684
+ */
21685
+ function createDefaultChaintracksClient(chain) {
21686
+ switch (chain) {
21687
+ case "main":
21688
+ case "test":
21689
+ case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
21690
+ case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
21691
+ case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
21692
+ }
21693
+ }
21461
21694
  function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallbackToken, taalArcApiKey, gorillaPoolArcApiKey, bitailsApiKey, deploymentId, chaintracks, arcadeUrl, arcadeApiKey, arcadeCallbackToken]) {
21462
21695
  if (chain === "mock") throw new Error("createDefaultWalletServicesOptions does not support 'mock' chain. Use MockServices directly.");
21463
21696
  deploymentId ||= `wallet-toolbox-${randomBytesHex(16)}`;
21464
- let chaintracksUrl;
21465
- if (chain === "ttn") chaintracksUrl = "https://arcade-v2-ttn-us-1.bsvblockchain.tech/chaintracks/v1";
21466
- else if (chain === "tstn") chaintracksUrl = tstnChaintracksUrl();
21467
- else chaintracksUrl = `https://${chain}net-chaintracks.babbage.systems`;
21468
21697
  const chaintracksFiatExchangeRatesUrl = "https://mainnet-chaintracks.babbage.systems/getFiatExchangeRates";
21469
- chaintracks ||= new ChaintracksServiceClient(chain, chaintracksUrl);
21698
+ chaintracks ||= createDefaultChaintracksClient(chain);
21470
21699
  const o = {
21471
21700
  chain,
21472
21701
  taalApiKey: void 0,
@@ -21524,14 +21753,15 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
21524
21753
  }
21525
21754
  /**
21526
21755
  * Default Arcade (bsv-blockchain/arcade) endpoint per chain.
21527
- * Returns undefined when no public default is known for the chain (e.g. testnet not yet deployed).
21756
+ * Returns undefined when no public default is known for the chain.
21528
21757
  */
21529
21758
  function arcadeDefaultUrl(chain) {
21530
21759
  switch (chain) {
21531
- case "main": return "https://arcade-v2-us-1.bsvblockchain.tech";
21532
- case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech";
21760
+ case "main":
21761
+ case "test":
21762
+ case "ttn": return publicArcadeUrl(chain);
21763
+ case "stn": return stnArcadeUrl();
21533
21764
  case "tstn": return tstnArcadeUrl();
21534
- case "test": return;
21535
21765
  case "mock": return;
21536
21766
  }
21537
21767
  }
@@ -21539,6 +21769,7 @@ function arcDefaultUrl(chain) {
21539
21769
  switch (chain) {
21540
21770
  case "main": return "https://arc.taal.com";
21541
21771
  case "test": return "https://arc-test.taal.com";
21772
+ case "stn": return stnArcadeUrl() ?? "";
21542
21773
  case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech/";
21543
21774
  case "tstn": return tstnArcadeUrl() ?? "";
21544
21775
  case "mock": return "";
@@ -22920,7 +23151,7 @@ var Services = class Services {
22920
23151
  telemetry;
22921
23152
  constructor(optionsOrChain) {
22922
23153
  this.chain = typeof optionsOrChain === "string" ? optionsOrChain : optionsOrChain.chain;
22923
- if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
23154
+ if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'stn', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
22924
23155
  this.options = typeof optionsOrChain === "string" ? Services.createDefaultOptions(this.chain) : optionsOrChain;
22925
23156
  this.telemetry = new Telemetry(this.options.telemetry);
22926
23157
  this.whatsonchain = new WhatsOnChain(this.chain, { apiKey: this.options.whatsOnChainApiKey }, this);
@@ -22934,7 +23165,7 @@ var Services = class Services {
22934
23165
  if (this.options.arcGorillaPoolUrl != null && this.options.arcGorillaPoolUrl !== "") this.arcGorillaPool = new ARC(this.options.arcGorillaPoolUrl, this.options.arcGorillaPoolConfig, "arcGorillaPool");
22935
23166
  if (this.options.arcadeUrl != null && this.options.arcadeUrl !== "") this.arcade = new Arcade(this.options.arcadeUrl, this.options.arcadeConfig, "arcade");
22936
23167
  const hasBitails = this.chain === "main" || this.chain === "test";
22937
- const hasWhatsOnChain = this.chain !== "tstn";
23168
+ const hasWhatsOnChain = this.chain === "main" || this.chain === "test";
22938
23169
  if (hasBitails) this.bitails = new Bitails(this.chain, { apiKey: this.options.bitailsApiKey });
22939
23170
  return {
22940
23171
  hasBitails,
@@ -23648,9 +23879,22 @@ function classifyMerklePathResponse(status, statusText, retry) {
23648
23879
  //#endregion
23649
23880
  //#region ../src/services/providers/WhatsOnChain.ts
23650
23881
  var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
23882
+ requestGate;
23651
23883
  constructor(chain = "main", config = {}) {
23652
23884
  if (chain === "mock") throw new Error("WhatsOnChain does not support 'mock' chain. Use MockServices directly.");
23653
23885
  super(chain, config);
23886
+ this.requestGate = config.requestGate;
23887
+ }
23888
+ async requestWithAnonymousAuthFallback(url, requestOptions) {
23889
+ await this.requestGate?.();
23890
+ const response = await this.httpClient.request(url, requestOptions);
23891
+ if (response.status !== 401 && response.status !== 403 || this.apiKey.trim() === "") return response;
23892
+ if (this.requestGate != null) await this.requestGate();
23893
+ else await wait(350);
23894
+ return await this.httpClient.request(url, {
23895
+ method: "GET",
23896
+ headers: { Accept: "application/json" }
23897
+ });
23654
23898
  }
23655
23899
  /**
23656
23900
  * POST
@@ -24059,7 +24303,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
24059
24303
  };
24060
24304
  const url = `${this.URL}/block/${hash}/header`;
24061
24305
  for (let retry = 0; retry < 2; retry++) {
24062
- const response = await this.httpClient.request(url, requestOptions);
24306
+ const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
24063
24307
  if (response.statusText === "Too Many Requests" && retry < 2) {
24064
24308
  await wait(2e3);
24065
24309
  continue;
@@ -24077,7 +24321,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
24077
24321
  };
24078
24322
  const url = `${this.URL}/chain/info`;
24079
24323
  for (let retry = 0; retry < 2; retry++) {
24080
- const response = await this.httpClient.request(url, requestOptions);
24324
+ const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
24081
24325
  if (response.statusText === "Too Many Requests" && retry < 2) {
24082
24326
  await wait(2e3);
24083
24327
  continue;
@@ -24263,12 +24507,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24263
24507
  timeout: 3e4,
24264
24508
  userAgent: "BabbageWhatsOnChainServices",
24265
24509
  enableCache: true,
24266
- chainInfoMsecs: 5e3
24510
+ chainInfoMsecs: 5e3,
24511
+ minRequestIntervalMsecs: 350
24267
24512
  };
24268
24513
  }
24269
24514
  static chainInfo = [];
24270
24515
  static chainInfoTime = [];
24271
24516
  static chainInfoMsecs = [];
24517
+ static chainInfoPromise = {};
24518
+ static requestTail = Promise.resolve();
24519
+ static nextRequestMsecs = 0;
24272
24520
  chain;
24273
24521
  woc;
24274
24522
  constructor(options) {
@@ -24277,7 +24525,8 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24277
24525
  apiKey: this.options.apiKey,
24278
24526
  timeout: this.options.timeout,
24279
24527
  userAgent: this.options.userAgent,
24280
- enableCache: this.options.enableCache
24528
+ enableCache: this.options.enableCache,
24529
+ requestGate: async () => await this.waitForRateLimit()
24281
24530
  };
24282
24531
  this.chain = options.chain;
24283
24532
  const chainInfoMsecs = WhatsOnChainServices.chainInfoMsecs;
@@ -24295,7 +24544,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24295
24544
  let update = chainInfo[this.chain] === void 0;
24296
24545
  if (!update && chainInfoTime[this.chain] !== void 0) update = now.getTime() - chainInfoTime[this.chain].getTime() > chainInfoMsecs[this.chain];
24297
24546
  if (update) {
24298
- chainInfo[this.chain] = await this.woc.getChainInfo();
24547
+ let pending = WhatsOnChainServices.chainInfoPromise[this.chain];
24548
+ if (pending == null) {
24549
+ pending = this.woc.getChainInfo();
24550
+ WhatsOnChainServices.chainInfoPromise[this.chain] = pending;
24551
+ }
24552
+ try {
24553
+ chainInfo[this.chain] = await pending;
24554
+ } finally {
24555
+ if (WhatsOnChainServices.chainInfoPromise[this.chain] === pending) delete WhatsOnChainServices.chainInfoPromise[this.chain];
24556
+ }
24299
24557
  chainInfoTime[this.chain] = now;
24300
24558
  }
24301
24559
  if (!chainInfo[this.chain]) throw new Error("Unexpected failure to update chainInfo.");
@@ -24313,10 +24571,12 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24313
24571
  */
24314
24572
  async getHeaders(fetch) {
24315
24573
  fetch ||= new ChaintracksFetch();
24574
+ await this.waitForRateLimit();
24316
24575
  return await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers`);
24317
24576
  }
24318
24577
  async getHeaderByteFileLinks(neededRange, fetch) {
24319
24578
  fetch ||= new ChaintracksFetch();
24579
+ await this.waitForRateLimit();
24320
24580
  const files = await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers/resources`);
24321
24581
  const r = [];
24322
24582
  let range;
@@ -24329,6 +24589,21 @@ var WhatsOnChainServices = class WhatsOnChainServices {
24329
24589
  }
24330
24590
  return r;
24331
24591
  }
24592
+ async waitForRateLimit() {
24593
+ let release;
24594
+ const previous = WhatsOnChainServices.requestTail;
24595
+ WhatsOnChainServices.requestTail = new Promise((resolve) => {
24596
+ release = resolve;
24597
+ });
24598
+ await previous;
24599
+ try {
24600
+ const delay = Math.max(0, WhatsOnChainServices.nextRequestMsecs - Date.now());
24601
+ if (delay > 0) await wait(delay);
24602
+ WhatsOnChainServices.nextRequestMsecs = Date.now() + (this.options.minRequestIntervalMsecs ?? 350);
24603
+ } finally {
24604
+ release();
24605
+ }
24606
+ }
24332
24607
  };
24333
24608
  function wocGetHeadersHeaderToBlockHeader(h) {
24334
24609
  const bits = typeof h.bits === "string" ? Number.parseInt(h.bits, 16) : h.bits;
@@ -24389,6 +24664,51 @@ var BulkIngestorWhatsOnChainCdn = class extends BulkIngestorBase {
24389
24664
  }
24390
24665
  };
24391
24666
  //#endregion
24667
+ //#region ../src/services/chaintracker/chaintracks/Ingest/BulkIngestorChaintracks.ts
24668
+ /**
24669
+ * Uses a go-chaintracks/Arcade-compatible service as a validated bulk source.
24670
+ * Retrieved bytes still pass through ChainTracks' local serialization, hash,
24671
+ * continuity, and genesis checks before storage.
24672
+ */
24673
+ var BulkIngestorChaintracks = class extends BulkIngestorBase {
24674
+ chaintracks;
24675
+ maxHeadersPerRequest;
24676
+ networkChecked = false;
24677
+ constructor(options) {
24678
+ super(options);
24679
+ this.chaintracks = options.chaintracks;
24680
+ this.maxHeadersPerRequest = options.maxHeadersPerRequest ?? 1e3;
24681
+ if (!Number.isInteger(this.maxHeadersPerRequest) || this.maxHeadersPerRequest < 1) throw new Error("maxHeadersPerRequest must be a positive integer.");
24682
+ }
24683
+ async getPresentHeight() {
24684
+ await this.ensureNetwork();
24685
+ return await this.chaintracks.getPresentHeight();
24686
+ }
24687
+ async fetchHeaders(_before, fetchRange, bulkRange, priorLiveHeaders) {
24688
+ if (fetchRange.isEmpty) return priorLiveHeaders;
24689
+ await this.ensureNetwork();
24690
+ let liveHeaders = priorLiveHeaders;
24691
+ let height = fetchRange.minHeight;
24692
+ while (height <= fetchRange.maxHeight) {
24693
+ const requested = Math.min(this.maxHeadersPerRequest, fetchRange.maxHeight - height + 1);
24694
+ const bytes = asUint8Array(await this.chaintracks.getHeaders(height, requested));
24695
+ if (bytes.length === 0) throw new Error(`ChainTracks upstream returned no headers at height ${height}.`);
24696
+ if (bytes.length % 80 !== 0 || bytes.length > requested * 80) throw new Error(`ChainTracks upstream returned ${bytes.length} bytes for ${requested} headers at height ${height}.`);
24697
+ const headers = deserializeBlockHeaders(height, bytes);
24698
+ liveHeaders = await this.storage().addBulkHeaders(headers, bulkRange, liveHeaders);
24699
+ height += headers.length;
24700
+ if (headers.length < requested && height <= fetchRange.maxHeight) throw new Error(`ChainTracks upstream returned ${headers.length} of ${requested} headers at height ${height - headers.length}.`);
24701
+ }
24702
+ return liveHeaders;
24703
+ }
24704
+ async ensureNetwork() {
24705
+ if (this.networkChecked) return;
24706
+ const actual = await this.chaintracks.getChain();
24707
+ if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
24708
+ this.networkChecked = true;
24709
+ }
24710
+ };
24711
+ //#endregion
24392
24712
  //#region ../src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainPoll.ts
24393
24713
  /**
24394
24714
  * Reports new headers by polling periodically.
@@ -24495,9 +24815,17 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
24495
24815
  }
24496
24816
  async startListening(liveHeaders) {
24497
24817
  this.stopped = false;
24498
- this.subscriptionId = await this.options.chaintracks.subscribeHeaders((header) => {
24818
+ const actual = await this.options.chaintracks.getChain();
24819
+ if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
24820
+ if (this.stopped) return;
24821
+ const subscriptionId = await this.options.chaintracks.subscribeHeaders((header) => {
24499
24822
  if (!this.stopped) liveHeaders.push(header);
24500
24823
  });
24824
+ if (this.stopped) {
24825
+ await this.options.chaintracks.unsubscribe(subscriptionId);
24826
+ return;
24827
+ }
24828
+ this.subscriptionId = subscriptionId;
24501
24829
  await new Promise((resolve) => {
24502
24830
  this.resolveStopped = resolve;
24503
24831
  if (this.stopped) resolve();
@@ -24510,7 +24838,9 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
24510
24838
  if (subscriptionId != null) this.options.chaintracks.unsubscribe(subscriptionId).catch((e) => {
24511
24839
  this.log(`LiveIngestorChaintracksSSE unsubscribe failed: ${e}`);
24512
24840
  });
24513
- this.resolveStopped?.();
24841
+ const resolveStopped = this.resolveStopped;
24842
+ this.resolveStopped = void 0;
24843
+ resolveStopped?.();
24514
24844
  }
24515
24845
  async shutdown() {
24516
24846
  this.stopListening();
@@ -25206,6 +25536,27 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
25206
25536
  tipHeaderId: 0,
25207
25537
  hashToHeaderId: /* @__PURE__ */ new Map()
25208
25538
  };
25539
+ static stnData = {
25540
+ chain: "stn",
25541
+ liveHeaders: /* @__PURE__ */ new Map(),
25542
+ maxHeaderId: 0,
25543
+ tipHeaderId: 0,
25544
+ hashToHeaderId: /* @__PURE__ */ new Map()
25545
+ };
25546
+ static ttnData = {
25547
+ chain: "ttn",
25548
+ liveHeaders: /* @__PURE__ */ new Map(),
25549
+ maxHeaderId: 0,
25550
+ tipHeaderId: 0,
25551
+ hashToHeaderId: /* @__PURE__ */ new Map()
25552
+ };
25553
+ static tstnData = {
25554
+ chain: "tstn",
25555
+ liveHeaders: /* @__PURE__ */ new Map(),
25556
+ maxHeaderId: 0,
25557
+ tipHeaderId: 0,
25558
+ hashToHeaderId: /* @__PURE__ */ new Map()
25559
+ };
25209
25560
  constructor(options) {
25210
25561
  super(options);
25211
25562
  }
@@ -25213,10 +25564,11 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
25213
25564
  async getData() {
25214
25565
  switch (this.chain) {
25215
25566
  case "main": return ChaintracksStorageNoDb.mainData;
25216
- case "test":
25217
- case "ttn":
25218
- case "tstn": return ChaintracksStorageNoDb.testData;
25219
- default: throw new WERR_INVALID_PARAMETER("chain", `'main', 'test', 'ttn', or 'tstn'. '${this.chain}' is unsupported.`);
25567
+ case "test": return ChaintracksStorageNoDb.testData;
25568
+ case "stn": return ChaintracksStorageNoDb.stnData;
25569
+ case "ttn": return ChaintracksStorageNoDb.ttnData;
25570
+ case "tstn": return ChaintracksStorageNoDb.tstnData;
25571
+ default: throw new WERR_INVALID_PARAMETER("chain", `'main', 'test', 'stn', 'ttn', or 'tstn'. '${this.chain}' is unsupported.`);
25220
25572
  }
25221
25573
  }
25222
25574
  async deleteLiveBlockHeaders() {
@@ -25805,7 +26157,7 @@ var ChaintracksStorageIdb = class extends ChaintracksStorageBase {
25805
26157
  //#endregion
25806
26158
  //#region ../src/services/chaintracker/chaintracks/configureChaintracksIngestors.ts
25807
26159
  function resolveDefaultChaintracksArguments(args) {
25808
- 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;
26160
+ 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;
25809
26161
  return {
25810
26162
  chain,
25811
26163
  whatsonchainApiKey,
@@ -25817,11 +26169,12 @@ function resolveDefaultChaintracksArguments(args) {
25817
26169
  reorgHeightThreshold,
25818
26170
  bulkMigrationChunkSize,
25819
26171
  batchInsertLimit,
25820
- addLiveRecursionLimit
26172
+ addLiveRecursionLimit,
26173
+ sources
25821
26174
  };
25822
26175
  }
25823
26176
  function toDefaultChaintracksArguments(params) {
25824
- return [
26177
+ const args = [
25825
26178
  params.chain,
25826
26179
  params.whatsonchainApiKey,
25827
26180
  params.maxPerFile,
@@ -25834,6 +26187,8 @@ function toDefaultChaintracksArguments(params) {
25834
26187
  params.batchInsertLimit,
25835
26188
  params.addLiveRecursionLimit
25836
26189
  ];
26190
+ if (Object.keys(params.sources).length > 0) args.push(params.sources);
26191
+ return args;
25837
26192
  }
25838
26193
  function createDefaultBulkFileDataManager(params) {
25839
26194
  return new BulkFileDataManager({
@@ -25876,7 +26231,7 @@ function createAndStartDefaultChaintracks(args, createOptions) {
25876
26231
  * The caller is responsible for providing the storage implementation.
25877
26232
  */
25878
26233
  function buildChaintracksOptionsWithIngestors(params, storage) {
25879
- const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit } = params;
26234
+ const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit, sources } = params;
25880
26235
  const co = {
25881
26236
  chain,
25882
26237
  storage,
@@ -25887,35 +26242,58 @@ function buildChaintracksOptionsWithIngestors(params, storage) {
25887
26242
  readonly: false
25888
26243
  };
25889
26244
  const jsonResource = `${chain}NetBlockHeaders.json`;
25890
- const bulkCdnOptions = {
25891
- chain,
25892
- jsonResource,
25893
- fetch,
25894
- cdnUrl,
25895
- maxPerFile
25896
- };
25897
- co.bulkIngestors.push(new BulkIngestorCDNBabbage(bulkCdnOptions));
25898
- const wocOptions = {
25899
- chain,
25900
- apiKey: whatsonchainApiKey,
25901
- timeout: 3e4,
25902
- userAgent: "BabbageWhatsOnChainServices",
25903
- enableCache: true,
25904
- chainInfoMsecs: 5e3
25905
- };
25906
- const bulkOptions = {
25907
- ...wocOptions,
25908
- jsonResource,
25909
- idleWait: 5e3
25910
- };
25911
- co.bulkIngestors.push(new BulkIngestorWhatsOnChainCdn(bulkOptions));
25912
- const liveOptions = {
25913
- ...wocOptions,
25914
- idleWait: 1e5
25915
- };
25916
- co.liveIngestors.push(new LiveIngestorWhatsOnChainPoll(liveOptions));
26245
+ if (!sources.disableCdn && cdnUrl !== "") {
26246
+ const bulkCdnOptions = {
26247
+ chain,
26248
+ jsonResource,
26249
+ fetch,
26250
+ cdnUrl,
26251
+ maxPerFile
26252
+ };
26253
+ co.bulkIngestors.push(new BulkIngestorCDNBabbage(bulkCdnOptions));
26254
+ }
26255
+ const chaintracksSource = sources.chaintracks ?? (sources.disableChaintracks ? void 0 : createPublicChaintracksSource(chain));
26256
+ if (chaintracksSource != null) {
26257
+ co.bulkIngestors.push(new BulkIngestorChaintracks({
26258
+ chain,
26259
+ jsonResource,
26260
+ chaintracks: chaintracksSource,
26261
+ maxHeadersPerRequest: sources.remoteMaxHeadersPerRequest
26262
+ }));
26263
+ co.liveIngestors.push(new LiveIngestorChaintracksSSE({
26264
+ chain,
26265
+ chaintracks: chaintracksSource
26266
+ }));
26267
+ }
26268
+ if ((chain === "main" || chain === "test") && !sources.disableWhatsOnChain) {
26269
+ const wocOptions = {
26270
+ chain,
26271
+ apiKey: whatsonchainApiKey,
26272
+ timeout: 3e4,
26273
+ userAgent: "BabbageWhatsOnChainServices",
26274
+ enableCache: true,
26275
+ chainInfoMsecs: 5e3
26276
+ };
26277
+ const bulkOptions = {
26278
+ ...wocOptions,
26279
+ jsonResource,
26280
+ idleWait: 5e3
26281
+ };
26282
+ co.bulkIngestors.push(new BulkIngestorWhatsOnChainCdn(bulkOptions));
26283
+ const liveOptions = {
26284
+ ...wocOptions,
26285
+ idleWait: 1e5
26286
+ };
26287
+ co.liveIngestors.push(new LiveIngestorWhatsOnChainPoll(liveOptions));
26288
+ }
26289
+ 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.`);
25917
26290
  return co;
25918
26291
  }
26292
+ function createPublicChaintracksSource(chain) {
26293
+ const serviceUrl = publicArcadeUrl(chain);
26294
+ if (serviceUrl == null) return void 0;
26295
+ return new GoChaintracksServiceClient(chain, serviceUrl, { apiPrefix: "/chaintracks/v2" });
26296
+ }
25919
26297
  //#endregion
25920
26298
  //#region ../src/services/chaintracker/chaintracks/createDefaultNoDbChaintracksOptions.ts
25921
26299
  function createDefaultNoDbChaintracksOptions(...args) {
@@ -34494,6 +34872,6 @@ var WalletPermissionsManager = class WalletPermissionsManager {
34494
34872
  }
34495
34873
  };
34496
34874
  //#endregion
34497
- export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, convertProofToMerklePath, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, blockHeaderUtilities_exports as utils, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
34875
+ export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorChaintracks, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, buildChaintracksOptionsWithIngestors, convertProofToMerklePath, createAndStartDefaultChaintracks, createDefaultBulkFileDataManager, createDefaultChaintracksClient, createDefaultChaintracksStorageOptions, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, resolveDefaultChaintracksArguments, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, startChaintracks, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toDefaultChaintracksArguments, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, blockHeaderUtilities_exports as utils, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
34498
34876
 
34499
34877
  //# sourceMappingURL=index.client.mjs.map