@kheopskit/core 5.3.0 → 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 -12
  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 +170 -106
  39. package/dist/polkadot.js.map +1 -1
  40. package/dist/polkadot.mjs +163 -99
  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 +30 -6
  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-AxUDtkR-.d.mts +0 -338
  80. package/dist/types-AxUDtkR-.d.ts +0 -338
  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
@@ -0,0 +1,837 @@
1
+ # Migrating to Kheopskit v6
2
+
3
+ v6 tightens the wallet connection contract. `connect()` and `disconnect()` are
4
+ safe to call from UI code, `connect()` rejections are typed, and no emission of
5
+ the state stream shows a disconnected wallet still listing live accounts or a
6
+ connected wallet listing the accounts of a previous connection. v6 also renames
7
+ the store's remembered list, keeps the extension's account order, and changes
8
+ how the Polkadot plugin talks to an extension. Kheopskit calls `enable()`
9
+ itself, keeps the raw object the extension returns, and exposes raw signing on
10
+ every live account.
11
+
12
+ `@kheopskit/core`, `@kheopskit/react` and the repo release tag move to
13
+ **`6.0.0`** together.
14
+
15
+ This guide grows with each v6 change. Each section covers one area.
16
+
17
+ ---
18
+
19
+ ## At a glance
20
+
21
+ | Area | v5 | v6 | See |
22
+ |------|----|----|-----|
23
+ | `connect()` twice in a row | the second call asks the extension again | both calls share one platform attempt; the extension is asked once and each call gets its own promise | [1](#1-connect-and-disconnect) |
24
+ | `connect()` on a connected wallet | rejects `WALLET_ALREADY_CONNECTED` | resolves at once | [1](#1-connect-and-disconnect) |
25
+ | `disconnect()` on a disconnected wallet | rejects `WALLET_NOT_CONNECTED` | resolves at once | [1](#1-connect-and-disconnect) |
26
+ | Cancel a pending `connect()` | not possible | `connect({ signal })` rejects that caller `ABORTED` until the platform answers | [1](#1-connect-and-disconnect) |
27
+ | `disconnect()` while a `connect()` pends | rejects `WALLET_NOT_CONNECTED` | aborts the connect and resolves | [1](#1-connect-and-disconnect) |
28
+ | Refused prompt | the extension's plain `Error` | `KheopskitError` `USER_REJECTED` with `cause` | [1](#1-connect-and-disconnect) |
29
+ | Other connect failure | platform error passed through | `KheopskitError` `CONNECT_FAILED` with `cause` | [1](#1-connect-and-disconnect) |
30
+ | When `connect()` resolves | when the platform call returned | after `kheopskit$` emitted the wallet with `isConnected: true`, with two exceptions; accounts a platform lists asynchronously (Ethereum `eth_accounts`) may follow in a later emission | [1](#1-connect-and-disconnect) |
31
+ | Intermediate emissions | a wallet could show connected with the previous connection's accounts for one frame | a synchronous burst is one emission; no emission shows a disconnected wallet still listing live accounts or a connected wallet listing a previous connection's accounts; cached placeholder accounts stay listed during hydration | [1](#1-connect-and-disconnect) |
32
+ | Account streams | one read per platform, re-run for every wallet of the platform whenever any wallet changes | in `kheopskit$`, one stream per wallet connection, started empty; a sibling's connect or disconnect leaves it alone and its pending request delays nobody | [1](#1-connect-and-disconnect) |
33
+ | Store's remembered list | `KheopskitStoreData.autoReconnect` | `KheopskitStoreData.remembered`; v5 data migrates on read | [2](#2-store) |
34
+ | Edit the remembered list | `store.addEnabledWalletId`, `store.removeEnabledWalletId` | `store.rememberWallet`, `store.forgetWallet` | [2](#2-store) |
35
+ | Custom storage | not possible | `createKheopskitStore({ storage })` | [2](#2-store) |
36
+ | Release a store | not possible | `store.destroy()` | [2](#2-store) |
37
+ | Account order inside a wallet | sorted by name, address or id | the order the extension lists them in | [2](#2-store) |
38
+ | `wallet.extension` on a Polkadot wallet | pjs-signer's `InjectedExtension` (`getAccounts`, `subscribe`, `disconnect`) | the raw `enable()` result, typed `PolkadotInjected` (`accounts`, `signer`) | [3](#3-polkadot-raw-signing-and-the-injected-handle) |
39
+ | Raw signing | `txCreator.signBytes(bytes)` only, hex with `type: "bytes"` | plus `account.signRaw(data, { type? })`, text or bytes | [3](#3-polkadot-raw-signing-and-the-injected-handle) |
40
+ | `ethereum` accounts | signer built with type `ethereum`, 64-byte mock signature | on polkadot-api v3, signer built with `ecdsa`, 65-byte mock signature | [3](#3-polkadot-raw-signing-and-the-injected-handle) |
41
+ | Extension account subscription | kept after disconnect | released on disconnect and on teardown | [3](#3-polkadot-raw-signing-and-the-injected-handle) |
42
+ | `readonly`, `partOfPortfolio` (Talisman, its own origins) | present at runtime, not typed | typed on `PolkadotAccount` | [3](#3-polkadot-raw-signing-and-the-injected-handle) |
43
+ | Wallet status | `isConnected` only | `status` (`disconnected`, `connecting`, `connected`) and `error` | [4](#4-wallet-status-and-reconnect) |
44
+ | `autoReconnect` | `boolean` | `boolean` or a pure function of the wallet | [4](#4-wallet-status-and-reconnect) |
45
+ | Refused reconnect | logged with `console.error`; the wallet stayed remembered | forgets the wallet | [4](#4-wallet-status-and-reconnect) |
46
+ | `getHydratedSnapshot` | `(store, platforms, options)` | `(store, config, options)`; `config` carries `autoReconnect` | [4](#4-wallet-status-and-reconnect) |
47
+ | `config.walletConnect` | raw options object | `walletConnect(options)` from `@kheopskit/core/walletconnect`; a raw object throws `INVALID_CONFIG` | [5](#5-walletconnect-is-a-plugin) |
48
+ | Test kit | none; apps fake `injectedWeb3` themselves | `@kheopskit/core/testing` | [6](#6-test-kit) |
49
+ | Placeholder SDK handles | typed as present, `undefined` at runtime while hydrating | absent from the placeholder type; narrow on `isPlaceholder` | [7](#7-placeholders-carry-isplaceholder) |
50
+ | `connect()` on a placeholder | throws `PendingWalletError` | waits for the live wallet, or rejects `WALLET_PENDING` | [7](#7-placeholders-carry-isplaceholder) |
51
+ | Hydration end | `isHydrating` stays `true` until every cached connected wallet reconnected, capped at six times the grace period | `isHydrating` ends once the grace period has elapsed, discovery has ended and every wallet that hydrated as `connecting` has settled, or at `hydrationTimeout` (default 3000 ms) | [8](#8-discovery-hydration-end-and-persistence) |
52
+ | Discovery at page load | the Polkadot registry was read at 0, 100, 300, 600 and 1100 ms after subscribe only | read again at `load` and 100 ms later; `state.isDiscovering` says whether that read has landed | [8](#8-discovery-hydration-end-and-persistence) |
53
+ | Cache during a slow reconnect | a reconnect slower than the persistence debounce wrote `isConnected: false` and no accounts | a wallet still `connecting` keeps its previous cached entry and accounts | [8](#8-discovery-hydration-end-and-persistence) |
54
+ | React instance and hooks | `createKheopskit` returns a provider and `useWallets`/`useAccounts`; reaching one wallet means reading the whole state; unmounting the provider leaves the pipeline running | `createKheopskit` also returns `kheopskit$` and `store`; `useWallet`, `useAccount` and `useConnect` read one item; unmounting the provider stops the pipeline | [9](#9-kheopskitreact-one-shared-instance-and-wallet-hooks) |
55
+
56
+ ---
57
+
58
+ ## What you can delete
59
+
60
+ Each section ends with the app code it makes redundant. In short:
61
+
62
+ - A page-load gate that waits a fixed delay before concluding that a wallet is
63
+ missing. Wait for `isDiscovering` to turn `false` (section 8).
64
+ - Connect dedup wrappers, such as a `requestOnce` or `connectOnce` map keyed by
65
+ wallet id (section 1).
66
+ - `auditTime(20)` or `debounceTime(20)` placed after `kheopskit$` to skip the
67
+ stale intermediate state (section 1).
68
+ - Refusal detection by `error.name === "Error"` or by message text. Branch on
69
+ `error.code === "USER_REJECTED"` (section 1).
70
+ - A hand-rolled status map and reconnect loop over the remembered list. Read
71
+ `wallet.status` and `wallet.error`, and set `autoReconnect` (section 4).
72
+ - A second `enable()` call made to reach `signer.signRaw`. Call
73
+ `account.signRaw(message)` (section 3).
74
+ - A hand-built `ecdsa` tx creator for ethereum-format accounts (section 3).
75
+ - A second `getKheopskit$` subscription next to the React provider. Subscribe
76
+ to the `kheopskit$` that `createKheopskit` returns (section 9).
77
+ - Test seeds that write the persisted shape to raw storage. Use
78
+ `store.rememberWallet` and `store.setCachedState` with the test kit's
79
+ builders (section 6).
80
+
81
+ ---
82
+
83
+ ## 1. Connect and disconnect
84
+
85
+ ### Removed codes
86
+
87
+ `WALLET_ALREADY_CONNECTED` and `WALLET_NOT_CONNECTED` leave
88
+ `KheopskitErrorCode`. A repeat call is a no-op that resolves.
89
+
90
+ ```diff
91
+ - try {
92
+ - await wallet.connect();
93
+ - } catch (error) {
94
+ - if (error instanceof KheopskitError && error.code === "WALLET_ALREADY_CONNECTED") return;
95
+ - throw error;
96
+ - }
97
+ + await wallet.connect();
98
+ ```
99
+
100
+ ### New codes
101
+
102
+ - `ABORTED`: this `connect()` or `disconnect()` call was abandoned. For
103
+ `connect()`, its `signal` fired before the platform answered, `disconnect()`
104
+ was called while it pended, or the last `kheopskit$` subscriber
105
+ unsubscribed. For either call on an injected wallet, the wallet object came
106
+ from a subscription that had already ended; the call then rejects without
107
+ asking the platform and without changing the remembered list. A
108
+ placeholder's `connect()` rejects the same way and its `disconnect()`
109
+ resolves. The WalletConnect connector is not wrapped and still calls AppKit.
110
+ - `USER_REJECTED`: the user refused the wallet prompt. `cause` holds the
111
+ platform error. It is an EIP-1193 error with `code: 4001` on Ethereum; on
112
+ Polkadot the extension's own `Error`, for Talisman
113
+ `Site <url> has not been authorised for Talisman yet` (observed live for
114
+ both the Reject button and a closed prompt), for polkadot-js
115
+ `Connection request was rejected by the user.` or `... cancelled by the
116
+ user.`, for SubWallet `Rejected` or `Cancelled`.
117
+ - `CONNECT_FAILED`: any other platform failure. `cause` holds the platform
118
+ error.
119
+ - `INVALID_CONFIG`: `getKheopskit$` throws it synchronously when
120
+ `config.walletConnect` is not a plugin created by `walletConnect()` from
121
+ `@kheopskit/core/walletconnect` (see section 5). It is not a rejection and
122
+ carries no `walletId`.
123
+
124
+ Every rejection of `connect()` is a `KheopskitError` carrying `walletId`, and
125
+ so is an `ABORTED` `disconnect()`. A `disconnect()` whose platform call fails
126
+ (Solana, the WalletConnect connector) rejects with the platform's own error.
127
+
128
+ ```ts
129
+ try {
130
+ await wallet.connect({ signal: AbortSignal.timeout(60_000) });
131
+ } catch (error) {
132
+ if (!(error instanceof KheopskitError)) throw error;
133
+ switch (error.code) {
134
+ case "USER_REJECTED":
135
+ case "ABORTED":
136
+ return;
137
+ case "CONNECT_FAILED":
138
+ report(error.cause);
139
+ return;
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### New semantics
145
+
146
+ - **One platform attempt per wallet.** Concurrent `connect()` calls share it
147
+ and the platform is asked once. Each call still gets its own promise. An
148
+ abort through a caller's `signal` before the platform answers rejects that
149
+ caller only, and an abort after the grant changes nothing. A Polkadot
150
+ extension counts as granted only once its account list has been read. The
151
+ attempt is abandoned once every caller that passed a signal has aborted and
152
+ no caller without one joined, on `disconnect()`, or when the last `kheopskit$`
153
+ subscriber unsubscribes; those reject every remaining caller `ABORTED`.
154
+ - **Resolution follows the state.** `connect()` resolves once `kheopskit$` has
155
+ emitted a state in which the wallet has `isConnected: true`. `disconnect()`
156
+ first waits for the platform's disconnect, when the platform has one, then
157
+ resolves once a state shows `isConnected: false`, or once a later
158
+ `connect()`, the wallet leaving the list or the end of the subscription
159
+ makes that emission impossible. A `disconnect()` still waiting for the
160
+ platform settles only when the platform answers, whatever happens to the
161
+ subscription. The state your UI reads already reflects the change when the
162
+ promise settles, with two exceptions. A `connect()` whose wallet leaves the
163
+ list after the grant resolves without such a state. One whose wallet a
164
+ `disconnect()` takes down first rejects `ABORTED`. This is about
165
+ `isConnected`, not the account list. An authorised wallet may share zero
166
+ accounts, and on platforms that list accounts asynchronously (Ethereum
167
+ `eth_accounts`) the accounts may arrive in a later emission. What holds for
168
+ every emission is that none shows a disconnected wallet still listing live
169
+ accounts or a connected wallet listing the accounts of a previous
170
+ connection. During hydration a wallet's cached placeholder accounts stay
171
+ listed whatever its status.
172
+ - **No default timeout.** A Polkadot extension prompt pends until the user
173
+ answers it. Pass `AbortSignal.timeout(ms)` to cap your wait for the platform's
174
+ answer. The wait for the state after a grant is not capped. Aborting ends
175
+ your wait; it cannot close the extension's own prompt. Once no caller waits
176
+ any more, kheopskit drops a handle the extension returns afterwards without
177
+ revoking the extension's grant, and the wallet is neither connected nor
178
+ remembered.
179
+ - **A wallet that disappears.** A `connect()` whose wallet leaves the list (an
180
+ extension removed, a Wallet Standard wallet unregistered) before the platform
181
+ grants it rejects `ABORTED`. Granted first, it resolves and the wallet stays
182
+ remembered. A `disconnect()` waiting for the state resolves, since the wallet
183
+ is no longer connected. One still waiting for the platform settles when the
184
+ platform answers.
185
+ - **`disconnect()` during a pending `connect()`** that the platform has not
186
+ granted aborts the connect (its promise rejects `ABORTED`) and resolves. It
187
+ never waits for the prompt. A `disconnect()` that completes while a granted
188
+ `connect()` still waits for `kheopskit$` to show it rejects that connect
189
+ `ABORTED`. If the platform's disconnect is still pending or refuses, the
190
+ connect resolves once the state shows the wallet connected (today only
191
+ Solana has a platform disconnect). In the other direction, `connect()`
192
+ resolves a `disconnect()` still waiting for the state, since that disconnect
193
+ did complete.
194
+ - **`disconnect()` forgets first.** The wallet leaves the remembered list
195
+ before the extension is asked, so a disconnect still pending when the page
196
+ unloads does not reconnect on the next load. If the extension refuses, the
197
+ wallet is remembered again and stays connected. A `connect()` queued behind
198
+ a pending `disconnect()` rejects `ABORTED` as soon as its `signal` fires or
199
+ the last `kheopskit$` subscriber unsubscribes, even if the extension never
200
+ answers.
201
+ - **Persistence is best-effort.** A storage write that throws
202
+ (`QuotaExceededError`, a denied write) keeps the in-memory state while the
203
+ store is observed (an unobserved store re-reads storage), and neither
204
+ `connect()` nor `disconnect()` rejects because of it.
205
+ - **Wallet objects are stable.** An emission that only changes accounts keeps
206
+ the same wallet object, with the same `connect` and `disconnect` functions,
207
+ so effects and memos keyed on them do not re-run.
208
+ - **WalletConnect still resolves when the modal opens.** The connector's
209
+ `connect()` opens the modal and resolves once it is open; `isConnected`
210
+ follows when the user approves.
211
+ - **No throttle on the stream.** `throttleTime(16)` is replaced by a microtask
212
+ coalesce. A synchronous burst (wallets, then accounts) becomes one emission,
213
+ so no emission shows a disconnected wallet still listing live accounts. A
214
+ wallet's live accounts leave the state in the same emission that shows it
215
+ disconnected,
216
+ even while another wallet's `eth_accounts` request is pending. Accounts a
217
+ platform lists asynchronously (Ethereum `eth_accounts`) may arrive in a later
218
+ emission. The first emission can arrive one microtask after subscribe.
219
+ `KheopskitProvider` still paints the cached snapshot synchronously.
220
+ - **One account stream per wallet connection.** v5 read every wallet of a
221
+ platform together and re-read them all whenever any wallet connected or
222
+ disconnected, so one wallet's slow `eth_accounts` held back the others, a
223
+ reconnecting wallet could show the accounts of its previous connection until
224
+ a sibling answered, and `connect()` could resolve on that emission. v6 starts
225
+ one stream per wallet when it connects, empty, and ends it when the wallet
226
+ disconnects. No emission shows a connected wallet listing a previous
227
+ connection's accounts, a sibling's pending request delays nobody, a sibling's
228
+ connect or disconnect neither re-reads nor resubscribes a wallet's accounts,
229
+ and a Polkadot extension's `accounts.subscribe` runs once per connection.
230
+ One exception. The first value of a platform plugin's `getAccounts$`, called
231
+ directly with wallets that are already connected, waits until each of them
232
+ has answered. `kheopskit$` subscribes before any wallet is connected, so it
233
+ never waits.
234
+
235
+ ### What to delete in your app
236
+
237
+ - Dedup wrappers around `connect()`, such as a `requestOnce` or `connectOnce`
238
+ map keyed by wallet id.
239
+ - `auditTime(20)` or `debounceTime(20)` placed after `kheopskit$` to skip the
240
+ stale intermediate state.
241
+ - Treating `WALLET_ALREADY_CONNECTED` as success and swallowing
242
+ `WALLET_NOT_CONNECTED`.
243
+ - Refusal detection by `error.name === "Error"` or by message text. Branch on
244
+ `error.code === "USER_REJECTED"`.
245
+ - Cancel flows that forget the wallet and disconnect a late approval. Pass a
246
+ `signal` and abort it. When a caller without a signal shares the attempt
247
+ (auto-reconnect does), a late approval still connects the wallet, so call
248
+ `disconnect()` if the user must end up disconnected.
249
+
250
+ ---
251
+
252
+ ## 2. Store
253
+
254
+ ### Renames
255
+
256
+ | v5 | v6 |
257
+ |----|----|
258
+ | `KheopskitStoreData.autoReconnect` | `KheopskitStoreData.remembered` |
259
+ | `store.addEnabledWalletId(walletId)` | `store.rememberWallet(walletId)` |
260
+ | `store.removeEnabledWalletId(walletId)` | `store.forgetWallet(walletId)` |
261
+
262
+ The list holds the injected wallets the user connected. Kheopskit adds a
263
+ wallet on every successful connect, whatever `config.autoReconnect` says, and
264
+ removes it when a connected wallet is disconnected. The WalletConnect
265
+ connector is never added; AppKit restores its own session. The `autoReconnect`
266
+ config option keeps its name and meaning. It decides whether the remembered
267
+ wallets reconnect on load.
268
+
269
+ The compact cookie format is unchanged. The list stays under the key `r`.
270
+
271
+ ### Existing data migrates on read
272
+
273
+ Data written by v5 under `autoReconnect` is read as `remembered`, from
274
+ `localStorage`, from cookies (compact or legacy verbose JSON) and from changes
275
+ made by other tabs. The old key disappears on the next write. Users keep their
276
+ remembered wallets, and test fixtures that write `{ autoReconnect: [...] }`
277
+ into storage keep working. A literal typed as `KheopskitStoreData` must rename
278
+ the key to `remembered`, since `autoReconnect` is no longer a field of it.
279
+
280
+ ### New: `storage` option and `destroy()`
281
+
282
+ `createKheopskitStore({ storage })` persists into any `SyncableStorage` (now
283
+ exported from `@kheopskit/core`). It takes precedence over `ssrCookies` and
284
+ `localStorage`.
285
+
286
+ The store holds its cross-tab subscription only while `store.observable` is
287
+ observed. Explicit owners call `store.destroy()`, which completes
288
+ `store.observable` and so ends that subscription.
289
+
290
+ ### Account order inside a wallet
291
+
292
+ Accounts are still grouped by platform (Polkadot, Ethereum, Solana), then by
293
+ wallet (Talisman first, then by wallet name). Inside a wallet they now keep the
294
+ order the extension reported, instead of being sorted by name, address or id.
295
+
296
+ ### Account renames
297
+
298
+ Renaming an account in the extension now emits a new state and updates the
299
+ cached account name. In v5 the rename was ignored until the account list
300
+ changed for another reason.
301
+
302
+ ### What downstream apps delete
303
+
304
+ - signet: the `remembered$` read of `store.observable.autoReconnect` and the
305
+ `store.removeEnabledWalletId` call in the extension domain. If the code stays
306
+ for now, rename them to `remembered` and `forgetWallet`. Test seeds that
307
+ write `{ autoReconnect: [...] }` into storage keep working through the
308
+ migration.
309
+ - talisman-web: the `walletOrder` re-sort, which restored the extension's
310
+ account order inside a wallet. The `injectionOrder` re-sort orders wallets by
311
+ injection order; kheopskit still orders wallets Talisman first, then by name.
312
+ Delete it if that order is acceptable.
313
+
314
+ ---
315
+
316
+ ## 3. Polkadot: raw signing and the injected handle
317
+
318
+ ### `extension` is the raw `enable()` result
319
+
320
+ `PolkadotInjectedWallet.extension` is no longer pjs-signer's
321
+ `InjectedExtension`. It is the object `window.injectedWeb3[id].enable(appName)`
322
+ resolved to, typed structurally as `PolkadotInjected`:
323
+
324
+ ```ts
325
+ type PolkadotInjected = {
326
+ accounts: {
327
+ get(): Promise<PolkadotInjectedAccount[]>;
328
+ subscribe(cb: (accounts: PolkadotInjectedAccount[]) => void): () => void;
329
+ };
330
+ signer: { signPayload: SignPayload; signRaw(payload): Promise<{ id; signature }> };
331
+ metadata?: unknown;
332
+ provider?: unknown;
333
+ };
334
+ ```
335
+
336
+ `getAccounts()`, `subscribe()` and `disconnect()` are gone from it. Read
337
+ accounts from `state.accounts`; kheopskit owns the subscription. As in v5,
338
+ `connect()` reads `accounts.get()` before it resolves, so the emission that
339
+ first shows the wallet connected already lists its accounts; the list then
340
+ follows `accounts.subscribe`. The field is `undefined` until the wallet is
341
+ connected, so also while it connects, and a placeholder (section 7) has none.
342
+
343
+ ```diff
344
+ - const accounts = wallet.extension?.getAccounts() ?? [];
345
+ + const accounts = state.accounts.filter((a) => a.walletId === wallet.id);
346
+ ```
347
+
348
+ ### `account.signRaw`
349
+
350
+ Every Polkadot account that is not a placeholder (section 7) has
351
+ `signRaw(data: string | Uint8Array, options?: { type?: "bytes" | "payload" })`.
352
+ It resolves the hex signature. A string is sent as text and a `Uint8Array` as
353
+ `0x` hex. Injected accounts go through the extension's `signer.signRaw`,
354
+ WalletConnect accounts through `polkadot_signMessage`.
355
+
356
+ With no `options.type`, a string is sent as text with `type: "payload"` and a
357
+ `Uint8Array` as `0x` hex with `type: "bytes"`. An explicit `options.type` is
358
+ sent as given for either input, except over WalletConnect, which sends no
359
+ `type`. A hex-looking string is sent as text; pass a `Uint8Array` for bytes.
360
+ From their sources: Talisman, polkadot.js, SubWallet and Nova ignore `type`
361
+ and decode `data` by shape; Enkrypt reads it as the encoding of `data`,
362
+ `"payload"` for UTF-8 text and `"bytes"` for hex, so it needs the value the
363
+ derived default sends. Only Talisman was driven live.
364
+
365
+ Talisman 3.11.0 opens its Sign In screen when the text parses as a SIWS
366
+ message and its generic Sign Request screen otherwise, hex included. The
367
+ screen follows the text `data`, not `type`. Live receipts with the same text
368
+ and both values show the same screen. The SIWS `Version` line must read `1.0.0`;
369
+ Talisman's parser rejects `1` and falls back to the generic screen.
370
+
371
+ ```ts
372
+ if (account.isPlaceholder) return;
373
+ const signature = await account.signRaw(siwsMessage);
374
+ ```
375
+
376
+ On a live account, `txCreator.signBytes` still sends hex with
377
+ `type: "bytes"`. Reading `txCreator` needs the same `isPlaceholder` narrowing
378
+ ([section 7](#7-placeholders-carry-isplaceholder)).
379
+
380
+ ### `ethereum` accounts sign as `ecdsa`
381
+
382
+ For an account whose `type` is `"ethereum"`, kheopskit builds the `txCreator`
383
+ with the `ecdsa` signature type. On polkadot-api v2, `getPolkadotSignerFromPjs`
384
+ ignores the signature type, so this change does not reach `polkadotSigner`.
385
+ pjs-signer's own map knows `ed25519`, `sr25519` and `ecdsa` only, so an
386
+ `ethereum` type got a 64-byte mock signature where ecdsa needs 65. Fee
387
+ estimation and dry runs for ethereum-format accounts no longer use the wrong
388
+ length.
389
+
390
+ ### The account subscription is released
391
+
392
+ v5 subscribed to the extension's account list and never unsubscribed. v6
393
+ unsubscribes when the wallet disconnects and when the last `kheopskit$`
394
+ subscriber leaves.
395
+
396
+ ### Untyped accounts are kept
397
+
398
+ Accounts the extension lists without a `type` are now kept as `sr25519`. v5
399
+ went through pjs-signer, which dropped them. `polkadot({ accountTypes })` is
400
+ the only type filter.
401
+
402
+ ### `connect()` rejects an incomplete extension
403
+
404
+ `connect()` rejects with `CONNECT_FAILED`, and the wallet stays disconnected
405
+ and unremembered, when the `enable()` result lacks `accounts.get`,
406
+ `accounts.subscribe`, `signer.signPayload` or `signer.signRaw`, or when
407
+ `accounts.get()` rejects. `cause` holds an `Error` whose message names the
408
+ extension, for example `Unsupported extension: "talisman"`, or the extension's
409
+ own error. An `accounts.get()` rejection whose message matches a known refusal
410
+ (section 1) maps to `USER_REJECTED` instead, and a reconnect that fails this
411
+ way forgets the wallet. An account kheopskit cannot build is skipped and the
412
+ wallet's other accounts are listed.
413
+
414
+ ### `readonly` and `partOfPortfolio`
415
+
416
+ Talisman adds these two fields to the accounts it shares with its own dapp
417
+ origins: `readonly` marks a watch-only account that cannot sign, and
418
+ `partOfPortfolio` a watch-only account that is part of the user's portfolio.
419
+ They are typed on `PolkadotInjectedAccount` and `PolkadotAccount` as optional
420
+ booleans and are absent for other origins and other wallets.
421
+
422
+ ### polkadot-api v2
423
+
424
+ v2 still works. Apart from the `isPlaceholder` narrowing of
425
+ [section 7](#7-placeholders-carry-isplaceholder), apps on it change nothing in
426
+ their signing code. An un-narrowed `account.polkadotSigner` is possibly
427
+ `undefined` and no longer compiles where a signer is required. The account
428
+ keeps carrying `polkadotSigner` instead of `txCreator`. The fourth
429
+ factory argument (the signature type) is ignored by `getPolkadotSignerFromPjs`
430
+ at runtime.
431
+
432
+ ### What to delete in your app
433
+
434
+ - A second `enable()` call made to reach `signer.signRaw` for a Sign-In With
435
+ Substrate message. Call `account.signRaw(message)`.
436
+ - A hand-built `ecdsaTxCreator` for ethereum-format accounts, and a
437
+ `signatureTypeOf` helper that maps `ethereum` to `ecdsa`.
438
+ - A memoised second `enable()` and a `pjsSignerOf` helper that rebuilds a pjs
439
+ signer from the raw extension. Use `account.txCreator`.
440
+ - A `TalismanInjectedAccount` cast that adds `readonly` and `partOfPortfolio`.
441
+ They are on `PolkadotAccount`.
442
+
443
+ ---
444
+
445
+ ## 4. Wallet status and reconnect
446
+
447
+ ### New fields
448
+
449
+ | Field | Type | Meaning |
450
+ |-------|------|---------|
451
+ | `wallet.status` | `"disconnected" \| "connecting" \| "connected"` | `connecting` while a `connect()` call or an auto-reconnect waits for the platform. On the WalletConnect connector, while `connect()` opens the modal. |
452
+ | `wallet.error` | `KheopskitError \| undefined` | The last `connect()` or auto-reconnect that the platform refused (`USER_REJECTED`) or failed (`CONNECT_FAILED`, or `FEATURE_NOT_SUPPORTED` for a Wallet Standard wallet without `standard:connect`). Cleared when the next attempt starts. An aborted attempt sets nothing. |
453
+ | `wallet.isConnected` | `boolean` | Unchanged, now defined as `status === "connected"`. No emission shows `isConnected: true` with another status. |
454
+
455
+ Test code that builds wallet objects by hand needs `status`.
456
+
457
+ ### `autoReconnect` takes a function
458
+
459
+ ```ts
460
+ autoReconnect: boolean | ((wallet: BaseWallet) => boolean)
461
+ ```
462
+
463
+ The option governs injected wallets. The function is called for each cached
464
+ connected injected wallet on the first frame, then for each remembered wallet
465
+ with its live object before each attempt. It must be synchronous and pure. The
466
+ server and the client evaluate it for the same input and both must agree, or
467
+ the markup diverges. It receives the `BaseWallet` fields only, so key on
468
+ `wallet.id`.
469
+
470
+ Every successful `connect()` of an injected wallet is still recorded in the
471
+ remembered list, whatever `autoReconnect` says.
472
+
473
+ ### Refused reconnects are forgotten
474
+
475
+ A reconnect opens no connect prompt while the site stays authorised in the
476
+ extension. When the authorisation was revoked the extension prompts once; a
477
+ refusal forgets the wallet (it leaves the remembered list), so the next load
478
+ stays silent. Any other failure sets `wallet.error`, keeps the wallet
479
+ remembered and is retried on the next change to the wallet list, 3 attempts in
480
+ total. A manual `connect()` or `disconnect()` on a remembered wallet ends its
481
+ automatic reconnect for the session. A `disconnect()` while the automatic
482
+ reconnect still waits for the extension aborts it without forgetting the
483
+ wallet, so the next load reconnects it. Automatic reconnect also ends when the
484
+ wallet disappears mid-attempt; the wallet stays remembered for the next load.
485
+ Nothing is written to `console.error` any more; `debug: true` logs the failure
486
+ with `console.debug`.
487
+
488
+ ### First frame after a reload
489
+
490
+ A cached connected wallet hydrates as `status: "connecting"`, `isConnected:
491
+ false`, with its cached accounts as placeholders, and stays so until its
492
+ reconnect settles. A wallet the predicate rejects hydrates as `disconnected`
493
+ without its cached accounts. With `autoReconnect: false` every cached injected
494
+ wallet hydrates as `disconnected`. The WalletConnect row follows AppKit's own
495
+ session restore, whatever `autoReconnect` says, and hydrates `connecting` while
496
+ AppKit restores. Nothing flashes from connected to disconnected.
497
+
498
+ ### `getHydratedSnapshot` signature
499
+
500
+ `getHydratedSnapshot`, a public export of `@kheopskit/core`, takes the config
501
+ instead of the platform list:
502
+
503
+ ```diff
504
+ - getHydratedSnapshot(store, config.platforms, options)
505
+ + getHydratedSnapshot(store, config, options)
506
+ ```
507
+
508
+ The second argument is `Pick<KheopskitConfig, "platforms" | "autoReconnect">`,
509
+ so the snapshot applies the same `autoReconnect` boolean or predicate as the
510
+ live stream. A cached connected wallet the predicate rejects hydrates as
511
+ `disconnected` without its cached accounts.
512
+ `KheopskitProvider` passes it to both its server and client snapshots.
513
+
514
+ ### What downstream apps delete
515
+
516
+ - A hand-rolled status map (`intents`, `statusOf`) keyed by wallet id. Read
517
+ `wallet.status` and `wallet.error`.
518
+ - A reconnect loop over the remembered list (`reconnectRemembered$`) and its
519
+ forget-on-refusal branch. Set `autoReconnect: true`.
520
+ - A "reconnect only this wallet" rule implemented by reading storage before
521
+ calling `connect()`. Pass it as the `autoReconnect` function.
522
+
523
+ ---
524
+
525
+ ## 5. WalletConnect is a plugin
526
+
527
+ WalletConnect moved to its own entry point, `@kheopskit/core/walletconnect`.
528
+ `config.walletConnect` takes the plugin that entry's `walletConnect()` returns.
529
+ A dapp that does not import the entry bundles none of kheopskit's AppKit code
530
+ and none of the AppKit chunks.
531
+
532
+ ```ts
533
+ // v5
534
+ import { getKheopskit$ } from "@kheopskit/core";
535
+
536
+ getKheopskit$({
537
+ platforms: [polkadot()],
538
+ walletConnect: { projectId, metadata, networks },
539
+ });
540
+
541
+ // v6
542
+ import { getKheopskit$ } from "@kheopskit/core";
543
+ import { walletConnect } from "@kheopskit/core/walletconnect";
544
+
545
+ getKheopskit$({
546
+ platforms: [polkadot()],
547
+ walletConnect: walletConnect({ projectId, metadata, networks }),
548
+ });
549
+ ```
550
+
551
+ | v5 | v6 |
552
+ |----|----|
553
+ | `walletConnect: { projectId, metadata, networks, ... }` | `walletConnect: walletConnect({ projectId, metadata, networks, ... })`. A raw object is a type error, and `getKheopskit$` throws a `KheopskitError` with the new code `INVALID_CONFIG` |
554
+ | `import { resetAppKitCache } from "@kheopskit/core"` | `import { resetAppKitCache } from "@kheopskit/core/walletconnect"` |
555
+ | `WalletConnectConfig` from `@kheopskit/core` | `WalletConnectOptions` from `@kheopskit/core/walletconnect` |
556
+ | `relayUrl` option | removed. It was never passed to AppKit |
557
+
558
+ `WalletConnectWallet`, `isWalletConnectWallet`, `WalletConnectMetadata` and
559
+ `AppKitInstance` stay in `@kheopskit/core`. The Polkadot `appName` fallback
560
+ still reads the plugin's `metadata.name`.
561
+
562
+ Dapps that never set `walletConnect` need no change for this section. Their
563
+ bundle shrinks. A Polkadot-only Vite app built against 5.3.0 and against
564
+ 6.0.0, with `@reown/appkit` installed (as it is when another dependency pulls it in):
565
+
566
+ | | 5.3.0 | 6.0.0 |
567
+ |---|---|---|
568
+ | JS chunks | 67 | 1 |
569
+ | JS total, gzip -9 | 465,575 B | 46,804 B |
570
+ | Entry chunk, gzip -9 | 47,739 B | 46,822 B |
571
+
572
+ Without `@reown/appkit` installed, 5.3.0 builds a stub chunk that throws
573
+ `Could not resolve "@reown/appkit/core"`; 6.0.0 builds none.
574
+
575
+ ---
576
+
577
+ ## 6. Test kit
578
+
579
+ v6 adds `@kheopskit/core/testing` (see the
580
+ [core README](README.md#testing)). It replaces the `injectedWeb3` fakes that
581
+ signet and talisman-web keep in `domains/extension/fakeInjectedWeb3.ts`.
582
+
583
+ | App helper | Kit |
584
+ |------------|-----|
585
+ | `fakeWallet(accounts)` in an `injectedWeb3` map passed to `vi.stubGlobal` | `createFakePolkadotExtension({ name, accounts })`, which installs itself |
586
+ | `publish(accounts)` | `setAccounts(accounts)` |
587
+ | `enable.mock.calls` | `enableCalls()`, one app name per call |
588
+ | the `signRaw` mock (signet) and `signer.signRaw` (talisman-web) | `signRawCalls()`; signatures resolve `FAKE_SIGNATURE` |
589
+ | `heldWallet(accounts)` with `approve()` and `refuse()` (signet) | `{ enable: "hold" }` with `resolveEnable()` and `rejectEnable()`, which rejects with `new Error("Rejected")` |
590
+ | `refusingWallet()` (talisman-web) | `{ enable: "reject" }` |
591
+ | `enableDelay.ms` (signet) | `{ enable: "hold" }`, then `resolveEnable()` after advancing the timers |
592
+ | `localStorage.setItem("kheopskit", JSON.stringify(cache))` (signet's `load`) | `store.rememberWallet(id)` and `store.setCachedState([cachedWalletOf(id, name)], [cachedAccountOf(wallet, address, name)])` |
593
+ | `vi.doMock("@kheopskit/core")` wrapping `getKheopskit$` in `takeUntil(disposed$)` (signet's `load`) | unsubscribe from `getKheopskit$` and `store.destroy()`; unsubscribing stops kheopskit's timers |
594
+ | `vi.unstubAllGlobals()` | `uninstall()` on each fake |
595
+
596
+ Two things stay in the apps. The kit has no hook to make `disconnect` fail,
597
+ so signet's `disconnectFailure` keeps its `getKheopskit$` wrapper. An extension
598
+ that returns a broken `enable()` result, such as signet's
599
+ `broken.enable.mockResolvedValue(...)`, stays a hand-written entry next to the
600
+ fakes on `window.injectedWeb3`.
601
+
602
+ An app that calls `getKheopskit$` without a `store` persists to
603
+ `localStorage`. Seed it with `createKheopskitStore()`, then call `destroy()` on
604
+ that seeding store. The seeded data uses `remembered`, not v5's `autoReconnect`.
605
+
606
+ ---
607
+
608
+ ## 7. Placeholders carry `isPlaceholder`
609
+
610
+ ### The union
611
+
612
+ Every wallet and account in `kheopskit$` and in the hydrated snapshot carries
613
+ `isPlaceholder`. It is `true` for an item restored from the cache that its live
614
+ counterpart has not replaced yet, and `false` otherwise. Each platform type is a
615
+ union on it:
616
+
617
+ | Type | Absent on a placeholder |
618
+ |------|-------------------------|
619
+ | `PolkadotAccount` | `txCreator` (polkadot-api v3) or `polkadotSigner` (v2), `signRaw` |
620
+ | `EthereumAccount` | `client` |
621
+ | `SolanaAccount` | `signer`, `getSigner` |
622
+ | `PolkadotInjectedWallet` | `extension`, `sourceId` |
623
+ | `EthereumInjectedWallet` | `provider`, `sourceId` |
624
+ | `SolanaInjectedWallet` | `wallet`, `chains`, `sourceId` |
625
+ | `WalletConnectWallet` | `appKit` |
626
+
627
+ In v5 these fields were typed as present and were `undefined` at runtime until
628
+ hydration ended. In v6 the placeholder type marks them absent, so reading one
629
+ without narrowing yields a possibly-`undefined` value, and using it as present
630
+ (calling it, passing it where the handle is required) fails to compile until
631
+ you narrow:
632
+
633
+ ```ts
634
+ // v5: compiles, throws while hydrating
635
+ await account.txCreator.signBytes(bytes);
636
+ // v6
637
+ if (account.isPlaceholder) return;
638
+ await account.txCreator.signBytes(bytes);
639
+ ```
640
+
641
+ A placeholder wallet is never connected (`isConnected: false`). A cached wallet
642
+ that kheopskit has already discovered is not a placeholder. It carries its live
643
+ `connect()` and `disconnect()` and shows `status: "connecting"` while its
644
+ reconnect is pending. A Polkadot wallet's `extension` stays `undefined` until
645
+ that reconnect succeeds. Its
646
+ cached accounts stay placeholders until the live accounts replace them.
647
+
648
+ Plugins never set the flag. Kheopskit sets it on every item it emits, so a
649
+ custom plugin's wallet and account types type it as `isPlaceholder?: false`.
650
+
651
+ ### `connect()` on a placeholder
652
+
653
+ | | v5 | v6 |
654
+ |---|----|----|
655
+ | `connect()` | throws `PendingWalletError` synchronously | waits for the live wallet with the same id and calls its `connect(options)`; rejects `WALLET_PENDING` if hydration ends without it, `ABORTED` if `signal` fires or the subscription ends first |
656
+ | `disconnect()` | throws `PendingWalletError` synchronously | resolves at once |
657
+
658
+ `PendingWalletError` is gone. On a bare `getHydratedSnapshot()` result, which
659
+ has no live stream behind it, `connect()` rejects `WALLET_PENDING`.
660
+
661
+ ### What downstream apps delete
662
+
663
+ - Guards that read an SDK field behind `!isHydrating` and cast it to present.
664
+ Narrow on `isPlaceholder` instead; `isHydrating` can still disable the
665
+ button.
666
+ - A `hasAccountsFrom`-style check that counts cached accounts as usable.
667
+ Filter on `!account.isPlaceholder`.
668
+ - `try`/`catch` around a placeholder's `connect()` for `PendingWalletError`.
669
+
670
+ ---
671
+
672
+ ## 8. Discovery, hydration end and persistence
673
+
674
+ ### New state field: `isDiscovering`
675
+
676
+ `KheopskitState.isDiscovering` is new and required. It is `true` until the page
677
+ has fired `load` and kheopskit has read `window.injectedWeb3` again, so the
678
+ emission that turns it `false` already lists every Polkadot extension injected
679
+ by then. It is `false` from the first emission when you subscribe after
680
+ `load`, and always `true` on the server.
681
+
682
+ Kheopskit now reads the Polkadot registry at `load` and 100 ms after `load`, on
683
+ top of the reads at subscribe and 100, 300, 600 and 1100 ms after it. An
684
+ extension that injects after the 1100 ms read but before `load` is listed
685
+ without a reload. Nothing reads the registry after the last of these reads. A
686
+ wallet that injects after `load` is listed only when a read still runs after
687
+ it, the one 100 ms after `load` or a subscribe read still pending, so its
688
+ discovery is not guaranteed.
689
+
690
+ A state literal or test mock typed as `KheopskitState` must add the field:
691
+
692
+ ```diff
693
+ const state: KheopskitState = {
694
+ wallets: [],
695
+ accounts: [],
696
+ config,
697
+ isHydrating: false,
698
+ + isDiscovering: false,
699
+ };
700
+ ```
701
+
702
+ Hydration now also waits for discovery. With a cache, `isHydrating` stays
703
+ `true` until `load` or `hydrationTimeout`, whichever comes first. A
704
+ `hydrationGracePeriod` of `0` still turns hydration off in `kheopskit$`. Under
705
+ `KheopskitProvider`, the first render still shows the cached placeholders with
706
+ `isHydrating: true` until the first emission.
707
+
708
+ ### New option: `hydrationTimeout`
709
+
710
+ ```ts
711
+ hydrationTimeout: number // default 3000
712
+ ```
713
+
714
+ The upper bound in milliseconds on `isHydrating`. `hydrationGracePeriod` is now
715
+ only the minimum time placeholders are shown; `0` still disables them in
716
+ `kheopskit$`, though not in `KheopskitProvider`'s first render.
717
+ `hydrationTimeout` replaces the old cap of six times `hydrationGracePeriod` and
718
+ wins over a longer grace period, so an app that raised `hydrationGracePeriod`
719
+ sets `hydrationTimeout` as well. To keep the old cap, set it to six times the
720
+ grace period.
721
+
722
+ ### New end rule
723
+
724
+ `isHydrating` turns `false` once `hydrationGracePeriod` has elapsed, discovery
725
+ has ended (`isDiscovering` is `false`), and every wallet that hydrated as
726
+ `connecting` has settled. A wallet has settled when it is absent from the
727
+ discovered wallets, connected once its account list has been read (an empty
728
+ list counts) or connected with no cached accounts, disconnected with `error`
729
+ set, or disconnected and not remembered when hydration began. At
730
+ `hydrationTimeout` hydration ends whatever is pending, and a wallet whose
731
+ reconnect is still pending stays `connecting`.
732
+
733
+ "Its account list has been read" relies on a signal the built-in plugins send.
734
+ A custom `KheopskitPlatform` plugin, or a wrapper that pipes a built-in
735
+ plugin's `getAccounts$`, does not send it. Such a wallet settles on its first
736
+ listed account, as in 5.x, and a reconnected wallet with cached accounts that
737
+ now lists none holds hydration until `hydrationTimeout`.
738
+
739
+ - Discovery ends only after the page fires `load`, so a page with a cache
740
+ hydrates until `load` or `hydrationTimeout`, whichever comes first, even
741
+ when no wallet reconnects.
742
+ - A wallet that reconnects without sharing any account with the site settles
743
+ as soon as it answers; its cached accounts leave the list when hydration
744
+ ends.
745
+ - A failed reconnect ends that wallet's wait, even when the reconnect policy
746
+ still permits a retry. A retry runs only on the next change to the wallet
747
+ list (a wallet discovered or removed, or any wallet's status changing), which
748
+ may never happen, so waiting
749
+ for it would hold most failed reconnects until `hydrationTimeout`. A retry
750
+ that does run shows on the live wallet's `status`, not as a placeholder.
751
+ - A remembered wallet the user disconnects during or right after its reconnect
752
+ holds hydration, and shows its cached accounts, until `hydrationTimeout`.
753
+ - A prompt the extension opens for a revoked site holds hydration until the
754
+ user answers or `hydrationTimeout` elapses. After a refusal the wallet is
755
+ forgotten, so the next load opens no prompt.
756
+
757
+ In practice, with the extension removed or with `autoReconnect: false`,
758
+ hydration no longer runs to 3 s. It ends once the grace period has elapsed and
759
+ the page has loaded, or at `hydrationTimeout` if that comes first.
760
+
761
+ ### Persistence guarantee
762
+
763
+ The cache is written after hydration ends, one second after the state
764
+ stabilises, as before. When it is written, a wallet whose `status` is
765
+ `connecting` keeps its previous cached entry and its previous cached accounts;
766
+ every other wallet is written from live data. A reconnect slower than the
767
+ debounce no longer overwrites a connected cache with `isConnected: false` and
768
+ no accounts, and a wallet the user connects while another wallet's prompt is
769
+ still open is persisted.
770
+
771
+ ### What downstream apps delete
772
+
773
+ - A wait for `isHydrating` padded for the 3 s cap, or a `hydrationGracePeriod:
774
+ 0` chosen to avoid it. The default grace period ends hydration when the facts
775
+ allow.
776
+ - A guard that re-seeds the cache after a reload because kheopskit could write
777
+ it empty.
778
+
779
+ ---
780
+
781
+ ## 9. `@kheopskit/react`: one shared instance and wallet hooks
782
+
783
+ `useWallets`, `useAccounts` and `KheopskitProvider` keep their signatures.
784
+ Their results are v6 state, so they change the way sections 4, 7 and 8 describe.
785
+ A mock of `useWallets()` adds `isDiscovering`, a wallet built by hand adds
786
+ `status`, and code that reads an SDK handle or calls `signRaw` narrows on
787
+ `isPlaceholder` first:
788
+
789
+ ```diff
790
+ const mock: ReturnType<typeof useWallets> = {
791
+ config,
792
+ wallets: [],
793
+ accounts: [],
794
+ isHydrating: false,
795
+ + isDiscovering: false,
796
+ };
797
+
798
+ const { accounts } = useWallets<typeof platforms>();
799
+ const account = accounts.find((a) => a.id === accountId);
800
+ - if (account?.platform === "polkadot")
801
+ + if (account?.platform === "polkadot" && !account.isPlaceholder)
802
+ await account.txCreator.signBytes(bytes);
803
+ ```
804
+
805
+ ### Context shape (internal)
806
+
807
+ The provider's context now carries an external store (`subscribe`,
808
+ `getSnapshot`, `getServerSnapshot`) instead of `{ state }`. Only code that read
809
+ `KheopskitContext` directly sees the change. The package does not export it.
810
+
811
+ ### Unmounting the provider stops kheopskit
812
+
813
+ In v5, unmounting `KheopskitProvider` left its `kheopskit$` subscription open,
814
+ so the pipeline kept running and persisting state changes. In v6 the provider subscribes while
815
+ it is mounted and unsubscribes on unmount, and the pipeline stops once no
816
+ subscriber is left.
817
+
818
+ ### Additions
819
+
820
+ | Addition | What it does |
821
+ |----------|--------------|
822
+ | `createKheopskit(config, { store? })` returns `kheopskit$` and `store` | the stream its provider subscribes to and the store that stream persists into; code outside React subscribes to the same pipeline |
823
+ | `useWallet(walletId)`, `useAccount(accountId)` | one item or `undefined`; re-render only when that item changes; also returned by `createKheopskit`, typed to its platforms |
824
+ | `useConnect(walletId)` | `{ connect, disconnect, status, error }`; aborts a pending `connect()` on unmount or when `walletId` changes; rejects `ABORTED` a `connect()` that starts after either |
825
+ | wallet actions act on the instance's pipeline | `connect` and `disconnect` from `useWallet` and `useWallets`, and `disconnect` from `useConnect`, act on the instance's pipeline when they are called, while any provider of the instance or any `kheopskit$` subscriber holds it, and wait for its first emission after an `<Activity>` reveal restarts it; called once nothing holds the pipeline, they reject `ABORTED` without restarting it. `useConnect`'s `connect` is component-scoped. It rejects `ABORTED` once its component has unmounted or been hidden by `<Activity>`, even while something else holds the pipeline |
826
+ | `store` prop on `KheopskitProvider` | the store the provider persists into, such as a memory store in tests |
827
+
828
+ See the [React README](../react/README.md#one-instance-for-react-and-rxjs-code).
829
+
830
+ ### What downstream apps delete
831
+
832
+ - A second `getKheopskit$` call next to the provider for RxJS code. Subscribe
833
+ to the `kheopskit$` returned by `createKheopskit`.
834
+ - Components that read `useWallets()` only to find one wallet or account. Use
835
+ `useWallet` or `useAccount`.
836
+ - An `AbortController` kept in a ref to cancel `connect()` on unmount. Use
837
+ `useConnect`.