@kheopskit/core 5.2.1 → 6.0.0-rc.0

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 (83) hide show
  1. package/CHANGELOG.md +426 -0
  2. package/MIGRATING_TO_V6.md +837 -0
  3. package/README.md +628 -11
  4. package/dist/ethereum.d.mts +12 -6
  5. package/dist/ethereum.d.ts +12 -6
  6. package/dist/ethereum.js +13 -10
  7. package/dist/ethereum.js.map +1 -1
  8. package/dist/ethereum.mjs +7 -4
  9. package/dist/ethereum.mjs.map +1 -1
  10. package/dist/{iconCache-pP2ES4pJ.mjs → iconCache-C-U3IEbP.mjs} +21 -40
  11. package/dist/iconCache-C-U3IEbP.mjs.map +1 -0
  12. package/dist/{iconCache-DUtkJlaX.js → iconCache-FR4iy6go.js} +21 -40
  13. package/dist/iconCache-FR4iy6go.js.map +1 -0
  14. package/dist/index.d.mts +17 -63
  15. package/dist/index.d.ts +17 -63
  16. package/dist/index.js +387 -373
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +384 -369
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/injectedWallets-48L-AL1I.mjs +173 -0
  21. package/dist/injectedWallets-48L-AL1I.mjs.map +1 -0
  22. package/dist/injectedWallets-Dy8YGKL-.js +178 -0
  23. package/dist/injectedWallets-Dy8YGKL-.js.map +1 -0
  24. package/dist/internal.d.mts +3 -27
  25. package/dist/internal.d.ts +3 -27
  26. package/dist/internal.js +2 -2
  27. package/dist/internal.mjs +2 -2
  28. package/dist/pageLoad-BZ7o8p5R.mjs +24 -0
  29. package/dist/pageLoad-BZ7o8p5R.mjs.map +1 -0
  30. package/dist/pageLoad-D1wLeuFG.js +28 -0
  31. package/dist/pageLoad-D1wLeuFG.js.map +1 -0
  32. package/dist/platformAccounts-BjIeRcFN.js +321 -0
  33. package/dist/platformAccounts-BjIeRcFN.js.map +1 -0
  34. package/dist/platformAccounts-yyRUrnel.mjs +256 -0
  35. package/dist/platformAccounts-yyRUrnel.mjs.map +1 -0
  36. package/dist/polkadot.d.mts +127 -11
  37. package/dist/polkadot.d.ts +127 -11
  38. package/dist/polkadot.js +171 -107
  39. package/dist/polkadot.js.map +1 -1
  40. package/dist/polkadot.mjs +164 -100
  41. package/dist/polkadot.mjs.map +1 -1
  42. package/dist/solana.d.mts +11 -5
  43. package/dist/solana.d.ts +11 -5
  44. package/dist/solana.js +18 -15
  45. package/dist/solana.js.map +1 -1
  46. package/dist/solana.mjs +7 -4
  47. package/dist/solana.mjs.map +1 -1
  48. package/dist/storage-D3AwNgqc.d.mts +27 -0
  49. package/dist/storage-D3AwNgqc.d.ts +27 -0
  50. package/dist/testing.d.mts +71 -0
  51. package/dist/testing.d.ts +71 -0
  52. package/dist/testing.js +136 -0
  53. package/dist/testing.js.map +1 -0
  54. package/dist/testing.mjs +131 -0
  55. package/dist/testing.mjs.map +1 -0
  56. package/dist/{types-DKU-SNio.js → types-BWHvNFlN.js} +149 -2
  57. package/dist/types-BWHvNFlN.js.map +1 -0
  58. package/dist/types-Dkyt9bf4.d.mts +683 -0
  59. package/dist/types-Dkyt9bf4.d.ts +683 -0
  60. package/dist/types-Yor672md.mjs +215 -0
  61. package/dist/types-Yor672md.mjs.map +1 -0
  62. package/dist/walletconnect.d.mts +41 -0
  63. package/dist/walletconnect.d.ts +41 -0
  64. package/dist/walletconnect.js +219 -0
  65. package/dist/walletconnect.js.map +1 -0
  66. package/dist/walletconnect.mjs +216 -0
  67. package/dist/walletconnect.mjs.map +1 -0
  68. package/package.json +33 -9
  69. package/dist/errors-BLFP1dQI.mjs +0 -181
  70. package/dist/errors-BLFP1dQI.mjs.map +0 -1
  71. package/dist/errors-DYWVPHrR.js +0 -222
  72. package/dist/errors-DYWVPHrR.js.map +0 -1
  73. package/dist/iconCache-DUtkJlaX.js.map +0 -1
  74. package/dist/iconCache-pP2ES4pJ.mjs.map +0 -1
  75. package/dist/injectedWallets-BqR-_aAy.js +0 -148
  76. package/dist/injectedWallets-BqR-_aAy.js.map +0 -1
  77. package/dist/injectedWallets-Uz4hPuoW.mjs +0 -119
  78. package/dist/injectedWallets-Uz4hPuoW.mjs.map +0 -1
  79. package/dist/types-Cb-vuEru.d.mts +0 -330
  80. package/dist/types-Cb-vuEru.d.ts +0 -330
  81. package/dist/types-DKU-SNio.js.map +0 -1
  82. package/dist/types-jnifAAA0.mjs +0 -104
  83. package/dist/types-jnifAAA0.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,171 +1,9 @@
1
- import { a as isValidWalletId, c as clearAllCachedObservables, i as getWalletId, n as isWalletConnectWallet, o as parseWalletId, r as WALLET_CONNECT_WALLET_ID, s as isWalletPlatform, t as isInjectedWallet, u as clearCachedObservablesByPrefix } from "./types-jnifAAA0.mjs";
2
- import { i as isValidAddress, n as getWalletAccountId, r as parseWalletAccountId, t as KheopskitError } from "./errors-BLFP1dQI.mjs";
3
- import { a as safeLocalStorage, c as serializeAccount, l as serializeWallet, n as setCachedIcons, o as hydrateAccount, r as cookieStorage, s as hydrateWallet, t as getCachedIcon } from "./iconCache-pP2ES4pJ.mjs";
4
- import { BehaviorSubject, Observable, Subscription, combineLatest, debounceTime, distinctUntilChanged, filter, from, map, mergeMap, of, shareReplay, startWith, switchMap, take, tap, throttleTime, timer } from "rxjs";
1
+ import { c as walletPendingError, d as isValidWalletId, f as parseWalletId, i as abortedError, l as WALLET_CONNECT_WALLET_ID, m as clearAllCachedObservables, n as isWalletConnectWallet, p as isWalletPlatform, r as KheopskitError, t as isInjectedWallet, u as getWalletId } from "./types-Yor672md.mjs";
2
+ import { c as isValidAddress, n as framesOf, o as getWalletAccountId, s as parseWalletAccountId } from "./platformAccounts-yyRUrnel.mjs";
3
+ import { a as safeLocalStorage, c as serializeAccount, l as serializeWallet, n as setCachedIcons, o as hydrateAccount, r as cookieStorage, s as hydrateWallet, t as getCachedIcon } from "./iconCache-C-U3IEbP.mjs";
4
+ import { t as pageLoaded$ } from "./pageLoad-BZ7o8p5R.mjs";
5
+ import { BehaviorSubject, Observable, Subscription, asapScheduler, combineLatest, debounceTime, distinctUntilChanged, filter, map, mergeMap, of, shareReplay, startWith, switchMap, take, tap, timer } from "rxjs";
5
6
  import { uniq } from "lodash-es";
6
- //#region src/api/appKit.ts
7
- /**
8
- * Dynamically import @reown/appkit/core to avoid loading browser-only code
9
- * during SSR or in edge runtimes like Cloudflare Workers.
10
- * The AppKit library uses Lit for web components which requires document.
11
- */
12
- const loadAppKit = async () => {
13
- try {
14
- const { createAppKit } = await import("@reown/appkit/core");
15
- return createAppKit;
16
- } catch (cause) {
17
- console.error("[kheopskit] WalletConnect is configured but @reown/appkit could not be loaded. Install it with `pnpm add @reown/appkit` (or remove config.walletConnect). WalletConnect wallets are disabled; injected wallets still work.", cause);
18
- return null;
19
- }
20
- };
21
- const WALLET_CONNECT_ICON = "data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjQwMCIgdmlld0JveD0iMCAwIDQwMCA0MDAiIHdpZHRoPSI0MDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxjbGlwUGF0aCBpZD0iYSI+PHBhdGggZD0ibTAgMGg0MDB2NDAwaC00MDB6Ii8+PC9jbGlwUGF0aD48ZyBjbGlwLXBhdGg9InVybCgjYSkiPjxjaXJjbGUgY3g9IjIwMCIgY3k9IjIwMCIgZmlsbD0iIzMzOTZmZiIgcj0iMTk5LjUiIHN0cm9rZT0iIzY2YjFmZiIvPjxwYXRoIGQ9Im0xMjIuNTE5IDE0OC45NjVjNDIuNzkxLTQxLjcyOSAxMTIuMTcxLTQxLjcyOSAxNTQuOTYyIDBsNS4xNSA1LjAyMmMyLjE0IDIuMDg2IDIuMTQgNS40NjkgMCA3LjU1NWwtMTcuNjE3IDE3LjE4Yy0xLjA3IDEuMDQzLTIuODA0IDEuMDQzLTMuODc0IDBsLTcuMDg3LTYuOTExYy0yOS44NTMtMjkuMTExLTc4LjI1My0yOS4xMTEtMTA4LjEwNiAwbC03LjU5IDcuNDAxYy0xLjA3IDEuMDQzLTIuODA0IDEuMDQzLTMuODc0IDBsLTE3LjYxNy0xNy4xOGMtMi4xNC0yLjA4Ni0yLjE0LTUuNDY5IDAtNy41NTV6bTE5MS4zOTcgMzUuNTI5IDE1LjY3OSAxNS4yOWMyLjE0IDIuMDg2IDIuMTQgNS40NjkgMCA3LjU1NWwtNzAuNyA2OC45NDRjLTIuMTM5IDIuMDg3LTUuNjA4IDIuMDg3LTcuNzQ4IDBsLTUwLjE3OC00OC45MzFjLS41MzUtLjUyMi0xLjQwMi0uNTIyLTEuOTM3IDBsLTUwLjE3OCA0OC45MzFjLTIuMTM5IDIuMDg3LTUuNjA4IDIuMDg3LTcuNzQ4IDBsLTcwLjcwMTUtNjguOTQ1Yy0yLjEzOTYtMi4wODYtMi4xMzk2LTUuNDY5IDAtNy41NTVsMTUuNjc5NS0xNS4yOWMyLjEzOTYtMi4wODYgNS42MDg1LTIuMDg2IDcuNzQ4MSAwbDUwLjE3ODkgNDguOTMyYy41MzUuNTIyIDEuNDAyLjUyMiAxLjkzNyAwbDUwLjE3Ny00OC45MzJjMi4xMzktMi4wODcgNS42MDgtMi4wODcgNy43NDggMGw1MC4xNzkgNDguOTMyYy41MzUuNTIyIDEuNDAyLjUyMiAxLjkzNyAwbDUwLjE3OS00OC45MzFjMi4xMzktMi4wODcgNS42MDgtMi4wODcgNy43NDggMHoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+";
22
- const APPKIT_SYMBOL = Symbol.for("kheopskit.cachedAppKit");
23
- const APPKIT_PROJECT_ID_SYMBOL = Symbol.for("kheopskit.cachedAppKitProjectId");
24
- const getCachedAppKit = () => globalThis[APPKIT_SYMBOL];
25
- const getCachedAppKitProjectId = () => globalThis[APPKIT_PROJECT_ID_SYMBOL];
26
- const setCachedAppKit = (value, projectId) => {
27
- globalThis[APPKIT_SYMBOL] = value;
28
- globalThis[APPKIT_PROJECT_ID_SYMBOL] = projectId;
29
- };
30
- /**
31
- * Clears the cached AppKit observable.
32
- * Use when configuration changes or for testing purposes.
33
- * Note: This does NOT destroy the appKit instance created by Reown.
34
- */
35
- const resetAppKitCache = () => {
36
- setCachedAppKit(void 0);
37
- };
38
- /**
39
- * Drops a WalletConnect namespace's cached account observables. Called when a
40
- * namespace transitions to disconnected — including external disconnects (from
41
- * the wallet app), which flip status via `subscribeProviders` without ever
42
- * calling `disconnect()` — so a later reconnect rebuilds the account observables
43
- * against the fresh session instead of a stale closure.
44
- *
45
- * Keys are `accounts:<walletId>:...`; the trailing colon scopes the prefix to
46
- * this wallet and avoids matching a sibling whose id is a string prefix.
47
- */
48
- const dropAccountsCache = (platform) => {
49
- clearCachedObservablesByPrefix(`accounts:${WALLET_CONNECT_WALLET_ID}:${platform}:`);
50
- };
51
- /**
52
- * The single, platform-less WalletConnect connector for the given config — or
53
- * `null` when WalletConnect isn't configured, on the server, or when
54
- * `@reown/appkit` isn't installed.
55
- *
56
- * @remarks
57
- * The AppKit instance is a process-wide singleton (Reown AppKit itself cannot
58
- * be instantiated twice). The **first** call with a `walletConnect` config wins;
59
- * later calls with a *different* `walletConnect` config reuse that first
60
- * instance. Call {@link resetAppKitCache} before re-initialising if the
61
- * WalletConnect config must change.
62
- *
63
- * One WalletConnect session is shared across every namespace, and a namespace
64
- * can only be established during the initial pairing — it can't be added to a
65
- * live session afterwards. So this exposes ONE connector (not one per platform):
66
- * `connect()` opens the modal; the wallet approves whichever namespaces it
67
- * supports in that single pairing, and `platforms` reflects what the live
68
- * session carries. Per-namespace accounts are derived by each platform plugin.
69
- */
70
- const getWalletConnectWallet$ = (config) => {
71
- if (!config.walletConnect) return of(null);
72
- if (typeof window === "undefined") return of(null);
73
- const walletConnect = config.walletConnect;
74
- const cachedProjectId = getCachedAppKitProjectId();
75
- if (cachedProjectId !== void 0 && cachedProjectId !== walletConnect.projectId) console.warn("[kheopskit] WalletConnect is already initialised with projectId %s; AppKit is a process-wide singleton, so the first configuration wins and projectId %s is ignored. Call resetAppKitCache() before re-initialising if the WalletConnect config must change.", cachedProjectId, walletConnect.projectId);
76
- let cachedAppKit = getCachedAppKit();
77
- if (!cachedAppKit) {
78
- cachedAppKit = from(loadAppKit()).pipe(switchMap((createAppKit) => {
79
- if (!createAppKit) return of(null);
80
- return new Observable((subscriber) => {
81
- const appKit = createAppKit({
82
- projectId: walletConnect.projectId,
83
- metadata: walletConnect.metadata,
84
- networks: walletConnect.networks,
85
- themeMode: walletConnect.themeMode,
86
- themeVariables: walletConnect.themeVariables,
87
- universalProviderConfigOverride: { methods: {
88
- polkadot: ["polkadot_signTransaction", "polkadot_signMessage"],
89
- solana: [
90
- "solana_signTransaction",
91
- "solana_signMessage",
92
- "solana_signAndSendTransaction"
93
- ]
94
- } },
95
- allWallets: "HIDE",
96
- debug: config.debug,
97
- allowUnsupportedChain: true
98
- });
99
- const appKitInstance = appKit;
100
- const status$ = new BehaviorSubject({
101
- isPolkadotConnected: false,
102
- isEthereumConnected: false,
103
- isSolanaConnected: false
104
- });
105
- const unsubProviders = appKit.subscribeProviders((providers) => {
106
- status$.next({
107
- isPolkadotConnected: !!providers.polkadot,
108
- isEthereumConnected: !!providers.eip155,
109
- isSolanaConnected: !!providers.solana
110
- });
111
- });
112
- const namespaceOf = {
113
- polkadot: "polkadot",
114
- ethereum: "eip155",
115
- solana: "solana"
116
- };
117
- const allPlatforms = [
118
- "polkadot",
119
- "ethereum",
120
- "solana"
121
- ];
122
- const enabledPlatforms = allPlatforms.filter((p) => appKit.chainNamespaces.includes(namespaceOf[p]));
123
- let prevConnected = {
124
- polkadot: false,
125
- ethereum: false,
126
- solana: false
127
- };
128
- const sub = status$.pipe(map((s) => ({
129
- polkadot: s.isPolkadotConnected,
130
- ethereum: s.isEthereumConnected,
131
- solana: s.isSolanaConnected
132
- })), distinctUntilChanged((a, b) => a.polkadot === b.polkadot && a.ethereum === b.ethereum && a.solana === b.solana), tap((connected) => {
133
- for (const platform of allPlatforms) if (prevConnected[platform] && !connected[platform]) dropAccountsCache(platform);
134
- prevConnected = connected;
135
- }), map((connected) => {
136
- const platforms = enabledPlatforms.filter((p) => connected[p]);
137
- const isConnected = platforms.length > 0;
138
- const walletInfo = appKit.getWalletInfo();
139
- return {
140
- id: WALLET_CONNECT_WALLET_ID,
141
- type: "walletconnect",
142
- platforms,
143
- appKit: appKitInstance,
144
- name: walletInfo?.name ?? "WalletConnect",
145
- icon: walletInfo?.icon ?? WALLET_CONNECT_ICON,
146
- connect: async () => {
147
- if (!isConnected) await appKit.open();
148
- },
149
- disconnect: async () => {
150
- if (isConnected) await appKit.disconnect();
151
- },
152
- isConnected
153
- };
154
- })).subscribe(subscriber);
155
- return () => {
156
- sub.unsubscribe();
157
- unsubProviders();
158
- };
159
- });
160
- }), shareReplay({
161
- refCount: false,
162
- bufferSize: 1
163
- }));
164
- setCachedAppKit(cachedAppKit, walletConnect.projectId);
165
- }
166
- return cachedAppKit;
167
- };
168
- //#endregion
169
7
  //#region src/api/config.ts
170
8
  /**
171
9
  * Default storage key for persisting wallet connection state.
@@ -177,12 +15,14 @@ const DEFAULTS = {
177
15
  autoReconnect: true,
178
16
  debug: false,
179
17
  storageKey: DEFAULT_STORAGE_KEY,
180
- hydrationGracePeriod: 500
18
+ hydrationGracePeriod: 500,
19
+ hydrationTimeout: 3e3
181
20
  };
182
21
  const resolveConfig = (config) => {
183
22
  const platforms = config?.platforms ?? [];
184
23
  const invalidPlatforms = platforms.filter((p) => typeof p !== "object" || p === null || typeof p.getWallets$ !== "function");
185
24
  if (invalidPlatforms.length > 0) throw new Error(`[kheopskit] config.platforms must contain plugin instances created by the per-platform factories (e.g. platforms: [polkadot(), ethereum(), solana()] imported from "@kheopskit/core/<platform>"). Invalid entries: ${JSON.stringify(invalidPlatforms)}. String platform names like "polkadot" were removed in v4 — see MIGRATING_TO_V4.md.`);
25
+ if (config?.walletConnect !== void 0 && typeof config.walletConnect?.getWallet$ !== "function") throw new KheopskitError("INVALID_CONFIG", "config.walletConnect must be created by walletConnect() imported from \"@kheopskit/core/walletconnect\", e.g. walletConnect: walletConnect({ projectId, metadata, networks }). Raw WalletConnect options were removed in v6, see MIGRATING_TO_V6.md.");
186
26
  if (platforms.length === 0) console.warn("[kheopskit] No platforms configured; wallets and accounts will be empty. Pass e.g. platforms: [polkadot()] from \"@kheopskit/core/polkadot\".");
187
27
  return Object.assign({}, DEFAULTS, config, { platforms });
188
28
  };
@@ -193,17 +33,14 @@ const PLATFORM_ORDER$1 = {
193
33
  ethereum: 1,
194
34
  solana: 2
195
35
  };
36
+ const byPlatform = (a1, a2) => PLATFORM_ORDER$1[a1.platform] - PLATFORM_ORDER$1[a2.platform];
196
37
  const byWalletName = (a1, a2) => {
38
+ if (a1.walletName === a2.walletName) return 0;
197
39
  if (a1.walletName.toLowerCase() === "talisman") return -1;
198
40
  if (a2.walletName.toLowerCase() === "talisman") return 1;
199
41
  return a1.walletName.localeCompare(a2.walletName);
200
42
  };
201
- const sortAccounts = (a1, a2) => {
202
- if (a1.platform !== a2.platform) return PLATFORM_ORDER$1[a1.platform] - PLATFORM_ORDER$1[a2.platform];
203
- if (a1.walletName !== a2.walletName) return byWalletName(a1, a2);
204
- if (a1.platform === "polkadot" && a2.platform === "polkadot") return a1.name !== a2.name ? (a1.name ?? "").localeCompare(a2.name ?? "") : a1.address.localeCompare(a2.address);
205
- return a1.id.localeCompare(a2.id);
206
- };
43
+ const sortAccounts = (a1, a2) => byPlatform(a1, a2) || byWalletName(a1, a2);
207
44
  //#endregion
208
45
  //#region src/utils/sortWallets.ts
209
46
  const PLATFORM_ORDER = {
@@ -232,58 +69,80 @@ const acceptsCachedAccount = (cached, platforms) => {
232
69
  return platforms.find((p) => p.platform === cached.platform)?.acceptsCachedAccount?.(cached) ?? true;
233
70
  };
234
71
  //#endregion
72
+ //#region src/api/reconnectPolicy.ts
73
+ const createReconnectPolicy = ({ maxAttempts = 3 } = {}) => {
74
+ const states = /* @__PURE__ */ new Map();
75
+ const shouldAttempt = (walletId) => {
76
+ const state = states.get(walletId);
77
+ if (!state) return true;
78
+ return state.kind === "failed" && state.attempts < maxAttempts;
79
+ };
80
+ const attempt = async (walletId, connect) => {
81
+ if (!shouldAttempt(walletId)) return false;
82
+ const before = states.get(walletId);
83
+ states.set(walletId, { kind: "pending" });
84
+ const recordOutcome = (outcome) => {
85
+ if (states.get(walletId)?.kind !== "settled") states.set(walletId, outcome);
86
+ };
87
+ try {
88
+ await connect();
89
+ recordOutcome({ kind: "reconnected" });
90
+ return true;
91
+ } catch (err) {
92
+ recordOutcome({
93
+ kind: "failed",
94
+ attempts: before?.kind === "failed" ? before.attempts + 1 : 1
95
+ });
96
+ throw err;
97
+ }
98
+ };
99
+ const settle = (walletId) => {
100
+ states.set(walletId, { kind: "settled" });
101
+ };
102
+ return {
103
+ shouldAttempt,
104
+ attempt,
105
+ settle
106
+ };
107
+ };
108
+ const reconnectAccepts = (autoReconnect, wallet) => typeof autoReconnect === "function" ? autoReconnect(wallet) : autoReconnect;
109
+ //#endregion
235
110
  //#region src/api/hydration.ts
236
111
  /**
237
112
  * Builds the cached-state snapshot every consumer must agree on: read the
238
- * store's cached state, hydrate wallets/accounts into placeholders, filter
239
- * accounts through each platform plugin's `acceptsCachedAccount` hook, and sort
240
- * with the same comparators the live pipeline uses.
113
+ * store's cached state, hydrate wallets/accounts into placeholders, apply the
114
+ * `autoReconnect` rule (a cached connected wallet it rejects becomes
115
+ * `disconnected` and loses its cached accounts), filter accounts through each
116
+ * platform plugin's `acceptsCachedAccount` hook, and sort with the same
117
+ * comparators the live pipeline uses.
241
118
  *
242
119
  * This is the single implementation behind the observable's initial seed and
243
120
  * both React snapshot sources (`serverValue`/`initialValue`). They must produce
244
121
  * the same shape in the same order or the UI flickers/reorders on reload — by
245
122
  * sharing this helper, that agreement holds by construction.
246
123
  */
247
- const getHydratedSnapshot = (store, platforms, options = {}) => {
124
+ const getHydratedSnapshot = (store, config, options = {}) => {
248
125
  const cached = store.getCachedState();
126
+ const wallets = cached.wallets.map(hydrateWallet).map((wallet) => wallet.status === "connecting" && isInjectedWallet(wallet) && !reconnectAccepts(config.autoReconnect, wallet) ? {
127
+ ...wallet,
128
+ status: "disconnected"
129
+ } : wallet).map((wallet) => {
130
+ if (!options.icons || wallet.icon) return wallet;
131
+ const icon = getCachedIcon(wallet.id);
132
+ return icon ? {
133
+ ...wallet,
134
+ icon
135
+ } : wallet;
136
+ }).sort(sortWallets);
137
+ const disconnected = new Set(wallets.filter((w) => w.status === "disconnected").map((w) => w.id));
249
138
  return {
250
- wallets: cached.wallets.map((cachedWallet) => {
251
- const wallet = hydrateWallet(cachedWallet);
252
- if (!options.icons || wallet.icon) return wallet;
253
- const icon = getCachedIcon(wallet.id);
254
- return icon ? {
255
- ...wallet,
256
- icon
257
- } : wallet;
258
- }).sort(sortWallets),
259
- accounts: cached.accounts.filter((account) => acceptsCachedAccount(account, platforms)).map(hydrateAccount).sort(sortAccounts)
139
+ wallets,
140
+ accounts: cached.accounts.filter((account) => !disconnected.has(account.walletId)).filter((account) => acceptsCachedAccount(account, config.platforms)).map(hydrateAccount).sort(sortAccounts)
260
141
  };
261
142
  };
262
143
  //#endregion
263
144
  //#region src/utils/createHydrationBuffer.ts
264
- /**
265
- * Core hydration buffer logic shared between wallets and accounts.
266
- *
267
- * During the grace period:
268
- * - Emits cached items immediately (before any live emissions)
269
- * - As live items arrive, merges them using the provided merge function
270
- *
271
- * After the grace period (and convergence, if provided):
272
- * - Only live items are returned
273
- * - isHydrating becomes false
274
- *
275
- * If an `isConverged` function is provided, hydration ends only when BOTH
276
- * the grace period has elapsed AND the live items have converged with cached
277
- * items (e.g., all previously-connected wallets have reconnected).
278
- * A max timeout (6x grace period) prevents infinite hydration.
279
- *
280
- * @param cachedItems - Initial cached items from storage
281
- * @param liveItems$ - Observable of live items as they become available
282
- * @param gracePeriodMs - Time in ms to wait before syncing to live state
283
- * @param mergeFn - Function to merge live and cached items during hydration
284
- * @param isConverged - Optional function to check if live items have caught up to cached items
285
- */
286
- const createBufferCore = (cachedItems, liveItems$, gracePeriodMs, mergeFn, isConverged) => {
145
+ const createBufferCore = (cachedItems, liveItems$, gracePeriodMs, timeoutMs, mergeFn, settled$) => {
287
146
  if (gracePeriodMs <= 0 || cachedItems.length === 0) return liveItems$.pipe(map((items) => ({
288
147
  items,
289
148
  isHydrating: false
@@ -291,30 +150,19 @@ const createBufferCore = (cachedItems, liveItems$, gracePeriodMs, mergeFn, isCon
291
150
  return new Observable((subscriber) => {
292
151
  const subscriptions = new Subscription();
293
152
  const isHydrating$ = new BehaviorSubject(true);
294
- const liveWithInitial$ = liveItems$.pipe(startWith([]));
295
- if (isConverged) {
296
- const timerFired$ = timer(gracePeriodMs).pipe(map(() => true), startWith(false), shareReplay({
297
- bufferSize: 1,
298
- refCount: true
299
- }));
300
- subscriptions.add(combineLatest([liveWithInitial$, timerFired$]).pipe(filter(([, timerFired]) => timerFired), filter(([liveItems]) => isConverged(liveItems, cachedItems)), take(1)).subscribe(() => {
301
- isHydrating$.next(false);
302
- }));
303
- subscriptions.add(timer(gracePeriodMs * 6).subscribe(() => {
304
- if (isHydrating$.value) isHydrating$.next(false);
305
- }));
306
- } else subscriptions.add(timer(gracePeriodMs).subscribe(() => {
307
- isHydrating$.next(false);
308
- }));
309
- subscriptions.add(combineLatest([liveWithInitial$, isHydrating$]).pipe(map(([liveItems, isHydrating]) => {
310
- if (!isHydrating) return {
311
- items: liveItems,
312
- isHydrating: false
313
- };
314
- return {
315
- items: mergeFn(liveItems, cachedItems),
316
- isHydrating: true
317
- };
153
+ const endHydration = () => {
154
+ if (isHydrating$.value) isHydrating$.next(false);
155
+ };
156
+ const graceElapsed$ = timer(gracePeriodMs).pipe(map(() => true), startWith(false));
157
+ const ended$ = settled$ ? combineLatest([graceElapsed$, settled$]).pipe(filter(([graceElapsed, settled]) => graceElapsed && settled), take(1)) : timer(gracePeriodMs);
158
+ subscriptions.add(ended$.subscribe(endHydration));
159
+ subscriptions.add(timer(timeoutMs).subscribe(endHydration));
160
+ subscriptions.add(combineLatest([liveItems$.pipe(startWith([])), isHydrating$]).pipe(map(([liveItems, isHydrating]) => isHydrating ? {
161
+ items: mergeFn(liveItems, cachedItems),
162
+ isHydrating
163
+ } : {
164
+ items: liveItems,
165
+ isHydrating
318
166
  })).subscribe(subscriber));
319
167
  return () => {
320
168
  subscriptions.unsubscribe();
@@ -331,14 +179,15 @@ const createBufferCore = (cachedItems, liveItems$, gracePeriodMs, mergeFn, isCon
331
179
  *
332
180
  * @param cachedItems - Initial cached items from storage
333
181
  * @param liveItems$ - Observable of live items as they become available
334
- * @param gracePeriodMs - Time in ms to wait before syncing to live state
182
+ * @param gracePeriodMs - Minimum time in ms the cached items are shown
183
+ * @param timeoutMs - Upper bound in ms on hydration
335
184
  * @param getKey - Function to extract a unique key from an item
336
- * @param isConverged - Optional function to check if live items have caught up to cached items
185
+ * @param settled$ - Whether the live state has caught up with the cache
337
186
  * @param mergeItem - Optional function to merge a live item with its cached counterpart
338
187
  * @param transformCachedOnly - Optional function to transform cached items not yet in the live stream
339
188
  */
340
- const createHydrationBuffer = (cachedItems, liveItems$, gracePeriodMs, getKey, isConverged, mergeItem, transformCachedOnly) => {
341
- return createBufferCore(cachedItems, liveItems$, gracePeriodMs, (liveItems, cached) => {
189
+ const createHydrationBuffer = (cachedItems, liveItems$, gracePeriodMs, timeoutMs, getKey, settled$, mergeItem, transformCachedOnly) => {
190
+ return createBufferCore(cachedItems, liveItems$, gracePeriodMs, timeoutMs, (liveItems, cached) => {
342
191
  const cachedByKey = new Map(cached.map((item) => [getKey(item), item]));
343
192
  const liveKeys = new Set(liveItems.map(getKey));
344
193
  const mergedItems = liveItems.map((liveItem) => {
@@ -352,7 +201,7 @@ const createHydrationBuffer = (cachedItems, liveItems$, gracePeriodMs, getKey, i
352
201
  mergedItems.push(transformed);
353
202
  }
354
203
  return mergedItems;
355
- }, isConverged);
204
+ }, settled$);
356
205
  };
357
206
  /**
358
207
  * Creates a hydration buffer specifically for accounts, which need special handling.
@@ -363,17 +212,18 @@ const createHydrationBuffer = (cachedItems, liveItems$, gracePeriodMs, getKey, i
363
212
  *
364
213
  * @param cachedAccounts - Initial cached accounts from storage
365
214
  * @param liveAccounts$ - Observable of live accounts as they become available
366
- * @param gracePeriodMs - Time in ms to wait before syncing to live state
215
+ * @param gracePeriodMs - Minimum time in ms the cached accounts are shown
216
+ * @param timeoutMs - Upper bound in ms on hydration
367
217
  * @param getWalletId - Function to extract the wallet ID from an account
368
- * @param isConverged - Optional function to check if live accounts have caught up to cached accounts
218
+ * @param settled$ - Whether the live state has caught up with the cache
369
219
  */
370
- const createAccountHydrationBuffer = (cachedAccounts, liveAccounts$, gracePeriodMs, getWalletId, isConverged) => {
371
- return createBufferCore(cachedAccounts, liveAccounts$, gracePeriodMs, (liveAccounts, cached) => {
220
+ const createAccountHydrationBuffer = (cachedAccounts, liveAccounts$, gracePeriodMs, timeoutMs, getWalletId, settled$) => {
221
+ return createBufferCore(cachedAccounts, liveAccounts$, gracePeriodMs, timeoutMs, (liveAccounts, cached) => {
372
222
  const walletsWithLiveAccounts = new Set(liveAccounts.map(getWalletId));
373
223
  const mergedAccounts = [...liveAccounts];
374
224
  for (const cachedAccount of cached) if (!walletsWithLiveAccounts.has(getWalletId(cachedAccount))) mergedAccounts.push(cachedAccount);
375
225
  return mergedAccounts;
376
- }, isConverged);
226
+ }, settled$);
377
227
  };
378
228
  //#endregion
379
229
  //#region src/utils/logObservable.ts
@@ -388,8 +238,14 @@ const logObservable = (label, opts) => tap((value) => {
388
238
  //#region src/api/accounts.ts
389
239
  const getAccounts$ = (config, wallets) => {
390
240
  return new Observable((subscriber) => {
391
- const sources = config.platforms.map((plugin) => plugin.getAccounts$(wallets.pipe(map((ws) => ws.filter((w) => isWalletConnectWallet(w) || w.platform === plugin.platform)))));
392
- const sub = (sources.length ? combineLatest(sources).pipe(map((accounts) => accounts.flat().sort(sortAccounts))) : of([])).subscribe(subscriber);
241
+ const sources = config.platforms.map((plugin) => framesOf(plugin.getAccounts$(wallets.pipe(map((ws) => ws.filter((w) => isWalletConnectWallet(w) || w.platform === plugin.platform))))));
242
+ const sub = (sources.length ? combineLatest(sources).pipe(map((frames) => ({
243
+ accounts: frames.flatMap((frame) => frame.accounts).sort(sortAccounts),
244
+ answered: frames.flatMap((frame) => frame.answered)
245
+ }))) : of({
246
+ accounts: [],
247
+ answered: []
248
+ })).subscribe(subscriber);
393
249
  return () => {
394
250
  sub.unsubscribe();
395
251
  };
@@ -400,28 +256,41 @@ const getAccounts$ = (config, wallets) => {
400
256
  };
401
257
  //#endregion
402
258
  //#region src/utils/createStore.ts
403
- const createStore = (key, defaultValue, storage = safeLocalStorage) => {
404
- const subject = new BehaviorSubject(getStoredData(key, defaultValue, storage));
405
- let unsubscribeStorage;
406
- if (typeof window !== "undefined" && storage.subscribe) unsubscribeStorage = storage.subscribe(key, (newValue) => {
407
- subject.next(parseData(newValue, defaultValue));
259
+ const createStore = (key, defaultValue, storage = safeLocalStorage, normalize = (value) => value) => {
260
+ const read = (str) => normalize(parseData(str, defaultValue));
261
+ const syncsAcrossTabs = typeof window !== "undefined" && storage.subscribe !== void 0;
262
+ const subject = new BehaviorSubject(read(storage.getItem(key)));
263
+ let observers = 0;
264
+ let stopSync;
265
+ const latestValue = () => {
266
+ if (syncsAcrossTabs && observers === 0) subject.next(read(storage.getItem(key)));
267
+ return subject.getValue();
268
+ };
269
+ const observable = new Observable((subscriber) => {
270
+ if (observers === 0 && syncsAcrossTabs) {
271
+ latestValue();
272
+ stopSync = storage.subscribe?.(key, (newValue) => {
273
+ subject.next(read(newValue));
274
+ });
275
+ }
276
+ observers++;
277
+ const subscription = subject.subscribe(subscriber);
278
+ return () => {
279
+ subscription.unsubscribe();
280
+ observers--;
281
+ if (observers === 0) stopSync?.();
282
+ };
408
283
  });
409
284
  const update = (val) => {
410
- setStoredData(key, val, storage);
285
+ persistBestEffort(key, val, storage);
411
286
  subject.next(val);
412
287
  };
413
288
  return {
414
- observable: subject.asObservable(),
289
+ observable,
415
290
  set: (val) => update(val),
416
- mutate: (transform) => update(transform(subject.getValue())),
417
- get: () => structuredClone(subject.getValue()),
418
- /**
419
- * Cleanup subscriptions. Call this when the store is no longer needed.
420
- */
421
- destroy: () => {
422
- unsubscribeStorage?.();
423
- subject.complete();
424
- }
291
+ mutate: (transform) => update(transform(latestValue())),
292
+ get: () => structuredClone(latestValue()),
293
+ destroy: () => subject.complete()
425
294
  };
426
295
  };
427
296
  const parseData = (str, defaultValue) => {
@@ -430,17 +299,23 @@ const parseData = (str, defaultValue) => {
430
299
  } catch {}
431
300
  return defaultValue;
432
301
  };
433
- const getStoredData = (key, defaultValue, storage) => {
434
- const str = storage.getItem(key);
435
- return parseData(str, defaultValue);
436
- };
437
- const setStoredData = (key, val, storage) => {
302
+ const persistBestEffort = (key, val, storage) => {
438
303
  const str = JSON.stringify(val);
439
- storage.setItem(key, str);
304
+ try {
305
+ storage.setItem(key, str);
306
+ } catch {}
440
307
  };
441
308
  //#endregion
442
309
  //#region src/api/compactStore.ts
443
310
  const EMPTY_STORE_DATA = {};
311
+ const migratePreV6StoreData = (data) => {
312
+ if (data?.autoReconnect === void 0) return data;
313
+ const { autoReconnect, ...rest } = data;
314
+ return rest.remembered === void 0 && Array.isArray(autoReconnect) ? {
315
+ ...rest,
316
+ remembered: autoReconnect
317
+ } : rest;
318
+ };
444
319
  const toCompactPolkadotAccountType = (type) => {
445
320
  switch (type) {
446
321
  case "sr25519": return 0;
@@ -496,7 +371,7 @@ const toCompactStore = (data) => {
496
371
  ]);
497
372
  return {
498
373
  v: 1,
499
- r: data.autoReconnect,
374
+ r: data.remembered,
500
375
  w: wallets?.length ? wallets : void 0,
501
376
  a: accounts?.length ? accounts : void 0
502
377
  };
@@ -539,7 +414,7 @@ const fromCompactStore = (data) => {
539
414
  });
540
415
  }
541
416
  return {
542
- autoReconnect: data.r,
417
+ remembered: data.r,
543
418
  cachedWallets: wallets,
544
419
  cachedAccounts: accounts
545
420
  };
@@ -548,7 +423,7 @@ const decodeStore = (raw, fallback) => {
548
423
  try {
549
424
  const parsed = JSON.parse(raw);
550
425
  if (isCompactStore(parsed)) return fromCompactStore(parsed);
551
- return parsed;
426
+ return migratePreV6StoreData(parsed);
552
427
  } catch {
553
428
  return fallback;
554
429
  }
@@ -616,26 +491,26 @@ const isValidCachedAccount = (value) => {
616
491
  };
617
492
  /**
618
493
  * Creates a kheopskit store with the appropriate storage backend.
619
- * Uses cookieStorage when ssrCookies is provided (for SSR hydration),
620
- * otherwise falls back to safeLocalStorage.
494
+ * Uses the given `storage` when provided, else cookieStorage when ssrCookies is
495
+ * provided (for SSR hydration), otherwise safeLocalStorage.
621
496
  *
622
497
  * @param options - Configuration options for the store
623
498
  */
624
499
  const createKheopskitStore = (options = {}) => {
625
500
  const { ssrCookies, storageKey = DEFAULT_STORAGE_KEY } = options;
626
- const storage = ssrCookies !== void 0 ? createCompactCookieStorage(ssrCookies) : safeLocalStorage;
627
- const store = createStore(storageKey, DEFAULT_SETTINGS, storage);
628
- const addEnabledWalletId = (walletId) => {
501
+ const storage = options.storage ?? (ssrCookies !== void 0 ? createCompactCookieStorage(ssrCookies) : safeLocalStorage);
502
+ const store = createStore(storageKey, DEFAULT_SETTINGS, storage, migratePreV6StoreData);
503
+ const rememberWallet = (walletId) => {
629
504
  parseWalletId(walletId);
630
505
  store.mutate((prev) => ({
631
506
  ...prev,
632
- autoReconnect: uniq((prev.autoReconnect ?? []).concat(walletId))
507
+ remembered: uniq((prev.remembered ?? []).concat(walletId))
633
508
  }));
634
509
  };
635
- const removeEnabledWalletId = (walletId) => {
510
+ const forgetWallet = (walletId) => {
636
511
  store.mutate((prev) => ({
637
512
  ...prev,
638
- autoReconnect: uniq((prev.autoReconnect ?? []).filter((id) => id !== walletId))
513
+ remembered: uniq((prev.remembered ?? []).filter((id) => id !== walletId))
639
514
  }));
640
515
  };
641
516
  const getCachedState = () => {
@@ -656,10 +531,11 @@ const createKheopskitStore = (options = {}) => {
656
531
  };
657
532
  return {
658
533
  observable: store.observable,
659
- addEnabledWalletId,
660
- removeEnabledWalletId,
534
+ rememberWallet,
535
+ forgetWallet,
661
536
  getCachedState,
662
- setCachedState
537
+ setCachedState,
538
+ destroy: store.destroy
663
539
  };
664
540
  };
665
541
  /**
@@ -679,43 +555,9 @@ const getDefaultStore = () => {
679
555
  return g[DEFAULT_STORE_SYMBOL];
680
556
  };
681
557
  //#endregion
682
- //#region src/api/reconnectPolicy.ts
683
- const createReconnectPolicy = ({ maxAttempts = 3 } = {}) => {
684
- const reconnecting = /* @__PURE__ */ new Set();
685
- const reconnected = /* @__PURE__ */ new Set();
686
- const failedAttempts = /* @__PURE__ */ new Map();
687
- const shouldAttempt = (walletId) => !reconnecting.has(walletId) && !reconnected.has(walletId) && (failedAttempts.get(walletId) ?? 0) < maxAttempts;
688
- /**
689
- * Runs `connect` under the policy. Resolves `false` without connecting when
690
- * an attempt is already in flight, the wallet already reconnected, or it
691
- * exhausted its attempts; resolves `true` on success. A rejection from
692
- * `connect` is recorded as a failed attempt and rethrown for the caller to
693
- * report.
694
- */
695
- const attempt = async (walletId, connect) => {
696
- if (!shouldAttempt(walletId)) return false;
697
- reconnecting.add(walletId);
698
- try {
699
- await connect();
700
- reconnected.add(walletId);
701
- failedAttempts.delete(walletId);
702
- return true;
703
- } catch (err) {
704
- failedAttempts.set(walletId, (failedAttempts.get(walletId) ?? 0) + 1);
705
- throw err;
706
- } finally {
707
- reconnecting.delete(walletId);
708
- }
709
- };
710
- return {
711
- shouldAttempt,
712
- attempt
713
- };
714
- };
715
- //#endregion
716
558
  //#region src/api/wallets.ts
717
559
  const getWallets$ = (config, store) => {
718
- const autoReconnectWalletIds$ = store.observable.pipe(map((s) => s.autoReconnect ?? []), take(1), shareReplay({
560
+ const rememberedWalletIds$ = store.observable.pipe(map((s) => s.remembered ?? []), take(1), shareReplay({
719
561
  bufferSize: 1,
720
562
  refCount: true
721
563
  }));
@@ -725,20 +567,39 @@ const getWallets$ = (config, store) => {
725
567
  store
726
568
  };
727
569
  const observables = config.platforms.map((plugin) => plugin.getWallets$(ctx));
728
- const platformWallets$ = observables.length ? combineLatest(observables).pipe(map((wallets) => wallets.flat())) : of([]);
729
- const wallets$ = combineLatest([platformWallets$, getWalletConnectWallet$(config)]).pipe(map(([platformWallets, walletConnect]) => {
730
- return (walletConnect ? [...platformWallets, walletConnect] : platformWallets).sort(sortWallets);
731
- }));
732
570
  const reconnectPolicy = createReconnectPolicy();
733
- const subAutoReconnect = combineLatest([wallets$, autoReconnectWalletIds$]).pipe(filter(([, walletIds]) => config.autoReconnect && !!walletIds?.length), mergeMap(([wallets, walletIds]) => wallets.filter((wallet) => walletIds?.includes(wallet.id)))).subscribe(async (wallet) => {
734
- if (wallet.isConnected) return;
571
+ const settleOnUserCall = (wallet) => ({
572
+ ...wallet,
573
+ connect: (options) => {
574
+ reconnectPolicy.settle(wallet.id);
575
+ return wallet.connect(options);
576
+ },
577
+ disconnect: () => {
578
+ reconnectPolicy.settle(wallet.id);
579
+ return wallet.disconnect();
580
+ }
581
+ });
582
+ const platformWallets$ = observables.length ? combineLatest(observables).pipe(map((wallets) => wallets.flat())) : of([]);
583
+ const walletConnect$ = config.walletConnect?.getWallet$(ctx);
584
+ const rawWallets$ = (walletConnect$ ? combineLatest([platformWallets$, walletConnect$]).pipe(map(([platformWallets, walletConnect]) => walletConnect ? [...platformWallets, walletConnect] : platformWallets)) : platformWallets$).pipe(map((all) => all.sort(sortWallets)));
585
+ const wallets$ = rawWallets$.pipe(map((wallets) => wallets.map((wallet) => isInjectedWallet(wallet) ? settleOnUserCall(wallet) : wallet)));
586
+ const reconnect = async (wallet) => {
735
587
  try {
736
588
  await reconnectPolicy.attempt(wallet.id, () => wallet.connect());
737
589
  } catch (err) {
738
- console.error("Failed to reconnect wallet %s", wallet.id, { err });
590
+ if (subscriber.closed) return;
591
+ if (err instanceof KheopskitError) {
592
+ if (err.code === "USER_REJECTED") store.forgetWallet(wallet.id);
593
+ if (err.code === "USER_REJECTED" || err.code === "ABORTED") reconnectPolicy.settle(wallet.id);
594
+ }
595
+ if (config.debug) console.debug("[kheopskit] reconnect of %s failed", wallet.id, err);
739
596
  }
740
- });
597
+ };
741
598
  const subWallets = wallets$.subscribe(subscriber);
599
+ if (subscriber.closed) return subWallets;
600
+ const subAutoReconnect = combineLatest([rawWallets$, rememberedWalletIds$]).pipe(filter(([, walletIds]) => config.autoReconnect !== false && walletIds.length > 0), mergeMap(([wallets, walletIds]) => wallets.filter(isInjectedWallet).filter((wallet) => walletIds.includes(wallet.id))), tap((wallet) => {
601
+ if (wallet.status === "connected") reconnectPolicy.settle(wallet.id);
602
+ }), filter((wallet) => wallet.status === "disconnected" && reconnectAccepts(config.autoReconnect, wallet))).subscribe(reconnect);
742
603
  return () => {
743
604
  subAutoReconnect.unsubscribe();
744
605
  subWallets.unsubscribe();
@@ -759,33 +620,129 @@ const getKheopskit$ = (config, options = {}) => {
759
620
  });
760
621
  if (kc.debug) console.debug("[kheopskit] config", kc);
761
622
  if (kc.debug && typeof window === "undefined" && ssrCookies === void 0) console.warn("[kheopskit] Running on server without `ssrCookies`. Wallet state will not be hydrated. Pass cookies for SSR support.");
762
- const { wallets: cachedWallets, accounts: cachedAccounts } = getHydratedSnapshot(store, kc.platforms, { icons: true });
623
+ const { wallets: cachedWallets, accounts: cachedAccounts } = getHydratedSnapshot(store, kc, { icons: true });
763
624
  if (kc.debug && cachedWallets.length > 0) console.debug("[kheopskit] hydrating from cache:", {
764
625
  wallets: cachedWallets.length,
765
626
  accounts: cachedAccounts.length
766
627
  });
767
628
  return new Observable((subscriber) => {
629
+ let latest;
630
+ let closed = false;
631
+ const waiters = /* @__PURE__ */ new Set();
632
+ const liveWaiters = /* @__PURE__ */ new Set();
633
+ const wrapped = /* @__PURE__ */ new WeakMap();
634
+ const abandon = (waiter) => {
635
+ waiters.delete(waiter);
636
+ if (waiter.isConnected) waiter.reject(abortedError(waiter.walletId, "connect"));
637
+ else waiter.resolve();
638
+ };
639
+ const settleIn = (waiter, state) => {
640
+ const wallet = state.wallets.find((w) => w.id === waiter.walletId);
641
+ if (!wallet || wallet.isConnected === waiter.isConnected) {
642
+ waiters.delete(waiter);
643
+ waiter.resolve();
644
+ }
645
+ };
646
+ const supersede = (walletId, isConnected) => {
647
+ for (const waiter of waiters) if (waiter.walletId === walletId && waiter.isConnected !== isConnected) abandon(waiter);
648
+ };
649
+ const whenState = (walletId, isConnected) => new Promise((resolve, reject) => {
650
+ const waiter = {
651
+ walletId,
652
+ isConnected,
653
+ resolve,
654
+ reject
655
+ };
656
+ waiters.add(waiter);
657
+ if (latest) settleIn(waiter, latest);
658
+ if (closed && waiters.has(waiter)) abandon(waiter);
659
+ });
660
+ const whenLive = (walletId, signal) => new Promise((resolve, reject) => {
661
+ const settle = (finish) => {
662
+ liveWaiters.delete(check);
663
+ signal?.removeEventListener("abort", onAbort);
664
+ finish();
665
+ };
666
+ const onAbort = () => settle(() => reject(abortedError(walletId, "connect")));
667
+ const check = (state) => {
668
+ if (!state) return onAbort();
669
+ const live = state.wallets.find((w) => w.id === walletId && !w.isPlaceholder);
670
+ if (live) settle(() => resolve(live));
671
+ else if (!state.isHydrating) settle(() => reject(walletPendingError(walletId)));
672
+ };
673
+ if (closed || signal?.aborted) return onAbort();
674
+ signal?.addEventListener("abort", onAbort, { once: true });
675
+ liveWaiters.add(check);
676
+ if (latest) check(latest);
677
+ });
678
+ const deferToLive = (placeholder) => ({
679
+ ...placeholder,
680
+ connect: async (options) => {
681
+ const live = await whenLive(placeholder.id, options?.signal);
682
+ if (closed) throw abortedError(placeholder.id, "connect");
683
+ await live.connect(options);
684
+ }
685
+ });
686
+ const settleWithState = (wallet) => {
687
+ const existing = wrapped.get(wallet);
688
+ if (existing) return existing;
689
+ if (wallet.isPlaceholder) {
690
+ const deferred = deferToLive(wallet);
691
+ wrapped.set(wallet, deferred);
692
+ return deferred;
693
+ }
694
+ if (!isInjectedWallet(wallet)) return wallet;
695
+ const settled = {
696
+ ...wallet,
697
+ connect: async (options) => {
698
+ if (closed) throw abortedError(wallet.id, "connect");
699
+ await wallet.connect(options);
700
+ supersede(wallet.id, true);
701
+ await whenState(wallet.id, true);
702
+ },
703
+ disconnect: async () => {
704
+ if (closed) throw abortedError(wallet.id, "disconnect");
705
+ await wallet.disconnect();
706
+ supersede(wallet.id, false);
707
+ await whenState(wallet.id, false);
708
+ }
709
+ };
710
+ wrapped.set(wallet, settled);
711
+ return settled;
712
+ };
768
713
  const liveWallets$ = getWallets$(kc, store);
769
- const liveAccounts$ = getAccounts$(kc, liveWallets$);
770
- const bufferedWallets$ = createHydrationBuffer(cachedWallets, liveWallets$, kc.hydrationGracePeriod, (w) => w.id, (liveWallets, cached) => {
771
- const cachedConnectedIds = new Set(cached.filter((w) => w.isConnected).map((w) => w.id));
772
- if (cachedConnectedIds.size === 0) return true;
773
- return [...cachedConnectedIds].every((id) => liveWallets.some((w) => w.id === id && w.isConnected));
774
- }, (live, cached) => ({
775
- ...cached,
776
- icon: cached.icon || getCachedIcon(cached.id) || live.icon,
777
- connect: live.connect,
778
- disconnect: live.disconnect
779
- }), (cached) => ({
714
+ const accountFrames$ = getAccounts$(kc, liveWallets$);
715
+ const liveAccounts$ = accountFrames$.pipe(map(({ accounts }) => accounts.map(asLive)));
716
+ const isDiscovering$ = pageLoaded$.pipe(switchMap((loaded) => loaded ? liveWallets$.pipe(take(1), map(() => false)) : of(true)), shareReplay({
717
+ bufferSize: 1,
718
+ refCount: true
719
+ }));
720
+ const reconnecting = cachedWallets.filter((w) => w.status === "connecting");
721
+ const hydrationSettled$ = combineLatest([
722
+ liveWallets$,
723
+ accountFrames$.pipe(startWith(noAnswerYet)),
724
+ isDiscovering$,
725
+ store.observable.pipe(map((s) => s.remembered ?? []), take(1))
726
+ ]).pipe(map(([wallets, { accounts, answered }, isDiscovering, remembered]) => !isDiscovering && reconnecting.every((cached) => reconnectSettled(cached, {
727
+ wallets,
728
+ accounts,
729
+ answered,
730
+ cachedAccounts,
731
+ remembered
732
+ }))), debounceTime(0, asapScheduler), distinctUntilChanged());
733
+ const bufferedWallets$ = createHydrationBuffer(cachedWallets, liveWallets$.pipe(map((wallets) => wallets.map(asLive))), kc.hydrationGracePeriod, kc.hydrationTimeout, (w) => w.id, hydrationSettled$, (live, cached) => {
734
+ const status = sessionRestoring(live, cached) ? "connecting" : live.status;
735
+ return {
736
+ ...live,
737
+ status,
738
+ isConnected: status === "connected",
739
+ icon: cached.icon || getCachedIcon(cached.id) || live.icon
740
+ };
741
+ }, (cached) => ({
780
742
  ...cached,
781
743
  icon: cached.icon || getCachedIcon(cached.id) || ""
782
744
  }));
783
- const bufferedAccounts$ = createAccountHydrationBuffer(cachedAccounts, liveAccounts$, kc.hydrationGracePeriod, (a) => a.walletId, (liveAccounts, cached) => {
784
- const cachedWalletIds = new Set(cached.map((a) => a.walletId));
785
- if (cachedWalletIds.size === 0) return true;
786
- const liveWalletIds = new Set(liveAccounts.map((a) => a.walletId));
787
- return [...cachedWalletIds].every((id) => liveWalletIds.has(id));
788
- });
745
+ const bufferedAccounts$ = createAccountHydrationBuffer(cachedAccounts, liveAccounts$, kc.hydrationGracePeriod, kc.hydrationTimeout, (a) => a.walletId, hydrationSettled$);
789
746
  const sharedWallets$ = bufferedWallets$.pipe(shareReplay({
790
747
  bufferSize: 1,
791
748
  refCount: true
@@ -796,38 +753,44 @@ const getKheopskit$ = (config, options = {}) => {
796
753
  }));
797
754
  const subscription = combineLatest({
798
755
  wallets: sharedWallets$,
799
- accounts: sharedAccounts$
800
- }).pipe(map(({ wallets, accounts }) => {
756
+ accounts: sharedAccounts$,
757
+ isDiscovering: isDiscovering$
758
+ }).pipe(map(({ wallets, accounts, isDiscovering }) => {
801
759
  if (kc.debug) console.debug("[kheopskit] hydration state", {
802
760
  walletsHydrating: wallets.isHydrating,
803
761
  accountsHydrating: accounts.isHydrating,
762
+ isDiscovering,
804
763
  walletsConnected: wallets.items.filter((w) => w.isConnected).length,
805
764
  walletsTotal: wallets.items.length
806
765
  });
807
766
  return {
808
767
  config: kc,
809
- wallets: [...wallets.items].sort(sortWallets),
768
+ wallets: [...wallets.items].sort(sortWallets).map(settleWithState),
810
769
  accounts: [...accounts.items].sort(sortAccounts),
811
- isHydrating: wallets.isHydrating || accounts.isHydrating
770
+ isHydrating: wallets.isHydrating || accounts.isHydrating,
771
+ isDiscovering
812
772
  };
773
+ }), debounceTime(0, asapScheduler), distinctUntilChanged(statesEqual), tap((state) => {
774
+ latest = state;
775
+ for (const check of liveWaiters) check(state);
776
+ for (const waiter of waiters) settleIn(waiter, state);
813
777
  })).subscribe(subscriber);
814
778
  const persistSub = combineLatest({
815
779
  wallets: sharedWallets$,
816
780
  accounts: sharedAccounts$
817
781
  }).pipe(filter(({ wallets, accounts }) => !wallets.isHydrating && !accounts.isHydrating), debounceTime(1e3), distinctUntilChanged((prev, curr) => {
818
- const prevWalletKeys = prev.wallets.items.map(walletPersistKey);
819
- const currWalletKeys = curr.wallets.items.map(walletPersistKey);
782
+ const prevWalletKeys = prev.wallets.items.map(persistedWalletKey);
783
+ const currWalletKeys = curr.wallets.items.map(persistedWalletKey);
820
784
  const prevAccountKeys = prev.accounts.items.map(accountChangeKey);
821
785
  const currAccountKeys = curr.accounts.items.map(accountChangeKey);
822
786
  return arraysEqual(prevWalletKeys, currWalletKeys) && arraysEqual(prevAccountKeys, currAccountKeys);
823
787
  })).subscribe(({ wallets, accounts }) => {
824
- const connectedWalletIds = new Set(wallets.items.filter((w) => w.isConnected).map((w) => w.id));
825
- const relevantAccounts = accounts.items.filter((a) => connectedWalletIds.has(a.walletId));
788
+ const snapshot = persistedSnapshot(wallets.items, accounts.items, store.getCachedState());
826
789
  if (kc.debug) console.debug("[kheopskit] persisting state snapshot:", {
827
- wallets: wallets.items.length,
828
- accounts: relevantAccounts.length
790
+ wallets: snapshot.wallets.length,
791
+ accounts: snapshot.accounts.length
829
792
  });
830
- store.setCachedState(wallets.items.map(serializeWallet), relevantAccounts.map(serializeAccount));
793
+ store.setCachedState(snapshot.wallets, snapshot.accounts);
831
794
  const icons = {};
832
795
  for (const wallet of wallets.items) if (wallet.icon) icons[wallet.id] = wallet.icon;
833
796
  setCachedIcons(icons);
@@ -835,11 +798,11 @@ const getKheopskit$ = (config, options = {}) => {
835
798
  return () => {
836
799
  subscription.unsubscribe();
837
800
  persistSub.unsubscribe();
801
+ closed = true;
802
+ for (const waiter of waiters) abandon(waiter);
803
+ for (const check of liveWaiters) check(void 0);
838
804
  };
839
- }).pipe(distinctUntilChanged(statesEqual), throttleTime(16, void 0, {
840
- leading: true,
841
- trailing: true
842
- }), logObservable("kheopskit$", {
805
+ }).pipe(logObservable("kheopskit$", {
843
806
  enabled: kc.debug,
844
807
  printValue: true
845
808
  }), shareReplay({
@@ -847,14 +810,60 @@ const getKheopskit$ = (config, options = {}) => {
847
810
  refCount: true
848
811
  }));
849
812
  };
813
+ const sessionRestoring = (live, cached) => isWalletConnectWallet(live) && live.status === "disconnected" && cached.status === "connecting";
814
+ const flagged = /* @__PURE__ */ new WeakMap();
815
+ const asLive = (item) => {
816
+ const existing = flagged.get(item);
817
+ if (existing) return existing;
818
+ const live = {
819
+ ...item,
820
+ isPlaceholder: false
821
+ };
822
+ flagged.set(item, live);
823
+ return live;
824
+ };
825
+ const noAnswerYet = {
826
+ accounts: [],
827
+ answered: []
828
+ };
829
+ /**
830
+ * A remembered wallet that reads disconnected without an error is still
831
+ * waiting for its reconnect: getWallets$ emits the wallet list before its
832
+ * connect() flips the status to connecting.
833
+ */
834
+ const reconnectSettled = (cached, live) => {
835
+ const wallet = live.wallets.find((w) => w.id === cached.id);
836
+ if (!wallet) return true;
837
+ const status = sessionRestoring(wallet, cached) ? "connecting" : wallet.status;
838
+ if (status === "connecting") return false;
839
+ if (status === "disconnected") return wallet.error !== void 0 || !live.remembered.includes(cached.id);
840
+ return live.answered.includes(cached.id) || !live.cachedAccounts.some((a) => a.walletId === cached.id) || live.accounts.some((a) => a.walletId === cached.id);
841
+ };
842
+ /**
843
+ * A wallet still connecting keeps its previous cache entry and accounts:
844
+ * written live it would read isConnected: false with no accounts, and the
845
+ * next load would hydrate it as disconnected without placeholders while its
846
+ * reconnect runs.
847
+ */
848
+ const persistedSnapshot = (wallets, accounts, previous) => {
849
+ const connecting = new Set(wallets.filter((w) => w.status === "connecting").map((w) => w.id));
850
+ const connected = new Set(wallets.filter((w) => w.isConnected).map((w) => w.id));
851
+ const previousWallets = new Map(previous.wallets.map((w) => [w.id, w]));
852
+ return {
853
+ wallets: wallets.map((w) => connecting.has(w.id) && previousWallets.get(w.id) || serializeWallet(w)),
854
+ accounts: [...accounts.filter((a) => connected.has(a.walletId)).map(serializeAccount), ...previous.accounts.filter((a) => connecting.has(a.walletId))]
855
+ };
856
+ };
850
857
  const arraysEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
851
858
  /**
852
- * A signature of an account covering its id plus the platform-specific fields
853
- * that can change without the id changing (Ethereum chainId, Polkadot key type,
854
- * Solana clusters). Used to detect both UI-relevant changes and changes worth
855
- * re-persisting, so the two comparators can't drift apart.
859
+ * A signature of an account covering its id, its name, plus the
860
+ * platform-specific fields that can change without the id changing (Ethereum
861
+ * chainId, Polkadot key type, Solana clusters). Used to detect both UI-relevant
862
+ * changes and changes worth re-persisting, so the two comparators can't drift
863
+ * apart.
856
864
  */
857
- const accountChangeKey = (account) => {
865
+ const accountChangeKey = (account) => `${account.name ?? ""}|${accountPlatformKey(account)}`;
866
+ const accountPlatformKey = (account) => {
858
867
  switch (account.platform) {
859
868
  case "ethereum": return `${account.id}|${account.chainId ?? ""}`;
860
869
  case "polkadot": return `${account.id}|${account.type ?? ""}`;
@@ -867,24 +876,30 @@ const walletPlatforms = (wallet) => isWalletConnectWallet(wallet) ? wallet.platf
867
876
  /** Fields persisted to the cache for a wallet (icon is stored separately). */
868
877
  const walletPersistKey = (wallet) => `${wallet.id}|${wallet.isConnected ? 1 : 0}|${wallet.name}`;
869
878
  /**
879
+ * What decides a cache write: the persisted fields plus `status`, because a
880
+ * wallet whose kept entry was written while connecting must be rewritten live
881
+ * once its reconnect fails, and `isConnected` reads false on both sides.
882
+ */
883
+ const persistedWalletKey = (wallet) => `${walletPersistKey(wallet)}|${wallet.status}`;
884
+ /**
870
885
  * UI-significant signature of a wallet: the persisted fields plus icon and the
871
886
  * WalletConnect connector's `platforms` array, which drive what the UI shows but
872
887
  * aren't part of the id (icon and platforms can arrive asynchronously after
873
888
  * connect without isConnected flipping).
874
889
  */
875
- const walletUiKey = (wallet) => `${walletPersistKey(wallet)}|${wallet.icon}|${walletPlatforms(wallet)}`;
890
+ const walletUiKey = (wallet) => `${walletPersistKey(wallet)}|${wallet.status}|${wallet.error?.code ?? ""}|${wallet.icon}|${walletPlatforms(wallet)}`;
876
891
  /**
877
892
  * Equality check for KheopskitState to prevent unnecessary emissions. Compares
878
893
  * the UI-significant signature of every wallet and account, in order.
879
894
  */
880
- const statesEqual = (a, b) => a.isHydrating === b.isHydrating && a.wallets.length === b.wallets.length && a.accounts.length === b.accounts.length && a.wallets.every((w, i) => {
895
+ const statesEqual = (a, b) => a.isHydrating === b.isHydrating && a.isDiscovering === b.isDiscovering && a.wallets.length === b.wallets.length && a.accounts.length === b.accounts.length && a.wallets.every((w, i) => {
881
896
  const other = b.wallets[i];
882
- return !!other && walletUiKey(w) === walletUiKey(other);
897
+ return !!other && w.isPlaceholder === other.isPlaceholder && walletUiKey(w) === walletUiKey(other);
883
898
  }) && a.accounts.every((acc, i) => {
884
899
  const other = b.accounts[i];
885
- return !!other && accountChangeKey(acc) === accountChangeKey(other);
900
+ return !!other && acc.isPlaceholder === other.isPlaceholder && accountChangeKey(acc) === accountChangeKey(other);
886
901
  });
887
902
  //#endregion
888
- export { DEFAULT_STORAGE_KEY, KheopskitError, clearAllCachedObservables, createKheopskitStore, getDefaultStore, getHydratedSnapshot, getKheopskit$, getWalletAccountId, getWalletId, isInjectedWallet, isValidAddress, isValidWalletId, isWalletConnectWallet, parseWalletAccountId, parseWalletId, resetAppKitCache, resolveConfig };
903
+ export { DEFAULT_STORAGE_KEY, KheopskitError, clearAllCachedObservables, createKheopskitStore, getDefaultStore, getHydratedSnapshot, getKheopskit$, getWalletAccountId, getWalletId, isInjectedWallet, isValidAddress, isValidWalletId, isWalletConnectWallet, parseWalletAccountId, parseWalletId, resolveConfig };
889
904
 
890
905
  //# sourceMappingURL=index.mjs.map