@unicitylabs/sphere-sdk 0.15.0 → 0.16.0-dev.2
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.
- package/README.md +15 -5
- package/dist/connect/index.cjs +26 -25
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.d.cts +4 -0
- package/dist/connect/index.d.ts +4 -0
- package/dist/connect/index.js +26 -25
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +728 -227
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +174 -23
- package/dist/core/index.d.ts +174 -23
- package/dist/core/index.js +728 -226
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +16 -25
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +16 -25
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +333 -506
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +333 -506
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +15 -24
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +15 -24
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +300 -571
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +92 -15
- package/dist/impl/nodejs/index.d.ts +92 -15
- package/dist/impl/nodejs/index.js +300 -571
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/impl/shared/wallet-api/index.d.cts +54 -2
- package/dist/impl/shared/wallet-api/index.d.ts +54 -2
- package/dist/index.cjs +730 -227
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +229 -110
- package/dist/index.d.ts +229 -110
- package/dist/index.js +730 -226
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +11 -7
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +56 -3
- package/dist/modules/payments-v2/index.d.ts +56 -3
- package/dist/modules/payments-v2/index.js +11 -7
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +46 -0
- package/dist/token-engine/index.cjs.map +1 -1
- package/dist/token-engine/index.js +46 -0
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -143,6 +143,58 @@ var logger = {
|
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
+
// core/global-cell.ts
|
|
147
|
+
var CELLS_KEY = "__sphere_sdk_cells_v1__";
|
|
148
|
+
var bundleLocalBag = null;
|
|
149
|
+
function isObject(value) {
|
|
150
|
+
return typeof value === "object" && value !== null;
|
|
151
|
+
}
|
|
152
|
+
function installBag(host, fresh) {
|
|
153
|
+
try {
|
|
154
|
+
Object.defineProperty(host, CELLS_KEY, {
|
|
155
|
+
value: fresh,
|
|
156
|
+
writable: true,
|
|
157
|
+
configurable: true,
|
|
158
|
+
enumerable: false
|
|
159
|
+
});
|
|
160
|
+
return host[CELLS_KEY] === fresh;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function cellBag() {
|
|
166
|
+
if (bundleLocalBag) return bundleLocalBag;
|
|
167
|
+
const host = globalThis;
|
|
168
|
+
const found = host[CELLS_KEY];
|
|
169
|
+
if (isObject(found)) return found;
|
|
170
|
+
const fresh = {};
|
|
171
|
+
if (!installBag(host, fresh)) bundleLocalBag = fresh;
|
|
172
|
+
return fresh;
|
|
173
|
+
}
|
|
174
|
+
function accepted(intact, candidate) {
|
|
175
|
+
try {
|
|
176
|
+
return intact(candidate);
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function put(bag, name, cell) {
|
|
182
|
+
try {
|
|
183
|
+
bag[name] = cell;
|
|
184
|
+
if (bag[name] === cell) return;
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
if (bag !== bundleLocalBag) bundleLocalBag = { [name]: cell };
|
|
188
|
+
}
|
|
189
|
+
function sharedCell(name, create, intact) {
|
|
190
|
+
const bag = cellBag();
|
|
191
|
+
const found = bag[name];
|
|
192
|
+
if (isObject(found) && accepted(intact, found)) return found;
|
|
193
|
+
const fresh = create();
|
|
194
|
+
put(bag, name, fresh);
|
|
195
|
+
return fresh;
|
|
196
|
+
}
|
|
197
|
+
|
|
146
198
|
// core/errors.ts
|
|
147
199
|
var SphereError = class extends Error {
|
|
148
200
|
code;
|
|
@@ -245,7 +297,6 @@ function isNetworkScopedAddressKey(key) {
|
|
|
245
297
|
return false;
|
|
246
298
|
}
|
|
247
299
|
var DEFAULT_NOSTR_RELAYS = [
|
|
248
|
-
"wss://relay.unicity.network",
|
|
249
300
|
"wss://relay.damus.io",
|
|
250
301
|
"wss://nos.lol",
|
|
251
302
|
"wss://relay.nostr.band"
|
|
@@ -258,13 +309,9 @@ var NOSTR_EVENT_KINDS = {
|
|
|
258
309
|
/** Public broadcast */
|
|
259
310
|
BROADCAST: 1
|
|
260
311
|
};
|
|
261
|
-
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
262
|
-
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
263
312
|
var DEFAULT_AGGREGATOR_TIMEOUT = 3e4;
|
|
264
313
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
265
314
|
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
266
|
-
var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
|
|
267
|
-
var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
|
|
268
315
|
var TEST_NOSTR_RELAYS = [
|
|
269
316
|
"wss://nostr-relay.testnet.unicity.network"
|
|
270
317
|
];
|
|
@@ -274,16 +321,25 @@ var DEFAULT_GROUP_RELAYS = [
|
|
|
274
321
|
var NETWORKS = {
|
|
275
322
|
mainnet: {
|
|
276
323
|
name: "Mainnet",
|
|
277
|
-
|
|
278
|
-
|
|
324
|
+
networkId: 1,
|
|
325
|
+
// v3 state-transition gateway (networkId 1 comes from the trust base). apiKey is env-injected;
|
|
326
|
+
// unlike testnet2's, a mainnet gateway key is a SECRET — never commit one.
|
|
327
|
+
aggregatorUrl: "https://gateway.mainnet.unicity.network",
|
|
328
|
+
// Mainnet has no relay of its own yet — it shares the testnet relay until one is stood up.
|
|
329
|
+
// Consequence while shared: nametag bindings for both networks live in ONE namespace, and
|
|
330
|
+
// since bindings carry no network the cross-network recipient guard can only signal (#734).
|
|
331
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
279
332
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
280
|
-
|
|
333
|
+
// Published, but currently only the non-fungible base token type — no fungible
|
|
334
|
+
// coins yet, so those still miss the registry (decimals 0, symbol falls back to
|
|
335
|
+
// six hex chars). Presentation only: the money path treats coinId as opaque bytes.
|
|
336
|
+
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json"
|
|
281
337
|
},
|
|
282
338
|
// v1 cutover: 'testnet' now POINTS AT TESTNET2 (the v2 gateway network). The
|
|
283
339
|
// old goggregator testnet spoke the removed v1 protocol — a v2 engine cannot
|
|
284
340
|
// run against it. 'testnet2' stays as an alias of the same configuration.
|
|
285
341
|
testnet: {
|
|
286
|
-
name: "
|
|
342
|
+
name: "Testnet",
|
|
287
343
|
networkId: 4,
|
|
288
344
|
// v2 state-transition gateway (networkId 4 comes from the trust base). apiKey is env-injected.
|
|
289
345
|
aggregatorUrl: "https://gateway.testnet2.unicity.network",
|
|
@@ -293,7 +349,7 @@ var NETWORKS = {
|
|
|
293
349
|
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
|
|
294
350
|
},
|
|
295
351
|
testnet2: {
|
|
296
|
-
name: "
|
|
352
|
+
name: "Testnet",
|
|
297
353
|
networkId: 4,
|
|
298
354
|
// v2 state-transition gateway (networkId 4 comes from the trust base). apiKey is env-injected.
|
|
299
355
|
aggregatorUrl: "https://gateway.testnet2.unicity.network",
|
|
@@ -301,19 +357,10 @@ var NETWORKS = {
|
|
|
301
357
|
// reuse testnet infra (shared relays/ipfs)
|
|
302
358
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
303
359
|
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
|
|
304
|
-
},
|
|
305
|
-
// NOTE: mainnet/dev still point at v1-era aggregators. The v2 engine cannot
|
|
306
|
-
// operate against them until their gateways are cut over to the v2 protocol —
|
|
307
|
-
// wallet operations on these networks fail loudly (AGGREGATOR_ERROR) until then.
|
|
308
|
-
dev: {
|
|
309
|
-
name: "Development",
|
|
310
|
-
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
311
|
-
nostrRelays: TEST_NOSTR_RELAYS,
|
|
312
|
-
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
313
|
-
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
314
360
|
}
|
|
315
361
|
};
|
|
316
362
|
var SPHERE_NETWORKS = {
|
|
363
|
+
mainnet: { id: NETWORKS.mainnet.networkId, name: "mainnet" },
|
|
317
364
|
testnet2: { id: NETWORKS.testnet2.networkId, name: "testnet2" }
|
|
318
365
|
};
|
|
319
366
|
var TIMEOUTS = {
|
|
@@ -329,12 +376,106 @@ var TIMEOUTS = {
|
|
|
329
376
|
SYNC_INTERVAL: 6e4
|
|
330
377
|
};
|
|
331
378
|
|
|
379
|
+
// storage/tracked-addresses.ts
|
|
380
|
+
function isDerivableIndex(value) {
|
|
381
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
|
|
382
|
+
}
|
|
383
|
+
function num(value, fallback) {
|
|
384
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
385
|
+
}
|
|
386
|
+
function toEntry(value) {
|
|
387
|
+
if (typeof value !== "object" || value === null) return null;
|
|
388
|
+
const e = value;
|
|
389
|
+
if (!isDerivableIndex(e.index)) return null;
|
|
390
|
+
return {
|
|
391
|
+
...e,
|
|
392
|
+
index: e.index,
|
|
393
|
+
hidden: e.hidden === true,
|
|
394
|
+
createdAt: num(e.createdAt, 0),
|
|
395
|
+
updatedAt: num(e.updatedAt, 0)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function parseTrackedAddresses(raw) {
|
|
399
|
+
if (!raw) return [];
|
|
400
|
+
let parsed;
|
|
401
|
+
try {
|
|
402
|
+
parsed = JSON.parse(raw);
|
|
403
|
+
} catch {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
if (typeof parsed !== "object" || parsed === null) return [];
|
|
407
|
+
const addresses = parsed.addresses;
|
|
408
|
+
if (!Array.isArray(addresses)) return [];
|
|
409
|
+
const entries = [];
|
|
410
|
+
for (const row of addresses) {
|
|
411
|
+
const entry = toEntry(row);
|
|
412
|
+
if (entry) entries.push(entry);
|
|
413
|
+
}
|
|
414
|
+
return entries;
|
|
415
|
+
}
|
|
416
|
+
function mergeTrackedAddresses(onDisk, incoming) {
|
|
417
|
+
const merged = /* @__PURE__ */ new Map();
|
|
418
|
+
for (const entry of onDisk) {
|
|
419
|
+
if (isDerivableIndex(entry.index)) merged.set(entry.index, entry);
|
|
420
|
+
}
|
|
421
|
+
for (const entry of incoming) {
|
|
422
|
+
if (!isDerivableIndex(entry.index)) {
|
|
423
|
+
throw new SphereError(
|
|
424
|
+
`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.`,
|
|
425
|
+
"VALIDATION_ERROR"
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const existing = merged.get(entry.index);
|
|
429
|
+
if (!existing) {
|
|
430
|
+
merged.set(entry.index, entry);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const winner = entry.updatedAt >= existing.updatedAt ? entry : existing;
|
|
434
|
+
merged.set(entry.index, {
|
|
435
|
+
...existing,
|
|
436
|
+
...winner,
|
|
437
|
+
index: entry.index,
|
|
438
|
+
createdAt: Math.min(existing.createdAt, entry.createdAt)
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return Array.from(merged.values()).sort((a, b) => a.index - b.index);
|
|
442
|
+
}
|
|
443
|
+
|
|
332
444
|
// impl/browser/storage/LocalStorageProvider.ts
|
|
445
|
+
var storeIdentity = sharedCell(
|
|
446
|
+
"impl.browser.localStorage.identity@1",
|
|
447
|
+
() => ({ tags: /* @__PURE__ */ new WeakMap(), writeChains: /* @__PURE__ */ new Map(), seq: 0 }),
|
|
448
|
+
(cell) => {
|
|
449
|
+
const c = cell;
|
|
450
|
+
return c.tags instanceof WeakMap && c.writeChains instanceof Map && typeof c.seq === "number";
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
function storageObjectTag(storage) {
|
|
454
|
+
let tag = storeIdentity.tags.get(storage);
|
|
455
|
+
if (!tag) {
|
|
456
|
+
tag = String(++storeIdentity.seq);
|
|
457
|
+
storeIdentity.tags.set(storage, tag);
|
|
458
|
+
}
|
|
459
|
+
return tag;
|
|
460
|
+
}
|
|
461
|
+
var trackedWriteChains = storeIdentity.writeChains;
|
|
462
|
+
function serializeTrackedWrite(storeId, task) {
|
|
463
|
+
const previous = trackedWriteChains.get(storeId) ?? Promise.resolve();
|
|
464
|
+
const run = previous.then(task);
|
|
465
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
466
|
+
trackedWriteChains.set(storeId, tail);
|
|
467
|
+
void tail.then(() => {
|
|
468
|
+
if (trackedWriteChains.get(storeId) === tail) trackedWriteChains.delete(storeId);
|
|
469
|
+
});
|
|
470
|
+
return run;
|
|
471
|
+
}
|
|
333
472
|
var LocalStorageProvider = class {
|
|
334
473
|
id = "localStorage";
|
|
335
474
|
name = "Local Storage";
|
|
336
475
|
type = "local";
|
|
337
476
|
description = "Browser localStorage for single-device persistence";
|
|
477
|
+
/** The `Storage` object + prefix — two providers over one pair share erasure (#766). */
|
|
478
|
+
backingStoreId;
|
|
338
479
|
config;
|
|
339
480
|
network;
|
|
340
481
|
identity = null;
|
|
@@ -347,6 +488,7 @@ var LocalStorageProvider = class {
|
|
|
347
488
|
debug: config?.debug ?? false
|
|
348
489
|
};
|
|
349
490
|
this.network = config?.network;
|
|
491
|
+
this.backingStoreId = `localstorage:${storageObjectTag(storage)}:${encodeURIComponent(this.config.prefix)}`;
|
|
350
492
|
}
|
|
351
493
|
// ===========================================================================
|
|
352
494
|
// BaseProvider Implementation
|
|
@@ -422,18 +564,27 @@ var LocalStorageProvider = class {
|
|
|
422
564
|
await this.remove(key);
|
|
423
565
|
}
|
|
424
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* Persist the tracked-address registry by MERGING, never replacing.
|
|
569
|
+
*
|
|
570
|
+
* Every Sphere over this storage holds its own snapshot and writes it in
|
|
571
|
+
* full, so a wholesale write drops the addresses this writer never saw
|
|
572
|
+
* (#766 item 5 — a lost update, reproducible on one network). localStorage
|
|
573
|
+
* offers no transaction, so the read-merge-write is serialized by BACKING
|
|
574
|
+
* STORE — see `serializeTrackedWrite`, and the realm limit stated there.
|
|
575
|
+
*/
|
|
425
576
|
async saveTrackedAddresses(entries) {
|
|
426
|
-
await this.
|
|
577
|
+
await serializeTrackedWrite(this.backingStoreId, async () => {
|
|
578
|
+
const onDisk = parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
|
|
579
|
+
const file = {
|
|
580
|
+
version: 1,
|
|
581
|
+
addresses: mergeTrackedAddresses(onDisk, entries)
|
|
582
|
+
};
|
|
583
|
+
await this.set(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES, JSON.stringify(file));
|
|
584
|
+
});
|
|
427
585
|
}
|
|
428
586
|
async loadTrackedAddresses() {
|
|
429
|
-
|
|
430
|
-
if (!data) return [];
|
|
431
|
-
try {
|
|
432
|
-
const parsed = JSON.parse(data);
|
|
433
|
-
return parsed.addresses ?? [];
|
|
434
|
-
} catch {
|
|
435
|
-
return [];
|
|
436
|
-
}
|
|
587
|
+
return parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
|
|
437
588
|
}
|
|
438
589
|
// ===========================================================================
|
|
439
590
|
// Helpers
|
|
@@ -520,6 +671,8 @@ var IndexedDBStorageProvider = class {
|
|
|
520
671
|
name = "IndexedDB Storage";
|
|
521
672
|
type = "local";
|
|
522
673
|
description = "Browser IndexedDB for large-capacity persistence";
|
|
674
|
+
/** The database + prefix pair — two providers over one pair share erasure (#766). */
|
|
675
|
+
backingStoreId;
|
|
523
676
|
prefix;
|
|
524
677
|
dbName;
|
|
525
678
|
network;
|
|
@@ -534,6 +687,7 @@ var IndexedDBStorageProvider = class {
|
|
|
534
687
|
this.dbName = config?.dbName ?? DB_NAME;
|
|
535
688
|
this.network = config?.network;
|
|
536
689
|
this.debug = config?.debug ?? false;
|
|
690
|
+
this.backingStoreId = `indexeddb:${encodeURIComponent(this.dbName)}:${encodeURIComponent(this.prefix)}`;
|
|
537
691
|
}
|
|
538
692
|
// ===========================================================================
|
|
539
693
|
// BaseProvider Implementation
|
|
@@ -663,18 +817,27 @@ var IndexedDBStorageProvider = class {
|
|
|
663
817
|
await this.remove(key);
|
|
664
818
|
}
|
|
665
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* Persist the tracked-address registry by MERGING, never replacing.
|
|
822
|
+
*
|
|
823
|
+
* Every Sphere over this storage holds its own snapshot and writes it in
|
|
824
|
+
* full, so a wholesale write drops the addresses this writer never saw
|
|
825
|
+
* (#766 item 5 — a lost update, reproducible on one network). The read, the
|
|
826
|
+
* merge and the write share ONE transaction: a lock on this object would
|
|
827
|
+
* order only this object's calls, and `backingStoreId` exists precisely
|
|
828
|
+
* because two provider objects — or two TABS — can address one database.
|
|
829
|
+
* IndexedDB serializes overlapping readwrite transactions across every
|
|
830
|
+
* connection, so both are covered.
|
|
831
|
+
*/
|
|
666
832
|
async saveTrackedAddresses(entries) {
|
|
667
|
-
|
|
833
|
+
this.ensureConnected();
|
|
834
|
+
await this.idbMergeTrackedAddresses(
|
|
835
|
+
this.getFullKey(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES),
|
|
836
|
+
entries
|
|
837
|
+
);
|
|
668
838
|
}
|
|
669
839
|
async loadTrackedAddresses() {
|
|
670
|
-
|
|
671
|
-
if (!data) return [];
|
|
672
|
-
try {
|
|
673
|
-
const parsed = JSON.parse(data);
|
|
674
|
-
return parsed.addresses ?? [];
|
|
675
|
-
} catch {
|
|
676
|
-
return [];
|
|
677
|
-
}
|
|
840
|
+
return parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
|
|
678
841
|
}
|
|
679
842
|
// ===========================================================================
|
|
680
843
|
// Helpers
|
|
@@ -791,6 +954,25 @@ var IndexedDBStorageProvider = class {
|
|
|
791
954
|
request.onsuccess = () => resolve(request.result ?? []);
|
|
792
955
|
});
|
|
793
956
|
}
|
|
957
|
+
idbMergeTrackedAddresses(key, entries) {
|
|
958
|
+
return new Promise((resolve, reject) => {
|
|
959
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
960
|
+
const store = tx.objectStore(STORE_NAME);
|
|
961
|
+
const read = store.get(key);
|
|
962
|
+
read.onerror = () => reject(read.error);
|
|
963
|
+
read.onsuccess = () => {
|
|
964
|
+
const onDisk = parseTrackedAddresses(read.result?.v ?? null);
|
|
965
|
+
const file = {
|
|
966
|
+
version: 1,
|
|
967
|
+
addresses: mergeTrackedAddresses(onDisk, entries)
|
|
968
|
+
};
|
|
969
|
+
store.put({ k: key, v: JSON.stringify(file) });
|
|
970
|
+
};
|
|
971
|
+
tx.oncomplete = () => resolve();
|
|
972
|
+
tx.onerror = () => reject(tx.error);
|
|
973
|
+
tx.onabort = () => reject(tx.error ?? new Error("tracked-address write aborted"));
|
|
974
|
+
});
|
|
975
|
+
}
|
|
794
976
|
idbClear() {
|
|
795
977
|
return new Promise((resolve, reject) => {
|
|
796
978
|
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
@@ -1369,14 +1551,10 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1369
1551
|
});
|
|
1370
1552
|
}
|
|
1371
1553
|
});
|
|
1372
|
-
await
|
|
1373
|
-
this.nostrClient
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1377
|
-
)), this.config.timeout)
|
|
1378
|
-
)
|
|
1379
|
-
]);
|
|
1554
|
+
await this.connectWithDeadline(
|
|
1555
|
+
this.nostrClient,
|
|
1556
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1557
|
+
);
|
|
1380
1558
|
if (!this.nostrClient.isConnected()) {
|
|
1381
1559
|
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
1382
1560
|
}
|
|
@@ -1391,6 +1569,27 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1391
1569
|
throw error;
|
|
1392
1570
|
}
|
|
1393
1571
|
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Race one client's relay connect against the configured timeout.
|
|
1574
|
+
*
|
|
1575
|
+
* The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
|
|
1576
|
+
* cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
|
|
1577
|
+
* for the whole `config.timeout` after the call has already returned (and
|
|
1578
|
+
* then rejects a promise nobody is listening to any more).
|
|
1579
|
+
*/
|
|
1580
|
+
async connectWithDeadline(client, timeoutMessage) {
|
|
1581
|
+
let timer;
|
|
1582
|
+
try {
|
|
1583
|
+
await Promise.race([
|
|
1584
|
+
client.connect(...this.config.relays),
|
|
1585
|
+
new Promise((_, reject) => {
|
|
1586
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), this.config.timeout);
|
|
1587
|
+
})
|
|
1588
|
+
]);
|
|
1589
|
+
} finally {
|
|
1590
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1394
1593
|
async disconnect() {
|
|
1395
1594
|
if (this.nostrClient) {
|
|
1396
1595
|
this.nostrClient.disconnect();
|
|
@@ -1509,19 +1708,22 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1509
1708
|
// TransportProvider Implementation
|
|
1510
1709
|
// ===========================================================================
|
|
1511
1710
|
async setIdentity(identity) {
|
|
1512
|
-
this.identity = identity;
|
|
1513
|
-
this.processedEventIds.clear();
|
|
1514
|
-
this.lastEventTs = 0;
|
|
1515
|
-
this.lastDmEventTs = 0;
|
|
1516
|
-
this.fallbackDmSince = null;
|
|
1517
1711
|
const secretKey = import_buffer.Buffer.from(identity.privateKey, "hex");
|
|
1518
|
-
|
|
1519
|
-
const nostrPubkey =
|
|
1520
|
-
logger.debug("Nostr", "Identity
|
|
1712
|
+
const nextKeyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(secretKey);
|
|
1713
|
+
const nostrPubkey = nextKeyManager.getPublicKeyHex();
|
|
1714
|
+
logger.debug("Nostr", "Identity staged, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
|
|
1715
|
+
const applyStagedIdentity = () => {
|
|
1716
|
+
this.identity = identity;
|
|
1717
|
+
this.keyManager = nextKeyManager;
|
|
1718
|
+
this.processedEventIds.clear();
|
|
1719
|
+
this.lastEventTs = 0;
|
|
1720
|
+
this.lastDmEventTs = 0;
|
|
1721
|
+
this.fallbackDmSince = null;
|
|
1722
|
+
};
|
|
1521
1723
|
if (this.nostrClient && this.status === "connected") {
|
|
1522
1724
|
logger.debug("Nostr", "Identity changed while connected - recreating NostrClient");
|
|
1523
1725
|
const oldClient = this.nostrClient;
|
|
1524
|
-
|
|
1726
|
+
const nextClient = new import_nostr_js_sdk.NostrClient(nextKeyManager, {
|
|
1525
1727
|
autoReconnect: this.config.autoReconnect,
|
|
1526
1728
|
reconnectIntervalMs: this.config.reconnectDelay,
|
|
1527
1729
|
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
@@ -1530,7 +1732,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1530
1732
|
queryTimeoutMs: 2e4
|
|
1531
1733
|
// see same option above for rationale
|
|
1532
1734
|
});
|
|
1533
|
-
|
|
1735
|
+
nextClient.addConnectionListener({
|
|
1534
1736
|
onConnect: (url) => {
|
|
1535
1737
|
logger.debug("Nostr", "NostrClient connected to relay:", url);
|
|
1536
1738
|
},
|
|
@@ -1544,18 +1746,43 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1544
1746
|
logger.debug("Nostr", "NostrClient reconnected to relay:", url);
|
|
1545
1747
|
}
|
|
1546
1748
|
});
|
|
1547
|
-
|
|
1548
|
-
this.
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1749
|
+
try {
|
|
1750
|
+
await this.connectWithDeadline(
|
|
1751
|
+
nextClient,
|
|
1752
|
+
`Transport reconnection timed out after ${this.config.timeout}ms`
|
|
1753
|
+
);
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
try {
|
|
1756
|
+
nextClient.disconnect();
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
throw error;
|
|
1760
|
+
}
|
|
1761
|
+
if (this.nostrClient !== oldClient || this.status !== "connected") {
|
|
1762
|
+
try {
|
|
1763
|
+
nextClient.disconnect();
|
|
1764
|
+
} catch {
|
|
1765
|
+
}
|
|
1766
|
+
throw new SphereError(
|
|
1767
|
+
"Transport client changed or left the connected state during setIdentity - identity not applied",
|
|
1768
|
+
"TRANSPORT_ERROR"
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
this.nostrClient = nextClient;
|
|
1772
|
+
applyStagedIdentity();
|
|
1773
|
+
try {
|
|
1774
|
+
await this.subscribeToEvents();
|
|
1775
|
+
} finally {
|
|
1776
|
+
try {
|
|
1777
|
+
oldClient.disconnect();
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1557
1781
|
} else if (this.isConnected()) {
|
|
1782
|
+
applyStagedIdentity();
|
|
1558
1783
|
await this.subscribeToEvents();
|
|
1784
|
+
} else {
|
|
1785
|
+
applyStagedIdentity();
|
|
1559
1786
|
}
|
|
1560
1787
|
}
|
|
1561
1788
|
setFallbackSince(sinceSeconds) {
|
|
@@ -2850,26 +3077,6 @@ var UnicityAggregatorProvider = class {
|
|
|
2850
3077
|
var UnicityOracleProvider = UnicityAggregatorProvider;
|
|
2851
3078
|
|
|
2852
3079
|
// assets/trustbase.ts
|
|
2853
|
-
var TRUSTBASE_TESTNET = {
|
|
2854
|
-
version: 1,
|
|
2855
|
-
networkId: 3,
|
|
2856
|
-
epoch: 1,
|
|
2857
|
-
epochStartRound: 1,
|
|
2858
|
-
rootNodes: [
|
|
2859
|
-
{
|
|
2860
|
-
nodeId: "16Uiu2HAkyQRiA7pMgzgLj9GgaBJEJa8zmx9dzqUDa6WxQPJ82ghU",
|
|
2861
|
-
sigKey: "0x039afb2acb65f5fbc272d8907f763d0a5d189aadc9b97afdcc5897ea4dd112e68b",
|
|
2862
|
-
stake: 1
|
|
2863
|
-
}
|
|
2864
|
-
],
|
|
2865
|
-
quorumThreshold: 1,
|
|
2866
|
-
stateHash: "",
|
|
2867
|
-
changeRecordHash: "",
|
|
2868
|
-
previousEntryHash: "",
|
|
2869
|
-
signatures: {
|
|
2870
|
-
"16Uiu2HAkyQRiA7pMgzgLj9GgaBJEJa8zmx9dzqUDa6WxQPJ82ghU": "0xf157c9fdd8a378e3ca70d354ccc4475ab2cd8de360127bc46b0aeab4b453a80f07fd9136a5843b60a8babaff23e20acc8879861f7651440a5e2829f7541b31f100"
|
|
2871
|
-
}
|
|
2872
|
-
};
|
|
2873
3080
|
var TRUSTBASE_TESTNET2 = {
|
|
2874
3081
|
version: 1,
|
|
2875
3082
|
networkId: 4,
|
|
@@ -2908,8 +3115,44 @@ var TRUSTBASE_TESTNET2 = {
|
|
|
2908
3115
|
"16Uiu2HAmU5T1TqKyKkUB3sitWb431hVFfp9nqvaLgVagQF6ywLFF": "0x9f28f3e3c50d4bfdaf54d7290fbbe311fffb3be2281a1d211be56818fa43d06a2e9a81925e9d365e552c8b772c99285a686de1d9fb581920be7524c7a5d467a301"
|
|
2909
3116
|
}
|
|
2910
3117
|
};
|
|
2911
|
-
var TRUSTBASE_MAINNET =
|
|
2912
|
-
|
|
3118
|
+
var TRUSTBASE_MAINNET = {
|
|
3119
|
+
version: 1,
|
|
3120
|
+
networkId: 1,
|
|
3121
|
+
epoch: 1,
|
|
3122
|
+
epochStartRound: 1,
|
|
3123
|
+
rootNodes: [
|
|
3124
|
+
{
|
|
3125
|
+
nodeId: "16Uiu2HAkvk4EpXoyh7SVLTX3TcsZkjbni5jY1sBgJ7qCEWfx1Gma",
|
|
3126
|
+
sigKey: "0x03059e30b5c980ce92e2c6d4e14775838be91a6f0e993f3a4899ab459c4b5225f2",
|
|
3127
|
+
stake: 1
|
|
3128
|
+
},
|
|
3129
|
+
{
|
|
3130
|
+
nodeId: "16Uiu2HAmF9Mq9pH9yUTdao3TpJeFZY9oX9gyMqfxggVA4fNmzmXr",
|
|
3131
|
+
sigKey: "0x03a514db8b17b1c79b4a6affeb5d88a129c4d5ff0f92c8f86c311971b0beb94335",
|
|
3132
|
+
stake: 1
|
|
3133
|
+
},
|
|
3134
|
+
{
|
|
3135
|
+
nodeId: "16Uiu2HAmLAHTSMvZ2apRRtBN9piYVhaK2i7sTMaQeSWSxsLh75TG",
|
|
3136
|
+
sigKey: "0x02e5669b2daa5d1afccfe66603f7d3fe171ecbd7ed891487ab3683cdebc3c3cd8b",
|
|
3137
|
+
stake: 1
|
|
3138
|
+
},
|
|
3139
|
+
{
|
|
3140
|
+
nodeId: "16Uiu2HAmNNaWfK4RTTAq7Ap8w2yZUryrEejqmmGf44ogMaYEnkJ4",
|
|
3141
|
+
sigKey: "0x03ffee964da8ca276d4fc1cf391929e6759db106ef1d0aea998154b51e5a7fb1b7",
|
|
3142
|
+
stake: 1
|
|
3143
|
+
}
|
|
3144
|
+
],
|
|
3145
|
+
quorumThreshold: 3,
|
|
3146
|
+
stateHash: "",
|
|
3147
|
+
changeRecordHash: "",
|
|
3148
|
+
previousEntryHash: "",
|
|
3149
|
+
signatures: {
|
|
3150
|
+
"16Uiu2HAkvk4EpXoyh7SVLTX3TcsZkjbni5jY1sBgJ7qCEWfx1Gma": "0xc2f1d67dbb47fe4dcf502301d8cd5021b8903c0d3385781653d09c703d566a3a3a4e59c75792b36f929e32564ddc14c40ef728c0bde5859774d08d802de2318c00",
|
|
3151
|
+
"16Uiu2HAmF9Mq9pH9yUTdao3TpJeFZY9oX9gyMqfxggVA4fNmzmXr": "0x3e3ad3a7540daa0eded457e31d2ebcacfb354036798f49f63433d26c44857e76157e4abd36cbb1d39de8cdf6e1d33f88b250077d4347c3e5bf0f35a64f2d815300",
|
|
3152
|
+
"16Uiu2HAmLAHTSMvZ2apRRtBN9piYVhaK2i7sTMaQeSWSxsLh75TG": "0x6101bd9df8ab87439d90b52cd74727bc8bf31dd74173c8f731ec3ea501d3d86556ba7c0e84643337959f3be0e448637c301e8e0b28196e780d4f2ff930d9062100",
|
|
3153
|
+
"16Uiu2HAmNNaWfK4RTTAq7Ap8w2yZUryrEejqmmGf44ogMaYEnkJ4": "0xda13c777b832425e5cbd8d6e49eb08ddbde35c1e4a9bee8bd495c3883f039e434b64560865c3bc52b68d65780932b1f2bf8971f245d2244face822af5984945c00"
|
|
3154
|
+
}
|
|
3155
|
+
};
|
|
2913
3156
|
|
|
2914
3157
|
// impl/shared/trustbase-loader.ts
|
|
2915
3158
|
function getEmbeddedTrustBase(network) {
|
|
@@ -2922,8 +3165,6 @@ function getEmbeddedTrustBase(network) {
|
|
|
2922
3165
|
case "testnet":
|
|
2923
3166
|
case "testnet2":
|
|
2924
3167
|
return TRUSTBASE_TESTNET2;
|
|
2925
|
-
case "dev":
|
|
2926
|
-
return TRUSTBASE_DEV;
|
|
2927
3168
|
default:
|
|
2928
3169
|
throw new SphereError(`getEmbeddedTrustBase: unknown network "${network}"`, "INVALID_CONFIG");
|
|
2929
3170
|
}
|
|
@@ -3080,6 +3321,7 @@ async function readFileAsUint8Array(file) {
|
|
|
3080
3321
|
|
|
3081
3322
|
// impl/shared/network.ts
|
|
3082
3323
|
var EXPECTED_NETWORK_ID = {
|
|
3324
|
+
mainnet: 1,
|
|
3083
3325
|
// v1 cutover: 'testnet' is an alias of testnet2 (the v2 gateway network),
|
|
3084
3326
|
// so both expect the testnet2 trust base (networkId 4).
|
|
3085
3327
|
testnet: 4,
|
|
@@ -3100,7 +3342,7 @@ function assertNetworkConsistency(network) {
|
|
|
3100
3342
|
);
|
|
3101
3343
|
}
|
|
3102
3344
|
const expected = EXPECTED_NETWORK_ID[network];
|
|
3103
|
-
if (
|
|
3345
|
+
if (networkId !== expected) {
|
|
3104
3346
|
throw new SphereError(
|
|
3105
3347
|
`Network "${network}" trust base networkId ${networkId} does not match expected ${expected} (wrong trust base wired to this network).`,
|
|
3106
3348
|
"INVALID_CONFIG"
|
|
@@ -3365,419 +3607,6 @@ function createPriceProvider(config) {
|
|
|
3365
3607
|
}
|
|
3366
3608
|
}
|
|
3367
3609
|
|
|
3368
|
-
// registry/TokenRegistry.ts
|
|
3369
|
-
var FETCH_TIMEOUT_MS = 1e4;
|
|
3370
|
-
var TokenRegistry = class _TokenRegistry {
|
|
3371
|
-
static instance = null;
|
|
3372
|
-
definitionsById;
|
|
3373
|
-
definitionsBySymbol;
|
|
3374
|
-
definitionsByName;
|
|
3375
|
-
// Remote refresh state
|
|
3376
|
-
remoteUrl = null;
|
|
3377
|
-
storage = null;
|
|
3378
|
-
refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
|
|
3379
|
-
refreshTimer = null;
|
|
3380
|
-
lastRefreshAt = 0;
|
|
3381
|
-
refreshPromise = null;
|
|
3382
|
-
initialLoadPromise = null;
|
|
3383
|
-
constructor() {
|
|
3384
|
-
this.definitionsById = /* @__PURE__ */ new Map();
|
|
3385
|
-
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
3386
|
-
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
3387
|
-
}
|
|
3388
|
-
/**
|
|
3389
|
-
* Get singleton instance of TokenRegistry
|
|
3390
|
-
*/
|
|
3391
|
-
static getInstance() {
|
|
3392
|
-
if (!_TokenRegistry.instance) {
|
|
3393
|
-
_TokenRegistry.instance = new _TokenRegistry();
|
|
3394
|
-
}
|
|
3395
|
-
return _TokenRegistry.instance;
|
|
3396
|
-
}
|
|
3397
|
-
/**
|
|
3398
|
-
* Configure remote registry refresh with persistent caching.
|
|
3399
|
-
*
|
|
3400
|
-
* On first call:
|
|
3401
|
-
* 1. Loads cached data from StorageProvider (if available and fresh)
|
|
3402
|
-
* 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
|
|
3403
|
-
*
|
|
3404
|
-
* @param options - Configuration options
|
|
3405
|
-
* @param options.remoteUrl - Remote URL to fetch definitions from
|
|
3406
|
-
* @param options.storage - StorageProvider for persistent caching
|
|
3407
|
-
* @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
|
|
3408
|
-
* @param options.autoRefresh - Start auto-refresh immediately (default: true)
|
|
3409
|
-
*/
|
|
3410
|
-
static configure(options) {
|
|
3411
|
-
const instance = _TokenRegistry.getInstance();
|
|
3412
|
-
if (options.remoteUrl !== void 0) {
|
|
3413
|
-
instance.remoteUrl = options.remoteUrl;
|
|
3414
|
-
}
|
|
3415
|
-
if (options.storage !== void 0) {
|
|
3416
|
-
instance.storage = options.storage;
|
|
3417
|
-
}
|
|
3418
|
-
if (options.refreshIntervalMs !== void 0) {
|
|
3419
|
-
instance.refreshIntervalMs = options.refreshIntervalMs;
|
|
3420
|
-
}
|
|
3421
|
-
const autoRefresh = options.autoRefresh ?? true;
|
|
3422
|
-
instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
|
|
3423
|
-
}
|
|
3424
|
-
/**
|
|
3425
|
-
* Reset the singleton instance (useful for testing).
|
|
3426
|
-
* Stops auto-refresh if running.
|
|
3427
|
-
*/
|
|
3428
|
-
static resetInstance() {
|
|
3429
|
-
if (_TokenRegistry.instance) {
|
|
3430
|
-
_TokenRegistry.instance.stopAutoRefresh();
|
|
3431
|
-
}
|
|
3432
|
-
_TokenRegistry.instance = null;
|
|
3433
|
-
}
|
|
3434
|
-
/**
|
|
3435
|
-
* Destroy the singleton: stop auto-refresh and reset.
|
|
3436
|
-
*/
|
|
3437
|
-
static destroy() {
|
|
3438
|
-
_TokenRegistry.resetInstance();
|
|
3439
|
-
}
|
|
3440
|
-
/**
|
|
3441
|
-
* Wait for the initial data load (cache or remote) to complete.
|
|
3442
|
-
* Returns true if data was loaded, false if not (timeout or no data source).
|
|
3443
|
-
*
|
|
3444
|
-
* @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
|
|
3445
|
-
*/
|
|
3446
|
-
static async waitForReady(timeoutMs = 1e4) {
|
|
3447
|
-
const instance = _TokenRegistry.getInstance();
|
|
3448
|
-
if (!instance.initialLoadPromise) {
|
|
3449
|
-
return instance.definitionsById.size > 0;
|
|
3450
|
-
}
|
|
3451
|
-
if (timeoutMs <= 0) {
|
|
3452
|
-
return instance.initialLoadPromise;
|
|
3453
|
-
}
|
|
3454
|
-
return Promise.race([
|
|
3455
|
-
instance.initialLoadPromise,
|
|
3456
|
-
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
|
|
3457
|
-
]);
|
|
3458
|
-
}
|
|
3459
|
-
// ===========================================================================
|
|
3460
|
-
// Initial Load
|
|
3461
|
-
// ===========================================================================
|
|
3462
|
-
/**
|
|
3463
|
-
* Perform initial data load: try cache first, fall back to remote fetch.
|
|
3464
|
-
* After initial data is available, start periodic auto-refresh if configured.
|
|
3465
|
-
*/
|
|
3466
|
-
async performInitialLoad(autoRefresh) {
|
|
3467
|
-
let loaded = false;
|
|
3468
|
-
if (this.storage) {
|
|
3469
|
-
loaded = await this.loadFromCache();
|
|
3470
|
-
}
|
|
3471
|
-
if (loaded) {
|
|
3472
|
-
if (autoRefresh && this.remoteUrl) {
|
|
3473
|
-
this.startAutoRefresh();
|
|
3474
|
-
}
|
|
3475
|
-
return true;
|
|
3476
|
-
}
|
|
3477
|
-
if (autoRefresh && this.remoteUrl) {
|
|
3478
|
-
loaded = await this.refreshFromRemote();
|
|
3479
|
-
this.stopAutoRefresh();
|
|
3480
|
-
this.refreshTimer = setInterval(() => {
|
|
3481
|
-
this.refreshFromRemote();
|
|
3482
|
-
}, this.refreshIntervalMs);
|
|
3483
|
-
return loaded;
|
|
3484
|
-
}
|
|
3485
|
-
return false;
|
|
3486
|
-
}
|
|
3487
|
-
// ===========================================================================
|
|
3488
|
-
// Cache (StorageProvider)
|
|
3489
|
-
// ===========================================================================
|
|
3490
|
-
/**
|
|
3491
|
-
* Per-network cache keys. The registry cache MUST be namespaced by remoteUrl:
|
|
3492
|
-
* testnet and testnet2 ship DIFFERENT coinIds for the same symbol, so a single
|
|
3493
|
-
* global key let a stale testnet snapshot be served for a testnet2 request —
|
|
3494
|
-
* wrong/missing icons AND self-mint resolving the wrong-network coinId. Keying
|
|
3495
|
-
* by remoteUrl isolates the snapshots so a network can never read another's.
|
|
3496
|
-
*/
|
|
3497
|
-
cacheKeys() {
|
|
3498
|
-
const base = STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE;
|
|
3499
|
-
const baseTs = STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS;
|
|
3500
|
-
if (!this.remoteUrl) return { data: base, ts: baseTs };
|
|
3501
|
-
return { data: `${base}:${this.remoteUrl}`, ts: `${baseTs}:${this.remoteUrl}` };
|
|
3502
|
-
}
|
|
3503
|
-
/**
|
|
3504
|
-
* Load definitions from StorageProvider cache.
|
|
3505
|
-
* Only applies if cache exists and is fresh (within refreshIntervalMs).
|
|
3506
|
-
*/
|
|
3507
|
-
async loadFromCache() {
|
|
3508
|
-
if (!this.storage) return false;
|
|
3509
|
-
try {
|
|
3510
|
-
const { data: dataKey, ts: tsKey } = this.cacheKeys();
|
|
3511
|
-
const [cached, cachedTs] = await Promise.all([
|
|
3512
|
-
this.storage.get(dataKey),
|
|
3513
|
-
this.storage.get(tsKey)
|
|
3514
|
-
]);
|
|
3515
|
-
if (!cached || !cachedTs) return false;
|
|
3516
|
-
const ts = parseInt(cachedTs, 10);
|
|
3517
|
-
if (isNaN(ts)) return false;
|
|
3518
|
-
const age = Date.now() - ts;
|
|
3519
|
-
if (age > this.refreshIntervalMs) return false;
|
|
3520
|
-
if (this.lastRefreshAt > ts) return false;
|
|
3521
|
-
const data = JSON.parse(cached);
|
|
3522
|
-
if (!this.isValidDefinitionsArray(data)) return false;
|
|
3523
|
-
this.applyDefinitions(data);
|
|
3524
|
-
this.lastRefreshAt = ts;
|
|
3525
|
-
return true;
|
|
3526
|
-
} catch {
|
|
3527
|
-
return false;
|
|
3528
|
-
}
|
|
3529
|
-
}
|
|
3530
|
-
/**
|
|
3531
|
-
* Save definitions to StorageProvider cache.
|
|
3532
|
-
*/
|
|
3533
|
-
async saveToCache(definitions) {
|
|
3534
|
-
if (!this.storage) return;
|
|
3535
|
-
try {
|
|
3536
|
-
const { data: dataKey, ts: tsKey } = this.cacheKeys();
|
|
3537
|
-
await Promise.all([
|
|
3538
|
-
this.storage.set(dataKey, JSON.stringify(definitions)),
|
|
3539
|
-
this.storage.set(tsKey, String(Date.now()))
|
|
3540
|
-
]);
|
|
3541
|
-
} catch {
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
// ===========================================================================
|
|
3545
|
-
// Remote Refresh
|
|
3546
|
-
// ===========================================================================
|
|
3547
|
-
/**
|
|
3548
|
-
* Apply an array of token definitions to the internal maps.
|
|
3549
|
-
* Clears existing data before applying.
|
|
3550
|
-
*/
|
|
3551
|
-
applyDefinitions(definitions) {
|
|
3552
|
-
this.definitionsById.clear();
|
|
3553
|
-
this.definitionsBySymbol.clear();
|
|
3554
|
-
this.definitionsByName.clear();
|
|
3555
|
-
for (const def of definitions) {
|
|
3556
|
-
const idLower = def.id.toLowerCase();
|
|
3557
|
-
this.definitionsById.set(idLower, def);
|
|
3558
|
-
if (def.symbol) {
|
|
3559
|
-
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
3560
|
-
}
|
|
3561
|
-
this.definitionsByName.set(def.name.toLowerCase(), def);
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
/**
|
|
3565
|
-
* Validate that data is an array of objects with 'id' field
|
|
3566
|
-
*/
|
|
3567
|
-
isValidDefinitionsArray(data) {
|
|
3568
|
-
return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
|
|
3569
|
-
}
|
|
3570
|
-
/**
|
|
3571
|
-
* Fetch token definitions from the remote URL and update the registry.
|
|
3572
|
-
* On success, also persists to StorageProvider cache.
|
|
3573
|
-
* Returns true on success, false on failure. On failure, existing data is preserved.
|
|
3574
|
-
* Concurrent calls are deduplicated — only one fetch runs at a time.
|
|
3575
|
-
*/
|
|
3576
|
-
async refreshFromRemote() {
|
|
3577
|
-
if (!this.remoteUrl) {
|
|
3578
|
-
return false;
|
|
3579
|
-
}
|
|
3580
|
-
if (this.refreshPromise) {
|
|
3581
|
-
return this.refreshPromise;
|
|
3582
|
-
}
|
|
3583
|
-
this.refreshPromise = this.doRefresh();
|
|
3584
|
-
try {
|
|
3585
|
-
return await this.refreshPromise;
|
|
3586
|
-
} finally {
|
|
3587
|
-
this.refreshPromise = null;
|
|
3588
|
-
}
|
|
3589
|
-
}
|
|
3590
|
-
async doRefresh() {
|
|
3591
|
-
try {
|
|
3592
|
-
const controller = new AbortController();
|
|
3593
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
3594
|
-
let response;
|
|
3595
|
-
try {
|
|
3596
|
-
response = await fetch(this.remoteUrl, {
|
|
3597
|
-
headers: { Accept: "application/json" },
|
|
3598
|
-
signal: controller.signal
|
|
3599
|
-
});
|
|
3600
|
-
} finally {
|
|
3601
|
-
clearTimeout(timer);
|
|
3602
|
-
}
|
|
3603
|
-
if (!response.ok) {
|
|
3604
|
-
logger.warn("TokenRegistry", `Remote fetch failed: HTTP ${response.status} ${response.statusText}`);
|
|
3605
|
-
return false;
|
|
3606
|
-
}
|
|
3607
|
-
const data = await response.json();
|
|
3608
|
-
if (!this.isValidDefinitionsArray(data)) {
|
|
3609
|
-
logger.warn("TokenRegistry", "Remote data is not a valid token definitions array");
|
|
3610
|
-
return false;
|
|
3611
|
-
}
|
|
3612
|
-
const definitions = data;
|
|
3613
|
-
this.applyDefinitions(definitions);
|
|
3614
|
-
this.lastRefreshAt = Date.now();
|
|
3615
|
-
this.saveToCache(definitions);
|
|
3616
|
-
return true;
|
|
3617
|
-
} catch (error) {
|
|
3618
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3619
|
-
logger.warn("TokenRegistry", `Remote refresh failed: ${message}`);
|
|
3620
|
-
return false;
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
/**
|
|
3624
|
-
* Start periodic auto-refresh from the remote URL.
|
|
3625
|
-
* Does an immediate fetch, then repeats at the configured interval.
|
|
3626
|
-
*/
|
|
3627
|
-
startAutoRefresh(intervalMs) {
|
|
3628
|
-
this.stopAutoRefresh();
|
|
3629
|
-
if (intervalMs !== void 0) {
|
|
3630
|
-
this.refreshIntervalMs = intervalMs;
|
|
3631
|
-
}
|
|
3632
|
-
this.refreshFromRemote();
|
|
3633
|
-
this.refreshTimer = setInterval(() => {
|
|
3634
|
-
this.refreshFromRemote();
|
|
3635
|
-
}, this.refreshIntervalMs);
|
|
3636
|
-
}
|
|
3637
|
-
/**
|
|
3638
|
-
* Stop periodic auto-refresh
|
|
3639
|
-
*/
|
|
3640
|
-
stopAutoRefresh() {
|
|
3641
|
-
if (this.refreshTimer !== null) {
|
|
3642
|
-
clearInterval(this.refreshTimer);
|
|
3643
|
-
this.refreshTimer = null;
|
|
3644
|
-
}
|
|
3645
|
-
}
|
|
3646
|
-
/**
|
|
3647
|
-
* Timestamp of the last successful remote refresh (0 if never refreshed)
|
|
3648
|
-
*/
|
|
3649
|
-
getLastRefreshAt() {
|
|
3650
|
-
return this.lastRefreshAt;
|
|
3651
|
-
}
|
|
3652
|
-
// ===========================================================================
|
|
3653
|
-
// Lookup Methods
|
|
3654
|
-
// ===========================================================================
|
|
3655
|
-
/**
|
|
3656
|
-
* Get token definition by hex coin ID
|
|
3657
|
-
* @param coinId - 64-character hex string
|
|
3658
|
-
* @returns Token definition or undefined if not found
|
|
3659
|
-
*/
|
|
3660
|
-
getDefinition(coinId) {
|
|
3661
|
-
if (!coinId) return void 0;
|
|
3662
|
-
return this.definitionsById.get(coinId.toLowerCase());
|
|
3663
|
-
}
|
|
3664
|
-
/**
|
|
3665
|
-
* Get token definition by symbol (e.g., "UCT", "BTC")
|
|
3666
|
-
* @param symbol - Token symbol (case-insensitive)
|
|
3667
|
-
* @returns Token definition or undefined if not found
|
|
3668
|
-
*/
|
|
3669
|
-
getDefinitionBySymbol(symbol) {
|
|
3670
|
-
if (!symbol) return void 0;
|
|
3671
|
-
return this.definitionsBySymbol.get(symbol.toUpperCase());
|
|
3672
|
-
}
|
|
3673
|
-
/**
|
|
3674
|
-
* Get token definition by name (e.g., "bitcoin", "ethereum")
|
|
3675
|
-
* @param name - Token name (case-insensitive)
|
|
3676
|
-
* @returns Token definition or undefined if not found
|
|
3677
|
-
*/
|
|
3678
|
-
getDefinitionByName(name) {
|
|
3679
|
-
if (!name) return void 0;
|
|
3680
|
-
return this.definitionsByName.get(name.toLowerCase());
|
|
3681
|
-
}
|
|
3682
|
-
/**
|
|
3683
|
-
* Get token symbol for a coin ID
|
|
3684
|
-
* @param coinId - 64-character hex string
|
|
3685
|
-
* @returns Symbol (e.g., "UCT") or truncated ID if not found
|
|
3686
|
-
*/
|
|
3687
|
-
getSymbol(coinId) {
|
|
3688
|
-
const def = this.getDefinition(coinId);
|
|
3689
|
-
if (def?.symbol) {
|
|
3690
|
-
return def.symbol;
|
|
3691
|
-
}
|
|
3692
|
-
return coinId.slice(0, 6).toUpperCase();
|
|
3693
|
-
}
|
|
3694
|
-
/**
|
|
3695
|
-
* Get token name for a coin ID
|
|
3696
|
-
* @param coinId - 64-character hex string
|
|
3697
|
-
* @returns Name (e.g., "Bitcoin") or coin ID if not found
|
|
3698
|
-
*/
|
|
3699
|
-
getName(coinId) {
|
|
3700
|
-
const def = this.getDefinition(coinId);
|
|
3701
|
-
if (def?.name) {
|
|
3702
|
-
return def.name.charAt(0).toUpperCase() + def.name.slice(1);
|
|
3703
|
-
}
|
|
3704
|
-
return coinId;
|
|
3705
|
-
}
|
|
3706
|
-
/**
|
|
3707
|
-
* Get decimal places for a coin ID
|
|
3708
|
-
* @param coinId - 64-character hex string
|
|
3709
|
-
* @returns Decimals or 0 if not found
|
|
3710
|
-
*/
|
|
3711
|
-
getDecimals(coinId) {
|
|
3712
|
-
const def = this.getDefinition(coinId);
|
|
3713
|
-
return def?.decimals ?? 0;
|
|
3714
|
-
}
|
|
3715
|
-
/**
|
|
3716
|
-
* Get icon URL for a coin ID
|
|
3717
|
-
* @param coinId - 64-character hex string
|
|
3718
|
-
* @param preferPng - Prefer PNG format over SVG
|
|
3719
|
-
* @returns Icon URL or null if not found
|
|
3720
|
-
*/
|
|
3721
|
-
getIconUrl(coinId, preferPng = true) {
|
|
3722
|
-
const def = this.getDefinition(coinId);
|
|
3723
|
-
if (!def?.icons || def.icons.length === 0) {
|
|
3724
|
-
return null;
|
|
3725
|
-
}
|
|
3726
|
-
if (preferPng) {
|
|
3727
|
-
const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
|
|
3728
|
-
if (pngIcon) return pngIcon.url;
|
|
3729
|
-
}
|
|
3730
|
-
return def.icons[0].url;
|
|
3731
|
-
}
|
|
3732
|
-
/**
|
|
3733
|
-
* Check if a coin ID is known in the registry
|
|
3734
|
-
* @param coinId - 64-character hex string
|
|
3735
|
-
* @returns true if the coin is in the registry
|
|
3736
|
-
*/
|
|
3737
|
-
isKnown(coinId) {
|
|
3738
|
-
return this.definitionsById.has(coinId.toLowerCase());
|
|
3739
|
-
}
|
|
3740
|
-
/**
|
|
3741
|
-
* Get all token definitions
|
|
3742
|
-
* @returns Array of all token definitions
|
|
3743
|
-
*/
|
|
3744
|
-
getAllDefinitions() {
|
|
3745
|
-
return Array.from(this.definitionsById.values());
|
|
3746
|
-
}
|
|
3747
|
-
/**
|
|
3748
|
-
* Get all fungible token definitions
|
|
3749
|
-
* @returns Array of fungible token definitions
|
|
3750
|
-
*/
|
|
3751
|
-
getFungibleTokens() {
|
|
3752
|
-
return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
|
|
3753
|
-
}
|
|
3754
|
-
/**
|
|
3755
|
-
* Get all non-fungible token definitions
|
|
3756
|
-
* @returns Array of non-fungible token definitions
|
|
3757
|
-
*/
|
|
3758
|
-
getNonFungibleTokens() {
|
|
3759
|
-
return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
|
|
3760
|
-
}
|
|
3761
|
-
/**
|
|
3762
|
-
* Get coin ID by symbol
|
|
3763
|
-
* @param symbol - Token symbol (e.g., "UCT")
|
|
3764
|
-
* @returns Coin ID hex string or undefined if not found
|
|
3765
|
-
*/
|
|
3766
|
-
getCoinIdBySymbol(symbol) {
|
|
3767
|
-
const def = this.getDefinitionBySymbol(symbol);
|
|
3768
|
-
return def?.id;
|
|
3769
|
-
}
|
|
3770
|
-
/**
|
|
3771
|
-
* Get coin ID by name
|
|
3772
|
-
* @param name - Token name (e.g., "bitcoin")
|
|
3773
|
-
* @returns Coin ID hex string or undefined if not found
|
|
3774
|
-
*/
|
|
3775
|
-
getCoinIdByName(name) {
|
|
3776
|
-
const def = this.getDefinitionByName(name);
|
|
3777
|
-
return def?.id;
|
|
3778
|
-
}
|
|
3779
|
-
};
|
|
3780
|
-
|
|
3781
3610
|
// impl/shared/resolvers.ts
|
|
3782
3611
|
function getNetworkConfig(network = "mainnet") {
|
|
3783
3612
|
return NETWORKS[network];
|
|
@@ -3871,8 +3700,6 @@ function createBrowserProviders(config) {
|
|
|
3871
3700
|
const priceConfig = resolvePriceConfig(config?.price, storage);
|
|
3872
3701
|
const groupChat = resolveGroupChatConfig(network, config?.groupChat);
|
|
3873
3702
|
const market = resolveMarketConfig(config?.market);
|
|
3874
|
-
const networkConfig = getNetworkConfig(network);
|
|
3875
|
-
TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
|
|
3876
3703
|
return {
|
|
3877
3704
|
storage,
|
|
3878
3705
|
groupChat,
|