@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/core/index.cjs
CHANGED
|
@@ -81,7 +81,6 @@ __export(core_exports, {
|
|
|
81
81
|
generateMnemonic: () => generateMnemonic2,
|
|
82
82
|
generateRandomKey: () => generateRandomKey,
|
|
83
83
|
getPublicKey: () => getPublicKey,
|
|
84
|
-
getSphere: () => getSphere,
|
|
85
84
|
hash160: () => hash160,
|
|
86
85
|
hashSignMessage: () => hashSignMessage,
|
|
87
86
|
hexToBytes: () => hexToBytes4,
|
|
@@ -209,6 +208,58 @@ var logger = {
|
|
|
209
208
|
}
|
|
210
209
|
};
|
|
211
210
|
|
|
211
|
+
// core/global-cell.ts
|
|
212
|
+
var CELLS_KEY = "__sphere_sdk_cells_v1__";
|
|
213
|
+
var bundleLocalBag = null;
|
|
214
|
+
function isObject(value) {
|
|
215
|
+
return typeof value === "object" && value !== null;
|
|
216
|
+
}
|
|
217
|
+
function installBag(host, fresh) {
|
|
218
|
+
try {
|
|
219
|
+
Object.defineProperty(host, CELLS_KEY, {
|
|
220
|
+
value: fresh,
|
|
221
|
+
writable: true,
|
|
222
|
+
configurable: true,
|
|
223
|
+
enumerable: false
|
|
224
|
+
});
|
|
225
|
+
return host[CELLS_KEY] === fresh;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function cellBag() {
|
|
231
|
+
if (bundleLocalBag) return bundleLocalBag;
|
|
232
|
+
const host = globalThis;
|
|
233
|
+
const found = host[CELLS_KEY];
|
|
234
|
+
if (isObject(found)) return found;
|
|
235
|
+
const fresh = {};
|
|
236
|
+
if (!installBag(host, fresh)) bundleLocalBag = fresh;
|
|
237
|
+
return fresh;
|
|
238
|
+
}
|
|
239
|
+
function accepted(intact, candidate) {
|
|
240
|
+
try {
|
|
241
|
+
return intact(candidate);
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function put(bag, name, cell) {
|
|
247
|
+
try {
|
|
248
|
+
bag[name] = cell;
|
|
249
|
+
if (bag[name] === cell) return;
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
if (bag !== bundleLocalBag) bundleLocalBag = { [name]: cell };
|
|
253
|
+
}
|
|
254
|
+
function sharedCell(name, create, intact) {
|
|
255
|
+
const bag = cellBag();
|
|
256
|
+
const found = bag[name];
|
|
257
|
+
if (isObject(found) && accepted(intact, found)) return found;
|
|
258
|
+
const fresh = create();
|
|
259
|
+
put(bag, name, fresh);
|
|
260
|
+
return fresh;
|
|
261
|
+
}
|
|
262
|
+
|
|
212
263
|
// core/errors.ts
|
|
213
264
|
var SphereError = class extends Error {
|
|
214
265
|
code;
|
|
@@ -273,6 +324,11 @@ function isSphereError(err) {
|
|
|
273
324
|
return err instanceof SphereError;
|
|
274
325
|
}
|
|
275
326
|
|
|
327
|
+
// storage/tracked-addresses.ts
|
|
328
|
+
function isDerivableIndex(value) {
|
|
329
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
|
|
330
|
+
}
|
|
331
|
+
|
|
276
332
|
// transport/MultiAddressTransportMux.ts
|
|
277
333
|
var import_buffer2 = require("buffer");
|
|
278
334
|
var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
|
|
@@ -801,7 +857,6 @@ function getAddressId(directAddress) {
|
|
|
801
857
|
return `DIRECT_${first}_${last}`;
|
|
802
858
|
}
|
|
803
859
|
var DEFAULT_NOSTR_RELAYS = [
|
|
804
|
-
"wss://relay.unicity.network",
|
|
805
860
|
"wss://relay.damus.io",
|
|
806
861
|
"wss://nos.lol",
|
|
807
862
|
"wss://relay.nostr.band"
|
|
@@ -848,11 +903,8 @@ var NIP29_KINDS = {
|
|
|
848
903
|
/** Relay-signed group roles */
|
|
849
904
|
GROUP_ROLES: 39003
|
|
850
905
|
};
|
|
851
|
-
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
852
|
-
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
853
906
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
854
907
|
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
855
|
-
var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
|
|
856
908
|
var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
|
|
857
909
|
var TEST_NOSTR_RELAYS = [
|
|
858
910
|
"wss://nostr-relay.testnet.unicity.network"
|
|
@@ -863,10 +915,19 @@ var DEFAULT_GROUP_RELAYS = [
|
|
|
863
915
|
var NETWORKS = {
|
|
864
916
|
mainnet: {
|
|
865
917
|
name: "Mainnet",
|
|
866
|
-
|
|
867
|
-
|
|
918
|
+
networkId: 1,
|
|
919
|
+
// v3 state-transition gateway (networkId 1 comes from the trust base). apiKey is env-injected;
|
|
920
|
+
// unlike testnet2's, a mainnet gateway key is a SECRET — never commit one.
|
|
921
|
+
aggregatorUrl: "https://gateway.mainnet.unicity.network",
|
|
922
|
+
// Mainnet has no relay of its own yet — it shares the testnet relay until one is stood up.
|
|
923
|
+
// Consequence while shared: nametag bindings for both networks live in ONE namespace, and
|
|
924
|
+
// since bindings carry no network the cross-network recipient guard can only signal (#734).
|
|
925
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
868
926
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
869
|
-
|
|
927
|
+
// Published, but currently only the non-fungible base token type — no fungible
|
|
928
|
+
// coins yet, so those still miss the registry (decimals 0, symbol falls back to
|
|
929
|
+
// six hex chars). Presentation only: the money path treats coinId as opaque bytes.
|
|
930
|
+
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json"
|
|
870
931
|
},
|
|
871
932
|
// v1 cutover: 'testnet' now POINTS AT TESTNET2 (the v2 gateway network). The
|
|
872
933
|
// old goggregator testnet spoke the removed v1 protocol — a v2 engine cannot
|
|
@@ -890,19 +951,10 @@ var NETWORKS = {
|
|
|
890
951
|
// reuse testnet infra (shared relays/ipfs)
|
|
891
952
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
892
953
|
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
|
|
893
|
-
},
|
|
894
|
-
// NOTE: mainnet/dev still point at v1-era aggregators. The v2 engine cannot
|
|
895
|
-
// operate against them until their gateways are cut over to the v2 protocol —
|
|
896
|
-
// wallet operations on these networks fail loudly (AGGREGATOR_ERROR) until then.
|
|
897
|
-
dev: {
|
|
898
|
-
name: "Development",
|
|
899
|
-
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
900
|
-
nostrRelays: TEST_NOSTR_RELAYS,
|
|
901
|
-
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
902
|
-
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
903
954
|
}
|
|
904
955
|
};
|
|
905
956
|
var SPHERE_NETWORKS = {
|
|
957
|
+
mainnet: { id: NETWORKS.mainnet.networkId, name: "mainnet" },
|
|
906
958
|
testnet2: { id: NETWORKS.testnet2.networkId, name: "testnet2" }
|
|
907
959
|
};
|
|
908
960
|
var TIMEOUTS = {
|
|
@@ -1078,14 +1130,10 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1078
1130
|
});
|
|
1079
1131
|
}
|
|
1080
1132
|
});
|
|
1081
|
-
await
|
|
1082
|
-
this.nostrClient
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1086
|
-
)), this.config.timeout)
|
|
1087
|
-
)
|
|
1088
|
-
]);
|
|
1133
|
+
await this.connectWithDeadline(
|
|
1134
|
+
this.nostrClient,
|
|
1135
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1136
|
+
);
|
|
1089
1137
|
if (!this.nostrClient.isConnected()) {
|
|
1090
1138
|
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
1091
1139
|
}
|
|
@@ -1100,6 +1148,27 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1100
1148
|
throw error;
|
|
1101
1149
|
}
|
|
1102
1150
|
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Race one client's relay connect against the configured timeout.
|
|
1153
|
+
*
|
|
1154
|
+
* The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
|
|
1155
|
+
* cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
|
|
1156
|
+
* for the whole `config.timeout` after the call has already returned (and
|
|
1157
|
+
* then rejects a promise nobody is listening to any more).
|
|
1158
|
+
*/
|
|
1159
|
+
async connectWithDeadline(client, timeoutMessage) {
|
|
1160
|
+
let timer;
|
|
1161
|
+
try {
|
|
1162
|
+
await Promise.race([
|
|
1163
|
+
client.connect(...this.config.relays),
|
|
1164
|
+
new Promise((_, reject) => {
|
|
1165
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), this.config.timeout);
|
|
1166
|
+
})
|
|
1167
|
+
]);
|
|
1168
|
+
} finally {
|
|
1169
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1103
1172
|
async disconnect() {
|
|
1104
1173
|
if (this.nostrClient) {
|
|
1105
1174
|
this.nostrClient.disconnect();
|
|
@@ -1218,19 +1287,22 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1218
1287
|
// TransportProvider Implementation
|
|
1219
1288
|
// ===========================================================================
|
|
1220
1289
|
async setIdentity(identity) {
|
|
1221
|
-
this.identity = identity;
|
|
1222
|
-
this.processedEventIds.clear();
|
|
1223
|
-
this.lastEventTs = 0;
|
|
1224
|
-
this.lastDmEventTs = 0;
|
|
1225
|
-
this.fallbackDmSince = null;
|
|
1226
1290
|
const secretKey = import_buffer.Buffer.from(identity.privateKey, "hex");
|
|
1227
|
-
|
|
1228
|
-
const nostrPubkey =
|
|
1229
|
-
logger.debug("Nostr", "Identity
|
|
1291
|
+
const nextKeyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(secretKey);
|
|
1292
|
+
const nostrPubkey = nextKeyManager.getPublicKeyHex();
|
|
1293
|
+
logger.debug("Nostr", "Identity staged, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
|
|
1294
|
+
const applyStagedIdentity = () => {
|
|
1295
|
+
this.identity = identity;
|
|
1296
|
+
this.keyManager = nextKeyManager;
|
|
1297
|
+
this.processedEventIds.clear();
|
|
1298
|
+
this.lastEventTs = 0;
|
|
1299
|
+
this.lastDmEventTs = 0;
|
|
1300
|
+
this.fallbackDmSince = null;
|
|
1301
|
+
};
|
|
1230
1302
|
if (this.nostrClient && this.status === "connected") {
|
|
1231
1303
|
logger.debug("Nostr", "Identity changed while connected - recreating NostrClient");
|
|
1232
1304
|
const oldClient = this.nostrClient;
|
|
1233
|
-
|
|
1305
|
+
const nextClient = new import_nostr_js_sdk.NostrClient(nextKeyManager, {
|
|
1234
1306
|
autoReconnect: this.config.autoReconnect,
|
|
1235
1307
|
reconnectIntervalMs: this.config.reconnectDelay,
|
|
1236
1308
|
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
@@ -1239,7 +1311,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1239
1311
|
queryTimeoutMs: 2e4
|
|
1240
1312
|
// see same option above for rationale
|
|
1241
1313
|
});
|
|
1242
|
-
|
|
1314
|
+
nextClient.addConnectionListener({
|
|
1243
1315
|
onConnect: (url) => {
|
|
1244
1316
|
logger.debug("Nostr", "NostrClient connected to relay:", url);
|
|
1245
1317
|
},
|
|
@@ -1253,18 +1325,43 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1253
1325
|
logger.debug("Nostr", "NostrClient reconnected to relay:", url);
|
|
1254
1326
|
}
|
|
1255
1327
|
});
|
|
1256
|
-
|
|
1257
|
-
this.
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1328
|
+
try {
|
|
1329
|
+
await this.connectWithDeadline(
|
|
1330
|
+
nextClient,
|
|
1331
|
+
`Transport reconnection timed out after ${this.config.timeout}ms`
|
|
1332
|
+
);
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
try {
|
|
1335
|
+
nextClient.disconnect();
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
throw error;
|
|
1339
|
+
}
|
|
1340
|
+
if (this.nostrClient !== oldClient || this.status !== "connected") {
|
|
1341
|
+
try {
|
|
1342
|
+
nextClient.disconnect();
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
throw new SphereError(
|
|
1346
|
+
"Transport client changed or left the connected state during setIdentity - identity not applied",
|
|
1347
|
+
"TRANSPORT_ERROR"
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
this.nostrClient = nextClient;
|
|
1351
|
+
applyStagedIdentity();
|
|
1352
|
+
try {
|
|
1353
|
+
await this.subscribeToEvents();
|
|
1354
|
+
} finally {
|
|
1355
|
+
try {
|
|
1356
|
+
oldClient.disconnect();
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1266
1360
|
} else if (this.isConnected()) {
|
|
1361
|
+
applyStagedIdentity();
|
|
1267
1362
|
await this.subscribeToEvents();
|
|
1363
|
+
} else {
|
|
1364
|
+
applyStagedIdentity();
|
|
1268
1365
|
}
|
|
1269
1366
|
}
|
|
1270
1367
|
setFallbackSince(sinceSeconds) {
|
|
@@ -8125,6 +8222,12 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8125
8222
|
lastRefreshAt = 0;
|
|
8126
8223
|
refreshPromise = null;
|
|
8127
8224
|
initialLoadPromise = null;
|
|
8225
|
+
/** Bumped on every remoteUrl change; loads started in an older generation are discarded. */
|
|
8226
|
+
generation = 0;
|
|
8227
|
+
/** Set by dispose(). A disposed registry starts no work and applies no late result. */
|
|
8228
|
+
disposed = false;
|
|
8229
|
+
/** Cancels the in-flight fetch (and its abort timer) when the registry is disposed. */
|
|
8230
|
+
inFlight = null;
|
|
8128
8231
|
constructor() {
|
|
8129
8232
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
8130
8233
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
@@ -8153,27 +8256,90 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8153
8256
|
* @param options.autoRefresh - Start auto-refresh immediately (default: true)
|
|
8154
8257
|
*/
|
|
8155
8258
|
static configure(options) {
|
|
8156
|
-
|
|
8259
|
+
_TokenRegistry.getInstance().applyConfig(options);
|
|
8260
|
+
}
|
|
8261
|
+
/**
|
|
8262
|
+
* Create an INDEPENDENT registry, not the process-global singleton.
|
|
8263
|
+
*
|
|
8264
|
+
* One Sphere owns one of these, so two Spheres on different networks cannot wipe each
|
|
8265
|
+
* other's definitions — the singleton's `configure()` reaches into whatever instance
|
|
8266
|
+
* exists and repoints it, which is how a mainnet init silently retargeted a live
|
|
8267
|
+
* testnet2 wallet's decimals. Dispose it when the owner is destroyed.
|
|
8268
|
+
*/
|
|
8269
|
+
static create(options) {
|
|
8270
|
+
const registry = new _TokenRegistry();
|
|
8271
|
+
registry.applyConfig(options);
|
|
8272
|
+
return registry;
|
|
8273
|
+
}
|
|
8274
|
+
/** The body of configure(), on the instance, so owned registries share it exactly. */
|
|
8275
|
+
applyConfig(options) {
|
|
8276
|
+
if (this.disposed) return;
|
|
8157
8277
|
if (options.remoteUrl !== void 0) {
|
|
8158
|
-
|
|
8278
|
+
const previous = this.remoteUrl;
|
|
8279
|
+
if (previous && previous !== options.remoteUrl) {
|
|
8280
|
+
this.applyDefinitions([]);
|
|
8281
|
+
this.lastRefreshAt = 0;
|
|
8282
|
+
this.generation++;
|
|
8283
|
+
this.refreshPromise = null;
|
|
8284
|
+
}
|
|
8285
|
+
this.remoteUrl = options.remoteUrl;
|
|
8159
8286
|
}
|
|
8160
8287
|
if (options.storage !== void 0) {
|
|
8161
|
-
|
|
8288
|
+
this.storage = options.storage;
|
|
8162
8289
|
}
|
|
8163
8290
|
if (options.refreshIntervalMs !== void 0) {
|
|
8164
|
-
|
|
8291
|
+
this.refreshIntervalMs = options.refreshIntervalMs;
|
|
8165
8292
|
}
|
|
8166
8293
|
const autoRefresh = options.autoRefresh ?? true;
|
|
8167
|
-
|
|
8294
|
+
this.initialLoadPromise = this.performInitialLoad(autoRefresh);
|
|
8295
|
+
}
|
|
8296
|
+
/**
|
|
8297
|
+
* Stop this registry for good: no timer, no late apply, no late cache write.
|
|
8298
|
+
*
|
|
8299
|
+
* Sphere.destroy() calls this on the registry it owns. Without it a discarded Sphere
|
|
8300
|
+
* leaves an hourly fetch running forever — nothing in this file calls unref(), so in
|
|
8301
|
+
* Node it also keeps the event loop alive.
|
|
8302
|
+
*/
|
|
8303
|
+
dispose() {
|
|
8304
|
+
if (this.disposed) return;
|
|
8305
|
+
this.disposed = true;
|
|
8306
|
+
this.stopAutoRefresh();
|
|
8307
|
+
this.inFlight?.abort();
|
|
8308
|
+
this.inFlight = null;
|
|
8309
|
+
this.generation++;
|
|
8310
|
+
this.refreshPromise = null;
|
|
8311
|
+
this.initialLoadPromise = null;
|
|
8312
|
+
}
|
|
8313
|
+
/** Whether dispose() has been called. Reads still work; they are simply frozen. */
|
|
8314
|
+
get isDisposed() {
|
|
8315
|
+
return this.disposed;
|
|
8316
|
+
}
|
|
8317
|
+
/** Per-instance readiness — the same contract as the static, for an owned registry. */
|
|
8318
|
+
async waitForReady(timeoutMs = 1e4) {
|
|
8319
|
+
if (!this.initialLoadPromise) {
|
|
8320
|
+
return this.definitionsById.size > 0;
|
|
8321
|
+
}
|
|
8322
|
+
if (timeoutMs <= 0) {
|
|
8323
|
+
return this.initialLoadPromise;
|
|
8324
|
+
}
|
|
8325
|
+
let timer;
|
|
8326
|
+
try {
|
|
8327
|
+
return await Promise.race([
|
|
8328
|
+
this.initialLoadPromise,
|
|
8329
|
+
new Promise((resolve) => {
|
|
8330
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
8331
|
+
})
|
|
8332
|
+
]);
|
|
8333
|
+
} finally {
|
|
8334
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8335
|
+
}
|
|
8168
8336
|
}
|
|
8169
8337
|
/**
|
|
8170
8338
|
* Reset the singleton instance (useful for testing).
|
|
8171
8339
|
* Stops auto-refresh if running.
|
|
8172
8340
|
*/
|
|
8173
8341
|
static resetInstance() {
|
|
8174
|
-
|
|
8175
|
-
_TokenRegistry.instance.stopAutoRefresh();
|
|
8176
|
-
}
|
|
8342
|
+
_TokenRegistry.instance?.dispose();
|
|
8177
8343
|
_TokenRegistry.instance = null;
|
|
8178
8344
|
}
|
|
8179
8345
|
/**
|
|
@@ -8189,17 +8355,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8189
8355
|
* @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
|
|
8190
8356
|
*/
|
|
8191
8357
|
static async waitForReady(timeoutMs = 1e4) {
|
|
8192
|
-
|
|
8193
|
-
if (!instance.initialLoadPromise) {
|
|
8194
|
-
return instance.definitionsById.size > 0;
|
|
8195
|
-
}
|
|
8196
|
-
if (timeoutMs <= 0) {
|
|
8197
|
-
return instance.initialLoadPromise;
|
|
8198
|
-
}
|
|
8199
|
-
return Promise.race([
|
|
8200
|
-
instance.initialLoadPromise,
|
|
8201
|
-
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
|
|
8202
|
-
]);
|
|
8358
|
+
return _TokenRegistry.getInstance().waitForReady(timeoutMs);
|
|
8203
8359
|
}
|
|
8204
8360
|
// ===========================================================================
|
|
8205
8361
|
// Initial Load
|
|
@@ -8209,6 +8365,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8209
8365
|
* After initial data is available, start periodic auto-refresh if configured.
|
|
8210
8366
|
*/
|
|
8211
8367
|
async performInitialLoad(autoRefresh) {
|
|
8368
|
+
if (this.disposed) return false;
|
|
8212
8369
|
let loaded = false;
|
|
8213
8370
|
if (this.storage) {
|
|
8214
8371
|
loaded = await this.loadFromCache();
|
|
@@ -8221,6 +8378,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8221
8378
|
}
|
|
8222
8379
|
if (autoRefresh && this.remoteUrl) {
|
|
8223
8380
|
loaded = await this.refreshFromRemote();
|
|
8381
|
+
if (this.disposed) return loaded;
|
|
8224
8382
|
this.stopAutoRefresh();
|
|
8225
8383
|
this.refreshTimer = setInterval(() => {
|
|
8226
8384
|
this.refreshFromRemote();
|
|
@@ -8251,6 +8409,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8251
8409
|
*/
|
|
8252
8410
|
async loadFromCache() {
|
|
8253
8411
|
if (!this.storage) return false;
|
|
8412
|
+
const gen = this.generation;
|
|
8254
8413
|
try {
|
|
8255
8414
|
const { data: dataKey, ts: tsKey } = this.cacheKeys();
|
|
8256
8415
|
const [cached, cachedTs] = await Promise.all([
|
|
@@ -8265,6 +8424,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8265
8424
|
if (this.lastRefreshAt > ts) return false;
|
|
8266
8425
|
const data = JSON.parse(cached);
|
|
8267
8426
|
if (!this.isValidDefinitionsArray(data)) return false;
|
|
8427
|
+
if (gen !== this.generation) return false;
|
|
8268
8428
|
this.applyDefinitions(data);
|
|
8269
8429
|
this.lastRefreshAt = ts;
|
|
8270
8430
|
return true;
|
|
@@ -8319,32 +8479,51 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8319
8479
|
* Concurrent calls are deduplicated — only one fetch runs at a time.
|
|
8320
8480
|
*/
|
|
8321
8481
|
async refreshFromRemote() {
|
|
8322
|
-
if (!this.remoteUrl) {
|
|
8482
|
+
if (this.disposed || !this.remoteUrl) {
|
|
8323
8483
|
return false;
|
|
8324
8484
|
}
|
|
8325
8485
|
if (this.refreshPromise) {
|
|
8326
8486
|
return this.refreshPromise;
|
|
8327
8487
|
}
|
|
8328
|
-
|
|
8488
|
+
const attempt = this.doRefresh();
|
|
8489
|
+
this.refreshPromise = attempt;
|
|
8329
8490
|
try {
|
|
8330
|
-
return await
|
|
8491
|
+
return await attempt;
|
|
8331
8492
|
} finally {
|
|
8332
|
-
this.refreshPromise = null;
|
|
8493
|
+
if (this.refreshPromise === attempt) this.refreshPromise = null;
|
|
8333
8494
|
}
|
|
8334
8495
|
}
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8496
|
+
/**
|
|
8497
|
+
* Fetch with a bounded timeout, exposing a cancel hook to dispose().
|
|
8498
|
+
*
|
|
8499
|
+
* Both the request and its abort timer keep Node's event loop alive for the full
|
|
8500
|
+
* FETCH_TIMEOUT_MS, so a registry disposed mid-flight must cancel them rather than
|
|
8501
|
+
* merely ignore the result.
|
|
8502
|
+
*/
|
|
8503
|
+
async fetchCancellable(url) {
|
|
8504
|
+
const controller = new AbortController();
|
|
8505
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
8506
|
+
this.inFlight = {
|
|
8507
|
+
abort: () => {
|
|
8346
8508
|
clearTimeout(timer);
|
|
8509
|
+
controller.abort();
|
|
8347
8510
|
}
|
|
8511
|
+
};
|
|
8512
|
+
try {
|
|
8513
|
+
return await fetch(url, {
|
|
8514
|
+
headers: { Accept: "application/json" },
|
|
8515
|
+
signal: controller.signal
|
|
8516
|
+
});
|
|
8517
|
+
} finally {
|
|
8518
|
+
clearTimeout(timer);
|
|
8519
|
+
this.inFlight = null;
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8522
|
+
async doRefresh() {
|
|
8523
|
+
const url = this.remoteUrl;
|
|
8524
|
+
const gen = this.generation;
|
|
8525
|
+
try {
|
|
8526
|
+
const response = await this.fetchCancellable(url);
|
|
8348
8527
|
if (!response.ok) {
|
|
8349
8528
|
logger.warn("TokenRegistry", `Remote fetch failed: HTTP ${response.status} ${response.statusText}`);
|
|
8350
8529
|
return false;
|
|
@@ -8354,6 +8533,10 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8354
8533
|
logger.warn("TokenRegistry", "Remote data is not a valid token definitions array");
|
|
8355
8534
|
return false;
|
|
8356
8535
|
}
|
|
8536
|
+
if (gen !== this.generation) {
|
|
8537
|
+
logger.debug("TokenRegistry", `Discarding stale refresh for ${url} (network changed)`);
|
|
8538
|
+
return false;
|
|
8539
|
+
}
|
|
8357
8540
|
const definitions = data;
|
|
8358
8541
|
this.applyDefinitions(definitions);
|
|
8359
8542
|
this.lastRefreshAt = Date.now();
|
|
@@ -8370,6 +8553,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8370
8553
|
* Does an immediate fetch, then repeats at the configured interval.
|
|
8371
8554
|
*/
|
|
8372
8555
|
startAutoRefresh(intervalMs) {
|
|
8556
|
+
if (this.disposed) return;
|
|
8373
8557
|
this.stopAutoRefresh();
|
|
8374
8558
|
if (intervalMs !== void 0) {
|
|
8375
8559
|
this.refreshIntervalMs = intervalMs;
|
|
@@ -10199,12 +10383,58 @@ var SphereTokenEngine = class {
|
|
|
10199
10383
|
|
|
10200
10384
|
// token-engine/factory.ts
|
|
10201
10385
|
var DEFAULT_VERIFICATION_POOL_SIZE = 4;
|
|
10386
|
+
function disposedError() {
|
|
10387
|
+
return new SphereError(
|
|
10388
|
+
"Verification worker pool disposed \u2014 the in-flight verification was cancelled",
|
|
10389
|
+
"MODULE_DESTROYED"
|
|
10390
|
+
);
|
|
10391
|
+
}
|
|
10202
10392
|
var ConfiguredWorkerTokenVerifier = class extends import_WorkerTokenVerifier.WorkerTokenVerifier {
|
|
10203
10393
|
constructor(spawn, poolSize) {
|
|
10204
10394
|
super(poolSize);
|
|
10205
10395
|
this.spawn = spawn;
|
|
10206
10396
|
}
|
|
10397
|
+
disposed = false;
|
|
10398
|
+
/** One rejector per IN-FLIGHT verify(), dropped on settle; empty when idle. */
|
|
10399
|
+
cancellations = /* @__PURE__ */ new Set();
|
|
10400
|
+
/** Verifications awaiting a verdict — back to 0 whenever the verifier is idle. */
|
|
10401
|
+
get pendingCancellations() {
|
|
10402
|
+
return this.cancellations.size;
|
|
10403
|
+
}
|
|
10404
|
+
/** Rejects (see the class note) if `dispose()` lands before the pool answers. */
|
|
10405
|
+
verify(...args) {
|
|
10406
|
+
if (this.disposed) return Promise.reject(disposedError());
|
|
10407
|
+
const verdict = super.verify(...args);
|
|
10408
|
+
return new Promise((resolve, reject) => {
|
|
10409
|
+
const cancel = () => reject(disposedError());
|
|
10410
|
+
this.cancellations.add(cancel);
|
|
10411
|
+
if (this.disposed) {
|
|
10412
|
+
this.cancellations.delete(cancel);
|
|
10413
|
+
cancel();
|
|
10414
|
+
}
|
|
10415
|
+
void verdict.then(
|
|
10416
|
+
(result) => {
|
|
10417
|
+
this.cancellations.delete(cancel);
|
|
10418
|
+
resolve(result);
|
|
10419
|
+
},
|
|
10420
|
+
(error) => {
|
|
10421
|
+
this.cancellations.delete(cancel);
|
|
10422
|
+
reject(error);
|
|
10423
|
+
}
|
|
10424
|
+
);
|
|
10425
|
+
});
|
|
10426
|
+
}
|
|
10427
|
+
/** Idempotent — Sphere.setOracleApiKey disposes the replaced engine twice (facade + caller). */
|
|
10428
|
+
dispose() {
|
|
10429
|
+
if (this.disposed) return;
|
|
10430
|
+
this.disposed = true;
|
|
10431
|
+
super.dispose();
|
|
10432
|
+
const cancelling = [...this.cancellations];
|
|
10433
|
+
this.cancellations.clear();
|
|
10434
|
+
for (const cancel of cancelling) cancel();
|
|
10435
|
+
}
|
|
10207
10436
|
createWorker() {
|
|
10437
|
+
if (this.disposed) throw disposedError();
|
|
10208
10438
|
return this.spawn();
|
|
10209
10439
|
}
|
|
10210
10440
|
};
|
|
@@ -13033,12 +13263,13 @@ var Receive = class {
|
|
|
13033
13263
|
return this.drainFlight.run(() => this.doDrain());
|
|
13034
13264
|
}
|
|
13035
13265
|
start(pollIntervalMs = POLL_INTERVAL_MS) {
|
|
13036
|
-
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() =>
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13266
|
+
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() => this.spawnDrain()) ?? null;
|
|
13267
|
+
this.pollTimer ??= setInterval(() => this.spawnDrain(), pollIntervalMs);
|
|
13268
|
+
}
|
|
13269
|
+
spawnDrain() {
|
|
13270
|
+
const op = this.drainOnce();
|
|
13271
|
+
if (this.deps.track !== void 0) this.deps.track(op);
|
|
13272
|
+
else void op;
|
|
13042
13273
|
}
|
|
13043
13274
|
stop() {
|
|
13044
13275
|
if (this.pollTimer !== null) clearInterval(this.pollTimer);
|
|
@@ -14141,6 +14372,8 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
|
|
|
14141
14372
|
);
|
|
14142
14373
|
},
|
|
14143
14374
|
syncEpoch: deps.syncEpoch,
|
|
14375
|
+
// #770: the poll/wake drains Receive spawns itself must hold stop() too.
|
|
14376
|
+
track: hooks.track,
|
|
14144
14377
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
14145
14378
|
});
|
|
14146
14379
|
}
|
|
@@ -14283,7 +14516,8 @@ var PaymentsFacade = class {
|
|
|
14283
14516
|
await Promise.allSettled([...this.pendingOps]);
|
|
14284
14517
|
}
|
|
14285
14518
|
}
|
|
14286
|
-
/** Swaps what FUTURE operations snapshot
|
|
14519
|
+
/** Swaps what FUTURE operations snapshot. Chain ops finish on the old engine, but it is
|
|
14520
|
+
* DISPOSED here, so one mid-`verify()` is cancelled — MODULE_DESTROYED, #770(4). */
|
|
14287
14521
|
setEngine(next) {
|
|
14288
14522
|
const previous = this.currentEngine ?? this.deps.engineRef();
|
|
14289
14523
|
this.currentEngine = next;
|
|
@@ -16008,7 +16242,7 @@ function composePaymentsV2(spec) {
|
|
|
16008
16242
|
}),
|
|
16009
16243
|
engineRef: spec.engineRef,
|
|
16010
16244
|
kv,
|
|
16011
|
-
registry:
|
|
16245
|
+
registry: host.registry,
|
|
16012
16246
|
...host.price !== null ? { price: host.price } : {},
|
|
16013
16247
|
emit: host.emit,
|
|
16014
16248
|
resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
|
|
@@ -16036,13 +16270,47 @@ async function deriveL3PredicateAddress(privateKey) {
|
|
|
16036
16270
|
const prehashedPublicKey = getPublicKey(sha2562(privateKey, "hex"));
|
|
16037
16271
|
return deriveDirectAddress(hexToBytes4(prehashedPublicKey));
|
|
16038
16272
|
}
|
|
16273
|
+
var lifecycle = sharedCell(
|
|
16274
|
+
"core.sphere.lifecycle@1",
|
|
16275
|
+
() => ({
|
|
16276
|
+
live: /* @__PURE__ */ new Map(),
|
|
16277
|
+
clearGenerations: /* @__PURE__ */ new Map(),
|
|
16278
|
+
objectStoreKeys: /* @__PURE__ */ new WeakMap(),
|
|
16279
|
+
objectStoreSeq: 0
|
|
16280
|
+
}),
|
|
16281
|
+
(cell) => {
|
|
16282
|
+
const c = cell;
|
|
16283
|
+
return c.live instanceof Map && c.clearGenerations instanceof Map && c.objectStoreKeys instanceof WeakMap && typeof c.objectStoreSeq === "number";
|
|
16284
|
+
}
|
|
16285
|
+
);
|
|
16039
16286
|
var Sphere = class _Sphere {
|
|
16040
|
-
//
|
|
16041
|
-
|
|
16287
|
+
// Live Spheres, keyed by the BACKING STORE their provider addresses. NOT a liveness
|
|
16288
|
+
// API: it exists only so clear()/import() tear down the instances whose data they
|
|
16289
|
+
// really erase. Object identity was too narrow — two providers over one dataDir/DB
|
|
16290
|
+
// are different objects but the same file, so clearing through one left the other's
|
|
16291
|
+
// Sphere live over an emptied KV (#766).
|
|
16292
|
+
static _liveByStorage = lifecycle.live;
|
|
16293
|
+
/** Fallback keys for providers that declare no `backingStoreId` — one per object. */
|
|
16294
|
+
static _objectStoreKeys = lifecycle.objectStoreKeys;
|
|
16295
|
+
/**
|
|
16296
|
+
* How many times each backing store has been cleared. An init is invisible to `clear()`
|
|
16297
|
+
* until it PUBLISHES (#767), so a clear cannot destroy one in flight and would wipe the
|
|
16298
|
+
* store under it. Every init records this number before its first storage work and
|
|
16299
|
+
* publication refuses if it moved (#772). Only cleared stores get an entry, so the
|
|
16300
|
+
* strongly-held keys are bounded by the stores a process actually clears.
|
|
16301
|
+
*/
|
|
16302
|
+
static _clearGenerations = lifecycle.clearGenerations;
|
|
16042
16303
|
// One-time best-effort cleanup of the orphaned vesting cache (prior versions).
|
|
16043
16304
|
static _orphanCacheCleaned = false;
|
|
16044
16305
|
// State
|
|
16045
16306
|
_initialized = false;
|
|
16307
|
+
/**
|
|
16308
|
+
* Destroyed latch (#770). Distinct from `_initialized`, which destroy() clears LAST — after
|
|
16309
|
+
* every teardown step — so it cannot mark "teardown has begun". This is set as destroy()'s
|
|
16310
|
+
* very FIRST statement, so the WHOLE teardown window is guarded, not just the instant after
|
|
16311
|
+
* it. Read by ensureAlive() and by the §7 lifecycle mutex.
|
|
16312
|
+
*/
|
|
16313
|
+
_destroyed = false;
|
|
16046
16314
|
_trackedAddressesLoaded = false;
|
|
16047
16315
|
_identity = null;
|
|
16048
16316
|
_masterKey = null;
|
|
@@ -16100,6 +16368,8 @@ var Sphere = class _Sphere {
|
|
|
16100
16368
|
* the rebuild after an api-key change share one pool configuration.
|
|
16101
16369
|
*/
|
|
16102
16370
|
_verification;
|
|
16371
|
+
/** This Sphere's OWN token registry. Disposed by destroy(); never the process global. */
|
|
16372
|
+
_registry = null;
|
|
16103
16373
|
// Events
|
|
16104
16374
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
16105
16375
|
// Provider management
|
|
@@ -16174,7 +16444,7 @@ var Sphere = class _Sphere {
|
|
|
16174
16444
|
* ```
|
|
16175
16445
|
*/
|
|
16176
16446
|
static async init(options) {
|
|
16177
|
-
if (options.debug) logger.configure({ debug:
|
|
16447
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16178
16448
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16179
16449
|
resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16180
16450
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
@@ -16194,6 +16464,7 @@ var Sphere = class _Sphere {
|
|
|
16194
16464
|
market,
|
|
16195
16465
|
communications: options.communications,
|
|
16196
16466
|
password: options.password,
|
|
16467
|
+
verification: options.verification,
|
|
16197
16468
|
discoverAddresses: options.discoverAddresses,
|
|
16198
16469
|
onProgress: options.onProgress
|
|
16199
16470
|
});
|
|
@@ -16230,6 +16501,7 @@ var Sphere = class _Sphere {
|
|
|
16230
16501
|
market,
|
|
16231
16502
|
communications: options.communications,
|
|
16232
16503
|
password: options.password,
|
|
16504
|
+
verification: options.verification,
|
|
16233
16505
|
discoverAddresses: options.discoverAddresses,
|
|
16234
16506
|
onProgress: options.onProgress
|
|
16235
16507
|
});
|
|
@@ -16311,16 +16583,53 @@ var Sphere = class _Sphere {
|
|
|
16311
16583
|
}
|
|
16312
16584
|
TokenRegistry.configure({ remoteUrl: NETWORKS[network].tokenRegistryUrl, storage });
|
|
16313
16585
|
}
|
|
16586
|
+
/**
|
|
16587
|
+
* Build the registry THIS Sphere owns, so its metadata cannot be repointed by another
|
|
16588
|
+
* Sphere's init. The global is still configured above for consumers that read it
|
|
16589
|
+
* directly; this instance is what the payments facade presents from.
|
|
16590
|
+
*/
|
|
16591
|
+
/**
|
|
16592
|
+
* Own a registry for the duration of `bringUp`, disposing it if any of that rejects.
|
|
16593
|
+
*
|
|
16594
|
+
* `bringUp` must cover EVERY fallible step from here until the Sphere is returned to
|
|
16595
|
+
* its caller — until then nobody holds anything they could destroy, so a
|
|
16596
|
+
* registry left behind is unreachable and its hourly fetch runs for the life of the
|
|
16597
|
+
* process. Guarding a named subset of the steps is what failed twice: the guarded region
|
|
16598
|
+
* and the fallible region were separate things, and drifted. Publication is the LAST
|
|
16599
|
+
* guarded step for the same reason: it can refuse, and a refusal must tear down.
|
|
16600
|
+
*/
|
|
16601
|
+
static async withOwnedRegistry(sphere, storage, network, clearGeneration, bringUp) {
|
|
16602
|
+
sphere._registry = _Sphere.createOwnedRegistry(storage, network);
|
|
16603
|
+
try {
|
|
16604
|
+
await bringUp();
|
|
16605
|
+
_Sphere.publishLive(sphere, clearGeneration);
|
|
16606
|
+
} catch (err) {
|
|
16607
|
+
await sphere.destroy().catch((teardownErr) => {
|
|
16608
|
+
logger.warn("Sphere", "teardown after failed initialization also failed:", teardownErr);
|
|
16609
|
+
});
|
|
16610
|
+
throw err;
|
|
16611
|
+
}
|
|
16612
|
+
}
|
|
16613
|
+
static createOwnedRegistry(storage, network) {
|
|
16614
|
+
if (!network) {
|
|
16615
|
+
throw new SphereError(
|
|
16616
|
+
"network is required to build the token registry. Every Sphere entry point must forward options.network.",
|
|
16617
|
+
"INVALID_CONFIG"
|
|
16618
|
+
);
|
|
16619
|
+
}
|
|
16620
|
+
return TokenRegistry.create({ remoteUrl: NETWORKS[network].tokenRegistryUrl, storage });
|
|
16621
|
+
}
|
|
16314
16622
|
/**
|
|
16315
16623
|
* Create new wallet with mnemonic
|
|
16316
16624
|
*/
|
|
16317
16625
|
static async create(options) {
|
|
16318
|
-
if (options.debug) logger.configure({ debug:
|
|
16626
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16319
16627
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16320
16628
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16321
16629
|
if (!options.mnemonic || !_Sphere.validateMnemonic(options.mnemonic)) {
|
|
16322
16630
|
throw new SphereError("Invalid mnemonic", "INVALID_IDENTITY");
|
|
16323
16631
|
}
|
|
16632
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16324
16633
|
if (await _Sphere.exists(options.storage)) {
|
|
16325
16634
|
throw new SphereError("Wallet already exists. Use Sphere.load() or Sphere.clear() first.", "ALREADY_INITIALIZED");
|
|
16326
16635
|
}
|
|
@@ -16348,44 +16657,46 @@ var Sphere = class _Sphere {
|
|
|
16348
16657
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
16349
16658
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
16350
16659
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16351
|
-
await sphere.
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
|
|
16360
|
-
|
|
16361
|
-
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
16365
|
-
|
|
16366
|
-
|
|
16367
|
-
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
16372
|
-
|
|
16373
|
-
|
|
16660
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16661
|
+
await sphere.initializeProviders();
|
|
16662
|
+
await sphere.initializeModules();
|
|
16663
|
+
progress?.({ step: "finalizing", message: "Finalizing wallet..." });
|
|
16664
|
+
await sphere.finalizeWalletCreation();
|
|
16665
|
+
sphere._initialized = true;
|
|
16666
|
+
await sphere.ensureAddressTracked(0);
|
|
16667
|
+
if (options.nametag) {
|
|
16668
|
+
progress?.({ step: "registering_nametag", message: "Registering nametag..." });
|
|
16669
|
+
await sphere.registerNametag(options.nametag);
|
|
16670
|
+
} else {
|
|
16671
|
+
progress?.({ step: "recovering_nametag", message: "Recovering nametag..." });
|
|
16672
|
+
await sphere.recoverNametagFromTransport();
|
|
16673
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16674
|
+
await sphere.syncIdentityWithTransport();
|
|
16675
|
+
}
|
|
16676
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses) {
|
|
16677
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16678
|
+
try {
|
|
16679
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16680
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16681
|
+
if (result.addresses.length > 0) {
|
|
16682
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16683
|
+
}
|
|
16684
|
+
} catch (err) {
|
|
16685
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16374
16686
|
}
|
|
16375
|
-
} catch (err) {
|
|
16376
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16377
16687
|
}
|
|
16378
|
-
|
|
16379
|
-
|
|
16688
|
+
progress?.({ step: "complete", message: "Wallet created" });
|
|
16689
|
+
});
|
|
16380
16690
|
return sphere;
|
|
16381
16691
|
}
|
|
16382
16692
|
/**
|
|
16383
16693
|
* Load existing wallet from storage
|
|
16384
16694
|
*/
|
|
16385
16695
|
static async load(options) {
|
|
16386
|
-
if (options.debug) logger.configure({ debug:
|
|
16696
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16387
16697
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16388
16698
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16699
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16389
16700
|
if (!await _Sphere.exists(options.storage)) {
|
|
16390
16701
|
throw new SphereError("No wallet found. Use Sphere.create() to create a new wallet.", "NOT_INITIALIZED");
|
|
16391
16702
|
}
|
|
@@ -16412,32 +16723,33 @@ var Sphere = class _Sphere {
|
|
|
16412
16723
|
progress?.({ step: "storing_keys", message: "Loading wallet keys..." });
|
|
16413
16724
|
await sphere.loadIdentityFromStorage();
|
|
16414
16725
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16415
|
-
await sphere.
|
|
16416
|
-
|
|
16417
|
-
|
|
16418
|
-
|
|
16419
|
-
|
|
16420
|
-
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16726
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16727
|
+
await sphere.initializeProviders();
|
|
16728
|
+
await sphere.initializeModules();
|
|
16729
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16730
|
+
await sphere.syncIdentityWithTransport();
|
|
16731
|
+
sphere._initialized = true;
|
|
16732
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses && sphere._masterKey) {
|
|
16733
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16734
|
+
try {
|
|
16735
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16736
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16737
|
+
if (result.addresses.length > 0) {
|
|
16738
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16739
|
+
}
|
|
16740
|
+
} catch (err) {
|
|
16741
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16428
16742
|
}
|
|
16429
|
-
} catch (err) {
|
|
16430
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16431
16743
|
}
|
|
16432
|
-
|
|
16433
|
-
|
|
16744
|
+
progress?.({ step: "complete", message: "Wallet loaded" });
|
|
16745
|
+
});
|
|
16434
16746
|
return sphere;
|
|
16435
16747
|
}
|
|
16436
16748
|
/**
|
|
16437
16749
|
* Import wallet from mnemonic or master key
|
|
16438
16750
|
*/
|
|
16439
16751
|
static async import(options) {
|
|
16440
|
-
if (options.debug) logger.configure({ debug:
|
|
16752
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16441
16753
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16442
16754
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16443
16755
|
if (!options.mnemonic && !options.masterKey) {
|
|
@@ -16445,7 +16757,7 @@ var Sphere = class _Sphere {
|
|
|
16445
16757
|
}
|
|
16446
16758
|
const progress = options.onProgress;
|
|
16447
16759
|
logger.debug("Sphere", "Starting import...");
|
|
16448
|
-
const needsClear = _Sphere.
|
|
16760
|
+
const needsClear = _Sphere.liveOn(options.storage).length > 0 || await _Sphere.exists(options.storage);
|
|
16449
16761
|
if (needsClear) {
|
|
16450
16762
|
progress?.({ step: "clearing", message: "Clearing previous wallet data..." });
|
|
16451
16763
|
logger.debug("Sphere", "Clearing existing wallet data...");
|
|
@@ -16459,6 +16771,7 @@ var Sphere = class _Sphere {
|
|
|
16459
16771
|
await options.storage.connect();
|
|
16460
16772
|
logger.debug("Sphere", "Storage reconnected");
|
|
16461
16773
|
}
|
|
16774
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16462
16775
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
16463
16776
|
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
16464
16777
|
const marketConfig = _Sphere.resolveMarketConfig(options.market);
|
|
@@ -16502,44 +16815,44 @@ var Sphere = class _Sphere {
|
|
|
16502
16815
|
}
|
|
16503
16816
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16504
16817
|
logger.debug("Sphere", "Initializing providers...");
|
|
16505
|
-
await sphere.
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
|
|
16509
|
-
|
|
16510
|
-
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
-
|
|
16522
|
-
|
|
16523
|
-
|
|
16524
|
-
|
|
16525
|
-
|
|
16526
|
-
|
|
16527
|
-
|
|
16528
|
-
|
|
16529
|
-
|
|
16530
|
-
|
|
16531
|
-
|
|
16532
|
-
|
|
16533
|
-
|
|
16534
|
-
|
|
16535
|
-
|
|
16818
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16819
|
+
await sphere.initializeProviders();
|
|
16820
|
+
await sphere.initializeModules();
|
|
16821
|
+
logger.debug("Sphere", "Modules initialized");
|
|
16822
|
+
if (!options.nametag) {
|
|
16823
|
+
progress?.({ step: "recovering_nametag", message: "Recovering nametag..." });
|
|
16824
|
+
logger.debug("Sphere", "Recovering Unicity ID from transport...");
|
|
16825
|
+
await sphere.recoverNametagFromTransport();
|
|
16826
|
+
logger.debug("Sphere", "Unicity ID recovery done");
|
|
16827
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16828
|
+
await sphere.syncIdentityWithTransport();
|
|
16829
|
+
}
|
|
16830
|
+
progress?.({ step: "finalizing", message: "Finalizing wallet creation..." });
|
|
16831
|
+
logger.debug("Sphere", "Finalizing wallet creation...");
|
|
16832
|
+
await sphere.finalizeWalletCreation();
|
|
16833
|
+
sphere._initialized = true;
|
|
16834
|
+
logger.debug("Sphere", "Tracking address 0...");
|
|
16835
|
+
await sphere.ensureAddressTracked(0);
|
|
16836
|
+
if (options.nametag) {
|
|
16837
|
+
progress?.({ step: "registering_nametag", message: "Registering nametag..." });
|
|
16838
|
+
logger.debug("Sphere", "Registering Unicity ID...");
|
|
16839
|
+
await sphere.registerNametag(options.nametag);
|
|
16840
|
+
}
|
|
16841
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses) {
|
|
16842
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16843
|
+
try {
|
|
16844
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16845
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16846
|
+
if (result.addresses.length > 0) {
|
|
16847
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16848
|
+
}
|
|
16849
|
+
} catch (err) {
|
|
16850
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16536
16851
|
}
|
|
16537
|
-
} catch (err) {
|
|
16538
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16539
16852
|
}
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16853
|
+
progress?.({ step: "complete", message: "Import complete" });
|
|
16854
|
+
logger.debug("Sphere", "Import complete");
|
|
16855
|
+
});
|
|
16543
16856
|
return sphere;
|
|
16544
16857
|
}
|
|
16545
16858
|
/**
|
|
@@ -16557,9 +16870,17 @@ var Sphere = class _Sphere {
|
|
|
16557
16870
|
*/
|
|
16558
16871
|
static async clear(options) {
|
|
16559
16872
|
const storage = options.storage;
|
|
16560
|
-
|
|
16561
|
-
|
|
16562
|
-
await _Sphere.
|
|
16873
|
+
_Sphere.bumpClearGeneration(storage);
|
|
16874
|
+
try {
|
|
16875
|
+
await _Sphere.clearStore(storage);
|
|
16876
|
+
} finally {
|
|
16877
|
+
_Sphere.bumpClearGeneration(storage);
|
|
16878
|
+
}
|
|
16879
|
+
}
|
|
16880
|
+
static async clearStore(storage) {
|
|
16881
|
+
for (const live of _Sphere.liveOn(storage)) {
|
|
16882
|
+
logger.debug("Sphere", "Destroying Sphere instance on this storage...");
|
|
16883
|
+
await live.destroy();
|
|
16563
16884
|
logger.debug("Sphere", "Sphere instance destroyed");
|
|
16564
16885
|
}
|
|
16565
16886
|
logger.debug("Sphere", "Yielding 50ms for IDB transaction settlement...");
|
|
@@ -16619,16 +16940,64 @@ var Sphere = class _Sphere {
|
|
|
16619
16940
|
}
|
|
16620
16941
|
}
|
|
16621
16942
|
/**
|
|
16622
|
-
*
|
|
16943
|
+
* The key a provider registers under: the store it addresses, so every provider
|
|
16944
|
+
* over one dataDir/DB shares an entry. A provider that declares no store keeps a
|
|
16945
|
+
* private key, leaving custom implementations scoped by object identity.
|
|
16623
16946
|
*/
|
|
16624
|
-
static
|
|
16625
|
-
|
|
16947
|
+
static storeKeyOf(storage) {
|
|
16948
|
+
const declared = storage.backingStoreId;
|
|
16949
|
+
if (declared) return `store:${declared}`;
|
|
16950
|
+
let key = _Sphere._objectStoreKeys.get(storage);
|
|
16951
|
+
if (!key) {
|
|
16952
|
+
key = `object:${++lifecycle.objectStoreSeq}`;
|
|
16953
|
+
_Sphere._objectStoreKeys.set(storage, key);
|
|
16954
|
+
}
|
|
16955
|
+
return key;
|
|
16956
|
+
}
|
|
16957
|
+
/** The clears this store has seen. Absent means none — `clear()` creates the entry. */
|
|
16958
|
+
static clearGenerationOf(storage) {
|
|
16959
|
+
return _Sphere._clearGenerations.get(_Sphere.storeKeyOf(storage)) ?? 0;
|
|
16960
|
+
}
|
|
16961
|
+
static bumpClearGeneration(storage) {
|
|
16962
|
+
const key = _Sphere.storeKeyOf(storage);
|
|
16963
|
+
_Sphere._clearGenerations.set(key, (_Sphere._clearGenerations.get(key) ?? 0) + 1);
|
|
16626
16964
|
}
|
|
16627
16965
|
/**
|
|
16628
|
-
*
|
|
16966
|
+
* Make a fully-built Sphere reachable — or refuse, if `clear()` emptied the store
|
|
16967
|
+
* under it while it was building. Check and registration are one SYNCHRONOUS step on
|
|
16968
|
+
* purpose: split by an await, a clear could land between them and wipe a store whose
|
|
16969
|
+
* Sphere is already published. The refusal throws inside `withOwnedRegistry`, whose
|
|
16970
|
+
* teardown then destroys the half-built Sphere the caller never received.
|
|
16629
16971
|
*/
|
|
16630
|
-
static
|
|
16631
|
-
|
|
16972
|
+
static publishLive(sphere, clearGeneration) {
|
|
16973
|
+
if (_Sphere.clearGenerationOf(sphere._storage) !== clearGeneration) {
|
|
16974
|
+
throw new SphereError(
|
|
16975
|
+
"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.",
|
|
16976
|
+
"STORAGE_ERROR"
|
|
16977
|
+
);
|
|
16978
|
+
}
|
|
16979
|
+
_Sphere.registerLive(sphere);
|
|
16980
|
+
}
|
|
16981
|
+
/** Record a fully-built Sphere against the storage it owns. See `_liveByStorage`. */
|
|
16982
|
+
static registerLive(sphere) {
|
|
16983
|
+
const key = _Sphere.storeKeyOf(sphere._storage);
|
|
16984
|
+
let live = _Sphere._liveByStorage.get(key);
|
|
16985
|
+
if (!live) {
|
|
16986
|
+
live = /* @__PURE__ */ new Set();
|
|
16987
|
+
_Sphere._liveByStorage.set(key, live);
|
|
16988
|
+
}
|
|
16989
|
+
live.add(sphere);
|
|
16990
|
+
}
|
|
16991
|
+
static unregisterLive(sphere) {
|
|
16992
|
+
const key = _Sphere.storeKeyOf(sphere._storage);
|
|
16993
|
+
const live = _Sphere._liveByStorage.get(key);
|
|
16994
|
+
if (!live) return;
|
|
16995
|
+
live.delete(sphere);
|
|
16996
|
+
if (live.size === 0) _Sphere._liveByStorage.delete(key);
|
|
16997
|
+
}
|
|
16998
|
+
/** Snapshot — callers iterate this while destroy() mutates the underlying Set. */
|
|
16999
|
+
static liveOn(storage) {
|
|
17000
|
+
return Array.from(_Sphere._liveByStorage.get(_Sphere.storeKeyOf(storage)) ?? []);
|
|
16632
17001
|
}
|
|
16633
17002
|
/**
|
|
16634
17003
|
* Validate mnemonic using BIP39
|
|
@@ -16839,8 +17208,7 @@ var Sphere = class _Sphere {
|
|
|
16839
17208
|
password
|
|
16840
17209
|
});
|
|
16841
17210
|
if (result.success) {
|
|
16842
|
-
|
|
16843
|
-
return { success: true, sphere: sphere2, mnemonic: result.mnemonic };
|
|
17211
|
+
return { success: true, sphere: result.sphere, mnemonic: result.mnemonic };
|
|
16844
17212
|
}
|
|
16845
17213
|
if (!password && result.error?.includes("Password required")) {
|
|
16846
17214
|
return { success: false, needsPassword: true, error: result.error };
|
|
@@ -17142,7 +17510,8 @@ var Sphere = class _Sphere {
|
|
|
17142
17510
|
/**
|
|
17143
17511
|
* Import wallet from JSON backup
|
|
17144
17512
|
*
|
|
17145
|
-
* @returns
|
|
17513
|
+
* @returns `{ success, sphere?, mnemonic?, error? }`. `sphere` is the instance built
|
|
17514
|
+
* on the SUPPLIED storage — hold it, there is no global to look it up from (#766).
|
|
17146
17515
|
*
|
|
17147
17516
|
* @example
|
|
17148
17517
|
* ```ts
|
|
@@ -17183,18 +17552,18 @@ var Sphere = class _Sphere {
|
|
|
17183
17552
|
}
|
|
17184
17553
|
const basePath = data.wallet.descriptorPath ? `m/${data.wallet.descriptorPath}` : DEFAULT_BASE_PATH;
|
|
17185
17554
|
if (mnemonic) {
|
|
17186
|
-
await _Sphere.import({ ...baseOptions, mnemonic, basePath });
|
|
17187
|
-
return { success: true, mnemonic };
|
|
17555
|
+
const sphere = await _Sphere.import({ ...baseOptions, mnemonic, basePath });
|
|
17556
|
+
return { success: true, sphere, mnemonic };
|
|
17188
17557
|
}
|
|
17189
17558
|
if (masterKey) {
|
|
17190
|
-
await _Sphere.import({
|
|
17559
|
+
const sphere = await _Sphere.import({
|
|
17191
17560
|
...baseOptions,
|
|
17192
17561
|
masterKey,
|
|
17193
17562
|
chainCode: data.wallet.chainCode,
|
|
17194
17563
|
basePath,
|
|
17195
17564
|
derivationMode: data.derivationMode || (data.wallet.isBIP32 ? "bip32" : "wif_hmac")
|
|
17196
17565
|
});
|
|
17197
|
-
return { success: true };
|
|
17566
|
+
return { success: true, sphere };
|
|
17198
17567
|
}
|
|
17199
17568
|
return { success: false, error: "No mnemonic or master key in wallet data" };
|
|
17200
17569
|
} catch (e) {
|
|
@@ -17317,6 +17686,7 @@ var Sphere = class _Sphere {
|
|
|
17317
17686
|
}
|
|
17318
17687
|
if (entry.hidden === hidden) return;
|
|
17319
17688
|
entry.hidden = hidden;
|
|
17689
|
+
entry.updatedAt = Date.now();
|
|
17320
17690
|
await this.persistTrackedAddresses();
|
|
17321
17691
|
const eventType = hidden ? "address:hidden" : "address:unhidden";
|
|
17322
17692
|
this.emitEvent(eventType, { index, addressId: entry.addressId });
|
|
@@ -17345,9 +17715,6 @@ var Sphere = class _Sphere {
|
|
|
17345
17715
|
if (!this._masterKey) {
|
|
17346
17716
|
throw new SphereError("HD derivation requires master key with chain code. Cannot switch addresses.", "INVALID_CONFIG");
|
|
17347
17717
|
}
|
|
17348
|
-
if (index < 0) {
|
|
17349
|
-
throw new SphereError("Address index must be non-negative", "INVALID_CONFIG");
|
|
17350
|
-
}
|
|
17351
17718
|
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
17352
17719
|
if (newNametag && !isValidNametag(newNametag)) {
|
|
17353
17720
|
throw new SphereError("Invalid Unicity ID format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.", "VALIDATION_ERROR");
|
|
@@ -17384,7 +17751,9 @@ var Sphere = class _Sphere {
|
|
|
17384
17751
|
if (firstVisit) {
|
|
17385
17752
|
logger.debug("Sphere", `switchToAddress(${index}): creating per-address modules (lazy init)`);
|
|
17386
17753
|
this._storage.setIdentity(newIdentity);
|
|
17754
|
+
this.ensureAlive();
|
|
17387
17755
|
await this.initializeAddressModules({ index, identity: newIdentity });
|
|
17756
|
+
if (this._destroyed) await this.discardModulesBuiltDuringDestroy(index);
|
|
17388
17757
|
} else if (nametag !== this._addressModules.get(index).identity.nametag) {
|
|
17389
17758
|
this._addressModules.get(index).identity = newIdentity;
|
|
17390
17759
|
}
|
|
@@ -17395,14 +17764,18 @@ var Sphere = class _Sphere {
|
|
|
17395
17764
|
this._communications = activeModules.communications;
|
|
17396
17765
|
this._groupChat = activeModules.groupChat;
|
|
17397
17766
|
this._market = activeModules.market;
|
|
17767
|
+
this.ensureAlive();
|
|
17398
17768
|
await this.stopThenStartPaymentsV2(index, newIdentity);
|
|
17769
|
+
this.ensureAlive();
|
|
17399
17770
|
await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
|
|
17400
17771
|
this._storage.setIdentity(this._identity);
|
|
17401
17772
|
if (this._transport.setFallbackSince) {
|
|
17402
17773
|
const fallbackTs = Math.floor(Date.now() / 1e3) - 86400;
|
|
17403
17774
|
this._transport.setFallbackSince(fallbackTs);
|
|
17404
17775
|
}
|
|
17776
|
+
this.ensureAlive();
|
|
17405
17777
|
await this._transport.setIdentity(this._identity);
|
|
17778
|
+
this.ensureAlive();
|
|
17406
17779
|
if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
|
|
17407
17780
|
await this._transportMux.rebindToSharedClient();
|
|
17408
17781
|
}
|
|
@@ -17422,6 +17795,7 @@ var Sphere = class _Sphere {
|
|
|
17422
17795
|
* Runs after switchToAddress returns so L3 queries can start immediately.
|
|
17423
17796
|
*/
|
|
17424
17797
|
async postSwitchSync(index, newNametag) {
|
|
17798
|
+
this.ensureAlive();
|
|
17425
17799
|
if (!newNametag) {
|
|
17426
17800
|
await this.syncIdentityWithTransport();
|
|
17427
17801
|
}
|
|
@@ -17439,6 +17813,39 @@ var Sphere = class _Sphere {
|
|
|
17439
17813
|
* independently in background. The payments vertical is NOT created here —
|
|
17440
17814
|
* the caller starts it via stopThenStartPaymentsV2 (§7 single vertical).
|
|
17441
17815
|
*/
|
|
17816
|
+
/** Tear one address's module set down. Each address owns its own engine, so its own pool. */
|
|
17817
|
+
static destroyModuleSet(index, moduleSet) {
|
|
17818
|
+
try {
|
|
17819
|
+
moduleSet.communications.destroy();
|
|
17820
|
+
moduleSet.groupChat?.destroy();
|
|
17821
|
+
moduleSet.market?.destroy();
|
|
17822
|
+
moduleSet.tokenEngine?.dispose?.();
|
|
17823
|
+
logger.debug("Sphere", `Destroyed modules for address ${index}`);
|
|
17824
|
+
} catch (err) {
|
|
17825
|
+
logger.warn("Sphere", `Error destroying modules for address ${index}:`, err);
|
|
17826
|
+
}
|
|
17827
|
+
}
|
|
17828
|
+
/**
|
|
17829
|
+
* Undo a module set built while `destroy()` was running.
|
|
17830
|
+
*
|
|
17831
|
+
* `destroy()`'s teardown loop has already emptied `_addressModules`, so a set
|
|
17832
|
+
* registered after it would stay live and unreachable — its own engine (and worker
|
|
17833
|
+
* pool), and a transport mux `ensureTransportMux` rebuilt because teardown had just
|
|
17834
|
+
* nulled the field. Then re-raise, so the switch fails rather than reporting success
|
|
17835
|
+
* on a destroyed Sphere (#770, #772 review).
|
|
17836
|
+
*/
|
|
17837
|
+
async discardModulesBuiltDuringDestroy(index) {
|
|
17838
|
+
const built = this._addressModules.get(index);
|
|
17839
|
+
this._addressModules.delete(index);
|
|
17840
|
+
if (built) _Sphere.destroyModuleSet(index, built);
|
|
17841
|
+
if (this._transportMux) {
|
|
17842
|
+
const mux = this._transportMux;
|
|
17843
|
+
this._transportMux = null;
|
|
17844
|
+
await _Sphere.safeDisconnect("transport mux", () => mux.disconnect());
|
|
17845
|
+
}
|
|
17846
|
+
this.ensureAlive();
|
|
17847
|
+
throw new SphereError("Sphere destroyed", "NOT_INITIALIZED");
|
|
17848
|
+
}
|
|
17442
17849
|
async initializeAddressModules(spec) {
|
|
17443
17850
|
const { index, identity } = spec;
|
|
17444
17851
|
const emitEvent = this.emitEvent.bind(this);
|
|
@@ -17526,6 +17933,14 @@ var Sphere = class _Sphere {
|
|
|
17526
17933
|
const adapter = await this._transportMux.addAddress(index, identity, this._transport);
|
|
17527
17934
|
return adapter;
|
|
17528
17935
|
}
|
|
17936
|
+
/** A BIP32 child number: an integer in 0…0xffffffff. Anything else aliases an address. */
|
|
17937
|
+
static assertDerivableIndex(index) {
|
|
17938
|
+
if (isDerivableIndex(index)) return;
|
|
17939
|
+
throw new SphereError(
|
|
17940
|
+
`Address index ${String(index)} is not a BIP32 child number: it must be an integer in 0\u20260xffffffff.`,
|
|
17941
|
+
"INVALID_CONFIG"
|
|
17942
|
+
);
|
|
17943
|
+
}
|
|
17529
17944
|
/**
|
|
17530
17945
|
* Derive address at a specific index
|
|
17531
17946
|
*
|
|
@@ -17571,6 +17986,7 @@ var Sphere = class _Sphere {
|
|
|
17571
17986
|
* when _initialized is still false.
|
|
17572
17987
|
*/
|
|
17573
17988
|
_deriveAddressInternal(index, isChange = false) {
|
|
17989
|
+
_Sphere.assertDerivableIndex(index);
|
|
17574
17990
|
if (!this._masterKey) {
|
|
17575
17991
|
throw new SphereError("HD derivation requires master key with chain code", "INVALID_CONFIG");
|
|
17576
17992
|
}
|
|
@@ -17662,6 +18078,7 @@ var Sphere = class _Sphere {
|
|
|
17662
18078
|
}
|
|
17663
18079
|
if (tracked.hidden !== hidden) {
|
|
17664
18080
|
tracked.hidden = hidden;
|
|
18081
|
+
tracked.updatedAt = Date.now();
|
|
17665
18082
|
}
|
|
17666
18083
|
}
|
|
17667
18084
|
await this.persistTrackedAddresses();
|
|
@@ -18303,7 +18720,48 @@ var Sphere = class _Sphere {
|
|
|
18303
18720
|
// ===========================================================================
|
|
18304
18721
|
// Public Methods - Lifecycle
|
|
18305
18722
|
// ===========================================================================
|
|
18723
|
+
/**
|
|
18724
|
+
* Disconnect transport, storage and oracle, attempting each even if an earlier one
|
|
18725
|
+
* rejects. They are separate resources, and one failure must not skip the rest — that
|
|
18726
|
+
* is how a partial teardown leaves connections open, which is exactly the state the
|
|
18727
|
+
* failed-initialization path calls destroy() in.
|
|
18728
|
+
*/
|
|
18729
|
+
async disconnectProvidersIndependently() {
|
|
18730
|
+
const steps = [
|
|
18731
|
+
["transport", () => this._transport.disconnect()],
|
|
18732
|
+
["storage", () => this._storage.disconnect()],
|
|
18733
|
+
["oracle", () => this._oracle.disconnect()]
|
|
18734
|
+
];
|
|
18735
|
+
for (const [what, disconnect] of steps) {
|
|
18736
|
+
await _Sphere.safeDisconnect(what, disconnect);
|
|
18737
|
+
}
|
|
18738
|
+
}
|
|
18739
|
+
/** Run one teardown step, logging rather than propagating so the next one still runs. */
|
|
18740
|
+
static async safeDisconnect(what, run) {
|
|
18741
|
+
try {
|
|
18742
|
+
await run();
|
|
18743
|
+
} catch (err) {
|
|
18744
|
+
logger.warn("Sphere", `${what} disconnect failed during destroy:`, err);
|
|
18745
|
+
}
|
|
18746
|
+
}
|
|
18747
|
+
/**
|
|
18748
|
+
* Tear this Sphere down: stop the vertical, destroy the modules, disconnect the providers
|
|
18749
|
+
* and zero the key material. Idempotent by construction (every step is null-guarded) and
|
|
18750
|
+
* deliberately WITHOUT an early return on re-entry, which would change what a double call
|
|
18751
|
+
* means.
|
|
18752
|
+
*
|
|
18753
|
+
* #770: the FIRST statement flips `_destroyed` — before any await, and before this queues
|
|
18754
|
+
* its own stop on the §7 lifecycle mutex. That position is load-bearing. Every guard reads
|
|
18755
|
+
* the flag, so from that instant any switchToAddress step not yet begun refuses; and
|
|
18756
|
+
* because it flips SYNCHRONOUSLY at entry, a stop/start pair the mutex runs after this call
|
|
18757
|
+
* sees `true` and skips its start. Set it beside `_initialized` at the bottom instead and a
|
|
18758
|
+
* concurrent switch re-arms a wallet whose owner already had destroy() return: fresh
|
|
18759
|
+
* sockets, a fresh wallet-api session, a whole fresh vertical nothing will ever stop.
|
|
18760
|
+
*/
|
|
18306
18761
|
async destroy() {
|
|
18762
|
+
this._destroyed = true;
|
|
18763
|
+
this._registry?.dispose();
|
|
18764
|
+
this._registry = null;
|
|
18307
18765
|
this.cleanupProviderEventSubscriptions();
|
|
18308
18766
|
try {
|
|
18309
18767
|
await this.queuePaymentsV2Op(() => this.stopPaymentsV2Inner());
|
|
@@ -18311,28 +18769,19 @@ var Sphere = class _Sphere {
|
|
|
18311
18769
|
logger.warn("Sphere", "payments vertical stop failed during destroy:", err);
|
|
18312
18770
|
}
|
|
18313
18771
|
for (const [idx, moduleSet] of this._addressModules.entries()) {
|
|
18314
|
-
|
|
18315
|
-
moduleSet.communications.destroy();
|
|
18316
|
-
moduleSet.groupChat?.destroy();
|
|
18317
|
-
moduleSet.market?.destroy();
|
|
18318
|
-
moduleSet.tokenEngine?.dispose?.();
|
|
18319
|
-
logger.debug("Sphere", `Destroyed modules for address ${idx}`);
|
|
18320
|
-
} catch (err) {
|
|
18321
|
-
logger.warn("Sphere", `Error destroying modules for address ${idx}:`, err);
|
|
18322
|
-
}
|
|
18772
|
+
_Sphere.destroyModuleSet(idx, moduleSet);
|
|
18323
18773
|
}
|
|
18324
18774
|
this._addressModules.clear();
|
|
18325
18775
|
this._communications.destroy();
|
|
18326
18776
|
this._groupChat?.destroy();
|
|
18327
18777
|
this._market?.destroy();
|
|
18328
18778
|
if (this._transportMux) {
|
|
18329
|
-
|
|
18779
|
+
const mux = this._transportMux;
|
|
18780
|
+
await _Sphere.safeDisconnect("transport mux", () => mux.disconnect());
|
|
18330
18781
|
this._transportMux = null;
|
|
18331
18782
|
}
|
|
18332
18783
|
this._tokenEngine?.dispose?.();
|
|
18333
|
-
await this.
|
|
18334
|
-
await this._storage.disconnect();
|
|
18335
|
-
await this._oracle.disconnect();
|
|
18784
|
+
await this.disconnectProvidersIndependently();
|
|
18336
18785
|
this._initialized = false;
|
|
18337
18786
|
this._trackedAddressesLoaded = false;
|
|
18338
18787
|
this._identity = null;
|
|
@@ -18344,9 +18793,7 @@ var Sphere = class _Sphere {
|
|
|
18344
18793
|
this._addressNametags.clear();
|
|
18345
18794
|
this._disabledProviders.clear();
|
|
18346
18795
|
this.eventHandlers.clear();
|
|
18347
|
-
|
|
18348
|
-
_Sphere.instance = null;
|
|
18349
|
-
}
|
|
18796
|
+
_Sphere.unregisterLive(this);
|
|
18350
18797
|
}
|
|
18351
18798
|
// ===========================================================================
|
|
18352
18799
|
// Private: Storage
|
|
@@ -18683,10 +19130,21 @@ var Sphere = class _Sphere {
|
|
|
18683
19130
|
);
|
|
18684
19131
|
return run;
|
|
18685
19132
|
}
|
|
18686
|
-
/**
|
|
19133
|
+
/**
|
|
19134
|
+
* Switch/boot: stop whatever runs, then start `index`'s vertical — atomically vs other
|
|
19135
|
+
* lifecycle ops.
|
|
19136
|
+
*
|
|
19137
|
+
* #770: the destroyed check between the two halves settles the FACADE vector on its own.
|
|
19138
|
+
* `_destroyed` flips synchronously at destroy() entry, so any closure that BEGINS executing
|
|
19139
|
+
* after destroy() was called sees `true`, and any closure already past the check has
|
|
19140
|
+
* facade.start() in flight — which destroy()'s own queued stop is necessarily ordered
|
|
19141
|
+
* after. The TRANSPORT vector is not on this mutex at all; the ensureAlive() calls in
|
|
19142
|
+
* switchToAddress are what cover it.
|
|
19143
|
+
*/
|
|
18687
19144
|
stopThenStartPaymentsV2(index, identity) {
|
|
18688
19145
|
return this.queuePaymentsV2Op(async () => {
|
|
18689
19146
|
await this.stopPaymentsV2Inner();
|
|
19147
|
+
if (this._destroyed) return;
|
|
18690
19148
|
await this.startPaymentsV2Inner(index, identity);
|
|
18691
19149
|
});
|
|
18692
19150
|
}
|
|
@@ -18718,6 +19176,7 @@ var Sphere = class _Sphere {
|
|
|
18718
19176
|
host: {
|
|
18719
19177
|
storage: this._storage,
|
|
18720
19178
|
price: this._priceProvider,
|
|
19179
|
+
registry: this._registry ?? TokenRegistry.getInstance(),
|
|
18721
19180
|
emit: (event, payload) => this.emitEvent(event, payload),
|
|
18722
19181
|
resolvePeer: (identifier) => this._transport.resolve?.(identifier) ?? Promise.resolve(null),
|
|
18723
19182
|
// Scoped to THIS facade's address, never "whichever is active":
|
|
@@ -18738,7 +19197,18 @@ var Sphere = class _Sphere {
|
|
|
18738
19197
|
this._paymentsV2Active = null;
|
|
18739
19198
|
await active.facade.stop();
|
|
18740
19199
|
}
|
|
19200
|
+
/**
|
|
19201
|
+
* Refuse once destroy() has STARTED (#770). `_initialized` cannot carry this: destroy()
|
|
19202
|
+
* clears it last, so every teardown step is a window in which a concurrent call still reads
|
|
19203
|
+
* a ready Sphere and re-arms it.
|
|
19204
|
+
*/
|
|
19205
|
+
ensureAlive() {
|
|
19206
|
+
if (this._destroyed) {
|
|
19207
|
+
throw new SphereError("Sphere destroyed", "NOT_INITIALIZED");
|
|
19208
|
+
}
|
|
19209
|
+
}
|
|
18741
19210
|
ensureReady() {
|
|
19211
|
+
this.ensureAlive();
|
|
18742
19212
|
if (!this._initialized) {
|
|
18743
19213
|
throw new SphereError("Sphere not initialized", "NOT_INITIALIZED");
|
|
18744
19214
|
}
|
|
@@ -18788,7 +19258,6 @@ var createSphere = Sphere.create.bind(Sphere);
|
|
|
18788
19258
|
var loadSphere = Sphere.load.bind(Sphere);
|
|
18789
19259
|
var importSphere = Sphere.import.bind(Sphere);
|
|
18790
19260
|
var initSphere = Sphere.init.bind(Sphere);
|
|
18791
|
-
var getSphere = Sphere.getInstance.bind(Sphere);
|
|
18792
19261
|
var sphereExists = Sphere.exists.bind(Sphere);
|
|
18793
19262
|
|
|
18794
19263
|
// core/currency.ts
|
|
@@ -18966,28 +19435,59 @@ async function checkWebSocket(url, timeoutMs) {
|
|
|
18966
19435
|
};
|
|
18967
19436
|
});
|
|
18968
19437
|
}
|
|
19438
|
+
var HEALTH_PROBE_STATE_ID = "00".repeat(32);
|
|
19439
|
+
function readBlockNumber(body) {
|
|
19440
|
+
if (typeof body !== "object" || body === null) return null;
|
|
19441
|
+
const result = body.result;
|
|
19442
|
+
if (typeof result !== "object" || result === null) return null;
|
|
19443
|
+
const n = result.blockNumber;
|
|
19444
|
+
if (typeof n === "string") return /^\d+$/.test(n) ? n : null;
|
|
19445
|
+
if (typeof n === "number") return Number.isInteger(n) && n >= 0 ? String(n) : null;
|
|
19446
|
+
return null;
|
|
19447
|
+
}
|
|
19448
|
+
function readRpcError(body) {
|
|
19449
|
+
if (typeof body !== "object" || body === null) return null;
|
|
19450
|
+
const err = body.error;
|
|
19451
|
+
if (typeof err === "string") return err;
|
|
19452
|
+
if (typeof err === "object" && err !== null) {
|
|
19453
|
+
const message = err.message;
|
|
19454
|
+
if (typeof message === "string") return message;
|
|
19455
|
+
return JSON.stringify(err);
|
|
19456
|
+
}
|
|
19457
|
+
return null;
|
|
19458
|
+
}
|
|
19459
|
+
function oracleVerdict(url, responseTimeMs, response, body) {
|
|
19460
|
+
if (readBlockNumber(body) !== null) return { healthy: true, url, responseTimeMs };
|
|
19461
|
+
const rpcError = readRpcError(body);
|
|
19462
|
+
return {
|
|
19463
|
+
healthy: false,
|
|
19464
|
+
url,
|
|
19465
|
+
responseTimeMs,
|
|
19466
|
+
error: rpcError ?? (response.ok ? "aggregator answered without a block height" : `HTTP ${String(response.status)} ${response.statusText}`)
|
|
19467
|
+
};
|
|
19468
|
+
}
|
|
18969
19469
|
async function checkOracle(url, timeoutMs) {
|
|
18970
19470
|
const startTime = Date.now();
|
|
19471
|
+
const controller = new AbortController();
|
|
19472
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18971
19473
|
try {
|
|
18972
|
-
const controller = new AbortController();
|
|
18973
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18974
19474
|
const response = await fetch(url, {
|
|
18975
19475
|
method: "POST",
|
|
18976
19476
|
headers: { "Content-Type": "application/json" },
|
|
18977
|
-
body: JSON.stringify({
|
|
19477
|
+
body: JSON.stringify({
|
|
19478
|
+
jsonrpc: "2.0",
|
|
19479
|
+
id: 1,
|
|
19480
|
+
method: "get_block_height",
|
|
19481
|
+
params: { stateId: HEALTH_PROBE_STATE_ID }
|
|
19482
|
+
}),
|
|
18978
19483
|
signal: controller.signal
|
|
18979
19484
|
});
|
|
18980
|
-
clearTimeout(timer);
|
|
18981
19485
|
const responseTimeMs = Date.now() - startTime;
|
|
18982
|
-
|
|
18983
|
-
|
|
19486
|
+
const body = await response.json().catch(() => null);
|
|
19487
|
+
if (controller.signal.aborted) {
|
|
19488
|
+
return { healthy: false, url, responseTimeMs, error: `Connection timeout after ${timeoutMs}ms` };
|
|
18984
19489
|
}
|
|
18985
|
-
return
|
|
18986
|
-
healthy: false,
|
|
18987
|
-
url,
|
|
18988
|
-
responseTimeMs,
|
|
18989
|
-
error: `HTTP ${response.status} ${response.statusText}`
|
|
18990
|
-
};
|
|
19490
|
+
return oracleVerdict(url, responseTimeMs, response, body);
|
|
18991
19491
|
} catch (err) {
|
|
18992
19492
|
return {
|
|
18993
19493
|
healthy: false,
|
|
@@ -18995,6 +19495,8 @@ async function checkOracle(url, timeoutMs) {
|
|
|
18995
19495
|
responseTimeMs: null,
|
|
18996
19496
|
error: err instanceof Error ? err.name === "AbortError" ? `Connection timeout after ${timeoutMs}ms` : err.message : String(err)
|
|
18997
19497
|
};
|
|
19498
|
+
} finally {
|
|
19499
|
+
clearTimeout(timer);
|
|
18998
19500
|
}
|
|
18999
19501
|
}
|
|
19000
19502
|
async function runCustomCheck(name, checkFn, timeoutMs) {
|
|
@@ -19076,7 +19578,6 @@ async function runCustomCheck(name, checkFn, timeoutMs) {
|
|
|
19076
19578
|
generateMnemonic,
|
|
19077
19579
|
generateRandomKey,
|
|
19078
19580
|
getPublicKey,
|
|
19079
|
-
getSphere,
|
|
19080
19581
|
hash160,
|
|
19081
19582
|
hashSignMessage,
|
|
19082
19583
|
hexToBytes,
|