@unicitylabs/sphere-sdk 0.15.0 → 0.16.0-dev.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.
Files changed (50) hide show
  1. package/README.md +15 -5
  2. package/dist/connect/index.cjs +14 -23
  3. package/dist/connect/index.cjs.map +1 -1
  4. package/dist/connect/index.d.cts +4 -0
  5. package/dist/connect/index.d.ts +4 -0
  6. package/dist/connect/index.js +14 -23
  7. package/dist/connect/index.js.map +1 -1
  8. package/dist/core/index.cjs +726 -225
  9. package/dist/core/index.cjs.map +1 -1
  10. package/dist/core/index.d.cts +172 -21
  11. package/dist/core/index.d.ts +172 -21
  12. package/dist/core/index.js +726 -224
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/impl/browser/connect/index.cjs +14 -23
  15. package/dist/impl/browser/connect/index.cjs.map +1 -1
  16. package/dist/impl/browser/connect/index.js +14 -23
  17. package/dist/impl/browser/connect/index.js.map +1 -1
  18. package/dist/impl/browser/index.cjs +331 -504
  19. package/dist/impl/browser/index.cjs.map +1 -1
  20. package/dist/impl/browser/index.js +331 -504
  21. package/dist/impl/browser/index.js.map +1 -1
  22. package/dist/impl/nodejs/connect/index.cjs +13 -22
  23. package/dist/impl/nodejs/connect/index.cjs.map +1 -1
  24. package/dist/impl/nodejs/connect/index.js +13 -22
  25. package/dist/impl/nodejs/connect/index.js.map +1 -1
  26. package/dist/impl/nodejs/index.cjs +298 -569
  27. package/dist/impl/nodejs/index.cjs.map +1 -1
  28. package/dist/impl/nodejs/index.d.cts +90 -13
  29. package/dist/impl/nodejs/index.d.ts +90 -13
  30. package/dist/impl/nodejs/index.js +298 -569
  31. package/dist/impl/nodejs/index.js.map +1 -1
  32. package/dist/impl/shared/wallet-api/index.d.cts +54 -2
  33. package/dist/impl/shared/wallet-api/index.d.ts +54 -2
  34. package/dist/index.cjs +728 -225
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +227 -108
  37. package/dist/index.d.ts +227 -108
  38. package/dist/index.js +728 -224
  39. package/dist/index.js.map +1 -1
  40. package/dist/modules/payments-v2/index.cjs +11 -7
  41. package/dist/modules/payments-v2/index.cjs.map +1 -1
  42. package/dist/modules/payments-v2/index.d.cts +56 -3
  43. package/dist/modules/payments-v2/index.d.ts +56 -3
  44. package/dist/modules/payments-v2/index.js +11 -7
  45. package/dist/modules/payments-v2/index.js.map +1 -1
  46. package/dist/token-engine/index.cjs +46 -0
  47. package/dist/token-engine/index.cjs.map +1 -1
  48. package/dist/token-engine/index.js +46 -0
  49. package/dist/token-engine/index.js.map +1 -1
  50. package/package.json +1 -1
@@ -2631,9 +2631,9 @@ var require_dispatcher_base = __commonJS({
2631
2631
  }
2632
2632
  close(callback) {
2633
2633
  if (callback === void 0) {
2634
- return new Promise((resolve, reject) => {
2634
+ return new Promise((resolve2, reject) => {
2635
2635
  this.close((err, data) => {
2636
- return err ? reject(err) : resolve(data);
2636
+ return err ? reject(err) : resolve2(data);
2637
2637
  });
2638
2638
  });
2639
2639
  }
@@ -2671,9 +2671,9 @@ var require_dispatcher_base = __commonJS({
2671
2671
  err = null;
2672
2672
  }
2673
2673
  if (callback === void 0) {
2674
- return new Promise((resolve, reject) => {
2674
+ return new Promise((resolve2, reject) => {
2675
2675
  this.destroy(err, (err2, data) => {
2676
- return err2 ? reject(err2) : resolve(data);
2676
+ return err2 ? reject(err2) : resolve2(data);
2677
2677
  });
2678
2678
  });
2679
2679
  }
@@ -6127,8 +6127,8 @@ var require_promise = __commonJS({
6127
6127
  function createDeferredPromise() {
6128
6128
  let res;
6129
6129
  let rej;
6130
- const promise = new Promise((resolve, reject) => {
6131
- res = resolve;
6130
+ const promise = new Promise((resolve2, reject) => {
6131
+ res = resolve2;
6132
6132
  rej = reject;
6133
6133
  });
6134
6134
  return { promise, resolve: res, reject: rej };
@@ -7435,12 +7435,12 @@ upgrade: ${upgrade}\r
7435
7435
  cb();
7436
7436
  }
7437
7437
  }
7438
- const waitForDrain = () => new Promise((resolve, reject) => {
7438
+ const waitForDrain = () => new Promise((resolve2, reject) => {
7439
7439
  assert(callback === null);
7440
7440
  if (socket[kError]) {
7441
7441
  reject(socket[kError]);
7442
7442
  } else {
7443
- callback = resolve;
7443
+ callback = resolve2;
7444
7444
  }
7445
7445
  });
7446
7446
  socket.on("close", onDrain).on("drain", onDrain);
@@ -8252,12 +8252,12 @@ var require_client_h2 = __commonJS({
8252
8252
  cb();
8253
8253
  }
8254
8254
  }
8255
- const waitForDrain = () => new Promise((resolve, reject) => {
8255
+ const waitForDrain = () => new Promise((resolve2, reject) => {
8256
8256
  assert(callback === null);
8257
8257
  if (socket[kError]) {
8258
8258
  reject(socket[kError]);
8259
8259
  } else {
8260
- callback = resolve;
8260
+ callback = resolve2;
8261
8261
  }
8262
8262
  });
8263
8263
  h2stream.on("close", onDrain).on("drain", onDrain);
@@ -8559,16 +8559,16 @@ var require_client = __commonJS({
8559
8559
  return this[kNeedDrain] < 2;
8560
8560
  }
8561
8561
  [kClose]() {
8562
- return new Promise((resolve) => {
8562
+ return new Promise((resolve2) => {
8563
8563
  if (this[kSize]) {
8564
- this[kClosedResolve] = resolve;
8564
+ this[kClosedResolve] = resolve2;
8565
8565
  } else {
8566
- resolve(null);
8566
+ resolve2(null);
8567
8567
  }
8568
8568
  });
8569
8569
  }
8570
8570
  [kDestroy](err) {
8571
- return new Promise((resolve) => {
8571
+ return new Promise((resolve2) => {
8572
8572
  const requests = this[kQueue].splice(this[kPendingIdx]);
8573
8573
  for (let i = 0; i < requests.length; i++) {
8574
8574
  const request = requests[i];
@@ -8579,7 +8579,7 @@ var require_client = __commonJS({
8579
8579
  this[kClosedResolve]();
8580
8580
  this[kClosedResolve] = null;
8581
8581
  }
8582
- resolve(null);
8582
+ resolve2(null);
8583
8583
  };
8584
8584
  if (this[kHTTPContext]) {
8585
8585
  this[kHTTPContext].destroy(err, callback);
@@ -8970,8 +8970,8 @@ var require_pool_base = __commonJS({
8970
8970
  }
8971
8971
  return Promise.all(closeAll);
8972
8972
  } else {
8973
- return new Promise((resolve) => {
8974
- this[kClosedResolve] = resolve;
8973
+ return new Promise((resolve2) => {
8974
+ this[kClosedResolve] = resolve2;
8975
8975
  });
8976
8976
  }
8977
8977
  }
@@ -10500,7 +10500,7 @@ var require_readable = __commonJS({
10500
10500
  if (this._readableState.closeEmitted) {
10501
10501
  return Promise.resolve(null);
10502
10502
  }
10503
- return new Promise((resolve, reject) => {
10503
+ return new Promise((resolve2, reject) => {
10504
10504
  if (this[kContentLength] && this[kContentLength] > limit || this[kBytesRead] > limit) {
10505
10505
  this.destroy(new AbortError());
10506
10506
  }
@@ -10514,11 +10514,11 @@ var require_readable = __commonJS({
10514
10514
  if (signal.aborted) {
10515
10515
  reject(signal.reason ?? new AbortError());
10516
10516
  } else {
10517
- resolve(null);
10517
+ resolve2(null);
10518
10518
  }
10519
10519
  });
10520
10520
  } else {
10521
- this.on("close", resolve);
10521
+ this.on("close", resolve2);
10522
10522
  }
10523
10523
  this.on("error", noop).on("data", () => {
10524
10524
  if (this[kBytesRead] > limit) {
@@ -10546,7 +10546,7 @@ var require_readable = __commonJS({
10546
10546
  }
10547
10547
  function consume(stream, type) {
10548
10548
  assert(!stream[kConsume]);
10549
- return new Promise((resolve, reject) => {
10549
+ return new Promise((resolve2, reject) => {
10550
10550
  if (isUnusable(stream)) {
10551
10551
  const rState = stream._readableState;
10552
10552
  if (rState.destroyed && rState.closeEmitted === false) {
@@ -10561,7 +10561,7 @@ var require_readable = __commonJS({
10561
10561
  stream[kConsume] = {
10562
10562
  type,
10563
10563
  stream,
10564
- resolve,
10564
+ resolve: resolve2,
10565
10565
  reject,
10566
10566
  length: 0,
10567
10567
  body: []
@@ -10635,18 +10635,18 @@ var require_readable = __commonJS({
10635
10635
  return buffer;
10636
10636
  }
10637
10637
  function consumeEnd(consume2, encoding) {
10638
- const { type, body, resolve, stream, length } = consume2;
10638
+ const { type, body, resolve: resolve2, stream, length } = consume2;
10639
10639
  try {
10640
10640
  if (type === "text") {
10641
- resolve(chunksDecode(body, length, encoding));
10641
+ resolve2(chunksDecode(body, length, encoding));
10642
10642
  } else if (type === "json") {
10643
- resolve(JSON.parse(chunksDecode(body, length, encoding)));
10643
+ resolve2(JSON.parse(chunksDecode(body, length, encoding)));
10644
10644
  } else if (type === "arrayBuffer") {
10645
- resolve(chunksConcat(body, length).buffer);
10645
+ resolve2(chunksConcat(body, length).buffer);
10646
10646
  } else if (type === "blob") {
10647
- resolve(new Blob(body, { type: stream[kContentType] }));
10647
+ resolve2(new Blob(body, { type: stream[kContentType] }));
10648
10648
  } else if (type === "bytes") {
10649
- resolve(chunksConcat(body, length));
10649
+ resolve2(chunksConcat(body, length));
10650
10650
  }
10651
10651
  consumeFinish(consume2);
10652
10652
  } catch (err) {
@@ -10835,9 +10835,9 @@ var require_api_request = __commonJS({
10835
10835
  };
10836
10836
  function request(opts, callback) {
10837
10837
  if (callback === void 0) {
10838
- return new Promise((resolve, reject) => {
10838
+ return new Promise((resolve2, reject) => {
10839
10839
  request.call(this, opts, (err, data) => {
10840
- return err ? reject(err) : resolve(data);
10840
+ return err ? reject(err) : resolve2(data);
10841
10841
  });
10842
10842
  });
10843
10843
  }
@@ -11049,9 +11049,9 @@ var require_api_stream = __commonJS({
11049
11049
  };
11050
11050
  function stream(opts, factory, callback) {
11051
11051
  if (callback === void 0) {
11052
- return new Promise((resolve, reject) => {
11052
+ return new Promise((resolve2, reject) => {
11053
11053
  stream.call(this, opts, factory, (err, data) => {
11054
- return err ? reject(err) : resolve(data);
11054
+ return err ? reject(err) : resolve2(data);
11055
11055
  });
11056
11056
  });
11057
11057
  }
@@ -11339,9 +11339,9 @@ var require_api_upgrade = __commonJS({
11339
11339
  };
11340
11340
  function upgrade(opts, callback) {
11341
11341
  if (callback === void 0) {
11342
- return new Promise((resolve, reject) => {
11342
+ return new Promise((resolve2, reject) => {
11343
11343
  upgrade.call(this, opts, (err, data) => {
11344
- return err ? reject(err) : resolve(data);
11344
+ return err ? reject(err) : resolve2(data);
11345
11345
  });
11346
11346
  });
11347
11347
  }
@@ -11434,9 +11434,9 @@ var require_api_connect = __commonJS({
11434
11434
  };
11435
11435
  function connect(opts, callback) {
11436
11436
  if (callback === void 0) {
11437
- return new Promise((resolve, reject) => {
11437
+ return new Promise((resolve2, reject) => {
11438
11438
  connect.call(this, opts, (err, data) => {
11439
- return err ? reject(err) : resolve(data);
11439
+ return err ? reject(err) : resolve2(data);
11440
11440
  });
11441
11441
  });
11442
11442
  }
@@ -12684,7 +12684,7 @@ var require_snapshot_recorder = __commonJS({
12684
12684
  "node_modules/undici/lib/mock/snapshot-recorder.js"(exports2, module2) {
12685
12685
  "use strict";
12686
12686
  var { writeFile, readFile, mkdir } = require("fs/promises");
12687
- var { dirname, resolve } = require("path");
12687
+ var { dirname, resolve: resolve2 } = require("path");
12688
12688
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = require("timers");
12689
12689
  var { InvalidArgumentError, UndiciError } = require_errors();
12690
12690
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -12885,7 +12885,7 @@ var require_snapshot_recorder = __commonJS({
12885
12885
  throw new InvalidArgumentError("Snapshot path is required");
12886
12886
  }
12887
12887
  try {
12888
- const data = await readFile(resolve(path2), "utf8");
12888
+ const data = await readFile(resolve2(path2), "utf8");
12889
12889
  const parsed = JSON.parse(data);
12890
12890
  if (Array.isArray(parsed)) {
12891
12891
  this.#snapshots.clear();
@@ -12914,7 +12914,7 @@ var require_snapshot_recorder = __commonJS({
12914
12914
  if (!path2) {
12915
12915
  throw new InvalidArgumentError("Snapshot path is required");
12916
12916
  }
12917
- const resolvedPath = resolve(path2);
12917
+ const resolvedPath = resolve2(path2);
12918
12918
  await mkdir(dirname(resolvedPath), { recursive: true });
12919
12919
  const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
12920
12920
  hash,
@@ -19471,7 +19471,7 @@ var require_fetch = __commonJS({
19471
19471
  function dispatch({ body }) {
19472
19472
  const url = requestCurrentURL(request);
19473
19473
  const agent = fetchParams.controller.dispatcher;
19474
- return new Promise((resolve, reject) => agent.dispatch(
19474
+ return new Promise((resolve2, reject) => agent.dispatch(
19475
19475
  {
19476
19476
  path: url.pathname + url.search,
19477
19477
  origin: url.origin,
@@ -19551,7 +19551,7 @@ var require_fetch = __commonJS({
19551
19551
  }
19552
19552
  }
19553
19553
  const onError = this.onError.bind(this);
19554
- resolve({
19554
+ resolve2({
19555
19555
  status,
19556
19556
  statusText,
19557
19557
  headersList,
@@ -19594,7 +19594,7 @@ var require_fetch = __commonJS({
19594
19594
  for (let i = 0; i < rawHeaders.length; i += 2) {
19595
19595
  headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true);
19596
19596
  }
19597
- resolve({
19597
+ resolve2({
19598
19598
  status,
19599
19599
  statusText: STATUS_CODES[status],
19600
19600
  headersList,
@@ -23689,7 +23689,6 @@ function isNetworkScopedAddressKey(key) {
23689
23689
  return false;
23690
23690
  }
23691
23691
  var DEFAULT_NOSTR_RELAYS = [
23692
- "wss://relay.unicity.network",
23693
23692
  "wss://relay.damus.io",
23694
23693
  "wss://nos.lol",
23695
23694
  "wss://relay.nostr.band"
@@ -23702,13 +23701,9 @@ var NOSTR_EVENT_KINDS = {
23702
23701
  /** Public broadcast */
23703
23702
  BROADCAST: 1
23704
23703
  };
23705
- var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
23706
- var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
23707
23704
  var DEFAULT_AGGREGATOR_TIMEOUT = 3e4;
23708
23705
  var DEFAULT_BASE_PATH = "m/44'/0'/0'";
23709
23706
  var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
23710
- var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
23711
- var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
23712
23707
  var TEST_NOSTR_RELAYS = [
23713
23708
  "wss://nostr-relay.testnet.unicity.network"
23714
23709
  ];
@@ -23718,10 +23713,19 @@ var DEFAULT_GROUP_RELAYS = [
23718
23713
  var NETWORKS = {
23719
23714
  mainnet: {
23720
23715
  name: "Mainnet",
23721
- aggregatorUrl: DEFAULT_AGGREGATOR_URL,
23722
- nostrRelays: DEFAULT_NOSTR_RELAYS,
23716
+ networkId: 1,
23717
+ // v3 state-transition gateway (networkId 1 comes from the trust base). apiKey is env-injected;
23718
+ // unlike testnet2's, a mainnet gateway key is a SECRET — never commit one.
23719
+ aggregatorUrl: "https://gateway.mainnet.unicity.network",
23720
+ // Mainnet has no relay of its own yet — it shares the testnet relay until one is stood up.
23721
+ // Consequence while shared: nametag bindings for both networks live in ONE namespace, and
23722
+ // since bindings carry no network the cross-network recipient guard can only signal (#734).
23723
+ nostrRelays: TEST_NOSTR_RELAYS,
23723
23724
  groupRelays: DEFAULT_GROUP_RELAYS,
23724
- tokenRegistryUrl: TOKEN_REGISTRY_URL
23725
+ // Published, but currently only the non-fungible base token type — no fungible
23726
+ // coins yet, so those still miss the registry (decimals 0, symbol falls back to
23727
+ // six hex chars). Presentation only: the money path treats coinId as opaque bytes.
23728
+ tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json"
23725
23729
  },
23726
23730
  // v1 cutover: 'testnet' now POINTS AT TESTNET2 (the v2 gateway network). The
23727
23731
  // old goggregator testnet spoke the removed v1 protocol — a v2 engine cannot
@@ -23745,19 +23749,10 @@ var NETWORKS = {
23745
23749
  // reuse testnet infra (shared relays/ipfs)
23746
23750
  groupRelays: DEFAULT_GROUP_RELAYS,
23747
23751
  tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
23748
- },
23749
- // NOTE: mainnet/dev still point at v1-era aggregators. The v2 engine cannot
23750
- // operate against them until their gateways are cut over to the v2 protocol —
23751
- // wallet operations on these networks fail loudly (AGGREGATOR_ERROR) until then.
23752
- dev: {
23753
- name: "Development",
23754
- aggregatorUrl: DEV_AGGREGATOR_URL,
23755
- nostrRelays: TEST_NOSTR_RELAYS,
23756
- groupRelays: DEFAULT_GROUP_RELAYS,
23757
- tokenRegistryUrl: TOKEN_REGISTRY_URL
23758
23752
  }
23759
23753
  };
23760
23754
  var SPHERE_NETWORKS = {
23755
+ mainnet: { id: NETWORKS.mainnet.networkId, name: "mainnet" },
23761
23756
  testnet2: { id: NETWORKS.testnet2.networkId, name: "testnet2" }
23762
23757
  };
23763
23758
  var TIMEOUTS = {
@@ -23773,11 +23768,98 @@ var TIMEOUTS = {
23773
23768
  SYNC_INTERVAL: 6e4
23774
23769
  };
23775
23770
 
23771
+ // core/errors.ts
23772
+ var SphereError = class extends Error {
23773
+ code;
23774
+ cause;
23775
+ /**
23776
+ * #441 deferred-paid linkage: the transferId of the possibly-committed send,
23777
+ * stamped by `sendOnce` before the error leaves so a payment-request consumer
23778
+ * can durably journal request→transfer and resolve 'paid' only when that
23779
+ * transfer actually completes (never optimistically). Present ONLY on
23780
+ * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
23781
+ */
23782
+ transferId;
23783
+ constructor(message, code, cause) {
23784
+ super(message);
23785
+ this.name = "SphereError";
23786
+ this.code = code;
23787
+ this.cause = cause;
23788
+ }
23789
+ };
23790
+
23791
+ // storage/tracked-addresses.ts
23792
+ function isDerivableIndex(value) {
23793
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
23794
+ }
23795
+ function num(value, fallback) {
23796
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
23797
+ }
23798
+ function toEntry(value) {
23799
+ if (typeof value !== "object" || value === null) return null;
23800
+ const e = value;
23801
+ if (!isDerivableIndex(e.index)) return null;
23802
+ return {
23803
+ ...e,
23804
+ index: e.index,
23805
+ hidden: e.hidden === true,
23806
+ createdAt: num(e.createdAt, 0),
23807
+ updatedAt: num(e.updatedAt, 0)
23808
+ };
23809
+ }
23810
+ function parseTrackedAddresses(raw) {
23811
+ if (!raw) return [];
23812
+ let parsed;
23813
+ try {
23814
+ parsed = JSON.parse(raw);
23815
+ } catch {
23816
+ return [];
23817
+ }
23818
+ if (typeof parsed !== "object" || parsed === null) return [];
23819
+ const addresses = parsed.addresses;
23820
+ if (!Array.isArray(addresses)) return [];
23821
+ const entries = [];
23822
+ for (const row of addresses) {
23823
+ const entry = toEntry(row);
23824
+ if (entry) entries.push(entry);
23825
+ }
23826
+ return entries;
23827
+ }
23828
+ function mergeTrackedAddresses(onDisk, incoming) {
23829
+ const merged = /* @__PURE__ */ new Map();
23830
+ for (const entry of onDisk) {
23831
+ if (isDerivableIndex(entry.index)) merged.set(entry.index, entry);
23832
+ }
23833
+ for (const entry of incoming) {
23834
+ if (!isDerivableIndex(entry.index)) {
23835
+ throw new SphereError(
23836
+ `Tracked address index ${String(entry.index)} is not a BIP32 child number: it must be an integer in 0\u20260xffffffff. Refusing the write \u2014 such a row derives another address's keys (1.5 parses to 1) instead of its own.`,
23837
+ "VALIDATION_ERROR"
23838
+ );
23839
+ }
23840
+ const existing = merged.get(entry.index);
23841
+ if (!existing) {
23842
+ merged.set(entry.index, entry);
23843
+ continue;
23844
+ }
23845
+ const winner = entry.updatedAt >= existing.updatedAt ? entry : existing;
23846
+ merged.set(entry.index, {
23847
+ ...existing,
23848
+ ...winner,
23849
+ index: entry.index,
23850
+ createdAt: Math.min(existing.createdAt, entry.createdAt)
23851
+ });
23852
+ }
23853
+ return Array.from(merged.values()).sort((a, b) => a.index - b.index);
23854
+ }
23855
+
23776
23856
  // impl/nodejs/storage/FileStorageProvider.ts
23777
23857
  var FileStorageProvider = class {
23778
23858
  id = "file-storage";
23779
23859
  name = "File Storage";
23780
23860
  type = "local";
23861
+ /** The resolved wallet file — two providers over one path share erasure (#766). */
23862
+ backingStoreId;
23781
23863
  dataDir;
23782
23864
  filePath;
23783
23865
  isTxtMode;
@@ -23787,14 +23869,15 @@ var FileStorageProvider = class {
23787
23869
  _identity = null;
23788
23870
  constructor(config) {
23789
23871
  if (typeof config === "string") {
23790
- this.dataDir = config;
23791
- this.filePath = path.join(config, "wallet.json");
23872
+ this.dataDir = path.resolve(config);
23873
+ this.filePath = path.join(this.dataDir, "wallet.json");
23792
23874
  } else {
23793
- this.dataDir = config.dataDir;
23794
- this.filePath = path.join(config.dataDir, config.fileName ?? "wallet.json");
23875
+ this.dataDir = path.resolve(config.dataDir);
23876
+ this.filePath = path.join(this.dataDir, config.fileName ?? "wallet.json");
23795
23877
  this.network = config.network;
23796
23878
  }
23797
23879
  this.isTxtMode = this.filePath.endsWith(".txt");
23880
+ this.backingStoreId = `file:${this.filePath}`;
23798
23881
  }
23799
23882
  setIdentity(identity) {
23800
23883
  this._identity = identity;
@@ -23891,18 +23974,40 @@ var FileStorageProvider = class {
23891
23974
  }
23892
23975
  await this.save();
23893
23976
  }
23977
+ /** Serializes the read-merge-write below, per provider instance. */
23978
+ trackedWrites = Promise.resolve();
23979
+ /**
23980
+ * Persist the tracked-address registry by MERGING, never replacing.
23981
+ *
23982
+ * Every Sphere over this storage holds its own snapshot and writes it in
23983
+ * full, so a wholesale write drops the addresses this writer never saw
23984
+ * (#766 item 5 — a lost update, reproducible on one network). Concurrent
23985
+ * calls are serialized on `trackedWrites` so a read can never interleave
23986
+ * with another call's write.
23987
+ *
23988
+ * Deliberately PER OBJECT, unlike the browser providers. Two objects over one
23989
+ * `dataDir` share a `backingStoreId` but not this cache, and `save()` rewrites the
23990
+ * WHOLE file from it — so a sibling's *unrelated* `set()` rolls the registry back
23991
+ * regardless of how this one write is serialized. Sharing the chain here would make
23992
+ * the cross-object contract case pass while leaving the provider unsafe. The real
23993
+ * fix is #771 (refresh from disk under a per-file lock, on every write); until then
23994
+ * `backingStoreId` scopes TEARDOWN only. Reviewers keep re-finding this — see the
23995
+ * `unsupported:` note in tests/unit/storage/tracked-addresses-providers.test.ts.
23996
+ */
23894
23997
  async saveTrackedAddresses(entries) {
23895
- await this.set(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES, JSON.stringify({ version: 1, addresses: entries }));
23998
+ const run = this.trackedWrites.then(async () => {
23999
+ const onDisk = parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
24000
+ const file = {
24001
+ version: 1,
24002
+ addresses: mergeTrackedAddresses(onDisk, entries)
24003
+ };
24004
+ await this.set(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES, JSON.stringify(file));
24005
+ });
24006
+ this.trackedWrites = run.then(() => void 0, () => void 0);
24007
+ await run;
23896
24008
  }
23897
24009
  async loadTrackedAddresses() {
23898
- const data = await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
23899
- if (!data) return [];
23900
- try {
23901
- const parsed = JSON.parse(data);
23902
- return parsed.addresses ?? [];
23903
- } catch {
23904
- return [];
23905
- }
24010
+ return parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
23906
24011
  }
23907
24012
  /**
23908
24013
  * Get full storage key with address prefix for per-address keys
@@ -24421,26 +24526,6 @@ var logger = {
24421
24526
  }
24422
24527
  };
24423
24528
 
24424
- // core/errors.ts
24425
- var SphereError = class extends Error {
24426
- code;
24427
- cause;
24428
- /**
24429
- * #441 deferred-paid linkage: the transferId of the possibly-committed send,
24430
- * stamped by `sendOnce` before the error leaves so a payment-request consumer
24431
- * can durably journal request→transfer and resolve 'paid' only when that
24432
- * transfer actually completes (never optimistically). Present ONLY on
24433
- * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
24434
- */
24435
- transferId;
24436
- constructor(message, code, cause) {
24437
- super(message);
24438
- this.name = "SphereError";
24439
- this.code = code;
24440
- this.cause = cause;
24441
- }
24442
- };
24443
-
24444
24529
  // transport/websocket.ts
24445
24530
  function defaultUUIDGenerator() {
24446
24531
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -24613,14 +24698,10 @@ var NostrTransportProvider = class _NostrTransportProvider {
24613
24698
  });
24614
24699
  }
24615
24700
  });
24616
- await Promise.race([
24617
- this.nostrClient.connect(...this.config.relays),
24618
- new Promise(
24619
- (_, reject) => setTimeout(() => reject(new Error(
24620
- `Transport connection timed out after ${this.config.timeout}ms`
24621
- )), this.config.timeout)
24622
- )
24623
- ]);
24701
+ await this.connectWithDeadline(
24702
+ this.nostrClient,
24703
+ `Transport connection timed out after ${this.config.timeout}ms`
24704
+ );
24624
24705
  if (!this.nostrClient.isConnected()) {
24625
24706
  throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
24626
24707
  }
@@ -24635,6 +24716,27 @@ var NostrTransportProvider = class _NostrTransportProvider {
24635
24716
  throw error;
24636
24717
  }
24637
24718
  }
24719
+ /**
24720
+ * Race one client's relay connect against the configured timeout.
24721
+ *
24722
+ * The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
24723
+ * cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
24724
+ * for the whole `config.timeout` after the call has already returned (and
24725
+ * then rejects a promise nobody is listening to any more).
24726
+ */
24727
+ async connectWithDeadline(client, timeoutMessage) {
24728
+ let timer;
24729
+ try {
24730
+ await Promise.race([
24731
+ client.connect(...this.config.relays),
24732
+ new Promise((_, reject) => {
24733
+ timer = setTimeout(() => reject(new Error(timeoutMessage)), this.config.timeout);
24734
+ })
24735
+ ]);
24736
+ } finally {
24737
+ if (timer !== void 0) clearTimeout(timer);
24738
+ }
24739
+ }
24638
24740
  async disconnect() {
24639
24741
  if (this.nostrClient) {
24640
24742
  this.nostrClient.disconnect();
@@ -24753,19 +24855,22 @@ var NostrTransportProvider = class _NostrTransportProvider {
24753
24855
  // TransportProvider Implementation
24754
24856
  // ===========================================================================
24755
24857
  async setIdentity(identity) {
24756
- this.identity = identity;
24757
- this.processedEventIds.clear();
24758
- this.lastEventTs = 0;
24759
- this.lastDmEventTs = 0;
24760
- this.fallbackDmSince = null;
24761
24858
  const secretKey = import_buffer.Buffer.from(identity.privateKey, "hex");
24762
- this.keyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(secretKey);
24763
- const nostrPubkey = this.keyManager.getPublicKeyHex();
24764
- logger.debug("Nostr", "Identity set, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
24859
+ const nextKeyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(secretKey);
24860
+ const nostrPubkey = nextKeyManager.getPublicKeyHex();
24861
+ logger.debug("Nostr", "Identity staged, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
24862
+ const applyStagedIdentity = () => {
24863
+ this.identity = identity;
24864
+ this.keyManager = nextKeyManager;
24865
+ this.processedEventIds.clear();
24866
+ this.lastEventTs = 0;
24867
+ this.lastDmEventTs = 0;
24868
+ this.fallbackDmSince = null;
24869
+ };
24765
24870
  if (this.nostrClient && this.status === "connected") {
24766
24871
  logger.debug("Nostr", "Identity changed while connected - recreating NostrClient");
24767
24872
  const oldClient = this.nostrClient;
24768
- this.nostrClient = new import_nostr_js_sdk.NostrClient(this.keyManager, {
24873
+ const nextClient = new import_nostr_js_sdk.NostrClient(nextKeyManager, {
24769
24874
  autoReconnect: this.config.autoReconnect,
24770
24875
  reconnectIntervalMs: this.config.reconnectDelay,
24771
24876
  maxReconnectIntervalMs: this.config.reconnectDelay * 16,
@@ -24774,7 +24879,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
24774
24879
  queryTimeoutMs: 2e4
24775
24880
  // see same option above for rationale
24776
24881
  });
24777
- this.nostrClient.addConnectionListener({
24882
+ nextClient.addConnectionListener({
24778
24883
  onConnect: (url) => {
24779
24884
  logger.debug("Nostr", "NostrClient connected to relay:", url);
24780
24885
  },
@@ -24788,18 +24893,43 @@ var NostrTransportProvider = class _NostrTransportProvider {
24788
24893
  logger.debug("Nostr", "NostrClient reconnected to relay:", url);
24789
24894
  }
24790
24895
  });
24791
- await Promise.race([
24792
- this.nostrClient.connect(...this.config.relays),
24793
- new Promise(
24794
- (_, reject) => setTimeout(() => reject(new Error(
24795
- `Transport reconnection timed out after ${this.config.timeout}ms`
24796
- )), this.config.timeout)
24797
- )
24798
- ]);
24799
- await this.subscribeToEvents();
24800
- oldClient.disconnect();
24896
+ try {
24897
+ await this.connectWithDeadline(
24898
+ nextClient,
24899
+ `Transport reconnection timed out after ${this.config.timeout}ms`
24900
+ );
24901
+ } catch (error) {
24902
+ try {
24903
+ nextClient.disconnect();
24904
+ } catch {
24905
+ }
24906
+ throw error;
24907
+ }
24908
+ if (this.nostrClient !== oldClient || this.status !== "connected") {
24909
+ try {
24910
+ nextClient.disconnect();
24911
+ } catch {
24912
+ }
24913
+ throw new SphereError(
24914
+ "Transport client changed or left the connected state during setIdentity - identity not applied",
24915
+ "TRANSPORT_ERROR"
24916
+ );
24917
+ }
24918
+ this.nostrClient = nextClient;
24919
+ applyStagedIdentity();
24920
+ try {
24921
+ await this.subscribeToEvents();
24922
+ } finally {
24923
+ try {
24924
+ oldClient.disconnect();
24925
+ } catch {
24926
+ }
24927
+ }
24801
24928
  } else if (this.isConnected()) {
24929
+ applyStagedIdentity();
24802
24930
  await this.subscribeToEvents();
24931
+ } else {
24932
+ applyStagedIdentity();
24803
24933
  }
24804
24934
  }
24805
24935
  setFallbackSince(sinceSeconds) {
@@ -25566,14 +25696,14 @@ var NostrTransportProvider = class _NostrTransportProvider {
25566
25696
  const client = this.nostrClient;
25567
25697
  const events = [];
25568
25698
  let subId;
25569
- await new Promise((resolve, reject) => {
25699
+ await new Promise((resolve2, reject) => {
25570
25700
  let timeout;
25571
25701
  let settled = false;
25572
25702
  const settle = () => {
25573
25703
  if (settled) return;
25574
25704
  settled = true;
25575
25705
  clearTimeout(timeout);
25576
- resolve();
25706
+ resolve2();
25577
25707
  };
25578
25708
  try {
25579
25709
  subId = client.subscribe(walletFilter, {
@@ -25616,7 +25746,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
25616
25746
  const events = [];
25617
25747
  const filter = new import_nostr_js_sdk.Filter(filterObj);
25618
25748
  let subId;
25619
- return new Promise((resolve) => {
25749
+ return new Promise((resolve2) => {
25620
25750
  let settled = false;
25621
25751
  const timeout = setTimeout(() => {
25622
25752
  if (settled) return;
@@ -25628,7 +25758,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
25628
25758
  }
25629
25759
  }
25630
25760
  logger.warn("Nostr", `queryEvents timed out after 15s, returning ${events.length} event(s)`, { kinds: filterObj.kinds, limit: filterObj.limit });
25631
- resolve(events);
25761
+ resolve2(events);
25632
25762
  }, 15e3);
25633
25763
  const settle = () => {
25634
25764
  if (settled) return;
@@ -25640,7 +25770,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
25640
25770
  } catch {
25641
25771
  }
25642
25772
  }
25643
- resolve(events);
25773
+ resolve2(events);
25644
25774
  };
25645
25775
  try {
25646
25776
  subId = client.subscribe(filter, {
@@ -25659,7 +25789,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
25659
25789
  });
25660
25790
  } catch {
25661
25791
  clearTimeout(timeout);
25662
- resolve(events);
25792
+ resolve2(events);
25663
25793
  return;
25664
25794
  }
25665
25795
  });
@@ -26099,26 +26229,6 @@ var UnicityAggregatorProvider = class {
26099
26229
  var UnicityOracleProvider = UnicityAggregatorProvider;
26100
26230
 
26101
26231
  // assets/trustbase.ts
26102
- var TRUSTBASE_TESTNET = {
26103
- version: 1,
26104
- networkId: 3,
26105
- epoch: 1,
26106
- epochStartRound: 1,
26107
- rootNodes: [
26108
- {
26109
- nodeId: "16Uiu2HAkyQRiA7pMgzgLj9GgaBJEJa8zmx9dzqUDa6WxQPJ82ghU",
26110
- sigKey: "0x039afb2acb65f5fbc272d8907f763d0a5d189aadc9b97afdcc5897ea4dd112e68b",
26111
- stake: 1
26112
- }
26113
- ],
26114
- quorumThreshold: 1,
26115
- stateHash: "",
26116
- changeRecordHash: "",
26117
- previousEntryHash: "",
26118
- signatures: {
26119
- "16Uiu2HAkyQRiA7pMgzgLj9GgaBJEJa8zmx9dzqUDa6WxQPJ82ghU": "0xf157c9fdd8a378e3ca70d354ccc4475ab2cd8de360127bc46b0aeab4b453a80f07fd9136a5843b60a8babaff23e20acc8879861f7651440a5e2829f7541b31f100"
26120
- }
26121
- };
26122
26232
  var TRUSTBASE_TESTNET2 = {
26123
26233
  version: 1,
26124
26234
  networkId: 4,
@@ -26157,8 +26267,44 @@ var TRUSTBASE_TESTNET2 = {
26157
26267
  "16Uiu2HAmU5T1TqKyKkUB3sitWb431hVFfp9nqvaLgVagQF6ywLFF": "0x9f28f3e3c50d4bfdaf54d7290fbbe311fffb3be2281a1d211be56818fa43d06a2e9a81925e9d365e552c8b772c99285a686de1d9fb581920be7524c7a5d467a301"
26158
26268
  }
26159
26269
  };
26160
- var TRUSTBASE_MAINNET = null;
26161
- var TRUSTBASE_DEV = TRUSTBASE_TESTNET;
26270
+ var TRUSTBASE_MAINNET = {
26271
+ version: 1,
26272
+ networkId: 1,
26273
+ epoch: 1,
26274
+ epochStartRound: 1,
26275
+ rootNodes: [
26276
+ {
26277
+ nodeId: "16Uiu2HAkvk4EpXoyh7SVLTX3TcsZkjbni5jY1sBgJ7qCEWfx1Gma",
26278
+ sigKey: "0x03059e30b5c980ce92e2c6d4e14775838be91a6f0e993f3a4899ab459c4b5225f2",
26279
+ stake: 1
26280
+ },
26281
+ {
26282
+ nodeId: "16Uiu2HAmF9Mq9pH9yUTdao3TpJeFZY9oX9gyMqfxggVA4fNmzmXr",
26283
+ sigKey: "0x03a514db8b17b1c79b4a6affeb5d88a129c4d5ff0f92c8f86c311971b0beb94335",
26284
+ stake: 1
26285
+ },
26286
+ {
26287
+ nodeId: "16Uiu2HAmLAHTSMvZ2apRRtBN9piYVhaK2i7sTMaQeSWSxsLh75TG",
26288
+ sigKey: "0x02e5669b2daa5d1afccfe66603f7d3fe171ecbd7ed891487ab3683cdebc3c3cd8b",
26289
+ stake: 1
26290
+ },
26291
+ {
26292
+ nodeId: "16Uiu2HAmNNaWfK4RTTAq7Ap8w2yZUryrEejqmmGf44ogMaYEnkJ4",
26293
+ sigKey: "0x03ffee964da8ca276d4fc1cf391929e6759db106ef1d0aea998154b51e5a7fb1b7",
26294
+ stake: 1
26295
+ }
26296
+ ],
26297
+ quorumThreshold: 3,
26298
+ stateHash: "",
26299
+ changeRecordHash: "",
26300
+ previousEntryHash: "",
26301
+ signatures: {
26302
+ "16Uiu2HAkvk4EpXoyh7SVLTX3TcsZkjbni5jY1sBgJ7qCEWfx1Gma": "0xc2f1d67dbb47fe4dcf502301d8cd5021b8903c0d3385781653d09c703d566a3a3a4e59c75792b36f929e32564ddc14c40ef728c0bde5859774d08d802de2318c00",
26303
+ "16Uiu2HAmF9Mq9pH9yUTdao3TpJeFZY9oX9gyMqfxggVA4fNmzmXr": "0x3e3ad3a7540daa0eded457e31d2ebcacfb354036798f49f63433d26c44857e76157e4abd36cbb1d39de8cdf6e1d33f88b250077d4347c3e5bf0f35a64f2d815300",
26304
+ "16Uiu2HAmLAHTSMvZ2apRRtBN9piYVhaK2i7sTMaQeSWSxsLh75TG": "0x6101bd9df8ab87439d90b52cd74727bc8bf31dd74173c8f731ec3ea501d3d86556ba7c0e84643337959f3be0e448637c301e8e0b28196e780d4f2ff930d9062100",
26305
+ "16Uiu2HAmNNaWfK4RTTAq7Ap8w2yZUryrEejqmmGf44ogMaYEnkJ4": "0xda13c777b832425e5cbd8d6e49eb08ddbde35c1e4a9bee8bd495c3883f039e434b64560865c3bc52b68d65780932b1f2bf8971f245d2244face822af5984945c00"
26306
+ }
26307
+ };
26162
26308
 
26163
26309
  // impl/shared/trustbase-loader.ts
26164
26310
  function getEmbeddedTrustBase(network) {
@@ -26171,8 +26317,6 @@ function getEmbeddedTrustBase(network) {
26171
26317
  case "testnet":
26172
26318
  case "testnet2":
26173
26319
  return TRUSTBASE_TESTNET2;
26174
- case "dev":
26175
- return TRUSTBASE_DEV;
26176
26320
  default:
26177
26321
  throw new SphereError(`getEmbeddedTrustBase: unknown network "${network}"`, "INVALID_CONFIG");
26178
26322
  }
@@ -26268,6 +26412,7 @@ function createWalletApiProviders(base, config) {
26268
26412
 
26269
26413
  // impl/shared/network.ts
26270
26414
  var EXPECTED_NETWORK_ID = {
26415
+ mainnet: 1,
26271
26416
  // v1 cutover: 'testnet' is an alias of testnet2 (the v2 gateway network),
26272
26417
  // so both expect the testnet2 trust base (networkId 4).
26273
26418
  testnet: 4,
@@ -26288,7 +26433,7 @@ function assertNetworkConsistency(network) {
26288
26433
  );
26289
26434
  }
26290
26435
  const expected = EXPECTED_NETWORK_ID[network];
26291
- if (expected !== void 0 && networkId !== expected) {
26436
+ if (networkId !== expected) {
26292
26437
  throw new SphereError(
26293
26438
  `Network "${network}" trust base networkId ${networkId} does not match expected ${expected} (wrong trust base wired to this network).`,
26294
26439
  "INVALID_CONFIG"
@@ -26553,419 +26698,6 @@ function createPriceProvider(config) {
26553
26698
  }
26554
26699
  }
26555
26700
 
26556
- // registry/TokenRegistry.ts
26557
- var FETCH_TIMEOUT_MS = 1e4;
26558
- var TokenRegistry = class _TokenRegistry {
26559
- static instance = null;
26560
- definitionsById;
26561
- definitionsBySymbol;
26562
- definitionsByName;
26563
- // Remote refresh state
26564
- remoteUrl = null;
26565
- storage = null;
26566
- refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
26567
- refreshTimer = null;
26568
- lastRefreshAt = 0;
26569
- refreshPromise = null;
26570
- initialLoadPromise = null;
26571
- constructor() {
26572
- this.definitionsById = /* @__PURE__ */ new Map();
26573
- this.definitionsBySymbol = /* @__PURE__ */ new Map();
26574
- this.definitionsByName = /* @__PURE__ */ new Map();
26575
- }
26576
- /**
26577
- * Get singleton instance of TokenRegistry
26578
- */
26579
- static getInstance() {
26580
- if (!_TokenRegistry.instance) {
26581
- _TokenRegistry.instance = new _TokenRegistry();
26582
- }
26583
- return _TokenRegistry.instance;
26584
- }
26585
- /**
26586
- * Configure remote registry refresh with persistent caching.
26587
- *
26588
- * On first call:
26589
- * 1. Loads cached data from StorageProvider (if available and fresh)
26590
- * 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
26591
- *
26592
- * @param options - Configuration options
26593
- * @param options.remoteUrl - Remote URL to fetch definitions from
26594
- * @param options.storage - StorageProvider for persistent caching
26595
- * @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
26596
- * @param options.autoRefresh - Start auto-refresh immediately (default: true)
26597
- */
26598
- static configure(options) {
26599
- const instance = _TokenRegistry.getInstance();
26600
- if (options.remoteUrl !== void 0) {
26601
- instance.remoteUrl = options.remoteUrl;
26602
- }
26603
- if (options.storage !== void 0) {
26604
- instance.storage = options.storage;
26605
- }
26606
- if (options.refreshIntervalMs !== void 0) {
26607
- instance.refreshIntervalMs = options.refreshIntervalMs;
26608
- }
26609
- const autoRefresh = options.autoRefresh ?? true;
26610
- instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
26611
- }
26612
- /**
26613
- * Reset the singleton instance (useful for testing).
26614
- * Stops auto-refresh if running.
26615
- */
26616
- static resetInstance() {
26617
- if (_TokenRegistry.instance) {
26618
- _TokenRegistry.instance.stopAutoRefresh();
26619
- }
26620
- _TokenRegistry.instance = null;
26621
- }
26622
- /**
26623
- * Destroy the singleton: stop auto-refresh and reset.
26624
- */
26625
- static destroy() {
26626
- _TokenRegistry.resetInstance();
26627
- }
26628
- /**
26629
- * Wait for the initial data load (cache or remote) to complete.
26630
- * Returns true if data was loaded, false if not (timeout or no data source).
26631
- *
26632
- * @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
26633
- */
26634
- static async waitForReady(timeoutMs = 1e4) {
26635
- const instance = _TokenRegistry.getInstance();
26636
- if (!instance.initialLoadPromise) {
26637
- return instance.definitionsById.size > 0;
26638
- }
26639
- if (timeoutMs <= 0) {
26640
- return instance.initialLoadPromise;
26641
- }
26642
- return Promise.race([
26643
- instance.initialLoadPromise,
26644
- new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
26645
- ]);
26646
- }
26647
- // ===========================================================================
26648
- // Initial Load
26649
- // ===========================================================================
26650
- /**
26651
- * Perform initial data load: try cache first, fall back to remote fetch.
26652
- * After initial data is available, start periodic auto-refresh if configured.
26653
- */
26654
- async performInitialLoad(autoRefresh) {
26655
- let loaded = false;
26656
- if (this.storage) {
26657
- loaded = await this.loadFromCache();
26658
- }
26659
- if (loaded) {
26660
- if (autoRefresh && this.remoteUrl) {
26661
- this.startAutoRefresh();
26662
- }
26663
- return true;
26664
- }
26665
- if (autoRefresh && this.remoteUrl) {
26666
- loaded = await this.refreshFromRemote();
26667
- this.stopAutoRefresh();
26668
- this.refreshTimer = setInterval(() => {
26669
- this.refreshFromRemote();
26670
- }, this.refreshIntervalMs);
26671
- return loaded;
26672
- }
26673
- return false;
26674
- }
26675
- // ===========================================================================
26676
- // Cache (StorageProvider)
26677
- // ===========================================================================
26678
- /**
26679
- * Per-network cache keys. The registry cache MUST be namespaced by remoteUrl:
26680
- * testnet and testnet2 ship DIFFERENT coinIds for the same symbol, so a single
26681
- * global key let a stale testnet snapshot be served for a testnet2 request —
26682
- * wrong/missing icons AND self-mint resolving the wrong-network coinId. Keying
26683
- * by remoteUrl isolates the snapshots so a network can never read another's.
26684
- */
26685
- cacheKeys() {
26686
- const base = STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE;
26687
- const baseTs = STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS;
26688
- if (!this.remoteUrl) return { data: base, ts: baseTs };
26689
- return { data: `${base}:${this.remoteUrl}`, ts: `${baseTs}:${this.remoteUrl}` };
26690
- }
26691
- /**
26692
- * Load definitions from StorageProvider cache.
26693
- * Only applies if cache exists and is fresh (within refreshIntervalMs).
26694
- */
26695
- async loadFromCache() {
26696
- if (!this.storage) return false;
26697
- try {
26698
- const { data: dataKey, ts: tsKey } = this.cacheKeys();
26699
- const [cached, cachedTs] = await Promise.all([
26700
- this.storage.get(dataKey),
26701
- this.storage.get(tsKey)
26702
- ]);
26703
- if (!cached || !cachedTs) return false;
26704
- const ts = parseInt(cachedTs, 10);
26705
- if (isNaN(ts)) return false;
26706
- const age = Date.now() - ts;
26707
- if (age > this.refreshIntervalMs) return false;
26708
- if (this.lastRefreshAt > ts) return false;
26709
- const data = JSON.parse(cached);
26710
- if (!this.isValidDefinitionsArray(data)) return false;
26711
- this.applyDefinitions(data);
26712
- this.lastRefreshAt = ts;
26713
- return true;
26714
- } catch {
26715
- return false;
26716
- }
26717
- }
26718
- /**
26719
- * Save definitions to StorageProvider cache.
26720
- */
26721
- async saveToCache(definitions) {
26722
- if (!this.storage) return;
26723
- try {
26724
- const { data: dataKey, ts: tsKey } = this.cacheKeys();
26725
- await Promise.all([
26726
- this.storage.set(dataKey, JSON.stringify(definitions)),
26727
- this.storage.set(tsKey, String(Date.now()))
26728
- ]);
26729
- } catch {
26730
- }
26731
- }
26732
- // ===========================================================================
26733
- // Remote Refresh
26734
- // ===========================================================================
26735
- /**
26736
- * Apply an array of token definitions to the internal maps.
26737
- * Clears existing data before applying.
26738
- */
26739
- applyDefinitions(definitions) {
26740
- this.definitionsById.clear();
26741
- this.definitionsBySymbol.clear();
26742
- this.definitionsByName.clear();
26743
- for (const def of definitions) {
26744
- const idLower = def.id.toLowerCase();
26745
- this.definitionsById.set(idLower, def);
26746
- if (def.symbol) {
26747
- this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
26748
- }
26749
- this.definitionsByName.set(def.name.toLowerCase(), def);
26750
- }
26751
- }
26752
- /**
26753
- * Validate that data is an array of objects with 'id' field
26754
- */
26755
- isValidDefinitionsArray(data) {
26756
- return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
26757
- }
26758
- /**
26759
- * Fetch token definitions from the remote URL and update the registry.
26760
- * On success, also persists to StorageProvider cache.
26761
- * Returns true on success, false on failure. On failure, existing data is preserved.
26762
- * Concurrent calls are deduplicated — only one fetch runs at a time.
26763
- */
26764
- async refreshFromRemote() {
26765
- if (!this.remoteUrl) {
26766
- return false;
26767
- }
26768
- if (this.refreshPromise) {
26769
- return this.refreshPromise;
26770
- }
26771
- this.refreshPromise = this.doRefresh();
26772
- try {
26773
- return await this.refreshPromise;
26774
- } finally {
26775
- this.refreshPromise = null;
26776
- }
26777
- }
26778
- async doRefresh() {
26779
- try {
26780
- const controller = new AbortController();
26781
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
26782
- let response;
26783
- try {
26784
- response = await fetch(this.remoteUrl, {
26785
- headers: { Accept: "application/json" },
26786
- signal: controller.signal
26787
- });
26788
- } finally {
26789
- clearTimeout(timer);
26790
- }
26791
- if (!response.ok) {
26792
- logger.warn("TokenRegistry", `Remote fetch failed: HTTP ${response.status} ${response.statusText}`);
26793
- return false;
26794
- }
26795
- const data = await response.json();
26796
- if (!this.isValidDefinitionsArray(data)) {
26797
- logger.warn("TokenRegistry", "Remote data is not a valid token definitions array");
26798
- return false;
26799
- }
26800
- const definitions = data;
26801
- this.applyDefinitions(definitions);
26802
- this.lastRefreshAt = Date.now();
26803
- this.saveToCache(definitions);
26804
- return true;
26805
- } catch (error) {
26806
- const message = error instanceof Error ? error.message : String(error);
26807
- logger.warn("TokenRegistry", `Remote refresh failed: ${message}`);
26808
- return false;
26809
- }
26810
- }
26811
- /**
26812
- * Start periodic auto-refresh from the remote URL.
26813
- * Does an immediate fetch, then repeats at the configured interval.
26814
- */
26815
- startAutoRefresh(intervalMs) {
26816
- this.stopAutoRefresh();
26817
- if (intervalMs !== void 0) {
26818
- this.refreshIntervalMs = intervalMs;
26819
- }
26820
- this.refreshFromRemote();
26821
- this.refreshTimer = setInterval(() => {
26822
- this.refreshFromRemote();
26823
- }, this.refreshIntervalMs);
26824
- }
26825
- /**
26826
- * Stop periodic auto-refresh
26827
- */
26828
- stopAutoRefresh() {
26829
- if (this.refreshTimer !== null) {
26830
- clearInterval(this.refreshTimer);
26831
- this.refreshTimer = null;
26832
- }
26833
- }
26834
- /**
26835
- * Timestamp of the last successful remote refresh (0 if never refreshed)
26836
- */
26837
- getLastRefreshAt() {
26838
- return this.lastRefreshAt;
26839
- }
26840
- // ===========================================================================
26841
- // Lookup Methods
26842
- // ===========================================================================
26843
- /**
26844
- * Get token definition by hex coin ID
26845
- * @param coinId - 64-character hex string
26846
- * @returns Token definition or undefined if not found
26847
- */
26848
- getDefinition(coinId) {
26849
- if (!coinId) return void 0;
26850
- return this.definitionsById.get(coinId.toLowerCase());
26851
- }
26852
- /**
26853
- * Get token definition by symbol (e.g., "UCT", "BTC")
26854
- * @param symbol - Token symbol (case-insensitive)
26855
- * @returns Token definition or undefined if not found
26856
- */
26857
- getDefinitionBySymbol(symbol) {
26858
- if (!symbol) return void 0;
26859
- return this.definitionsBySymbol.get(symbol.toUpperCase());
26860
- }
26861
- /**
26862
- * Get token definition by name (e.g., "bitcoin", "ethereum")
26863
- * @param name - Token name (case-insensitive)
26864
- * @returns Token definition or undefined if not found
26865
- */
26866
- getDefinitionByName(name) {
26867
- if (!name) return void 0;
26868
- return this.definitionsByName.get(name.toLowerCase());
26869
- }
26870
- /**
26871
- * Get token symbol for a coin ID
26872
- * @param coinId - 64-character hex string
26873
- * @returns Symbol (e.g., "UCT") or truncated ID if not found
26874
- */
26875
- getSymbol(coinId) {
26876
- const def = this.getDefinition(coinId);
26877
- if (def?.symbol) {
26878
- return def.symbol;
26879
- }
26880
- return coinId.slice(0, 6).toUpperCase();
26881
- }
26882
- /**
26883
- * Get token name for a coin ID
26884
- * @param coinId - 64-character hex string
26885
- * @returns Name (e.g., "Bitcoin") or coin ID if not found
26886
- */
26887
- getName(coinId) {
26888
- const def = this.getDefinition(coinId);
26889
- if (def?.name) {
26890
- return def.name.charAt(0).toUpperCase() + def.name.slice(1);
26891
- }
26892
- return coinId;
26893
- }
26894
- /**
26895
- * Get decimal places for a coin ID
26896
- * @param coinId - 64-character hex string
26897
- * @returns Decimals or 0 if not found
26898
- */
26899
- getDecimals(coinId) {
26900
- const def = this.getDefinition(coinId);
26901
- return def?.decimals ?? 0;
26902
- }
26903
- /**
26904
- * Get icon URL for a coin ID
26905
- * @param coinId - 64-character hex string
26906
- * @param preferPng - Prefer PNG format over SVG
26907
- * @returns Icon URL or null if not found
26908
- */
26909
- getIconUrl(coinId, preferPng = true) {
26910
- const def = this.getDefinition(coinId);
26911
- if (!def?.icons || def.icons.length === 0) {
26912
- return null;
26913
- }
26914
- if (preferPng) {
26915
- const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
26916
- if (pngIcon) return pngIcon.url;
26917
- }
26918
- return def.icons[0].url;
26919
- }
26920
- /**
26921
- * Check if a coin ID is known in the registry
26922
- * @param coinId - 64-character hex string
26923
- * @returns true if the coin is in the registry
26924
- */
26925
- isKnown(coinId) {
26926
- return this.definitionsById.has(coinId.toLowerCase());
26927
- }
26928
- /**
26929
- * Get all token definitions
26930
- * @returns Array of all token definitions
26931
- */
26932
- getAllDefinitions() {
26933
- return Array.from(this.definitionsById.values());
26934
- }
26935
- /**
26936
- * Get all fungible token definitions
26937
- * @returns Array of fungible token definitions
26938
- */
26939
- getFungibleTokens() {
26940
- return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
26941
- }
26942
- /**
26943
- * Get all non-fungible token definitions
26944
- * @returns Array of non-fungible token definitions
26945
- */
26946
- getNonFungibleTokens() {
26947
- return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
26948
- }
26949
- /**
26950
- * Get coin ID by symbol
26951
- * @param symbol - Token symbol (e.g., "UCT")
26952
- * @returns Coin ID hex string or undefined if not found
26953
- */
26954
- getCoinIdBySymbol(symbol) {
26955
- const def = this.getDefinitionBySymbol(symbol);
26956
- return def?.id;
26957
- }
26958
- /**
26959
- * Get coin ID by name
26960
- * @param name - Token name (e.g., "bitcoin")
26961
- * @returns Coin ID hex string or undefined if not found
26962
- */
26963
- getCoinIdByName(name) {
26964
- const def = this.getDefinitionByName(name);
26965
- return def?.id;
26966
- }
26967
- };
26968
-
26969
26701
  // impl/shared/resolvers.ts
26970
26702
  function getNetworkConfig(network = "mainnet") {
26971
26703
  return NETWORKS[network];
@@ -27057,8 +26789,7 @@ function createNodeProviders(config) {
27057
26789
  }
27058
26790
  const network = config.network;
27059
26791
  assertNetworkConsistency(network);
27060
- const globalDebug = config?.debug ?? false;
27061
- logger.configure({ debug: globalDebug });
26792
+ if (config?.debug !== void 0) logger.configure({ debug: config.debug });
27062
26793
  if (config?.transport?.debug) logger.setTagDebug("Nostr", true);
27063
26794
  if (config?.oracle?.debug) logger.setTagDebug("Aggregator", true);
27064
26795
  if (config?.price?.debug) logger.setTagDebug("Price", true);
@@ -27072,8 +26803,6 @@ function createNodeProviders(config) {
27072
26803
  const priceConfig = resolvePriceConfig(config?.price, storage);
27073
26804
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
27074
26805
  const market = resolveMarketConfig(config?.market);
27075
- const networkConfig = getNetworkConfig(network);
27076
- TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
27077
26806
  return {
27078
26807
  storage,
27079
26808
  groupChat,