@unicitylabs/sphere-sdk 0.15.0 → 0.16.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/dist/connect/index.cjs +14 -23
- 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 +14 -23
- 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 +14 -23
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +14 -23
- 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 +13 -22
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +13 -22
- 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 +1 -1
package/dist/core/index.js
CHANGED
|
@@ -97,6 +97,58 @@ var logger = {
|
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
// core/global-cell.ts
|
|
101
|
+
var CELLS_KEY = "__sphere_sdk_cells_v1__";
|
|
102
|
+
var bundleLocalBag = null;
|
|
103
|
+
function isObject(value) {
|
|
104
|
+
return typeof value === "object" && value !== null;
|
|
105
|
+
}
|
|
106
|
+
function installBag(host, fresh) {
|
|
107
|
+
try {
|
|
108
|
+
Object.defineProperty(host, CELLS_KEY, {
|
|
109
|
+
value: fresh,
|
|
110
|
+
writable: true,
|
|
111
|
+
configurable: true,
|
|
112
|
+
enumerable: false
|
|
113
|
+
});
|
|
114
|
+
return host[CELLS_KEY] === fresh;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function cellBag() {
|
|
120
|
+
if (bundleLocalBag) return bundleLocalBag;
|
|
121
|
+
const host = globalThis;
|
|
122
|
+
const found = host[CELLS_KEY];
|
|
123
|
+
if (isObject(found)) return found;
|
|
124
|
+
const fresh = {};
|
|
125
|
+
if (!installBag(host, fresh)) bundleLocalBag = fresh;
|
|
126
|
+
return fresh;
|
|
127
|
+
}
|
|
128
|
+
function accepted(intact, candidate) {
|
|
129
|
+
try {
|
|
130
|
+
return intact(candidate);
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function put(bag, name, cell) {
|
|
136
|
+
try {
|
|
137
|
+
bag[name] = cell;
|
|
138
|
+
if (bag[name] === cell) return;
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
if (bag !== bundleLocalBag) bundleLocalBag = { [name]: cell };
|
|
142
|
+
}
|
|
143
|
+
function sharedCell(name, create, intact) {
|
|
144
|
+
const bag = cellBag();
|
|
145
|
+
const found = bag[name];
|
|
146
|
+
if (isObject(found) && accepted(intact, found)) return found;
|
|
147
|
+
const fresh = create();
|
|
148
|
+
put(bag, name, fresh);
|
|
149
|
+
return fresh;
|
|
150
|
+
}
|
|
151
|
+
|
|
100
152
|
// core/errors.ts
|
|
101
153
|
var SphereError = class extends Error {
|
|
102
154
|
code;
|
|
@@ -161,6 +213,11 @@ function isSphereError(err) {
|
|
|
161
213
|
return err instanceof SphereError;
|
|
162
214
|
}
|
|
163
215
|
|
|
216
|
+
// storage/tracked-addresses.ts
|
|
217
|
+
function isDerivableIndex(value) {
|
|
218
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
|
|
219
|
+
}
|
|
220
|
+
|
|
164
221
|
// transport/MultiAddressTransportMux.ts
|
|
165
222
|
import { Buffer as Buffer3 } from "buffer";
|
|
166
223
|
import {
|
|
@@ -711,7 +768,6 @@ function getAddressId(directAddress) {
|
|
|
711
768
|
return `DIRECT_${first}_${last}`;
|
|
712
769
|
}
|
|
713
770
|
var DEFAULT_NOSTR_RELAYS = [
|
|
714
|
-
"wss://relay.unicity.network",
|
|
715
771
|
"wss://relay.damus.io",
|
|
716
772
|
"wss://nos.lol",
|
|
717
773
|
"wss://relay.nostr.band"
|
|
@@ -758,11 +814,8 @@ var NIP29_KINDS = {
|
|
|
758
814
|
/** Relay-signed group roles */
|
|
759
815
|
GROUP_ROLES: 39003
|
|
760
816
|
};
|
|
761
|
-
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
762
|
-
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
763
817
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
764
818
|
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
765
|
-
var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
|
|
766
819
|
var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
|
|
767
820
|
var TEST_NOSTR_RELAYS = [
|
|
768
821
|
"wss://nostr-relay.testnet.unicity.network"
|
|
@@ -773,10 +826,19 @@ var DEFAULT_GROUP_RELAYS = [
|
|
|
773
826
|
var NETWORKS = {
|
|
774
827
|
mainnet: {
|
|
775
828
|
name: "Mainnet",
|
|
776
|
-
|
|
777
|
-
|
|
829
|
+
networkId: 1,
|
|
830
|
+
// v3 state-transition gateway (networkId 1 comes from the trust base). apiKey is env-injected;
|
|
831
|
+
// unlike testnet2's, a mainnet gateway key is a SECRET — never commit one.
|
|
832
|
+
aggregatorUrl: "https://gateway.mainnet.unicity.network",
|
|
833
|
+
// Mainnet has no relay of its own yet — it shares the testnet relay until one is stood up.
|
|
834
|
+
// Consequence while shared: nametag bindings for both networks live in ONE namespace, and
|
|
835
|
+
// since bindings carry no network the cross-network recipient guard can only signal (#734).
|
|
836
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
778
837
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
779
|
-
|
|
838
|
+
// Published, but currently only the non-fungible base token type — no fungible
|
|
839
|
+
// coins yet, so those still miss the registry (decimals 0, symbol falls back to
|
|
840
|
+
// six hex chars). Presentation only: the money path treats coinId as opaque bytes.
|
|
841
|
+
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json"
|
|
780
842
|
},
|
|
781
843
|
// v1 cutover: 'testnet' now POINTS AT TESTNET2 (the v2 gateway network). The
|
|
782
844
|
// old goggregator testnet spoke the removed v1 protocol — a v2 engine cannot
|
|
@@ -800,19 +862,10 @@ var NETWORKS = {
|
|
|
800
862
|
// reuse testnet infra (shared relays/ipfs)
|
|
801
863
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
802
864
|
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
|
|
803
|
-
},
|
|
804
|
-
// NOTE: mainnet/dev still point at v1-era aggregators. The v2 engine cannot
|
|
805
|
-
// operate against them until their gateways are cut over to the v2 protocol —
|
|
806
|
-
// wallet operations on these networks fail loudly (AGGREGATOR_ERROR) until then.
|
|
807
|
-
dev: {
|
|
808
|
-
name: "Development",
|
|
809
|
-
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
810
|
-
nostrRelays: TEST_NOSTR_RELAYS,
|
|
811
|
-
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
812
|
-
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
813
865
|
}
|
|
814
866
|
};
|
|
815
867
|
var SPHERE_NETWORKS = {
|
|
868
|
+
mainnet: { id: NETWORKS.mainnet.networkId, name: "mainnet" },
|
|
816
869
|
testnet2: { id: NETWORKS.testnet2.networkId, name: "testnet2" }
|
|
817
870
|
};
|
|
818
871
|
var TIMEOUTS = {
|
|
@@ -988,14 +1041,10 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
988
1041
|
});
|
|
989
1042
|
}
|
|
990
1043
|
});
|
|
991
|
-
await
|
|
992
|
-
this.nostrClient
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
996
|
-
)), this.config.timeout)
|
|
997
|
-
)
|
|
998
|
-
]);
|
|
1044
|
+
await this.connectWithDeadline(
|
|
1045
|
+
this.nostrClient,
|
|
1046
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1047
|
+
);
|
|
999
1048
|
if (!this.nostrClient.isConnected()) {
|
|
1000
1049
|
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
1001
1050
|
}
|
|
@@ -1010,6 +1059,27 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1010
1059
|
throw error;
|
|
1011
1060
|
}
|
|
1012
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Race one client's relay connect against the configured timeout.
|
|
1064
|
+
*
|
|
1065
|
+
* The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
|
|
1066
|
+
* cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
|
|
1067
|
+
* for the whole `config.timeout` after the call has already returned (and
|
|
1068
|
+
* then rejects a promise nobody is listening to any more).
|
|
1069
|
+
*/
|
|
1070
|
+
async connectWithDeadline(client, timeoutMessage) {
|
|
1071
|
+
let timer;
|
|
1072
|
+
try {
|
|
1073
|
+
await Promise.race([
|
|
1074
|
+
client.connect(...this.config.relays),
|
|
1075
|
+
new Promise((_, reject) => {
|
|
1076
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), this.config.timeout);
|
|
1077
|
+
})
|
|
1078
|
+
]);
|
|
1079
|
+
} finally {
|
|
1080
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1013
1083
|
async disconnect() {
|
|
1014
1084
|
if (this.nostrClient) {
|
|
1015
1085
|
this.nostrClient.disconnect();
|
|
@@ -1128,19 +1198,22 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1128
1198
|
// TransportProvider Implementation
|
|
1129
1199
|
// ===========================================================================
|
|
1130
1200
|
async setIdentity(identity) {
|
|
1131
|
-
this.identity = identity;
|
|
1132
|
-
this.processedEventIds.clear();
|
|
1133
|
-
this.lastEventTs = 0;
|
|
1134
|
-
this.lastDmEventTs = 0;
|
|
1135
|
-
this.fallbackDmSince = null;
|
|
1136
1201
|
const secretKey = Buffer2.from(identity.privateKey, "hex");
|
|
1137
|
-
|
|
1138
|
-
const nostrPubkey =
|
|
1139
|
-
logger.debug("Nostr", "Identity
|
|
1202
|
+
const nextKeyManager = NostrKeyManager.fromPrivateKey(secretKey);
|
|
1203
|
+
const nostrPubkey = nextKeyManager.getPublicKeyHex();
|
|
1204
|
+
logger.debug("Nostr", "Identity staged, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
|
|
1205
|
+
const applyStagedIdentity = () => {
|
|
1206
|
+
this.identity = identity;
|
|
1207
|
+
this.keyManager = nextKeyManager;
|
|
1208
|
+
this.processedEventIds.clear();
|
|
1209
|
+
this.lastEventTs = 0;
|
|
1210
|
+
this.lastDmEventTs = 0;
|
|
1211
|
+
this.fallbackDmSince = null;
|
|
1212
|
+
};
|
|
1140
1213
|
if (this.nostrClient && this.status === "connected") {
|
|
1141
1214
|
logger.debug("Nostr", "Identity changed while connected - recreating NostrClient");
|
|
1142
1215
|
const oldClient = this.nostrClient;
|
|
1143
|
-
|
|
1216
|
+
const nextClient = new NostrClient(nextKeyManager, {
|
|
1144
1217
|
autoReconnect: this.config.autoReconnect,
|
|
1145
1218
|
reconnectIntervalMs: this.config.reconnectDelay,
|
|
1146
1219
|
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
@@ -1149,7 +1222,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1149
1222
|
queryTimeoutMs: 2e4
|
|
1150
1223
|
// see same option above for rationale
|
|
1151
1224
|
});
|
|
1152
|
-
|
|
1225
|
+
nextClient.addConnectionListener({
|
|
1153
1226
|
onConnect: (url) => {
|
|
1154
1227
|
logger.debug("Nostr", "NostrClient connected to relay:", url);
|
|
1155
1228
|
},
|
|
@@ -1163,18 +1236,43 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1163
1236
|
logger.debug("Nostr", "NostrClient reconnected to relay:", url);
|
|
1164
1237
|
}
|
|
1165
1238
|
});
|
|
1166
|
-
|
|
1167
|
-
this.
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1239
|
+
try {
|
|
1240
|
+
await this.connectWithDeadline(
|
|
1241
|
+
nextClient,
|
|
1242
|
+
`Transport reconnection timed out after ${this.config.timeout}ms`
|
|
1243
|
+
);
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
try {
|
|
1246
|
+
nextClient.disconnect();
|
|
1247
|
+
} catch {
|
|
1248
|
+
}
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
if (this.nostrClient !== oldClient || this.status !== "connected") {
|
|
1252
|
+
try {
|
|
1253
|
+
nextClient.disconnect();
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
throw new SphereError(
|
|
1257
|
+
"Transport client changed or left the connected state during setIdentity - identity not applied",
|
|
1258
|
+
"TRANSPORT_ERROR"
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
this.nostrClient = nextClient;
|
|
1262
|
+
applyStagedIdentity();
|
|
1263
|
+
try {
|
|
1264
|
+
await this.subscribeToEvents();
|
|
1265
|
+
} finally {
|
|
1266
|
+
try {
|
|
1267
|
+
oldClient.disconnect();
|
|
1268
|
+
} catch {
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1176
1271
|
} else if (this.isConnected()) {
|
|
1272
|
+
applyStagedIdentity();
|
|
1177
1273
|
await this.subscribeToEvents();
|
|
1274
|
+
} else {
|
|
1275
|
+
applyStagedIdentity();
|
|
1178
1276
|
}
|
|
1179
1277
|
}
|
|
1180
1278
|
setFallbackSince(sinceSeconds) {
|
|
@@ -8039,6 +8137,12 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8039
8137
|
lastRefreshAt = 0;
|
|
8040
8138
|
refreshPromise = null;
|
|
8041
8139
|
initialLoadPromise = null;
|
|
8140
|
+
/** Bumped on every remoteUrl change; loads started in an older generation are discarded. */
|
|
8141
|
+
generation = 0;
|
|
8142
|
+
/** Set by dispose(). A disposed registry starts no work and applies no late result. */
|
|
8143
|
+
disposed = false;
|
|
8144
|
+
/** Cancels the in-flight fetch (and its abort timer) when the registry is disposed. */
|
|
8145
|
+
inFlight = null;
|
|
8042
8146
|
constructor() {
|
|
8043
8147
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
8044
8148
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
@@ -8067,27 +8171,90 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8067
8171
|
* @param options.autoRefresh - Start auto-refresh immediately (default: true)
|
|
8068
8172
|
*/
|
|
8069
8173
|
static configure(options) {
|
|
8070
|
-
|
|
8174
|
+
_TokenRegistry.getInstance().applyConfig(options);
|
|
8175
|
+
}
|
|
8176
|
+
/**
|
|
8177
|
+
* Create an INDEPENDENT registry, not the process-global singleton.
|
|
8178
|
+
*
|
|
8179
|
+
* One Sphere owns one of these, so two Spheres on different networks cannot wipe each
|
|
8180
|
+
* other's definitions — the singleton's `configure()` reaches into whatever instance
|
|
8181
|
+
* exists and repoints it, which is how a mainnet init silently retargeted a live
|
|
8182
|
+
* testnet2 wallet's decimals. Dispose it when the owner is destroyed.
|
|
8183
|
+
*/
|
|
8184
|
+
static create(options) {
|
|
8185
|
+
const registry = new _TokenRegistry();
|
|
8186
|
+
registry.applyConfig(options);
|
|
8187
|
+
return registry;
|
|
8188
|
+
}
|
|
8189
|
+
/** The body of configure(), on the instance, so owned registries share it exactly. */
|
|
8190
|
+
applyConfig(options) {
|
|
8191
|
+
if (this.disposed) return;
|
|
8071
8192
|
if (options.remoteUrl !== void 0) {
|
|
8072
|
-
|
|
8193
|
+
const previous = this.remoteUrl;
|
|
8194
|
+
if (previous && previous !== options.remoteUrl) {
|
|
8195
|
+
this.applyDefinitions([]);
|
|
8196
|
+
this.lastRefreshAt = 0;
|
|
8197
|
+
this.generation++;
|
|
8198
|
+
this.refreshPromise = null;
|
|
8199
|
+
}
|
|
8200
|
+
this.remoteUrl = options.remoteUrl;
|
|
8073
8201
|
}
|
|
8074
8202
|
if (options.storage !== void 0) {
|
|
8075
|
-
|
|
8203
|
+
this.storage = options.storage;
|
|
8076
8204
|
}
|
|
8077
8205
|
if (options.refreshIntervalMs !== void 0) {
|
|
8078
|
-
|
|
8206
|
+
this.refreshIntervalMs = options.refreshIntervalMs;
|
|
8079
8207
|
}
|
|
8080
8208
|
const autoRefresh = options.autoRefresh ?? true;
|
|
8081
|
-
|
|
8209
|
+
this.initialLoadPromise = this.performInitialLoad(autoRefresh);
|
|
8210
|
+
}
|
|
8211
|
+
/**
|
|
8212
|
+
* Stop this registry for good: no timer, no late apply, no late cache write.
|
|
8213
|
+
*
|
|
8214
|
+
* Sphere.destroy() calls this on the registry it owns. Without it a discarded Sphere
|
|
8215
|
+
* leaves an hourly fetch running forever — nothing in this file calls unref(), so in
|
|
8216
|
+
* Node it also keeps the event loop alive.
|
|
8217
|
+
*/
|
|
8218
|
+
dispose() {
|
|
8219
|
+
if (this.disposed) return;
|
|
8220
|
+
this.disposed = true;
|
|
8221
|
+
this.stopAutoRefresh();
|
|
8222
|
+
this.inFlight?.abort();
|
|
8223
|
+
this.inFlight = null;
|
|
8224
|
+
this.generation++;
|
|
8225
|
+
this.refreshPromise = null;
|
|
8226
|
+
this.initialLoadPromise = null;
|
|
8227
|
+
}
|
|
8228
|
+
/** Whether dispose() has been called. Reads still work; they are simply frozen. */
|
|
8229
|
+
get isDisposed() {
|
|
8230
|
+
return this.disposed;
|
|
8231
|
+
}
|
|
8232
|
+
/** Per-instance readiness — the same contract as the static, for an owned registry. */
|
|
8233
|
+
async waitForReady(timeoutMs = 1e4) {
|
|
8234
|
+
if (!this.initialLoadPromise) {
|
|
8235
|
+
return this.definitionsById.size > 0;
|
|
8236
|
+
}
|
|
8237
|
+
if (timeoutMs <= 0) {
|
|
8238
|
+
return this.initialLoadPromise;
|
|
8239
|
+
}
|
|
8240
|
+
let timer;
|
|
8241
|
+
try {
|
|
8242
|
+
return await Promise.race([
|
|
8243
|
+
this.initialLoadPromise,
|
|
8244
|
+
new Promise((resolve) => {
|
|
8245
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
8246
|
+
})
|
|
8247
|
+
]);
|
|
8248
|
+
} finally {
|
|
8249
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8250
|
+
}
|
|
8082
8251
|
}
|
|
8083
8252
|
/**
|
|
8084
8253
|
* Reset the singleton instance (useful for testing).
|
|
8085
8254
|
* Stops auto-refresh if running.
|
|
8086
8255
|
*/
|
|
8087
8256
|
static resetInstance() {
|
|
8088
|
-
|
|
8089
|
-
_TokenRegistry.instance.stopAutoRefresh();
|
|
8090
|
-
}
|
|
8257
|
+
_TokenRegistry.instance?.dispose();
|
|
8091
8258
|
_TokenRegistry.instance = null;
|
|
8092
8259
|
}
|
|
8093
8260
|
/**
|
|
@@ -8103,17 +8270,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8103
8270
|
* @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
|
|
8104
8271
|
*/
|
|
8105
8272
|
static async waitForReady(timeoutMs = 1e4) {
|
|
8106
|
-
|
|
8107
|
-
if (!instance.initialLoadPromise) {
|
|
8108
|
-
return instance.definitionsById.size > 0;
|
|
8109
|
-
}
|
|
8110
|
-
if (timeoutMs <= 0) {
|
|
8111
|
-
return instance.initialLoadPromise;
|
|
8112
|
-
}
|
|
8113
|
-
return Promise.race([
|
|
8114
|
-
instance.initialLoadPromise,
|
|
8115
|
-
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
|
|
8116
|
-
]);
|
|
8273
|
+
return _TokenRegistry.getInstance().waitForReady(timeoutMs);
|
|
8117
8274
|
}
|
|
8118
8275
|
// ===========================================================================
|
|
8119
8276
|
// Initial Load
|
|
@@ -8123,6 +8280,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8123
8280
|
* After initial data is available, start periodic auto-refresh if configured.
|
|
8124
8281
|
*/
|
|
8125
8282
|
async performInitialLoad(autoRefresh) {
|
|
8283
|
+
if (this.disposed) return false;
|
|
8126
8284
|
let loaded = false;
|
|
8127
8285
|
if (this.storage) {
|
|
8128
8286
|
loaded = await this.loadFromCache();
|
|
@@ -8135,6 +8293,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8135
8293
|
}
|
|
8136
8294
|
if (autoRefresh && this.remoteUrl) {
|
|
8137
8295
|
loaded = await this.refreshFromRemote();
|
|
8296
|
+
if (this.disposed) return loaded;
|
|
8138
8297
|
this.stopAutoRefresh();
|
|
8139
8298
|
this.refreshTimer = setInterval(() => {
|
|
8140
8299
|
this.refreshFromRemote();
|
|
@@ -8165,6 +8324,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8165
8324
|
*/
|
|
8166
8325
|
async loadFromCache() {
|
|
8167
8326
|
if (!this.storage) return false;
|
|
8327
|
+
const gen = this.generation;
|
|
8168
8328
|
try {
|
|
8169
8329
|
const { data: dataKey, ts: tsKey } = this.cacheKeys();
|
|
8170
8330
|
const [cached, cachedTs] = await Promise.all([
|
|
@@ -8179,6 +8339,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8179
8339
|
if (this.lastRefreshAt > ts) return false;
|
|
8180
8340
|
const data = JSON.parse(cached);
|
|
8181
8341
|
if (!this.isValidDefinitionsArray(data)) return false;
|
|
8342
|
+
if (gen !== this.generation) return false;
|
|
8182
8343
|
this.applyDefinitions(data);
|
|
8183
8344
|
this.lastRefreshAt = ts;
|
|
8184
8345
|
return true;
|
|
@@ -8233,32 +8394,51 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8233
8394
|
* Concurrent calls are deduplicated — only one fetch runs at a time.
|
|
8234
8395
|
*/
|
|
8235
8396
|
async refreshFromRemote() {
|
|
8236
|
-
if (!this.remoteUrl) {
|
|
8397
|
+
if (this.disposed || !this.remoteUrl) {
|
|
8237
8398
|
return false;
|
|
8238
8399
|
}
|
|
8239
8400
|
if (this.refreshPromise) {
|
|
8240
8401
|
return this.refreshPromise;
|
|
8241
8402
|
}
|
|
8242
|
-
|
|
8403
|
+
const attempt = this.doRefresh();
|
|
8404
|
+
this.refreshPromise = attempt;
|
|
8243
8405
|
try {
|
|
8244
|
-
return await
|
|
8406
|
+
return await attempt;
|
|
8245
8407
|
} finally {
|
|
8246
|
-
this.refreshPromise = null;
|
|
8408
|
+
if (this.refreshPromise === attempt) this.refreshPromise = null;
|
|
8247
8409
|
}
|
|
8248
8410
|
}
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8411
|
+
/**
|
|
8412
|
+
* Fetch with a bounded timeout, exposing a cancel hook to dispose().
|
|
8413
|
+
*
|
|
8414
|
+
* Both the request and its abort timer keep Node's event loop alive for the full
|
|
8415
|
+
* FETCH_TIMEOUT_MS, so a registry disposed mid-flight must cancel them rather than
|
|
8416
|
+
* merely ignore the result.
|
|
8417
|
+
*/
|
|
8418
|
+
async fetchCancellable(url) {
|
|
8419
|
+
const controller = new AbortController();
|
|
8420
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
8421
|
+
this.inFlight = {
|
|
8422
|
+
abort: () => {
|
|
8260
8423
|
clearTimeout(timer);
|
|
8424
|
+
controller.abort();
|
|
8261
8425
|
}
|
|
8426
|
+
};
|
|
8427
|
+
try {
|
|
8428
|
+
return await fetch(url, {
|
|
8429
|
+
headers: { Accept: "application/json" },
|
|
8430
|
+
signal: controller.signal
|
|
8431
|
+
});
|
|
8432
|
+
} finally {
|
|
8433
|
+
clearTimeout(timer);
|
|
8434
|
+
this.inFlight = null;
|
|
8435
|
+
}
|
|
8436
|
+
}
|
|
8437
|
+
async doRefresh() {
|
|
8438
|
+
const url = this.remoteUrl;
|
|
8439
|
+
const gen = this.generation;
|
|
8440
|
+
try {
|
|
8441
|
+
const response = await this.fetchCancellable(url);
|
|
8262
8442
|
if (!response.ok) {
|
|
8263
8443
|
logger.warn("TokenRegistry", `Remote fetch failed: HTTP ${response.status} ${response.statusText}`);
|
|
8264
8444
|
return false;
|
|
@@ -8268,6 +8448,10 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8268
8448
|
logger.warn("TokenRegistry", "Remote data is not a valid token definitions array");
|
|
8269
8449
|
return false;
|
|
8270
8450
|
}
|
|
8451
|
+
if (gen !== this.generation) {
|
|
8452
|
+
logger.debug("TokenRegistry", `Discarding stale refresh for ${url} (network changed)`);
|
|
8453
|
+
return false;
|
|
8454
|
+
}
|
|
8271
8455
|
const definitions = data;
|
|
8272
8456
|
this.applyDefinitions(definitions);
|
|
8273
8457
|
this.lastRefreshAt = Date.now();
|
|
@@ -8284,6 +8468,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8284
8468
|
* Does an immediate fetch, then repeats at the configured interval.
|
|
8285
8469
|
*/
|
|
8286
8470
|
startAutoRefresh(intervalMs) {
|
|
8471
|
+
if (this.disposed) return;
|
|
8287
8472
|
this.stopAutoRefresh();
|
|
8288
8473
|
if (intervalMs !== void 0) {
|
|
8289
8474
|
this.refreshIntervalMs = intervalMs;
|
|
@@ -10113,12 +10298,58 @@ var SphereTokenEngine = class {
|
|
|
10113
10298
|
|
|
10114
10299
|
// token-engine/factory.ts
|
|
10115
10300
|
var DEFAULT_VERIFICATION_POOL_SIZE = 4;
|
|
10301
|
+
function disposedError() {
|
|
10302
|
+
return new SphereError(
|
|
10303
|
+
"Verification worker pool disposed \u2014 the in-flight verification was cancelled",
|
|
10304
|
+
"MODULE_DESTROYED"
|
|
10305
|
+
);
|
|
10306
|
+
}
|
|
10116
10307
|
var ConfiguredWorkerTokenVerifier = class extends WorkerTokenVerifier {
|
|
10117
10308
|
constructor(spawn, poolSize) {
|
|
10118
10309
|
super(poolSize);
|
|
10119
10310
|
this.spawn = spawn;
|
|
10120
10311
|
}
|
|
10312
|
+
disposed = false;
|
|
10313
|
+
/** One rejector per IN-FLIGHT verify(), dropped on settle; empty when idle. */
|
|
10314
|
+
cancellations = /* @__PURE__ */ new Set();
|
|
10315
|
+
/** Verifications awaiting a verdict — back to 0 whenever the verifier is idle. */
|
|
10316
|
+
get pendingCancellations() {
|
|
10317
|
+
return this.cancellations.size;
|
|
10318
|
+
}
|
|
10319
|
+
/** Rejects (see the class note) if `dispose()` lands before the pool answers. */
|
|
10320
|
+
verify(...args) {
|
|
10321
|
+
if (this.disposed) return Promise.reject(disposedError());
|
|
10322
|
+
const verdict = super.verify(...args);
|
|
10323
|
+
return new Promise((resolve, reject) => {
|
|
10324
|
+
const cancel = () => reject(disposedError());
|
|
10325
|
+
this.cancellations.add(cancel);
|
|
10326
|
+
if (this.disposed) {
|
|
10327
|
+
this.cancellations.delete(cancel);
|
|
10328
|
+
cancel();
|
|
10329
|
+
}
|
|
10330
|
+
void verdict.then(
|
|
10331
|
+
(result) => {
|
|
10332
|
+
this.cancellations.delete(cancel);
|
|
10333
|
+
resolve(result);
|
|
10334
|
+
},
|
|
10335
|
+
(error) => {
|
|
10336
|
+
this.cancellations.delete(cancel);
|
|
10337
|
+
reject(error);
|
|
10338
|
+
}
|
|
10339
|
+
);
|
|
10340
|
+
});
|
|
10341
|
+
}
|
|
10342
|
+
/** Idempotent — Sphere.setOracleApiKey disposes the replaced engine twice (facade + caller). */
|
|
10343
|
+
dispose() {
|
|
10344
|
+
if (this.disposed) return;
|
|
10345
|
+
this.disposed = true;
|
|
10346
|
+
super.dispose();
|
|
10347
|
+
const cancelling = [...this.cancellations];
|
|
10348
|
+
this.cancellations.clear();
|
|
10349
|
+
for (const cancel of cancelling) cancel();
|
|
10350
|
+
}
|
|
10121
10351
|
createWorker() {
|
|
10352
|
+
if (this.disposed) throw disposedError();
|
|
10122
10353
|
return this.spawn();
|
|
10123
10354
|
}
|
|
10124
10355
|
};
|
|
@@ -12947,12 +13178,13 @@ var Receive = class {
|
|
|
12947
13178
|
return this.drainFlight.run(() => this.doDrain());
|
|
12948
13179
|
}
|
|
12949
13180
|
start(pollIntervalMs = POLL_INTERVAL_MS) {
|
|
12950
|
-
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() =>
|
|
12951
|
-
|
|
12952
|
-
|
|
12953
|
-
|
|
12954
|
-
|
|
12955
|
-
|
|
13181
|
+
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() => this.spawnDrain()) ?? null;
|
|
13182
|
+
this.pollTimer ??= setInterval(() => this.spawnDrain(), pollIntervalMs);
|
|
13183
|
+
}
|
|
13184
|
+
spawnDrain() {
|
|
13185
|
+
const op = this.drainOnce();
|
|
13186
|
+
if (this.deps.track !== void 0) this.deps.track(op);
|
|
13187
|
+
else void op;
|
|
12956
13188
|
}
|
|
12957
13189
|
stop() {
|
|
12958
13190
|
if (this.pollTimer !== null) clearInterval(this.pollTimer);
|
|
@@ -14055,6 +14287,8 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
|
|
|
14055
14287
|
);
|
|
14056
14288
|
},
|
|
14057
14289
|
syncEpoch: deps.syncEpoch,
|
|
14290
|
+
// #770: the poll/wake drains Receive spawns itself must hold stop() too.
|
|
14291
|
+
track: hooks.track,
|
|
14058
14292
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
14059
14293
|
});
|
|
14060
14294
|
}
|
|
@@ -14197,7 +14431,8 @@ var PaymentsFacade = class {
|
|
|
14197
14431
|
await Promise.allSettled([...this.pendingOps]);
|
|
14198
14432
|
}
|
|
14199
14433
|
}
|
|
14200
|
-
/** Swaps what FUTURE operations snapshot
|
|
14434
|
+
/** Swaps what FUTURE operations snapshot. Chain ops finish on the old engine, but it is
|
|
14435
|
+
* DISPOSED here, so one mid-`verify()` is cancelled — MODULE_DESTROYED, #770(4). */
|
|
14201
14436
|
setEngine(next) {
|
|
14202
14437
|
const previous = this.currentEngine ?? this.deps.engineRef();
|
|
14203
14438
|
this.currentEngine = next;
|
|
@@ -15922,7 +16157,7 @@ function composePaymentsV2(spec) {
|
|
|
15922
16157
|
}),
|
|
15923
16158
|
engineRef: spec.engineRef,
|
|
15924
16159
|
kv,
|
|
15925
|
-
registry:
|
|
16160
|
+
registry: host.registry,
|
|
15926
16161
|
...host.price !== null ? { price: host.price } : {},
|
|
15927
16162
|
emit: host.emit,
|
|
15928
16163
|
resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
|
|
@@ -15950,13 +16185,47 @@ async function deriveL3PredicateAddress(privateKey) {
|
|
|
15950
16185
|
const prehashedPublicKey = getPublicKey(sha2562(privateKey, "hex"));
|
|
15951
16186
|
return deriveDirectAddress(hexToBytes4(prehashedPublicKey));
|
|
15952
16187
|
}
|
|
16188
|
+
var lifecycle = sharedCell(
|
|
16189
|
+
"core.sphere.lifecycle@1",
|
|
16190
|
+
() => ({
|
|
16191
|
+
live: /* @__PURE__ */ new Map(),
|
|
16192
|
+
clearGenerations: /* @__PURE__ */ new Map(),
|
|
16193
|
+
objectStoreKeys: /* @__PURE__ */ new WeakMap(),
|
|
16194
|
+
objectStoreSeq: 0
|
|
16195
|
+
}),
|
|
16196
|
+
(cell) => {
|
|
16197
|
+
const c = cell;
|
|
16198
|
+
return c.live instanceof Map && c.clearGenerations instanceof Map && c.objectStoreKeys instanceof WeakMap && typeof c.objectStoreSeq === "number";
|
|
16199
|
+
}
|
|
16200
|
+
);
|
|
15953
16201
|
var Sphere = class _Sphere {
|
|
15954
|
-
//
|
|
15955
|
-
|
|
16202
|
+
// Live Spheres, keyed by the BACKING STORE their provider addresses. NOT a liveness
|
|
16203
|
+
// API: it exists only so clear()/import() tear down the instances whose data they
|
|
16204
|
+
// really erase. Object identity was too narrow — two providers over one dataDir/DB
|
|
16205
|
+
// are different objects but the same file, so clearing through one left the other's
|
|
16206
|
+
// Sphere live over an emptied KV (#766).
|
|
16207
|
+
static _liveByStorage = lifecycle.live;
|
|
16208
|
+
/** Fallback keys for providers that declare no `backingStoreId` — one per object. */
|
|
16209
|
+
static _objectStoreKeys = lifecycle.objectStoreKeys;
|
|
16210
|
+
/**
|
|
16211
|
+
* How many times each backing store has been cleared. An init is invisible to `clear()`
|
|
16212
|
+
* until it PUBLISHES (#767), so a clear cannot destroy one in flight and would wipe the
|
|
16213
|
+
* store under it. Every init records this number before its first storage work and
|
|
16214
|
+
* publication refuses if it moved (#772). Only cleared stores get an entry, so the
|
|
16215
|
+
* strongly-held keys are bounded by the stores a process actually clears.
|
|
16216
|
+
*/
|
|
16217
|
+
static _clearGenerations = lifecycle.clearGenerations;
|
|
15956
16218
|
// One-time best-effort cleanup of the orphaned vesting cache (prior versions).
|
|
15957
16219
|
static _orphanCacheCleaned = false;
|
|
15958
16220
|
// State
|
|
15959
16221
|
_initialized = false;
|
|
16222
|
+
/**
|
|
16223
|
+
* Destroyed latch (#770). Distinct from `_initialized`, which destroy() clears LAST — after
|
|
16224
|
+
* every teardown step — so it cannot mark "teardown has begun". This is set as destroy()'s
|
|
16225
|
+
* very FIRST statement, so the WHOLE teardown window is guarded, not just the instant after
|
|
16226
|
+
* it. Read by ensureAlive() and by the §7 lifecycle mutex.
|
|
16227
|
+
*/
|
|
16228
|
+
_destroyed = false;
|
|
15960
16229
|
_trackedAddressesLoaded = false;
|
|
15961
16230
|
_identity = null;
|
|
15962
16231
|
_masterKey = null;
|
|
@@ -16014,6 +16283,8 @@ var Sphere = class _Sphere {
|
|
|
16014
16283
|
* the rebuild after an api-key change share one pool configuration.
|
|
16015
16284
|
*/
|
|
16016
16285
|
_verification;
|
|
16286
|
+
/** This Sphere's OWN token registry. Disposed by destroy(); never the process global. */
|
|
16287
|
+
_registry = null;
|
|
16017
16288
|
// Events
|
|
16018
16289
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
16019
16290
|
// Provider management
|
|
@@ -16088,7 +16359,7 @@ var Sphere = class _Sphere {
|
|
|
16088
16359
|
* ```
|
|
16089
16360
|
*/
|
|
16090
16361
|
static async init(options) {
|
|
16091
|
-
if (options.debug) logger.configure({ debug:
|
|
16362
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16092
16363
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16093
16364
|
resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16094
16365
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
@@ -16108,6 +16379,7 @@ var Sphere = class _Sphere {
|
|
|
16108
16379
|
market,
|
|
16109
16380
|
communications: options.communications,
|
|
16110
16381
|
password: options.password,
|
|
16382
|
+
verification: options.verification,
|
|
16111
16383
|
discoverAddresses: options.discoverAddresses,
|
|
16112
16384
|
onProgress: options.onProgress
|
|
16113
16385
|
});
|
|
@@ -16144,6 +16416,7 @@ var Sphere = class _Sphere {
|
|
|
16144
16416
|
market,
|
|
16145
16417
|
communications: options.communications,
|
|
16146
16418
|
password: options.password,
|
|
16419
|
+
verification: options.verification,
|
|
16147
16420
|
discoverAddresses: options.discoverAddresses,
|
|
16148
16421
|
onProgress: options.onProgress
|
|
16149
16422
|
});
|
|
@@ -16225,16 +16498,53 @@ var Sphere = class _Sphere {
|
|
|
16225
16498
|
}
|
|
16226
16499
|
TokenRegistry.configure({ remoteUrl: NETWORKS[network].tokenRegistryUrl, storage });
|
|
16227
16500
|
}
|
|
16501
|
+
/**
|
|
16502
|
+
* Build the registry THIS Sphere owns, so its metadata cannot be repointed by another
|
|
16503
|
+
* Sphere's init. The global is still configured above for consumers that read it
|
|
16504
|
+
* directly; this instance is what the payments facade presents from.
|
|
16505
|
+
*/
|
|
16506
|
+
/**
|
|
16507
|
+
* Own a registry for the duration of `bringUp`, disposing it if any of that rejects.
|
|
16508
|
+
*
|
|
16509
|
+
* `bringUp` must cover EVERY fallible step from here until the Sphere is returned to
|
|
16510
|
+
* its caller — until then nobody holds anything they could destroy, so a
|
|
16511
|
+
* registry left behind is unreachable and its hourly fetch runs for the life of the
|
|
16512
|
+
* process. Guarding a named subset of the steps is what failed twice: the guarded region
|
|
16513
|
+
* and the fallible region were separate things, and drifted. Publication is the LAST
|
|
16514
|
+
* guarded step for the same reason: it can refuse, and a refusal must tear down.
|
|
16515
|
+
*/
|
|
16516
|
+
static async withOwnedRegistry(sphere, storage, network, clearGeneration, bringUp) {
|
|
16517
|
+
sphere._registry = _Sphere.createOwnedRegistry(storage, network);
|
|
16518
|
+
try {
|
|
16519
|
+
await bringUp();
|
|
16520
|
+
_Sphere.publishLive(sphere, clearGeneration);
|
|
16521
|
+
} catch (err) {
|
|
16522
|
+
await sphere.destroy().catch((teardownErr) => {
|
|
16523
|
+
logger.warn("Sphere", "teardown after failed initialization also failed:", teardownErr);
|
|
16524
|
+
});
|
|
16525
|
+
throw err;
|
|
16526
|
+
}
|
|
16527
|
+
}
|
|
16528
|
+
static createOwnedRegistry(storage, network) {
|
|
16529
|
+
if (!network) {
|
|
16530
|
+
throw new SphereError(
|
|
16531
|
+
"network is required to build the token registry. Every Sphere entry point must forward options.network.",
|
|
16532
|
+
"INVALID_CONFIG"
|
|
16533
|
+
);
|
|
16534
|
+
}
|
|
16535
|
+
return TokenRegistry.create({ remoteUrl: NETWORKS[network].tokenRegistryUrl, storage });
|
|
16536
|
+
}
|
|
16228
16537
|
/**
|
|
16229
16538
|
* Create new wallet with mnemonic
|
|
16230
16539
|
*/
|
|
16231
16540
|
static async create(options) {
|
|
16232
|
-
if (options.debug) logger.configure({ debug:
|
|
16541
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16233
16542
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16234
16543
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16235
16544
|
if (!options.mnemonic || !_Sphere.validateMnemonic(options.mnemonic)) {
|
|
16236
16545
|
throw new SphereError("Invalid mnemonic", "INVALID_IDENTITY");
|
|
16237
16546
|
}
|
|
16547
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16238
16548
|
if (await _Sphere.exists(options.storage)) {
|
|
16239
16549
|
throw new SphereError("Wallet already exists. Use Sphere.load() or Sphere.clear() first.", "ALREADY_INITIALIZED");
|
|
16240
16550
|
}
|
|
@@ -16262,44 +16572,46 @@ var Sphere = class _Sphere {
|
|
|
16262
16572
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
16263
16573
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
16264
16574
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16265
|
-
await sphere.
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
|
|
16274
|
-
|
|
16275
|
-
|
|
16276
|
-
|
|
16277
|
-
|
|
16278
|
-
|
|
16279
|
-
|
|
16280
|
-
|
|
16281
|
-
|
|
16282
|
-
|
|
16283
|
-
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16575
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16576
|
+
await sphere.initializeProviders();
|
|
16577
|
+
await sphere.initializeModules();
|
|
16578
|
+
progress?.({ step: "finalizing", message: "Finalizing wallet..." });
|
|
16579
|
+
await sphere.finalizeWalletCreation();
|
|
16580
|
+
sphere._initialized = true;
|
|
16581
|
+
await sphere.ensureAddressTracked(0);
|
|
16582
|
+
if (options.nametag) {
|
|
16583
|
+
progress?.({ step: "registering_nametag", message: "Registering nametag..." });
|
|
16584
|
+
await sphere.registerNametag(options.nametag);
|
|
16585
|
+
} else {
|
|
16586
|
+
progress?.({ step: "recovering_nametag", message: "Recovering nametag..." });
|
|
16587
|
+
await sphere.recoverNametagFromTransport();
|
|
16588
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16589
|
+
await sphere.syncIdentityWithTransport();
|
|
16590
|
+
}
|
|
16591
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses) {
|
|
16592
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16593
|
+
try {
|
|
16594
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16595
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16596
|
+
if (result.addresses.length > 0) {
|
|
16597
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16598
|
+
}
|
|
16599
|
+
} catch (err) {
|
|
16600
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16288
16601
|
}
|
|
16289
|
-
} catch (err) {
|
|
16290
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16291
16602
|
}
|
|
16292
|
-
|
|
16293
|
-
|
|
16603
|
+
progress?.({ step: "complete", message: "Wallet created" });
|
|
16604
|
+
});
|
|
16294
16605
|
return sphere;
|
|
16295
16606
|
}
|
|
16296
16607
|
/**
|
|
16297
16608
|
* Load existing wallet from storage
|
|
16298
16609
|
*/
|
|
16299
16610
|
static async load(options) {
|
|
16300
|
-
if (options.debug) logger.configure({ debug:
|
|
16611
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16301
16612
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16302
16613
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16614
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16303
16615
|
if (!await _Sphere.exists(options.storage)) {
|
|
16304
16616
|
throw new SphereError("No wallet found. Use Sphere.create() to create a new wallet.", "NOT_INITIALIZED");
|
|
16305
16617
|
}
|
|
@@ -16326,32 +16638,33 @@ var Sphere = class _Sphere {
|
|
|
16326
16638
|
progress?.({ step: "storing_keys", message: "Loading wallet keys..." });
|
|
16327
16639
|
await sphere.loadIdentityFromStorage();
|
|
16328
16640
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16329
|
-
await sphere.
|
|
16330
|
-
|
|
16331
|
-
|
|
16332
|
-
|
|
16333
|
-
|
|
16334
|
-
|
|
16335
|
-
|
|
16336
|
-
|
|
16337
|
-
|
|
16338
|
-
|
|
16339
|
-
|
|
16340
|
-
|
|
16341
|
-
|
|
16641
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16642
|
+
await sphere.initializeProviders();
|
|
16643
|
+
await sphere.initializeModules();
|
|
16644
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16645
|
+
await sphere.syncIdentityWithTransport();
|
|
16646
|
+
sphere._initialized = true;
|
|
16647
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses && sphere._masterKey) {
|
|
16648
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16649
|
+
try {
|
|
16650
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16651
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16652
|
+
if (result.addresses.length > 0) {
|
|
16653
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16654
|
+
}
|
|
16655
|
+
} catch (err) {
|
|
16656
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16342
16657
|
}
|
|
16343
|
-
} catch (err) {
|
|
16344
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16345
16658
|
}
|
|
16346
|
-
|
|
16347
|
-
|
|
16659
|
+
progress?.({ step: "complete", message: "Wallet loaded" });
|
|
16660
|
+
});
|
|
16348
16661
|
return sphere;
|
|
16349
16662
|
}
|
|
16350
16663
|
/**
|
|
16351
16664
|
* Import wallet from mnemonic or master key
|
|
16352
16665
|
*/
|
|
16353
16666
|
static async import(options) {
|
|
16354
|
-
if (options.debug) logger.configure({ debug:
|
|
16667
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16355
16668
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16356
16669
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16357
16670
|
if (!options.mnemonic && !options.masterKey) {
|
|
@@ -16359,7 +16672,7 @@ var Sphere = class _Sphere {
|
|
|
16359
16672
|
}
|
|
16360
16673
|
const progress = options.onProgress;
|
|
16361
16674
|
logger.debug("Sphere", "Starting import...");
|
|
16362
|
-
const needsClear = _Sphere.
|
|
16675
|
+
const needsClear = _Sphere.liveOn(options.storage).length > 0 || await _Sphere.exists(options.storage);
|
|
16363
16676
|
if (needsClear) {
|
|
16364
16677
|
progress?.({ step: "clearing", message: "Clearing previous wallet data..." });
|
|
16365
16678
|
logger.debug("Sphere", "Clearing existing wallet data...");
|
|
@@ -16373,6 +16686,7 @@ var Sphere = class _Sphere {
|
|
|
16373
16686
|
await options.storage.connect();
|
|
16374
16687
|
logger.debug("Sphere", "Storage reconnected");
|
|
16375
16688
|
}
|
|
16689
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16376
16690
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
16377
16691
|
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
16378
16692
|
const marketConfig = _Sphere.resolveMarketConfig(options.market);
|
|
@@ -16416,44 +16730,44 @@ var Sphere = class _Sphere {
|
|
|
16416
16730
|
}
|
|
16417
16731
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16418
16732
|
logger.debug("Sphere", "Initializing providers...");
|
|
16419
|
-
await sphere.
|
|
16420
|
-
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
|
|
16448
|
-
|
|
16449
|
-
|
|
16733
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16734
|
+
await sphere.initializeProviders();
|
|
16735
|
+
await sphere.initializeModules();
|
|
16736
|
+
logger.debug("Sphere", "Modules initialized");
|
|
16737
|
+
if (!options.nametag) {
|
|
16738
|
+
progress?.({ step: "recovering_nametag", message: "Recovering nametag..." });
|
|
16739
|
+
logger.debug("Sphere", "Recovering Unicity ID from transport...");
|
|
16740
|
+
await sphere.recoverNametagFromTransport();
|
|
16741
|
+
logger.debug("Sphere", "Unicity ID recovery done");
|
|
16742
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16743
|
+
await sphere.syncIdentityWithTransport();
|
|
16744
|
+
}
|
|
16745
|
+
progress?.({ step: "finalizing", message: "Finalizing wallet creation..." });
|
|
16746
|
+
logger.debug("Sphere", "Finalizing wallet creation...");
|
|
16747
|
+
await sphere.finalizeWalletCreation();
|
|
16748
|
+
sphere._initialized = true;
|
|
16749
|
+
logger.debug("Sphere", "Tracking address 0...");
|
|
16750
|
+
await sphere.ensureAddressTracked(0);
|
|
16751
|
+
if (options.nametag) {
|
|
16752
|
+
progress?.({ step: "registering_nametag", message: "Registering nametag..." });
|
|
16753
|
+
logger.debug("Sphere", "Registering Unicity ID...");
|
|
16754
|
+
await sphere.registerNametag(options.nametag);
|
|
16755
|
+
}
|
|
16756
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses) {
|
|
16757
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16758
|
+
try {
|
|
16759
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16760
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16761
|
+
if (result.addresses.length > 0) {
|
|
16762
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16763
|
+
}
|
|
16764
|
+
} catch (err) {
|
|
16765
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16450
16766
|
}
|
|
16451
|
-
} catch (err) {
|
|
16452
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16453
16767
|
}
|
|
16454
|
-
|
|
16455
|
-
|
|
16456
|
-
|
|
16768
|
+
progress?.({ step: "complete", message: "Import complete" });
|
|
16769
|
+
logger.debug("Sphere", "Import complete");
|
|
16770
|
+
});
|
|
16457
16771
|
return sphere;
|
|
16458
16772
|
}
|
|
16459
16773
|
/**
|
|
@@ -16471,9 +16785,17 @@ var Sphere = class _Sphere {
|
|
|
16471
16785
|
*/
|
|
16472
16786
|
static async clear(options) {
|
|
16473
16787
|
const storage = options.storage;
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
await _Sphere.
|
|
16788
|
+
_Sphere.bumpClearGeneration(storage);
|
|
16789
|
+
try {
|
|
16790
|
+
await _Sphere.clearStore(storage);
|
|
16791
|
+
} finally {
|
|
16792
|
+
_Sphere.bumpClearGeneration(storage);
|
|
16793
|
+
}
|
|
16794
|
+
}
|
|
16795
|
+
static async clearStore(storage) {
|
|
16796
|
+
for (const live of _Sphere.liveOn(storage)) {
|
|
16797
|
+
logger.debug("Sphere", "Destroying Sphere instance on this storage...");
|
|
16798
|
+
await live.destroy();
|
|
16477
16799
|
logger.debug("Sphere", "Sphere instance destroyed");
|
|
16478
16800
|
}
|
|
16479
16801
|
logger.debug("Sphere", "Yielding 50ms for IDB transaction settlement...");
|
|
@@ -16533,16 +16855,64 @@ var Sphere = class _Sphere {
|
|
|
16533
16855
|
}
|
|
16534
16856
|
}
|
|
16535
16857
|
/**
|
|
16536
|
-
*
|
|
16858
|
+
* The key a provider registers under: the store it addresses, so every provider
|
|
16859
|
+
* over one dataDir/DB shares an entry. A provider that declares no store keeps a
|
|
16860
|
+
* private key, leaving custom implementations scoped by object identity.
|
|
16537
16861
|
*/
|
|
16538
|
-
static
|
|
16539
|
-
|
|
16862
|
+
static storeKeyOf(storage) {
|
|
16863
|
+
const declared = storage.backingStoreId;
|
|
16864
|
+
if (declared) return `store:${declared}`;
|
|
16865
|
+
let key = _Sphere._objectStoreKeys.get(storage);
|
|
16866
|
+
if (!key) {
|
|
16867
|
+
key = `object:${++lifecycle.objectStoreSeq}`;
|
|
16868
|
+
_Sphere._objectStoreKeys.set(storage, key);
|
|
16869
|
+
}
|
|
16870
|
+
return key;
|
|
16871
|
+
}
|
|
16872
|
+
/** The clears this store has seen. Absent means none — `clear()` creates the entry. */
|
|
16873
|
+
static clearGenerationOf(storage) {
|
|
16874
|
+
return _Sphere._clearGenerations.get(_Sphere.storeKeyOf(storage)) ?? 0;
|
|
16875
|
+
}
|
|
16876
|
+
static bumpClearGeneration(storage) {
|
|
16877
|
+
const key = _Sphere.storeKeyOf(storage);
|
|
16878
|
+
_Sphere._clearGenerations.set(key, (_Sphere._clearGenerations.get(key) ?? 0) + 1);
|
|
16540
16879
|
}
|
|
16541
16880
|
/**
|
|
16542
|
-
*
|
|
16881
|
+
* Make a fully-built Sphere reachable — or refuse, if `clear()` emptied the store
|
|
16882
|
+
* under it while it was building. Check and registration are one SYNCHRONOUS step on
|
|
16883
|
+
* purpose: split by an await, a clear could land between them and wipe a store whose
|
|
16884
|
+
* Sphere is already published. The refusal throws inside `withOwnedRegistry`, whose
|
|
16885
|
+
* teardown then destroys the half-built Sphere the caller never received.
|
|
16543
16886
|
*/
|
|
16544
|
-
static
|
|
16545
|
-
|
|
16887
|
+
static publishLive(sphere, clearGeneration) {
|
|
16888
|
+
if (_Sphere.clearGenerationOf(sphere._storage) !== clearGeneration) {
|
|
16889
|
+
throw new SphereError(
|
|
16890
|
+
"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.",
|
|
16891
|
+
"STORAGE_ERROR"
|
|
16892
|
+
);
|
|
16893
|
+
}
|
|
16894
|
+
_Sphere.registerLive(sphere);
|
|
16895
|
+
}
|
|
16896
|
+
/** Record a fully-built Sphere against the storage it owns. See `_liveByStorage`. */
|
|
16897
|
+
static registerLive(sphere) {
|
|
16898
|
+
const key = _Sphere.storeKeyOf(sphere._storage);
|
|
16899
|
+
let live = _Sphere._liveByStorage.get(key);
|
|
16900
|
+
if (!live) {
|
|
16901
|
+
live = /* @__PURE__ */ new Set();
|
|
16902
|
+
_Sphere._liveByStorage.set(key, live);
|
|
16903
|
+
}
|
|
16904
|
+
live.add(sphere);
|
|
16905
|
+
}
|
|
16906
|
+
static unregisterLive(sphere) {
|
|
16907
|
+
const key = _Sphere.storeKeyOf(sphere._storage);
|
|
16908
|
+
const live = _Sphere._liveByStorage.get(key);
|
|
16909
|
+
if (!live) return;
|
|
16910
|
+
live.delete(sphere);
|
|
16911
|
+
if (live.size === 0) _Sphere._liveByStorage.delete(key);
|
|
16912
|
+
}
|
|
16913
|
+
/** Snapshot — callers iterate this while destroy() mutates the underlying Set. */
|
|
16914
|
+
static liveOn(storage) {
|
|
16915
|
+
return Array.from(_Sphere._liveByStorage.get(_Sphere.storeKeyOf(storage)) ?? []);
|
|
16546
16916
|
}
|
|
16547
16917
|
/**
|
|
16548
16918
|
* Validate mnemonic using BIP39
|
|
@@ -16753,8 +17123,7 @@ var Sphere = class _Sphere {
|
|
|
16753
17123
|
password
|
|
16754
17124
|
});
|
|
16755
17125
|
if (result.success) {
|
|
16756
|
-
|
|
16757
|
-
return { success: true, sphere: sphere2, mnemonic: result.mnemonic };
|
|
17126
|
+
return { success: true, sphere: result.sphere, mnemonic: result.mnemonic };
|
|
16758
17127
|
}
|
|
16759
17128
|
if (!password && result.error?.includes("Password required")) {
|
|
16760
17129
|
return { success: false, needsPassword: true, error: result.error };
|
|
@@ -17056,7 +17425,8 @@ var Sphere = class _Sphere {
|
|
|
17056
17425
|
/**
|
|
17057
17426
|
* Import wallet from JSON backup
|
|
17058
17427
|
*
|
|
17059
|
-
* @returns
|
|
17428
|
+
* @returns `{ success, sphere?, mnemonic?, error? }`. `sphere` is the instance built
|
|
17429
|
+
* on the SUPPLIED storage — hold it, there is no global to look it up from (#766).
|
|
17060
17430
|
*
|
|
17061
17431
|
* @example
|
|
17062
17432
|
* ```ts
|
|
@@ -17097,18 +17467,18 @@ var Sphere = class _Sphere {
|
|
|
17097
17467
|
}
|
|
17098
17468
|
const basePath = data.wallet.descriptorPath ? `m/${data.wallet.descriptorPath}` : DEFAULT_BASE_PATH;
|
|
17099
17469
|
if (mnemonic) {
|
|
17100
|
-
await _Sphere.import({ ...baseOptions, mnemonic, basePath });
|
|
17101
|
-
return { success: true, mnemonic };
|
|
17470
|
+
const sphere = await _Sphere.import({ ...baseOptions, mnemonic, basePath });
|
|
17471
|
+
return { success: true, sphere, mnemonic };
|
|
17102
17472
|
}
|
|
17103
17473
|
if (masterKey) {
|
|
17104
|
-
await _Sphere.import({
|
|
17474
|
+
const sphere = await _Sphere.import({
|
|
17105
17475
|
...baseOptions,
|
|
17106
17476
|
masterKey,
|
|
17107
17477
|
chainCode: data.wallet.chainCode,
|
|
17108
17478
|
basePath,
|
|
17109
17479
|
derivationMode: data.derivationMode || (data.wallet.isBIP32 ? "bip32" : "wif_hmac")
|
|
17110
17480
|
});
|
|
17111
|
-
return { success: true };
|
|
17481
|
+
return { success: true, sphere };
|
|
17112
17482
|
}
|
|
17113
17483
|
return { success: false, error: "No mnemonic or master key in wallet data" };
|
|
17114
17484
|
} catch (e) {
|
|
@@ -17231,6 +17601,7 @@ var Sphere = class _Sphere {
|
|
|
17231
17601
|
}
|
|
17232
17602
|
if (entry.hidden === hidden) return;
|
|
17233
17603
|
entry.hidden = hidden;
|
|
17604
|
+
entry.updatedAt = Date.now();
|
|
17234
17605
|
await this.persistTrackedAddresses();
|
|
17235
17606
|
const eventType = hidden ? "address:hidden" : "address:unhidden";
|
|
17236
17607
|
this.emitEvent(eventType, { index, addressId: entry.addressId });
|
|
@@ -17259,9 +17630,6 @@ var Sphere = class _Sphere {
|
|
|
17259
17630
|
if (!this._masterKey) {
|
|
17260
17631
|
throw new SphereError("HD derivation requires master key with chain code. Cannot switch addresses.", "INVALID_CONFIG");
|
|
17261
17632
|
}
|
|
17262
|
-
if (index < 0) {
|
|
17263
|
-
throw new SphereError("Address index must be non-negative", "INVALID_CONFIG");
|
|
17264
|
-
}
|
|
17265
17633
|
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
17266
17634
|
if (newNametag && !isValidNametag(newNametag)) {
|
|
17267
17635
|
throw new SphereError("Invalid Unicity ID format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.", "VALIDATION_ERROR");
|
|
@@ -17298,7 +17666,9 @@ var Sphere = class _Sphere {
|
|
|
17298
17666
|
if (firstVisit) {
|
|
17299
17667
|
logger.debug("Sphere", `switchToAddress(${index}): creating per-address modules (lazy init)`);
|
|
17300
17668
|
this._storage.setIdentity(newIdentity);
|
|
17669
|
+
this.ensureAlive();
|
|
17301
17670
|
await this.initializeAddressModules({ index, identity: newIdentity });
|
|
17671
|
+
if (this._destroyed) await this.discardModulesBuiltDuringDestroy(index);
|
|
17302
17672
|
} else if (nametag !== this._addressModules.get(index).identity.nametag) {
|
|
17303
17673
|
this._addressModules.get(index).identity = newIdentity;
|
|
17304
17674
|
}
|
|
@@ -17309,14 +17679,18 @@ var Sphere = class _Sphere {
|
|
|
17309
17679
|
this._communications = activeModules.communications;
|
|
17310
17680
|
this._groupChat = activeModules.groupChat;
|
|
17311
17681
|
this._market = activeModules.market;
|
|
17682
|
+
this.ensureAlive();
|
|
17312
17683
|
await this.stopThenStartPaymentsV2(index, newIdentity);
|
|
17684
|
+
this.ensureAlive();
|
|
17313
17685
|
await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
|
|
17314
17686
|
this._storage.setIdentity(this._identity);
|
|
17315
17687
|
if (this._transport.setFallbackSince) {
|
|
17316
17688
|
const fallbackTs = Math.floor(Date.now() / 1e3) - 86400;
|
|
17317
17689
|
this._transport.setFallbackSince(fallbackTs);
|
|
17318
17690
|
}
|
|
17691
|
+
this.ensureAlive();
|
|
17319
17692
|
await this._transport.setIdentity(this._identity);
|
|
17693
|
+
this.ensureAlive();
|
|
17320
17694
|
if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
|
|
17321
17695
|
await this._transportMux.rebindToSharedClient();
|
|
17322
17696
|
}
|
|
@@ -17336,6 +17710,7 @@ var Sphere = class _Sphere {
|
|
|
17336
17710
|
* Runs after switchToAddress returns so L3 queries can start immediately.
|
|
17337
17711
|
*/
|
|
17338
17712
|
async postSwitchSync(index, newNametag) {
|
|
17713
|
+
this.ensureAlive();
|
|
17339
17714
|
if (!newNametag) {
|
|
17340
17715
|
await this.syncIdentityWithTransport();
|
|
17341
17716
|
}
|
|
@@ -17353,6 +17728,39 @@ var Sphere = class _Sphere {
|
|
|
17353
17728
|
* independently in background. The payments vertical is NOT created here —
|
|
17354
17729
|
* the caller starts it via stopThenStartPaymentsV2 (§7 single vertical).
|
|
17355
17730
|
*/
|
|
17731
|
+
/** Tear one address's module set down. Each address owns its own engine, so its own pool. */
|
|
17732
|
+
static destroyModuleSet(index, moduleSet) {
|
|
17733
|
+
try {
|
|
17734
|
+
moduleSet.communications.destroy();
|
|
17735
|
+
moduleSet.groupChat?.destroy();
|
|
17736
|
+
moduleSet.market?.destroy();
|
|
17737
|
+
moduleSet.tokenEngine?.dispose?.();
|
|
17738
|
+
logger.debug("Sphere", `Destroyed modules for address ${index}`);
|
|
17739
|
+
} catch (err) {
|
|
17740
|
+
logger.warn("Sphere", `Error destroying modules for address ${index}:`, err);
|
|
17741
|
+
}
|
|
17742
|
+
}
|
|
17743
|
+
/**
|
|
17744
|
+
* Undo a module set built while `destroy()` was running.
|
|
17745
|
+
*
|
|
17746
|
+
* `destroy()`'s teardown loop has already emptied `_addressModules`, so a set
|
|
17747
|
+
* registered after it would stay live and unreachable — its own engine (and worker
|
|
17748
|
+
* pool), and a transport mux `ensureTransportMux` rebuilt because teardown had just
|
|
17749
|
+
* nulled the field. Then re-raise, so the switch fails rather than reporting success
|
|
17750
|
+
* on a destroyed Sphere (#770, #772 review).
|
|
17751
|
+
*/
|
|
17752
|
+
async discardModulesBuiltDuringDestroy(index) {
|
|
17753
|
+
const built = this._addressModules.get(index);
|
|
17754
|
+
this._addressModules.delete(index);
|
|
17755
|
+
if (built) _Sphere.destroyModuleSet(index, built);
|
|
17756
|
+
if (this._transportMux) {
|
|
17757
|
+
const mux = this._transportMux;
|
|
17758
|
+
this._transportMux = null;
|
|
17759
|
+
await _Sphere.safeDisconnect("transport mux", () => mux.disconnect());
|
|
17760
|
+
}
|
|
17761
|
+
this.ensureAlive();
|
|
17762
|
+
throw new SphereError("Sphere destroyed", "NOT_INITIALIZED");
|
|
17763
|
+
}
|
|
17356
17764
|
async initializeAddressModules(spec) {
|
|
17357
17765
|
const { index, identity } = spec;
|
|
17358
17766
|
const emitEvent = this.emitEvent.bind(this);
|
|
@@ -17440,6 +17848,14 @@ var Sphere = class _Sphere {
|
|
|
17440
17848
|
const adapter = await this._transportMux.addAddress(index, identity, this._transport);
|
|
17441
17849
|
return adapter;
|
|
17442
17850
|
}
|
|
17851
|
+
/** A BIP32 child number: an integer in 0…0xffffffff. Anything else aliases an address. */
|
|
17852
|
+
static assertDerivableIndex(index) {
|
|
17853
|
+
if (isDerivableIndex(index)) return;
|
|
17854
|
+
throw new SphereError(
|
|
17855
|
+
`Address index ${String(index)} is not a BIP32 child number: it must be an integer in 0\u20260xffffffff.`,
|
|
17856
|
+
"INVALID_CONFIG"
|
|
17857
|
+
);
|
|
17858
|
+
}
|
|
17443
17859
|
/**
|
|
17444
17860
|
* Derive address at a specific index
|
|
17445
17861
|
*
|
|
@@ -17485,6 +17901,7 @@ var Sphere = class _Sphere {
|
|
|
17485
17901
|
* when _initialized is still false.
|
|
17486
17902
|
*/
|
|
17487
17903
|
_deriveAddressInternal(index, isChange = false) {
|
|
17904
|
+
_Sphere.assertDerivableIndex(index);
|
|
17488
17905
|
if (!this._masterKey) {
|
|
17489
17906
|
throw new SphereError("HD derivation requires master key with chain code", "INVALID_CONFIG");
|
|
17490
17907
|
}
|
|
@@ -17576,6 +17993,7 @@ var Sphere = class _Sphere {
|
|
|
17576
17993
|
}
|
|
17577
17994
|
if (tracked.hidden !== hidden) {
|
|
17578
17995
|
tracked.hidden = hidden;
|
|
17996
|
+
tracked.updatedAt = Date.now();
|
|
17579
17997
|
}
|
|
17580
17998
|
}
|
|
17581
17999
|
await this.persistTrackedAddresses();
|
|
@@ -18217,7 +18635,48 @@ var Sphere = class _Sphere {
|
|
|
18217
18635
|
// ===========================================================================
|
|
18218
18636
|
// Public Methods - Lifecycle
|
|
18219
18637
|
// ===========================================================================
|
|
18638
|
+
/**
|
|
18639
|
+
* Disconnect transport, storage and oracle, attempting each even if an earlier one
|
|
18640
|
+
* rejects. They are separate resources, and one failure must not skip the rest — that
|
|
18641
|
+
* is how a partial teardown leaves connections open, which is exactly the state the
|
|
18642
|
+
* failed-initialization path calls destroy() in.
|
|
18643
|
+
*/
|
|
18644
|
+
async disconnectProvidersIndependently() {
|
|
18645
|
+
const steps = [
|
|
18646
|
+
["transport", () => this._transport.disconnect()],
|
|
18647
|
+
["storage", () => this._storage.disconnect()],
|
|
18648
|
+
["oracle", () => this._oracle.disconnect()]
|
|
18649
|
+
];
|
|
18650
|
+
for (const [what, disconnect] of steps) {
|
|
18651
|
+
await _Sphere.safeDisconnect(what, disconnect);
|
|
18652
|
+
}
|
|
18653
|
+
}
|
|
18654
|
+
/** Run one teardown step, logging rather than propagating so the next one still runs. */
|
|
18655
|
+
static async safeDisconnect(what, run) {
|
|
18656
|
+
try {
|
|
18657
|
+
await run();
|
|
18658
|
+
} catch (err) {
|
|
18659
|
+
logger.warn("Sphere", `${what} disconnect failed during destroy:`, err);
|
|
18660
|
+
}
|
|
18661
|
+
}
|
|
18662
|
+
/**
|
|
18663
|
+
* Tear this Sphere down: stop the vertical, destroy the modules, disconnect the providers
|
|
18664
|
+
* and zero the key material. Idempotent by construction (every step is null-guarded) and
|
|
18665
|
+
* deliberately WITHOUT an early return on re-entry, which would change what a double call
|
|
18666
|
+
* means.
|
|
18667
|
+
*
|
|
18668
|
+
* #770: the FIRST statement flips `_destroyed` — before any await, and before this queues
|
|
18669
|
+
* its own stop on the §7 lifecycle mutex. That position is load-bearing. Every guard reads
|
|
18670
|
+
* the flag, so from that instant any switchToAddress step not yet begun refuses; and
|
|
18671
|
+
* because it flips SYNCHRONOUSLY at entry, a stop/start pair the mutex runs after this call
|
|
18672
|
+
* sees `true` and skips its start. Set it beside `_initialized` at the bottom instead and a
|
|
18673
|
+
* concurrent switch re-arms a wallet whose owner already had destroy() return: fresh
|
|
18674
|
+
* sockets, a fresh wallet-api session, a whole fresh vertical nothing will ever stop.
|
|
18675
|
+
*/
|
|
18220
18676
|
async destroy() {
|
|
18677
|
+
this._destroyed = true;
|
|
18678
|
+
this._registry?.dispose();
|
|
18679
|
+
this._registry = null;
|
|
18221
18680
|
this.cleanupProviderEventSubscriptions();
|
|
18222
18681
|
try {
|
|
18223
18682
|
await this.queuePaymentsV2Op(() => this.stopPaymentsV2Inner());
|
|
@@ -18225,28 +18684,19 @@ var Sphere = class _Sphere {
|
|
|
18225
18684
|
logger.warn("Sphere", "payments vertical stop failed during destroy:", err);
|
|
18226
18685
|
}
|
|
18227
18686
|
for (const [idx, moduleSet] of this._addressModules.entries()) {
|
|
18228
|
-
|
|
18229
|
-
moduleSet.communications.destroy();
|
|
18230
|
-
moduleSet.groupChat?.destroy();
|
|
18231
|
-
moduleSet.market?.destroy();
|
|
18232
|
-
moduleSet.tokenEngine?.dispose?.();
|
|
18233
|
-
logger.debug("Sphere", `Destroyed modules for address ${idx}`);
|
|
18234
|
-
} catch (err) {
|
|
18235
|
-
logger.warn("Sphere", `Error destroying modules for address ${idx}:`, err);
|
|
18236
|
-
}
|
|
18687
|
+
_Sphere.destroyModuleSet(idx, moduleSet);
|
|
18237
18688
|
}
|
|
18238
18689
|
this._addressModules.clear();
|
|
18239
18690
|
this._communications.destroy();
|
|
18240
18691
|
this._groupChat?.destroy();
|
|
18241
18692
|
this._market?.destroy();
|
|
18242
18693
|
if (this._transportMux) {
|
|
18243
|
-
|
|
18694
|
+
const mux = this._transportMux;
|
|
18695
|
+
await _Sphere.safeDisconnect("transport mux", () => mux.disconnect());
|
|
18244
18696
|
this._transportMux = null;
|
|
18245
18697
|
}
|
|
18246
18698
|
this._tokenEngine?.dispose?.();
|
|
18247
|
-
await this.
|
|
18248
|
-
await this._storage.disconnect();
|
|
18249
|
-
await this._oracle.disconnect();
|
|
18699
|
+
await this.disconnectProvidersIndependently();
|
|
18250
18700
|
this._initialized = false;
|
|
18251
18701
|
this._trackedAddressesLoaded = false;
|
|
18252
18702
|
this._identity = null;
|
|
@@ -18258,9 +18708,7 @@ var Sphere = class _Sphere {
|
|
|
18258
18708
|
this._addressNametags.clear();
|
|
18259
18709
|
this._disabledProviders.clear();
|
|
18260
18710
|
this.eventHandlers.clear();
|
|
18261
|
-
|
|
18262
|
-
_Sphere.instance = null;
|
|
18263
|
-
}
|
|
18711
|
+
_Sphere.unregisterLive(this);
|
|
18264
18712
|
}
|
|
18265
18713
|
// ===========================================================================
|
|
18266
18714
|
// Private: Storage
|
|
@@ -18597,10 +19045,21 @@ var Sphere = class _Sphere {
|
|
|
18597
19045
|
);
|
|
18598
19046
|
return run;
|
|
18599
19047
|
}
|
|
18600
|
-
/**
|
|
19048
|
+
/**
|
|
19049
|
+
* Switch/boot: stop whatever runs, then start `index`'s vertical — atomically vs other
|
|
19050
|
+
* lifecycle ops.
|
|
19051
|
+
*
|
|
19052
|
+
* #770: the destroyed check between the two halves settles the FACADE vector on its own.
|
|
19053
|
+
* `_destroyed` flips synchronously at destroy() entry, so any closure that BEGINS executing
|
|
19054
|
+
* after destroy() was called sees `true`, and any closure already past the check has
|
|
19055
|
+
* facade.start() in flight — which destroy()'s own queued stop is necessarily ordered
|
|
19056
|
+
* after. The TRANSPORT vector is not on this mutex at all; the ensureAlive() calls in
|
|
19057
|
+
* switchToAddress are what cover it.
|
|
19058
|
+
*/
|
|
18601
19059
|
stopThenStartPaymentsV2(index, identity) {
|
|
18602
19060
|
return this.queuePaymentsV2Op(async () => {
|
|
18603
19061
|
await this.stopPaymentsV2Inner();
|
|
19062
|
+
if (this._destroyed) return;
|
|
18604
19063
|
await this.startPaymentsV2Inner(index, identity);
|
|
18605
19064
|
});
|
|
18606
19065
|
}
|
|
@@ -18632,6 +19091,7 @@ var Sphere = class _Sphere {
|
|
|
18632
19091
|
host: {
|
|
18633
19092
|
storage: this._storage,
|
|
18634
19093
|
price: this._priceProvider,
|
|
19094
|
+
registry: this._registry ?? TokenRegistry.getInstance(),
|
|
18635
19095
|
emit: (event, payload) => this.emitEvent(event, payload),
|
|
18636
19096
|
resolvePeer: (identifier) => this._transport.resolve?.(identifier) ?? Promise.resolve(null),
|
|
18637
19097
|
// Scoped to THIS facade's address, never "whichever is active":
|
|
@@ -18652,7 +19112,18 @@ var Sphere = class _Sphere {
|
|
|
18652
19112
|
this._paymentsV2Active = null;
|
|
18653
19113
|
await active.facade.stop();
|
|
18654
19114
|
}
|
|
19115
|
+
/**
|
|
19116
|
+
* Refuse once destroy() has STARTED (#770). `_initialized` cannot carry this: destroy()
|
|
19117
|
+
* clears it last, so every teardown step is a window in which a concurrent call still reads
|
|
19118
|
+
* a ready Sphere and re-arms it.
|
|
19119
|
+
*/
|
|
19120
|
+
ensureAlive() {
|
|
19121
|
+
if (this._destroyed) {
|
|
19122
|
+
throw new SphereError("Sphere destroyed", "NOT_INITIALIZED");
|
|
19123
|
+
}
|
|
19124
|
+
}
|
|
18655
19125
|
ensureReady() {
|
|
19126
|
+
this.ensureAlive();
|
|
18656
19127
|
if (!this._initialized) {
|
|
18657
19128
|
throw new SphereError("Sphere not initialized", "NOT_INITIALIZED");
|
|
18658
19129
|
}
|
|
@@ -18702,7 +19173,6 @@ var createSphere = Sphere.create.bind(Sphere);
|
|
|
18702
19173
|
var loadSphere = Sphere.load.bind(Sphere);
|
|
18703
19174
|
var importSphere = Sphere.import.bind(Sphere);
|
|
18704
19175
|
var initSphere = Sphere.init.bind(Sphere);
|
|
18705
|
-
var getSphere = Sphere.getInstance.bind(Sphere);
|
|
18706
19176
|
var sphereExists = Sphere.exists.bind(Sphere);
|
|
18707
19177
|
|
|
18708
19178
|
// core/currency.ts
|
|
@@ -18880,28 +19350,59 @@ async function checkWebSocket(url, timeoutMs) {
|
|
|
18880
19350
|
};
|
|
18881
19351
|
});
|
|
18882
19352
|
}
|
|
19353
|
+
var HEALTH_PROBE_STATE_ID = "00".repeat(32);
|
|
19354
|
+
function readBlockNumber(body) {
|
|
19355
|
+
if (typeof body !== "object" || body === null) return null;
|
|
19356
|
+
const result = body.result;
|
|
19357
|
+
if (typeof result !== "object" || result === null) return null;
|
|
19358
|
+
const n = result.blockNumber;
|
|
19359
|
+
if (typeof n === "string") return /^\d+$/.test(n) ? n : null;
|
|
19360
|
+
if (typeof n === "number") return Number.isInteger(n) && n >= 0 ? String(n) : null;
|
|
19361
|
+
return null;
|
|
19362
|
+
}
|
|
19363
|
+
function readRpcError(body) {
|
|
19364
|
+
if (typeof body !== "object" || body === null) return null;
|
|
19365
|
+
const err = body.error;
|
|
19366
|
+
if (typeof err === "string") return err;
|
|
19367
|
+
if (typeof err === "object" && err !== null) {
|
|
19368
|
+
const message = err.message;
|
|
19369
|
+
if (typeof message === "string") return message;
|
|
19370
|
+
return JSON.stringify(err);
|
|
19371
|
+
}
|
|
19372
|
+
return null;
|
|
19373
|
+
}
|
|
19374
|
+
function oracleVerdict(url, responseTimeMs, response, body) {
|
|
19375
|
+
if (readBlockNumber(body) !== null) return { healthy: true, url, responseTimeMs };
|
|
19376
|
+
const rpcError = readRpcError(body);
|
|
19377
|
+
return {
|
|
19378
|
+
healthy: false,
|
|
19379
|
+
url,
|
|
19380
|
+
responseTimeMs,
|
|
19381
|
+
error: rpcError ?? (response.ok ? "aggregator answered without a block height" : `HTTP ${String(response.status)} ${response.statusText}`)
|
|
19382
|
+
};
|
|
19383
|
+
}
|
|
18883
19384
|
async function checkOracle(url, timeoutMs) {
|
|
18884
19385
|
const startTime = Date.now();
|
|
19386
|
+
const controller = new AbortController();
|
|
19387
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18885
19388
|
try {
|
|
18886
|
-
const controller = new AbortController();
|
|
18887
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18888
19389
|
const response = await fetch(url, {
|
|
18889
19390
|
method: "POST",
|
|
18890
19391
|
headers: { "Content-Type": "application/json" },
|
|
18891
|
-
body: JSON.stringify({
|
|
19392
|
+
body: JSON.stringify({
|
|
19393
|
+
jsonrpc: "2.0",
|
|
19394
|
+
id: 1,
|
|
19395
|
+
method: "get_block_height",
|
|
19396
|
+
params: { stateId: HEALTH_PROBE_STATE_ID }
|
|
19397
|
+
}),
|
|
18892
19398
|
signal: controller.signal
|
|
18893
19399
|
});
|
|
18894
|
-
clearTimeout(timer);
|
|
18895
19400
|
const responseTimeMs = Date.now() - startTime;
|
|
18896
|
-
|
|
18897
|
-
|
|
19401
|
+
const body = await response.json().catch(() => null);
|
|
19402
|
+
if (controller.signal.aborted) {
|
|
19403
|
+
return { healthy: false, url, responseTimeMs, error: `Connection timeout after ${timeoutMs}ms` };
|
|
18898
19404
|
}
|
|
18899
|
-
return
|
|
18900
|
-
healthy: false,
|
|
18901
|
-
url,
|
|
18902
|
-
responseTimeMs,
|
|
18903
|
-
error: `HTTP ${response.status} ${response.statusText}`
|
|
18904
|
-
};
|
|
19405
|
+
return oracleVerdict(url, responseTimeMs, response, body);
|
|
18905
19406
|
} catch (err) {
|
|
18906
19407
|
return {
|
|
18907
19408
|
healthy: false,
|
|
@@ -18909,6 +19410,8 @@ async function checkOracle(url, timeoutMs) {
|
|
|
18909
19410
|
responseTimeMs: null,
|
|
18910
19411
|
error: err instanceof Error ? err.name === "AbortError" ? `Connection timeout after ${timeoutMs}ms` : err.message : String(err)
|
|
18911
19412
|
};
|
|
19413
|
+
} finally {
|
|
19414
|
+
clearTimeout(timer);
|
|
18912
19415
|
}
|
|
18913
19416
|
}
|
|
18914
19417
|
async function runCustomCheck(name, checkFn, timeoutMs) {
|
|
@@ -18989,7 +19492,6 @@ export {
|
|
|
18989
19492
|
generateMnemonic2 as generateMnemonic,
|
|
18990
19493
|
generateRandomKey,
|
|
18991
19494
|
getPublicKey,
|
|
18992
|
-
getSphere,
|
|
18993
19495
|
hash160,
|
|
18994
19496
|
hashSignMessage,
|
|
18995
19497
|
hexToBytes4 as hexToBytes,
|