@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.
- package/CHANGELOG.md +426 -0
- package/MIGRATING_TO_V6.md +837 -0
- package/README.md +628 -11
- package/dist/ethereum.d.mts +12 -6
- package/dist/ethereum.d.ts +12 -6
- package/dist/ethereum.js +13 -10
- package/dist/ethereum.js.map +1 -1
- package/dist/ethereum.mjs +7 -4
- package/dist/ethereum.mjs.map +1 -1
- package/dist/{iconCache-pP2ES4pJ.mjs → iconCache-C-U3IEbP.mjs} +21 -40
- package/dist/iconCache-C-U3IEbP.mjs.map +1 -0
- package/dist/{iconCache-DUtkJlaX.js → iconCache-FR4iy6go.js} +21 -40
- package/dist/iconCache-FR4iy6go.js.map +1 -0
- package/dist/index.d.mts +17 -63
- package/dist/index.d.ts +17 -63
- package/dist/index.js +387 -373
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +384 -369
- package/dist/index.mjs.map +1 -1
- package/dist/injectedWallets-48L-AL1I.mjs +173 -0
- package/dist/injectedWallets-48L-AL1I.mjs.map +1 -0
- package/dist/injectedWallets-Dy8YGKL-.js +178 -0
- package/dist/injectedWallets-Dy8YGKL-.js.map +1 -0
- package/dist/internal.d.mts +3 -27
- package/dist/internal.d.ts +3 -27
- package/dist/internal.js +2 -2
- package/dist/internal.mjs +2 -2
- package/dist/pageLoad-BZ7o8p5R.mjs +24 -0
- package/dist/pageLoad-BZ7o8p5R.mjs.map +1 -0
- package/dist/pageLoad-D1wLeuFG.js +28 -0
- package/dist/pageLoad-D1wLeuFG.js.map +1 -0
- package/dist/platformAccounts-BjIeRcFN.js +321 -0
- package/dist/platformAccounts-BjIeRcFN.js.map +1 -0
- package/dist/platformAccounts-yyRUrnel.mjs +256 -0
- package/dist/platformAccounts-yyRUrnel.mjs.map +1 -0
- package/dist/polkadot.d.mts +127 -11
- package/dist/polkadot.d.ts +127 -11
- package/dist/polkadot.js +171 -107
- package/dist/polkadot.js.map +1 -1
- package/dist/polkadot.mjs +164 -100
- package/dist/polkadot.mjs.map +1 -1
- package/dist/solana.d.mts +11 -5
- package/dist/solana.d.ts +11 -5
- package/dist/solana.js +18 -15
- package/dist/solana.js.map +1 -1
- package/dist/solana.mjs +7 -4
- package/dist/solana.mjs.map +1 -1
- package/dist/storage-D3AwNgqc.d.mts +27 -0
- package/dist/storage-D3AwNgqc.d.ts +27 -0
- package/dist/testing.d.mts +71 -0
- package/dist/testing.d.ts +71 -0
- package/dist/testing.js +136 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +131 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/{types-DKU-SNio.js → types-BWHvNFlN.js} +149 -2
- package/dist/types-BWHvNFlN.js.map +1 -0
- package/dist/types-Dkyt9bf4.d.mts +683 -0
- package/dist/types-Dkyt9bf4.d.ts +683 -0
- package/dist/types-Yor672md.mjs +215 -0
- package/dist/types-Yor672md.mjs.map +1 -0
- package/dist/walletconnect.d.mts +41 -0
- package/dist/walletconnect.d.ts +41 -0
- package/dist/walletconnect.js +219 -0
- package/dist/walletconnect.js.map +1 -0
- package/dist/walletconnect.mjs +216 -0
- package/dist/walletconnect.mjs.map +1 -0
- package/package.json +33 -9
- package/dist/errors-BLFP1dQI.mjs +0 -181
- package/dist/errors-BLFP1dQI.mjs.map +0 -1
- package/dist/errors-DYWVPHrR.js +0 -222
- package/dist/errors-DYWVPHrR.js.map +0 -1
- package/dist/iconCache-DUtkJlaX.js.map +0 -1
- package/dist/iconCache-pP2ES4pJ.mjs.map +0 -1
- package/dist/injectedWallets-BqR-_aAy.js +0 -148
- package/dist/injectedWallets-BqR-_aAy.js.map +0 -1
- package/dist/injectedWallets-Uz4hPuoW.mjs +0 -119
- package/dist/injectedWallets-Uz4hPuoW.mjs.map +0 -1
- package/dist/types-Cb-vuEru.d.mts +0 -330
- package/dist/types-Cb-vuEru.d.ts +0 -330
- package/dist/types-DKU-SNio.js.map +0 -1
- package/dist/types-jnifAAA0.mjs +0 -104
- 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`.
|