@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
package/dist/index.cjs
CHANGED
|
@@ -109,7 +109,6 @@ __export(index_exports, {
|
|
|
109
109
|
getCoinIdByName: () => getCoinIdByName,
|
|
110
110
|
getCoinIdBySymbol: () => getCoinIdBySymbol,
|
|
111
111
|
getPublicKey: () => getPublicKey,
|
|
112
|
-
getSphere: () => getSphere,
|
|
113
112
|
getTokenDecimals: () => getTokenDecimals,
|
|
114
113
|
getTokenDefinition: () => getTokenDefinition,
|
|
115
114
|
getTokenIconUrl: () => getTokenIconUrl,
|
|
@@ -251,6 +250,58 @@ var logger = {
|
|
|
251
250
|
}
|
|
252
251
|
};
|
|
253
252
|
|
|
253
|
+
// core/global-cell.ts
|
|
254
|
+
var CELLS_KEY = "__sphere_sdk_cells_v1__";
|
|
255
|
+
var bundleLocalBag = null;
|
|
256
|
+
function isObject(value) {
|
|
257
|
+
return typeof value === "object" && value !== null;
|
|
258
|
+
}
|
|
259
|
+
function installBag(host, fresh) {
|
|
260
|
+
try {
|
|
261
|
+
Object.defineProperty(host, CELLS_KEY, {
|
|
262
|
+
value: fresh,
|
|
263
|
+
writable: true,
|
|
264
|
+
configurable: true,
|
|
265
|
+
enumerable: false
|
|
266
|
+
});
|
|
267
|
+
return host[CELLS_KEY] === fresh;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function cellBag() {
|
|
273
|
+
if (bundleLocalBag) return bundleLocalBag;
|
|
274
|
+
const host = globalThis;
|
|
275
|
+
const found = host[CELLS_KEY];
|
|
276
|
+
if (isObject(found)) return found;
|
|
277
|
+
const fresh = {};
|
|
278
|
+
if (!installBag(host, fresh)) bundleLocalBag = fresh;
|
|
279
|
+
return fresh;
|
|
280
|
+
}
|
|
281
|
+
function accepted(intact, candidate) {
|
|
282
|
+
try {
|
|
283
|
+
return intact(candidate);
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function put(bag, name, cell) {
|
|
289
|
+
try {
|
|
290
|
+
bag[name] = cell;
|
|
291
|
+
if (bag[name] === cell) return;
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
if (bag !== bundleLocalBag) bundleLocalBag = { [name]: cell };
|
|
295
|
+
}
|
|
296
|
+
function sharedCell(name, create, intact) {
|
|
297
|
+
const bag = cellBag();
|
|
298
|
+
const found = bag[name];
|
|
299
|
+
if (isObject(found) && accepted(intact, found)) return found;
|
|
300
|
+
const fresh = create();
|
|
301
|
+
put(bag, name, fresh);
|
|
302
|
+
return fresh;
|
|
303
|
+
}
|
|
304
|
+
|
|
254
305
|
// core/errors.ts
|
|
255
306
|
var SphereError = class extends Error {
|
|
256
307
|
code;
|
|
@@ -315,6 +366,11 @@ function isSphereError(err) {
|
|
|
315
366
|
return err instanceof SphereError;
|
|
316
367
|
}
|
|
317
368
|
|
|
369
|
+
// storage/tracked-addresses.ts
|
|
370
|
+
function isDerivableIndex(value) {
|
|
371
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
|
|
372
|
+
}
|
|
373
|
+
|
|
318
374
|
// transport/MultiAddressTransportMux.ts
|
|
319
375
|
var import_buffer2 = require("buffer");
|
|
320
376
|
var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
|
|
@@ -847,7 +903,6 @@ function getAddressId(directAddress) {
|
|
|
847
903
|
return `DIRECT_${first}_${last}`;
|
|
848
904
|
}
|
|
849
905
|
var DEFAULT_NOSTR_RELAYS = [
|
|
850
|
-
"wss://relay.unicity.network",
|
|
851
906
|
"wss://relay.damus.io",
|
|
852
907
|
"wss://nos.lol",
|
|
853
908
|
"wss://relay.nostr.band"
|
|
@@ -894,8 +949,6 @@ var NIP29_KINDS = {
|
|
|
894
949
|
/** Relay-signed group roles */
|
|
895
950
|
GROUP_ROLES: 39003
|
|
896
951
|
};
|
|
897
|
-
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
898
|
-
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
899
952
|
var DEFAULT_AGGREGATOR_TIMEOUT = 3e4;
|
|
900
953
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
901
954
|
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
@@ -903,7 +956,6 @@ var COIN_TYPES = {
|
|
|
903
956
|
/** Test token */
|
|
904
957
|
TEST: "TEST"
|
|
905
958
|
};
|
|
906
|
-
var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
|
|
907
959
|
var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
|
|
908
960
|
var TEST_NOSTR_RELAYS = [
|
|
909
961
|
"wss://nostr-relay.testnet.unicity.network"
|
|
@@ -914,10 +966,19 @@ var DEFAULT_GROUP_RELAYS = [
|
|
|
914
966
|
var NETWORKS = {
|
|
915
967
|
mainnet: {
|
|
916
968
|
name: "Mainnet",
|
|
917
|
-
|
|
918
|
-
|
|
969
|
+
networkId: 1,
|
|
970
|
+
// v3 state-transition gateway (networkId 1 comes from the trust base). apiKey is env-injected;
|
|
971
|
+
// unlike testnet2's, a mainnet gateway key is a SECRET — never commit one.
|
|
972
|
+
aggregatorUrl: "https://gateway.mainnet.unicity.network",
|
|
973
|
+
// Mainnet has no relay of its own yet — it shares the testnet relay until one is stood up.
|
|
974
|
+
// Consequence while shared: nametag bindings for both networks live in ONE namespace, and
|
|
975
|
+
// since bindings carry no network the cross-network recipient guard can only signal (#734).
|
|
976
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
919
977
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
920
|
-
|
|
978
|
+
// Published, but currently only the non-fungible base token type — no fungible
|
|
979
|
+
// coins yet, so those still miss the registry (decimals 0, symbol falls back to
|
|
980
|
+
// six hex chars). Presentation only: the money path treats coinId as opaque bytes.
|
|
981
|
+
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json"
|
|
921
982
|
},
|
|
922
983
|
// v1 cutover: 'testnet' now POINTS AT TESTNET2 (the v2 gateway network). The
|
|
923
984
|
// old goggregator testnet spoke the removed v1 protocol — a v2 engine cannot
|
|
@@ -941,19 +1002,10 @@ var NETWORKS = {
|
|
|
941
1002
|
// reuse testnet infra (shared relays/ipfs)
|
|
942
1003
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
943
1004
|
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
|
|
944
|
-
},
|
|
945
|
-
// NOTE: mainnet/dev still point at v1-era aggregators. The v2 engine cannot
|
|
946
|
-
// operate against them until their gateways are cut over to the v2 protocol —
|
|
947
|
-
// wallet operations on these networks fail loudly (AGGREGATOR_ERROR) until then.
|
|
948
|
-
dev: {
|
|
949
|
-
name: "Development",
|
|
950
|
-
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
951
|
-
nostrRelays: TEST_NOSTR_RELAYS,
|
|
952
|
-
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
953
|
-
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
954
1005
|
}
|
|
955
1006
|
};
|
|
956
1007
|
var SPHERE_NETWORKS = {
|
|
1008
|
+
mainnet: { id: NETWORKS.mainnet.networkId, name: "mainnet" },
|
|
957
1009
|
testnet2: { id: NETWORKS.testnet2.networkId, name: "testnet2" }
|
|
958
1010
|
};
|
|
959
1011
|
var TIMEOUTS = {
|
|
@@ -1139,14 +1191,10 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1139
1191
|
});
|
|
1140
1192
|
}
|
|
1141
1193
|
});
|
|
1142
|
-
await
|
|
1143
|
-
this.nostrClient
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1147
|
-
)), this.config.timeout)
|
|
1148
|
-
)
|
|
1149
|
-
]);
|
|
1194
|
+
await this.connectWithDeadline(
|
|
1195
|
+
this.nostrClient,
|
|
1196
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1197
|
+
);
|
|
1150
1198
|
if (!this.nostrClient.isConnected()) {
|
|
1151
1199
|
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
1152
1200
|
}
|
|
@@ -1161,6 +1209,27 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1161
1209
|
throw error;
|
|
1162
1210
|
}
|
|
1163
1211
|
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Race one client's relay connect against the configured timeout.
|
|
1214
|
+
*
|
|
1215
|
+
* The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
|
|
1216
|
+
* cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
|
|
1217
|
+
* for the whole `config.timeout` after the call has already returned (and
|
|
1218
|
+
* then rejects a promise nobody is listening to any more).
|
|
1219
|
+
*/
|
|
1220
|
+
async connectWithDeadline(client, timeoutMessage) {
|
|
1221
|
+
let timer;
|
|
1222
|
+
try {
|
|
1223
|
+
await Promise.race([
|
|
1224
|
+
client.connect(...this.config.relays),
|
|
1225
|
+
new Promise((_, reject) => {
|
|
1226
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), this.config.timeout);
|
|
1227
|
+
})
|
|
1228
|
+
]);
|
|
1229
|
+
} finally {
|
|
1230
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1164
1233
|
async disconnect() {
|
|
1165
1234
|
if (this.nostrClient) {
|
|
1166
1235
|
this.nostrClient.disconnect();
|
|
@@ -1279,19 +1348,22 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1279
1348
|
// TransportProvider Implementation
|
|
1280
1349
|
// ===========================================================================
|
|
1281
1350
|
async setIdentity(identity) {
|
|
1282
|
-
this.identity = identity;
|
|
1283
|
-
this.processedEventIds.clear();
|
|
1284
|
-
this.lastEventTs = 0;
|
|
1285
|
-
this.lastDmEventTs = 0;
|
|
1286
|
-
this.fallbackDmSince = null;
|
|
1287
1351
|
const secretKey = import_buffer.Buffer.from(identity.privateKey, "hex");
|
|
1288
|
-
|
|
1289
|
-
const nostrPubkey =
|
|
1290
|
-
logger.debug("Nostr", "Identity
|
|
1352
|
+
const nextKeyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(secretKey);
|
|
1353
|
+
const nostrPubkey = nextKeyManager.getPublicKeyHex();
|
|
1354
|
+
logger.debug("Nostr", "Identity staged, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
|
|
1355
|
+
const applyStagedIdentity = () => {
|
|
1356
|
+
this.identity = identity;
|
|
1357
|
+
this.keyManager = nextKeyManager;
|
|
1358
|
+
this.processedEventIds.clear();
|
|
1359
|
+
this.lastEventTs = 0;
|
|
1360
|
+
this.lastDmEventTs = 0;
|
|
1361
|
+
this.fallbackDmSince = null;
|
|
1362
|
+
};
|
|
1291
1363
|
if (this.nostrClient && this.status === "connected") {
|
|
1292
1364
|
logger.debug("Nostr", "Identity changed while connected - recreating NostrClient");
|
|
1293
1365
|
const oldClient = this.nostrClient;
|
|
1294
|
-
|
|
1366
|
+
const nextClient = new import_nostr_js_sdk.NostrClient(nextKeyManager, {
|
|
1295
1367
|
autoReconnect: this.config.autoReconnect,
|
|
1296
1368
|
reconnectIntervalMs: this.config.reconnectDelay,
|
|
1297
1369
|
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
@@ -1300,7 +1372,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1300
1372
|
queryTimeoutMs: 2e4
|
|
1301
1373
|
// see same option above for rationale
|
|
1302
1374
|
});
|
|
1303
|
-
|
|
1375
|
+
nextClient.addConnectionListener({
|
|
1304
1376
|
onConnect: (url) => {
|
|
1305
1377
|
logger.debug("Nostr", "NostrClient connected to relay:", url);
|
|
1306
1378
|
},
|
|
@@ -1314,18 +1386,43 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1314
1386
|
logger.debug("Nostr", "NostrClient reconnected to relay:", url);
|
|
1315
1387
|
}
|
|
1316
1388
|
});
|
|
1317
|
-
|
|
1318
|
-
this.
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1389
|
+
try {
|
|
1390
|
+
await this.connectWithDeadline(
|
|
1391
|
+
nextClient,
|
|
1392
|
+
`Transport reconnection timed out after ${this.config.timeout}ms`
|
|
1393
|
+
);
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
try {
|
|
1396
|
+
nextClient.disconnect();
|
|
1397
|
+
} catch {
|
|
1398
|
+
}
|
|
1399
|
+
throw error;
|
|
1400
|
+
}
|
|
1401
|
+
if (this.nostrClient !== oldClient || this.status !== "connected") {
|
|
1402
|
+
try {
|
|
1403
|
+
nextClient.disconnect();
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
throw new SphereError(
|
|
1407
|
+
"Transport client changed or left the connected state during setIdentity - identity not applied",
|
|
1408
|
+
"TRANSPORT_ERROR"
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
this.nostrClient = nextClient;
|
|
1412
|
+
applyStagedIdentity();
|
|
1413
|
+
try {
|
|
1414
|
+
await this.subscribeToEvents();
|
|
1415
|
+
} finally {
|
|
1416
|
+
try {
|
|
1417
|
+
oldClient.disconnect();
|
|
1418
|
+
} catch {
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1327
1421
|
} else if (this.isConnected()) {
|
|
1422
|
+
applyStagedIdentity();
|
|
1328
1423
|
await this.subscribeToEvents();
|
|
1424
|
+
} else {
|
|
1425
|
+
applyStagedIdentity();
|
|
1329
1426
|
}
|
|
1330
1427
|
}
|
|
1331
1428
|
setFallbackSince(sinceSeconds) {
|
|
@@ -8186,6 +8283,12 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8186
8283
|
lastRefreshAt = 0;
|
|
8187
8284
|
refreshPromise = null;
|
|
8188
8285
|
initialLoadPromise = null;
|
|
8286
|
+
/** Bumped on every remoteUrl change; loads started in an older generation are discarded. */
|
|
8287
|
+
generation = 0;
|
|
8288
|
+
/** Set by dispose(). A disposed registry starts no work and applies no late result. */
|
|
8289
|
+
disposed = false;
|
|
8290
|
+
/** Cancels the in-flight fetch (and its abort timer) when the registry is disposed. */
|
|
8291
|
+
inFlight = null;
|
|
8189
8292
|
constructor() {
|
|
8190
8293
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
8191
8294
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
@@ -8214,27 +8317,90 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8214
8317
|
* @param options.autoRefresh - Start auto-refresh immediately (default: true)
|
|
8215
8318
|
*/
|
|
8216
8319
|
static configure(options) {
|
|
8217
|
-
|
|
8320
|
+
_TokenRegistry.getInstance().applyConfig(options);
|
|
8321
|
+
}
|
|
8322
|
+
/**
|
|
8323
|
+
* Create an INDEPENDENT registry, not the process-global singleton.
|
|
8324
|
+
*
|
|
8325
|
+
* One Sphere owns one of these, so two Spheres on different networks cannot wipe each
|
|
8326
|
+
* other's definitions — the singleton's `configure()` reaches into whatever instance
|
|
8327
|
+
* exists and repoints it, which is how a mainnet init silently retargeted a live
|
|
8328
|
+
* testnet2 wallet's decimals. Dispose it when the owner is destroyed.
|
|
8329
|
+
*/
|
|
8330
|
+
static create(options) {
|
|
8331
|
+
const registry = new _TokenRegistry();
|
|
8332
|
+
registry.applyConfig(options);
|
|
8333
|
+
return registry;
|
|
8334
|
+
}
|
|
8335
|
+
/** The body of configure(), on the instance, so owned registries share it exactly. */
|
|
8336
|
+
applyConfig(options) {
|
|
8337
|
+
if (this.disposed) return;
|
|
8218
8338
|
if (options.remoteUrl !== void 0) {
|
|
8219
|
-
|
|
8339
|
+
const previous = this.remoteUrl;
|
|
8340
|
+
if (previous && previous !== options.remoteUrl) {
|
|
8341
|
+
this.applyDefinitions([]);
|
|
8342
|
+
this.lastRefreshAt = 0;
|
|
8343
|
+
this.generation++;
|
|
8344
|
+
this.refreshPromise = null;
|
|
8345
|
+
}
|
|
8346
|
+
this.remoteUrl = options.remoteUrl;
|
|
8220
8347
|
}
|
|
8221
8348
|
if (options.storage !== void 0) {
|
|
8222
|
-
|
|
8349
|
+
this.storage = options.storage;
|
|
8223
8350
|
}
|
|
8224
8351
|
if (options.refreshIntervalMs !== void 0) {
|
|
8225
|
-
|
|
8352
|
+
this.refreshIntervalMs = options.refreshIntervalMs;
|
|
8226
8353
|
}
|
|
8227
8354
|
const autoRefresh = options.autoRefresh ?? true;
|
|
8228
|
-
|
|
8355
|
+
this.initialLoadPromise = this.performInitialLoad(autoRefresh);
|
|
8356
|
+
}
|
|
8357
|
+
/**
|
|
8358
|
+
* Stop this registry for good: no timer, no late apply, no late cache write.
|
|
8359
|
+
*
|
|
8360
|
+
* Sphere.destroy() calls this on the registry it owns. Without it a discarded Sphere
|
|
8361
|
+
* leaves an hourly fetch running forever — nothing in this file calls unref(), so in
|
|
8362
|
+
* Node it also keeps the event loop alive.
|
|
8363
|
+
*/
|
|
8364
|
+
dispose() {
|
|
8365
|
+
if (this.disposed) return;
|
|
8366
|
+
this.disposed = true;
|
|
8367
|
+
this.stopAutoRefresh();
|
|
8368
|
+
this.inFlight?.abort();
|
|
8369
|
+
this.inFlight = null;
|
|
8370
|
+
this.generation++;
|
|
8371
|
+
this.refreshPromise = null;
|
|
8372
|
+
this.initialLoadPromise = null;
|
|
8373
|
+
}
|
|
8374
|
+
/** Whether dispose() has been called. Reads still work; they are simply frozen. */
|
|
8375
|
+
get isDisposed() {
|
|
8376
|
+
return this.disposed;
|
|
8377
|
+
}
|
|
8378
|
+
/** Per-instance readiness — the same contract as the static, for an owned registry. */
|
|
8379
|
+
async waitForReady(timeoutMs = 1e4) {
|
|
8380
|
+
if (!this.initialLoadPromise) {
|
|
8381
|
+
return this.definitionsById.size > 0;
|
|
8382
|
+
}
|
|
8383
|
+
if (timeoutMs <= 0) {
|
|
8384
|
+
return this.initialLoadPromise;
|
|
8385
|
+
}
|
|
8386
|
+
let timer;
|
|
8387
|
+
try {
|
|
8388
|
+
return await Promise.race([
|
|
8389
|
+
this.initialLoadPromise,
|
|
8390
|
+
new Promise((resolve) => {
|
|
8391
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
8392
|
+
})
|
|
8393
|
+
]);
|
|
8394
|
+
} finally {
|
|
8395
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8396
|
+
}
|
|
8229
8397
|
}
|
|
8230
8398
|
/**
|
|
8231
8399
|
* Reset the singleton instance (useful for testing).
|
|
8232
8400
|
* Stops auto-refresh if running.
|
|
8233
8401
|
*/
|
|
8234
8402
|
static resetInstance() {
|
|
8235
|
-
|
|
8236
|
-
_TokenRegistry.instance.stopAutoRefresh();
|
|
8237
|
-
}
|
|
8403
|
+
_TokenRegistry.instance?.dispose();
|
|
8238
8404
|
_TokenRegistry.instance = null;
|
|
8239
8405
|
}
|
|
8240
8406
|
/**
|
|
@@ -8250,17 +8416,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8250
8416
|
* @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
|
|
8251
8417
|
*/
|
|
8252
8418
|
static async waitForReady(timeoutMs = 1e4) {
|
|
8253
|
-
|
|
8254
|
-
if (!instance.initialLoadPromise) {
|
|
8255
|
-
return instance.definitionsById.size > 0;
|
|
8256
|
-
}
|
|
8257
|
-
if (timeoutMs <= 0) {
|
|
8258
|
-
return instance.initialLoadPromise;
|
|
8259
|
-
}
|
|
8260
|
-
return Promise.race([
|
|
8261
|
-
instance.initialLoadPromise,
|
|
8262
|
-
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
|
|
8263
|
-
]);
|
|
8419
|
+
return _TokenRegistry.getInstance().waitForReady(timeoutMs);
|
|
8264
8420
|
}
|
|
8265
8421
|
// ===========================================================================
|
|
8266
8422
|
// Initial Load
|
|
@@ -8270,6 +8426,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8270
8426
|
* After initial data is available, start periodic auto-refresh if configured.
|
|
8271
8427
|
*/
|
|
8272
8428
|
async performInitialLoad(autoRefresh) {
|
|
8429
|
+
if (this.disposed) return false;
|
|
8273
8430
|
let loaded = false;
|
|
8274
8431
|
if (this.storage) {
|
|
8275
8432
|
loaded = await this.loadFromCache();
|
|
@@ -8282,6 +8439,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8282
8439
|
}
|
|
8283
8440
|
if (autoRefresh && this.remoteUrl) {
|
|
8284
8441
|
loaded = await this.refreshFromRemote();
|
|
8442
|
+
if (this.disposed) return loaded;
|
|
8285
8443
|
this.stopAutoRefresh();
|
|
8286
8444
|
this.refreshTimer = setInterval(() => {
|
|
8287
8445
|
this.refreshFromRemote();
|
|
@@ -8312,6 +8470,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8312
8470
|
*/
|
|
8313
8471
|
async loadFromCache() {
|
|
8314
8472
|
if (!this.storage) return false;
|
|
8473
|
+
const gen = this.generation;
|
|
8315
8474
|
try {
|
|
8316
8475
|
const { data: dataKey, ts: tsKey } = this.cacheKeys();
|
|
8317
8476
|
const [cached, cachedTs] = await Promise.all([
|
|
@@ -8326,6 +8485,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8326
8485
|
if (this.lastRefreshAt > ts) return false;
|
|
8327
8486
|
const data = JSON.parse(cached);
|
|
8328
8487
|
if (!this.isValidDefinitionsArray(data)) return false;
|
|
8488
|
+
if (gen !== this.generation) return false;
|
|
8329
8489
|
this.applyDefinitions(data);
|
|
8330
8490
|
this.lastRefreshAt = ts;
|
|
8331
8491
|
return true;
|
|
@@ -8380,32 +8540,51 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8380
8540
|
* Concurrent calls are deduplicated — only one fetch runs at a time.
|
|
8381
8541
|
*/
|
|
8382
8542
|
async refreshFromRemote() {
|
|
8383
|
-
if (!this.remoteUrl) {
|
|
8543
|
+
if (this.disposed || !this.remoteUrl) {
|
|
8384
8544
|
return false;
|
|
8385
8545
|
}
|
|
8386
8546
|
if (this.refreshPromise) {
|
|
8387
8547
|
return this.refreshPromise;
|
|
8388
8548
|
}
|
|
8389
|
-
|
|
8549
|
+
const attempt = this.doRefresh();
|
|
8550
|
+
this.refreshPromise = attempt;
|
|
8390
8551
|
try {
|
|
8391
|
-
return await
|
|
8552
|
+
return await attempt;
|
|
8392
8553
|
} finally {
|
|
8393
|
-
this.refreshPromise = null;
|
|
8554
|
+
if (this.refreshPromise === attempt) this.refreshPromise = null;
|
|
8394
8555
|
}
|
|
8395
8556
|
}
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8557
|
+
/**
|
|
8558
|
+
* Fetch with a bounded timeout, exposing a cancel hook to dispose().
|
|
8559
|
+
*
|
|
8560
|
+
* Both the request and its abort timer keep Node's event loop alive for the full
|
|
8561
|
+
* FETCH_TIMEOUT_MS, so a registry disposed mid-flight must cancel them rather than
|
|
8562
|
+
* merely ignore the result.
|
|
8563
|
+
*/
|
|
8564
|
+
async fetchCancellable(url) {
|
|
8565
|
+
const controller = new AbortController();
|
|
8566
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
8567
|
+
this.inFlight = {
|
|
8568
|
+
abort: () => {
|
|
8407
8569
|
clearTimeout(timer);
|
|
8570
|
+
controller.abort();
|
|
8408
8571
|
}
|
|
8572
|
+
};
|
|
8573
|
+
try {
|
|
8574
|
+
return await fetch(url, {
|
|
8575
|
+
headers: { Accept: "application/json" },
|
|
8576
|
+
signal: controller.signal
|
|
8577
|
+
});
|
|
8578
|
+
} finally {
|
|
8579
|
+
clearTimeout(timer);
|
|
8580
|
+
this.inFlight = null;
|
|
8581
|
+
}
|
|
8582
|
+
}
|
|
8583
|
+
async doRefresh() {
|
|
8584
|
+
const url = this.remoteUrl;
|
|
8585
|
+
const gen = this.generation;
|
|
8586
|
+
try {
|
|
8587
|
+
const response = await this.fetchCancellable(url);
|
|
8409
8588
|
if (!response.ok) {
|
|
8410
8589
|
logger.warn("TokenRegistry", `Remote fetch failed: HTTP ${response.status} ${response.statusText}`);
|
|
8411
8590
|
return false;
|
|
@@ -8415,6 +8594,10 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8415
8594
|
logger.warn("TokenRegistry", "Remote data is not a valid token definitions array");
|
|
8416
8595
|
return false;
|
|
8417
8596
|
}
|
|
8597
|
+
if (gen !== this.generation) {
|
|
8598
|
+
logger.debug("TokenRegistry", `Discarding stale refresh for ${url} (network changed)`);
|
|
8599
|
+
return false;
|
|
8600
|
+
}
|
|
8418
8601
|
const definitions = data;
|
|
8419
8602
|
this.applyDefinitions(definitions);
|
|
8420
8603
|
this.lastRefreshAt = Date.now();
|
|
@@ -8431,6 +8614,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8431
8614
|
* Does an immediate fetch, then repeats at the configured interval.
|
|
8432
8615
|
*/
|
|
8433
8616
|
startAutoRefresh(intervalMs) {
|
|
8617
|
+
if (this.disposed) return;
|
|
8434
8618
|
this.stopAutoRefresh();
|
|
8435
8619
|
if (intervalMs !== void 0) {
|
|
8436
8620
|
this.refreshIntervalMs = intervalMs;
|
|
@@ -8583,6 +8767,8 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8583
8767
|
return def?.id;
|
|
8584
8768
|
}
|
|
8585
8769
|
};
|
|
8770
|
+
|
|
8771
|
+
// registry/global-readers.ts
|
|
8586
8772
|
function getTokenDefinition(coinId) {
|
|
8587
8773
|
return TokenRegistry.getInstance().getDefinition(coinId);
|
|
8588
8774
|
}
|
|
@@ -10257,12 +10443,58 @@ var SphereTokenEngine = class {
|
|
|
10257
10443
|
|
|
10258
10444
|
// token-engine/factory.ts
|
|
10259
10445
|
var DEFAULT_VERIFICATION_POOL_SIZE = 4;
|
|
10446
|
+
function disposedError() {
|
|
10447
|
+
return new SphereError(
|
|
10448
|
+
"Verification worker pool disposed \u2014 the in-flight verification was cancelled",
|
|
10449
|
+
"MODULE_DESTROYED"
|
|
10450
|
+
);
|
|
10451
|
+
}
|
|
10260
10452
|
var ConfiguredWorkerTokenVerifier = class extends import_WorkerTokenVerifier.WorkerTokenVerifier {
|
|
10261
10453
|
constructor(spawn, poolSize) {
|
|
10262
10454
|
super(poolSize);
|
|
10263
10455
|
this.spawn = spawn;
|
|
10264
10456
|
}
|
|
10457
|
+
disposed = false;
|
|
10458
|
+
/** One rejector per IN-FLIGHT verify(), dropped on settle; empty when idle. */
|
|
10459
|
+
cancellations = /* @__PURE__ */ new Set();
|
|
10460
|
+
/** Verifications awaiting a verdict — back to 0 whenever the verifier is idle. */
|
|
10461
|
+
get pendingCancellations() {
|
|
10462
|
+
return this.cancellations.size;
|
|
10463
|
+
}
|
|
10464
|
+
/** Rejects (see the class note) if `dispose()` lands before the pool answers. */
|
|
10465
|
+
verify(...args) {
|
|
10466
|
+
if (this.disposed) return Promise.reject(disposedError());
|
|
10467
|
+
const verdict = super.verify(...args);
|
|
10468
|
+
return new Promise((resolve, reject) => {
|
|
10469
|
+
const cancel = () => reject(disposedError());
|
|
10470
|
+
this.cancellations.add(cancel);
|
|
10471
|
+
if (this.disposed) {
|
|
10472
|
+
this.cancellations.delete(cancel);
|
|
10473
|
+
cancel();
|
|
10474
|
+
}
|
|
10475
|
+
void verdict.then(
|
|
10476
|
+
(result) => {
|
|
10477
|
+
this.cancellations.delete(cancel);
|
|
10478
|
+
resolve(result);
|
|
10479
|
+
},
|
|
10480
|
+
(error) => {
|
|
10481
|
+
this.cancellations.delete(cancel);
|
|
10482
|
+
reject(error);
|
|
10483
|
+
}
|
|
10484
|
+
);
|
|
10485
|
+
});
|
|
10486
|
+
}
|
|
10487
|
+
/** Idempotent — Sphere.setOracleApiKey disposes the replaced engine twice (facade + caller). */
|
|
10488
|
+
dispose() {
|
|
10489
|
+
if (this.disposed) return;
|
|
10490
|
+
this.disposed = true;
|
|
10491
|
+
super.dispose();
|
|
10492
|
+
const cancelling = [...this.cancellations];
|
|
10493
|
+
this.cancellations.clear();
|
|
10494
|
+
for (const cancel of cancelling) cancel();
|
|
10495
|
+
}
|
|
10265
10496
|
createWorker() {
|
|
10497
|
+
if (this.disposed) throw disposedError();
|
|
10266
10498
|
return this.spawn();
|
|
10267
10499
|
}
|
|
10268
10500
|
};
|
|
@@ -13091,12 +13323,13 @@ var Receive = class {
|
|
|
13091
13323
|
return this.drainFlight.run(() => this.doDrain());
|
|
13092
13324
|
}
|
|
13093
13325
|
start(pollIntervalMs = POLL_INTERVAL_MS) {
|
|
13094
|
-
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() =>
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13326
|
+
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() => this.spawnDrain()) ?? null;
|
|
13327
|
+
this.pollTimer ??= setInterval(() => this.spawnDrain(), pollIntervalMs);
|
|
13328
|
+
}
|
|
13329
|
+
spawnDrain() {
|
|
13330
|
+
const op = this.drainOnce();
|
|
13331
|
+
if (this.deps.track !== void 0) this.deps.track(op);
|
|
13332
|
+
else void op;
|
|
13100
13333
|
}
|
|
13101
13334
|
stop() {
|
|
13102
13335
|
if (this.pollTimer !== null) clearInterval(this.pollTimer);
|
|
@@ -14199,6 +14432,8 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
|
|
|
14199
14432
|
);
|
|
14200
14433
|
},
|
|
14201
14434
|
syncEpoch: deps.syncEpoch,
|
|
14435
|
+
// #770: the poll/wake drains Receive spawns itself must hold stop() too.
|
|
14436
|
+
track: hooks.track,
|
|
14202
14437
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
14203
14438
|
});
|
|
14204
14439
|
}
|
|
@@ -14341,7 +14576,8 @@ var PaymentsFacade = class {
|
|
|
14341
14576
|
await Promise.allSettled([...this.pendingOps]);
|
|
14342
14577
|
}
|
|
14343
14578
|
}
|
|
14344
|
-
/** Swaps what FUTURE operations snapshot
|
|
14579
|
+
/** Swaps what FUTURE operations snapshot. Chain ops finish on the old engine, but it is
|
|
14580
|
+
* DISPOSED here, so one mid-`verify()` is cancelled — MODULE_DESTROYED, #770(4). */
|
|
14345
14581
|
setEngine(next) {
|
|
14346
14582
|
const previous = this.currentEngine ?? this.deps.engineRef();
|
|
14347
14583
|
this.currentEngine = next;
|
|
@@ -16066,7 +16302,7 @@ function composePaymentsV2(spec) {
|
|
|
16066
16302
|
}),
|
|
16067
16303
|
engineRef: spec.engineRef,
|
|
16068
16304
|
kv,
|
|
16069
|
-
registry:
|
|
16305
|
+
registry: host.registry,
|
|
16070
16306
|
...host.price !== null ? { price: host.price } : {},
|
|
16071
16307
|
emit: host.emit,
|
|
16072
16308
|
resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
|
|
@@ -16094,13 +16330,47 @@ async function deriveL3PredicateAddress(privateKey) {
|
|
|
16094
16330
|
const prehashedPublicKey = getPublicKey(sha2562(privateKey, "hex"));
|
|
16095
16331
|
return deriveDirectAddress(hexToBytes4(prehashedPublicKey));
|
|
16096
16332
|
}
|
|
16333
|
+
var lifecycle = sharedCell(
|
|
16334
|
+
"core.sphere.lifecycle@1",
|
|
16335
|
+
() => ({
|
|
16336
|
+
live: /* @__PURE__ */ new Map(),
|
|
16337
|
+
clearGenerations: /* @__PURE__ */ new Map(),
|
|
16338
|
+
objectStoreKeys: /* @__PURE__ */ new WeakMap(),
|
|
16339
|
+
objectStoreSeq: 0
|
|
16340
|
+
}),
|
|
16341
|
+
(cell) => {
|
|
16342
|
+
const c = cell;
|
|
16343
|
+
return c.live instanceof Map && c.clearGenerations instanceof Map && c.objectStoreKeys instanceof WeakMap && typeof c.objectStoreSeq === "number";
|
|
16344
|
+
}
|
|
16345
|
+
);
|
|
16097
16346
|
var Sphere = class _Sphere {
|
|
16098
|
-
//
|
|
16099
|
-
|
|
16347
|
+
// Live Spheres, keyed by the BACKING STORE their provider addresses. NOT a liveness
|
|
16348
|
+
// API: it exists only so clear()/import() tear down the instances whose data they
|
|
16349
|
+
// really erase. Object identity was too narrow — two providers over one dataDir/DB
|
|
16350
|
+
// are different objects but the same file, so clearing through one left the other's
|
|
16351
|
+
// Sphere live over an emptied KV (#766).
|
|
16352
|
+
static _liveByStorage = lifecycle.live;
|
|
16353
|
+
/** Fallback keys for providers that declare no `backingStoreId` — one per object. */
|
|
16354
|
+
static _objectStoreKeys = lifecycle.objectStoreKeys;
|
|
16355
|
+
/**
|
|
16356
|
+
* How many times each backing store has been cleared. An init is invisible to `clear()`
|
|
16357
|
+
* until it PUBLISHES (#767), so a clear cannot destroy one in flight and would wipe the
|
|
16358
|
+
* store under it. Every init records this number before its first storage work and
|
|
16359
|
+
* publication refuses if it moved (#772). Only cleared stores get an entry, so the
|
|
16360
|
+
* strongly-held keys are bounded by the stores a process actually clears.
|
|
16361
|
+
*/
|
|
16362
|
+
static _clearGenerations = lifecycle.clearGenerations;
|
|
16100
16363
|
// One-time best-effort cleanup of the orphaned vesting cache (prior versions).
|
|
16101
16364
|
static _orphanCacheCleaned = false;
|
|
16102
16365
|
// State
|
|
16103
16366
|
_initialized = false;
|
|
16367
|
+
/**
|
|
16368
|
+
* Destroyed latch (#770). Distinct from `_initialized`, which destroy() clears LAST — after
|
|
16369
|
+
* every teardown step — so it cannot mark "teardown has begun". This is set as destroy()'s
|
|
16370
|
+
* very FIRST statement, so the WHOLE teardown window is guarded, not just the instant after
|
|
16371
|
+
* it. Read by ensureAlive() and by the §7 lifecycle mutex.
|
|
16372
|
+
*/
|
|
16373
|
+
_destroyed = false;
|
|
16104
16374
|
_trackedAddressesLoaded = false;
|
|
16105
16375
|
_identity = null;
|
|
16106
16376
|
_masterKey = null;
|
|
@@ -16158,6 +16428,8 @@ var Sphere = class _Sphere {
|
|
|
16158
16428
|
* the rebuild after an api-key change share one pool configuration.
|
|
16159
16429
|
*/
|
|
16160
16430
|
_verification;
|
|
16431
|
+
/** This Sphere's OWN token registry. Disposed by destroy(); never the process global. */
|
|
16432
|
+
_registry = null;
|
|
16161
16433
|
// Events
|
|
16162
16434
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
16163
16435
|
// Provider management
|
|
@@ -16232,7 +16504,7 @@ var Sphere = class _Sphere {
|
|
|
16232
16504
|
* ```
|
|
16233
16505
|
*/
|
|
16234
16506
|
static async init(options) {
|
|
16235
|
-
if (options.debug) logger.configure({ debug:
|
|
16507
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16236
16508
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16237
16509
|
resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16238
16510
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
@@ -16252,6 +16524,7 @@ var Sphere = class _Sphere {
|
|
|
16252
16524
|
market,
|
|
16253
16525
|
communications: options.communications,
|
|
16254
16526
|
password: options.password,
|
|
16527
|
+
verification: options.verification,
|
|
16255
16528
|
discoverAddresses: options.discoverAddresses,
|
|
16256
16529
|
onProgress: options.onProgress
|
|
16257
16530
|
});
|
|
@@ -16288,6 +16561,7 @@ var Sphere = class _Sphere {
|
|
|
16288
16561
|
market,
|
|
16289
16562
|
communications: options.communications,
|
|
16290
16563
|
password: options.password,
|
|
16564
|
+
verification: options.verification,
|
|
16291
16565
|
discoverAddresses: options.discoverAddresses,
|
|
16292
16566
|
onProgress: options.onProgress
|
|
16293
16567
|
});
|
|
@@ -16369,16 +16643,53 @@ var Sphere = class _Sphere {
|
|
|
16369
16643
|
}
|
|
16370
16644
|
TokenRegistry.configure({ remoteUrl: NETWORKS[network].tokenRegistryUrl, storage });
|
|
16371
16645
|
}
|
|
16646
|
+
/**
|
|
16647
|
+
* Build the registry THIS Sphere owns, so its metadata cannot be repointed by another
|
|
16648
|
+
* Sphere's init. The global is still configured above for consumers that read it
|
|
16649
|
+
* directly; this instance is what the payments facade presents from.
|
|
16650
|
+
*/
|
|
16651
|
+
/**
|
|
16652
|
+
* Own a registry for the duration of `bringUp`, disposing it if any of that rejects.
|
|
16653
|
+
*
|
|
16654
|
+
* `bringUp` must cover EVERY fallible step from here until the Sphere is returned to
|
|
16655
|
+
* its caller — until then nobody holds anything they could destroy, so a
|
|
16656
|
+
* registry left behind is unreachable and its hourly fetch runs for the life of the
|
|
16657
|
+
* process. Guarding a named subset of the steps is what failed twice: the guarded region
|
|
16658
|
+
* and the fallible region were separate things, and drifted. Publication is the LAST
|
|
16659
|
+
* guarded step for the same reason: it can refuse, and a refusal must tear down.
|
|
16660
|
+
*/
|
|
16661
|
+
static async withOwnedRegistry(sphere, storage, network, clearGeneration, bringUp) {
|
|
16662
|
+
sphere._registry = _Sphere.createOwnedRegistry(storage, network);
|
|
16663
|
+
try {
|
|
16664
|
+
await bringUp();
|
|
16665
|
+
_Sphere.publishLive(sphere, clearGeneration);
|
|
16666
|
+
} catch (err) {
|
|
16667
|
+
await sphere.destroy().catch((teardownErr) => {
|
|
16668
|
+
logger.warn("Sphere", "teardown after failed initialization also failed:", teardownErr);
|
|
16669
|
+
});
|
|
16670
|
+
throw err;
|
|
16671
|
+
}
|
|
16672
|
+
}
|
|
16673
|
+
static createOwnedRegistry(storage, network) {
|
|
16674
|
+
if (!network) {
|
|
16675
|
+
throw new SphereError(
|
|
16676
|
+
"network is required to build the token registry. Every Sphere entry point must forward options.network.",
|
|
16677
|
+
"INVALID_CONFIG"
|
|
16678
|
+
);
|
|
16679
|
+
}
|
|
16680
|
+
return TokenRegistry.create({ remoteUrl: NETWORKS[network].tokenRegistryUrl, storage });
|
|
16681
|
+
}
|
|
16372
16682
|
/**
|
|
16373
16683
|
* Create new wallet with mnemonic
|
|
16374
16684
|
*/
|
|
16375
16685
|
static async create(options) {
|
|
16376
|
-
if (options.debug) logger.configure({ debug:
|
|
16686
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16377
16687
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16378
16688
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16379
16689
|
if (!options.mnemonic || !_Sphere.validateMnemonic(options.mnemonic)) {
|
|
16380
16690
|
throw new SphereError("Invalid mnemonic", "INVALID_IDENTITY");
|
|
16381
16691
|
}
|
|
16692
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16382
16693
|
if (await _Sphere.exists(options.storage)) {
|
|
16383
16694
|
throw new SphereError("Wallet already exists. Use Sphere.load() or Sphere.clear() first.", "ALREADY_INITIALIZED");
|
|
16384
16695
|
}
|
|
@@ -16406,44 +16717,46 @@ var Sphere = class _Sphere {
|
|
|
16406
16717
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
16407
16718
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
16408
16719
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16409
|
-
await sphere.
|
|
16410
|
-
|
|
16411
|
-
|
|
16412
|
-
|
|
16413
|
-
|
|
16414
|
-
|
|
16415
|
-
|
|
16416
|
-
|
|
16417
|
-
|
|
16418
|
-
|
|
16419
|
-
|
|
16420
|
-
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16720
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16721
|
+
await sphere.initializeProviders();
|
|
16722
|
+
await sphere.initializeModules();
|
|
16723
|
+
progress?.({ step: "finalizing", message: "Finalizing wallet..." });
|
|
16724
|
+
await sphere.finalizeWalletCreation();
|
|
16725
|
+
sphere._initialized = true;
|
|
16726
|
+
await sphere.ensureAddressTracked(0);
|
|
16727
|
+
if (options.nametag) {
|
|
16728
|
+
progress?.({ step: "registering_nametag", message: "Registering nametag..." });
|
|
16729
|
+
await sphere.registerNametag(options.nametag);
|
|
16730
|
+
} else {
|
|
16731
|
+
progress?.({ step: "recovering_nametag", message: "Recovering nametag..." });
|
|
16732
|
+
await sphere.recoverNametagFromTransport();
|
|
16733
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16734
|
+
await sphere.syncIdentityWithTransport();
|
|
16735
|
+
}
|
|
16736
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses) {
|
|
16737
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16738
|
+
try {
|
|
16739
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16740
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16741
|
+
if (result.addresses.length > 0) {
|
|
16742
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16743
|
+
}
|
|
16744
|
+
} catch (err) {
|
|
16745
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16432
16746
|
}
|
|
16433
|
-
} catch (err) {
|
|
16434
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16435
16747
|
}
|
|
16436
|
-
|
|
16437
|
-
|
|
16748
|
+
progress?.({ step: "complete", message: "Wallet created" });
|
|
16749
|
+
});
|
|
16438
16750
|
return sphere;
|
|
16439
16751
|
}
|
|
16440
16752
|
/**
|
|
16441
16753
|
* Load existing wallet from storage
|
|
16442
16754
|
*/
|
|
16443
16755
|
static async load(options) {
|
|
16444
|
-
if (options.debug) logger.configure({ debug:
|
|
16756
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16445
16757
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16446
16758
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16759
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16447
16760
|
if (!await _Sphere.exists(options.storage)) {
|
|
16448
16761
|
throw new SphereError("No wallet found. Use Sphere.create() to create a new wallet.", "NOT_INITIALIZED");
|
|
16449
16762
|
}
|
|
@@ -16470,32 +16783,33 @@ var Sphere = class _Sphere {
|
|
|
16470
16783
|
progress?.({ step: "storing_keys", message: "Loading wallet keys..." });
|
|
16471
16784
|
await sphere.loadIdentityFromStorage();
|
|
16472
16785
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16473
|
-
await sphere.
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
|
|
16485
|
-
|
|
16786
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16787
|
+
await sphere.initializeProviders();
|
|
16788
|
+
await sphere.initializeModules();
|
|
16789
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16790
|
+
await sphere.syncIdentityWithTransport();
|
|
16791
|
+
sphere._initialized = true;
|
|
16792
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses && sphere._masterKey) {
|
|
16793
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16794
|
+
try {
|
|
16795
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16796
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16797
|
+
if (result.addresses.length > 0) {
|
|
16798
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16799
|
+
}
|
|
16800
|
+
} catch (err) {
|
|
16801
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16486
16802
|
}
|
|
16487
|
-
} catch (err) {
|
|
16488
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16489
16803
|
}
|
|
16490
|
-
|
|
16491
|
-
|
|
16804
|
+
progress?.({ step: "complete", message: "Wallet loaded" });
|
|
16805
|
+
});
|
|
16492
16806
|
return sphere;
|
|
16493
16807
|
}
|
|
16494
16808
|
/**
|
|
16495
16809
|
* Import wallet from mnemonic or master key
|
|
16496
16810
|
*/
|
|
16497
16811
|
static async import(options) {
|
|
16498
|
-
if (options.debug) logger.configure({ debug:
|
|
16812
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16499
16813
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16500
16814
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16501
16815
|
if (!options.mnemonic && !options.masterKey) {
|
|
@@ -16503,7 +16817,7 @@ var Sphere = class _Sphere {
|
|
|
16503
16817
|
}
|
|
16504
16818
|
const progress = options.onProgress;
|
|
16505
16819
|
logger.debug("Sphere", "Starting import...");
|
|
16506
|
-
const needsClear = _Sphere.
|
|
16820
|
+
const needsClear = _Sphere.liveOn(options.storage).length > 0 || await _Sphere.exists(options.storage);
|
|
16507
16821
|
if (needsClear) {
|
|
16508
16822
|
progress?.({ step: "clearing", message: "Clearing previous wallet data..." });
|
|
16509
16823
|
logger.debug("Sphere", "Clearing existing wallet data...");
|
|
@@ -16517,6 +16831,7 @@ var Sphere = class _Sphere {
|
|
|
16517
16831
|
await options.storage.connect();
|
|
16518
16832
|
logger.debug("Sphere", "Storage reconnected");
|
|
16519
16833
|
}
|
|
16834
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16520
16835
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
16521
16836
|
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
16522
16837
|
const marketConfig = _Sphere.resolveMarketConfig(options.market);
|
|
@@ -16560,44 +16875,44 @@ var Sphere = class _Sphere {
|
|
|
16560
16875
|
}
|
|
16561
16876
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16562
16877
|
logger.debug("Sphere", "Initializing providers...");
|
|
16563
|
-
await sphere.
|
|
16564
|
-
|
|
16565
|
-
|
|
16566
|
-
|
|
16567
|
-
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16571
|
-
|
|
16572
|
-
|
|
16573
|
-
|
|
16574
|
-
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
|
|
16578
|
-
|
|
16579
|
-
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16585
|
-
|
|
16586
|
-
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
16592
|
-
|
|
16593
|
-
|
|
16878
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16879
|
+
await sphere.initializeProviders();
|
|
16880
|
+
await sphere.initializeModules();
|
|
16881
|
+
logger.debug("Sphere", "Modules initialized");
|
|
16882
|
+
if (!options.nametag) {
|
|
16883
|
+
progress?.({ step: "recovering_nametag", message: "Recovering nametag..." });
|
|
16884
|
+
logger.debug("Sphere", "Recovering Unicity ID from transport...");
|
|
16885
|
+
await sphere.recoverNametagFromTransport();
|
|
16886
|
+
logger.debug("Sphere", "Unicity ID recovery done");
|
|
16887
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16888
|
+
await sphere.syncIdentityWithTransport();
|
|
16889
|
+
}
|
|
16890
|
+
progress?.({ step: "finalizing", message: "Finalizing wallet creation..." });
|
|
16891
|
+
logger.debug("Sphere", "Finalizing wallet creation...");
|
|
16892
|
+
await sphere.finalizeWalletCreation();
|
|
16893
|
+
sphere._initialized = true;
|
|
16894
|
+
logger.debug("Sphere", "Tracking address 0...");
|
|
16895
|
+
await sphere.ensureAddressTracked(0);
|
|
16896
|
+
if (options.nametag) {
|
|
16897
|
+
progress?.({ step: "registering_nametag", message: "Registering nametag..." });
|
|
16898
|
+
logger.debug("Sphere", "Registering Unicity ID...");
|
|
16899
|
+
await sphere.registerNametag(options.nametag);
|
|
16900
|
+
}
|
|
16901
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses) {
|
|
16902
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16903
|
+
try {
|
|
16904
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16905
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16906
|
+
if (result.addresses.length > 0) {
|
|
16907
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16908
|
+
}
|
|
16909
|
+
} catch (err) {
|
|
16910
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16594
16911
|
}
|
|
16595
|
-
} catch (err) {
|
|
16596
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16597
16912
|
}
|
|
16598
|
-
|
|
16599
|
-
|
|
16600
|
-
|
|
16913
|
+
progress?.({ step: "complete", message: "Import complete" });
|
|
16914
|
+
logger.debug("Sphere", "Import complete");
|
|
16915
|
+
});
|
|
16601
16916
|
return sphere;
|
|
16602
16917
|
}
|
|
16603
16918
|
/**
|
|
@@ -16615,9 +16930,17 @@ var Sphere = class _Sphere {
|
|
|
16615
16930
|
*/
|
|
16616
16931
|
static async clear(options) {
|
|
16617
16932
|
const storage = options.storage;
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
await _Sphere.
|
|
16933
|
+
_Sphere.bumpClearGeneration(storage);
|
|
16934
|
+
try {
|
|
16935
|
+
await _Sphere.clearStore(storage);
|
|
16936
|
+
} finally {
|
|
16937
|
+
_Sphere.bumpClearGeneration(storage);
|
|
16938
|
+
}
|
|
16939
|
+
}
|
|
16940
|
+
static async clearStore(storage) {
|
|
16941
|
+
for (const live of _Sphere.liveOn(storage)) {
|
|
16942
|
+
logger.debug("Sphere", "Destroying Sphere instance on this storage...");
|
|
16943
|
+
await live.destroy();
|
|
16621
16944
|
logger.debug("Sphere", "Sphere instance destroyed");
|
|
16622
16945
|
}
|
|
16623
16946
|
logger.debug("Sphere", "Yielding 50ms for IDB transaction settlement...");
|
|
@@ -16677,16 +17000,64 @@ var Sphere = class _Sphere {
|
|
|
16677
17000
|
}
|
|
16678
17001
|
}
|
|
16679
17002
|
/**
|
|
16680
|
-
*
|
|
17003
|
+
* The key a provider registers under: the store it addresses, so every provider
|
|
17004
|
+
* over one dataDir/DB shares an entry. A provider that declares no store keeps a
|
|
17005
|
+
* private key, leaving custom implementations scoped by object identity.
|
|
16681
17006
|
*/
|
|
16682
|
-
static
|
|
16683
|
-
|
|
17007
|
+
static storeKeyOf(storage) {
|
|
17008
|
+
const declared = storage.backingStoreId;
|
|
17009
|
+
if (declared) return `store:${declared}`;
|
|
17010
|
+
let key = _Sphere._objectStoreKeys.get(storage);
|
|
17011
|
+
if (!key) {
|
|
17012
|
+
key = `object:${++lifecycle.objectStoreSeq}`;
|
|
17013
|
+
_Sphere._objectStoreKeys.set(storage, key);
|
|
17014
|
+
}
|
|
17015
|
+
return key;
|
|
17016
|
+
}
|
|
17017
|
+
/** The clears this store has seen. Absent means none — `clear()` creates the entry. */
|
|
17018
|
+
static clearGenerationOf(storage) {
|
|
17019
|
+
return _Sphere._clearGenerations.get(_Sphere.storeKeyOf(storage)) ?? 0;
|
|
17020
|
+
}
|
|
17021
|
+
static bumpClearGeneration(storage) {
|
|
17022
|
+
const key = _Sphere.storeKeyOf(storage);
|
|
17023
|
+
_Sphere._clearGenerations.set(key, (_Sphere._clearGenerations.get(key) ?? 0) + 1);
|
|
16684
17024
|
}
|
|
16685
17025
|
/**
|
|
16686
|
-
*
|
|
17026
|
+
* Make a fully-built Sphere reachable — or refuse, if `clear()` emptied the store
|
|
17027
|
+
* under it while it was building. Check and registration are one SYNCHRONOUS step on
|
|
17028
|
+
* purpose: split by an await, a clear could land between them and wipe a store whose
|
|
17029
|
+
* Sphere is already published. The refusal throws inside `withOwnedRegistry`, whose
|
|
17030
|
+
* teardown then destroys the half-built Sphere the caller never received.
|
|
16687
17031
|
*/
|
|
16688
|
-
static
|
|
16689
|
-
|
|
17032
|
+
static publishLive(sphere, clearGeneration) {
|
|
17033
|
+
if (_Sphere.clearGenerationOf(sphere._storage) !== clearGeneration) {
|
|
17034
|
+
throw new SphereError(
|
|
17035
|
+
"The wallet store was cleared while this wallet was initializing, so the keys and journals this init wrote are gone. Nothing was published; re-run Sphere.init() once the clear has settled.",
|
|
17036
|
+
"STORAGE_ERROR"
|
|
17037
|
+
);
|
|
17038
|
+
}
|
|
17039
|
+
_Sphere.registerLive(sphere);
|
|
17040
|
+
}
|
|
17041
|
+
/** Record a fully-built Sphere against the storage it owns. See `_liveByStorage`. */
|
|
17042
|
+
static registerLive(sphere) {
|
|
17043
|
+
const key = _Sphere.storeKeyOf(sphere._storage);
|
|
17044
|
+
let live = _Sphere._liveByStorage.get(key);
|
|
17045
|
+
if (!live) {
|
|
17046
|
+
live = /* @__PURE__ */ new Set();
|
|
17047
|
+
_Sphere._liveByStorage.set(key, live);
|
|
17048
|
+
}
|
|
17049
|
+
live.add(sphere);
|
|
17050
|
+
}
|
|
17051
|
+
static unregisterLive(sphere) {
|
|
17052
|
+
const key = _Sphere.storeKeyOf(sphere._storage);
|
|
17053
|
+
const live = _Sphere._liveByStorage.get(key);
|
|
17054
|
+
if (!live) return;
|
|
17055
|
+
live.delete(sphere);
|
|
17056
|
+
if (live.size === 0) _Sphere._liveByStorage.delete(key);
|
|
17057
|
+
}
|
|
17058
|
+
/** Snapshot — callers iterate this while destroy() mutates the underlying Set. */
|
|
17059
|
+
static liveOn(storage) {
|
|
17060
|
+
return Array.from(_Sphere._liveByStorage.get(_Sphere.storeKeyOf(storage)) ?? []);
|
|
16690
17061
|
}
|
|
16691
17062
|
/**
|
|
16692
17063
|
* Validate mnemonic using BIP39
|
|
@@ -16897,8 +17268,7 @@ var Sphere = class _Sphere {
|
|
|
16897
17268
|
password
|
|
16898
17269
|
});
|
|
16899
17270
|
if (result.success) {
|
|
16900
|
-
|
|
16901
|
-
return { success: true, sphere: sphere2, mnemonic: result.mnemonic };
|
|
17271
|
+
return { success: true, sphere: result.sphere, mnemonic: result.mnemonic };
|
|
16902
17272
|
}
|
|
16903
17273
|
if (!password && result.error?.includes("Password required")) {
|
|
16904
17274
|
return { success: false, needsPassword: true, error: result.error };
|
|
@@ -17200,7 +17570,8 @@ var Sphere = class _Sphere {
|
|
|
17200
17570
|
/**
|
|
17201
17571
|
* Import wallet from JSON backup
|
|
17202
17572
|
*
|
|
17203
|
-
* @returns
|
|
17573
|
+
* @returns `{ success, sphere?, mnemonic?, error? }`. `sphere` is the instance built
|
|
17574
|
+
* on the SUPPLIED storage — hold it, there is no global to look it up from (#766).
|
|
17204
17575
|
*
|
|
17205
17576
|
* @example
|
|
17206
17577
|
* ```ts
|
|
@@ -17241,18 +17612,18 @@ var Sphere = class _Sphere {
|
|
|
17241
17612
|
}
|
|
17242
17613
|
const basePath = data.wallet.descriptorPath ? `m/${data.wallet.descriptorPath}` : DEFAULT_BASE_PATH;
|
|
17243
17614
|
if (mnemonic) {
|
|
17244
|
-
await _Sphere.import({ ...baseOptions, mnemonic, basePath });
|
|
17245
|
-
return { success: true, mnemonic };
|
|
17615
|
+
const sphere = await _Sphere.import({ ...baseOptions, mnemonic, basePath });
|
|
17616
|
+
return { success: true, sphere, mnemonic };
|
|
17246
17617
|
}
|
|
17247
17618
|
if (masterKey) {
|
|
17248
|
-
await _Sphere.import({
|
|
17619
|
+
const sphere = await _Sphere.import({
|
|
17249
17620
|
...baseOptions,
|
|
17250
17621
|
masterKey,
|
|
17251
17622
|
chainCode: data.wallet.chainCode,
|
|
17252
17623
|
basePath,
|
|
17253
17624
|
derivationMode: data.derivationMode || (data.wallet.isBIP32 ? "bip32" : "wif_hmac")
|
|
17254
17625
|
});
|
|
17255
|
-
return { success: true };
|
|
17626
|
+
return { success: true, sphere };
|
|
17256
17627
|
}
|
|
17257
17628
|
return { success: false, error: "No mnemonic or master key in wallet data" };
|
|
17258
17629
|
} catch (e) {
|
|
@@ -17375,6 +17746,7 @@ var Sphere = class _Sphere {
|
|
|
17375
17746
|
}
|
|
17376
17747
|
if (entry.hidden === hidden) return;
|
|
17377
17748
|
entry.hidden = hidden;
|
|
17749
|
+
entry.updatedAt = Date.now();
|
|
17378
17750
|
await this.persistTrackedAddresses();
|
|
17379
17751
|
const eventType = hidden ? "address:hidden" : "address:unhidden";
|
|
17380
17752
|
this.emitEvent(eventType, { index, addressId: entry.addressId });
|
|
@@ -17403,9 +17775,6 @@ var Sphere = class _Sphere {
|
|
|
17403
17775
|
if (!this._masterKey) {
|
|
17404
17776
|
throw new SphereError("HD derivation requires master key with chain code. Cannot switch addresses.", "INVALID_CONFIG");
|
|
17405
17777
|
}
|
|
17406
|
-
if (index < 0) {
|
|
17407
|
-
throw new SphereError("Address index must be non-negative", "INVALID_CONFIG");
|
|
17408
|
-
}
|
|
17409
17778
|
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
17410
17779
|
if (newNametag && !isValidNametag(newNametag)) {
|
|
17411
17780
|
throw new SphereError("Invalid Unicity ID format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.", "VALIDATION_ERROR");
|
|
@@ -17442,7 +17811,9 @@ var Sphere = class _Sphere {
|
|
|
17442
17811
|
if (firstVisit) {
|
|
17443
17812
|
logger.debug("Sphere", `switchToAddress(${index}): creating per-address modules (lazy init)`);
|
|
17444
17813
|
this._storage.setIdentity(newIdentity);
|
|
17814
|
+
this.ensureAlive();
|
|
17445
17815
|
await this.initializeAddressModules({ index, identity: newIdentity });
|
|
17816
|
+
if (this._destroyed) await this.discardModulesBuiltDuringDestroy(index);
|
|
17446
17817
|
} else if (nametag !== this._addressModules.get(index).identity.nametag) {
|
|
17447
17818
|
this._addressModules.get(index).identity = newIdentity;
|
|
17448
17819
|
}
|
|
@@ -17453,14 +17824,18 @@ var Sphere = class _Sphere {
|
|
|
17453
17824
|
this._communications = activeModules.communications;
|
|
17454
17825
|
this._groupChat = activeModules.groupChat;
|
|
17455
17826
|
this._market = activeModules.market;
|
|
17827
|
+
this.ensureAlive();
|
|
17456
17828
|
await this.stopThenStartPaymentsV2(index, newIdentity);
|
|
17829
|
+
this.ensureAlive();
|
|
17457
17830
|
await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
|
|
17458
17831
|
this._storage.setIdentity(this._identity);
|
|
17459
17832
|
if (this._transport.setFallbackSince) {
|
|
17460
17833
|
const fallbackTs = Math.floor(Date.now() / 1e3) - 86400;
|
|
17461
17834
|
this._transport.setFallbackSince(fallbackTs);
|
|
17462
17835
|
}
|
|
17836
|
+
this.ensureAlive();
|
|
17463
17837
|
await this._transport.setIdentity(this._identity);
|
|
17838
|
+
this.ensureAlive();
|
|
17464
17839
|
if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
|
|
17465
17840
|
await this._transportMux.rebindToSharedClient();
|
|
17466
17841
|
}
|
|
@@ -17480,6 +17855,7 @@ var Sphere = class _Sphere {
|
|
|
17480
17855
|
* Runs after switchToAddress returns so L3 queries can start immediately.
|
|
17481
17856
|
*/
|
|
17482
17857
|
async postSwitchSync(index, newNametag) {
|
|
17858
|
+
this.ensureAlive();
|
|
17483
17859
|
if (!newNametag) {
|
|
17484
17860
|
await this.syncIdentityWithTransport();
|
|
17485
17861
|
}
|
|
@@ -17497,6 +17873,39 @@ var Sphere = class _Sphere {
|
|
|
17497
17873
|
* independently in background. The payments vertical is NOT created here —
|
|
17498
17874
|
* the caller starts it via stopThenStartPaymentsV2 (§7 single vertical).
|
|
17499
17875
|
*/
|
|
17876
|
+
/** Tear one address's module set down. Each address owns its own engine, so its own pool. */
|
|
17877
|
+
static destroyModuleSet(index, moduleSet) {
|
|
17878
|
+
try {
|
|
17879
|
+
moduleSet.communications.destroy();
|
|
17880
|
+
moduleSet.groupChat?.destroy();
|
|
17881
|
+
moduleSet.market?.destroy();
|
|
17882
|
+
moduleSet.tokenEngine?.dispose?.();
|
|
17883
|
+
logger.debug("Sphere", `Destroyed modules for address ${index}`);
|
|
17884
|
+
} catch (err) {
|
|
17885
|
+
logger.warn("Sphere", `Error destroying modules for address ${index}:`, err);
|
|
17886
|
+
}
|
|
17887
|
+
}
|
|
17888
|
+
/**
|
|
17889
|
+
* Undo a module set built while `destroy()` was running.
|
|
17890
|
+
*
|
|
17891
|
+
* `destroy()`'s teardown loop has already emptied `_addressModules`, so a set
|
|
17892
|
+
* registered after it would stay live and unreachable — its own engine (and worker
|
|
17893
|
+
* pool), and a transport mux `ensureTransportMux` rebuilt because teardown had just
|
|
17894
|
+
* nulled the field. Then re-raise, so the switch fails rather than reporting success
|
|
17895
|
+
* on a destroyed Sphere (#770, #772 review).
|
|
17896
|
+
*/
|
|
17897
|
+
async discardModulesBuiltDuringDestroy(index) {
|
|
17898
|
+
const built = this._addressModules.get(index);
|
|
17899
|
+
this._addressModules.delete(index);
|
|
17900
|
+
if (built) _Sphere.destroyModuleSet(index, built);
|
|
17901
|
+
if (this._transportMux) {
|
|
17902
|
+
const mux = this._transportMux;
|
|
17903
|
+
this._transportMux = null;
|
|
17904
|
+
await _Sphere.safeDisconnect("transport mux", () => mux.disconnect());
|
|
17905
|
+
}
|
|
17906
|
+
this.ensureAlive();
|
|
17907
|
+
throw new SphereError("Sphere destroyed", "NOT_INITIALIZED");
|
|
17908
|
+
}
|
|
17500
17909
|
async initializeAddressModules(spec) {
|
|
17501
17910
|
const { index, identity } = spec;
|
|
17502
17911
|
const emitEvent = this.emitEvent.bind(this);
|
|
@@ -17584,6 +17993,14 @@ var Sphere = class _Sphere {
|
|
|
17584
17993
|
const adapter = await this._transportMux.addAddress(index, identity, this._transport);
|
|
17585
17994
|
return adapter;
|
|
17586
17995
|
}
|
|
17996
|
+
/** A BIP32 child number: an integer in 0…0xffffffff. Anything else aliases an address. */
|
|
17997
|
+
static assertDerivableIndex(index) {
|
|
17998
|
+
if (isDerivableIndex(index)) return;
|
|
17999
|
+
throw new SphereError(
|
|
18000
|
+
`Address index ${String(index)} is not a BIP32 child number: it must be an integer in 0\u20260xffffffff.`,
|
|
18001
|
+
"INVALID_CONFIG"
|
|
18002
|
+
);
|
|
18003
|
+
}
|
|
17587
18004
|
/**
|
|
17588
18005
|
* Derive address at a specific index
|
|
17589
18006
|
*
|
|
@@ -17629,6 +18046,7 @@ var Sphere = class _Sphere {
|
|
|
17629
18046
|
* when _initialized is still false.
|
|
17630
18047
|
*/
|
|
17631
18048
|
_deriveAddressInternal(index, isChange = false) {
|
|
18049
|
+
_Sphere.assertDerivableIndex(index);
|
|
17632
18050
|
if (!this._masterKey) {
|
|
17633
18051
|
throw new SphereError("HD derivation requires master key with chain code", "INVALID_CONFIG");
|
|
17634
18052
|
}
|
|
@@ -17720,6 +18138,7 @@ var Sphere = class _Sphere {
|
|
|
17720
18138
|
}
|
|
17721
18139
|
if (tracked.hidden !== hidden) {
|
|
17722
18140
|
tracked.hidden = hidden;
|
|
18141
|
+
tracked.updatedAt = Date.now();
|
|
17723
18142
|
}
|
|
17724
18143
|
}
|
|
17725
18144
|
await this.persistTrackedAddresses();
|
|
@@ -18361,7 +18780,48 @@ var Sphere = class _Sphere {
|
|
|
18361
18780
|
// ===========================================================================
|
|
18362
18781
|
// Public Methods - Lifecycle
|
|
18363
18782
|
// ===========================================================================
|
|
18783
|
+
/**
|
|
18784
|
+
* Disconnect transport, storage and oracle, attempting each even if an earlier one
|
|
18785
|
+
* rejects. They are separate resources, and one failure must not skip the rest — that
|
|
18786
|
+
* is how a partial teardown leaves connections open, which is exactly the state the
|
|
18787
|
+
* failed-initialization path calls destroy() in.
|
|
18788
|
+
*/
|
|
18789
|
+
async disconnectProvidersIndependently() {
|
|
18790
|
+
const steps = [
|
|
18791
|
+
["transport", () => this._transport.disconnect()],
|
|
18792
|
+
["storage", () => this._storage.disconnect()],
|
|
18793
|
+
["oracle", () => this._oracle.disconnect()]
|
|
18794
|
+
];
|
|
18795
|
+
for (const [what, disconnect] of steps) {
|
|
18796
|
+
await _Sphere.safeDisconnect(what, disconnect);
|
|
18797
|
+
}
|
|
18798
|
+
}
|
|
18799
|
+
/** Run one teardown step, logging rather than propagating so the next one still runs. */
|
|
18800
|
+
static async safeDisconnect(what, run) {
|
|
18801
|
+
try {
|
|
18802
|
+
await run();
|
|
18803
|
+
} catch (err) {
|
|
18804
|
+
logger.warn("Sphere", `${what} disconnect failed during destroy:`, err);
|
|
18805
|
+
}
|
|
18806
|
+
}
|
|
18807
|
+
/**
|
|
18808
|
+
* Tear this Sphere down: stop the vertical, destroy the modules, disconnect the providers
|
|
18809
|
+
* and zero the key material. Idempotent by construction (every step is null-guarded) and
|
|
18810
|
+
* deliberately WITHOUT an early return on re-entry, which would change what a double call
|
|
18811
|
+
* means.
|
|
18812
|
+
*
|
|
18813
|
+
* #770: the FIRST statement flips `_destroyed` — before any await, and before this queues
|
|
18814
|
+
* its own stop on the §7 lifecycle mutex. That position is load-bearing. Every guard reads
|
|
18815
|
+
* the flag, so from that instant any switchToAddress step not yet begun refuses; and
|
|
18816
|
+
* because it flips SYNCHRONOUSLY at entry, a stop/start pair the mutex runs after this call
|
|
18817
|
+
* sees `true` and skips its start. Set it beside `_initialized` at the bottom instead and a
|
|
18818
|
+
* concurrent switch re-arms a wallet whose owner already had destroy() return: fresh
|
|
18819
|
+
* sockets, a fresh wallet-api session, a whole fresh vertical nothing will ever stop.
|
|
18820
|
+
*/
|
|
18364
18821
|
async destroy() {
|
|
18822
|
+
this._destroyed = true;
|
|
18823
|
+
this._registry?.dispose();
|
|
18824
|
+
this._registry = null;
|
|
18365
18825
|
this.cleanupProviderEventSubscriptions();
|
|
18366
18826
|
try {
|
|
18367
18827
|
await this.queuePaymentsV2Op(() => this.stopPaymentsV2Inner());
|
|
@@ -18369,28 +18829,19 @@ var Sphere = class _Sphere {
|
|
|
18369
18829
|
logger.warn("Sphere", "payments vertical stop failed during destroy:", err);
|
|
18370
18830
|
}
|
|
18371
18831
|
for (const [idx, moduleSet] of this._addressModules.entries()) {
|
|
18372
|
-
|
|
18373
|
-
moduleSet.communications.destroy();
|
|
18374
|
-
moduleSet.groupChat?.destroy();
|
|
18375
|
-
moduleSet.market?.destroy();
|
|
18376
|
-
moduleSet.tokenEngine?.dispose?.();
|
|
18377
|
-
logger.debug("Sphere", `Destroyed modules for address ${idx}`);
|
|
18378
|
-
} catch (err) {
|
|
18379
|
-
logger.warn("Sphere", `Error destroying modules for address ${idx}:`, err);
|
|
18380
|
-
}
|
|
18832
|
+
_Sphere.destroyModuleSet(idx, moduleSet);
|
|
18381
18833
|
}
|
|
18382
18834
|
this._addressModules.clear();
|
|
18383
18835
|
this._communications.destroy();
|
|
18384
18836
|
this._groupChat?.destroy();
|
|
18385
18837
|
this._market?.destroy();
|
|
18386
18838
|
if (this._transportMux) {
|
|
18387
|
-
|
|
18839
|
+
const mux = this._transportMux;
|
|
18840
|
+
await _Sphere.safeDisconnect("transport mux", () => mux.disconnect());
|
|
18388
18841
|
this._transportMux = null;
|
|
18389
18842
|
}
|
|
18390
18843
|
this._tokenEngine?.dispose?.();
|
|
18391
|
-
await this.
|
|
18392
|
-
await this._storage.disconnect();
|
|
18393
|
-
await this._oracle.disconnect();
|
|
18844
|
+
await this.disconnectProvidersIndependently();
|
|
18394
18845
|
this._initialized = false;
|
|
18395
18846
|
this._trackedAddressesLoaded = false;
|
|
18396
18847
|
this._identity = null;
|
|
@@ -18402,9 +18853,7 @@ var Sphere = class _Sphere {
|
|
|
18402
18853
|
this._addressNametags.clear();
|
|
18403
18854
|
this._disabledProviders.clear();
|
|
18404
18855
|
this.eventHandlers.clear();
|
|
18405
|
-
|
|
18406
|
-
_Sphere.instance = null;
|
|
18407
|
-
}
|
|
18856
|
+
_Sphere.unregisterLive(this);
|
|
18408
18857
|
}
|
|
18409
18858
|
// ===========================================================================
|
|
18410
18859
|
// Private: Storage
|
|
@@ -18741,10 +19190,21 @@ var Sphere = class _Sphere {
|
|
|
18741
19190
|
);
|
|
18742
19191
|
return run;
|
|
18743
19192
|
}
|
|
18744
|
-
/**
|
|
19193
|
+
/**
|
|
19194
|
+
* Switch/boot: stop whatever runs, then start `index`'s vertical — atomically vs other
|
|
19195
|
+
* lifecycle ops.
|
|
19196
|
+
*
|
|
19197
|
+
* #770: the destroyed check between the two halves settles the FACADE vector on its own.
|
|
19198
|
+
* `_destroyed` flips synchronously at destroy() entry, so any closure that BEGINS executing
|
|
19199
|
+
* after destroy() was called sees `true`, and any closure already past the check has
|
|
19200
|
+
* facade.start() in flight — which destroy()'s own queued stop is necessarily ordered
|
|
19201
|
+
* after. The TRANSPORT vector is not on this mutex at all; the ensureAlive() calls in
|
|
19202
|
+
* switchToAddress are what cover it.
|
|
19203
|
+
*/
|
|
18745
19204
|
stopThenStartPaymentsV2(index, identity) {
|
|
18746
19205
|
return this.queuePaymentsV2Op(async () => {
|
|
18747
19206
|
await this.stopPaymentsV2Inner();
|
|
19207
|
+
if (this._destroyed) return;
|
|
18748
19208
|
await this.startPaymentsV2Inner(index, identity);
|
|
18749
19209
|
});
|
|
18750
19210
|
}
|
|
@@ -18776,6 +19236,7 @@ var Sphere = class _Sphere {
|
|
|
18776
19236
|
host: {
|
|
18777
19237
|
storage: this._storage,
|
|
18778
19238
|
price: this._priceProvider,
|
|
19239
|
+
registry: this._registry ?? TokenRegistry.getInstance(),
|
|
18779
19240
|
emit: (event, payload) => this.emitEvent(event, payload),
|
|
18780
19241
|
resolvePeer: (identifier) => this._transport.resolve?.(identifier) ?? Promise.resolve(null),
|
|
18781
19242
|
// Scoped to THIS facade's address, never "whichever is active":
|
|
@@ -18796,7 +19257,18 @@ var Sphere = class _Sphere {
|
|
|
18796
19257
|
this._paymentsV2Active = null;
|
|
18797
19258
|
await active.facade.stop();
|
|
18798
19259
|
}
|
|
19260
|
+
/**
|
|
19261
|
+
* Refuse once destroy() has STARTED (#770). `_initialized` cannot carry this: destroy()
|
|
19262
|
+
* clears it last, so every teardown step is a window in which a concurrent call still reads
|
|
19263
|
+
* a ready Sphere and re-arms it.
|
|
19264
|
+
*/
|
|
19265
|
+
ensureAlive() {
|
|
19266
|
+
if (this._destroyed) {
|
|
19267
|
+
throw new SphereError("Sphere destroyed", "NOT_INITIALIZED");
|
|
19268
|
+
}
|
|
19269
|
+
}
|
|
18799
19270
|
ensureReady() {
|
|
19271
|
+
this.ensureAlive();
|
|
18800
19272
|
if (!this._initialized) {
|
|
18801
19273
|
throw new SphereError("Sphere not initialized", "NOT_INITIALIZED");
|
|
18802
19274
|
}
|
|
@@ -18846,7 +19318,6 @@ var createSphere = Sphere.create.bind(Sphere);
|
|
|
18846
19318
|
var loadSphere = Sphere.load.bind(Sphere);
|
|
18847
19319
|
var importSphere = Sphere.import.bind(Sphere);
|
|
18848
19320
|
var initSphere = Sphere.init.bind(Sphere);
|
|
18849
|
-
var getSphere = Sphere.getInstance.bind(Sphere);
|
|
18850
19321
|
var sphereExists = Sphere.exists.bind(Sphere);
|
|
18851
19322
|
|
|
18852
19323
|
// core/currency.ts
|
|
@@ -19018,28 +19489,59 @@ async function checkWebSocket(url, timeoutMs) {
|
|
|
19018
19489
|
};
|
|
19019
19490
|
});
|
|
19020
19491
|
}
|
|
19492
|
+
var HEALTH_PROBE_STATE_ID = "00".repeat(32);
|
|
19493
|
+
function readBlockNumber(body) {
|
|
19494
|
+
if (typeof body !== "object" || body === null) return null;
|
|
19495
|
+
const result = body.result;
|
|
19496
|
+
if (typeof result !== "object" || result === null) return null;
|
|
19497
|
+
const n = result.blockNumber;
|
|
19498
|
+
if (typeof n === "string") return /^\d+$/.test(n) ? n : null;
|
|
19499
|
+
if (typeof n === "number") return Number.isInteger(n) && n >= 0 ? String(n) : null;
|
|
19500
|
+
return null;
|
|
19501
|
+
}
|
|
19502
|
+
function readRpcError(body) {
|
|
19503
|
+
if (typeof body !== "object" || body === null) return null;
|
|
19504
|
+
const err = body.error;
|
|
19505
|
+
if (typeof err === "string") return err;
|
|
19506
|
+
if (typeof err === "object" && err !== null) {
|
|
19507
|
+
const message = err.message;
|
|
19508
|
+
if (typeof message === "string") return message;
|
|
19509
|
+
return JSON.stringify(err);
|
|
19510
|
+
}
|
|
19511
|
+
return null;
|
|
19512
|
+
}
|
|
19513
|
+
function oracleVerdict(url, responseTimeMs, response, body) {
|
|
19514
|
+
if (readBlockNumber(body) !== null) return { healthy: true, url, responseTimeMs };
|
|
19515
|
+
const rpcError = readRpcError(body);
|
|
19516
|
+
return {
|
|
19517
|
+
healthy: false,
|
|
19518
|
+
url,
|
|
19519
|
+
responseTimeMs,
|
|
19520
|
+
error: rpcError ?? (response.ok ? "aggregator answered without a block height" : `HTTP ${String(response.status)} ${response.statusText}`)
|
|
19521
|
+
};
|
|
19522
|
+
}
|
|
19021
19523
|
async function checkOracle(url, timeoutMs) {
|
|
19022
19524
|
const startTime = Date.now();
|
|
19525
|
+
const controller = new AbortController();
|
|
19526
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
19023
19527
|
try {
|
|
19024
|
-
const controller = new AbortController();
|
|
19025
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
19026
19528
|
const response = await fetch(url, {
|
|
19027
19529
|
method: "POST",
|
|
19028
19530
|
headers: { "Content-Type": "application/json" },
|
|
19029
|
-
body: JSON.stringify({
|
|
19531
|
+
body: JSON.stringify({
|
|
19532
|
+
jsonrpc: "2.0",
|
|
19533
|
+
id: 1,
|
|
19534
|
+
method: "get_block_height",
|
|
19535
|
+
params: { stateId: HEALTH_PROBE_STATE_ID }
|
|
19536
|
+
}),
|
|
19030
19537
|
signal: controller.signal
|
|
19031
19538
|
});
|
|
19032
|
-
clearTimeout(timer);
|
|
19033
19539
|
const responseTimeMs = Date.now() - startTime;
|
|
19034
|
-
|
|
19035
|
-
|
|
19540
|
+
const body = await response.json().catch(() => null);
|
|
19541
|
+
if (controller.signal.aborted) {
|
|
19542
|
+
return { healthy: false, url, responseTimeMs, error: `Connection timeout after ${timeoutMs}ms` };
|
|
19036
19543
|
}
|
|
19037
|
-
return
|
|
19038
|
-
healthy: false,
|
|
19039
|
-
url,
|
|
19040
|
-
responseTimeMs,
|
|
19041
|
-
error: `HTTP ${response.status} ${response.statusText}`
|
|
19042
|
-
};
|
|
19544
|
+
return oracleVerdict(url, responseTimeMs, response, body);
|
|
19043
19545
|
} catch (err) {
|
|
19044
19546
|
return {
|
|
19045
19547
|
healthy: false,
|
|
@@ -19047,6 +19549,8 @@ async function checkOracle(url, timeoutMs) {
|
|
|
19047
19549
|
responseTimeMs: null,
|
|
19048
19550
|
error: err instanceof Error ? err.name === "AbortError" ? `Connection timeout after ${timeoutMs}ms` : err.message : String(err)
|
|
19049
19551
|
};
|
|
19552
|
+
} finally {
|
|
19553
|
+
clearTimeout(timer);
|
|
19050
19554
|
}
|
|
19051
19555
|
}
|
|
19052
19556
|
async function runCustomCheck(name, checkFn, timeoutMs) {
|
|
@@ -19446,7 +19950,6 @@ function addressesMatch(a, b) {
|
|
|
19446
19950
|
getCoinIdByName,
|
|
19447
19951
|
getCoinIdBySymbol,
|
|
19448
19952
|
getPublicKey,
|
|
19449
|
-
getSphere,
|
|
19450
19953
|
getTokenDecimals,
|
|
19451
19954
|
getTokenDefinition,
|
|
19452
19955
|
getTokenIconUrl,
|