@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
@@ -2610,9 +2610,9 @@ var require_dispatcher_base = __commonJS({
2610
2610
  }
2611
2611
  close(callback) {
2612
2612
  if (callback === void 0) {
2613
- return new Promise((resolve, reject) => {
2613
+ return new Promise((resolve2, reject) => {
2614
2614
  this.close((err, data) => {
2615
- return err ? reject(err) : resolve(data);
2615
+ return err ? reject(err) : resolve2(data);
2616
2616
  });
2617
2617
  });
2618
2618
  }
@@ -2650,9 +2650,9 @@ var require_dispatcher_base = __commonJS({
2650
2650
  err = null;
2651
2651
  }
2652
2652
  if (callback === void 0) {
2653
- return new Promise((resolve, reject) => {
2653
+ return new Promise((resolve2, reject) => {
2654
2654
  this.destroy(err, (err2, data) => {
2655
- return err2 ? reject(err2) : resolve(data);
2655
+ return err2 ? reject(err2) : resolve2(data);
2656
2656
  });
2657
2657
  });
2658
2658
  }
@@ -6106,8 +6106,8 @@ var require_promise = __commonJS({
6106
6106
  function createDeferredPromise() {
6107
6107
  let res;
6108
6108
  let rej;
6109
- const promise = new Promise((resolve, reject) => {
6110
- res = resolve;
6109
+ const promise = new Promise((resolve2, reject) => {
6110
+ res = resolve2;
6111
6111
  rej = reject;
6112
6112
  });
6113
6113
  return { promise, resolve: res, reject: rej };
@@ -7414,12 +7414,12 @@ upgrade: ${upgrade}\r
7414
7414
  cb();
7415
7415
  }
7416
7416
  }
7417
- const waitForDrain = () => new Promise((resolve, reject) => {
7417
+ const waitForDrain = () => new Promise((resolve2, reject) => {
7418
7418
  assert(callback === null);
7419
7419
  if (socket[kError]) {
7420
7420
  reject(socket[kError]);
7421
7421
  } else {
7422
- callback = resolve;
7422
+ callback = resolve2;
7423
7423
  }
7424
7424
  });
7425
7425
  socket.on("close", onDrain).on("drain", onDrain);
@@ -8231,12 +8231,12 @@ var require_client_h2 = __commonJS({
8231
8231
  cb();
8232
8232
  }
8233
8233
  }
8234
- const waitForDrain = () => new Promise((resolve, reject) => {
8234
+ const waitForDrain = () => new Promise((resolve2, reject) => {
8235
8235
  assert(callback === null);
8236
8236
  if (socket[kError]) {
8237
8237
  reject(socket[kError]);
8238
8238
  } else {
8239
- callback = resolve;
8239
+ callback = resolve2;
8240
8240
  }
8241
8241
  });
8242
8242
  h2stream.on("close", onDrain).on("drain", onDrain);
@@ -8538,16 +8538,16 @@ var require_client = __commonJS({
8538
8538
  return this[kNeedDrain] < 2;
8539
8539
  }
8540
8540
  [kClose]() {
8541
- return new Promise((resolve) => {
8541
+ return new Promise((resolve2) => {
8542
8542
  if (this[kSize]) {
8543
- this[kClosedResolve] = resolve;
8543
+ this[kClosedResolve] = resolve2;
8544
8544
  } else {
8545
- resolve(null);
8545
+ resolve2(null);
8546
8546
  }
8547
8547
  });
8548
8548
  }
8549
8549
  [kDestroy](err) {
8550
- return new Promise((resolve) => {
8550
+ return new Promise((resolve2) => {
8551
8551
  const requests = this[kQueue].splice(this[kPendingIdx]);
8552
8552
  for (let i = 0; i < requests.length; i++) {
8553
8553
  const request = requests[i];
@@ -8558,7 +8558,7 @@ var require_client = __commonJS({
8558
8558
  this[kClosedResolve]();
8559
8559
  this[kClosedResolve] = null;
8560
8560
  }
8561
- resolve(null);
8561
+ resolve2(null);
8562
8562
  };
8563
8563
  if (this[kHTTPContext]) {
8564
8564
  this[kHTTPContext].destroy(err, callback);
@@ -8949,8 +8949,8 @@ var require_pool_base = __commonJS({
8949
8949
  }
8950
8950
  return Promise.all(closeAll);
8951
8951
  } else {
8952
- return new Promise((resolve) => {
8953
- this[kClosedResolve] = resolve;
8952
+ return new Promise((resolve2) => {
8953
+ this[kClosedResolve] = resolve2;
8954
8954
  });
8955
8955
  }
8956
8956
  }
@@ -10479,7 +10479,7 @@ var require_readable = __commonJS({
10479
10479
  if (this._readableState.closeEmitted) {
10480
10480
  return Promise.resolve(null);
10481
10481
  }
10482
- return new Promise((resolve, reject) => {
10482
+ return new Promise((resolve2, reject) => {
10483
10483
  if (this[kContentLength] && this[kContentLength] > limit || this[kBytesRead] > limit) {
10484
10484
  this.destroy(new AbortError());
10485
10485
  }
@@ -10493,11 +10493,11 @@ var require_readable = __commonJS({
10493
10493
  if (signal.aborted) {
10494
10494
  reject(signal.reason ?? new AbortError());
10495
10495
  } else {
10496
- resolve(null);
10496
+ resolve2(null);
10497
10497
  }
10498
10498
  });
10499
10499
  } else {
10500
- this.on("close", resolve);
10500
+ this.on("close", resolve2);
10501
10501
  }
10502
10502
  this.on("error", noop).on("data", () => {
10503
10503
  if (this[kBytesRead] > limit) {
@@ -10525,7 +10525,7 @@ var require_readable = __commonJS({
10525
10525
  }
10526
10526
  function consume(stream, type) {
10527
10527
  assert(!stream[kConsume]);
10528
- return new Promise((resolve, reject) => {
10528
+ return new Promise((resolve2, reject) => {
10529
10529
  if (isUnusable(stream)) {
10530
10530
  const rState = stream._readableState;
10531
10531
  if (rState.destroyed && rState.closeEmitted === false) {
@@ -10540,7 +10540,7 @@ var require_readable = __commonJS({
10540
10540
  stream[kConsume] = {
10541
10541
  type,
10542
10542
  stream,
10543
- resolve,
10543
+ resolve: resolve2,
10544
10544
  reject,
10545
10545
  length: 0,
10546
10546
  body: []
@@ -10614,18 +10614,18 @@ var require_readable = __commonJS({
10614
10614
  return buffer;
10615
10615
  }
10616
10616
  function consumeEnd(consume2, encoding) {
10617
- const { type, body, resolve, stream, length } = consume2;
10617
+ const { type, body, resolve: resolve2, stream, length } = consume2;
10618
10618
  try {
10619
10619
  if (type === "text") {
10620
- resolve(chunksDecode(body, length, encoding));
10620
+ resolve2(chunksDecode(body, length, encoding));
10621
10621
  } else if (type === "json") {
10622
- resolve(JSON.parse(chunksDecode(body, length, encoding)));
10622
+ resolve2(JSON.parse(chunksDecode(body, length, encoding)));
10623
10623
  } else if (type === "arrayBuffer") {
10624
- resolve(chunksConcat(body, length).buffer);
10624
+ resolve2(chunksConcat(body, length).buffer);
10625
10625
  } else if (type === "blob") {
10626
- resolve(new Blob(body, { type: stream[kContentType] }));
10626
+ resolve2(new Blob(body, { type: stream[kContentType] }));
10627
10627
  } else if (type === "bytes") {
10628
- resolve(chunksConcat(body, length));
10628
+ resolve2(chunksConcat(body, length));
10629
10629
  }
10630
10630
  consumeFinish(consume2);
10631
10631
  } catch (err) {
@@ -10814,9 +10814,9 @@ var require_api_request = __commonJS({
10814
10814
  };
10815
10815
  function request(opts, callback) {
10816
10816
  if (callback === void 0) {
10817
- return new Promise((resolve, reject) => {
10817
+ return new Promise((resolve2, reject) => {
10818
10818
  request.call(this, opts, (err, data) => {
10819
- return err ? reject(err) : resolve(data);
10819
+ return err ? reject(err) : resolve2(data);
10820
10820
  });
10821
10821
  });
10822
10822
  }
@@ -11028,9 +11028,9 @@ var require_api_stream = __commonJS({
11028
11028
  };
11029
11029
  function stream(opts, factory, callback) {
11030
11030
  if (callback === void 0) {
11031
- return new Promise((resolve, reject) => {
11031
+ return new Promise((resolve2, reject) => {
11032
11032
  stream.call(this, opts, factory, (err, data) => {
11033
- return err ? reject(err) : resolve(data);
11033
+ return err ? reject(err) : resolve2(data);
11034
11034
  });
11035
11035
  });
11036
11036
  }
@@ -11318,9 +11318,9 @@ var require_api_upgrade = __commonJS({
11318
11318
  };
11319
11319
  function upgrade(opts, callback) {
11320
11320
  if (callback === void 0) {
11321
- return new Promise((resolve, reject) => {
11321
+ return new Promise((resolve2, reject) => {
11322
11322
  upgrade.call(this, opts, (err, data) => {
11323
- return err ? reject(err) : resolve(data);
11323
+ return err ? reject(err) : resolve2(data);
11324
11324
  });
11325
11325
  });
11326
11326
  }
@@ -11413,9 +11413,9 @@ var require_api_connect = __commonJS({
11413
11413
  };
11414
11414
  function connect(opts, callback) {
11415
11415
  if (callback === void 0) {
11416
- return new Promise((resolve, reject) => {
11416
+ return new Promise((resolve2, reject) => {
11417
11417
  connect.call(this, opts, (err, data) => {
11418
- return err ? reject(err) : resolve(data);
11418
+ return err ? reject(err) : resolve2(data);
11419
11419
  });
11420
11420
  });
11421
11421
  }
@@ -12663,7 +12663,7 @@ var require_snapshot_recorder = __commonJS({
12663
12663
  "node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
12664
12664
  "use strict";
12665
12665
  var { writeFile, readFile, mkdir } = __require("fs/promises");
12666
- var { dirname, resolve } = __require("path");
12666
+ var { dirname, resolve: resolve2 } = __require("path");
12667
12667
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("timers");
12668
12668
  var { InvalidArgumentError, UndiciError } = require_errors();
12669
12669
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -12864,7 +12864,7 @@ var require_snapshot_recorder = __commonJS({
12864
12864
  throw new InvalidArgumentError("Snapshot path is required");
12865
12865
  }
12866
12866
  try {
12867
- const data = await readFile(resolve(path2), "utf8");
12867
+ const data = await readFile(resolve2(path2), "utf8");
12868
12868
  const parsed = JSON.parse(data);
12869
12869
  if (Array.isArray(parsed)) {
12870
12870
  this.#snapshots.clear();
@@ -12893,7 +12893,7 @@ var require_snapshot_recorder = __commonJS({
12893
12893
  if (!path2) {
12894
12894
  throw new InvalidArgumentError("Snapshot path is required");
12895
12895
  }
12896
- const resolvedPath = resolve(path2);
12896
+ const resolvedPath = resolve2(path2);
12897
12897
  await mkdir(dirname(resolvedPath), { recursive: true });
12898
12898
  const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
12899
12899
  hash,
@@ -19450,7 +19450,7 @@ var require_fetch = __commonJS({
19450
19450
  function dispatch({ body }) {
19451
19451
  const url = requestCurrentURL(request);
19452
19452
  const agent = fetchParams.controller.dispatcher;
19453
- return new Promise((resolve, reject) => agent.dispatch(
19453
+ return new Promise((resolve2, reject) => agent.dispatch(
19454
19454
  {
19455
19455
  path: url.pathname + url.search,
19456
19456
  origin: url.origin,
@@ -19530,7 +19530,7 @@ var require_fetch = __commonJS({
19530
19530
  }
19531
19531
  }
19532
19532
  const onError = this.onError.bind(this);
19533
- resolve({
19533
+ resolve2({
19534
19534
  status,
19535
19535
  statusText,
19536
19536
  headersList,
@@ -19573,7 +19573,7 @@ var require_fetch = __commonJS({
19573
19573
  for (let i = 0; i < rawHeaders.length; i += 2) {
19574
19574
  headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true);
19575
19575
  }
19576
- resolve({
19576
+ resolve2({
19577
19577
  status,
19578
19578
  statusText: STATUS_CODES[status],
19579
19579
  headersList,
@@ -23648,7 +23648,6 @@ function isNetworkScopedAddressKey(key) {
23648
23648
  return false;
23649
23649
  }
23650
23650
  var DEFAULT_NOSTR_RELAYS = [
23651
- "wss://relay.unicity.network",
23652
23651
  "wss://relay.damus.io",
23653
23652
  "wss://nos.lol",
23654
23653
  "wss://relay.nostr.band"
@@ -23661,13 +23660,9 @@ var NOSTR_EVENT_KINDS = {
23661
23660
  /** Public broadcast */
23662
23661
  BROADCAST: 1
23663
23662
  };
23664
- var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
23665
- var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
23666
23663
  var DEFAULT_AGGREGATOR_TIMEOUT = 3e4;
23667
23664
  var DEFAULT_BASE_PATH = "m/44'/0'/0'";
23668
23665
  var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
23669
- var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
23670
- var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
23671
23666
  var TEST_NOSTR_RELAYS = [
23672
23667
  "wss://nostr-relay.testnet.unicity.network"
23673
23668
  ];
@@ -23677,10 +23672,19 @@ var DEFAULT_GROUP_RELAYS = [
23677
23672
  var NETWORKS = {
23678
23673
  mainnet: {
23679
23674
  name: "Mainnet",
23680
- aggregatorUrl: DEFAULT_AGGREGATOR_URL,
23681
- nostrRelays: DEFAULT_NOSTR_RELAYS,
23675
+ networkId: 1,
23676
+ // v3 state-transition gateway (networkId 1 comes from the trust base). apiKey is env-injected;
23677
+ // unlike testnet2's, a mainnet gateway key is a SECRET — never commit one.
23678
+ aggregatorUrl: "https://gateway.mainnet.unicity.network",
23679
+ // Mainnet has no relay of its own yet — it shares the testnet relay until one is stood up.
23680
+ // Consequence while shared: nametag bindings for both networks live in ONE namespace, and
23681
+ // since bindings carry no network the cross-network recipient guard can only signal (#734).
23682
+ nostrRelays: TEST_NOSTR_RELAYS,
23682
23683
  groupRelays: DEFAULT_GROUP_RELAYS,
23683
- tokenRegistryUrl: TOKEN_REGISTRY_URL
23684
+ // Published, but currently only the non-fungible base token type — no fungible
23685
+ // coins yet, so those still miss the registry (decimals 0, symbol falls back to
23686
+ // six hex chars). Presentation only: the money path treats coinId as opaque bytes.
23687
+ tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json"
23684
23688
  },
23685
23689
  // v1 cutover: 'testnet' now POINTS AT TESTNET2 (the v2 gateway network). The
23686
23690
  // old goggregator testnet spoke the removed v1 protocol — a v2 engine cannot
@@ -23704,19 +23708,10 @@ var NETWORKS = {
23704
23708
  // reuse testnet infra (shared relays/ipfs)
23705
23709
  groupRelays: DEFAULT_GROUP_RELAYS,
23706
23710
  tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
23707
- },
23708
- // NOTE: mainnet/dev still point at v1-era aggregators. The v2 engine cannot
23709
- // operate against them until their gateways are cut over to the v2 protocol —
23710
- // wallet operations on these networks fail loudly (AGGREGATOR_ERROR) until then.
23711
- dev: {
23712
- name: "Development",
23713
- aggregatorUrl: DEV_AGGREGATOR_URL,
23714
- nostrRelays: TEST_NOSTR_RELAYS,
23715
- groupRelays: DEFAULT_GROUP_RELAYS,
23716
- tokenRegistryUrl: TOKEN_REGISTRY_URL
23717
23711
  }
23718
23712
  };
23719
23713
  var SPHERE_NETWORKS = {
23714
+ mainnet: { id: NETWORKS.mainnet.networkId, name: "mainnet" },
23720
23715
  testnet2: { id: NETWORKS.testnet2.networkId, name: "testnet2" }
23721
23716
  };
23722
23717
  var TIMEOUTS = {
@@ -23732,11 +23727,98 @@ var TIMEOUTS = {
23732
23727
  SYNC_INTERVAL: 6e4
23733
23728
  };
23734
23729
 
23730
+ // core/errors.ts
23731
+ var SphereError = class extends Error {
23732
+ code;
23733
+ cause;
23734
+ /**
23735
+ * #441 deferred-paid linkage: the transferId of the possibly-committed send,
23736
+ * stamped by `sendOnce` before the error leaves so a payment-request consumer
23737
+ * can durably journal request→transfer and resolve 'paid' only when that
23738
+ * transfer actually completes (never optimistically). Present ONLY on
23739
+ * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
23740
+ */
23741
+ transferId;
23742
+ constructor(message, code, cause) {
23743
+ super(message);
23744
+ this.name = "SphereError";
23745
+ this.code = code;
23746
+ this.cause = cause;
23747
+ }
23748
+ };
23749
+
23750
+ // storage/tracked-addresses.ts
23751
+ function isDerivableIndex(value) {
23752
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
23753
+ }
23754
+ function num(value, fallback) {
23755
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
23756
+ }
23757
+ function toEntry(value) {
23758
+ if (typeof value !== "object" || value === null) return null;
23759
+ const e = value;
23760
+ if (!isDerivableIndex(e.index)) return null;
23761
+ return {
23762
+ ...e,
23763
+ index: e.index,
23764
+ hidden: e.hidden === true,
23765
+ createdAt: num(e.createdAt, 0),
23766
+ updatedAt: num(e.updatedAt, 0)
23767
+ };
23768
+ }
23769
+ function parseTrackedAddresses(raw) {
23770
+ if (!raw) return [];
23771
+ let parsed;
23772
+ try {
23773
+ parsed = JSON.parse(raw);
23774
+ } catch {
23775
+ return [];
23776
+ }
23777
+ if (typeof parsed !== "object" || parsed === null) return [];
23778
+ const addresses = parsed.addresses;
23779
+ if (!Array.isArray(addresses)) return [];
23780
+ const entries = [];
23781
+ for (const row of addresses) {
23782
+ const entry = toEntry(row);
23783
+ if (entry) entries.push(entry);
23784
+ }
23785
+ return entries;
23786
+ }
23787
+ function mergeTrackedAddresses(onDisk, incoming) {
23788
+ const merged = /* @__PURE__ */ new Map();
23789
+ for (const entry of onDisk) {
23790
+ if (isDerivableIndex(entry.index)) merged.set(entry.index, entry);
23791
+ }
23792
+ for (const entry of incoming) {
23793
+ if (!isDerivableIndex(entry.index)) {
23794
+ throw new SphereError(
23795
+ `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.`,
23796
+ "VALIDATION_ERROR"
23797
+ );
23798
+ }
23799
+ const existing = merged.get(entry.index);
23800
+ if (!existing) {
23801
+ merged.set(entry.index, entry);
23802
+ continue;
23803
+ }
23804
+ const winner = entry.updatedAt >= existing.updatedAt ? entry : existing;
23805
+ merged.set(entry.index, {
23806
+ ...existing,
23807
+ ...winner,
23808
+ index: entry.index,
23809
+ createdAt: Math.min(existing.createdAt, entry.createdAt)
23810
+ });
23811
+ }
23812
+ return Array.from(merged.values()).sort((a, b) => a.index - b.index);
23813
+ }
23814
+
23735
23815
  // impl/nodejs/storage/FileStorageProvider.ts
23736
23816
  var FileStorageProvider = class {
23737
23817
  id = "file-storage";
23738
23818
  name = "File Storage";
23739
23819
  type = "local";
23820
+ /** The resolved wallet file — two providers over one path share erasure (#766). */
23821
+ backingStoreId;
23740
23822
  dataDir;
23741
23823
  filePath;
23742
23824
  isTxtMode;
@@ -23746,14 +23828,15 @@ var FileStorageProvider = class {
23746
23828
  _identity = null;
23747
23829
  constructor(config) {
23748
23830
  if (typeof config === "string") {
23749
- this.dataDir = config;
23750
- this.filePath = path.join(config, "wallet.json");
23831
+ this.dataDir = path.resolve(config);
23832
+ this.filePath = path.join(this.dataDir, "wallet.json");
23751
23833
  } else {
23752
- this.dataDir = config.dataDir;
23753
- this.filePath = path.join(config.dataDir, config.fileName ?? "wallet.json");
23834
+ this.dataDir = path.resolve(config.dataDir);
23835
+ this.filePath = path.join(this.dataDir, config.fileName ?? "wallet.json");
23754
23836
  this.network = config.network;
23755
23837
  }
23756
23838
  this.isTxtMode = this.filePath.endsWith(".txt");
23839
+ this.backingStoreId = `file:${this.filePath}`;
23757
23840
  }
23758
23841
  setIdentity(identity) {
23759
23842
  this._identity = identity;
@@ -23850,18 +23933,40 @@ var FileStorageProvider = class {
23850
23933
  }
23851
23934
  await this.save();
23852
23935
  }
23936
+ /** Serializes the read-merge-write below, per provider instance. */
23937
+ trackedWrites = Promise.resolve();
23938
+ /**
23939
+ * Persist the tracked-address registry by MERGING, never replacing.
23940
+ *
23941
+ * Every Sphere over this storage holds its own snapshot and writes it in
23942
+ * full, so a wholesale write drops the addresses this writer never saw
23943
+ * (#766 item 5 — a lost update, reproducible on one network). Concurrent
23944
+ * calls are serialized on `trackedWrites` so a read can never interleave
23945
+ * with another call's write.
23946
+ *
23947
+ * Deliberately PER OBJECT, unlike the browser providers. Two objects over one
23948
+ * `dataDir` share a `backingStoreId` but not this cache, and `save()` rewrites the
23949
+ * WHOLE file from it — so a sibling's *unrelated* `set()` rolls the registry back
23950
+ * regardless of how this one write is serialized. Sharing the chain here would make
23951
+ * the cross-object contract case pass while leaving the provider unsafe. The real
23952
+ * fix is #771 (refresh from disk under a per-file lock, on every write); until then
23953
+ * `backingStoreId` scopes TEARDOWN only. Reviewers keep re-finding this — see the
23954
+ * `unsupported:` note in tests/unit/storage/tracked-addresses-providers.test.ts.
23955
+ */
23853
23956
  async saveTrackedAddresses(entries) {
23854
- await this.set(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES, JSON.stringify({ version: 1, addresses: entries }));
23957
+ const run = this.trackedWrites.then(async () => {
23958
+ const onDisk = parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
23959
+ const file = {
23960
+ version: 1,
23961
+ addresses: mergeTrackedAddresses(onDisk, entries)
23962
+ };
23963
+ await this.set(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES, JSON.stringify(file));
23964
+ });
23965
+ this.trackedWrites = run.then(() => void 0, () => void 0);
23966
+ await run;
23855
23967
  }
23856
23968
  async loadTrackedAddresses() {
23857
- const data = await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
23858
- if (!data) return [];
23859
- try {
23860
- const parsed = JSON.parse(data);
23861
- return parsed.addresses ?? [];
23862
- } catch {
23863
- return [];
23864
- }
23969
+ return parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
23865
23970
  }
23866
23971
  /**
23867
23972
  * Get full storage key with address prefix for per-address keys
@@ -24392,26 +24497,6 @@ var logger = {
24392
24497
  }
24393
24498
  };
24394
24499
 
24395
- // core/errors.ts
24396
- var SphereError = class extends Error {
24397
- code;
24398
- cause;
24399
- /**
24400
- * #441 deferred-paid linkage: the transferId of the possibly-committed send,
24401
- * stamped by `sendOnce` before the error leaves so a payment-request consumer
24402
- * can durably journal request→transfer and resolve 'paid' only when that
24403
- * transfer actually completes (never optimistically). Present ONLY on
24404
- * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
24405
- */
24406
- transferId;
24407
- constructor(message, code, cause) {
24408
- super(message);
24409
- this.name = "SphereError";
24410
- this.code = code;
24411
- this.cause = cause;
24412
- }
24413
- };
24414
-
24415
24500
  // transport/websocket.ts
24416
24501
  function defaultUUIDGenerator() {
24417
24502
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -24584,14 +24669,10 @@ var NostrTransportProvider = class _NostrTransportProvider {
24584
24669
  });
24585
24670
  }
24586
24671
  });
24587
- await Promise.race([
24588
- this.nostrClient.connect(...this.config.relays),
24589
- new Promise(
24590
- (_, reject) => setTimeout(() => reject(new Error(
24591
- `Transport connection timed out after ${this.config.timeout}ms`
24592
- )), this.config.timeout)
24593
- )
24594
- ]);
24672
+ await this.connectWithDeadline(
24673
+ this.nostrClient,
24674
+ `Transport connection timed out after ${this.config.timeout}ms`
24675
+ );
24595
24676
  if (!this.nostrClient.isConnected()) {
24596
24677
  throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
24597
24678
  }
@@ -24606,6 +24687,27 @@ var NostrTransportProvider = class _NostrTransportProvider {
24606
24687
  throw error;
24607
24688
  }
24608
24689
  }
24690
+ /**
24691
+ * Race one client's relay connect against the configured timeout.
24692
+ *
24693
+ * The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
24694
+ * cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
24695
+ * for the whole `config.timeout` after the call has already returned (and
24696
+ * then rejects a promise nobody is listening to any more).
24697
+ */
24698
+ async connectWithDeadline(client, timeoutMessage) {
24699
+ let timer;
24700
+ try {
24701
+ await Promise.race([
24702
+ client.connect(...this.config.relays),
24703
+ new Promise((_, reject) => {
24704
+ timer = setTimeout(() => reject(new Error(timeoutMessage)), this.config.timeout);
24705
+ })
24706
+ ]);
24707
+ } finally {
24708
+ if (timer !== void 0) clearTimeout(timer);
24709
+ }
24710
+ }
24609
24711
  async disconnect() {
24610
24712
  if (this.nostrClient) {
24611
24713
  this.nostrClient.disconnect();
@@ -24724,19 +24826,22 @@ var NostrTransportProvider = class _NostrTransportProvider {
24724
24826
  // TransportProvider Implementation
24725
24827
  // ===========================================================================
24726
24828
  async setIdentity(identity) {
24727
- this.identity = identity;
24728
- this.processedEventIds.clear();
24729
- this.lastEventTs = 0;
24730
- this.lastDmEventTs = 0;
24731
- this.fallbackDmSince = null;
24732
24829
  const secretKey = Buffer2.from(identity.privateKey, "hex");
24733
- this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
24734
- const nostrPubkey = this.keyManager.getPublicKeyHex();
24735
- logger.debug("Nostr", "Identity set, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
24830
+ const nextKeyManager = NostrKeyManager.fromPrivateKey(secretKey);
24831
+ const nostrPubkey = nextKeyManager.getPublicKeyHex();
24832
+ logger.debug("Nostr", "Identity staged, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
24833
+ const applyStagedIdentity = () => {
24834
+ this.identity = identity;
24835
+ this.keyManager = nextKeyManager;
24836
+ this.processedEventIds.clear();
24837
+ this.lastEventTs = 0;
24838
+ this.lastDmEventTs = 0;
24839
+ this.fallbackDmSince = null;
24840
+ };
24736
24841
  if (this.nostrClient && this.status === "connected") {
24737
24842
  logger.debug("Nostr", "Identity changed while connected - recreating NostrClient");
24738
24843
  const oldClient = this.nostrClient;
24739
- this.nostrClient = new NostrClient(this.keyManager, {
24844
+ const nextClient = new NostrClient(nextKeyManager, {
24740
24845
  autoReconnect: this.config.autoReconnect,
24741
24846
  reconnectIntervalMs: this.config.reconnectDelay,
24742
24847
  maxReconnectIntervalMs: this.config.reconnectDelay * 16,
@@ -24745,7 +24850,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
24745
24850
  queryTimeoutMs: 2e4
24746
24851
  // see same option above for rationale
24747
24852
  });
24748
- this.nostrClient.addConnectionListener({
24853
+ nextClient.addConnectionListener({
24749
24854
  onConnect: (url) => {
24750
24855
  logger.debug("Nostr", "NostrClient connected to relay:", url);
24751
24856
  },
@@ -24759,18 +24864,43 @@ var NostrTransportProvider = class _NostrTransportProvider {
24759
24864
  logger.debug("Nostr", "NostrClient reconnected to relay:", url);
24760
24865
  }
24761
24866
  });
24762
- await Promise.race([
24763
- this.nostrClient.connect(...this.config.relays),
24764
- new Promise(
24765
- (_, reject) => setTimeout(() => reject(new Error(
24766
- `Transport reconnection timed out after ${this.config.timeout}ms`
24767
- )), this.config.timeout)
24768
- )
24769
- ]);
24770
- await this.subscribeToEvents();
24771
- oldClient.disconnect();
24867
+ try {
24868
+ await this.connectWithDeadline(
24869
+ nextClient,
24870
+ `Transport reconnection timed out after ${this.config.timeout}ms`
24871
+ );
24872
+ } catch (error) {
24873
+ try {
24874
+ nextClient.disconnect();
24875
+ } catch {
24876
+ }
24877
+ throw error;
24878
+ }
24879
+ if (this.nostrClient !== oldClient || this.status !== "connected") {
24880
+ try {
24881
+ nextClient.disconnect();
24882
+ } catch {
24883
+ }
24884
+ throw new SphereError(
24885
+ "Transport client changed or left the connected state during setIdentity - identity not applied",
24886
+ "TRANSPORT_ERROR"
24887
+ );
24888
+ }
24889
+ this.nostrClient = nextClient;
24890
+ applyStagedIdentity();
24891
+ try {
24892
+ await this.subscribeToEvents();
24893
+ } finally {
24894
+ try {
24895
+ oldClient.disconnect();
24896
+ } catch {
24897
+ }
24898
+ }
24772
24899
  } else if (this.isConnected()) {
24900
+ applyStagedIdentity();
24773
24901
  await this.subscribeToEvents();
24902
+ } else {
24903
+ applyStagedIdentity();
24774
24904
  }
24775
24905
  }
24776
24906
  setFallbackSince(sinceSeconds) {
@@ -25537,14 +25667,14 @@ var NostrTransportProvider = class _NostrTransportProvider {
25537
25667
  const client = this.nostrClient;
25538
25668
  const events = [];
25539
25669
  let subId;
25540
- await new Promise((resolve, reject) => {
25670
+ await new Promise((resolve2, reject) => {
25541
25671
  let timeout;
25542
25672
  let settled = false;
25543
25673
  const settle = () => {
25544
25674
  if (settled) return;
25545
25675
  settled = true;
25546
25676
  clearTimeout(timeout);
25547
- resolve();
25677
+ resolve2();
25548
25678
  };
25549
25679
  try {
25550
25680
  subId = client.subscribe(walletFilter, {
@@ -25587,7 +25717,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
25587
25717
  const events = [];
25588
25718
  const filter = new Filter(filterObj);
25589
25719
  let subId;
25590
- return new Promise((resolve) => {
25720
+ return new Promise((resolve2) => {
25591
25721
  let settled = false;
25592
25722
  const timeout = setTimeout(() => {
25593
25723
  if (settled) return;
@@ -25599,7 +25729,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
25599
25729
  }
25600
25730
  }
25601
25731
  logger.warn("Nostr", `queryEvents timed out after 15s, returning ${events.length} event(s)`, { kinds: filterObj.kinds, limit: filterObj.limit });
25602
- resolve(events);
25732
+ resolve2(events);
25603
25733
  }, 15e3);
25604
25734
  const settle = () => {
25605
25735
  if (settled) return;
@@ -25611,7 +25741,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
25611
25741
  } catch {
25612
25742
  }
25613
25743
  }
25614
- resolve(events);
25744
+ resolve2(events);
25615
25745
  };
25616
25746
  try {
25617
25747
  subId = client.subscribe(filter, {
@@ -25630,7 +25760,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
25630
25760
  });
25631
25761
  } catch {
25632
25762
  clearTimeout(timeout);
25633
- resolve(events);
25763
+ resolve2(events);
25634
25764
  return;
25635
25765
  }
25636
25766
  });
@@ -26070,26 +26200,6 @@ var UnicityAggregatorProvider = class {
26070
26200
  var UnicityOracleProvider = UnicityAggregatorProvider;
26071
26201
 
26072
26202
  // assets/trustbase.ts
26073
- var TRUSTBASE_TESTNET = {
26074
- version: 1,
26075
- networkId: 3,
26076
- epoch: 1,
26077
- epochStartRound: 1,
26078
- rootNodes: [
26079
- {
26080
- nodeId: "16Uiu2HAkyQRiA7pMgzgLj9GgaBJEJa8zmx9dzqUDa6WxQPJ82ghU",
26081
- sigKey: "0x039afb2acb65f5fbc272d8907f763d0a5d189aadc9b97afdcc5897ea4dd112e68b",
26082
- stake: 1
26083
- }
26084
- ],
26085
- quorumThreshold: 1,
26086
- stateHash: "",
26087
- changeRecordHash: "",
26088
- previousEntryHash: "",
26089
- signatures: {
26090
- "16Uiu2HAkyQRiA7pMgzgLj9GgaBJEJa8zmx9dzqUDa6WxQPJ82ghU": "0xf157c9fdd8a378e3ca70d354ccc4475ab2cd8de360127bc46b0aeab4b453a80f07fd9136a5843b60a8babaff23e20acc8879861f7651440a5e2829f7541b31f100"
26091
- }
26092
- };
26093
26203
  var TRUSTBASE_TESTNET2 = {
26094
26204
  version: 1,
26095
26205
  networkId: 4,
@@ -26128,8 +26238,44 @@ var TRUSTBASE_TESTNET2 = {
26128
26238
  "16Uiu2HAmU5T1TqKyKkUB3sitWb431hVFfp9nqvaLgVagQF6ywLFF": "0x9f28f3e3c50d4bfdaf54d7290fbbe311fffb3be2281a1d211be56818fa43d06a2e9a81925e9d365e552c8b772c99285a686de1d9fb581920be7524c7a5d467a301"
26129
26239
  }
26130
26240
  };
26131
- var TRUSTBASE_MAINNET = null;
26132
- var TRUSTBASE_DEV = TRUSTBASE_TESTNET;
26241
+ var TRUSTBASE_MAINNET = {
26242
+ version: 1,
26243
+ networkId: 1,
26244
+ epoch: 1,
26245
+ epochStartRound: 1,
26246
+ rootNodes: [
26247
+ {
26248
+ nodeId: "16Uiu2HAkvk4EpXoyh7SVLTX3TcsZkjbni5jY1sBgJ7qCEWfx1Gma",
26249
+ sigKey: "0x03059e30b5c980ce92e2c6d4e14775838be91a6f0e993f3a4899ab459c4b5225f2",
26250
+ stake: 1
26251
+ },
26252
+ {
26253
+ nodeId: "16Uiu2HAmF9Mq9pH9yUTdao3TpJeFZY9oX9gyMqfxggVA4fNmzmXr",
26254
+ sigKey: "0x03a514db8b17b1c79b4a6affeb5d88a129c4d5ff0f92c8f86c311971b0beb94335",
26255
+ stake: 1
26256
+ },
26257
+ {
26258
+ nodeId: "16Uiu2HAmLAHTSMvZ2apRRtBN9piYVhaK2i7sTMaQeSWSxsLh75TG",
26259
+ sigKey: "0x02e5669b2daa5d1afccfe66603f7d3fe171ecbd7ed891487ab3683cdebc3c3cd8b",
26260
+ stake: 1
26261
+ },
26262
+ {
26263
+ nodeId: "16Uiu2HAmNNaWfK4RTTAq7Ap8w2yZUryrEejqmmGf44ogMaYEnkJ4",
26264
+ sigKey: "0x03ffee964da8ca276d4fc1cf391929e6759db106ef1d0aea998154b51e5a7fb1b7",
26265
+ stake: 1
26266
+ }
26267
+ ],
26268
+ quorumThreshold: 3,
26269
+ stateHash: "",
26270
+ changeRecordHash: "",
26271
+ previousEntryHash: "",
26272
+ signatures: {
26273
+ "16Uiu2HAkvk4EpXoyh7SVLTX3TcsZkjbni5jY1sBgJ7qCEWfx1Gma": "0xc2f1d67dbb47fe4dcf502301d8cd5021b8903c0d3385781653d09c703d566a3a3a4e59c75792b36f929e32564ddc14c40ef728c0bde5859774d08d802de2318c00",
26274
+ "16Uiu2HAmF9Mq9pH9yUTdao3TpJeFZY9oX9gyMqfxggVA4fNmzmXr": "0x3e3ad3a7540daa0eded457e31d2ebcacfb354036798f49f63433d26c44857e76157e4abd36cbb1d39de8cdf6e1d33f88b250077d4347c3e5bf0f35a64f2d815300",
26275
+ "16Uiu2HAmLAHTSMvZ2apRRtBN9piYVhaK2i7sTMaQeSWSxsLh75TG": "0x6101bd9df8ab87439d90b52cd74727bc8bf31dd74173c8f731ec3ea501d3d86556ba7c0e84643337959f3be0e448637c301e8e0b28196e780d4f2ff930d9062100",
26276
+ "16Uiu2HAmNNaWfK4RTTAq7Ap8w2yZUryrEejqmmGf44ogMaYEnkJ4": "0xda13c777b832425e5cbd8d6e49eb08ddbde35c1e4a9bee8bd495c3883f039e434b64560865c3bc52b68d65780932b1f2bf8971f245d2244face822af5984945c00"
26277
+ }
26278
+ };
26133
26279
 
26134
26280
  // impl/shared/trustbase-loader.ts
26135
26281
  function getEmbeddedTrustBase(network) {
@@ -26142,8 +26288,6 @@ function getEmbeddedTrustBase(network) {
26142
26288
  case "testnet":
26143
26289
  case "testnet2":
26144
26290
  return TRUSTBASE_TESTNET2;
26145
- case "dev":
26146
- return TRUSTBASE_DEV;
26147
26291
  default:
26148
26292
  throw new SphereError(`getEmbeddedTrustBase: unknown network "${network}"`, "INVALID_CONFIG");
26149
26293
  }
@@ -26239,6 +26383,7 @@ function createWalletApiProviders(base, config) {
26239
26383
 
26240
26384
  // impl/shared/network.ts
26241
26385
  var EXPECTED_NETWORK_ID = {
26386
+ mainnet: 1,
26242
26387
  // v1 cutover: 'testnet' is an alias of testnet2 (the v2 gateway network),
26243
26388
  // so both expect the testnet2 trust base (networkId 4).
26244
26389
  testnet: 4,
@@ -26259,7 +26404,7 @@ function assertNetworkConsistency(network) {
26259
26404
  );
26260
26405
  }
26261
26406
  const expected = EXPECTED_NETWORK_ID[network];
26262
- if (expected !== void 0 && networkId !== expected) {
26407
+ if (networkId !== expected) {
26263
26408
  throw new SphereError(
26264
26409
  `Network "${network}" trust base networkId ${networkId} does not match expected ${expected} (wrong trust base wired to this network).`,
26265
26410
  "INVALID_CONFIG"
@@ -26524,419 +26669,6 @@ function createPriceProvider(config) {
26524
26669
  }
26525
26670
  }
26526
26671
 
26527
- // registry/TokenRegistry.ts
26528
- var FETCH_TIMEOUT_MS = 1e4;
26529
- var TokenRegistry = class _TokenRegistry {
26530
- static instance = null;
26531
- definitionsById;
26532
- definitionsBySymbol;
26533
- definitionsByName;
26534
- // Remote refresh state
26535
- remoteUrl = null;
26536
- storage = null;
26537
- refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
26538
- refreshTimer = null;
26539
- lastRefreshAt = 0;
26540
- refreshPromise = null;
26541
- initialLoadPromise = null;
26542
- constructor() {
26543
- this.definitionsById = /* @__PURE__ */ new Map();
26544
- this.definitionsBySymbol = /* @__PURE__ */ new Map();
26545
- this.definitionsByName = /* @__PURE__ */ new Map();
26546
- }
26547
- /**
26548
- * Get singleton instance of TokenRegistry
26549
- */
26550
- static getInstance() {
26551
- if (!_TokenRegistry.instance) {
26552
- _TokenRegistry.instance = new _TokenRegistry();
26553
- }
26554
- return _TokenRegistry.instance;
26555
- }
26556
- /**
26557
- * Configure remote registry refresh with persistent caching.
26558
- *
26559
- * On first call:
26560
- * 1. Loads cached data from StorageProvider (if available and fresh)
26561
- * 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
26562
- *
26563
- * @param options - Configuration options
26564
- * @param options.remoteUrl - Remote URL to fetch definitions from
26565
- * @param options.storage - StorageProvider for persistent caching
26566
- * @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
26567
- * @param options.autoRefresh - Start auto-refresh immediately (default: true)
26568
- */
26569
- static configure(options) {
26570
- const instance = _TokenRegistry.getInstance();
26571
- if (options.remoteUrl !== void 0) {
26572
- instance.remoteUrl = options.remoteUrl;
26573
- }
26574
- if (options.storage !== void 0) {
26575
- instance.storage = options.storage;
26576
- }
26577
- if (options.refreshIntervalMs !== void 0) {
26578
- instance.refreshIntervalMs = options.refreshIntervalMs;
26579
- }
26580
- const autoRefresh = options.autoRefresh ?? true;
26581
- instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
26582
- }
26583
- /**
26584
- * Reset the singleton instance (useful for testing).
26585
- * Stops auto-refresh if running.
26586
- */
26587
- static resetInstance() {
26588
- if (_TokenRegistry.instance) {
26589
- _TokenRegistry.instance.stopAutoRefresh();
26590
- }
26591
- _TokenRegistry.instance = null;
26592
- }
26593
- /**
26594
- * Destroy the singleton: stop auto-refresh and reset.
26595
- */
26596
- static destroy() {
26597
- _TokenRegistry.resetInstance();
26598
- }
26599
- /**
26600
- * Wait for the initial data load (cache or remote) to complete.
26601
- * Returns true if data was loaded, false if not (timeout or no data source).
26602
- *
26603
- * @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
26604
- */
26605
- static async waitForReady(timeoutMs = 1e4) {
26606
- const instance = _TokenRegistry.getInstance();
26607
- if (!instance.initialLoadPromise) {
26608
- return instance.definitionsById.size > 0;
26609
- }
26610
- if (timeoutMs <= 0) {
26611
- return instance.initialLoadPromise;
26612
- }
26613
- return Promise.race([
26614
- instance.initialLoadPromise,
26615
- new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
26616
- ]);
26617
- }
26618
- // ===========================================================================
26619
- // Initial Load
26620
- // ===========================================================================
26621
- /**
26622
- * Perform initial data load: try cache first, fall back to remote fetch.
26623
- * After initial data is available, start periodic auto-refresh if configured.
26624
- */
26625
- async performInitialLoad(autoRefresh) {
26626
- let loaded = false;
26627
- if (this.storage) {
26628
- loaded = await this.loadFromCache();
26629
- }
26630
- if (loaded) {
26631
- if (autoRefresh && this.remoteUrl) {
26632
- this.startAutoRefresh();
26633
- }
26634
- return true;
26635
- }
26636
- if (autoRefresh && this.remoteUrl) {
26637
- loaded = await this.refreshFromRemote();
26638
- this.stopAutoRefresh();
26639
- this.refreshTimer = setInterval(() => {
26640
- this.refreshFromRemote();
26641
- }, this.refreshIntervalMs);
26642
- return loaded;
26643
- }
26644
- return false;
26645
- }
26646
- // ===========================================================================
26647
- // Cache (StorageProvider)
26648
- // ===========================================================================
26649
- /**
26650
- * Per-network cache keys. The registry cache MUST be namespaced by remoteUrl:
26651
- * testnet and testnet2 ship DIFFERENT coinIds for the same symbol, so a single
26652
- * global key let a stale testnet snapshot be served for a testnet2 request —
26653
- * wrong/missing icons AND self-mint resolving the wrong-network coinId. Keying
26654
- * by remoteUrl isolates the snapshots so a network can never read another's.
26655
- */
26656
- cacheKeys() {
26657
- const base = STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE;
26658
- const baseTs = STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS;
26659
- if (!this.remoteUrl) return { data: base, ts: baseTs };
26660
- return { data: `${base}:${this.remoteUrl}`, ts: `${baseTs}:${this.remoteUrl}` };
26661
- }
26662
- /**
26663
- * Load definitions from StorageProvider cache.
26664
- * Only applies if cache exists and is fresh (within refreshIntervalMs).
26665
- */
26666
- async loadFromCache() {
26667
- if (!this.storage) return false;
26668
- try {
26669
- const { data: dataKey, ts: tsKey } = this.cacheKeys();
26670
- const [cached, cachedTs] = await Promise.all([
26671
- this.storage.get(dataKey),
26672
- this.storage.get(tsKey)
26673
- ]);
26674
- if (!cached || !cachedTs) return false;
26675
- const ts = parseInt(cachedTs, 10);
26676
- if (isNaN(ts)) return false;
26677
- const age = Date.now() - ts;
26678
- if (age > this.refreshIntervalMs) return false;
26679
- if (this.lastRefreshAt > ts) return false;
26680
- const data = JSON.parse(cached);
26681
- if (!this.isValidDefinitionsArray(data)) return false;
26682
- this.applyDefinitions(data);
26683
- this.lastRefreshAt = ts;
26684
- return true;
26685
- } catch {
26686
- return false;
26687
- }
26688
- }
26689
- /**
26690
- * Save definitions to StorageProvider cache.
26691
- */
26692
- async saveToCache(definitions) {
26693
- if (!this.storage) return;
26694
- try {
26695
- const { data: dataKey, ts: tsKey } = this.cacheKeys();
26696
- await Promise.all([
26697
- this.storage.set(dataKey, JSON.stringify(definitions)),
26698
- this.storage.set(tsKey, String(Date.now()))
26699
- ]);
26700
- } catch {
26701
- }
26702
- }
26703
- // ===========================================================================
26704
- // Remote Refresh
26705
- // ===========================================================================
26706
- /**
26707
- * Apply an array of token definitions to the internal maps.
26708
- * Clears existing data before applying.
26709
- */
26710
- applyDefinitions(definitions) {
26711
- this.definitionsById.clear();
26712
- this.definitionsBySymbol.clear();
26713
- this.definitionsByName.clear();
26714
- for (const def of definitions) {
26715
- const idLower = def.id.toLowerCase();
26716
- this.definitionsById.set(idLower, def);
26717
- if (def.symbol) {
26718
- this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
26719
- }
26720
- this.definitionsByName.set(def.name.toLowerCase(), def);
26721
- }
26722
- }
26723
- /**
26724
- * Validate that data is an array of objects with 'id' field
26725
- */
26726
- isValidDefinitionsArray(data) {
26727
- return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
26728
- }
26729
- /**
26730
- * Fetch token definitions from the remote URL and update the registry.
26731
- * On success, also persists to StorageProvider cache.
26732
- * Returns true on success, false on failure. On failure, existing data is preserved.
26733
- * Concurrent calls are deduplicated — only one fetch runs at a time.
26734
- */
26735
- async refreshFromRemote() {
26736
- if (!this.remoteUrl) {
26737
- return false;
26738
- }
26739
- if (this.refreshPromise) {
26740
- return this.refreshPromise;
26741
- }
26742
- this.refreshPromise = this.doRefresh();
26743
- try {
26744
- return await this.refreshPromise;
26745
- } finally {
26746
- this.refreshPromise = null;
26747
- }
26748
- }
26749
- async doRefresh() {
26750
- try {
26751
- const controller = new AbortController();
26752
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
26753
- let response;
26754
- try {
26755
- response = await fetch(this.remoteUrl, {
26756
- headers: { Accept: "application/json" },
26757
- signal: controller.signal
26758
- });
26759
- } finally {
26760
- clearTimeout(timer);
26761
- }
26762
- if (!response.ok) {
26763
- logger.warn("TokenRegistry", `Remote fetch failed: HTTP ${response.status} ${response.statusText}`);
26764
- return false;
26765
- }
26766
- const data = await response.json();
26767
- if (!this.isValidDefinitionsArray(data)) {
26768
- logger.warn("TokenRegistry", "Remote data is not a valid token definitions array");
26769
- return false;
26770
- }
26771
- const definitions = data;
26772
- this.applyDefinitions(definitions);
26773
- this.lastRefreshAt = Date.now();
26774
- this.saveToCache(definitions);
26775
- return true;
26776
- } catch (error) {
26777
- const message = error instanceof Error ? error.message : String(error);
26778
- logger.warn("TokenRegistry", `Remote refresh failed: ${message}`);
26779
- return false;
26780
- }
26781
- }
26782
- /**
26783
- * Start periodic auto-refresh from the remote URL.
26784
- * Does an immediate fetch, then repeats at the configured interval.
26785
- */
26786
- startAutoRefresh(intervalMs) {
26787
- this.stopAutoRefresh();
26788
- if (intervalMs !== void 0) {
26789
- this.refreshIntervalMs = intervalMs;
26790
- }
26791
- this.refreshFromRemote();
26792
- this.refreshTimer = setInterval(() => {
26793
- this.refreshFromRemote();
26794
- }, this.refreshIntervalMs);
26795
- }
26796
- /**
26797
- * Stop periodic auto-refresh
26798
- */
26799
- stopAutoRefresh() {
26800
- if (this.refreshTimer !== null) {
26801
- clearInterval(this.refreshTimer);
26802
- this.refreshTimer = null;
26803
- }
26804
- }
26805
- /**
26806
- * Timestamp of the last successful remote refresh (0 if never refreshed)
26807
- */
26808
- getLastRefreshAt() {
26809
- return this.lastRefreshAt;
26810
- }
26811
- // ===========================================================================
26812
- // Lookup Methods
26813
- // ===========================================================================
26814
- /**
26815
- * Get token definition by hex coin ID
26816
- * @param coinId - 64-character hex string
26817
- * @returns Token definition or undefined if not found
26818
- */
26819
- getDefinition(coinId) {
26820
- if (!coinId) return void 0;
26821
- return this.definitionsById.get(coinId.toLowerCase());
26822
- }
26823
- /**
26824
- * Get token definition by symbol (e.g., "UCT", "BTC")
26825
- * @param symbol - Token symbol (case-insensitive)
26826
- * @returns Token definition or undefined if not found
26827
- */
26828
- getDefinitionBySymbol(symbol) {
26829
- if (!symbol) return void 0;
26830
- return this.definitionsBySymbol.get(symbol.toUpperCase());
26831
- }
26832
- /**
26833
- * Get token definition by name (e.g., "bitcoin", "ethereum")
26834
- * @param name - Token name (case-insensitive)
26835
- * @returns Token definition or undefined if not found
26836
- */
26837
- getDefinitionByName(name) {
26838
- if (!name) return void 0;
26839
- return this.definitionsByName.get(name.toLowerCase());
26840
- }
26841
- /**
26842
- * Get token symbol for a coin ID
26843
- * @param coinId - 64-character hex string
26844
- * @returns Symbol (e.g., "UCT") or truncated ID if not found
26845
- */
26846
- getSymbol(coinId) {
26847
- const def = this.getDefinition(coinId);
26848
- if (def?.symbol) {
26849
- return def.symbol;
26850
- }
26851
- return coinId.slice(0, 6).toUpperCase();
26852
- }
26853
- /**
26854
- * Get token name for a coin ID
26855
- * @param coinId - 64-character hex string
26856
- * @returns Name (e.g., "Bitcoin") or coin ID if not found
26857
- */
26858
- getName(coinId) {
26859
- const def = this.getDefinition(coinId);
26860
- if (def?.name) {
26861
- return def.name.charAt(0).toUpperCase() + def.name.slice(1);
26862
- }
26863
- return coinId;
26864
- }
26865
- /**
26866
- * Get decimal places for a coin ID
26867
- * @param coinId - 64-character hex string
26868
- * @returns Decimals or 0 if not found
26869
- */
26870
- getDecimals(coinId) {
26871
- const def = this.getDefinition(coinId);
26872
- return def?.decimals ?? 0;
26873
- }
26874
- /**
26875
- * Get icon URL for a coin ID
26876
- * @param coinId - 64-character hex string
26877
- * @param preferPng - Prefer PNG format over SVG
26878
- * @returns Icon URL or null if not found
26879
- */
26880
- getIconUrl(coinId, preferPng = true) {
26881
- const def = this.getDefinition(coinId);
26882
- if (!def?.icons || def.icons.length === 0) {
26883
- return null;
26884
- }
26885
- if (preferPng) {
26886
- const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
26887
- if (pngIcon) return pngIcon.url;
26888
- }
26889
- return def.icons[0].url;
26890
- }
26891
- /**
26892
- * Check if a coin ID is known in the registry
26893
- * @param coinId - 64-character hex string
26894
- * @returns true if the coin is in the registry
26895
- */
26896
- isKnown(coinId) {
26897
- return this.definitionsById.has(coinId.toLowerCase());
26898
- }
26899
- /**
26900
- * Get all token definitions
26901
- * @returns Array of all token definitions
26902
- */
26903
- getAllDefinitions() {
26904
- return Array.from(this.definitionsById.values());
26905
- }
26906
- /**
26907
- * Get all fungible token definitions
26908
- * @returns Array of fungible token definitions
26909
- */
26910
- getFungibleTokens() {
26911
- return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
26912
- }
26913
- /**
26914
- * Get all non-fungible token definitions
26915
- * @returns Array of non-fungible token definitions
26916
- */
26917
- getNonFungibleTokens() {
26918
- return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
26919
- }
26920
- /**
26921
- * Get coin ID by symbol
26922
- * @param symbol - Token symbol (e.g., "UCT")
26923
- * @returns Coin ID hex string or undefined if not found
26924
- */
26925
- getCoinIdBySymbol(symbol) {
26926
- const def = this.getDefinitionBySymbol(symbol);
26927
- return def?.id;
26928
- }
26929
- /**
26930
- * Get coin ID by name
26931
- * @param name - Token name (e.g., "bitcoin")
26932
- * @returns Coin ID hex string or undefined if not found
26933
- */
26934
- getCoinIdByName(name) {
26935
- const def = this.getDefinitionByName(name);
26936
- return def?.id;
26937
- }
26938
- };
26939
-
26940
26672
  // impl/shared/resolvers.ts
26941
26673
  function getNetworkConfig(network = "mainnet") {
26942
26674
  return NETWORKS[network];
@@ -27028,8 +26760,7 @@ function createNodeProviders(config) {
27028
26760
  }
27029
26761
  const network = config.network;
27030
26762
  assertNetworkConsistency(network);
27031
- const globalDebug = config?.debug ?? false;
27032
- logger.configure({ debug: globalDebug });
26763
+ if (config?.debug !== void 0) logger.configure({ debug: config.debug });
27033
26764
  if (config?.transport?.debug) logger.setTagDebug("Nostr", true);
27034
26765
  if (config?.oracle?.debug) logger.setTagDebug("Aggregator", true);
27035
26766
  if (config?.price?.debug) logger.setTagDebug("Price", true);
@@ -27043,8 +26774,6 @@ function createNodeProviders(config) {
27043
26774
  const priceConfig = resolvePriceConfig(config?.price, storage);
27044
26775
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
27045
26776
  const market = resolveMarketConfig(config?.market);
27046
- const networkConfig = getNetworkConfig(network);
27047
- TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
27048
26777
  return {
27049
26778
  storage,
27050
26779
  groupChat,