@kheopskit/core 5.3.0 → 6.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +432 -0
- package/MIGRATING_TO_V6.md +840 -0
- package/README.md +629 -12
- 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 +187 -106
- package/dist/polkadot.js.map +1 -1
- package/dist/polkadot.mjs +180 -99
- 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-Yor672md.mjs +215 -0
- package/dist/types-Yor672md.mjs.map +1 -0
- package/dist/types-lkMXVJNG.d.mts +685 -0
- package/dist/types-lkMXVJNG.d.ts +685 -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 +30 -6
- 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-AxUDtkR-.d.mts +0 -338
- package/dist/types-AxUDtkR-.d.ts +0 -338
- package/dist/types-DKU-SNio.js.map +0 -1
- package/dist/types-jnifAAA0.mjs +0 -104
- package/dist/types-jnifAAA0.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
# @kheopskit/core
|
|
2
2
|
|
|
3
|
-
Framework-agnostic core for
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Framework-agnostic core for
|
|
4
|
+
[Kheopskit](https://github.com/kheopskit/kheopskit). It lists wallets and
|
|
5
|
+
accounts across Polkadot, Ethereum and Solana, with injected wallets and
|
|
6
|
+
WalletConnect (Reown AppKit).
|
|
6
7
|
|
|
7
|
-
For React, use
|
|
8
|
-
|
|
8
|
+
For React, use
|
|
9
|
+
[`@kheopskit/react`](https://www.npmjs.com/package/@kheopskit/react). Full
|
|
10
|
+
docs and the interactive playground are at
|
|
11
|
+
https://github.com/kheopskit/kheopskit.
|
|
9
12
|
|
|
10
13
|
> **Upgrading from v3?** v4 moves platforms to plugins and makes platform SDKs
|
|
11
14
|
> (and WalletConnect) optional peer dependencies. See
|
|
12
15
|
> [MIGRATING_TO_V4.md](./MIGRATING_TO_V4.md).
|
|
13
16
|
|
|
17
|
+
> **Upgrading from v5?** v6 makes `connect()` shared, idempotent and
|
|
18
|
+
> abortable, types its rejections, adds `status`, `isDiscovering`,
|
|
19
|
+
> `isPlaceholder` and `signRaw`, and moves WalletConnect to a plugin. See
|
|
20
|
+
> [MIGRATING_TO_V6.md](./MIGRATING_TO_V6.md).
|
|
21
|
+
|
|
14
22
|
## Install
|
|
15
23
|
|
|
16
|
-
`rxjs` is always required
|
|
17
|
-
peer dependency
|
|
24
|
+
`rxjs` is always required. Every platform SDK (and WalletConnect) is an
|
|
25
|
+
optional peer dependency, so install only what you use:
|
|
18
26
|
|
|
19
27
|
```bash
|
|
20
28
|
pnpm add @kheopskit/core rxjs
|
|
@@ -28,6 +36,34 @@ pnpm add @reown/appkit # WalletConnect
|
|
|
28
36
|
A Polkadot-only dapp that doesn't use WalletConnect installs none of the
|
|
29
37
|
Ethereum/Solana SDKs nor `@reown/appkit`, and none of their code is bundled.
|
|
30
38
|
|
|
39
|
+
WalletConnect is a plugin from its own entry point, so only a dapp that imports
|
|
40
|
+
`@kheopskit/core/walletconnect` bundles AppKit:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { getKheopskit$ } from "@kheopskit/core";
|
|
44
|
+
import { polkadot } from "@kheopskit/core/polkadot";
|
|
45
|
+
import { walletConnect } from "@kheopskit/core/walletconnect";
|
|
46
|
+
import { defineChain } from "@reown/appkit/networks";
|
|
47
|
+
|
|
48
|
+
const polkadotNetwork = defineChain({
|
|
49
|
+
id: "91b171bb158e2d3848fa23a9f1c25182",
|
|
50
|
+
name: "Polkadot",
|
|
51
|
+
nativeCurrency: { name: "Polkadot", symbol: "DOT", decimals: 10 },
|
|
52
|
+
rpcUrls: { default: { http: ["https://rpc.polkadot.io"] } },
|
|
53
|
+
chainNamespace: "polkadot",
|
|
54
|
+
caipNetworkId: "polkadot:91b171bb158e2d3848fa23a9f1c25182",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const kheopskit$ = getKheopskit$({
|
|
58
|
+
platforms: [polkadot()],
|
|
59
|
+
walletConnect: walletConnect({
|
|
60
|
+
projectId: "<your Reown project id>",
|
|
61
|
+
metadata: { name: "My Dapp", description: "", url: "https://my.dapp", icons: [] },
|
|
62
|
+
networks: [polkadotNetwork],
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
31
67
|
## Usage
|
|
32
68
|
|
|
33
69
|
Platforms are enabled by passing **plugins** (imported from their entry points)
|
|
@@ -45,23 +81,604 @@ const kheopskit$ = getKheopskit$({
|
|
|
45
81
|
ethereum(),
|
|
46
82
|
solana({ chain: "solana:mainnet" }),
|
|
47
83
|
],
|
|
48
|
-
appName: "My Dapp", // shown by Polkadot extensions; defaults to
|
|
84
|
+
appName: "My Dapp", // shown by Polkadot extensions; defaults to the WalletConnect metadata name, then the hostname
|
|
49
85
|
autoReconnect: true,
|
|
50
86
|
});
|
|
51
87
|
|
|
52
|
-
kheopskit$.subscribe(({ wallets, accounts, isHydrating }) => {
|
|
88
|
+
kheopskit$.subscribe(({ wallets, accounts, isHydrating, isDiscovering }) => {
|
|
53
89
|
// ...
|
|
54
90
|
});
|
|
55
91
|
```
|
|
56
92
|
|
|
93
|
+
[Wallet lifecycle](#wallet-lifecycle) explains what each field means over a
|
|
94
|
+
page load, with every default and delay.
|
|
95
|
+
|
|
57
96
|
Each platform entry point also exports its SDK-precise types
|
|
58
97
|
(`PolkadotAccount`, `EthereumAccount`, `SolanaAccount`, …). The root export
|
|
59
98
|
carries the SDK-free `KheopskitConfig`, `KheopskitState`, `BaseWallet` and
|
|
60
99
|
`BaseWalletAccount`.
|
|
61
100
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
101
|
+
## Wallet lifecycle
|
|
102
|
+
|
|
103
|
+
A page load runs these steps in order. Kheopskit discovers the installed
|
|
104
|
+
wallets, paints the cached state as placeholders, reconnects the wallets the
|
|
105
|
+
user connected last time, ends hydration, and writes the new state to the
|
|
106
|
+
cache. A user's `connect()` or `disconnect()` can happen at any point. Each
|
|
107
|
+
subsection below gives the rules and the numbers, and links the source that
|
|
108
|
+
implements them.
|
|
109
|
+
|
|
110
|
+
### Discovery
|
|
111
|
+
|
|
112
|
+
Source: [`api/polkadot/wallets.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/polkadot/wallets.ts),
|
|
113
|
+
[`utils/pageLoad.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/utils/pageLoad.ts),
|
|
114
|
+
[`api/kheopskit.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/kheopskit.ts).
|
|
115
|
+
|
|
116
|
+
Polkadot extensions register themselves on `window.injectedWeb3` and send no
|
|
117
|
+
event, so kheopskit reads that registry on a fixed schedule:
|
|
118
|
+
|
|
119
|
+
- At subscribe, then 100, 300, 600 and 1100 ms after subscribe.
|
|
120
|
+
- When `window` fires `load`, and again 100 ms after `load`. A subscription
|
|
121
|
+
that starts after `load` skips these two reads.
|
|
122
|
+
|
|
123
|
+
Nothing reads the registry after the last of these reads. A wallet that
|
|
124
|
+
injects after `load` is listed only when a read still runs after it, the one
|
|
125
|
+
100 ms after `load` or a subscribe read still pending, so its discovery is not
|
|
126
|
+
guaranteed. Wallets inject `injectedWeb3` while the page loads, so a wallet
|
|
127
|
+
that injects after the last read is not listed until the next subscription,
|
|
128
|
+
usually the next page load. Kheopskit has no
|
|
129
|
+
`refreshWallets()` and does not read again on focus or on connect.
|
|
130
|
+
|
|
131
|
+
Ethereum (EIP-6963, through `mipd`) and Solana (Wallet Standard) wallets
|
|
132
|
+
announce themselves with events, so those lists update whenever a wallet
|
|
133
|
+
announces or unregisters.
|
|
134
|
+
|
|
135
|
+
`state.isDiscovering` tells you whether the list may still grow:
|
|
136
|
+
|
|
137
|
+
- `true` when the subscription started before `document.readyState` was
|
|
138
|
+
`"complete"`.
|
|
139
|
+
- It turns `false` in the first emission after `load` in which the registry was
|
|
140
|
+
read again. That emission already lists every extension injected by `load`.
|
|
141
|
+
Wait for it before you conclude that a wallet is missing.
|
|
142
|
+
- `false` from the first emission when you subscribe after `load`.
|
|
143
|
+
- Always `true` on the server.
|
|
144
|
+
|
|
145
|
+
It says nothing about wallets that inject after `load`. In the end-to-end test
|
|
146
|
+
`e2e/tests/discovery-at-load.spec.ts`, an extension injected 1500 ms after the
|
|
147
|
+
first read and before `load` showed in the wallet list 16.2 ms after `load` on
|
|
148
|
+
vite-react, and 15.8 to 18.3 ms across the three example apps (the test's
|
|
149
|
+
limit is 300 ms).
|
|
150
|
+
|
|
151
|
+
### Hydration
|
|
152
|
+
|
|
153
|
+
Source: [`api/kheopskit.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/kheopskit.ts),
|
|
154
|
+
[`api/hydration.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/hydration.ts),
|
|
155
|
+
[`utils/createHydrationBuffer.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/utils/createHydrationBuffer.ts).
|
|
156
|
+
|
|
157
|
+
On a reload, the first emission shows the wallets and accounts of the previous
|
|
158
|
+
visit from the cache, so the list does not flash empty while the extensions
|
|
159
|
+
inject and the wallets reconnect. `state.isHydrating` is `true` while it does.
|
|
160
|
+
With nothing cached, or with `hydrationGracePeriod: 0`, `isHydrating` is
|
|
161
|
+
`false` from the first emission.
|
|
162
|
+
|
|
163
|
+
Items restored from the cache are placeholders, flagged `isPlaceholder: true`.
|
|
164
|
+
Each platform type is a union on that flag. A placeholder has no SDK handle
|
|
165
|
+
(`txCreator`, `polkadotSigner`, `signRaw`, `client`, `signer`, `getSigner`,
|
|
166
|
+
`provider`, `extension`, `wallet`, `appKit`, `sourceId`), so the compiler makes
|
|
167
|
+
you narrow before you sign:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
if (account.isPlaceholder) return;
|
|
171
|
+
await account.txCreator.signBytes(bytes);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Reading a handle without narrowing gives `undefined` in the type, so the
|
|
175
|
+
compiler rejects a call on it. The plain fields of the cache (Ethereum
|
|
176
|
+
`chainId`, Polkadot key `type`) are restored on placeholders. Solana `chains`
|
|
177
|
+
is not cached, so a placeholder Solana account has none.
|
|
178
|
+
|
|
179
|
+
A placeholder wallet is never connected. Its `connect()` waits for kheopskit to
|
|
180
|
+
discover the live wallet with the same id and connects that one. It rejects
|
|
181
|
+
with `WALLET_PENDING` if hydration ends first, and with `ABORTED` if its
|
|
182
|
+
`signal` fires or the last subscriber unsubscribes while it waits. Its
|
|
183
|
+
`disconnect()` resolves at once. A cached wallet that kheopskit has already
|
|
184
|
+
discovered is no longer a placeholder. It carries its live `connect()` and
|
|
185
|
+
`disconnect()` and shows `status: "connecting"` while its reconnect runs, and
|
|
186
|
+
its `connect()` joins that reconnect. A Polkadot wallet's `extension` stays
|
|
187
|
+
`undefined` until that reconnect succeeds. No item is a placeholder once
|
|
188
|
+
`isHydrating` is `false`.
|
|
189
|
+
|
|
190
|
+
`isHydrating` turns `false` once all three hold:
|
|
191
|
+
|
|
192
|
+
1. `hydrationGracePeriod` has elapsed (default 500 ms).
|
|
193
|
+
2. Discovery has ended (`isDiscovering` is `false`), so a page with a cache
|
|
194
|
+
hydrates until `load` or `hydrationTimeout`, whichever comes first.
|
|
195
|
+
3. Every wallet that hydrated as `connecting` has settled. A wallet has
|
|
196
|
+
settled when it is absent from the discovered wallets, connected once its
|
|
197
|
+
account list has been read (an empty list counts) or connected with no
|
|
198
|
+
cached accounts, disconnected with `error` set, or disconnected and not
|
|
199
|
+
remembered when hydration began.
|
|
200
|
+
|
|
201
|
+
At `hydrationTimeout` (default 3000 ms) hydration ends whatever is pending, and
|
|
202
|
+
a wallet whose reconnect is still pending stays `connecting`. The timeout wins
|
|
203
|
+
over a longer grace period, so an app that raises `hydrationGracePeriod` sets
|
|
204
|
+
`hydrationTimeout` as well.
|
|
205
|
+
|
|
206
|
+
- A wallet that reconnects without sharing any account with the site settles
|
|
207
|
+
as soon as it answers. Its cached accounts leave the list when hydration
|
|
208
|
+
ends.
|
|
209
|
+
- A failed reconnect ends that wallet's wait, even when the reconnect policy
|
|
210
|
+
still permits a retry.
|
|
211
|
+
- A remembered wallet the user disconnects during or right after its reconnect
|
|
212
|
+
holds hydration, and shows its cached accounts, until `hydrationTimeout`.
|
|
213
|
+
- A prompt the extension opens for a revoked site holds hydration until the
|
|
214
|
+
user answers or `hydrationTimeout` elapses.
|
|
215
|
+
- "Its account list has been read" relies on a signal that the built-in
|
|
216
|
+
plugins send. A custom `KheopskitPlatform` plugin, or a wrapper that pipes a
|
|
217
|
+
built-in plugin's `getAccounts$`, does not send it. For such a plugin, a
|
|
218
|
+
reconnected wallet with cached accounts that now lists none holds hydration
|
|
219
|
+
until `hydrationTimeout`.
|
|
220
|
+
|
|
221
|
+
Measured with Talisman 3.11.0 on vite-react, with the extension disabled and a
|
|
222
|
+
filled cache, hydration ended 633 ms after page start (subscribe at about
|
|
223
|
+
130 ms plus the 500 ms grace period), where v5 took 3 s. With the extension
|
|
224
|
+
present, the reloaded row read `Connecting…` at 176 ms and `Disconnect` at
|
|
225
|
+
282 ms. An extension re-enabled cold answered at 2667 ms, close to the 3000 ms
|
|
226
|
+
default, so raise `hydrationTimeout` if your users' wallets start slower.
|
|
227
|
+
|
|
228
|
+
The cache is written after hydration ends, 1 second after the state stops
|
|
229
|
+
changing (a debounce). The first write after hydration always happens; later
|
|
230
|
+
writes happen only when a persisted field or a wallet's `status` changed. A
|
|
231
|
+
wallet still `connecting` at that moment keeps its previous cached entry and
|
|
232
|
+
accounts, so a slow reconnect never writes an empty cache. Every other wallet is
|
|
233
|
+
written as it is, with the accounts of connected wallets only. See
|
|
234
|
+
[Persistence](#persistence) for the format.
|
|
235
|
+
|
|
236
|
+
### Reconnect
|
|
237
|
+
|
|
238
|
+
Source: [`api/wallets.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/wallets.ts),
|
|
239
|
+
[`api/reconnectPolicy.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/reconnectPolicy.ts),
|
|
240
|
+
[`api/injectedWallets.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/injectedWallets.ts).
|
|
241
|
+
|
|
242
|
+
Kheopskit adds an injected wallet to the store's `remembered` list on every
|
|
243
|
+
successful `connect()` and removes it when `disconnect()` starts on a
|
|
244
|
+
connected wallet, whatever `autoReconnect` says, so an app can turn reconnect
|
|
245
|
+
on later. On the next load it reconnects the remembered injected wallets that
|
|
246
|
+
`autoReconnect` accepts:
|
|
247
|
+
|
|
248
|
+
- `true` (the default) reconnects every remembered injected wallet, and
|
|
249
|
+
`false` none.
|
|
250
|
+
- A function chooses per wallet. It is called for each cached connected
|
|
251
|
+
injected wallet on the first frame, then for each remembered wallet with its
|
|
252
|
+
live object before the attempt. It must be synchronous and pure. It runs on
|
|
253
|
+
the server and on the client for the same input, and both must agree, or the
|
|
254
|
+
markup diverges. It receives the `BaseWallet` fields only, so key on `id`.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
getKheopskit$({
|
|
258
|
+
platforms: [polkadot()],
|
|
259
|
+
autoReconnect: (wallet) => wallet.id === "polkadot:talisman",
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A cached connected wallet the function accepts shows `status: "connecting"`
|
|
264
|
+
from the first frame until its attempt settles. A wallet it rejects shows
|
|
265
|
+
`disconnected` from the first frame, without its cached accounts, so nothing
|
|
266
|
+
flashes from connected to disconnected. The cache records a connect 1 s after
|
|
267
|
+
the state settles. After a quicker reload, the first frame shows the wallet as
|
|
268
|
+
the previous cache had it, until its reconnect starts.
|
|
269
|
+
|
|
270
|
+
A reconnect opens no connect prompt while the site stays authorised in the
|
|
271
|
+
extension. When the authorisation was revoked, the extension prompts once. A
|
|
272
|
+
refusal (`USER_REJECTED`) forgets the wallet, so the next load stays silent.
|
|
273
|
+
Any other failure sets `wallet.error` and keeps the wallet remembered.
|
|
274
|
+
Kheopskit retries it on the next change to the wallet list (a wallet discovered
|
|
275
|
+
or removed, or any wallet's status changing), 3 attempts in total. If the list
|
|
276
|
+
never changes again, no retry runs. A manual `connect()` or `disconnect()` on a
|
|
277
|
+
remembered wallet ends its automatic reconnect for the session. A
|
|
278
|
+
`disconnect()` while the automatic reconnect still waits for the extension
|
|
279
|
+
aborts it without forgetting the wallet, so the next load reconnects it.
|
|
280
|
+
Automatic reconnect also ends when the wallet disappears mid-attempt, and the
|
|
281
|
+
wallet stays remembered for the next load.
|
|
282
|
+
|
|
283
|
+
Every wallet carries `status` (`disconnected`, `connecting` or `connected`) and
|
|
284
|
+
`error`. `isConnected` is `status === "connected"`. `error` is the last
|
|
285
|
+
`connect()` or reconnect that the platform refused (`USER_REJECTED`) or failed
|
|
286
|
+
(`CONNECT_FAILED`, or `FEATURE_NOT_SUPPORTED` for a Wallet Standard wallet
|
|
287
|
+
without `standard:connect`). The next attempt clears it, and an aborted
|
|
288
|
+
attempt sets nothing. With `debug: true`, a failed reconnect is logged with
|
|
289
|
+
`console.debug`.
|
|
290
|
+
|
|
291
|
+
The WalletConnect connector is not governed by `autoReconnect`. AppKit restores
|
|
292
|
+
its own session on load, and the connector hydrates `connecting` while it does.
|
|
293
|
+
|
|
294
|
+
### Connect and disconnect
|
|
295
|
+
|
|
296
|
+
Source: [`api/injectedWallets.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/injectedWallets.ts),
|
|
297
|
+
[`api/kheopskit.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/kheopskit.ts),
|
|
298
|
+
[`api/errors.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/errors.ts).
|
|
299
|
+
|
|
300
|
+
`wallet.connect()` and `wallet.disconnect()` are safe to call straight from UI
|
|
301
|
+
code:
|
|
302
|
+
|
|
303
|
+
- **Shared.** Concurrent `connect()` calls on one wallet share one platform
|
|
304
|
+
attempt, so the extension is asked once. Each call gets its own promise.
|
|
305
|
+
- **Idempotent.** `connect()` on a connected wallet and `disconnect()` on a
|
|
306
|
+
disconnected wallet resolve at once without asking the platform. A
|
|
307
|
+
`connect()` made while a `disconnect()` still waits for the platform waits
|
|
308
|
+
for that disconnect to settle, then connects if the wallet ended up
|
|
309
|
+
disconnected.
|
|
310
|
+
- **Abortable.** `connect({ signal })` rejects that caller with `ABORTED` when
|
|
311
|
+
the signal fires before the platform answers. Other callers keep waiting.
|
|
312
|
+
The attempt itself is abandoned once every caller that passed a signal has
|
|
313
|
+
aborted and no caller without one joined, on `disconnect()`, or when the
|
|
314
|
+
last `kheopskit$` subscriber unsubscribes. An abort after the platform
|
|
315
|
+
granted the connect changes nothing, and a Polkadot extension counts as
|
|
316
|
+
granted only once its account list has been read. Aborting cannot close the
|
|
317
|
+
extension's own prompt, so a Polkadot `connect()` started while it is open
|
|
318
|
+
waits for that prompt instead of asking again.
|
|
319
|
+
- **No default timeout.** An extension prompt pends only while the user has
|
|
320
|
+
not answered it, so kheopskit sets no timeout. Pass `AbortSignal.timeout(ms)`
|
|
321
|
+
to cap the wait.
|
|
322
|
+
- **Settles with the state.** `connect()` resolves once `kheopskit$` has
|
|
323
|
+
emitted a state in which the wallet has `isConnected: true`, so the state
|
|
324
|
+
your UI reads already shows the connect. That costs one microtask after the
|
|
325
|
+
platform's grant (1.9 ms wall clock, measured once during K3 in a
|
|
326
|
+
fake-timer unit run). There are two exceptions. A `disconnect()` that
|
|
327
|
+
completes while `connect()` waits for that state rejects it with `ABORTED`.
|
|
328
|
+
When the wallet leaves the list (an extension removed) after the platform
|
|
329
|
+
granted the connect, `connect()` resolves even if no state showed the wallet
|
|
330
|
+
connected, and the wallet stays remembered. Removed before the grant, it
|
|
331
|
+
rejects with `ABORTED`. A `disconnect()` whose platform call is still
|
|
332
|
+
pending or refused does not count as completed, so the `connect()` still
|
|
333
|
+
resolves once the state shows the wallet connected (today only Solana has a
|
|
334
|
+
platform disconnect).
|
|
335
|
+
- **Disconnect.** `disconnect()` forgets the wallet first, then waits for the
|
|
336
|
+
platform's disconnect, if the platform has one, and rejects when that call
|
|
337
|
+
fails (the wallet is then remembered again and stays connected). It then
|
|
338
|
+
resolves once `kheopskit$` has emitted the wallet with `isConnected: false`,
|
|
339
|
+
or once a later `connect()`, the wallet leaving the list or the end of the
|
|
340
|
+
subscription makes that emission impossible. A `disconnect()` that still
|
|
341
|
+
waits for the platform settles when the platform answers, even after the
|
|
342
|
+
subscription ended. During a pending `connect()` that the platform has not
|
|
343
|
+
granted, `disconnect()` aborts it and resolves without waiting for the
|
|
344
|
+
prompt.
|
|
345
|
+
- **After the subscription.** Once the last `kheopskit$` subscriber has
|
|
346
|
+
unsubscribed, `connect()` and `disconnect()` on an injected wallet object
|
|
347
|
+
from that subscription reject with `ABORTED` without asking the platform. A
|
|
348
|
+
placeholder's `connect()` rejects the same way, and its `disconnect()` still
|
|
349
|
+
resolves at once. The live WalletConnect connector is not wrapped, so its
|
|
350
|
+
`connect()` still opens the AppKit modal and its `disconnect()` still ends a
|
|
351
|
+
live session.
|
|
352
|
+
|
|
353
|
+
Every rejection of `connect()` is a `KheopskitError` with a stable `code` and
|
|
354
|
+
the `walletId`, and so is an `ABORTED` `disconnect()`. A `disconnect()` whose
|
|
355
|
+
platform call fails (Solana, the WalletConnect connector) rejects with the
|
|
356
|
+
platform's own error.
|
|
357
|
+
|
|
358
|
+
| Code | When |
|
|
359
|
+
|------|------|
|
|
360
|
+
| `USER_REJECTED` | The user refused the prompt. `cause` holds the platform error. |
|
|
361
|
+
| `CONNECT_FAILED` | Any other platform failure, including a Polkadot extension whose `enable()` result lacks `accounts.get`, `accounts.subscribe`, `signer.signPayload` or `signer.signRaw`, or whose `accounts.get()` rejects. `cause` holds the platform error. |
|
|
362
|
+
| `ABORTED` | The call was abandoned, as listed above. The message reads `connect of wallet <id> was aborted` or `disconnect of wallet <id> was aborted`. |
|
|
363
|
+
| `FEATURE_NOT_SUPPORTED` | A Solana Wallet Standard wallet that lacks `standard:connect`. |
|
|
364
|
+
| `WALLET_PENDING` | `connect()` on a placeholder whose live wallet was not discovered before hydration ended, and always on a `getHydratedSnapshot` placeholder. |
|
|
365
|
+
|
|
366
|
+
A Polkadot extension sends a plain `Error` on refusal, so kheopskit maps its
|
|
367
|
+
message: Talisman's `Site <url> has not been authorised for Talisman yet`
|
|
368
|
+
(observed live), polkadot.js's `Connection request was rejected by the user.`,
|
|
369
|
+
SubWallet's `Rejected` or `Cancelled`. On Ethereum, an EIP-1193 error with code
|
|
370
|
+
`4001` maps to `USER_REJECTED`. Branch on `error.code`, not on the message. A
|
|
371
|
+
rejected `accounts.get()` during a Polkadot connect whose message matches one
|
|
372
|
+
of these refusals also maps to `USER_REJECTED`. Refused live in Talisman, a
|
|
373
|
+
connect rejects with the message `[kheopskit] the user rejected the connect
|
|
374
|
+
request of wallet polkadot:talisman` and the Talisman error as `cause`.
|
|
375
|
+
|
|
376
|
+
The WalletConnect connector differs. Its `connect()` opens the AppKit modal
|
|
377
|
+
and resolves once it is open, `isConnected` follows when the user approves a
|
|
378
|
+
session, and `signal` is accepted for signature compatibility only.
|
|
379
|
+
|
|
380
|
+
#### Accounts
|
|
381
|
+
|
|
382
|
+
In `kheopskit$`, each wallet connection has its own account stream. It starts
|
|
383
|
+
empty when the wallet connects and ends when it disconnects. No emission shows a
|
|
384
|
+
disconnected wallet still listing live accounts, or a connected wallet listing
|
|
385
|
+
the accounts of a previous connection, and another wallet's pending request
|
|
386
|
+
(Ethereum `eth_accounts`) never delays a wallet's accounts. A Polkadot
|
|
387
|
+
wallet's `connect()` reads `accounts.get()` before it resolves, so the
|
|
388
|
+
emission that first shows it connected already lists its accounts. On
|
|
389
|
+
platforms that list accounts asynchronously (Ethereum `eth_accounts`), the
|
|
390
|
+
accounts may arrive in a later emission. While `isHydrating` is `true`, a
|
|
391
|
+
wallet's cached placeholder accounts stay listed, whatever its status, until
|
|
392
|
+
live accounts replace them or hydration ends. The guarantee has one more
|
|
393
|
+
exception. The first value of a platform plugin's `getAccounts$`, called
|
|
394
|
+
directly with wallets that are already connected, waits until each of them has
|
|
395
|
+
answered. `kheopskit$` subscribes before any wallet is connected, so it never
|
|
396
|
+
waits.
|
|
397
|
+
|
|
398
|
+
`state.accounts` is grouped by platform (Polkadot, Ethereum, Solana), then by
|
|
399
|
+
wallet (Talisman first, then by wallet name). Inside a wallet, accounts keep
|
|
400
|
+
the order the extension lists them in. `state.wallets` follows the same
|
|
401
|
+
platform order, then the WalletConnect connector, with Talisman first, then by
|
|
402
|
+
name.
|
|
403
|
+
|
|
404
|
+
### Persistence
|
|
405
|
+
|
|
406
|
+
Source: [`api/store.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/store.ts),
|
|
407
|
+
[`api/compactStore.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/compactStore.ts),
|
|
408
|
+
[`utils/storage.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/utils/storage.ts).
|
|
409
|
+
|
|
410
|
+
The data lives under the key `kheopskit` (change it with the `storageKey`
|
|
411
|
+
config option, for example to run two dapps on one origin). In the browser it
|
|
412
|
+
goes to `localStorage` as JSON:
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
{
|
|
416
|
+
"remembered": ["polkadot:talisman"],
|
|
417
|
+
"cachedWallets": [
|
|
418
|
+
{ "id": "polkadot:talisman", "platform": "polkadot", "type": "injected", "name": "Talisman", "isConnected": true }
|
|
419
|
+
],
|
|
420
|
+
"cachedAccounts": [
|
|
421
|
+
{ "id": "polkadot:talisman::5Grw…", "platform": "polkadot", "address": "5Grw…", "name": "Alice", "polkadotAccountType": "sr25519", "walletId": "polkadot:talisman", "walletName": "Talisman" }
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
- `remembered` lists the injected wallets the user connected. It is written as
|
|
427
|
+
soon as a connect succeeds and cleared as soon as `disconnect()` starts on a
|
|
428
|
+
connected wallet. A `disconnect()` that aborts a pending connect leaves it
|
|
429
|
+
unchanged.
|
|
430
|
+
- `cachedWallets` and `cachedAccounts` are written by the hydration rule above:
|
|
431
|
+
after hydration ends, 1 second after the state stops changing.
|
|
432
|
+
- Wallet icons go to a separate `localStorage` key, `kheopskit-icons`, to keep
|
|
433
|
+
the main entry small.
|
|
434
|
+
|
|
435
|
+
With `ssrCookies`, the same data goes to a `kheopskit` cookie in a compact
|
|
436
|
+
form, to stay under the cookie size limit:
|
|
437
|
+
|
|
438
|
+
```json
|
|
439
|
+
{ "v": 1, "r": ["polkadot:talisman"], "w": [["polkadot:talisman", "Talisman", 1, 0]], "a": [["polkadot:talisman", "5Grw…", "Alice", null, 0, 0]] }
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
`r` is `remembered`, `w` holds `[id, name, isConnected, type]` per wallet and
|
|
443
|
+
`a` holds `[walletId, address, name, chainId, polkadotType, platform]` per
|
|
444
|
+
account. The cookie is URI-encoded, lasts one year, and is set with `path=/`,
|
|
445
|
+
`SameSite=Lax`, and `Secure` on HTTPS. Kheopskit warns in the console when the
|
|
446
|
+
encoded value exceeds 3072 bytes.
|
|
447
|
+
|
|
448
|
+
Data written before v6 stored `remembered` as `autoReconnect`. The store reads
|
|
449
|
+
that field as `remembered` and drops it on the next write. A storage write that
|
|
450
|
+
throws (`QuotaExceededError`, a denied write) keeps the in-memory state while
|
|
451
|
+
the store is observed, and
|
|
452
|
+
neither `connect()` nor `disconnect()` rejects because of it.
|
|
453
|
+
|
|
454
|
+
#### Custom storage
|
|
455
|
+
|
|
456
|
+
`createKheopskitStore` accepts a `storage` option, which takes precedence over
|
|
457
|
+
`ssrCookies` and `localStorage`. Pass the store to `getKheopskit$`:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
import {
|
|
461
|
+
createKheopskitStore,
|
|
462
|
+
getKheopskit$,
|
|
463
|
+
type SyncableStorage,
|
|
464
|
+
} from "@kheopskit/core";
|
|
465
|
+
|
|
466
|
+
const entries = new Map<string, string>();
|
|
467
|
+
const storage: SyncableStorage = {
|
|
468
|
+
getItem: (key) => entries.get(key) ?? null,
|
|
469
|
+
setItem: (key, value) => entries.set(key, value),
|
|
470
|
+
removeItem: (key) => entries.delete(key),
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const store = createKheopskitStore({ storage });
|
|
474
|
+
const kheopskit$ = getKheopskit$(config, { store });
|
|
475
|
+
|
|
476
|
+
// When done with the store
|
|
477
|
+
store.destroy();
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
`subscribe` is optional. Implement it to receive changes made by other tabs.
|
|
481
|
+
The store holds that subscription only while `store.observable` has
|
|
482
|
+
subscribers. `store.destroy()` completes `store.observable`, which ends it.
|
|
483
|
+
`store.rememberWallet(walletId)` and `store.forgetWallet(walletId)` edit the
|
|
484
|
+
remembered list directly.
|
|
485
|
+
|
|
486
|
+
### Signing
|
|
487
|
+
|
|
488
|
+
Source: [`api/polkadot/accounts.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/polkadot/accounts.ts),
|
|
489
|
+
[`api/polkadot/types.ts`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/api/polkadot/types.ts).
|
|
490
|
+
|
|
491
|
+
A live Polkadot account signs transactions through the signer polkadot-api
|
|
492
|
+
expects. The field follows the installed polkadot-api version, detected at
|
|
493
|
+
runtime. It is `txCreator` (a `TxCreator`) on v3 and later, and
|
|
494
|
+
`polkadotSigner` (a `PolkadotSigner`) on v2. With neither factory available,
|
|
495
|
+
kheopskit throws a `KheopskitError` with code `UNSUPPORTED_VERSION`. On v3 and
|
|
496
|
+
later, the `txCreator` of an account whose `type` is `ethereum` is built with
|
|
497
|
+
the `ecdsa` signature type, so fee estimation and dry runs use a 65-byte mock
|
|
498
|
+
signature. On v2, `getPolkadotSignerFromPjs` ignores the signature type.
|
|
499
|
+
Ethereum accounts expose `client` (viem), and Solana accounts `signer` and
|
|
500
|
+
`getSigner(chain)`.
|
|
501
|
+
|
|
502
|
+
`account.signRaw(data, options?)` signs arbitrary data with the wallet's raw
|
|
503
|
+
signing call (`signer.signRaw` for an extension, `polkadot_signMessage` over
|
|
504
|
+
WalletConnect) and resolves the hex signature:
|
|
505
|
+
|
|
506
|
+
- A string is sent as text with `type: "payload"`, and a `Uint8Array` as `0x`
|
|
507
|
+
hex with `type: "bytes"`. An explicit `options.type` is sent as given for
|
|
508
|
+
either input. Over WalletConnect, `polkadot_signMessage` has no `type`, so
|
|
509
|
+
none is sent.
|
|
510
|
+
- A hex-looking string is sent as text, so pass a `Uint8Array` for bytes.
|
|
511
|
+
Talisman, polkadot.js, SubWallet and Nova decode a `0x`-shaped string as
|
|
512
|
+
bytes (Talisman observed live, the others read from their sources).
|
|
513
|
+
- Pass a string for a Sign-In With Substrate (SIWS) message. Talisman opens its
|
|
514
|
+
Sign In screen when the text parses as SIWS and its generic Sign Request
|
|
515
|
+
screen for anything else, hex included. The SIWS `Version` line must read
|
|
516
|
+
`1.0.0`. Talisman rejects `1` and falls back to the generic screen. The
|
|
517
|
+
screen follows `data`, not `type`.
|
|
518
|
+
- Talisman, polkadot.js, SubWallet and Nova ignore `type` (read from their
|
|
519
|
+
sources). Enkrypt reads it as the encoding of `data`, so it needs the value
|
|
520
|
+
the derived default sends.
|
|
521
|
+
|
|
522
|
+
What Talisman 3.11.0 showed for one SIWS message (`Version: 1.0.0`), observed
|
|
523
|
+
live:
|
|
524
|
+
|
|
525
|
+
| Call | `data` sent | `type` sent | Screen |
|
|
526
|
+
|------|-------------|-------------|--------|
|
|
527
|
+
| `signRaw(message)` | text | `"payload"` | Sign In ("wants you to sign in with Substrate") |
|
|
528
|
+
| `signRaw(message, { type: "bytes" })` | text | `"bytes"` | Sign In |
|
|
529
|
+
| `signRaw(bytes)` with a `Uint8Array` | hex | `"bytes"` | Sign Request |
|
|
530
|
+
| `txCreator.signBytes(bytes)` | hex | `"bytes"` | Sign Request |
|
|
531
|
+
| `signRaw` of the same text with `Version: 1` | text | either | Sign Request |
|
|
532
|
+
|
|
533
|
+
Every signature taken verified. An `ethereum` account signs a 65-byte
|
|
534
|
+
signature that recovers to its address over keccak256 of the `<Bytes>`-wrapped
|
|
535
|
+
message.
|
|
536
|
+
|
|
537
|
+
On a connected Polkadot wallet, `wallet.extension` is the raw object
|
|
538
|
+
`enable(appName)` returned, typed `PolkadotInjected`. Use it for extension calls
|
|
539
|
+
kheopskit does not wrap. It is `undefined` until the wallet is connected, so
|
|
540
|
+
also while it connects, and a placeholder has none. Talisman adds `readonly`
|
|
541
|
+
and `partOfPortfolio` to the accounts it shares with its own origins. Both are
|
|
542
|
+
typed on `PolkadotAccount`.
|
|
543
|
+
|
|
544
|
+
### Testing
|
|
545
|
+
|
|
546
|
+
Source: [`testing/`](https://github.com/kheopskit/kheopskit/blob/main/packages/core/src/testing).
|
|
547
|
+
|
|
548
|
+
`@kheopskit/core/testing` runs kheopskit in a test without a browser
|
|
549
|
+
extension. It has no test-runner dependency, so it works with vitest and jest.
|
|
550
|
+
|
|
551
|
+
- `createFakePolkadotExtension(options?)` installs a fake extension on
|
|
552
|
+
`window.injectedWeb3[name]`. `options.name` defaults to `"fake-extension"`.
|
|
553
|
+
`options.accounts` is the account list. `options.enable` sets how `enable()`
|
|
554
|
+
answers: `"resolve"` (the default) at once, `"reject"` at once with
|
|
555
|
+
`new Error("Rejected")`, which `connect()` reports as `USER_REJECTED`, or
|
|
556
|
+
`"hold"` until the test calls `resolveEnable()` or `rejectEnable(error?)`.
|
|
557
|
+
- The fake returns `{ name, walletId, setAccounts, resolveEnable, rejectEnable,
|
|
558
|
+
enableCalls, signRawCalls, uninstall }`. `setAccounts(accounts)` pushes the
|
|
559
|
+
list to every `accounts.subscribe` listener. `enableCalls()` lists the app
|
|
560
|
+
name of each `enable()` call. `signRaw` and `signPayload` resolve
|
|
561
|
+
`FAKE_SIGNATURE`. `signRawCalls()` lists what `signRaw` received;
|
|
562
|
+
`signPayload` calls are not recorded.
|
|
563
|
+
- `createMemoryStorage()` returns a `SyncableStorage` held in memory, for
|
|
564
|
+
`createKheopskitStore({ storage })`. Stores created on the same instance see
|
|
565
|
+
each other's writes.
|
|
566
|
+
- `cachedWalletOf(walletId, name)` and `cachedAccountOf(wallet, address,
|
|
567
|
+
name?)` build the entries `store.setCachedState(wallets, accounts)` takes, so
|
|
568
|
+
a test seeds a previous session without writing the persisted shape.
|
|
569
|
+
|
|
570
|
+
A vitest test that seeds a remembered wallet and checks that it reconnects:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
import {
|
|
574
|
+
createKheopskitStore,
|
|
575
|
+
getKheopskit$,
|
|
576
|
+
type KheopskitState,
|
|
577
|
+
type KheopskitStore,
|
|
578
|
+
} from "@kheopskit/core";
|
|
579
|
+
import { polkadot } from "@kheopskit/core/polkadot";
|
|
580
|
+
import {
|
|
581
|
+
cachedAccountOf,
|
|
582
|
+
cachedWalletOf,
|
|
583
|
+
createFakePolkadotExtension,
|
|
584
|
+
createMemoryStorage,
|
|
585
|
+
type FakePolkadotExtension,
|
|
586
|
+
} from "@kheopskit/core/testing";
|
|
587
|
+
import type { Subscription } from "rxjs";
|
|
588
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
589
|
+
|
|
590
|
+
const alice = {
|
|
591
|
+
address: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
|
592
|
+
name: "Alice",
|
|
593
|
+
type: "sr25519" as const,
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
let talisman: FakePolkadotExtension;
|
|
597
|
+
let store: KheopskitStore;
|
|
598
|
+
let subscription: Subscription;
|
|
599
|
+
|
|
600
|
+
beforeEach(() => {
|
|
601
|
+
vi.useFakeTimers();
|
|
602
|
+
talisman = createFakePolkadotExtension({ name: "talisman", accounts: [alice] });
|
|
603
|
+
store = createKheopskitStore({ storage: createMemoryStorage() });
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
afterEach(() => {
|
|
607
|
+
talisman.uninstall();
|
|
608
|
+
subscription.unsubscribe();
|
|
609
|
+
store.destroy();
|
|
610
|
+
vi.useRealTimers();
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("reconnects the wallet the user connected last time", async () => {
|
|
614
|
+
const wallet = cachedWalletOf(talisman.walletId, "Talisman");
|
|
615
|
+
store.rememberWallet(talisman.walletId);
|
|
616
|
+
store.setCachedState([wallet], [cachedAccountOf(wallet, alice.address, alice.name)]);
|
|
617
|
+
|
|
618
|
+
let state: KheopskitState | undefined;
|
|
619
|
+
subscription = getKheopskit$({ platforms: [polkadot()] }, { store }).subscribe(
|
|
620
|
+
(next) => {
|
|
621
|
+
state = next;
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
625
|
+
|
|
626
|
+
expect(talisman.enableCalls()).toHaveLength(1);
|
|
627
|
+
expect(state?.wallets.map((w) => [w.id, w.isConnected])).toEqual([
|
|
628
|
+
["polkadot:talisman", true],
|
|
629
|
+
]);
|
|
630
|
+
expect(state?.accounts.map((a) => a.name)).toEqual(["Alice"]);
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Tear down with three calls:
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
talisman.uninstall();
|
|
638
|
+
subscription.unsubscribe();
|
|
639
|
+
store.destroy();
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Unsubscribing from `getKheopskit$` stops every timer it started, among them
|
|
643
|
+
the discovery reads, the hydration timers and the 1 second persistence
|
|
644
|
+
debounce. Without it, a persist can write to storage after the test ended.
|
|
645
|
+
`store.destroy()` ends the store's storage subscription. `uninstall()` removes
|
|
646
|
+
the fake from `window.injectedWeb3`, and removes `window.injectedWeb3` once it
|
|
647
|
+
is empty.
|
|
648
|
+
|
|
649
|
+
With fake timers, advance time with `vi.advanceTimersByTimeAsync` (jest:
|
|
650
|
+
`jest.advanceTimersByTimeAsync`). Kheopskit settles connects and reconnects in
|
|
651
|
+
promises, and the async variants run those microtasks between timers. The
|
|
652
|
+
synchronous variants leave them pending.
|
|
653
|
+
|
|
654
|
+
When the code under test calls `getKheopskit$` without a `store`, it uses
|
|
655
|
+
`localStorage`. Seed it through a store on the same storage, then destroy that
|
|
656
|
+
store:
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
const seed = createKheopskitStore();
|
|
660
|
+
seed.rememberWallet("polkadot:talisman");
|
|
661
|
+
seed.destroy();
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
The kit fakes Polkadot extensions only. It has no hook to make `disconnect()`
|
|
665
|
+
fail.
|
|
666
|
+
|
|
667
|
+
### Design choices
|
|
668
|
+
|
|
669
|
+
- Kheopskit does not look for wallets that inject after `load`, and has no
|
|
670
|
+
`refreshWallets()`. Wallets inject at page load, and a rescan would add
|
|
671
|
+
timers and states for a case no supported wallet produces.
|
|
672
|
+
- No `walletsReady` promise. It would be a second surface for the one fact
|
|
673
|
+
`isDiscovering` carries.
|
|
674
|
+
- No longer fixed poll. Any duration is still a guess about how long a page
|
|
675
|
+
takes to boot, and wallets inject before `load`.
|
|
676
|
+
- Discovery does not end at the last scheduled read (1100 ms). On a warm load
|
|
677
|
+
that is slower than `load`, for no gain.
|
|
678
|
+
- `remembered` is read once at subscribe. A live read would make a connect in
|
|
679
|
+
one tab connect every open tab, with no matching disconnect.
|
|
680
|
+
- No `persist: false` option. The `autoReconnect` predicate covers "do not
|
|
681
|
+
reconnect", and `storageKey` keeps two dapps apart.
|
|
65
682
|
|
|
66
683
|
## License
|
|
67
684
|
|