@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.
Files changed (50) hide show
  1. package/README.md +15 -5
  2. package/dist/connect/index.cjs +14 -23
  3. package/dist/connect/index.cjs.map +1 -1
  4. package/dist/connect/index.d.cts +4 -0
  5. package/dist/connect/index.d.ts +4 -0
  6. package/dist/connect/index.js +14 -23
  7. package/dist/connect/index.js.map +1 -1
  8. package/dist/core/index.cjs +726 -225
  9. package/dist/core/index.cjs.map +1 -1
  10. package/dist/core/index.d.cts +172 -21
  11. package/dist/core/index.d.ts +172 -21
  12. package/dist/core/index.js +726 -224
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/impl/browser/connect/index.cjs +14 -23
  15. package/dist/impl/browser/connect/index.cjs.map +1 -1
  16. package/dist/impl/browser/connect/index.js +14 -23
  17. package/dist/impl/browser/connect/index.js.map +1 -1
  18. package/dist/impl/browser/index.cjs +331 -504
  19. package/dist/impl/browser/index.cjs.map +1 -1
  20. package/dist/impl/browser/index.js +331 -504
  21. package/dist/impl/browser/index.js.map +1 -1
  22. package/dist/impl/nodejs/connect/index.cjs +13 -22
  23. package/dist/impl/nodejs/connect/index.cjs.map +1 -1
  24. package/dist/impl/nodejs/connect/index.js +13 -22
  25. package/dist/impl/nodejs/connect/index.js.map +1 -1
  26. package/dist/impl/nodejs/index.cjs +298 -569
  27. package/dist/impl/nodejs/index.cjs.map +1 -1
  28. package/dist/impl/nodejs/index.d.cts +90 -13
  29. package/dist/impl/nodejs/index.d.ts +90 -13
  30. package/dist/impl/nodejs/index.js +298 -569
  31. package/dist/impl/nodejs/index.js.map +1 -1
  32. package/dist/impl/shared/wallet-api/index.d.cts +54 -2
  33. package/dist/impl/shared/wallet-api/index.d.ts +54 -2
  34. package/dist/index.cjs +728 -225
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +227 -108
  37. package/dist/index.d.ts +227 -108
  38. package/dist/index.js +728 -224
  39. package/dist/index.js.map +1 -1
  40. package/dist/modules/payments-v2/index.cjs +11 -7
  41. package/dist/modules/payments-v2/index.cjs.map +1 -1
  42. package/dist/modules/payments-v2/index.d.cts +56 -3
  43. package/dist/modules/payments-v2/index.d.ts +56 -3
  44. package/dist/modules/payments-v2/index.js +11 -7
  45. package/dist/modules/payments-v2/index.js.map +1 -1
  46. package/dist/token-engine/index.cjs +46 -0
  47. package/dist/token-engine/index.cjs.map +1 -1
  48. package/dist/token-engine/index.js +46 -0
  49. package/dist/token-engine/index.js.map +1 -1
  50. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -861,6 +861,25 @@ interface SphereStatus {
861
861
  * All operations are async for platform flexibility
862
862
  */
863
863
  interface StorageProvider extends BaseProvider {
864
+ /**
865
+ * Stable identity of the BACKING STORE this provider addresses — not of this
866
+ * object, and not of the class (`id` is a class constant like `'file-storage'`,
867
+ * which is exactly the wrong granularity).
868
+ *
869
+ * Two providers that return the SAME value address the same data, so erasing
870
+ * through one erases through the other: `Sphere.clear({ storage })` tears down
871
+ * the live Spheres of every provider sharing this value, not merely those built
872
+ * on this object. Compose it from everything that selects the store (file path,
873
+ * database name, key prefix) behind a scheme prefix, so two kinds of store can
874
+ * never collide on one string.
875
+ *
876
+ * It must not change over the provider's lifetime — it is read again on teardown,
877
+ * and a value that moved would strand the entry it was registered under.
878
+ *
879
+ * Optional: omit it and liveness falls back to per-object identity, i.e. a
880
+ * second provider over the same data is treated as unrelated.
881
+ */
882
+ readonly backingStoreId?: string;
864
883
  /**
865
884
  * Set identity for scoped storage
866
885
  */
@@ -890,11 +909,44 @@ interface StorageProvider extends BaseProvider {
890
909
  */
891
910
  clear(prefix?: string): Promise<void>;
892
911
  /**
893
- * Save tracked addresses (only user state: index, hidden, timestamps)
912
+ * Save tracked addresses (only user state: index, hidden, timestamps).
913
+ *
914
+ * MUST MERGE, NEVER REPLACE (#766 item 5). `entries` is ONE writer's snapshot,
915
+ * not the whole truth: every Sphere sharing this storage keeps its own copy of
916
+ * the registry and persists all of it, so writing the argument verbatim is a
917
+ * lost update — A activates index 1, B (whose snapshot predates that) activates
918
+ * index 2, and B's write erases index 1 while A still reports it. This happens
919
+ * on a single network with a single provider; do NOT "fix" it by renaming or
920
+ * network-scoping the key.
921
+ *
922
+ * The contract, implemented by `storage/tracked-addresses.ts` — reuse those
923
+ * helpers rather than re-deriving this:
924
+ * - read the stored registry, union it with `entries` BY `index`;
925
+ * - on a conflicting index, the entry with the greater `updatedAt` supplies
926
+ * `hidden`, and `createdAt` keeps the earlier value;
927
+ * - serialize concurrent calls on the provider instance, so one call's read
928
+ * cannot interleave with another's write;
929
+ * - a failed write must not brick later writes, and must still reject to its
930
+ * own caller.
931
+ *
932
+ * An `index` must be a UINT32 — a BIP32 child number. `deriveKeyAtPath` parseInt()s
933
+ * that path segment, so `1.5` derives index 1's keys and the row aliases a real
934
+ * address. The ceiling matters too: `deriveChildKey` pads the child number to 8 hex
935
+ * digits, so anything above `0xffffffff` emits extra bytes and derives off-standard.
936
+ * An `entries` row that is not one must REJECT the whole call (`mergeTrackedAddresses`
937
+ * throws `VALIDATION_ERROR`); dropping it silently on a write reports a save that
938
+ * never happened. Already-stored rows are dropped on READ instead, so one bad row
939
+ * cannot brick every later write. Validate before opening the write transaction if
940
+ * your platform would otherwise replace the reason with a generic abort.
941
+ *
942
+ * A union is safe because there is no delete path: entries are only ever added,
943
+ * and wiping the wallet removes the key itself (`Sphere.clear()`). Adding a
944
+ * per-entry delete would require revisiting this contract.
894
945
  */
895
946
  saveTrackedAddresses(entries: TrackedAddressEntry[]): Promise<void>;
896
947
  /**
897
- * Load tracked addresses
948
+ * Load tracked addresses. Tolerant: unusable/corrupt storage reads as `[]`
949
+ * (see `parseTrackedAddresses` in `storage/tracked-addresses.ts`).
898
950
  */
899
951
  loadTrackedAddresses(): Promise<TrackedAddressEntry[]>;
900
952
  }
@@ -2002,8 +2054,12 @@ declare function getAddressStorageKey(addressId: string, key: string): string;
2002
2054
  * @returns Sanitized identifier like "DIRECT_abc123_xyz789"
2003
2055
  */
2004
2056
  declare function getAddressId(directAddress: string): string;
2005
- /** Default Nostr relays */
2006
- declare const DEFAULT_NOSTR_RELAYS: readonly ["wss://relay.unicity.network", "wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"];
2057
+ /**
2058
+ * Fallback Nostr relays, used only when no relay list is configured
2059
+ * (NostrTransportProvider / MultiAddressTransportMux). `relay.unicity.network`
2060
+ * was the v1 relay and no longer resolves, so it is not listed here.
2061
+ */
2062
+ declare const DEFAULT_NOSTR_RELAYS: readonly ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"];
2007
2063
  /** Nostr event kinds used by SDK - must match @unicitylabs/nostr-js-sdk */
2008
2064
  declare const NOSTR_EVENT_KINDS: {
2009
2065
  /** NIP-04 encrypted direct message */
@@ -2051,7 +2107,12 @@ declare const NIP29_KINDS: {
2051
2107
  /** Relay-signed group roles */
2052
2108
  readonly GROUP_ROLES: 39003;
2053
2109
  };
2054
- /** Default aggregator request timeout (ms) */
2110
+ /**
2111
+ * Default aggregator request timeout (ms)
2112
+ * Note: The aggregator is conceptually an oracle - a trusted service that provides
2113
+ * verifiable truth about token state through cryptographic inclusion proofs.
2114
+ * There is no default aggregator URL: every live network names its own gateway.
2115
+ */
2055
2116
  declare const DEFAULT_AGGREGATOR_TIMEOUT = 30000;
2056
2117
  /** Default BIP32 derivation path (full path with chain/index) */
2057
2118
  declare const DEFAULT_DERIVATION_PATH: "m/44'/0'/0'/0/0";
@@ -2068,10 +2129,11 @@ declare const DEFAULT_GROUP_RELAYS: readonly ["wss://sphere-relay.unicity.networ
2068
2129
  declare const NETWORKS: {
2069
2130
  readonly mainnet: {
2070
2131
  readonly name: "Mainnet";
2071
- readonly aggregatorUrl: "https://aggregator.unicity.network/rpc";
2072
- readonly nostrRelays: readonly ["wss://relay.unicity.network", "wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"];
2132
+ readonly networkId: 1;
2133
+ readonly aggregatorUrl: "https://gateway.mainnet.unicity.network";
2134
+ readonly nostrRelays: readonly ["wss://nostr-relay.testnet.unicity.network"];
2073
2135
  readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
2074
- readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
2136
+ readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json";
2075
2137
  };
2076
2138
  readonly testnet: {
2077
2139
  readonly name: "Testnet2";
@@ -2089,13 +2151,6 @@ declare const NETWORKS: {
2089
2151
  readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
2090
2152
  readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json";
2091
2153
  };
2092
- readonly dev: {
2093
- readonly name: "Development";
2094
- readonly aggregatorUrl: "https://dev-aggregator.dyndns.org/rpc";
2095
- readonly nostrRelays: readonly ["wss://nostr-relay.testnet.unicity.network"];
2096
- readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
2097
- readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
2098
- };
2099
2154
  };
2100
2155
  type NetworkType = keyof typeof NETWORKS;
2101
2156
  /** Default timeouts (ms) */
@@ -2964,9 +3019,26 @@ interface SphereInitResult {
2964
3019
  generatedMnemonic?: string;
2965
3020
  }
2966
3021
  declare class Sphere {
2967
- private static instance;
3022
+ private static readonly _liveByStorage;
3023
+ /** Fallback keys for providers that declare no `backingStoreId` — one per object. */
3024
+ private static readonly _objectStoreKeys;
3025
+ /**
3026
+ * How many times each backing store has been cleared. An init is invisible to `clear()`
3027
+ * until it PUBLISHES (#767), so a clear cannot destroy one in flight and would wipe the
3028
+ * store under it. Every init records this number before its first storage work and
3029
+ * publication refuses if it moved (#772). Only cleared stores get an entry, so the
3030
+ * strongly-held keys are bounded by the stores a process actually clears.
3031
+ */
3032
+ private static readonly _clearGenerations;
2968
3033
  private static _orphanCacheCleaned;
2969
3034
  private _initialized;
3035
+ /**
3036
+ * Destroyed latch (#770). Distinct from `_initialized`, which destroy() clears LAST — after
3037
+ * every teardown step — so it cannot mark "teardown has begun". This is set as destroy()'s
3038
+ * very FIRST statement, so the WHOLE teardown window is guarded, not just the instant after
3039
+ * it. Read by ensureAlive() and by the §7 lifecycle mutex.
3040
+ */
3041
+ private _destroyed;
2970
3042
  private _trackedAddressesLoaded;
2971
3043
  private _identity;
2972
3044
  private _masterKey;
@@ -3019,6 +3091,8 @@ declare class Sphere {
3019
3091
  * the rebuild after an api-key change share one pool configuration.
3020
3092
  */
3021
3093
  private _verification;
3094
+ /** This Sphere's OWN token registry. Disposed by destroy(); never the process global. */
3095
+ private _registry;
3022
3096
  private eventHandlers;
3023
3097
  private _disabledProviders;
3024
3098
  private _providerEventCleanups;
@@ -3090,6 +3164,23 @@ declare class Sphere {
3090
3164
  * bundle). This method ensures the main bundle's TokenRegistry is configured.
3091
3165
  */
3092
3166
  private static configureTokenRegistry;
3167
+ /**
3168
+ * Build the registry THIS Sphere owns, so its metadata cannot be repointed by another
3169
+ * Sphere's init. The global is still configured above for consumers that read it
3170
+ * directly; this instance is what the payments facade presents from.
3171
+ */
3172
+ /**
3173
+ * Own a registry for the duration of `bringUp`, disposing it if any of that rejects.
3174
+ *
3175
+ * `bringUp` must cover EVERY fallible step from here until the Sphere is returned to
3176
+ * its caller — until then nobody holds anything they could destroy, so a
3177
+ * registry left behind is unreachable and its hourly fetch runs for the life of the
3178
+ * process. Guarding a named subset of the steps is what failed twice: the guarded region
3179
+ * and the fallible region were separate things, and drifted. Publication is the LAST
3180
+ * guarded step for the same reason: it can refuse, and a refusal must tear down.
3181
+ */
3182
+ private static withOwnedRegistry;
3183
+ private static createOwnedRegistry;
3093
3184
  /**
3094
3185
  * Create new wallet with mnemonic
3095
3186
  */
@@ -3118,6 +3209,7 @@ declare class Sphere {
3118
3209
  static clear(options: {
3119
3210
  storage: StorageProvider;
3120
3211
  }): Promise<void>;
3212
+ private static clearStore;
3121
3213
  /**
3122
3214
  * Raw `indexedDB.deleteDatabase` sweep of `sphere-token-storage-*` databases
3123
3215
  * left by pre-flip versions (tokens are server-custody now). Browser-only,
@@ -3132,13 +3224,27 @@ declare class Sphere {
3132
3224
  */
3133
3225
  private static cleanupOrphanedVestingCache;
3134
3226
  /**
3135
- * Get current instance
3227
+ * The key a provider registers under: the store it addresses, so every provider
3228
+ * over one dataDir/DB shares an entry. A provider that declares no store keeps a
3229
+ * private key, leaving custom implementations scoped by object identity.
3136
3230
  */
3137
- static getInstance(): Sphere | null;
3231
+ private static storeKeyOf;
3232
+ /** The clears this store has seen. Absent means none — `clear()` creates the entry. */
3233
+ private static clearGenerationOf;
3234
+ private static bumpClearGeneration;
3138
3235
  /**
3139
- * Check if initialized
3236
+ * Make a fully-built Sphere reachable — or refuse, if `clear()` emptied the store
3237
+ * under it while it was building. Check and registration are one SYNCHRONOUS step on
3238
+ * purpose: split by an await, a clear could land between them and wipe a store whose
3239
+ * Sphere is already published. The refusal throws inside `withOwnedRegistry`, whose
3240
+ * teardown then destroys the half-built Sphere the caller never received.
3140
3241
  */
3141
- static isInitialized(): boolean;
3242
+ private static publishLive;
3243
+ /** Record a fully-built Sphere against the storage it owns. See `_liveByStorage`. */
3244
+ private static registerLive;
3245
+ private static unregisterLive;
3246
+ /** Snapshot — callers iterate this while destroy() mutates the underlying Set. */
3247
+ private static liveOn;
3142
3248
  /**
3143
3249
  * Validate mnemonic using BIP39
3144
3250
  */
@@ -3307,7 +3413,8 @@ declare class Sphere {
3307
3413
  /**
3308
3414
  * Import wallet from JSON backup
3309
3415
  *
3310
- * @returns Object with success status and optionally recovered mnemonic
3416
+ * @returns `{ success, sphere?, mnemonic?, error? }`. `sphere` is the instance built
3417
+ * on the SUPPLIED storage — hold it, there is no global to look it up from (#766).
3311
3418
  *
3312
3419
  * @example
3313
3420
  * ```ts
@@ -3324,6 +3431,7 @@ declare class Sphere {
3324
3431
  password?: string;
3325
3432
  }): Promise<{
3326
3433
  success: boolean;
3434
+ sphere?: Sphere;
3327
3435
  mnemonic?: string;
3328
3436
  error?: string;
3329
3437
  }>;
@@ -3423,6 +3531,18 @@ declare class Sphere {
3423
3531
  * independently in background. The payments vertical is NOT created here —
3424
3532
  * the caller starts it via stopThenStartPaymentsV2 (§7 single vertical).
3425
3533
  */
3534
+ /** Tear one address's module set down. Each address owns its own engine, so its own pool. */
3535
+ private static destroyModuleSet;
3536
+ /**
3537
+ * Undo a module set built while `destroy()` was running.
3538
+ *
3539
+ * `destroy()`'s teardown loop has already emptied `_addressModules`, so a set
3540
+ * registered after it would stay live and unreachable — its own engine (and worker
3541
+ * pool), and a transport mux `ensureTransportMux` rebuilt because teardown had just
3542
+ * nulled the field. Then re-raise, so the switch fails rather than reporting success
3543
+ * on a destroyed Sphere (#770, #772 review).
3544
+ */
3545
+ private discardModulesBuiltDuringDestroy;
3426
3546
  private initializeAddressModules;
3427
3547
  /**
3428
3548
  * Ensure the transport multiplexer exists and register an address.
@@ -3431,6 +3551,8 @@ declare class Sphere {
3431
3551
  * @returns AddressTransportAdapter or null if transport is not Nostr-based
3432
3552
  */
3433
3553
  private ensureTransportMux;
3554
+ /** A BIP32 child number: an integer in 0…0xffffffff. Anything else aliases an address. */
3555
+ private static assertDerivableIndex;
3434
3556
  /**
3435
3557
  * Derive address at a specific index
3436
3558
  *
@@ -3669,6 +3791,29 @@ declare class Sphere {
3669
3791
  * Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
3670
3792
  */
3671
3793
  private cleanNametag;
3794
+ /**
3795
+ * Disconnect transport, storage and oracle, attempting each even if an earlier one
3796
+ * rejects. They are separate resources, and one failure must not skip the rest — that
3797
+ * is how a partial teardown leaves connections open, which is exactly the state the
3798
+ * failed-initialization path calls destroy() in.
3799
+ */
3800
+ private disconnectProvidersIndependently;
3801
+ /** Run one teardown step, logging rather than propagating so the next one still runs. */
3802
+ private static safeDisconnect;
3803
+ /**
3804
+ * Tear this Sphere down: stop the vertical, destroy the modules, disconnect the providers
3805
+ * and zero the key material. Idempotent by construction (every step is null-guarded) and
3806
+ * deliberately WITHOUT an early return on re-entry, which would change what a double call
3807
+ * means.
3808
+ *
3809
+ * #770: the FIRST statement flips `_destroyed` — before any await, and before this queues
3810
+ * its own stop on the §7 lifecycle mutex. That position is load-bearing. Every guard reads
3811
+ * the flag, so from that instant any switchToAddress step not yet begun refuses; and
3812
+ * because it flips SYNCHRONOUSLY at entry, a stop/start pair the mutex runs after this call
3813
+ * sees `true` and skips its start. Set it beside `_initialized` at the bottom instead and a
3814
+ * concurrent switch re-arms a wallet whose owner already had destroy() return: fresh
3815
+ * sockets, a fresh wallet-api session, a whole fresh vertical nothing will ever stop.
3816
+ */
3672
3817
  destroy(): Promise<void>;
3673
3818
  private storeMnemonic;
3674
3819
  private storeMasterKey;
@@ -3706,7 +3851,17 @@ declare class Sphere {
3706
3851
  private initializeModules;
3707
3852
  /** §7 mutex: run one stop/start lifecycle op after all queued ones settle. */
3708
3853
  private queuePaymentsV2Op;
3709
- /** Switch/boot: stop whatever runs, then start `index`'s vertical — atomically vs other lifecycle ops. */
3854
+ /**
3855
+ * Switch/boot: stop whatever runs, then start `index`'s vertical — atomically vs other
3856
+ * lifecycle ops.
3857
+ *
3858
+ * #770: the destroyed check between the two halves settles the FACADE vector on its own.
3859
+ * `_destroyed` flips synchronously at destroy() entry, so any closure that BEGINS executing
3860
+ * after destroy() was called sees `true`, and any closure already past the check has
3861
+ * facade.start() in flight — which destroy()'s own queued stop is necessarily ordered
3862
+ * after. The TRANSPORT vector is not on this mutex at all; the ensureAlive() calls in
3863
+ * switchToAddress are what cover it.
3864
+ */
3710
3865
  private stopThenStartPaymentsV2;
3711
3866
  /**
3712
3867
  * Compose + start the payments vertical for ONE address (the §7 rule:
@@ -3717,6 +3872,12 @@ declare class Sphere {
3717
3872
  private startPaymentsV2Inner;
3718
3873
  /** P9 §7: stop the active vertical and await quiescence (in-flight ops settle). */
3719
3874
  private stopPaymentsV2Inner;
3875
+ /**
3876
+ * Refuse once destroy() has STARTED (#770). `_initialized` cannot carry this: destroy()
3877
+ * clears it last, so every teardown step is a window in which a concurrent call still reads
3878
+ * a ready Sphere and re-arms it.
3879
+ */
3880
+ private ensureAlive;
3720
3881
  private ensureReady;
3721
3882
  private emitEvent;
3722
3883
  private encrypt;
@@ -3725,7 +3886,6 @@ declare class Sphere {
3725
3886
  declare const createSphere: typeof Sphere.create;
3726
3887
  declare const loadSphere: typeof Sphere.load;
3727
3888
  declare const initSphere: typeof Sphere.init;
3728
- declare const getSphere: typeof Sphere.getInstance;
3729
3889
  declare const sphereExists: typeof Sphere.exists;
3730
3890
 
3731
3891
  /**
@@ -4241,7 +4401,7 @@ interface TokenDefinition {
4241
4401
  /**
4242
4402
  * Network type for registry lookup
4243
4403
  */
4244
- type RegistryNetwork = 'testnet' | 'mainnet' | 'dev';
4404
+ type RegistryNetwork = 'testnet' | 'testnet2' | 'mainnet';
4245
4405
  /**
4246
4406
  * Configuration options for remote registry refresh
4247
4407
  */
@@ -4275,7 +4435,7 @@ interface TokenRegistryConfig {
4275
4435
  *
4276
4436
  * // Usually called automatically by createBrowserProviders / createNodeProviders
4277
4437
  * TokenRegistry.configure({
4278
- * remoteUrl: 'https://raw.githubusercontent.com/.../unicity-ids.testnet.json',
4438
+ * remoteUrl: 'https://raw.githubusercontent.com/.../unicity-ids.testnet2.json',
4279
4439
  * storage: myStorageProvider,
4280
4440
  * });
4281
4441
  *
@@ -4296,6 +4456,12 @@ declare class TokenRegistry {
4296
4456
  private lastRefreshAt;
4297
4457
  private refreshPromise;
4298
4458
  private initialLoadPromise;
4459
+ /** Bumped on every remoteUrl change; loads started in an older generation are discarded. */
4460
+ private generation;
4461
+ /** Set by dispose(). A disposed registry starts no work and applies no late result. */
4462
+ private disposed;
4463
+ /** Cancels the in-flight fetch (and its abort timer) when the registry is disposed. */
4464
+ private inFlight;
4299
4465
  private constructor();
4300
4466
  /**
4301
4467
  * Get singleton instance of TokenRegistry
@@ -4315,6 +4481,29 @@ declare class TokenRegistry {
4315
4481
  * @param options.autoRefresh - Start auto-refresh immediately (default: true)
4316
4482
  */
4317
4483
  static configure(options: TokenRegistryConfig): void;
4484
+ /**
4485
+ * Create an INDEPENDENT registry, not the process-global singleton.
4486
+ *
4487
+ * One Sphere owns one of these, so two Spheres on different networks cannot wipe each
4488
+ * other's definitions — the singleton's `configure()` reaches into whatever instance
4489
+ * exists and repoints it, which is how a mainnet init silently retargeted a live
4490
+ * testnet2 wallet's decimals. Dispose it when the owner is destroyed.
4491
+ */
4492
+ static create(options: TokenRegistryConfig): TokenRegistry;
4493
+ /** The body of configure(), on the instance, so owned registries share it exactly. */
4494
+ private applyConfig;
4495
+ /**
4496
+ * Stop this registry for good: no timer, no late apply, no late cache write.
4497
+ *
4498
+ * Sphere.destroy() calls this on the registry it owns. Without it a discarded Sphere
4499
+ * leaves an hourly fetch running forever — nothing in this file calls unref(), so in
4500
+ * Node it also keeps the event loop alive.
4501
+ */
4502
+ dispose(): void;
4503
+ /** Whether dispose() has been called. Reads still work; they are simply frozen. */
4504
+ get isDisposed(): boolean;
4505
+ /** Per-instance readiness — the same contract as the static, for an owned registry. */
4506
+ waitForReady(timeoutMs?: number): Promise<boolean>;
4318
4507
  /**
4319
4508
  * Reset the singleton instance (useful for testing).
4320
4509
  * Stops auto-refresh if running.
@@ -4369,6 +4558,14 @@ declare class TokenRegistry {
4369
4558
  * Concurrent calls are deduplicated — only one fetch runs at a time.
4370
4559
  */
4371
4560
  refreshFromRemote(): Promise<boolean>;
4561
+ /**
4562
+ * Fetch with a bounded timeout, exposing a cancel hook to dispose().
4563
+ *
4564
+ * Both the request and its abort timer keep Node's event loop alive for the full
4565
+ * FETCH_TIMEOUT_MS, so a registry disposed mid-flight must cancel them rather than
4566
+ * merely ignore the result.
4567
+ */
4568
+ private fetchCancellable;
4372
4569
  private doRefresh;
4373
4570
  /**
4374
4571
  * Start periodic auto-refresh from the remote URL.
@@ -4460,99 +4657,21 @@ declare class TokenRegistry {
4460
4657
  */
4461
4658
  getCoinIdByName(name: string): string | undefined;
4462
4659
  }
4660
+
4463
4661
  /**
4464
- * Get token definition by coin ID
4465
- * @param coinId - 64-character hex string
4466
- * @returns Token definition or undefined
4662
+ * Readers bound to the PROCESS-GLOBAL registry: with two Spheres alive they answer for
4663
+ * whichever network configured it last. Prefer a Sphere-owned registry when it matters.
4467
4664
  */
4665
+
4468
4666
  declare function getTokenDefinition(coinId: string): TokenDefinition | undefined;
4469
- /**
4470
- * Get token symbol by coin ID
4471
- * @param coinId - 64-character hex string
4472
- * @returns Symbol or truncated ID
4473
- */
4474
4667
  declare function getTokenSymbol(coinId: string): string;
4475
- /**
4476
- * Get token name by coin ID
4477
- * @param coinId - 64-character hex string
4478
- * @returns Name or coin ID
4479
- */
4480
4668
  declare function getTokenName(coinId: string): string;
4481
- /**
4482
- * Get token decimals by coin ID
4483
- * @param coinId - 64-character hex string
4484
- * @returns Decimals or 0
4485
- */
4486
4669
  declare function getTokenDecimals(coinId: string): number;
4487
- /**
4488
- * Get token icon URL by coin ID
4489
- * @param coinId - 64-character hex string
4490
- * @param preferPng - Prefer PNG over SVG
4491
- * @returns Icon URL or null
4492
- */
4493
4670
  declare function getTokenIconUrl(coinId: string, preferPng?: boolean): string | null;
4494
- /**
4495
- * Check if coin ID is in registry
4496
- * @param coinId - 64-character hex string
4497
- * @returns true if known
4498
- */
4499
4671
  declare function isKnownToken(coinId: string): boolean;
4500
- /**
4501
- * Get coin ID by symbol
4502
- * @param symbol - Token symbol (e.g., "UCT")
4503
- * @returns Coin ID or undefined
4504
- */
4505
4672
  declare function getCoinIdBySymbol(symbol: string): string | undefined;
4506
- /**
4507
- * Get coin ID by name
4508
- * @param name - Token name (e.g., "bitcoin")
4509
- * @returns Coin ID or undefined
4510
- */
4511
4673
  declare function getCoinIdByName(name: string): string | undefined;
4512
- /**
4513
- * Normalize a coin identifier to its canonical hash coinId.
4514
- *
4515
- * Accepts both symbolic names ("BTC", "ETH") and hash coinIds (64-char hex).
4516
- * If the input is a short symbol, resolves it via the TokenRegistry.
4517
- * Returns the input unchanged if it's already a hash coinId or unknown.
4518
- *
4519
- * @public
4520
- *
4521
- * @remarks
4522
- * Heuristic: inputs matching `length <= 20 && /^[A-Za-z0-9]+$/` are treated
4523
- * as symbolic names and looked up in the registry. Long inputs, hyphenated
4524
- * inputs ("TOKEN-123"), or dotted inputs ("BTC.CASH") pass through unchanged.
4525
- *
4526
- * **Stability:** This function depends on `TokenRegistry` content. Adding a
4527
- * new symbol that shadows a previously-unknown short alphanumeric coinId
4528
- * changes normalization semantics retroactively. Consumers building stable
4529
- * keys against this output should be aware that registry growth is a
4530
- * non-breaking change *for unknown inputs* but a breaking change *for inputs
4531
- * that newly resolve*.
4532
- *
4533
- * @param coinId - A symbolic name or hash coinId.
4534
- * @returns The canonical hash coinId, or the original string if not resolvable.
4535
- */
4536
4674
  declare function normalizeCoinId(coinId: string): string;
4537
- /**
4538
- * Compare two coin identifiers for equality, normalizing both sides.
4539
- *
4540
- * Handles mixed-format comparisons: "BTC" vs hash coinId, or two hash coinIds.
4541
- * Both values are normalized to hash coinIds via the TokenRegistry before comparison.
4542
- *
4543
- * @public
4544
- *
4545
- * @remarks
4546
- * Reflexive byte-equality short-circuit comes first; otherwise both sides
4547
- * are normalized via {@link normalizeCoinId} and compared. Inherits the
4548
- * registry-dependence caveat from `normalizeCoinId`. Two distinct symbols
4549
- * that both fail to resolve will compare unequal even if they're semantic
4550
- * aliases — register them as aliases in `TokenRegistry` for matching.
4551
- *
4552
- * @param a - First coin identifier (symbol or hash coinId).
4553
- * @param b - Second coin identifier (symbol or hash coinId).
4554
- * @returns true if both resolve to the same canonical coinId.
4555
- */
4556
4675
  declare function coinIdsMatch(a: string, b: string): boolean;
4557
4676
 
4558
4677
  /**
@@ -4618,4 +4737,4 @@ declare function normalizeAddress(address: string): string;
4618
4737
  */
4619
4738
  declare function addressesMatch(a: string, b: string): boolean;
4620
4739
 
4621
- export { AUTH_CHALLENGE_PREFIX, type AddressInfo, type AddressType, type AggregatorEvent, type AggregatorEventCallback, type AggregatorEventType, type AggregatorProvider, type Asset, type BaseProvider, type BroadcastHandler, type BroadcastMessage, COIN_TYPES, ChallengeTemplateError, type CheckNetworkHealthOptions, CoinGeckoPriceProvider, CommunicationsModule, type CommunicationsModuleConfig, type CommunicationsModuleDependencies, type ComposingIndicator, type ConversationPage, type CreateGroupOptions, DEFAULT_AGGREGATOR_TIMEOUT, DEFAULT_DERIVATION_PATH, DEFAULT_GROUP_RELAYS, DEFAULT_MARKET_API_URL, DEFAULT_NOSTR_RELAYS, type DecryptionProgressCallback, type DerivationMode, type DirectMessage, type DiscoverAddressProgress, type DiscoverAddressesOptions, type DiscoverAddressesResult, type DiscoveredAddress, type EncryptedData, FIELD_ENCRYPTION_HKDF_INFO, FIELD_ENVELOPE_MAX_BYTES, FIELD_ENVELOPE_NONCE_BYTES, FIELD_ENVELOPE_PREFIX, type FullIdentity, type GetConversationPageOptions, GroupChatModule, type GroupChatModuleConfig, type GroupChatModuleDependencies, type GroupData, type GroupMemberData, type GroupMessageData, GroupRole, GroupVisibility, type HealthCheckFn, type HistoryRecord, type Identity, type IdentityConfig, type IncomingBroadcast, type IncomingMessage, type IncomingTransfer, type InitProgress, type InitProgressCallback, type InitProgressStep, type IntentStatus, type IntentType, LIMITS, type LegacyFileParseResult, type LegacyFileParsedData, type LegacyFileType, type LogHandler, type LogLevel, type LoggerConfig, type MarketIntent, MarketModule, type MarketModuleConfig, type MarketModuleDependencies, type MessageHandler, NETWORKS, NIP29_KINDS, NOSTR_EVENT_KINDS, type NetworkHealthResult, type NetworkType, type OracleEvent, type OracleEventCallback, type OracleEventType, type OracleProvider, type ParsedAddress, PartialSendConflictError, type PeerInfo, type PostIntentRequest, type PostIntentResult, type PricePlatform, type PriceProvider, type PriceProviderConfig, type ProviderMetadata, type ProviderRole, type ProviderStatus, type ProviderStatusInfo, type RegistryNetwork, SIGN_MESSAGE_PREFIX, STORAGE_KEYS_ADDRESS, STORAGE_KEYS_GLOBAL, STORAGE_PREFIX, type SearchFilters, type SearchIntentResult, type SearchOptions, type SearchResult, type ServiceHealthResult, Sphere, type SphereCreateOptions, SphereError, type SphereErrorCode, type SphereEventHandler, type SphereEventMap, type SphereEventType, type SphereImportOptions, type SphereInitOptions, type SphereInitResult, type SphereLoadOptions, type SphereStatus, type StorageProvider, TEST_NOSTR_RELAYS, TIMEOUTS, type Token, type TokenDefinition, type TokenIcon, type TokenPrice, TokenRegistry, type TokenStatus, type TokenTransferDetail, type TrackedAddress, type TrackedAddressEntry, type HistoryRecord as TransactionHistoryEntry, type TransferRequest, type TransferResult, type TransferStatus, type TransportEvent, type TransportEventCallback, type TransportEventType, type TransportProvider, type WalletApiTransportConfig, type WalletInfo, type WalletJSON, type WalletJSONExportOptions, type WalletSource, addressesMatch, assertFieldEnvelopeShape, base58Decode, base58Encode, bytesToHex, checkNetworkHealth, coinIdsMatch, createCommunicationsModule, createGroupChatModule, createKeyPair, createMarketModule, createPriceProvider, createSphere, decrypt, decryptField, decryptFieldBytes, decryptJson, decryptMnemonic, decryptSimple, decryptTextFormatKey, decryptWithSalt, deriveAddressInfo, deriveChildKey, deriveFieldEncryptionKey, deriveKeyAtPath, doubleSha256, encrypt, encryptField, encryptFieldBytes, encryptMnemonic, encryptSimple, extractFromText, findPattern, formatAmount, generateAddressFromMasterKey, generateMasterKey, generateMnemonic, getAddressId, getAddressStorageKey, getCoinIdByName, getCoinIdBySymbol, getPublicKey, getSphere, getTokenDecimals, getTokenDefinition, getTokenIconUrl, getTokenName, getTokenSymbol, hash160, hashSignMessage, hexToBytes, identityFromMnemonicSync, initSphere, isKnownToken, isPossiblyCommittedSendOutcome, isSphereError, isTextWalletEncrypted, isValidAddress, isValidDirectAddress, isValidNametag, isValidPrivateKey, isWalletTextFormat, loadSphere, logger, mnemonicToSeedSync, normalizeAddress, normalizeCoinId, parseAddress, parseAndDecryptWalletText, parseTokenAmount, parseWalletText, randomBytes, randomHex, randomUUID, recoverPubkeyFromSignature, ripemd160, safeParseTokenAmount, sha256, signMessage, sleep, sphereExists, toHumanReadable, validateMnemonic, verifyChallengeTemplate, verifySignedMessage };
4740
+ export { AUTH_CHALLENGE_PREFIX, type AddressInfo, type AddressType, type AggregatorEvent, type AggregatorEventCallback, type AggregatorEventType, type AggregatorProvider, type Asset, type BaseProvider, type BroadcastHandler, type BroadcastMessage, COIN_TYPES, ChallengeTemplateError, type CheckNetworkHealthOptions, CoinGeckoPriceProvider, CommunicationsModule, type CommunicationsModuleConfig, type CommunicationsModuleDependencies, type ComposingIndicator, type ConversationPage, type CreateGroupOptions, DEFAULT_AGGREGATOR_TIMEOUT, DEFAULT_DERIVATION_PATH, DEFAULT_GROUP_RELAYS, DEFAULT_MARKET_API_URL, DEFAULT_NOSTR_RELAYS, type DecryptionProgressCallback, type DerivationMode, type DirectMessage, type DiscoverAddressProgress, type DiscoverAddressesOptions, type DiscoverAddressesResult, type DiscoveredAddress, type EncryptedData, FIELD_ENCRYPTION_HKDF_INFO, FIELD_ENVELOPE_MAX_BYTES, FIELD_ENVELOPE_NONCE_BYTES, FIELD_ENVELOPE_PREFIX, type FullIdentity, type GetConversationPageOptions, GroupChatModule, type GroupChatModuleConfig, type GroupChatModuleDependencies, type GroupData, type GroupMemberData, type GroupMessageData, GroupRole, GroupVisibility, type HealthCheckFn, type HistoryRecord, type Identity, type IdentityConfig, type IncomingBroadcast, type IncomingMessage, type IncomingTransfer, type InitProgress, type InitProgressCallback, type InitProgressStep, type IntentStatus, type IntentType, LIMITS, type LegacyFileParseResult, type LegacyFileParsedData, type LegacyFileType, type LogHandler, type LogLevel, type LoggerConfig, type MarketIntent, MarketModule, type MarketModuleConfig, type MarketModuleDependencies, type MessageHandler, NETWORKS, NIP29_KINDS, NOSTR_EVENT_KINDS, type NetworkHealthResult, type NetworkType, type OracleEvent, type OracleEventCallback, type OracleEventType, type OracleProvider, type ParsedAddress, PartialSendConflictError, type PeerInfo, type PostIntentRequest, type PostIntentResult, type PricePlatform, type PriceProvider, type PriceProviderConfig, type ProviderMetadata, type ProviderRole, type ProviderStatus, type ProviderStatusInfo, type RegistryNetwork, SIGN_MESSAGE_PREFIX, STORAGE_KEYS_ADDRESS, STORAGE_KEYS_GLOBAL, STORAGE_PREFIX, type SearchFilters, type SearchIntentResult, type SearchOptions, type SearchResult, type ServiceHealthResult, Sphere, type SphereCreateOptions, SphereError, type SphereErrorCode, type SphereEventHandler, type SphereEventMap, type SphereEventType, type SphereImportOptions, type SphereInitOptions, type SphereInitResult, type SphereLoadOptions, type SphereStatus, type StorageProvider, TEST_NOSTR_RELAYS, TIMEOUTS, type Token, type TokenDefinition, type TokenIcon, type TokenPrice, TokenRegistry, type TokenStatus, type TokenTransferDetail, type TrackedAddress, type TrackedAddressEntry, type HistoryRecord as TransactionHistoryEntry, type TransferRequest, type TransferResult, type TransferStatus, type TransportEvent, type TransportEventCallback, type TransportEventType, type TransportProvider, type WalletApiTransportConfig, type WalletInfo, type WalletJSON, type WalletJSONExportOptions, type WalletSource, addressesMatch, assertFieldEnvelopeShape, base58Decode, base58Encode, bytesToHex, checkNetworkHealth, coinIdsMatch, createCommunicationsModule, createGroupChatModule, createKeyPair, createMarketModule, createPriceProvider, createSphere, decrypt, decryptField, decryptFieldBytes, decryptJson, decryptMnemonic, decryptSimple, decryptTextFormatKey, decryptWithSalt, deriveAddressInfo, deriveChildKey, deriveFieldEncryptionKey, deriveKeyAtPath, doubleSha256, encrypt, encryptField, encryptFieldBytes, encryptMnemonic, encryptSimple, extractFromText, findPattern, formatAmount, generateAddressFromMasterKey, generateMasterKey, generateMnemonic, getAddressId, getAddressStorageKey, getCoinIdByName, getCoinIdBySymbol, getPublicKey, getTokenDecimals, getTokenDefinition, getTokenIconUrl, getTokenName, getTokenSymbol, hash160, hashSignMessage, hexToBytes, identityFromMnemonicSync, initSphere, isKnownToken, isPossiblyCommittedSendOutcome, isSphereError, isTextWalletEncrypted, isValidAddress, isValidDirectAddress, isValidNametag, isValidPrivateKey, isWalletTextFormat, loadSphere, logger, mnemonicToSeedSync, normalizeAddress, normalizeCoinId, parseAddress, parseAndDecryptWalletText, parseTokenAmount, parseWalletText, randomBytes, randomHex, randomUUID, recoverPubkeyFromSignature, ripemd160, safeParseTokenAmount, sha256, signMessage, sleep, sphereExists, toHumanReadable, validateMnemonic, verifyChallengeTemplate, verifySignedMessage };