@unicitylabs/sphere-sdk 0.15.0-dev.1 → 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.
- package/README.md +15 -5
- package/dist/connect/index.cjs +23 -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 +23 -25
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +726 -225
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +172 -21
- package/dist/core/index.d.ts +172 -21
- package/dist/core/index.js +726 -224
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +23 -25
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +23 -25
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +331 -504
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +331 -504
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +22 -24
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +22 -24
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +298 -569
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +90 -13
- package/dist/impl/nodejs/index.d.ts +90 -13
- package/dist/impl/nodejs/index.js +298 -569
- 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 +728 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -108
- package/dist/index.d.ts +227 -108
- package/dist/index.js +728 -224
- 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 +2 -1
|
@@ -93,6 +93,58 @@ var logger = {
|
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
// core/global-cell.ts
|
|
97
|
+
var CELLS_KEY = "__sphere_sdk_cells_v1__";
|
|
98
|
+
var bundleLocalBag = null;
|
|
99
|
+
function isObject(value) {
|
|
100
|
+
return typeof value === "object" && value !== null;
|
|
101
|
+
}
|
|
102
|
+
function installBag(host, fresh) {
|
|
103
|
+
try {
|
|
104
|
+
Object.defineProperty(host, CELLS_KEY, {
|
|
105
|
+
value: fresh,
|
|
106
|
+
writable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
enumerable: false
|
|
109
|
+
});
|
|
110
|
+
return host[CELLS_KEY] === fresh;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function cellBag() {
|
|
116
|
+
if (bundleLocalBag) return bundleLocalBag;
|
|
117
|
+
const host = globalThis;
|
|
118
|
+
const found = host[CELLS_KEY];
|
|
119
|
+
if (isObject(found)) return found;
|
|
120
|
+
const fresh = {};
|
|
121
|
+
if (!installBag(host, fresh)) bundleLocalBag = fresh;
|
|
122
|
+
return fresh;
|
|
123
|
+
}
|
|
124
|
+
function accepted(intact, candidate) {
|
|
125
|
+
try {
|
|
126
|
+
return intact(candidate);
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function put(bag, name, cell) {
|
|
132
|
+
try {
|
|
133
|
+
bag[name] = cell;
|
|
134
|
+
if (bag[name] === cell) return;
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
if (bag !== bundleLocalBag) bundleLocalBag = { [name]: cell };
|
|
138
|
+
}
|
|
139
|
+
function sharedCell(name, create, intact) {
|
|
140
|
+
const bag = cellBag();
|
|
141
|
+
const found = bag[name];
|
|
142
|
+
if (isObject(found) && accepted(intact, found)) return found;
|
|
143
|
+
const fresh = create();
|
|
144
|
+
put(bag, name, fresh);
|
|
145
|
+
return fresh;
|
|
146
|
+
}
|
|
147
|
+
|
|
96
148
|
// core/errors.ts
|
|
97
149
|
var SphereError = class extends Error {
|
|
98
150
|
code;
|
|
@@ -195,7 +247,6 @@ function isNetworkScopedAddressKey(key) {
|
|
|
195
247
|
return false;
|
|
196
248
|
}
|
|
197
249
|
var DEFAULT_NOSTR_RELAYS = [
|
|
198
|
-
"wss://relay.unicity.network",
|
|
199
250
|
"wss://relay.damus.io",
|
|
200
251
|
"wss://nos.lol",
|
|
201
252
|
"wss://relay.nostr.band"
|
|
@@ -208,13 +259,9 @@ var NOSTR_EVENT_KINDS = {
|
|
|
208
259
|
/** Public broadcast */
|
|
209
260
|
BROADCAST: 1
|
|
210
261
|
};
|
|
211
|
-
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
212
|
-
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
213
262
|
var DEFAULT_AGGREGATOR_TIMEOUT = 3e4;
|
|
214
263
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
215
264
|
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
216
|
-
var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
|
|
217
|
-
var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
|
|
218
265
|
var TEST_NOSTR_RELAYS = [
|
|
219
266
|
"wss://nostr-relay.testnet.unicity.network"
|
|
220
267
|
];
|
|
@@ -224,10 +271,19 @@ var DEFAULT_GROUP_RELAYS = [
|
|
|
224
271
|
var NETWORKS = {
|
|
225
272
|
mainnet: {
|
|
226
273
|
name: "Mainnet",
|
|
227
|
-
|
|
228
|
-
|
|
274
|
+
networkId: 1,
|
|
275
|
+
// v3 state-transition gateway (networkId 1 comes from the trust base). apiKey is env-injected;
|
|
276
|
+
// unlike testnet2's, a mainnet gateway key is a SECRET — never commit one.
|
|
277
|
+
aggregatorUrl: "https://gateway.mainnet.unicity.network",
|
|
278
|
+
// Mainnet has no relay of its own yet — it shares the testnet relay until one is stood up.
|
|
279
|
+
// Consequence while shared: nametag bindings for both networks live in ONE namespace, and
|
|
280
|
+
// since bindings carry no network the cross-network recipient guard can only signal (#734).
|
|
281
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
229
282
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
230
|
-
|
|
283
|
+
// Published, but currently only the non-fungible base token type — no fungible
|
|
284
|
+
// coins yet, so those still miss the registry (decimals 0, symbol falls back to
|
|
285
|
+
// six hex chars). Presentation only: the money path treats coinId as opaque bytes.
|
|
286
|
+
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json"
|
|
231
287
|
},
|
|
232
288
|
// v1 cutover: 'testnet' now POINTS AT TESTNET2 (the v2 gateway network). The
|
|
233
289
|
// old goggregator testnet spoke the removed v1 protocol — a v2 engine cannot
|
|
@@ -251,19 +307,10 @@ var NETWORKS = {
|
|
|
251
307
|
// reuse testnet infra (shared relays/ipfs)
|
|
252
308
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
253
309
|
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
|
|
254
|
-
},
|
|
255
|
-
// NOTE: mainnet/dev still point at v1-era aggregators. The v2 engine cannot
|
|
256
|
-
// operate against them until their gateways are cut over to the v2 protocol —
|
|
257
|
-
// wallet operations on these networks fail loudly (AGGREGATOR_ERROR) until then.
|
|
258
|
-
dev: {
|
|
259
|
-
name: "Development",
|
|
260
|
-
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
261
|
-
nostrRelays: TEST_NOSTR_RELAYS,
|
|
262
|
-
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
263
|
-
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
264
310
|
}
|
|
265
311
|
};
|
|
266
312
|
var SPHERE_NETWORKS = {
|
|
313
|
+
mainnet: { id: NETWORKS.mainnet.networkId, name: "mainnet" },
|
|
267
314
|
testnet2: { id: NETWORKS.testnet2.networkId, name: "testnet2" }
|
|
268
315
|
};
|
|
269
316
|
var TIMEOUTS = {
|
|
@@ -279,12 +326,106 @@ var TIMEOUTS = {
|
|
|
279
326
|
SYNC_INTERVAL: 6e4
|
|
280
327
|
};
|
|
281
328
|
|
|
329
|
+
// storage/tracked-addresses.ts
|
|
330
|
+
function isDerivableIndex(value) {
|
|
331
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
|
|
332
|
+
}
|
|
333
|
+
function num(value, fallback) {
|
|
334
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
335
|
+
}
|
|
336
|
+
function toEntry(value) {
|
|
337
|
+
if (typeof value !== "object" || value === null) return null;
|
|
338
|
+
const e = value;
|
|
339
|
+
if (!isDerivableIndex(e.index)) return null;
|
|
340
|
+
return {
|
|
341
|
+
...e,
|
|
342
|
+
index: e.index,
|
|
343
|
+
hidden: e.hidden === true,
|
|
344
|
+
createdAt: num(e.createdAt, 0),
|
|
345
|
+
updatedAt: num(e.updatedAt, 0)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function parseTrackedAddresses(raw) {
|
|
349
|
+
if (!raw) return [];
|
|
350
|
+
let parsed;
|
|
351
|
+
try {
|
|
352
|
+
parsed = JSON.parse(raw);
|
|
353
|
+
} catch {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
if (typeof parsed !== "object" || parsed === null) return [];
|
|
357
|
+
const addresses = parsed.addresses;
|
|
358
|
+
if (!Array.isArray(addresses)) return [];
|
|
359
|
+
const entries = [];
|
|
360
|
+
for (const row of addresses) {
|
|
361
|
+
const entry = toEntry(row);
|
|
362
|
+
if (entry) entries.push(entry);
|
|
363
|
+
}
|
|
364
|
+
return entries;
|
|
365
|
+
}
|
|
366
|
+
function mergeTrackedAddresses(onDisk, incoming) {
|
|
367
|
+
const merged = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const entry of onDisk) {
|
|
369
|
+
if (isDerivableIndex(entry.index)) merged.set(entry.index, entry);
|
|
370
|
+
}
|
|
371
|
+
for (const entry of incoming) {
|
|
372
|
+
if (!isDerivableIndex(entry.index)) {
|
|
373
|
+
throw new SphereError(
|
|
374
|
+
`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.`,
|
|
375
|
+
"VALIDATION_ERROR"
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const existing = merged.get(entry.index);
|
|
379
|
+
if (!existing) {
|
|
380
|
+
merged.set(entry.index, entry);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const winner = entry.updatedAt >= existing.updatedAt ? entry : existing;
|
|
384
|
+
merged.set(entry.index, {
|
|
385
|
+
...existing,
|
|
386
|
+
...winner,
|
|
387
|
+
index: entry.index,
|
|
388
|
+
createdAt: Math.min(existing.createdAt, entry.createdAt)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return Array.from(merged.values()).sort((a, b) => a.index - b.index);
|
|
392
|
+
}
|
|
393
|
+
|
|
282
394
|
// impl/browser/storage/LocalStorageProvider.ts
|
|
395
|
+
var storeIdentity = sharedCell(
|
|
396
|
+
"impl.browser.localStorage.identity@1",
|
|
397
|
+
() => ({ tags: /* @__PURE__ */ new WeakMap(), writeChains: /* @__PURE__ */ new Map(), seq: 0 }),
|
|
398
|
+
(cell) => {
|
|
399
|
+
const c = cell;
|
|
400
|
+
return c.tags instanceof WeakMap && c.writeChains instanceof Map && typeof c.seq === "number";
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
function storageObjectTag(storage) {
|
|
404
|
+
let tag = storeIdentity.tags.get(storage);
|
|
405
|
+
if (!tag) {
|
|
406
|
+
tag = String(++storeIdentity.seq);
|
|
407
|
+
storeIdentity.tags.set(storage, tag);
|
|
408
|
+
}
|
|
409
|
+
return tag;
|
|
410
|
+
}
|
|
411
|
+
var trackedWriteChains = storeIdentity.writeChains;
|
|
412
|
+
function serializeTrackedWrite(storeId, task) {
|
|
413
|
+
const previous = trackedWriteChains.get(storeId) ?? Promise.resolve();
|
|
414
|
+
const run = previous.then(task);
|
|
415
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
416
|
+
trackedWriteChains.set(storeId, tail);
|
|
417
|
+
void tail.then(() => {
|
|
418
|
+
if (trackedWriteChains.get(storeId) === tail) trackedWriteChains.delete(storeId);
|
|
419
|
+
});
|
|
420
|
+
return run;
|
|
421
|
+
}
|
|
283
422
|
var LocalStorageProvider = class {
|
|
284
423
|
id = "localStorage";
|
|
285
424
|
name = "Local Storage";
|
|
286
425
|
type = "local";
|
|
287
426
|
description = "Browser localStorage for single-device persistence";
|
|
427
|
+
/** The `Storage` object + prefix — two providers over one pair share erasure (#766). */
|
|
428
|
+
backingStoreId;
|
|
288
429
|
config;
|
|
289
430
|
network;
|
|
290
431
|
identity = null;
|
|
@@ -297,6 +438,7 @@ var LocalStorageProvider = class {
|
|
|
297
438
|
debug: config?.debug ?? false
|
|
298
439
|
};
|
|
299
440
|
this.network = config?.network;
|
|
441
|
+
this.backingStoreId = `localstorage:${storageObjectTag(storage)}:${encodeURIComponent(this.config.prefix)}`;
|
|
300
442
|
}
|
|
301
443
|
// ===========================================================================
|
|
302
444
|
// BaseProvider Implementation
|
|
@@ -372,18 +514,27 @@ var LocalStorageProvider = class {
|
|
|
372
514
|
await this.remove(key);
|
|
373
515
|
}
|
|
374
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Persist the tracked-address registry by MERGING, never replacing.
|
|
519
|
+
*
|
|
520
|
+
* Every Sphere over this storage holds its own snapshot and writes it in
|
|
521
|
+
* full, so a wholesale write drops the addresses this writer never saw
|
|
522
|
+
* (#766 item 5 — a lost update, reproducible on one network). localStorage
|
|
523
|
+
* offers no transaction, so the read-merge-write is serialized by BACKING
|
|
524
|
+
* STORE — see `serializeTrackedWrite`, and the realm limit stated there.
|
|
525
|
+
*/
|
|
375
526
|
async saveTrackedAddresses(entries) {
|
|
376
|
-
await this.
|
|
527
|
+
await serializeTrackedWrite(this.backingStoreId, async () => {
|
|
528
|
+
const onDisk = parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
|
|
529
|
+
const file = {
|
|
530
|
+
version: 1,
|
|
531
|
+
addresses: mergeTrackedAddresses(onDisk, entries)
|
|
532
|
+
};
|
|
533
|
+
await this.set(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES, JSON.stringify(file));
|
|
534
|
+
});
|
|
377
535
|
}
|
|
378
536
|
async loadTrackedAddresses() {
|
|
379
|
-
|
|
380
|
-
if (!data) return [];
|
|
381
|
-
try {
|
|
382
|
-
const parsed = JSON.parse(data);
|
|
383
|
-
return parsed.addresses ?? [];
|
|
384
|
-
} catch {
|
|
385
|
-
return [];
|
|
386
|
-
}
|
|
537
|
+
return parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
|
|
387
538
|
}
|
|
388
539
|
// ===========================================================================
|
|
389
540
|
// Helpers
|
|
@@ -470,6 +621,8 @@ var IndexedDBStorageProvider = class {
|
|
|
470
621
|
name = "IndexedDB Storage";
|
|
471
622
|
type = "local";
|
|
472
623
|
description = "Browser IndexedDB for large-capacity persistence";
|
|
624
|
+
/** The database + prefix pair — two providers over one pair share erasure (#766). */
|
|
625
|
+
backingStoreId;
|
|
473
626
|
prefix;
|
|
474
627
|
dbName;
|
|
475
628
|
network;
|
|
@@ -484,6 +637,7 @@ var IndexedDBStorageProvider = class {
|
|
|
484
637
|
this.dbName = config?.dbName ?? DB_NAME;
|
|
485
638
|
this.network = config?.network;
|
|
486
639
|
this.debug = config?.debug ?? false;
|
|
640
|
+
this.backingStoreId = `indexeddb:${encodeURIComponent(this.dbName)}:${encodeURIComponent(this.prefix)}`;
|
|
487
641
|
}
|
|
488
642
|
// ===========================================================================
|
|
489
643
|
// BaseProvider Implementation
|
|
@@ -613,18 +767,27 @@ var IndexedDBStorageProvider = class {
|
|
|
613
767
|
await this.remove(key);
|
|
614
768
|
}
|
|
615
769
|
}
|
|
770
|
+
/**
|
|
771
|
+
* Persist the tracked-address registry by MERGING, never replacing.
|
|
772
|
+
*
|
|
773
|
+
* Every Sphere over this storage holds its own snapshot and writes it in
|
|
774
|
+
* full, so a wholesale write drops the addresses this writer never saw
|
|
775
|
+
* (#766 item 5 — a lost update, reproducible on one network). The read, the
|
|
776
|
+
* merge and the write share ONE transaction: a lock on this object would
|
|
777
|
+
* order only this object's calls, and `backingStoreId` exists precisely
|
|
778
|
+
* because two provider objects — or two TABS — can address one database.
|
|
779
|
+
* IndexedDB serializes overlapping readwrite transactions across every
|
|
780
|
+
* connection, so both are covered.
|
|
781
|
+
*/
|
|
616
782
|
async saveTrackedAddresses(entries) {
|
|
617
|
-
|
|
783
|
+
this.ensureConnected();
|
|
784
|
+
await this.idbMergeTrackedAddresses(
|
|
785
|
+
this.getFullKey(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES),
|
|
786
|
+
entries
|
|
787
|
+
);
|
|
618
788
|
}
|
|
619
789
|
async loadTrackedAddresses() {
|
|
620
|
-
|
|
621
|
-
if (!data) return [];
|
|
622
|
-
try {
|
|
623
|
-
const parsed = JSON.parse(data);
|
|
624
|
-
return parsed.addresses ?? [];
|
|
625
|
-
} catch {
|
|
626
|
-
return [];
|
|
627
|
-
}
|
|
790
|
+
return parseTrackedAddresses(await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES));
|
|
628
791
|
}
|
|
629
792
|
// ===========================================================================
|
|
630
793
|
// Helpers
|
|
@@ -741,6 +904,25 @@ var IndexedDBStorageProvider = class {
|
|
|
741
904
|
request.onsuccess = () => resolve(request.result ?? []);
|
|
742
905
|
});
|
|
743
906
|
}
|
|
907
|
+
idbMergeTrackedAddresses(key, entries) {
|
|
908
|
+
return new Promise((resolve, reject) => {
|
|
909
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
910
|
+
const store = tx.objectStore(STORE_NAME);
|
|
911
|
+
const read = store.get(key);
|
|
912
|
+
read.onerror = () => reject(read.error);
|
|
913
|
+
read.onsuccess = () => {
|
|
914
|
+
const onDisk = parseTrackedAddresses(read.result?.v ?? null);
|
|
915
|
+
const file = {
|
|
916
|
+
version: 1,
|
|
917
|
+
addresses: mergeTrackedAddresses(onDisk, entries)
|
|
918
|
+
};
|
|
919
|
+
store.put({ k: key, v: JSON.stringify(file) });
|
|
920
|
+
};
|
|
921
|
+
tx.oncomplete = () => resolve();
|
|
922
|
+
tx.onerror = () => reject(tx.error);
|
|
923
|
+
tx.onabort = () => reject(tx.error ?? new Error("tracked-address write aborted"));
|
|
924
|
+
});
|
|
925
|
+
}
|
|
744
926
|
idbClear() {
|
|
745
927
|
return new Promise((resolve, reject) => {
|
|
746
928
|
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
@@ -1331,14 +1513,10 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1331
1513
|
});
|
|
1332
1514
|
}
|
|
1333
1515
|
});
|
|
1334
|
-
await
|
|
1335
|
-
this.nostrClient
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1339
|
-
)), this.config.timeout)
|
|
1340
|
-
)
|
|
1341
|
-
]);
|
|
1516
|
+
await this.connectWithDeadline(
|
|
1517
|
+
this.nostrClient,
|
|
1518
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1519
|
+
);
|
|
1342
1520
|
if (!this.nostrClient.isConnected()) {
|
|
1343
1521
|
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
1344
1522
|
}
|
|
@@ -1353,6 +1531,27 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1353
1531
|
throw error;
|
|
1354
1532
|
}
|
|
1355
1533
|
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Race one client's relay connect against the configured timeout.
|
|
1536
|
+
*
|
|
1537
|
+
* The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
|
|
1538
|
+
* cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
|
|
1539
|
+
* for the whole `config.timeout` after the call has already returned (and
|
|
1540
|
+
* then rejects a promise nobody is listening to any more).
|
|
1541
|
+
*/
|
|
1542
|
+
async connectWithDeadline(client, timeoutMessage) {
|
|
1543
|
+
let timer;
|
|
1544
|
+
try {
|
|
1545
|
+
await Promise.race([
|
|
1546
|
+
client.connect(...this.config.relays),
|
|
1547
|
+
new Promise((_, reject) => {
|
|
1548
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), this.config.timeout);
|
|
1549
|
+
})
|
|
1550
|
+
]);
|
|
1551
|
+
} finally {
|
|
1552
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1356
1555
|
async disconnect() {
|
|
1357
1556
|
if (this.nostrClient) {
|
|
1358
1557
|
this.nostrClient.disconnect();
|
|
@@ -1471,19 +1670,22 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1471
1670
|
// TransportProvider Implementation
|
|
1472
1671
|
// ===========================================================================
|
|
1473
1672
|
async setIdentity(identity) {
|
|
1474
|
-
this.identity = identity;
|
|
1475
|
-
this.processedEventIds.clear();
|
|
1476
|
-
this.lastEventTs = 0;
|
|
1477
|
-
this.lastDmEventTs = 0;
|
|
1478
|
-
this.fallbackDmSince = null;
|
|
1479
1673
|
const secretKey = Buffer.from(identity.privateKey, "hex");
|
|
1480
|
-
|
|
1481
|
-
const nostrPubkey =
|
|
1482
|
-
logger.debug("Nostr", "Identity
|
|
1674
|
+
const nextKeyManager = NostrKeyManager.fromPrivateKey(secretKey);
|
|
1675
|
+
const nostrPubkey = nextKeyManager.getPublicKeyHex();
|
|
1676
|
+
logger.debug("Nostr", "Identity staged, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
|
|
1677
|
+
const applyStagedIdentity = () => {
|
|
1678
|
+
this.identity = identity;
|
|
1679
|
+
this.keyManager = nextKeyManager;
|
|
1680
|
+
this.processedEventIds.clear();
|
|
1681
|
+
this.lastEventTs = 0;
|
|
1682
|
+
this.lastDmEventTs = 0;
|
|
1683
|
+
this.fallbackDmSince = null;
|
|
1684
|
+
};
|
|
1483
1685
|
if (this.nostrClient && this.status === "connected") {
|
|
1484
1686
|
logger.debug("Nostr", "Identity changed while connected - recreating NostrClient");
|
|
1485
1687
|
const oldClient = this.nostrClient;
|
|
1486
|
-
|
|
1688
|
+
const nextClient = new NostrClient(nextKeyManager, {
|
|
1487
1689
|
autoReconnect: this.config.autoReconnect,
|
|
1488
1690
|
reconnectIntervalMs: this.config.reconnectDelay,
|
|
1489
1691
|
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
@@ -1492,7 +1694,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1492
1694
|
queryTimeoutMs: 2e4
|
|
1493
1695
|
// see same option above for rationale
|
|
1494
1696
|
});
|
|
1495
|
-
|
|
1697
|
+
nextClient.addConnectionListener({
|
|
1496
1698
|
onConnect: (url) => {
|
|
1497
1699
|
logger.debug("Nostr", "NostrClient connected to relay:", url);
|
|
1498
1700
|
},
|
|
@@ -1506,18 +1708,43 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1506
1708
|
logger.debug("Nostr", "NostrClient reconnected to relay:", url);
|
|
1507
1709
|
}
|
|
1508
1710
|
});
|
|
1509
|
-
|
|
1510
|
-
this.
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1711
|
+
try {
|
|
1712
|
+
await this.connectWithDeadline(
|
|
1713
|
+
nextClient,
|
|
1714
|
+
`Transport reconnection timed out after ${this.config.timeout}ms`
|
|
1715
|
+
);
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
try {
|
|
1718
|
+
nextClient.disconnect();
|
|
1719
|
+
} catch {
|
|
1720
|
+
}
|
|
1721
|
+
throw error;
|
|
1722
|
+
}
|
|
1723
|
+
if (this.nostrClient !== oldClient || this.status !== "connected") {
|
|
1724
|
+
try {
|
|
1725
|
+
nextClient.disconnect();
|
|
1726
|
+
} catch {
|
|
1727
|
+
}
|
|
1728
|
+
throw new SphereError(
|
|
1729
|
+
"Transport client changed or left the connected state during setIdentity - identity not applied",
|
|
1730
|
+
"TRANSPORT_ERROR"
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
this.nostrClient = nextClient;
|
|
1734
|
+
applyStagedIdentity();
|
|
1735
|
+
try {
|
|
1736
|
+
await this.subscribeToEvents();
|
|
1737
|
+
} finally {
|
|
1738
|
+
try {
|
|
1739
|
+
oldClient.disconnect();
|
|
1740
|
+
} catch {
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1519
1743
|
} else if (this.isConnected()) {
|
|
1744
|
+
applyStagedIdentity();
|
|
1520
1745
|
await this.subscribeToEvents();
|
|
1746
|
+
} else {
|
|
1747
|
+
applyStagedIdentity();
|
|
1521
1748
|
}
|
|
1522
1749
|
}
|
|
1523
1750
|
setFallbackSince(sinceSeconds) {
|
|
@@ -2812,26 +3039,6 @@ var UnicityAggregatorProvider = class {
|
|
|
2812
3039
|
var UnicityOracleProvider = UnicityAggregatorProvider;
|
|
2813
3040
|
|
|
2814
3041
|
// assets/trustbase.ts
|
|
2815
|
-
var TRUSTBASE_TESTNET = {
|
|
2816
|
-
version: 1,
|
|
2817
|
-
networkId: 3,
|
|
2818
|
-
epoch: 1,
|
|
2819
|
-
epochStartRound: 1,
|
|
2820
|
-
rootNodes: [
|
|
2821
|
-
{
|
|
2822
|
-
nodeId: "16Uiu2HAkyQRiA7pMgzgLj9GgaBJEJa8zmx9dzqUDa6WxQPJ82ghU",
|
|
2823
|
-
sigKey: "0x039afb2acb65f5fbc272d8907f763d0a5d189aadc9b97afdcc5897ea4dd112e68b",
|
|
2824
|
-
stake: 1
|
|
2825
|
-
}
|
|
2826
|
-
],
|
|
2827
|
-
quorumThreshold: 1,
|
|
2828
|
-
stateHash: "",
|
|
2829
|
-
changeRecordHash: "",
|
|
2830
|
-
previousEntryHash: "",
|
|
2831
|
-
signatures: {
|
|
2832
|
-
"16Uiu2HAkyQRiA7pMgzgLj9GgaBJEJa8zmx9dzqUDa6WxQPJ82ghU": "0xf157c9fdd8a378e3ca70d354ccc4475ab2cd8de360127bc46b0aeab4b453a80f07fd9136a5843b60a8babaff23e20acc8879861f7651440a5e2829f7541b31f100"
|
|
2833
|
-
}
|
|
2834
|
-
};
|
|
2835
3042
|
var TRUSTBASE_TESTNET2 = {
|
|
2836
3043
|
version: 1,
|
|
2837
3044
|
networkId: 4,
|
|
@@ -2870,8 +3077,44 @@ var TRUSTBASE_TESTNET2 = {
|
|
|
2870
3077
|
"16Uiu2HAmU5T1TqKyKkUB3sitWb431hVFfp9nqvaLgVagQF6ywLFF": "0x9f28f3e3c50d4bfdaf54d7290fbbe311fffb3be2281a1d211be56818fa43d06a2e9a81925e9d365e552c8b772c99285a686de1d9fb581920be7524c7a5d467a301"
|
|
2871
3078
|
}
|
|
2872
3079
|
};
|
|
2873
|
-
var TRUSTBASE_MAINNET =
|
|
2874
|
-
|
|
3080
|
+
var TRUSTBASE_MAINNET = {
|
|
3081
|
+
version: 1,
|
|
3082
|
+
networkId: 1,
|
|
3083
|
+
epoch: 1,
|
|
3084
|
+
epochStartRound: 1,
|
|
3085
|
+
rootNodes: [
|
|
3086
|
+
{
|
|
3087
|
+
nodeId: "16Uiu2HAkvk4EpXoyh7SVLTX3TcsZkjbni5jY1sBgJ7qCEWfx1Gma",
|
|
3088
|
+
sigKey: "0x03059e30b5c980ce92e2c6d4e14775838be91a6f0e993f3a4899ab459c4b5225f2",
|
|
3089
|
+
stake: 1
|
|
3090
|
+
},
|
|
3091
|
+
{
|
|
3092
|
+
nodeId: "16Uiu2HAmF9Mq9pH9yUTdao3TpJeFZY9oX9gyMqfxggVA4fNmzmXr",
|
|
3093
|
+
sigKey: "0x03a514db8b17b1c79b4a6affeb5d88a129c4d5ff0f92c8f86c311971b0beb94335",
|
|
3094
|
+
stake: 1
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
nodeId: "16Uiu2HAmLAHTSMvZ2apRRtBN9piYVhaK2i7sTMaQeSWSxsLh75TG",
|
|
3098
|
+
sigKey: "0x02e5669b2daa5d1afccfe66603f7d3fe171ecbd7ed891487ab3683cdebc3c3cd8b",
|
|
3099
|
+
stake: 1
|
|
3100
|
+
},
|
|
3101
|
+
{
|
|
3102
|
+
nodeId: "16Uiu2HAmNNaWfK4RTTAq7Ap8w2yZUryrEejqmmGf44ogMaYEnkJ4",
|
|
3103
|
+
sigKey: "0x03ffee964da8ca276d4fc1cf391929e6759db106ef1d0aea998154b51e5a7fb1b7",
|
|
3104
|
+
stake: 1
|
|
3105
|
+
}
|
|
3106
|
+
],
|
|
3107
|
+
quorumThreshold: 3,
|
|
3108
|
+
stateHash: "",
|
|
3109
|
+
changeRecordHash: "",
|
|
3110
|
+
previousEntryHash: "",
|
|
3111
|
+
signatures: {
|
|
3112
|
+
"16Uiu2HAkvk4EpXoyh7SVLTX3TcsZkjbni5jY1sBgJ7qCEWfx1Gma": "0xc2f1d67dbb47fe4dcf502301d8cd5021b8903c0d3385781653d09c703d566a3a3a4e59c75792b36f929e32564ddc14c40ef728c0bde5859774d08d802de2318c00",
|
|
3113
|
+
"16Uiu2HAmF9Mq9pH9yUTdao3TpJeFZY9oX9gyMqfxggVA4fNmzmXr": "0x3e3ad3a7540daa0eded457e31d2ebcacfb354036798f49f63433d26c44857e76157e4abd36cbb1d39de8cdf6e1d33f88b250077d4347c3e5bf0f35a64f2d815300",
|
|
3114
|
+
"16Uiu2HAmLAHTSMvZ2apRRtBN9piYVhaK2i7sTMaQeSWSxsLh75TG": "0x6101bd9df8ab87439d90b52cd74727bc8bf31dd74173c8f731ec3ea501d3d86556ba7c0e84643337959f3be0e448637c301e8e0b28196e780d4f2ff930d9062100",
|
|
3115
|
+
"16Uiu2HAmNNaWfK4RTTAq7Ap8w2yZUryrEejqmmGf44ogMaYEnkJ4": "0xda13c777b832425e5cbd8d6e49eb08ddbde35c1e4a9bee8bd495c3883f039e434b64560865c3bc52b68d65780932b1f2bf8971f245d2244face822af5984945c00"
|
|
3116
|
+
}
|
|
3117
|
+
};
|
|
2875
3118
|
|
|
2876
3119
|
// impl/shared/trustbase-loader.ts
|
|
2877
3120
|
function getEmbeddedTrustBase(network) {
|
|
@@ -2884,8 +3127,6 @@ function getEmbeddedTrustBase(network) {
|
|
|
2884
3127
|
case "testnet":
|
|
2885
3128
|
case "testnet2":
|
|
2886
3129
|
return TRUSTBASE_TESTNET2;
|
|
2887
|
-
case "dev":
|
|
2888
|
-
return TRUSTBASE_DEV;
|
|
2889
3130
|
default:
|
|
2890
3131
|
throw new SphereError(`getEmbeddedTrustBase: unknown network "${network}"`, "INVALID_CONFIG");
|
|
2891
3132
|
}
|
|
@@ -3042,6 +3283,7 @@ async function readFileAsUint8Array(file) {
|
|
|
3042
3283
|
|
|
3043
3284
|
// impl/shared/network.ts
|
|
3044
3285
|
var EXPECTED_NETWORK_ID = {
|
|
3286
|
+
mainnet: 1,
|
|
3045
3287
|
// v1 cutover: 'testnet' is an alias of testnet2 (the v2 gateway network),
|
|
3046
3288
|
// so both expect the testnet2 trust base (networkId 4).
|
|
3047
3289
|
testnet: 4,
|
|
@@ -3062,7 +3304,7 @@ function assertNetworkConsistency(network) {
|
|
|
3062
3304
|
);
|
|
3063
3305
|
}
|
|
3064
3306
|
const expected = EXPECTED_NETWORK_ID[network];
|
|
3065
|
-
if (
|
|
3307
|
+
if (networkId !== expected) {
|
|
3066
3308
|
throw new SphereError(
|
|
3067
3309
|
`Network "${network}" trust base networkId ${networkId} does not match expected ${expected} (wrong trust base wired to this network).`,
|
|
3068
3310
|
"INVALID_CONFIG"
|
|
@@ -3327,419 +3569,6 @@ function createPriceProvider(config) {
|
|
|
3327
3569
|
}
|
|
3328
3570
|
}
|
|
3329
3571
|
|
|
3330
|
-
// registry/TokenRegistry.ts
|
|
3331
|
-
var FETCH_TIMEOUT_MS = 1e4;
|
|
3332
|
-
var TokenRegistry = class _TokenRegistry {
|
|
3333
|
-
static instance = null;
|
|
3334
|
-
definitionsById;
|
|
3335
|
-
definitionsBySymbol;
|
|
3336
|
-
definitionsByName;
|
|
3337
|
-
// Remote refresh state
|
|
3338
|
-
remoteUrl = null;
|
|
3339
|
-
storage = null;
|
|
3340
|
-
refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
|
|
3341
|
-
refreshTimer = null;
|
|
3342
|
-
lastRefreshAt = 0;
|
|
3343
|
-
refreshPromise = null;
|
|
3344
|
-
initialLoadPromise = null;
|
|
3345
|
-
constructor() {
|
|
3346
|
-
this.definitionsById = /* @__PURE__ */ new Map();
|
|
3347
|
-
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
3348
|
-
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
3349
|
-
}
|
|
3350
|
-
/**
|
|
3351
|
-
* Get singleton instance of TokenRegistry
|
|
3352
|
-
*/
|
|
3353
|
-
static getInstance() {
|
|
3354
|
-
if (!_TokenRegistry.instance) {
|
|
3355
|
-
_TokenRegistry.instance = new _TokenRegistry();
|
|
3356
|
-
}
|
|
3357
|
-
return _TokenRegistry.instance;
|
|
3358
|
-
}
|
|
3359
|
-
/**
|
|
3360
|
-
* Configure remote registry refresh with persistent caching.
|
|
3361
|
-
*
|
|
3362
|
-
* On first call:
|
|
3363
|
-
* 1. Loads cached data from StorageProvider (if available and fresh)
|
|
3364
|
-
* 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
|
|
3365
|
-
*
|
|
3366
|
-
* @param options - Configuration options
|
|
3367
|
-
* @param options.remoteUrl - Remote URL to fetch definitions from
|
|
3368
|
-
* @param options.storage - StorageProvider for persistent caching
|
|
3369
|
-
* @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
|
|
3370
|
-
* @param options.autoRefresh - Start auto-refresh immediately (default: true)
|
|
3371
|
-
*/
|
|
3372
|
-
static configure(options) {
|
|
3373
|
-
const instance = _TokenRegistry.getInstance();
|
|
3374
|
-
if (options.remoteUrl !== void 0) {
|
|
3375
|
-
instance.remoteUrl = options.remoteUrl;
|
|
3376
|
-
}
|
|
3377
|
-
if (options.storage !== void 0) {
|
|
3378
|
-
instance.storage = options.storage;
|
|
3379
|
-
}
|
|
3380
|
-
if (options.refreshIntervalMs !== void 0) {
|
|
3381
|
-
instance.refreshIntervalMs = options.refreshIntervalMs;
|
|
3382
|
-
}
|
|
3383
|
-
const autoRefresh = options.autoRefresh ?? true;
|
|
3384
|
-
instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
|
|
3385
|
-
}
|
|
3386
|
-
/**
|
|
3387
|
-
* Reset the singleton instance (useful for testing).
|
|
3388
|
-
* Stops auto-refresh if running.
|
|
3389
|
-
*/
|
|
3390
|
-
static resetInstance() {
|
|
3391
|
-
if (_TokenRegistry.instance) {
|
|
3392
|
-
_TokenRegistry.instance.stopAutoRefresh();
|
|
3393
|
-
}
|
|
3394
|
-
_TokenRegistry.instance = null;
|
|
3395
|
-
}
|
|
3396
|
-
/**
|
|
3397
|
-
* Destroy the singleton: stop auto-refresh and reset.
|
|
3398
|
-
*/
|
|
3399
|
-
static destroy() {
|
|
3400
|
-
_TokenRegistry.resetInstance();
|
|
3401
|
-
}
|
|
3402
|
-
/**
|
|
3403
|
-
* Wait for the initial data load (cache or remote) to complete.
|
|
3404
|
-
* Returns true if data was loaded, false if not (timeout or no data source).
|
|
3405
|
-
*
|
|
3406
|
-
* @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
|
|
3407
|
-
*/
|
|
3408
|
-
static async waitForReady(timeoutMs = 1e4) {
|
|
3409
|
-
const instance = _TokenRegistry.getInstance();
|
|
3410
|
-
if (!instance.initialLoadPromise) {
|
|
3411
|
-
return instance.definitionsById.size > 0;
|
|
3412
|
-
}
|
|
3413
|
-
if (timeoutMs <= 0) {
|
|
3414
|
-
return instance.initialLoadPromise;
|
|
3415
|
-
}
|
|
3416
|
-
return Promise.race([
|
|
3417
|
-
instance.initialLoadPromise,
|
|
3418
|
-
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
|
|
3419
|
-
]);
|
|
3420
|
-
}
|
|
3421
|
-
// ===========================================================================
|
|
3422
|
-
// Initial Load
|
|
3423
|
-
// ===========================================================================
|
|
3424
|
-
/**
|
|
3425
|
-
* Perform initial data load: try cache first, fall back to remote fetch.
|
|
3426
|
-
* After initial data is available, start periodic auto-refresh if configured.
|
|
3427
|
-
*/
|
|
3428
|
-
async performInitialLoad(autoRefresh) {
|
|
3429
|
-
let loaded = false;
|
|
3430
|
-
if (this.storage) {
|
|
3431
|
-
loaded = await this.loadFromCache();
|
|
3432
|
-
}
|
|
3433
|
-
if (loaded) {
|
|
3434
|
-
if (autoRefresh && this.remoteUrl) {
|
|
3435
|
-
this.startAutoRefresh();
|
|
3436
|
-
}
|
|
3437
|
-
return true;
|
|
3438
|
-
}
|
|
3439
|
-
if (autoRefresh && this.remoteUrl) {
|
|
3440
|
-
loaded = await this.refreshFromRemote();
|
|
3441
|
-
this.stopAutoRefresh();
|
|
3442
|
-
this.refreshTimer = setInterval(() => {
|
|
3443
|
-
this.refreshFromRemote();
|
|
3444
|
-
}, this.refreshIntervalMs);
|
|
3445
|
-
return loaded;
|
|
3446
|
-
}
|
|
3447
|
-
return false;
|
|
3448
|
-
}
|
|
3449
|
-
// ===========================================================================
|
|
3450
|
-
// Cache (StorageProvider)
|
|
3451
|
-
// ===========================================================================
|
|
3452
|
-
/**
|
|
3453
|
-
* Per-network cache keys. The registry cache MUST be namespaced by remoteUrl:
|
|
3454
|
-
* testnet and testnet2 ship DIFFERENT coinIds for the same symbol, so a single
|
|
3455
|
-
* global key let a stale testnet snapshot be served for a testnet2 request —
|
|
3456
|
-
* wrong/missing icons AND self-mint resolving the wrong-network coinId. Keying
|
|
3457
|
-
* by remoteUrl isolates the snapshots so a network can never read another's.
|
|
3458
|
-
*/
|
|
3459
|
-
cacheKeys() {
|
|
3460
|
-
const base = STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE;
|
|
3461
|
-
const baseTs = STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS;
|
|
3462
|
-
if (!this.remoteUrl) return { data: base, ts: baseTs };
|
|
3463
|
-
return { data: `${base}:${this.remoteUrl}`, ts: `${baseTs}:${this.remoteUrl}` };
|
|
3464
|
-
}
|
|
3465
|
-
/**
|
|
3466
|
-
* Load definitions from StorageProvider cache.
|
|
3467
|
-
* Only applies if cache exists and is fresh (within refreshIntervalMs).
|
|
3468
|
-
*/
|
|
3469
|
-
async loadFromCache() {
|
|
3470
|
-
if (!this.storage) return false;
|
|
3471
|
-
try {
|
|
3472
|
-
const { data: dataKey, ts: tsKey } = this.cacheKeys();
|
|
3473
|
-
const [cached, cachedTs] = await Promise.all([
|
|
3474
|
-
this.storage.get(dataKey),
|
|
3475
|
-
this.storage.get(tsKey)
|
|
3476
|
-
]);
|
|
3477
|
-
if (!cached || !cachedTs) return false;
|
|
3478
|
-
const ts = parseInt(cachedTs, 10);
|
|
3479
|
-
if (isNaN(ts)) return false;
|
|
3480
|
-
const age = Date.now() - ts;
|
|
3481
|
-
if (age > this.refreshIntervalMs) return false;
|
|
3482
|
-
if (this.lastRefreshAt > ts) return false;
|
|
3483
|
-
const data = JSON.parse(cached);
|
|
3484
|
-
if (!this.isValidDefinitionsArray(data)) return false;
|
|
3485
|
-
this.applyDefinitions(data);
|
|
3486
|
-
this.lastRefreshAt = ts;
|
|
3487
|
-
return true;
|
|
3488
|
-
} catch {
|
|
3489
|
-
return false;
|
|
3490
|
-
}
|
|
3491
|
-
}
|
|
3492
|
-
/**
|
|
3493
|
-
* Save definitions to StorageProvider cache.
|
|
3494
|
-
*/
|
|
3495
|
-
async saveToCache(definitions) {
|
|
3496
|
-
if (!this.storage) return;
|
|
3497
|
-
try {
|
|
3498
|
-
const { data: dataKey, ts: tsKey } = this.cacheKeys();
|
|
3499
|
-
await Promise.all([
|
|
3500
|
-
this.storage.set(dataKey, JSON.stringify(definitions)),
|
|
3501
|
-
this.storage.set(tsKey, String(Date.now()))
|
|
3502
|
-
]);
|
|
3503
|
-
} catch {
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
// ===========================================================================
|
|
3507
|
-
// Remote Refresh
|
|
3508
|
-
// ===========================================================================
|
|
3509
|
-
/**
|
|
3510
|
-
* Apply an array of token definitions to the internal maps.
|
|
3511
|
-
* Clears existing data before applying.
|
|
3512
|
-
*/
|
|
3513
|
-
applyDefinitions(definitions) {
|
|
3514
|
-
this.definitionsById.clear();
|
|
3515
|
-
this.definitionsBySymbol.clear();
|
|
3516
|
-
this.definitionsByName.clear();
|
|
3517
|
-
for (const def of definitions) {
|
|
3518
|
-
const idLower = def.id.toLowerCase();
|
|
3519
|
-
this.definitionsById.set(idLower, def);
|
|
3520
|
-
if (def.symbol) {
|
|
3521
|
-
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
3522
|
-
}
|
|
3523
|
-
this.definitionsByName.set(def.name.toLowerCase(), def);
|
|
3524
|
-
}
|
|
3525
|
-
}
|
|
3526
|
-
/**
|
|
3527
|
-
* Validate that data is an array of objects with 'id' field
|
|
3528
|
-
*/
|
|
3529
|
-
isValidDefinitionsArray(data) {
|
|
3530
|
-
return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
|
|
3531
|
-
}
|
|
3532
|
-
/**
|
|
3533
|
-
* Fetch token definitions from the remote URL and update the registry.
|
|
3534
|
-
* On success, also persists to StorageProvider cache.
|
|
3535
|
-
* Returns true on success, false on failure. On failure, existing data is preserved.
|
|
3536
|
-
* Concurrent calls are deduplicated — only one fetch runs at a time.
|
|
3537
|
-
*/
|
|
3538
|
-
async refreshFromRemote() {
|
|
3539
|
-
if (!this.remoteUrl) {
|
|
3540
|
-
return false;
|
|
3541
|
-
}
|
|
3542
|
-
if (this.refreshPromise) {
|
|
3543
|
-
return this.refreshPromise;
|
|
3544
|
-
}
|
|
3545
|
-
this.refreshPromise = this.doRefresh();
|
|
3546
|
-
try {
|
|
3547
|
-
return await this.refreshPromise;
|
|
3548
|
-
} finally {
|
|
3549
|
-
this.refreshPromise = null;
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
async doRefresh() {
|
|
3553
|
-
try {
|
|
3554
|
-
const controller = new AbortController();
|
|
3555
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
3556
|
-
let response;
|
|
3557
|
-
try {
|
|
3558
|
-
response = await fetch(this.remoteUrl, {
|
|
3559
|
-
headers: { Accept: "application/json" },
|
|
3560
|
-
signal: controller.signal
|
|
3561
|
-
});
|
|
3562
|
-
} finally {
|
|
3563
|
-
clearTimeout(timer);
|
|
3564
|
-
}
|
|
3565
|
-
if (!response.ok) {
|
|
3566
|
-
logger.warn("TokenRegistry", `Remote fetch failed: HTTP ${response.status} ${response.statusText}`);
|
|
3567
|
-
return false;
|
|
3568
|
-
}
|
|
3569
|
-
const data = await response.json();
|
|
3570
|
-
if (!this.isValidDefinitionsArray(data)) {
|
|
3571
|
-
logger.warn("TokenRegistry", "Remote data is not a valid token definitions array");
|
|
3572
|
-
return false;
|
|
3573
|
-
}
|
|
3574
|
-
const definitions = data;
|
|
3575
|
-
this.applyDefinitions(definitions);
|
|
3576
|
-
this.lastRefreshAt = Date.now();
|
|
3577
|
-
this.saveToCache(definitions);
|
|
3578
|
-
return true;
|
|
3579
|
-
} catch (error) {
|
|
3580
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3581
|
-
logger.warn("TokenRegistry", `Remote refresh failed: ${message}`);
|
|
3582
|
-
return false;
|
|
3583
|
-
}
|
|
3584
|
-
}
|
|
3585
|
-
/**
|
|
3586
|
-
* Start periodic auto-refresh from the remote URL.
|
|
3587
|
-
* Does an immediate fetch, then repeats at the configured interval.
|
|
3588
|
-
*/
|
|
3589
|
-
startAutoRefresh(intervalMs) {
|
|
3590
|
-
this.stopAutoRefresh();
|
|
3591
|
-
if (intervalMs !== void 0) {
|
|
3592
|
-
this.refreshIntervalMs = intervalMs;
|
|
3593
|
-
}
|
|
3594
|
-
this.refreshFromRemote();
|
|
3595
|
-
this.refreshTimer = setInterval(() => {
|
|
3596
|
-
this.refreshFromRemote();
|
|
3597
|
-
}, this.refreshIntervalMs);
|
|
3598
|
-
}
|
|
3599
|
-
/**
|
|
3600
|
-
* Stop periodic auto-refresh
|
|
3601
|
-
*/
|
|
3602
|
-
stopAutoRefresh() {
|
|
3603
|
-
if (this.refreshTimer !== null) {
|
|
3604
|
-
clearInterval(this.refreshTimer);
|
|
3605
|
-
this.refreshTimer = null;
|
|
3606
|
-
}
|
|
3607
|
-
}
|
|
3608
|
-
/**
|
|
3609
|
-
* Timestamp of the last successful remote refresh (0 if never refreshed)
|
|
3610
|
-
*/
|
|
3611
|
-
getLastRefreshAt() {
|
|
3612
|
-
return this.lastRefreshAt;
|
|
3613
|
-
}
|
|
3614
|
-
// ===========================================================================
|
|
3615
|
-
// Lookup Methods
|
|
3616
|
-
// ===========================================================================
|
|
3617
|
-
/**
|
|
3618
|
-
* Get token definition by hex coin ID
|
|
3619
|
-
* @param coinId - 64-character hex string
|
|
3620
|
-
* @returns Token definition or undefined if not found
|
|
3621
|
-
*/
|
|
3622
|
-
getDefinition(coinId) {
|
|
3623
|
-
if (!coinId) return void 0;
|
|
3624
|
-
return this.definitionsById.get(coinId.toLowerCase());
|
|
3625
|
-
}
|
|
3626
|
-
/**
|
|
3627
|
-
* Get token definition by symbol (e.g., "UCT", "BTC")
|
|
3628
|
-
* @param symbol - Token symbol (case-insensitive)
|
|
3629
|
-
* @returns Token definition or undefined if not found
|
|
3630
|
-
*/
|
|
3631
|
-
getDefinitionBySymbol(symbol) {
|
|
3632
|
-
if (!symbol) return void 0;
|
|
3633
|
-
return this.definitionsBySymbol.get(symbol.toUpperCase());
|
|
3634
|
-
}
|
|
3635
|
-
/**
|
|
3636
|
-
* Get token definition by name (e.g., "bitcoin", "ethereum")
|
|
3637
|
-
* @param name - Token name (case-insensitive)
|
|
3638
|
-
* @returns Token definition or undefined if not found
|
|
3639
|
-
*/
|
|
3640
|
-
getDefinitionByName(name) {
|
|
3641
|
-
if (!name) return void 0;
|
|
3642
|
-
return this.definitionsByName.get(name.toLowerCase());
|
|
3643
|
-
}
|
|
3644
|
-
/**
|
|
3645
|
-
* Get token symbol for a coin ID
|
|
3646
|
-
* @param coinId - 64-character hex string
|
|
3647
|
-
* @returns Symbol (e.g., "UCT") or truncated ID if not found
|
|
3648
|
-
*/
|
|
3649
|
-
getSymbol(coinId) {
|
|
3650
|
-
const def = this.getDefinition(coinId);
|
|
3651
|
-
if (def?.symbol) {
|
|
3652
|
-
return def.symbol;
|
|
3653
|
-
}
|
|
3654
|
-
return coinId.slice(0, 6).toUpperCase();
|
|
3655
|
-
}
|
|
3656
|
-
/**
|
|
3657
|
-
* Get token name for a coin ID
|
|
3658
|
-
* @param coinId - 64-character hex string
|
|
3659
|
-
* @returns Name (e.g., "Bitcoin") or coin ID if not found
|
|
3660
|
-
*/
|
|
3661
|
-
getName(coinId) {
|
|
3662
|
-
const def = this.getDefinition(coinId);
|
|
3663
|
-
if (def?.name) {
|
|
3664
|
-
return def.name.charAt(0).toUpperCase() + def.name.slice(1);
|
|
3665
|
-
}
|
|
3666
|
-
return coinId;
|
|
3667
|
-
}
|
|
3668
|
-
/**
|
|
3669
|
-
* Get decimal places for a coin ID
|
|
3670
|
-
* @param coinId - 64-character hex string
|
|
3671
|
-
* @returns Decimals or 0 if not found
|
|
3672
|
-
*/
|
|
3673
|
-
getDecimals(coinId) {
|
|
3674
|
-
const def = this.getDefinition(coinId);
|
|
3675
|
-
return def?.decimals ?? 0;
|
|
3676
|
-
}
|
|
3677
|
-
/**
|
|
3678
|
-
* Get icon URL for a coin ID
|
|
3679
|
-
* @param coinId - 64-character hex string
|
|
3680
|
-
* @param preferPng - Prefer PNG format over SVG
|
|
3681
|
-
* @returns Icon URL or null if not found
|
|
3682
|
-
*/
|
|
3683
|
-
getIconUrl(coinId, preferPng = true) {
|
|
3684
|
-
const def = this.getDefinition(coinId);
|
|
3685
|
-
if (!def?.icons || def.icons.length === 0) {
|
|
3686
|
-
return null;
|
|
3687
|
-
}
|
|
3688
|
-
if (preferPng) {
|
|
3689
|
-
const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
|
|
3690
|
-
if (pngIcon) return pngIcon.url;
|
|
3691
|
-
}
|
|
3692
|
-
return def.icons[0].url;
|
|
3693
|
-
}
|
|
3694
|
-
/**
|
|
3695
|
-
* Check if a coin ID is known in the registry
|
|
3696
|
-
* @param coinId - 64-character hex string
|
|
3697
|
-
* @returns true if the coin is in the registry
|
|
3698
|
-
*/
|
|
3699
|
-
isKnown(coinId) {
|
|
3700
|
-
return this.definitionsById.has(coinId.toLowerCase());
|
|
3701
|
-
}
|
|
3702
|
-
/**
|
|
3703
|
-
* Get all token definitions
|
|
3704
|
-
* @returns Array of all token definitions
|
|
3705
|
-
*/
|
|
3706
|
-
getAllDefinitions() {
|
|
3707
|
-
return Array.from(this.definitionsById.values());
|
|
3708
|
-
}
|
|
3709
|
-
/**
|
|
3710
|
-
* Get all fungible token definitions
|
|
3711
|
-
* @returns Array of fungible token definitions
|
|
3712
|
-
*/
|
|
3713
|
-
getFungibleTokens() {
|
|
3714
|
-
return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
|
|
3715
|
-
}
|
|
3716
|
-
/**
|
|
3717
|
-
* Get all non-fungible token definitions
|
|
3718
|
-
* @returns Array of non-fungible token definitions
|
|
3719
|
-
*/
|
|
3720
|
-
getNonFungibleTokens() {
|
|
3721
|
-
return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
|
|
3722
|
-
}
|
|
3723
|
-
/**
|
|
3724
|
-
* Get coin ID by symbol
|
|
3725
|
-
* @param symbol - Token symbol (e.g., "UCT")
|
|
3726
|
-
* @returns Coin ID hex string or undefined if not found
|
|
3727
|
-
*/
|
|
3728
|
-
getCoinIdBySymbol(symbol) {
|
|
3729
|
-
const def = this.getDefinitionBySymbol(symbol);
|
|
3730
|
-
return def?.id;
|
|
3731
|
-
}
|
|
3732
|
-
/**
|
|
3733
|
-
* Get coin ID by name
|
|
3734
|
-
* @param name - Token name (e.g., "bitcoin")
|
|
3735
|
-
* @returns Coin ID hex string or undefined if not found
|
|
3736
|
-
*/
|
|
3737
|
-
getCoinIdByName(name) {
|
|
3738
|
-
const def = this.getDefinitionByName(name);
|
|
3739
|
-
return def?.id;
|
|
3740
|
-
}
|
|
3741
|
-
};
|
|
3742
|
-
|
|
3743
3572
|
// impl/shared/resolvers.ts
|
|
3744
3573
|
function getNetworkConfig(network = "mainnet") {
|
|
3745
3574
|
return NETWORKS[network];
|
|
@@ -3833,8 +3662,6 @@ function createBrowserProviders(config) {
|
|
|
3833
3662
|
const priceConfig = resolvePriceConfig(config?.price, storage);
|
|
3834
3663
|
const groupChat = resolveGroupChatConfig(network, config?.groupChat);
|
|
3835
3664
|
const market = resolveMarketConfig(config?.market);
|
|
3836
|
-
const networkConfig = getNetworkConfig(network);
|
|
3837
|
-
TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
|
|
3838
3665
|
return {
|
|
3839
3666
|
storage,
|
|
3840
3667
|
groupChat,
|