@unicitylabs/sphere-sdk 0.15.0 → 0.16.0-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/dist/connect/index.cjs +26 -25
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.d.cts +4 -0
- package/dist/connect/index.d.ts +4 -0
- package/dist/connect/index.js +26 -25
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +728 -227
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +174 -23
- package/dist/core/index.d.ts +174 -23
- package/dist/core/index.js +728 -226
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +16 -25
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +16 -25
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +333 -506
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +333 -506
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +15 -24
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +15 -24
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +300 -571
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +92 -15
- package/dist/impl/nodejs/index.d.ts +92 -15
- package/dist/impl/nodejs/index.js +300 -571
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/impl/shared/wallet-api/index.d.cts +54 -2
- package/dist/impl/shared/wallet-api/index.d.ts +54 -2
- package/dist/index.cjs +730 -227
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +229 -110
- package/dist/index.d.ts +229 -110
- package/dist/index.js +730 -226
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +11 -7
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +56 -3
- package/dist/modules/payments-v2/index.d.ts +56 -3
- package/dist/modules/payments-v2/index.js +11 -7
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +46 -0
- package/dist/token-engine/index.cjs.map +1 -1
- package/dist/token-engine/index.js +46 -0
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +1 -1
package/dist/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 {
|
|
@@ -715,7 +772,6 @@ function getAddressId(directAddress) {
|
|
|
715
772
|
return `DIRECT_${first}_${last}`;
|
|
716
773
|
}
|
|
717
774
|
var DEFAULT_NOSTR_RELAYS = [
|
|
718
|
-
"wss://relay.unicity.network",
|
|
719
775
|
"wss://relay.damus.io",
|
|
720
776
|
"wss://nos.lol",
|
|
721
777
|
"wss://relay.nostr.band"
|
|
@@ -762,8 +818,6 @@ var NIP29_KINDS = {
|
|
|
762
818
|
/** Relay-signed group roles */
|
|
763
819
|
GROUP_ROLES: 39003
|
|
764
820
|
};
|
|
765
|
-
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
766
|
-
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
767
821
|
var DEFAULT_AGGREGATOR_TIMEOUT = 3e4;
|
|
768
822
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
769
823
|
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
@@ -771,7 +825,6 @@ var COIN_TYPES = {
|
|
|
771
825
|
/** Test token */
|
|
772
826
|
TEST: "TEST"
|
|
773
827
|
};
|
|
774
|
-
var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
|
|
775
828
|
var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
|
|
776
829
|
var TEST_NOSTR_RELAYS = [
|
|
777
830
|
"wss://nostr-relay.testnet.unicity.network"
|
|
@@ -782,16 +835,25 @@ var DEFAULT_GROUP_RELAYS = [
|
|
|
782
835
|
var NETWORKS = {
|
|
783
836
|
mainnet: {
|
|
784
837
|
name: "Mainnet",
|
|
785
|
-
|
|
786
|
-
|
|
838
|
+
networkId: 1,
|
|
839
|
+
// v3 state-transition gateway (networkId 1 comes from the trust base). apiKey is env-injected;
|
|
840
|
+
// unlike testnet2's, a mainnet gateway key is a SECRET — never commit one.
|
|
841
|
+
aggregatorUrl: "https://gateway.mainnet.unicity.network",
|
|
842
|
+
// Mainnet has no relay of its own yet — it shares the testnet relay until one is stood up.
|
|
843
|
+
// Consequence while shared: nametag bindings for both networks live in ONE namespace, and
|
|
844
|
+
// since bindings carry no network the cross-network recipient guard can only signal (#734).
|
|
845
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
787
846
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
788
|
-
|
|
847
|
+
// Published, but currently only the non-fungible base token type — no fungible
|
|
848
|
+
// coins yet, so those still miss the registry (decimals 0, symbol falls back to
|
|
849
|
+
// six hex chars). Presentation only: the money path treats coinId as opaque bytes.
|
|
850
|
+
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json"
|
|
789
851
|
},
|
|
790
852
|
// v1 cutover: 'testnet' now POINTS AT TESTNET2 (the v2 gateway network). The
|
|
791
853
|
// old goggregator testnet spoke the removed v1 protocol — a v2 engine cannot
|
|
792
854
|
// run against it. 'testnet2' stays as an alias of the same configuration.
|
|
793
855
|
testnet: {
|
|
794
|
-
name: "
|
|
856
|
+
name: "Testnet",
|
|
795
857
|
networkId: 4,
|
|
796
858
|
// v2 state-transition gateway (networkId 4 comes from the trust base). apiKey is env-injected.
|
|
797
859
|
aggregatorUrl: "https://gateway.testnet2.unicity.network",
|
|
@@ -801,7 +863,7 @@ var NETWORKS = {
|
|
|
801
863
|
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
|
|
802
864
|
},
|
|
803
865
|
testnet2: {
|
|
804
|
-
name: "
|
|
866
|
+
name: "Testnet",
|
|
805
867
|
networkId: 4,
|
|
806
868
|
// v2 state-transition gateway (networkId 4 comes from the trust base). apiKey is env-injected.
|
|
807
869
|
aggregatorUrl: "https://gateway.testnet2.unicity.network",
|
|
@@ -809,19 +871,10 @@ var NETWORKS = {
|
|
|
809
871
|
// reuse testnet infra (shared relays/ipfs)
|
|
810
872
|
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
811
873
|
tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json"
|
|
812
|
-
},
|
|
813
|
-
// NOTE: mainnet/dev still point at v1-era aggregators. The v2 engine cannot
|
|
814
|
-
// operate against them until their gateways are cut over to the v2 protocol —
|
|
815
|
-
// wallet operations on these networks fail loudly (AGGREGATOR_ERROR) until then.
|
|
816
|
-
dev: {
|
|
817
|
-
name: "Development",
|
|
818
|
-
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
819
|
-
nostrRelays: TEST_NOSTR_RELAYS,
|
|
820
|
-
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
821
|
-
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
822
874
|
}
|
|
823
875
|
};
|
|
824
876
|
var SPHERE_NETWORKS = {
|
|
877
|
+
mainnet: { id: NETWORKS.mainnet.networkId, name: "mainnet" },
|
|
825
878
|
testnet2: { id: NETWORKS.testnet2.networkId, name: "testnet2" }
|
|
826
879
|
};
|
|
827
880
|
var TIMEOUTS = {
|
|
@@ -1007,14 +1060,10 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1007
1060
|
});
|
|
1008
1061
|
}
|
|
1009
1062
|
});
|
|
1010
|
-
await
|
|
1011
|
-
this.nostrClient
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1015
|
-
)), this.config.timeout)
|
|
1016
|
-
)
|
|
1017
|
-
]);
|
|
1063
|
+
await this.connectWithDeadline(
|
|
1064
|
+
this.nostrClient,
|
|
1065
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1066
|
+
);
|
|
1018
1067
|
if (!this.nostrClient.isConnected()) {
|
|
1019
1068
|
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
1020
1069
|
}
|
|
@@ -1029,6 +1078,27 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1029
1078
|
throw error;
|
|
1030
1079
|
}
|
|
1031
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Race one client's relay connect against the configured timeout.
|
|
1083
|
+
*
|
|
1084
|
+
* The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
|
|
1085
|
+
* cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
|
|
1086
|
+
* for the whole `config.timeout` after the call has already returned (and
|
|
1087
|
+
* then rejects a promise nobody is listening to any more).
|
|
1088
|
+
*/
|
|
1089
|
+
async connectWithDeadline(client, timeoutMessage) {
|
|
1090
|
+
let timer;
|
|
1091
|
+
try {
|
|
1092
|
+
await Promise.race([
|
|
1093
|
+
client.connect(...this.config.relays),
|
|
1094
|
+
new Promise((_, reject) => {
|
|
1095
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), this.config.timeout);
|
|
1096
|
+
})
|
|
1097
|
+
]);
|
|
1098
|
+
} finally {
|
|
1099
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1032
1102
|
async disconnect() {
|
|
1033
1103
|
if (this.nostrClient) {
|
|
1034
1104
|
this.nostrClient.disconnect();
|
|
@@ -1147,19 +1217,22 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1147
1217
|
// TransportProvider Implementation
|
|
1148
1218
|
// ===========================================================================
|
|
1149
1219
|
async setIdentity(identity) {
|
|
1150
|
-
this.identity = identity;
|
|
1151
|
-
this.processedEventIds.clear();
|
|
1152
|
-
this.lastEventTs = 0;
|
|
1153
|
-
this.lastDmEventTs = 0;
|
|
1154
|
-
this.fallbackDmSince = null;
|
|
1155
1220
|
const secretKey = Buffer2.from(identity.privateKey, "hex");
|
|
1156
|
-
|
|
1157
|
-
const nostrPubkey =
|
|
1158
|
-
logger.debug("Nostr", "Identity
|
|
1221
|
+
const nextKeyManager = NostrKeyManager.fromPrivateKey(secretKey);
|
|
1222
|
+
const nostrPubkey = nextKeyManager.getPublicKeyHex();
|
|
1223
|
+
logger.debug("Nostr", "Identity staged, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
|
|
1224
|
+
const applyStagedIdentity = () => {
|
|
1225
|
+
this.identity = identity;
|
|
1226
|
+
this.keyManager = nextKeyManager;
|
|
1227
|
+
this.processedEventIds.clear();
|
|
1228
|
+
this.lastEventTs = 0;
|
|
1229
|
+
this.lastDmEventTs = 0;
|
|
1230
|
+
this.fallbackDmSince = null;
|
|
1231
|
+
};
|
|
1159
1232
|
if (this.nostrClient && this.status === "connected") {
|
|
1160
1233
|
logger.debug("Nostr", "Identity changed while connected - recreating NostrClient");
|
|
1161
1234
|
const oldClient = this.nostrClient;
|
|
1162
|
-
|
|
1235
|
+
const nextClient = new NostrClient(nextKeyManager, {
|
|
1163
1236
|
autoReconnect: this.config.autoReconnect,
|
|
1164
1237
|
reconnectIntervalMs: this.config.reconnectDelay,
|
|
1165
1238
|
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
@@ -1168,7 +1241,7 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1168
1241
|
queryTimeoutMs: 2e4
|
|
1169
1242
|
// see same option above for rationale
|
|
1170
1243
|
});
|
|
1171
|
-
|
|
1244
|
+
nextClient.addConnectionListener({
|
|
1172
1245
|
onConnect: (url) => {
|
|
1173
1246
|
logger.debug("Nostr", "NostrClient connected to relay:", url);
|
|
1174
1247
|
},
|
|
@@ -1182,18 +1255,43 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1182
1255
|
logger.debug("Nostr", "NostrClient reconnected to relay:", url);
|
|
1183
1256
|
}
|
|
1184
1257
|
});
|
|
1185
|
-
|
|
1186
|
-
this.
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1258
|
+
try {
|
|
1259
|
+
await this.connectWithDeadline(
|
|
1260
|
+
nextClient,
|
|
1261
|
+
`Transport reconnection timed out after ${this.config.timeout}ms`
|
|
1262
|
+
);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
try {
|
|
1265
|
+
nextClient.disconnect();
|
|
1266
|
+
} catch {
|
|
1267
|
+
}
|
|
1268
|
+
throw error;
|
|
1269
|
+
}
|
|
1270
|
+
if (this.nostrClient !== oldClient || this.status !== "connected") {
|
|
1271
|
+
try {
|
|
1272
|
+
nextClient.disconnect();
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
throw new SphereError(
|
|
1276
|
+
"Transport client changed or left the connected state during setIdentity - identity not applied",
|
|
1277
|
+
"TRANSPORT_ERROR"
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
this.nostrClient = nextClient;
|
|
1281
|
+
applyStagedIdentity();
|
|
1282
|
+
try {
|
|
1283
|
+
await this.subscribeToEvents();
|
|
1284
|
+
} finally {
|
|
1285
|
+
try {
|
|
1286
|
+
oldClient.disconnect();
|
|
1287
|
+
} catch {
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1195
1290
|
} else if (this.isConnected()) {
|
|
1291
|
+
applyStagedIdentity();
|
|
1196
1292
|
await this.subscribeToEvents();
|
|
1293
|
+
} else {
|
|
1294
|
+
applyStagedIdentity();
|
|
1197
1295
|
}
|
|
1198
1296
|
}
|
|
1199
1297
|
setFallbackSince(sinceSeconds) {
|
|
@@ -8058,6 +8156,12 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8058
8156
|
lastRefreshAt = 0;
|
|
8059
8157
|
refreshPromise = null;
|
|
8060
8158
|
initialLoadPromise = null;
|
|
8159
|
+
/** Bumped on every remoteUrl change; loads started in an older generation are discarded. */
|
|
8160
|
+
generation = 0;
|
|
8161
|
+
/** Set by dispose(). A disposed registry starts no work and applies no late result. */
|
|
8162
|
+
disposed = false;
|
|
8163
|
+
/** Cancels the in-flight fetch (and its abort timer) when the registry is disposed. */
|
|
8164
|
+
inFlight = null;
|
|
8061
8165
|
constructor() {
|
|
8062
8166
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
8063
8167
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
@@ -8086,27 +8190,90 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8086
8190
|
* @param options.autoRefresh - Start auto-refresh immediately (default: true)
|
|
8087
8191
|
*/
|
|
8088
8192
|
static configure(options) {
|
|
8089
|
-
|
|
8193
|
+
_TokenRegistry.getInstance().applyConfig(options);
|
|
8194
|
+
}
|
|
8195
|
+
/**
|
|
8196
|
+
* Create an INDEPENDENT registry, not the process-global singleton.
|
|
8197
|
+
*
|
|
8198
|
+
* One Sphere owns one of these, so two Spheres on different networks cannot wipe each
|
|
8199
|
+
* other's definitions — the singleton's `configure()` reaches into whatever instance
|
|
8200
|
+
* exists and repoints it, which is how a mainnet init silently retargeted a live
|
|
8201
|
+
* testnet2 wallet's decimals. Dispose it when the owner is destroyed.
|
|
8202
|
+
*/
|
|
8203
|
+
static create(options) {
|
|
8204
|
+
const registry = new _TokenRegistry();
|
|
8205
|
+
registry.applyConfig(options);
|
|
8206
|
+
return registry;
|
|
8207
|
+
}
|
|
8208
|
+
/** The body of configure(), on the instance, so owned registries share it exactly. */
|
|
8209
|
+
applyConfig(options) {
|
|
8210
|
+
if (this.disposed) return;
|
|
8090
8211
|
if (options.remoteUrl !== void 0) {
|
|
8091
|
-
|
|
8212
|
+
const previous = this.remoteUrl;
|
|
8213
|
+
if (previous && previous !== options.remoteUrl) {
|
|
8214
|
+
this.applyDefinitions([]);
|
|
8215
|
+
this.lastRefreshAt = 0;
|
|
8216
|
+
this.generation++;
|
|
8217
|
+
this.refreshPromise = null;
|
|
8218
|
+
}
|
|
8219
|
+
this.remoteUrl = options.remoteUrl;
|
|
8092
8220
|
}
|
|
8093
8221
|
if (options.storage !== void 0) {
|
|
8094
|
-
|
|
8222
|
+
this.storage = options.storage;
|
|
8095
8223
|
}
|
|
8096
8224
|
if (options.refreshIntervalMs !== void 0) {
|
|
8097
|
-
|
|
8225
|
+
this.refreshIntervalMs = options.refreshIntervalMs;
|
|
8098
8226
|
}
|
|
8099
8227
|
const autoRefresh = options.autoRefresh ?? true;
|
|
8100
|
-
|
|
8228
|
+
this.initialLoadPromise = this.performInitialLoad(autoRefresh);
|
|
8229
|
+
}
|
|
8230
|
+
/**
|
|
8231
|
+
* Stop this registry for good: no timer, no late apply, no late cache write.
|
|
8232
|
+
*
|
|
8233
|
+
* Sphere.destroy() calls this on the registry it owns. Without it a discarded Sphere
|
|
8234
|
+
* leaves an hourly fetch running forever — nothing in this file calls unref(), so in
|
|
8235
|
+
* Node it also keeps the event loop alive.
|
|
8236
|
+
*/
|
|
8237
|
+
dispose() {
|
|
8238
|
+
if (this.disposed) return;
|
|
8239
|
+
this.disposed = true;
|
|
8240
|
+
this.stopAutoRefresh();
|
|
8241
|
+
this.inFlight?.abort();
|
|
8242
|
+
this.inFlight = null;
|
|
8243
|
+
this.generation++;
|
|
8244
|
+
this.refreshPromise = null;
|
|
8245
|
+
this.initialLoadPromise = null;
|
|
8246
|
+
}
|
|
8247
|
+
/** Whether dispose() has been called. Reads still work; they are simply frozen. */
|
|
8248
|
+
get isDisposed() {
|
|
8249
|
+
return this.disposed;
|
|
8250
|
+
}
|
|
8251
|
+
/** Per-instance readiness — the same contract as the static, for an owned registry. */
|
|
8252
|
+
async waitForReady(timeoutMs = 1e4) {
|
|
8253
|
+
if (!this.initialLoadPromise) {
|
|
8254
|
+
return this.definitionsById.size > 0;
|
|
8255
|
+
}
|
|
8256
|
+
if (timeoutMs <= 0) {
|
|
8257
|
+
return this.initialLoadPromise;
|
|
8258
|
+
}
|
|
8259
|
+
let timer;
|
|
8260
|
+
try {
|
|
8261
|
+
return await Promise.race([
|
|
8262
|
+
this.initialLoadPromise,
|
|
8263
|
+
new Promise((resolve) => {
|
|
8264
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
8265
|
+
})
|
|
8266
|
+
]);
|
|
8267
|
+
} finally {
|
|
8268
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8269
|
+
}
|
|
8101
8270
|
}
|
|
8102
8271
|
/**
|
|
8103
8272
|
* Reset the singleton instance (useful for testing).
|
|
8104
8273
|
* Stops auto-refresh if running.
|
|
8105
8274
|
*/
|
|
8106
8275
|
static resetInstance() {
|
|
8107
|
-
|
|
8108
|
-
_TokenRegistry.instance.stopAutoRefresh();
|
|
8109
|
-
}
|
|
8276
|
+
_TokenRegistry.instance?.dispose();
|
|
8110
8277
|
_TokenRegistry.instance = null;
|
|
8111
8278
|
}
|
|
8112
8279
|
/**
|
|
@@ -8122,17 +8289,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8122
8289
|
* @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
|
|
8123
8290
|
*/
|
|
8124
8291
|
static async waitForReady(timeoutMs = 1e4) {
|
|
8125
|
-
|
|
8126
|
-
if (!instance.initialLoadPromise) {
|
|
8127
|
-
return instance.definitionsById.size > 0;
|
|
8128
|
-
}
|
|
8129
|
-
if (timeoutMs <= 0) {
|
|
8130
|
-
return instance.initialLoadPromise;
|
|
8131
|
-
}
|
|
8132
|
-
return Promise.race([
|
|
8133
|
-
instance.initialLoadPromise,
|
|
8134
|
-
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
|
|
8135
|
-
]);
|
|
8292
|
+
return _TokenRegistry.getInstance().waitForReady(timeoutMs);
|
|
8136
8293
|
}
|
|
8137
8294
|
// ===========================================================================
|
|
8138
8295
|
// Initial Load
|
|
@@ -8142,6 +8299,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8142
8299
|
* After initial data is available, start periodic auto-refresh if configured.
|
|
8143
8300
|
*/
|
|
8144
8301
|
async performInitialLoad(autoRefresh) {
|
|
8302
|
+
if (this.disposed) return false;
|
|
8145
8303
|
let loaded = false;
|
|
8146
8304
|
if (this.storage) {
|
|
8147
8305
|
loaded = await this.loadFromCache();
|
|
@@ -8154,6 +8312,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8154
8312
|
}
|
|
8155
8313
|
if (autoRefresh && this.remoteUrl) {
|
|
8156
8314
|
loaded = await this.refreshFromRemote();
|
|
8315
|
+
if (this.disposed) return loaded;
|
|
8157
8316
|
this.stopAutoRefresh();
|
|
8158
8317
|
this.refreshTimer = setInterval(() => {
|
|
8159
8318
|
this.refreshFromRemote();
|
|
@@ -8184,6 +8343,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8184
8343
|
*/
|
|
8185
8344
|
async loadFromCache() {
|
|
8186
8345
|
if (!this.storage) return false;
|
|
8346
|
+
const gen = this.generation;
|
|
8187
8347
|
try {
|
|
8188
8348
|
const { data: dataKey, ts: tsKey } = this.cacheKeys();
|
|
8189
8349
|
const [cached, cachedTs] = await Promise.all([
|
|
@@ -8198,6 +8358,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8198
8358
|
if (this.lastRefreshAt > ts) return false;
|
|
8199
8359
|
const data = JSON.parse(cached);
|
|
8200
8360
|
if (!this.isValidDefinitionsArray(data)) return false;
|
|
8361
|
+
if (gen !== this.generation) return false;
|
|
8201
8362
|
this.applyDefinitions(data);
|
|
8202
8363
|
this.lastRefreshAt = ts;
|
|
8203
8364
|
return true;
|
|
@@ -8252,32 +8413,51 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8252
8413
|
* Concurrent calls are deduplicated — only one fetch runs at a time.
|
|
8253
8414
|
*/
|
|
8254
8415
|
async refreshFromRemote() {
|
|
8255
|
-
if (!this.remoteUrl) {
|
|
8416
|
+
if (this.disposed || !this.remoteUrl) {
|
|
8256
8417
|
return false;
|
|
8257
8418
|
}
|
|
8258
8419
|
if (this.refreshPromise) {
|
|
8259
8420
|
return this.refreshPromise;
|
|
8260
8421
|
}
|
|
8261
|
-
|
|
8422
|
+
const attempt = this.doRefresh();
|
|
8423
|
+
this.refreshPromise = attempt;
|
|
8262
8424
|
try {
|
|
8263
|
-
return await
|
|
8425
|
+
return await attempt;
|
|
8264
8426
|
} finally {
|
|
8265
|
-
this.refreshPromise = null;
|
|
8427
|
+
if (this.refreshPromise === attempt) this.refreshPromise = null;
|
|
8266
8428
|
}
|
|
8267
8429
|
}
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8430
|
+
/**
|
|
8431
|
+
* Fetch with a bounded timeout, exposing a cancel hook to dispose().
|
|
8432
|
+
*
|
|
8433
|
+
* Both the request and its abort timer keep Node's event loop alive for the full
|
|
8434
|
+
* FETCH_TIMEOUT_MS, so a registry disposed mid-flight must cancel them rather than
|
|
8435
|
+
* merely ignore the result.
|
|
8436
|
+
*/
|
|
8437
|
+
async fetchCancellable(url) {
|
|
8438
|
+
const controller = new AbortController();
|
|
8439
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
8440
|
+
this.inFlight = {
|
|
8441
|
+
abort: () => {
|
|
8279
8442
|
clearTimeout(timer);
|
|
8443
|
+
controller.abort();
|
|
8280
8444
|
}
|
|
8445
|
+
};
|
|
8446
|
+
try {
|
|
8447
|
+
return await fetch(url, {
|
|
8448
|
+
headers: { Accept: "application/json" },
|
|
8449
|
+
signal: controller.signal
|
|
8450
|
+
});
|
|
8451
|
+
} finally {
|
|
8452
|
+
clearTimeout(timer);
|
|
8453
|
+
this.inFlight = null;
|
|
8454
|
+
}
|
|
8455
|
+
}
|
|
8456
|
+
async doRefresh() {
|
|
8457
|
+
const url = this.remoteUrl;
|
|
8458
|
+
const gen = this.generation;
|
|
8459
|
+
try {
|
|
8460
|
+
const response = await this.fetchCancellable(url);
|
|
8281
8461
|
if (!response.ok) {
|
|
8282
8462
|
logger.warn("TokenRegistry", `Remote fetch failed: HTTP ${response.status} ${response.statusText}`);
|
|
8283
8463
|
return false;
|
|
@@ -8287,6 +8467,10 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8287
8467
|
logger.warn("TokenRegistry", "Remote data is not a valid token definitions array");
|
|
8288
8468
|
return false;
|
|
8289
8469
|
}
|
|
8470
|
+
if (gen !== this.generation) {
|
|
8471
|
+
logger.debug("TokenRegistry", `Discarding stale refresh for ${url} (network changed)`);
|
|
8472
|
+
return false;
|
|
8473
|
+
}
|
|
8290
8474
|
const definitions = data;
|
|
8291
8475
|
this.applyDefinitions(definitions);
|
|
8292
8476
|
this.lastRefreshAt = Date.now();
|
|
@@ -8303,6 +8487,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8303
8487
|
* Does an immediate fetch, then repeats at the configured interval.
|
|
8304
8488
|
*/
|
|
8305
8489
|
startAutoRefresh(intervalMs) {
|
|
8490
|
+
if (this.disposed) return;
|
|
8306
8491
|
this.stopAutoRefresh();
|
|
8307
8492
|
if (intervalMs !== void 0) {
|
|
8308
8493
|
this.refreshIntervalMs = intervalMs;
|
|
@@ -8455,6 +8640,8 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8455
8640
|
return def?.id;
|
|
8456
8641
|
}
|
|
8457
8642
|
};
|
|
8643
|
+
|
|
8644
|
+
// registry/global-readers.ts
|
|
8458
8645
|
function getTokenDefinition(coinId) {
|
|
8459
8646
|
return TokenRegistry.getInstance().getDefinition(coinId);
|
|
8460
8647
|
}
|
|
@@ -10129,12 +10316,58 @@ var SphereTokenEngine = class {
|
|
|
10129
10316
|
|
|
10130
10317
|
// token-engine/factory.ts
|
|
10131
10318
|
var DEFAULT_VERIFICATION_POOL_SIZE = 4;
|
|
10319
|
+
function disposedError() {
|
|
10320
|
+
return new SphereError(
|
|
10321
|
+
"Verification worker pool disposed \u2014 the in-flight verification was cancelled",
|
|
10322
|
+
"MODULE_DESTROYED"
|
|
10323
|
+
);
|
|
10324
|
+
}
|
|
10132
10325
|
var ConfiguredWorkerTokenVerifier = class extends WorkerTokenVerifier {
|
|
10133
10326
|
constructor(spawn, poolSize) {
|
|
10134
10327
|
super(poolSize);
|
|
10135
10328
|
this.spawn = spawn;
|
|
10136
10329
|
}
|
|
10330
|
+
disposed = false;
|
|
10331
|
+
/** One rejector per IN-FLIGHT verify(), dropped on settle; empty when idle. */
|
|
10332
|
+
cancellations = /* @__PURE__ */ new Set();
|
|
10333
|
+
/** Verifications awaiting a verdict — back to 0 whenever the verifier is idle. */
|
|
10334
|
+
get pendingCancellations() {
|
|
10335
|
+
return this.cancellations.size;
|
|
10336
|
+
}
|
|
10337
|
+
/** Rejects (see the class note) if `dispose()` lands before the pool answers. */
|
|
10338
|
+
verify(...args) {
|
|
10339
|
+
if (this.disposed) return Promise.reject(disposedError());
|
|
10340
|
+
const verdict = super.verify(...args);
|
|
10341
|
+
return new Promise((resolve, reject) => {
|
|
10342
|
+
const cancel = () => reject(disposedError());
|
|
10343
|
+
this.cancellations.add(cancel);
|
|
10344
|
+
if (this.disposed) {
|
|
10345
|
+
this.cancellations.delete(cancel);
|
|
10346
|
+
cancel();
|
|
10347
|
+
}
|
|
10348
|
+
void verdict.then(
|
|
10349
|
+
(result) => {
|
|
10350
|
+
this.cancellations.delete(cancel);
|
|
10351
|
+
resolve(result);
|
|
10352
|
+
},
|
|
10353
|
+
(error) => {
|
|
10354
|
+
this.cancellations.delete(cancel);
|
|
10355
|
+
reject(error);
|
|
10356
|
+
}
|
|
10357
|
+
);
|
|
10358
|
+
});
|
|
10359
|
+
}
|
|
10360
|
+
/** Idempotent — Sphere.setOracleApiKey disposes the replaced engine twice (facade + caller). */
|
|
10361
|
+
dispose() {
|
|
10362
|
+
if (this.disposed) return;
|
|
10363
|
+
this.disposed = true;
|
|
10364
|
+
super.dispose();
|
|
10365
|
+
const cancelling = [...this.cancellations];
|
|
10366
|
+
this.cancellations.clear();
|
|
10367
|
+
for (const cancel of cancelling) cancel();
|
|
10368
|
+
}
|
|
10137
10369
|
createWorker() {
|
|
10370
|
+
if (this.disposed) throw disposedError();
|
|
10138
10371
|
return this.spawn();
|
|
10139
10372
|
}
|
|
10140
10373
|
};
|
|
@@ -12963,12 +13196,13 @@ var Receive = class {
|
|
|
12963
13196
|
return this.drainFlight.run(() => this.doDrain());
|
|
12964
13197
|
}
|
|
12965
13198
|
start(pollIntervalMs = POLL_INTERVAL_MS) {
|
|
12966
|
-
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() =>
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
13199
|
+
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() => this.spawnDrain()) ?? null;
|
|
13200
|
+
this.pollTimer ??= setInterval(() => this.spawnDrain(), pollIntervalMs);
|
|
13201
|
+
}
|
|
13202
|
+
spawnDrain() {
|
|
13203
|
+
const op = this.drainOnce();
|
|
13204
|
+
if (this.deps.track !== void 0) this.deps.track(op);
|
|
13205
|
+
else void op;
|
|
12972
13206
|
}
|
|
12973
13207
|
stop() {
|
|
12974
13208
|
if (this.pollTimer !== null) clearInterval(this.pollTimer);
|
|
@@ -14071,6 +14305,8 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
|
|
|
14071
14305
|
);
|
|
14072
14306
|
},
|
|
14073
14307
|
syncEpoch: deps.syncEpoch,
|
|
14308
|
+
// #770: the poll/wake drains Receive spawns itself must hold stop() too.
|
|
14309
|
+
track: hooks.track,
|
|
14074
14310
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
14075
14311
|
});
|
|
14076
14312
|
}
|
|
@@ -14213,7 +14449,8 @@ var PaymentsFacade = class {
|
|
|
14213
14449
|
await Promise.allSettled([...this.pendingOps]);
|
|
14214
14450
|
}
|
|
14215
14451
|
}
|
|
14216
|
-
/** Swaps what FUTURE operations snapshot
|
|
14452
|
+
/** Swaps what FUTURE operations snapshot. Chain ops finish on the old engine, but it is
|
|
14453
|
+
* DISPOSED here, so one mid-`verify()` is cancelled — MODULE_DESTROYED, #770(4). */
|
|
14217
14454
|
setEngine(next) {
|
|
14218
14455
|
const previous = this.currentEngine ?? this.deps.engineRef();
|
|
14219
14456
|
this.currentEngine = next;
|
|
@@ -15938,7 +16175,7 @@ function composePaymentsV2(spec) {
|
|
|
15938
16175
|
}),
|
|
15939
16176
|
engineRef: spec.engineRef,
|
|
15940
16177
|
kv,
|
|
15941
|
-
registry:
|
|
16178
|
+
registry: host.registry,
|
|
15942
16179
|
...host.price !== null ? { price: host.price } : {},
|
|
15943
16180
|
emit: host.emit,
|
|
15944
16181
|
resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
|
|
@@ -15966,13 +16203,47 @@ async function deriveL3PredicateAddress(privateKey) {
|
|
|
15966
16203
|
const prehashedPublicKey = getPublicKey(sha2562(privateKey, "hex"));
|
|
15967
16204
|
return deriveDirectAddress(hexToBytes4(prehashedPublicKey));
|
|
15968
16205
|
}
|
|
16206
|
+
var lifecycle = sharedCell(
|
|
16207
|
+
"core.sphere.lifecycle@1",
|
|
16208
|
+
() => ({
|
|
16209
|
+
live: /* @__PURE__ */ new Map(),
|
|
16210
|
+
clearGenerations: /* @__PURE__ */ new Map(),
|
|
16211
|
+
objectStoreKeys: /* @__PURE__ */ new WeakMap(),
|
|
16212
|
+
objectStoreSeq: 0
|
|
16213
|
+
}),
|
|
16214
|
+
(cell) => {
|
|
16215
|
+
const c = cell;
|
|
16216
|
+
return c.live instanceof Map && c.clearGenerations instanceof Map && c.objectStoreKeys instanceof WeakMap && typeof c.objectStoreSeq === "number";
|
|
16217
|
+
}
|
|
16218
|
+
);
|
|
15969
16219
|
var Sphere = class _Sphere {
|
|
15970
|
-
//
|
|
15971
|
-
|
|
16220
|
+
// Live Spheres, keyed by the BACKING STORE their provider addresses. NOT a liveness
|
|
16221
|
+
// API: it exists only so clear()/import() tear down the instances whose data they
|
|
16222
|
+
// really erase. Object identity was too narrow — two providers over one dataDir/DB
|
|
16223
|
+
// are different objects but the same file, so clearing through one left the other's
|
|
16224
|
+
// Sphere live over an emptied KV (#766).
|
|
16225
|
+
static _liveByStorage = lifecycle.live;
|
|
16226
|
+
/** Fallback keys for providers that declare no `backingStoreId` — one per object. */
|
|
16227
|
+
static _objectStoreKeys = lifecycle.objectStoreKeys;
|
|
16228
|
+
/**
|
|
16229
|
+
* How many times each backing store has been cleared. An init is invisible to `clear()`
|
|
16230
|
+
* until it PUBLISHES (#767), so a clear cannot destroy one in flight and would wipe the
|
|
16231
|
+
* store under it. Every init records this number before its first storage work and
|
|
16232
|
+
* publication refuses if it moved (#772). Only cleared stores get an entry, so the
|
|
16233
|
+
* strongly-held keys are bounded by the stores a process actually clears.
|
|
16234
|
+
*/
|
|
16235
|
+
static _clearGenerations = lifecycle.clearGenerations;
|
|
15972
16236
|
// One-time best-effort cleanup of the orphaned vesting cache (prior versions).
|
|
15973
16237
|
static _orphanCacheCleaned = false;
|
|
15974
16238
|
// State
|
|
15975
16239
|
_initialized = false;
|
|
16240
|
+
/**
|
|
16241
|
+
* Destroyed latch (#770). Distinct from `_initialized`, which destroy() clears LAST — after
|
|
16242
|
+
* every teardown step — so it cannot mark "teardown has begun". This is set as destroy()'s
|
|
16243
|
+
* very FIRST statement, so the WHOLE teardown window is guarded, not just the instant after
|
|
16244
|
+
* it. Read by ensureAlive() and by the §7 lifecycle mutex.
|
|
16245
|
+
*/
|
|
16246
|
+
_destroyed = false;
|
|
15976
16247
|
_trackedAddressesLoaded = false;
|
|
15977
16248
|
_identity = null;
|
|
15978
16249
|
_masterKey = null;
|
|
@@ -16030,6 +16301,8 @@ var Sphere = class _Sphere {
|
|
|
16030
16301
|
* the rebuild after an api-key change share one pool configuration.
|
|
16031
16302
|
*/
|
|
16032
16303
|
_verification;
|
|
16304
|
+
/** This Sphere's OWN token registry. Disposed by destroy(); never the process global. */
|
|
16305
|
+
_registry = null;
|
|
16033
16306
|
// Events
|
|
16034
16307
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
16035
16308
|
// Provider management
|
|
@@ -16104,7 +16377,7 @@ var Sphere = class _Sphere {
|
|
|
16104
16377
|
* ```
|
|
16105
16378
|
*/
|
|
16106
16379
|
static async init(options) {
|
|
16107
|
-
if (options.debug) logger.configure({ debug:
|
|
16380
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16108
16381
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16109
16382
|
resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16110
16383
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
@@ -16124,6 +16397,7 @@ var Sphere = class _Sphere {
|
|
|
16124
16397
|
market,
|
|
16125
16398
|
communications: options.communications,
|
|
16126
16399
|
password: options.password,
|
|
16400
|
+
verification: options.verification,
|
|
16127
16401
|
discoverAddresses: options.discoverAddresses,
|
|
16128
16402
|
onProgress: options.onProgress
|
|
16129
16403
|
});
|
|
@@ -16160,6 +16434,7 @@ var Sphere = class _Sphere {
|
|
|
16160
16434
|
market,
|
|
16161
16435
|
communications: options.communications,
|
|
16162
16436
|
password: options.password,
|
|
16437
|
+
verification: options.verification,
|
|
16163
16438
|
discoverAddresses: options.discoverAddresses,
|
|
16164
16439
|
onProgress: options.onProgress
|
|
16165
16440
|
});
|
|
@@ -16241,16 +16516,53 @@ var Sphere = class _Sphere {
|
|
|
16241
16516
|
}
|
|
16242
16517
|
TokenRegistry.configure({ remoteUrl: NETWORKS[network].tokenRegistryUrl, storage });
|
|
16243
16518
|
}
|
|
16519
|
+
/**
|
|
16520
|
+
* Build the registry THIS Sphere owns, so its metadata cannot be repointed by another
|
|
16521
|
+
* Sphere's init. The global is still configured above for consumers that read it
|
|
16522
|
+
* directly; this instance is what the payments facade presents from.
|
|
16523
|
+
*/
|
|
16524
|
+
/**
|
|
16525
|
+
* Own a registry for the duration of `bringUp`, disposing it if any of that rejects.
|
|
16526
|
+
*
|
|
16527
|
+
* `bringUp` must cover EVERY fallible step from here until the Sphere is returned to
|
|
16528
|
+
* its caller — until then nobody holds anything they could destroy, so a
|
|
16529
|
+
* registry left behind is unreachable and its hourly fetch runs for the life of the
|
|
16530
|
+
* process. Guarding a named subset of the steps is what failed twice: the guarded region
|
|
16531
|
+
* and the fallible region were separate things, and drifted. Publication is the LAST
|
|
16532
|
+
* guarded step for the same reason: it can refuse, and a refusal must tear down.
|
|
16533
|
+
*/
|
|
16534
|
+
static async withOwnedRegistry(sphere, storage, network, clearGeneration, bringUp) {
|
|
16535
|
+
sphere._registry = _Sphere.createOwnedRegistry(storage, network);
|
|
16536
|
+
try {
|
|
16537
|
+
await bringUp();
|
|
16538
|
+
_Sphere.publishLive(sphere, clearGeneration);
|
|
16539
|
+
} catch (err) {
|
|
16540
|
+
await sphere.destroy().catch((teardownErr) => {
|
|
16541
|
+
logger.warn("Sphere", "teardown after failed initialization also failed:", teardownErr);
|
|
16542
|
+
});
|
|
16543
|
+
throw err;
|
|
16544
|
+
}
|
|
16545
|
+
}
|
|
16546
|
+
static createOwnedRegistry(storage, network) {
|
|
16547
|
+
if (!network) {
|
|
16548
|
+
throw new SphereError(
|
|
16549
|
+
"network is required to build the token registry. Every Sphere entry point must forward options.network.",
|
|
16550
|
+
"INVALID_CONFIG"
|
|
16551
|
+
);
|
|
16552
|
+
}
|
|
16553
|
+
return TokenRegistry.create({ remoteUrl: NETWORKS[network].tokenRegistryUrl, storage });
|
|
16554
|
+
}
|
|
16244
16555
|
/**
|
|
16245
16556
|
* Create new wallet with mnemonic
|
|
16246
16557
|
*/
|
|
16247
16558
|
static async create(options) {
|
|
16248
|
-
if (options.debug) logger.configure({ debug:
|
|
16559
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16249
16560
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16250
16561
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16251
16562
|
if (!options.mnemonic || !_Sphere.validateMnemonic(options.mnemonic)) {
|
|
16252
16563
|
throw new SphereError("Invalid mnemonic", "INVALID_IDENTITY");
|
|
16253
16564
|
}
|
|
16565
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16254
16566
|
if (await _Sphere.exists(options.storage)) {
|
|
16255
16567
|
throw new SphereError("Wallet already exists. Use Sphere.load() or Sphere.clear() first.", "ALREADY_INITIALIZED");
|
|
16256
16568
|
}
|
|
@@ -16278,44 +16590,46 @@ var Sphere = class _Sphere {
|
|
|
16278
16590
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
16279
16591
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
16280
16592
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16281
|
-
await sphere.
|
|
16282
|
-
|
|
16283
|
-
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
|
|
16291
|
-
|
|
16292
|
-
|
|
16293
|
-
|
|
16294
|
-
|
|
16295
|
-
|
|
16296
|
-
|
|
16297
|
-
|
|
16298
|
-
|
|
16299
|
-
|
|
16300
|
-
|
|
16301
|
-
|
|
16302
|
-
|
|
16303
|
-
|
|
16593
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16594
|
+
await sphere.initializeProviders();
|
|
16595
|
+
await sphere.initializeModules();
|
|
16596
|
+
progress?.({ step: "finalizing", message: "Finalizing wallet..." });
|
|
16597
|
+
await sphere.finalizeWalletCreation();
|
|
16598
|
+
sphere._initialized = true;
|
|
16599
|
+
await sphere.ensureAddressTracked(0);
|
|
16600
|
+
if (options.nametag) {
|
|
16601
|
+
progress?.({ step: "registering_nametag", message: "Registering nametag..." });
|
|
16602
|
+
await sphere.registerNametag(options.nametag);
|
|
16603
|
+
} else {
|
|
16604
|
+
progress?.({ step: "recovering_nametag", message: "Recovering nametag..." });
|
|
16605
|
+
await sphere.recoverNametagFromTransport();
|
|
16606
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16607
|
+
await sphere.syncIdentityWithTransport();
|
|
16608
|
+
}
|
|
16609
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses) {
|
|
16610
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16611
|
+
try {
|
|
16612
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16613
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16614
|
+
if (result.addresses.length > 0) {
|
|
16615
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16616
|
+
}
|
|
16617
|
+
} catch (err) {
|
|
16618
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16304
16619
|
}
|
|
16305
|
-
} catch (err) {
|
|
16306
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16307
16620
|
}
|
|
16308
|
-
|
|
16309
|
-
|
|
16621
|
+
progress?.({ step: "complete", message: "Wallet created" });
|
|
16622
|
+
});
|
|
16310
16623
|
return sphere;
|
|
16311
16624
|
}
|
|
16312
16625
|
/**
|
|
16313
16626
|
* Load existing wallet from storage
|
|
16314
16627
|
*/
|
|
16315
16628
|
static async load(options) {
|
|
16316
|
-
if (options.debug) logger.configure({ debug:
|
|
16629
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16317
16630
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16318
16631
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16632
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16319
16633
|
if (!await _Sphere.exists(options.storage)) {
|
|
16320
16634
|
throw new SphereError("No wallet found. Use Sphere.create() to create a new wallet.", "NOT_INITIALIZED");
|
|
16321
16635
|
}
|
|
@@ -16342,32 +16656,33 @@ var Sphere = class _Sphere {
|
|
|
16342
16656
|
progress?.({ step: "storing_keys", message: "Loading wallet keys..." });
|
|
16343
16657
|
await sphere.loadIdentityFromStorage();
|
|
16344
16658
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16345
|
-
await sphere.
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
|
|
16659
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16660
|
+
await sphere.initializeProviders();
|
|
16661
|
+
await sphere.initializeModules();
|
|
16662
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16663
|
+
await sphere.syncIdentityWithTransport();
|
|
16664
|
+
sphere._initialized = true;
|
|
16665
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses && sphere._masterKey) {
|
|
16666
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16667
|
+
try {
|
|
16668
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16669
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16670
|
+
if (result.addresses.length > 0) {
|
|
16671
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16672
|
+
}
|
|
16673
|
+
} catch (err) {
|
|
16674
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16358
16675
|
}
|
|
16359
|
-
} catch (err) {
|
|
16360
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16361
16676
|
}
|
|
16362
|
-
|
|
16363
|
-
|
|
16677
|
+
progress?.({ step: "complete", message: "Wallet loaded" });
|
|
16678
|
+
});
|
|
16364
16679
|
return sphere;
|
|
16365
16680
|
}
|
|
16366
16681
|
/**
|
|
16367
16682
|
* Import wallet from mnemonic or master key
|
|
16368
16683
|
*/
|
|
16369
16684
|
static async import(options) {
|
|
16370
|
-
if (options.debug) logger.configure({ debug:
|
|
16685
|
+
if (options.debug !== void 0) logger.configure({ debug: options.debug });
|
|
16371
16686
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
16372
16687
|
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
16373
16688
|
if (!options.mnemonic && !options.masterKey) {
|
|
@@ -16375,7 +16690,7 @@ var Sphere = class _Sphere {
|
|
|
16375
16690
|
}
|
|
16376
16691
|
const progress = options.onProgress;
|
|
16377
16692
|
logger.debug("Sphere", "Starting import...");
|
|
16378
|
-
const needsClear = _Sphere.
|
|
16693
|
+
const needsClear = _Sphere.liveOn(options.storage).length > 0 || await _Sphere.exists(options.storage);
|
|
16379
16694
|
if (needsClear) {
|
|
16380
16695
|
progress?.({ step: "clearing", message: "Clearing previous wallet data..." });
|
|
16381
16696
|
logger.debug("Sphere", "Clearing existing wallet data...");
|
|
@@ -16389,6 +16704,7 @@ var Sphere = class _Sphere {
|
|
|
16389
16704
|
await options.storage.connect();
|
|
16390
16705
|
logger.debug("Sphere", "Storage reconnected");
|
|
16391
16706
|
}
|
|
16707
|
+
const clearGeneration = _Sphere.clearGenerationOf(options.storage);
|
|
16392
16708
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
16393
16709
|
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
16394
16710
|
const marketConfig = _Sphere.resolveMarketConfig(options.market);
|
|
@@ -16432,44 +16748,44 @@ var Sphere = class _Sphere {
|
|
|
16432
16748
|
}
|
|
16433
16749
|
progress?.({ step: "initializing", message: "Initializing wallet..." });
|
|
16434
16750
|
logger.debug("Sphere", "Initializing providers...");
|
|
16435
|
-
await sphere.
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
|
|
16448
|
-
|
|
16449
|
-
|
|
16450
|
-
|
|
16451
|
-
|
|
16452
|
-
|
|
16453
|
-
|
|
16454
|
-
|
|
16455
|
-
|
|
16456
|
-
|
|
16457
|
-
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16751
|
+
await _Sphere.withOwnedRegistry(sphere, options.storage, options.network, clearGeneration, async () => {
|
|
16752
|
+
await sphere.initializeProviders();
|
|
16753
|
+
await sphere.initializeModules();
|
|
16754
|
+
logger.debug("Sphere", "Modules initialized");
|
|
16755
|
+
if (!options.nametag) {
|
|
16756
|
+
progress?.({ step: "recovering_nametag", message: "Recovering nametag..." });
|
|
16757
|
+
logger.debug("Sphere", "Recovering Unicity ID from transport...");
|
|
16758
|
+
await sphere.recoverNametagFromTransport();
|
|
16759
|
+
logger.debug("Sphere", "Unicity ID recovery done");
|
|
16760
|
+
progress?.({ step: "syncing_identity", message: "Publishing identity..." });
|
|
16761
|
+
await sphere.syncIdentityWithTransport();
|
|
16762
|
+
}
|
|
16763
|
+
progress?.({ step: "finalizing", message: "Finalizing wallet creation..." });
|
|
16764
|
+
logger.debug("Sphere", "Finalizing wallet creation...");
|
|
16765
|
+
await sphere.finalizeWalletCreation();
|
|
16766
|
+
sphere._initialized = true;
|
|
16767
|
+
logger.debug("Sphere", "Tracking address 0...");
|
|
16768
|
+
await sphere.ensureAddressTracked(0);
|
|
16769
|
+
if (options.nametag) {
|
|
16770
|
+
progress?.({ step: "registering_nametag", message: "Registering nametag..." });
|
|
16771
|
+
logger.debug("Sphere", "Registering Unicity ID...");
|
|
16772
|
+
await sphere.registerNametag(options.nametag);
|
|
16773
|
+
}
|
|
16774
|
+
if (options.discoverAddresses !== false && sphere._transport.discoverAddresses) {
|
|
16775
|
+
progress?.({ step: "discovering_addresses", message: "Discovering addresses..." });
|
|
16776
|
+
try {
|
|
16777
|
+
const discoverOpts = typeof options.discoverAddresses === "object" ? { ...options.discoverAddresses, autoTrack: options.discoverAddresses.autoTrack ?? true } : { autoTrack: true };
|
|
16778
|
+
const result = await sphere.discoverAddresses(discoverOpts);
|
|
16779
|
+
if (result.addresses.length > 0) {
|
|
16780
|
+
logger.debug("Sphere", `Address discovery: found ${result.addresses.length} address(es)`);
|
|
16781
|
+
}
|
|
16782
|
+
} catch (err) {
|
|
16783
|
+
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16466
16784
|
}
|
|
16467
|
-
} catch (err) {
|
|
16468
|
-
logger.warn("Sphere", "Address discovery failed (non-fatal):", err);
|
|
16469
16785
|
}
|
|
16470
|
-
|
|
16471
|
-
|
|
16472
|
-
|
|
16786
|
+
progress?.({ step: "complete", message: "Import complete" });
|
|
16787
|
+
logger.debug("Sphere", "Import complete");
|
|
16788
|
+
});
|
|
16473
16789
|
return sphere;
|
|
16474
16790
|
}
|
|
16475
16791
|
/**
|
|
@@ -16487,9 +16803,17 @@ var Sphere = class _Sphere {
|
|
|
16487
16803
|
*/
|
|
16488
16804
|
static async clear(options) {
|
|
16489
16805
|
const storage = options.storage;
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
await _Sphere.
|
|
16806
|
+
_Sphere.bumpClearGeneration(storage);
|
|
16807
|
+
try {
|
|
16808
|
+
await _Sphere.clearStore(storage);
|
|
16809
|
+
} finally {
|
|
16810
|
+
_Sphere.bumpClearGeneration(storage);
|
|
16811
|
+
}
|
|
16812
|
+
}
|
|
16813
|
+
static async clearStore(storage) {
|
|
16814
|
+
for (const live of _Sphere.liveOn(storage)) {
|
|
16815
|
+
logger.debug("Sphere", "Destroying Sphere instance on this storage...");
|
|
16816
|
+
await live.destroy();
|
|
16493
16817
|
logger.debug("Sphere", "Sphere instance destroyed");
|
|
16494
16818
|
}
|
|
16495
16819
|
logger.debug("Sphere", "Yielding 50ms for IDB transaction settlement...");
|
|
@@ -16549,16 +16873,64 @@ var Sphere = class _Sphere {
|
|
|
16549
16873
|
}
|
|
16550
16874
|
}
|
|
16551
16875
|
/**
|
|
16552
|
-
*
|
|
16876
|
+
* The key a provider registers under: the store it addresses, so every provider
|
|
16877
|
+
* over one dataDir/DB shares an entry. A provider that declares no store keeps a
|
|
16878
|
+
* private key, leaving custom implementations scoped by object identity.
|
|
16553
16879
|
*/
|
|
16554
|
-
static
|
|
16555
|
-
|
|
16880
|
+
static storeKeyOf(storage) {
|
|
16881
|
+
const declared = storage.backingStoreId;
|
|
16882
|
+
if (declared) return `store:${declared}`;
|
|
16883
|
+
let key = _Sphere._objectStoreKeys.get(storage);
|
|
16884
|
+
if (!key) {
|
|
16885
|
+
key = `object:${++lifecycle.objectStoreSeq}`;
|
|
16886
|
+
_Sphere._objectStoreKeys.set(storage, key);
|
|
16887
|
+
}
|
|
16888
|
+
return key;
|
|
16889
|
+
}
|
|
16890
|
+
/** The clears this store has seen. Absent means none — `clear()` creates the entry. */
|
|
16891
|
+
static clearGenerationOf(storage) {
|
|
16892
|
+
return _Sphere._clearGenerations.get(_Sphere.storeKeyOf(storage)) ?? 0;
|
|
16893
|
+
}
|
|
16894
|
+
static bumpClearGeneration(storage) {
|
|
16895
|
+
const key = _Sphere.storeKeyOf(storage);
|
|
16896
|
+
_Sphere._clearGenerations.set(key, (_Sphere._clearGenerations.get(key) ?? 0) + 1);
|
|
16556
16897
|
}
|
|
16557
16898
|
/**
|
|
16558
|
-
*
|
|
16899
|
+
* Make a fully-built Sphere reachable — or refuse, if `clear()` emptied the store
|
|
16900
|
+
* under it while it was building. Check and registration are one SYNCHRONOUS step on
|
|
16901
|
+
* purpose: split by an await, a clear could land between them and wipe a store whose
|
|
16902
|
+
* Sphere is already published. The refusal throws inside `withOwnedRegistry`, whose
|
|
16903
|
+
* teardown then destroys the half-built Sphere the caller never received.
|
|
16559
16904
|
*/
|
|
16560
|
-
static
|
|
16561
|
-
|
|
16905
|
+
static publishLive(sphere, clearGeneration) {
|
|
16906
|
+
if (_Sphere.clearGenerationOf(sphere._storage) !== clearGeneration) {
|
|
16907
|
+
throw new SphereError(
|
|
16908
|
+
"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.",
|
|
16909
|
+
"STORAGE_ERROR"
|
|
16910
|
+
);
|
|
16911
|
+
}
|
|
16912
|
+
_Sphere.registerLive(sphere);
|
|
16913
|
+
}
|
|
16914
|
+
/** Record a fully-built Sphere against the storage it owns. See `_liveByStorage`. */
|
|
16915
|
+
static registerLive(sphere) {
|
|
16916
|
+
const key = _Sphere.storeKeyOf(sphere._storage);
|
|
16917
|
+
let live = _Sphere._liveByStorage.get(key);
|
|
16918
|
+
if (!live) {
|
|
16919
|
+
live = /* @__PURE__ */ new Set();
|
|
16920
|
+
_Sphere._liveByStorage.set(key, live);
|
|
16921
|
+
}
|
|
16922
|
+
live.add(sphere);
|
|
16923
|
+
}
|
|
16924
|
+
static unregisterLive(sphere) {
|
|
16925
|
+
const key = _Sphere.storeKeyOf(sphere._storage);
|
|
16926
|
+
const live = _Sphere._liveByStorage.get(key);
|
|
16927
|
+
if (!live) return;
|
|
16928
|
+
live.delete(sphere);
|
|
16929
|
+
if (live.size === 0) _Sphere._liveByStorage.delete(key);
|
|
16930
|
+
}
|
|
16931
|
+
/** Snapshot — callers iterate this while destroy() mutates the underlying Set. */
|
|
16932
|
+
static liveOn(storage) {
|
|
16933
|
+
return Array.from(_Sphere._liveByStorage.get(_Sphere.storeKeyOf(storage)) ?? []);
|
|
16562
16934
|
}
|
|
16563
16935
|
/**
|
|
16564
16936
|
* Validate mnemonic using BIP39
|
|
@@ -16769,8 +17141,7 @@ var Sphere = class _Sphere {
|
|
|
16769
17141
|
password
|
|
16770
17142
|
});
|
|
16771
17143
|
if (result.success) {
|
|
16772
|
-
|
|
16773
|
-
return { success: true, sphere: sphere2, mnemonic: result.mnemonic };
|
|
17144
|
+
return { success: true, sphere: result.sphere, mnemonic: result.mnemonic };
|
|
16774
17145
|
}
|
|
16775
17146
|
if (!password && result.error?.includes("Password required")) {
|
|
16776
17147
|
return { success: false, needsPassword: true, error: result.error };
|
|
@@ -17072,7 +17443,8 @@ var Sphere = class _Sphere {
|
|
|
17072
17443
|
/**
|
|
17073
17444
|
* Import wallet from JSON backup
|
|
17074
17445
|
*
|
|
17075
|
-
* @returns
|
|
17446
|
+
* @returns `{ success, sphere?, mnemonic?, error? }`. `sphere` is the instance built
|
|
17447
|
+
* on the SUPPLIED storage — hold it, there is no global to look it up from (#766).
|
|
17076
17448
|
*
|
|
17077
17449
|
* @example
|
|
17078
17450
|
* ```ts
|
|
@@ -17113,18 +17485,18 @@ var Sphere = class _Sphere {
|
|
|
17113
17485
|
}
|
|
17114
17486
|
const basePath = data.wallet.descriptorPath ? `m/${data.wallet.descriptorPath}` : DEFAULT_BASE_PATH;
|
|
17115
17487
|
if (mnemonic) {
|
|
17116
|
-
await _Sphere.import({ ...baseOptions, mnemonic, basePath });
|
|
17117
|
-
return { success: true, mnemonic };
|
|
17488
|
+
const sphere = await _Sphere.import({ ...baseOptions, mnemonic, basePath });
|
|
17489
|
+
return { success: true, sphere, mnemonic };
|
|
17118
17490
|
}
|
|
17119
17491
|
if (masterKey) {
|
|
17120
|
-
await _Sphere.import({
|
|
17492
|
+
const sphere = await _Sphere.import({
|
|
17121
17493
|
...baseOptions,
|
|
17122
17494
|
masterKey,
|
|
17123
17495
|
chainCode: data.wallet.chainCode,
|
|
17124
17496
|
basePath,
|
|
17125
17497
|
derivationMode: data.derivationMode || (data.wallet.isBIP32 ? "bip32" : "wif_hmac")
|
|
17126
17498
|
});
|
|
17127
|
-
return { success: true };
|
|
17499
|
+
return { success: true, sphere };
|
|
17128
17500
|
}
|
|
17129
17501
|
return { success: false, error: "No mnemonic or master key in wallet data" };
|
|
17130
17502
|
} catch (e) {
|
|
@@ -17247,6 +17619,7 @@ var Sphere = class _Sphere {
|
|
|
17247
17619
|
}
|
|
17248
17620
|
if (entry.hidden === hidden) return;
|
|
17249
17621
|
entry.hidden = hidden;
|
|
17622
|
+
entry.updatedAt = Date.now();
|
|
17250
17623
|
await this.persistTrackedAddresses();
|
|
17251
17624
|
const eventType = hidden ? "address:hidden" : "address:unhidden";
|
|
17252
17625
|
this.emitEvent(eventType, { index, addressId: entry.addressId });
|
|
@@ -17275,9 +17648,6 @@ var Sphere = class _Sphere {
|
|
|
17275
17648
|
if (!this._masterKey) {
|
|
17276
17649
|
throw new SphereError("HD derivation requires master key with chain code. Cannot switch addresses.", "INVALID_CONFIG");
|
|
17277
17650
|
}
|
|
17278
|
-
if (index < 0) {
|
|
17279
|
-
throw new SphereError("Address index must be non-negative", "INVALID_CONFIG");
|
|
17280
|
-
}
|
|
17281
17651
|
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
17282
17652
|
if (newNametag && !isValidNametag(newNametag)) {
|
|
17283
17653
|
throw new SphereError("Invalid Unicity ID format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.", "VALIDATION_ERROR");
|
|
@@ -17314,7 +17684,9 @@ var Sphere = class _Sphere {
|
|
|
17314
17684
|
if (firstVisit) {
|
|
17315
17685
|
logger.debug("Sphere", `switchToAddress(${index}): creating per-address modules (lazy init)`);
|
|
17316
17686
|
this._storage.setIdentity(newIdentity);
|
|
17687
|
+
this.ensureAlive();
|
|
17317
17688
|
await this.initializeAddressModules({ index, identity: newIdentity });
|
|
17689
|
+
if (this._destroyed) await this.discardModulesBuiltDuringDestroy(index);
|
|
17318
17690
|
} else if (nametag !== this._addressModules.get(index).identity.nametag) {
|
|
17319
17691
|
this._addressModules.get(index).identity = newIdentity;
|
|
17320
17692
|
}
|
|
@@ -17325,14 +17697,18 @@ var Sphere = class _Sphere {
|
|
|
17325
17697
|
this._communications = activeModules.communications;
|
|
17326
17698
|
this._groupChat = activeModules.groupChat;
|
|
17327
17699
|
this._market = activeModules.market;
|
|
17700
|
+
this.ensureAlive();
|
|
17328
17701
|
await this.stopThenStartPaymentsV2(index, newIdentity);
|
|
17702
|
+
this.ensureAlive();
|
|
17329
17703
|
await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
|
|
17330
17704
|
this._storage.setIdentity(this._identity);
|
|
17331
17705
|
if (this._transport.setFallbackSince) {
|
|
17332
17706
|
const fallbackTs = Math.floor(Date.now() / 1e3) - 86400;
|
|
17333
17707
|
this._transport.setFallbackSince(fallbackTs);
|
|
17334
17708
|
}
|
|
17709
|
+
this.ensureAlive();
|
|
17335
17710
|
await this._transport.setIdentity(this._identity);
|
|
17711
|
+
this.ensureAlive();
|
|
17336
17712
|
if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
|
|
17337
17713
|
await this._transportMux.rebindToSharedClient();
|
|
17338
17714
|
}
|
|
@@ -17352,6 +17728,7 @@ var Sphere = class _Sphere {
|
|
|
17352
17728
|
* Runs after switchToAddress returns so L3 queries can start immediately.
|
|
17353
17729
|
*/
|
|
17354
17730
|
async postSwitchSync(index, newNametag) {
|
|
17731
|
+
this.ensureAlive();
|
|
17355
17732
|
if (!newNametag) {
|
|
17356
17733
|
await this.syncIdentityWithTransport();
|
|
17357
17734
|
}
|
|
@@ -17369,6 +17746,39 @@ var Sphere = class _Sphere {
|
|
|
17369
17746
|
* independently in background. The payments vertical is NOT created here —
|
|
17370
17747
|
* the caller starts it via stopThenStartPaymentsV2 (§7 single vertical).
|
|
17371
17748
|
*/
|
|
17749
|
+
/** Tear one address's module set down. Each address owns its own engine, so its own pool. */
|
|
17750
|
+
static destroyModuleSet(index, moduleSet) {
|
|
17751
|
+
try {
|
|
17752
|
+
moduleSet.communications.destroy();
|
|
17753
|
+
moduleSet.groupChat?.destroy();
|
|
17754
|
+
moduleSet.market?.destroy();
|
|
17755
|
+
moduleSet.tokenEngine?.dispose?.();
|
|
17756
|
+
logger.debug("Sphere", `Destroyed modules for address ${index}`);
|
|
17757
|
+
} catch (err) {
|
|
17758
|
+
logger.warn("Sphere", `Error destroying modules for address ${index}:`, err);
|
|
17759
|
+
}
|
|
17760
|
+
}
|
|
17761
|
+
/**
|
|
17762
|
+
* Undo a module set built while `destroy()` was running.
|
|
17763
|
+
*
|
|
17764
|
+
* `destroy()`'s teardown loop has already emptied `_addressModules`, so a set
|
|
17765
|
+
* registered after it would stay live and unreachable — its own engine (and worker
|
|
17766
|
+
* pool), and a transport mux `ensureTransportMux` rebuilt because teardown had just
|
|
17767
|
+
* nulled the field. Then re-raise, so the switch fails rather than reporting success
|
|
17768
|
+
* on a destroyed Sphere (#770, #772 review).
|
|
17769
|
+
*/
|
|
17770
|
+
async discardModulesBuiltDuringDestroy(index) {
|
|
17771
|
+
const built = this._addressModules.get(index);
|
|
17772
|
+
this._addressModules.delete(index);
|
|
17773
|
+
if (built) _Sphere.destroyModuleSet(index, built);
|
|
17774
|
+
if (this._transportMux) {
|
|
17775
|
+
const mux = this._transportMux;
|
|
17776
|
+
this._transportMux = null;
|
|
17777
|
+
await _Sphere.safeDisconnect("transport mux", () => mux.disconnect());
|
|
17778
|
+
}
|
|
17779
|
+
this.ensureAlive();
|
|
17780
|
+
throw new SphereError("Sphere destroyed", "NOT_INITIALIZED");
|
|
17781
|
+
}
|
|
17372
17782
|
async initializeAddressModules(spec) {
|
|
17373
17783
|
const { index, identity } = spec;
|
|
17374
17784
|
const emitEvent = this.emitEvent.bind(this);
|
|
@@ -17456,6 +17866,14 @@ var Sphere = class _Sphere {
|
|
|
17456
17866
|
const adapter = await this._transportMux.addAddress(index, identity, this._transport);
|
|
17457
17867
|
return adapter;
|
|
17458
17868
|
}
|
|
17869
|
+
/** A BIP32 child number: an integer in 0…0xffffffff. Anything else aliases an address. */
|
|
17870
|
+
static assertDerivableIndex(index) {
|
|
17871
|
+
if (isDerivableIndex(index)) return;
|
|
17872
|
+
throw new SphereError(
|
|
17873
|
+
`Address index ${String(index)} is not a BIP32 child number: it must be an integer in 0\u20260xffffffff.`,
|
|
17874
|
+
"INVALID_CONFIG"
|
|
17875
|
+
);
|
|
17876
|
+
}
|
|
17459
17877
|
/**
|
|
17460
17878
|
* Derive address at a specific index
|
|
17461
17879
|
*
|
|
@@ -17501,6 +17919,7 @@ var Sphere = class _Sphere {
|
|
|
17501
17919
|
* when _initialized is still false.
|
|
17502
17920
|
*/
|
|
17503
17921
|
_deriveAddressInternal(index, isChange = false) {
|
|
17922
|
+
_Sphere.assertDerivableIndex(index);
|
|
17504
17923
|
if (!this._masterKey) {
|
|
17505
17924
|
throw new SphereError("HD derivation requires master key with chain code", "INVALID_CONFIG");
|
|
17506
17925
|
}
|
|
@@ -17592,6 +18011,7 @@ var Sphere = class _Sphere {
|
|
|
17592
18011
|
}
|
|
17593
18012
|
if (tracked.hidden !== hidden) {
|
|
17594
18013
|
tracked.hidden = hidden;
|
|
18014
|
+
tracked.updatedAt = Date.now();
|
|
17595
18015
|
}
|
|
17596
18016
|
}
|
|
17597
18017
|
await this.persistTrackedAddresses();
|
|
@@ -18233,7 +18653,48 @@ var Sphere = class _Sphere {
|
|
|
18233
18653
|
// ===========================================================================
|
|
18234
18654
|
// Public Methods - Lifecycle
|
|
18235
18655
|
// ===========================================================================
|
|
18656
|
+
/**
|
|
18657
|
+
* Disconnect transport, storage and oracle, attempting each even if an earlier one
|
|
18658
|
+
* rejects. They are separate resources, and one failure must not skip the rest — that
|
|
18659
|
+
* is how a partial teardown leaves connections open, which is exactly the state the
|
|
18660
|
+
* failed-initialization path calls destroy() in.
|
|
18661
|
+
*/
|
|
18662
|
+
async disconnectProvidersIndependently() {
|
|
18663
|
+
const steps = [
|
|
18664
|
+
["transport", () => this._transport.disconnect()],
|
|
18665
|
+
["storage", () => this._storage.disconnect()],
|
|
18666
|
+
["oracle", () => this._oracle.disconnect()]
|
|
18667
|
+
];
|
|
18668
|
+
for (const [what, disconnect] of steps) {
|
|
18669
|
+
await _Sphere.safeDisconnect(what, disconnect);
|
|
18670
|
+
}
|
|
18671
|
+
}
|
|
18672
|
+
/** Run one teardown step, logging rather than propagating so the next one still runs. */
|
|
18673
|
+
static async safeDisconnect(what, run) {
|
|
18674
|
+
try {
|
|
18675
|
+
await run();
|
|
18676
|
+
} catch (err) {
|
|
18677
|
+
logger.warn("Sphere", `${what} disconnect failed during destroy:`, err);
|
|
18678
|
+
}
|
|
18679
|
+
}
|
|
18680
|
+
/**
|
|
18681
|
+
* Tear this Sphere down: stop the vertical, destroy the modules, disconnect the providers
|
|
18682
|
+
* and zero the key material. Idempotent by construction (every step is null-guarded) and
|
|
18683
|
+
* deliberately WITHOUT an early return on re-entry, which would change what a double call
|
|
18684
|
+
* means.
|
|
18685
|
+
*
|
|
18686
|
+
* #770: the FIRST statement flips `_destroyed` — before any await, and before this queues
|
|
18687
|
+
* its own stop on the §7 lifecycle mutex. That position is load-bearing. Every guard reads
|
|
18688
|
+
* the flag, so from that instant any switchToAddress step not yet begun refuses; and
|
|
18689
|
+
* because it flips SYNCHRONOUSLY at entry, a stop/start pair the mutex runs after this call
|
|
18690
|
+
* sees `true` and skips its start. Set it beside `_initialized` at the bottom instead and a
|
|
18691
|
+
* concurrent switch re-arms a wallet whose owner already had destroy() return: fresh
|
|
18692
|
+
* sockets, a fresh wallet-api session, a whole fresh vertical nothing will ever stop.
|
|
18693
|
+
*/
|
|
18236
18694
|
async destroy() {
|
|
18695
|
+
this._destroyed = true;
|
|
18696
|
+
this._registry?.dispose();
|
|
18697
|
+
this._registry = null;
|
|
18237
18698
|
this.cleanupProviderEventSubscriptions();
|
|
18238
18699
|
try {
|
|
18239
18700
|
await this.queuePaymentsV2Op(() => this.stopPaymentsV2Inner());
|
|
@@ -18241,28 +18702,19 @@ var Sphere = class _Sphere {
|
|
|
18241
18702
|
logger.warn("Sphere", "payments vertical stop failed during destroy:", err);
|
|
18242
18703
|
}
|
|
18243
18704
|
for (const [idx, moduleSet] of this._addressModules.entries()) {
|
|
18244
|
-
|
|
18245
|
-
moduleSet.communications.destroy();
|
|
18246
|
-
moduleSet.groupChat?.destroy();
|
|
18247
|
-
moduleSet.market?.destroy();
|
|
18248
|
-
moduleSet.tokenEngine?.dispose?.();
|
|
18249
|
-
logger.debug("Sphere", `Destroyed modules for address ${idx}`);
|
|
18250
|
-
} catch (err) {
|
|
18251
|
-
logger.warn("Sphere", `Error destroying modules for address ${idx}:`, err);
|
|
18252
|
-
}
|
|
18705
|
+
_Sphere.destroyModuleSet(idx, moduleSet);
|
|
18253
18706
|
}
|
|
18254
18707
|
this._addressModules.clear();
|
|
18255
18708
|
this._communications.destroy();
|
|
18256
18709
|
this._groupChat?.destroy();
|
|
18257
18710
|
this._market?.destroy();
|
|
18258
18711
|
if (this._transportMux) {
|
|
18259
|
-
|
|
18712
|
+
const mux = this._transportMux;
|
|
18713
|
+
await _Sphere.safeDisconnect("transport mux", () => mux.disconnect());
|
|
18260
18714
|
this._transportMux = null;
|
|
18261
18715
|
}
|
|
18262
18716
|
this._tokenEngine?.dispose?.();
|
|
18263
|
-
await this.
|
|
18264
|
-
await this._storage.disconnect();
|
|
18265
|
-
await this._oracle.disconnect();
|
|
18717
|
+
await this.disconnectProvidersIndependently();
|
|
18266
18718
|
this._initialized = false;
|
|
18267
18719
|
this._trackedAddressesLoaded = false;
|
|
18268
18720
|
this._identity = null;
|
|
@@ -18274,9 +18726,7 @@ var Sphere = class _Sphere {
|
|
|
18274
18726
|
this._addressNametags.clear();
|
|
18275
18727
|
this._disabledProviders.clear();
|
|
18276
18728
|
this.eventHandlers.clear();
|
|
18277
|
-
|
|
18278
|
-
_Sphere.instance = null;
|
|
18279
|
-
}
|
|
18729
|
+
_Sphere.unregisterLive(this);
|
|
18280
18730
|
}
|
|
18281
18731
|
// ===========================================================================
|
|
18282
18732
|
// Private: Storage
|
|
@@ -18613,10 +19063,21 @@ var Sphere = class _Sphere {
|
|
|
18613
19063
|
);
|
|
18614
19064
|
return run;
|
|
18615
19065
|
}
|
|
18616
|
-
/**
|
|
19066
|
+
/**
|
|
19067
|
+
* Switch/boot: stop whatever runs, then start `index`'s vertical — atomically vs other
|
|
19068
|
+
* lifecycle ops.
|
|
19069
|
+
*
|
|
19070
|
+
* #770: the destroyed check between the two halves settles the FACADE vector on its own.
|
|
19071
|
+
* `_destroyed` flips synchronously at destroy() entry, so any closure that BEGINS executing
|
|
19072
|
+
* after destroy() was called sees `true`, and any closure already past the check has
|
|
19073
|
+
* facade.start() in flight — which destroy()'s own queued stop is necessarily ordered
|
|
19074
|
+
* after. The TRANSPORT vector is not on this mutex at all; the ensureAlive() calls in
|
|
19075
|
+
* switchToAddress are what cover it.
|
|
19076
|
+
*/
|
|
18617
19077
|
stopThenStartPaymentsV2(index, identity) {
|
|
18618
19078
|
return this.queuePaymentsV2Op(async () => {
|
|
18619
19079
|
await this.stopPaymentsV2Inner();
|
|
19080
|
+
if (this._destroyed) return;
|
|
18620
19081
|
await this.startPaymentsV2Inner(index, identity);
|
|
18621
19082
|
});
|
|
18622
19083
|
}
|
|
@@ -18648,6 +19109,7 @@ var Sphere = class _Sphere {
|
|
|
18648
19109
|
host: {
|
|
18649
19110
|
storage: this._storage,
|
|
18650
19111
|
price: this._priceProvider,
|
|
19112
|
+
registry: this._registry ?? TokenRegistry.getInstance(),
|
|
18651
19113
|
emit: (event, payload) => this.emitEvent(event, payload),
|
|
18652
19114
|
resolvePeer: (identifier) => this._transport.resolve?.(identifier) ?? Promise.resolve(null),
|
|
18653
19115
|
// Scoped to THIS facade's address, never "whichever is active":
|
|
@@ -18668,7 +19130,18 @@ var Sphere = class _Sphere {
|
|
|
18668
19130
|
this._paymentsV2Active = null;
|
|
18669
19131
|
await active.facade.stop();
|
|
18670
19132
|
}
|
|
19133
|
+
/**
|
|
19134
|
+
* Refuse once destroy() has STARTED (#770). `_initialized` cannot carry this: destroy()
|
|
19135
|
+
* clears it last, so every teardown step is a window in which a concurrent call still reads
|
|
19136
|
+
* a ready Sphere and re-arms it.
|
|
19137
|
+
*/
|
|
19138
|
+
ensureAlive() {
|
|
19139
|
+
if (this._destroyed) {
|
|
19140
|
+
throw new SphereError("Sphere destroyed", "NOT_INITIALIZED");
|
|
19141
|
+
}
|
|
19142
|
+
}
|
|
18671
19143
|
ensureReady() {
|
|
19144
|
+
this.ensureAlive();
|
|
18672
19145
|
if (!this._initialized) {
|
|
18673
19146
|
throw new SphereError("Sphere not initialized", "NOT_INITIALIZED");
|
|
18674
19147
|
}
|
|
@@ -18718,7 +19191,6 @@ var createSphere = Sphere.create.bind(Sphere);
|
|
|
18718
19191
|
var loadSphere = Sphere.load.bind(Sphere);
|
|
18719
19192
|
var importSphere = Sphere.import.bind(Sphere);
|
|
18720
19193
|
var initSphere = Sphere.init.bind(Sphere);
|
|
18721
|
-
var getSphere = Sphere.getInstance.bind(Sphere);
|
|
18722
19194
|
var sphereExists = Sphere.exists.bind(Sphere);
|
|
18723
19195
|
|
|
18724
19196
|
// core/currency.ts
|
|
@@ -18890,28 +19362,59 @@ async function checkWebSocket(url, timeoutMs) {
|
|
|
18890
19362
|
};
|
|
18891
19363
|
});
|
|
18892
19364
|
}
|
|
19365
|
+
var HEALTH_PROBE_STATE_ID = "00".repeat(32);
|
|
19366
|
+
function readBlockNumber(body) {
|
|
19367
|
+
if (typeof body !== "object" || body === null) return null;
|
|
19368
|
+
const result = body.result;
|
|
19369
|
+
if (typeof result !== "object" || result === null) return null;
|
|
19370
|
+
const n = result.blockNumber;
|
|
19371
|
+
if (typeof n === "string") return /^\d+$/.test(n) ? n : null;
|
|
19372
|
+
if (typeof n === "number") return Number.isInteger(n) && n >= 0 ? String(n) : null;
|
|
19373
|
+
return null;
|
|
19374
|
+
}
|
|
19375
|
+
function readRpcError(body) {
|
|
19376
|
+
if (typeof body !== "object" || body === null) return null;
|
|
19377
|
+
const err = body.error;
|
|
19378
|
+
if (typeof err === "string") return err;
|
|
19379
|
+
if (typeof err === "object" && err !== null) {
|
|
19380
|
+
const message = err.message;
|
|
19381
|
+
if (typeof message === "string") return message;
|
|
19382
|
+
return JSON.stringify(err);
|
|
19383
|
+
}
|
|
19384
|
+
return null;
|
|
19385
|
+
}
|
|
19386
|
+
function oracleVerdict(url, responseTimeMs, response, body) {
|
|
19387
|
+
if (readBlockNumber(body) !== null) return { healthy: true, url, responseTimeMs };
|
|
19388
|
+
const rpcError = readRpcError(body);
|
|
19389
|
+
return {
|
|
19390
|
+
healthy: false,
|
|
19391
|
+
url,
|
|
19392
|
+
responseTimeMs,
|
|
19393
|
+
error: rpcError ?? (response.ok ? "aggregator answered without a block height" : `HTTP ${String(response.status)} ${response.statusText}`)
|
|
19394
|
+
};
|
|
19395
|
+
}
|
|
18893
19396
|
async function checkOracle(url, timeoutMs) {
|
|
18894
19397
|
const startTime = Date.now();
|
|
19398
|
+
const controller = new AbortController();
|
|
19399
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18895
19400
|
try {
|
|
18896
|
-
const controller = new AbortController();
|
|
18897
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18898
19401
|
const response = await fetch(url, {
|
|
18899
19402
|
method: "POST",
|
|
18900
19403
|
headers: { "Content-Type": "application/json" },
|
|
18901
|
-
body: JSON.stringify({
|
|
19404
|
+
body: JSON.stringify({
|
|
19405
|
+
jsonrpc: "2.0",
|
|
19406
|
+
id: 1,
|
|
19407
|
+
method: "get_block_height",
|
|
19408
|
+
params: { stateId: HEALTH_PROBE_STATE_ID }
|
|
19409
|
+
}),
|
|
18902
19410
|
signal: controller.signal
|
|
18903
19411
|
});
|
|
18904
|
-
clearTimeout(timer);
|
|
18905
19412
|
const responseTimeMs = Date.now() - startTime;
|
|
18906
|
-
|
|
18907
|
-
|
|
19413
|
+
const body = await response.json().catch(() => null);
|
|
19414
|
+
if (controller.signal.aborted) {
|
|
19415
|
+
return { healthy: false, url, responseTimeMs, error: `Connection timeout after ${timeoutMs}ms` };
|
|
18908
19416
|
}
|
|
18909
|
-
return
|
|
18910
|
-
healthy: false,
|
|
18911
|
-
url,
|
|
18912
|
-
responseTimeMs,
|
|
18913
|
-
error: `HTTP ${response.status} ${response.statusText}`
|
|
18914
|
-
};
|
|
19417
|
+
return oracleVerdict(url, responseTimeMs, response, body);
|
|
18915
19418
|
} catch (err) {
|
|
18916
19419
|
return {
|
|
18917
19420
|
healthy: false,
|
|
@@ -18919,6 +19422,8 @@ async function checkOracle(url, timeoutMs) {
|
|
|
18919
19422
|
responseTimeMs: null,
|
|
18920
19423
|
error: err instanceof Error ? err.name === "AbortError" ? `Connection timeout after ${timeoutMs}ms` : err.message : String(err)
|
|
18921
19424
|
};
|
|
19425
|
+
} finally {
|
|
19426
|
+
clearTimeout(timer);
|
|
18922
19427
|
}
|
|
18923
19428
|
}
|
|
18924
19429
|
async function runCustomCheck(name, checkFn, timeoutMs) {
|
|
@@ -19325,7 +19830,6 @@ export {
|
|
|
19325
19830
|
getCoinIdByName,
|
|
19326
19831
|
getCoinIdBySymbol,
|
|
19327
19832
|
getPublicKey,
|
|
19328
|
-
getSphere,
|
|
19329
19833
|
getTokenDecimals,
|
|
19330
19834
|
getTokenDefinition,
|
|
19331
19835
|
getTokenIconUrl,
|