@solana/kit-plugin-wallet 0.12.0 → 0.13.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/README.md +102 -4
- package/dist/index.browser.cjs +120 -36
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.mjs +120 -37
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.node.cjs +9 -2
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +9 -3
- package/dist/index.node.mjs.map +1 -1
- package/dist/index.react-native.mjs +9 -3
- package/dist/index.react-native.mjs.map +1 -1
- package/dist/react/index.browser.cjs +77 -0
- package/dist/react/index.browser.cjs.map +1 -0
- package/dist/react/index.browser.mjs +66 -0
- package/dist/react/index.browser.mjs.map +1 -0
- package/dist/react/index.node.cjs +77 -0
- package/dist/react/index.node.cjs.map +1 -0
- package/dist/react/index.node.mjs +66 -0
- package/dist/react/index.node.mjs.map +1 -0
- package/dist/react/index.react-native.mjs +66 -0
- package/dist/react/index.react-native.mjs.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/react/index.d.ts +235 -0
- package/dist/types/react/index.d.ts.map +1 -0
- package/dist/types/status.d.ts +31 -0
- package/dist/types/status.d.ts.map +1 -0
- package/dist/types/store.d.ts.map +1 -1
- package/dist/types/types.d.ts +57 -9
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/wallet.d.ts +3 -3
- package/dist/types/wallet.d.ts.map +1 -1
- package/package.json +45 -14
- package/src/__typetests__/wallet-typetest.ts +15 -0
- package/src/index.ts +1 -0
- package/src/react/__typetests__/index.ts +108 -0
- package/src/react/index.ts +301 -0
- package/src/status.ts +33 -0
- package/src/store.ts +208 -81
- package/src/types.ts +58 -9
- package/src/wallet.ts +3 -1
package/src/store.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ReadonlyUint8Array,
|
|
2
3
|
type SignatureBytes,
|
|
3
4
|
SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE,
|
|
4
5
|
SOLANA_ERROR__WALLET__NOT_CONNECTED,
|
|
@@ -26,6 +27,7 @@ import type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';
|
|
|
26
27
|
import { getWalletAccountFeature, getWalletFeature } from '@wallet-standard/ui-features';
|
|
27
28
|
import { getOrCreateUiWalletForStandardWallet, getWalletForHandle } from '@wallet-standard/ui-registry';
|
|
28
29
|
|
|
30
|
+
import { isWalletWarmingUp } from './status';
|
|
29
31
|
import type {
|
|
30
32
|
WalletActionOptions,
|
|
31
33
|
WalletNamespace,
|
|
@@ -85,6 +87,7 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
85
87
|
Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' })),
|
|
86
88
|
subscribe: () => () => {},
|
|
87
89
|
subscribeSigner: () => () => {},
|
|
90
|
+
whenReady: () => Promise.resolve(),
|
|
88
91
|
[Symbol.dispose]: () => {},
|
|
89
92
|
};
|
|
90
93
|
}
|
|
@@ -101,13 +104,22 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
101
104
|
|
|
102
105
|
const listeners = new Set<() => void>();
|
|
103
106
|
const signerListeners = new Set<() => void>();
|
|
104
|
-
|
|
107
|
+
// One `standard:events change` subscription per discovered wallet, keyed by
|
|
108
|
+
// name (the stable identity; the UiWallet handle regenerates on changes). The
|
|
109
|
+
// underlying subscription attaches to the raw wallet's events, so it survives
|
|
110
|
+
// handle regeneration — no need to resubscribe when a wallet's accounts change.
|
|
111
|
+
const walletEventSubscriptions = new Map<string, () => void>();
|
|
105
112
|
let reconnectCleanup: (() => void) | null = null;
|
|
106
113
|
let userHasSelected = false;
|
|
107
114
|
let connectGeneration = 0;
|
|
108
115
|
let disposed = false;
|
|
109
116
|
// Wallet that is in the process of disconnecting, stored to guard against re-connecting to it if a future connect fails
|
|
110
117
|
let disconnectingWalletName: string | null = null;
|
|
118
|
+
// Deferred backing `whenReady`. Created lazily while the status is transient
|
|
119
|
+
// (see `isWalletWarmingUp`) and resolved by `updateState` once the status
|
|
120
|
+
// settles, so `whenReady` can hand out a stable promise for the duration of
|
|
121
|
+
// one warm-up episode (required by React Suspense).
|
|
122
|
+
let readyDeferred: PromiseWithResolvers<void> | null = null;
|
|
111
123
|
|
|
112
124
|
// Resolve storage: default to localStorage in browser, null to disable.
|
|
113
125
|
// Merely *accessing* `localStorage` throws a `SecurityError` in sandboxed
|
|
@@ -155,6 +167,19 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
155
167
|
const prev = state;
|
|
156
168
|
state = { ...state, ...updates };
|
|
157
169
|
|
|
170
|
+
// Resolve `whenReady` the moment the status leaves the transient
|
|
171
|
+
// warm-up set. `readyDeferred` is only ever created while transient (in
|
|
172
|
+
// `whenReady`), so clearing it here lets the next transient episode arm
|
|
173
|
+
// a fresh one.
|
|
174
|
+
if (isWalletWarmingUp(prev.status) && !isWalletWarmingUp(state.status)) {
|
|
175
|
+
readyDeferred?.resolve();
|
|
176
|
+
readyDeferred = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (state.wallets !== prev.wallets) {
|
|
180
|
+
syncWalletEventSubscriptions(state.wallets);
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
// Signer subscribers (`subscribeToPayer` / `subscribeToIdentity`) fire
|
|
159
184
|
// only when the observed signer changes.
|
|
160
185
|
const signerChanged = observableSigner(state) !== observableSigner(prev);
|
|
@@ -175,6 +200,18 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
175
200
|
}
|
|
176
201
|
}
|
|
177
202
|
|
|
203
|
+
// Advanced: see the `whenReady` docblock on `WalletNamespace`. Returns a
|
|
204
|
+
// stable, already-resolved promise once the initial warm-up has settled;
|
|
205
|
+
// while transient, returns the same pending promise on every call so it is
|
|
206
|
+
// safe to throw for React Suspense.
|
|
207
|
+
function whenReady(): Promise<void> {
|
|
208
|
+
if (!isWalletWarmingUp(state.status)) {
|
|
209
|
+
return Promise.resolve();
|
|
210
|
+
}
|
|
211
|
+
readyDeferred ??= Promise.withResolvers<void>();
|
|
212
|
+
return readyDeferred.promise;
|
|
213
|
+
}
|
|
214
|
+
|
|
178
215
|
// -- Signer creation ---------------------------------------------------
|
|
179
216
|
|
|
180
217
|
function tryCreateSigner(account: UiWalletAccount): WalletSigner | null {
|
|
@@ -227,6 +264,29 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
227
264
|
return next;
|
|
228
265
|
}
|
|
229
266
|
|
|
267
|
+
// Reconcile the per-wallet event subscriptions against the current wallet set:
|
|
268
|
+
// subscribe wallets that just appeared, unsubscribe those that left. Called
|
|
269
|
+
// whenever `state.wallets` changes reference. Subscribing fires nothing, so
|
|
270
|
+
// this never causes re-render churn. `wallets` is the already-filtered visible
|
|
271
|
+
// list, so wallets excluded by chain/feature/`config.filter` are intentionally
|
|
272
|
+
// never subscribed — a `register` / `unregister` event reconciles them in or
|
|
273
|
+
// out of the list (and thus in or out of this map) when their eligibility
|
|
274
|
+
// changes.
|
|
275
|
+
function syncWalletEventSubscriptions(wallets: readonly UiWallet[]): void {
|
|
276
|
+
const names = new Set(wallets.map(w => w.name));
|
|
277
|
+
for (const [name, cleanup] of walletEventSubscriptions) {
|
|
278
|
+
if (!names.has(name)) {
|
|
279
|
+
cleanup();
|
|
280
|
+
walletEventSubscriptions.delete(name);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const w of wallets) {
|
|
284
|
+
if (!walletEventSubscriptions.has(w.name)) {
|
|
285
|
+
walletEventSubscriptions.set(w.name, subscribeToWalletEvents(w));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
230
290
|
// True if a wallet matching `uiWallet` (by name) is currently registered and
|
|
231
291
|
// passes the filter. Used to re-check membership after an awaited connect /
|
|
232
292
|
// sign-in / silent reconnect resolves: the wallet may have unregistered (or
|
|
@@ -253,8 +313,6 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
253
313
|
|
|
254
314
|
// If the connected wallet was unregistered, disconnect locally.
|
|
255
315
|
if (state.connectedWallet && !newWallets.some(w => w.name === state.connectedWallet!.name)) {
|
|
256
|
-
walletEventsCleanup?.();
|
|
257
|
-
walletEventsCleanup = null;
|
|
258
316
|
updates.connectedWallet = null;
|
|
259
317
|
updates.account = null;
|
|
260
318
|
updates.signer = null;
|
|
@@ -272,11 +330,6 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
272
330
|
reconnectCleanup = null;
|
|
273
331
|
}
|
|
274
332
|
|
|
275
|
-
function resubscribeToWalletEvents(uiWallet: UiWallet): void {
|
|
276
|
-
walletEventsCleanup?.();
|
|
277
|
-
walletEventsCleanup = subscribeToWalletEvents(uiWallet);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
333
|
function setConnected(account: UiWalletAccount, wallet: UiWallet, options?: { persist?: boolean }): void {
|
|
281
334
|
// The store may have been disposed while a connect / sign-in / silent
|
|
282
335
|
// reconnect awaited. The `connectGeneration` guard at each call site
|
|
@@ -287,9 +340,23 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
287
340
|
// itself connected.
|
|
288
341
|
if (disposed) return;
|
|
289
342
|
const signer = tryCreateSigner(account);
|
|
290
|
-
resubscribeToWalletEvents(wallet);
|
|
291
343
|
disconnectingWalletName = null;
|
|
292
|
-
|
|
344
|
+
// Reconcile `wallets` alongside the connection: `connect`/`signIn`/silent
|
|
345
|
+
// reconnect authorize accounts on the underlying wallet, which regenerates
|
|
346
|
+
// its registry handle. Without rebuilding the list here, `getState().wallets`
|
|
347
|
+
// would keep the pre-connect handle (empty `.accounts`) until an unrelated
|
|
348
|
+
// `standard:events change` happened to fire — so consumers reading the
|
|
349
|
+
// just-connected wallet from `wallets` (e.g. rendering its authorized
|
|
350
|
+
// accounts) would see it as unconnected. `reconcileWalletList` returns the
|
|
351
|
+
// existing reference when the visible set is unchanged, keeping the snapshot
|
|
352
|
+
// churn-free when connect didn't actually add accounts.
|
|
353
|
+
updateState({
|
|
354
|
+
account,
|
|
355
|
+
connectedWallet: wallet,
|
|
356
|
+
signer,
|
|
357
|
+
status: 'connected',
|
|
358
|
+
wallets: reconcileWalletList(),
|
|
359
|
+
});
|
|
293
360
|
if (options?.persist !== false) {
|
|
294
361
|
persistAccount(account, wallet);
|
|
295
362
|
}
|
|
@@ -304,45 +371,43 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
304
371
|
if (!uiWallet.features.includes(StandardEvents)) {
|
|
305
372
|
return () => {};
|
|
306
373
|
}
|
|
307
|
-
|
|
308
374
|
const eventsFeature = getWalletFeature(
|
|
309
375
|
uiWallet,
|
|
310
376
|
StandardEvents,
|
|
311
377
|
) as StandardEventsFeature[typeof StandardEvents];
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
378
|
+
const walletName = uiWallet.name;
|
|
379
|
+
|
|
380
|
+
// The handler intentionally ignores the changed-property flags
|
|
381
|
+
// (`accounts`, `chains`, `features`) the wallet passes and instead
|
|
382
|
+
// reconciles against the refreshed wallet. The signer is derived
|
|
383
|
+
// entirely from the active account's features and chains, and the
|
|
384
|
+
// wallet-ui registry regenerates the account reference precisely when
|
|
385
|
+
// one of those changes — so the account reference, not the flags, is
|
|
386
|
+
// the single source of truth for whether the signer is affected. Branch
|
|
387
|
+
// on the reconciled state below, never on `properties`.
|
|
320
388
|
return eventsFeature.on('change', () => {
|
|
321
|
-
|
|
389
|
+
// Refresh the discovered list so this wallet's (and any others') accounts
|
|
390
|
+
// are current; `updateState` re-syncs subscriptions for added/removed wallets.
|
|
391
|
+
const newWallets = reconcileWalletList();
|
|
392
|
+
|
|
393
|
+
// Non-active wallet: reflect its new account/feature state in the list only.
|
|
394
|
+
if (!state.connectedWallet || state.connectedWallet.name !== walletName) {
|
|
395
|
+
updateState({ wallets: newWallets });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
322
398
|
|
|
323
|
-
//
|
|
324
|
-
|
|
399
|
+
// Active wallet: the original reconcile logic, unchanged.
|
|
400
|
+
const refreshed = refreshUiWallet(state.connectedWallet);
|
|
325
401
|
if (!filterWallet(refreshed)) {
|
|
326
402
|
disconnectLocally();
|
|
327
403
|
return;
|
|
328
404
|
}
|
|
329
|
-
|
|
330
405
|
const result = reconcileActiveAccount(refreshed);
|
|
331
406
|
if (result === null) {
|
|
332
407
|
disconnectLocally();
|
|
333
408
|
return;
|
|
334
409
|
}
|
|
335
|
-
|
|
336
|
-
// `connectedWallet` is always refreshed so consumers reading
|
|
337
|
-
// `getState().connected.wallet` see current metadata; the signer
|
|
338
|
-
// (in `result`) only churns when the active account changed, which
|
|
339
|
-
// keeps the snapshot referentially stable on no-op change events.
|
|
340
|
-
// `wallets` is reconciled too: the registry regenerates the wallet
|
|
341
|
-
// handle on a change, so without this the list entry would keep the
|
|
342
|
-
// stale handle (`wallets[i] !== connected.wallet`). `reconcileWalletList`
|
|
343
|
-
// returns the existing reference on a no-op, preserving that stability.
|
|
344
|
-
updateState({ connectedWallet: refreshed, wallets: reconcileWalletList(), ...result });
|
|
345
|
-
|
|
410
|
+
updateState({ connectedWallet: refreshed, wallets: newWallets, ...result });
|
|
346
411
|
if (result.account) {
|
|
347
412
|
persistAccount(result.account, refreshed);
|
|
348
413
|
}
|
|
@@ -441,17 +506,14 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
441
506
|
}
|
|
442
507
|
}
|
|
443
508
|
|
|
444
|
-
async function disconnect(options?: WalletActionOptions): Promise<void> {
|
|
509
|
+
async function disconnect(wallet?: UiWallet, options?: WalletActionOptions): Promise<void> {
|
|
445
510
|
options?.abortSignal?.throwIfAborted();
|
|
446
511
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
// any in-flight connect/signIn/silent-reconnect bails at its guard
|
|
453
|
-
// instead of resuming.
|
|
454
|
-
if (!state.connectedWallet) {
|
|
512
|
+
const target = wallet ?? state.connectedWallet;
|
|
513
|
+
|
|
514
|
+
// Nothing to disconnect (no arg, nothing active): cancel any pending reconnect, and
|
|
515
|
+
// supersede in-flight attempts.
|
|
516
|
+
if (!target) {
|
|
455
517
|
userHasSelected = true;
|
|
456
518
|
cancelReconnect();
|
|
457
519
|
connectGeneration++;
|
|
@@ -460,39 +522,68 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
460
522
|
return;
|
|
461
523
|
}
|
|
462
524
|
|
|
463
|
-
const
|
|
464
|
-
// Bump the generation so this disconnect supersedes any connect/signIn
|
|
465
|
-
// that was already in flight (its prompt opened before the user chose to
|
|
466
|
-
// disconnect): that attempt captured an earlier generation, so it will
|
|
467
|
-
// bail at its guard rather than establishing a connection the user has
|
|
468
|
-
// since dismissed. Mirrors the not-connected branch above — newest action
|
|
469
|
-
// wins. A connect/signIn started *after* this disconnect bumps the
|
|
470
|
-
// generation again, so the `finally` guard skips `disconnectLocally` and
|
|
471
|
-
// leaves the newer connection intact.
|
|
472
|
-
const generation = ++connectGeneration;
|
|
473
|
-
// Mark this wallet as disconnecting
|
|
474
|
-
disconnectingWalletName = currentWallet.name;
|
|
475
|
-
updateState({ status: 'disconnecting' });
|
|
525
|
+
const isActive = state.connectedWallet != null && state.connectedWallet.name === target.name;
|
|
476
526
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
527
|
+
if (isActive) {
|
|
528
|
+
const currentWallet = state.connectedWallet!;
|
|
529
|
+
// Bump generation so this disconnect supersedes any in-flight connect/signIn.
|
|
530
|
+
const generation = ++connectGeneration;
|
|
531
|
+
disconnectingWalletName = currentWallet.name;
|
|
532
|
+
updateState({ status: 'disconnecting' });
|
|
533
|
+
try {
|
|
534
|
+
if (currentWallet.features.includes(StandardDisconnect)) {
|
|
535
|
+
const disconnectFeature = getWalletFeature(
|
|
536
|
+
currentWallet,
|
|
537
|
+
StandardDisconnect,
|
|
538
|
+
) as StandardDisconnectFeature[typeof StandardDisconnect];
|
|
539
|
+
await disconnectFeature.disconnect();
|
|
540
|
+
}
|
|
541
|
+
} finally {
|
|
542
|
+
if (generation === connectGeneration) {
|
|
543
|
+
disconnectLocally();
|
|
544
|
+
}
|
|
484
545
|
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// targeted disconnect but wallet gone: leave the active connection, status,
|
|
550
|
+
// and persisted key untouched.
|
|
551
|
+
if (!isWalletStillAvailable(target)) {
|
|
552
|
+
return; // already gone / not authorized — forgiving no-op
|
|
553
|
+
}
|
|
554
|
+
const refreshedTarget = refreshUiWallet(target);
|
|
555
|
+
if (!refreshedTarget.features.includes(StandardDisconnect)) {
|
|
556
|
+
return; // can't deauthorize without standard:disconnect — no-op
|
|
557
|
+
}
|
|
558
|
+
// Guard against a concurrent select/connect into this wallet for the await;
|
|
559
|
+
// restore the prior marker afterward (it may belong to an active disconnect).
|
|
560
|
+
const prevDisconnecting = disconnectingWalletName;
|
|
561
|
+
disconnectingWalletName = refreshedTarget.name;
|
|
562
|
+
try {
|
|
563
|
+
const disconnectFeature = getWalletFeature(
|
|
564
|
+
refreshedTarget,
|
|
565
|
+
StandardDisconnect,
|
|
566
|
+
) as StandardDisconnectFeature[typeof StandardDisconnect];
|
|
567
|
+
await disconnectFeature.disconnect();
|
|
485
568
|
} finally {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
569
|
+
if (disconnectingWalletName === refreshedTarget.name) {
|
|
570
|
+
disconnectingWalletName = prevDisconnecting;
|
|
571
|
+
}
|
|
572
|
+
// Reflect the now-empty accounts immediately — but only if the store
|
|
573
|
+
// is still live. If it was disposed while `standard:disconnect`
|
|
574
|
+
// awaited, the disposer has already torn down every wallet-event
|
|
575
|
+
// subscription and cleared the map; deauthorizing the target
|
|
576
|
+
// regenerated its handle, so `reconcileWalletList` returns a fresh
|
|
577
|
+
// array, and letting `updateState` run here would re-invoke
|
|
578
|
+
// `syncWalletEventSubscriptions` and re-subscribe every wallet into
|
|
579
|
+
// the cleared map — listeners the disposer can no longer clean up.
|
|
580
|
+
if (!disposed) {
|
|
581
|
+
updateState({ wallets: reconcileWalletList() });
|
|
489
582
|
}
|
|
490
583
|
}
|
|
491
584
|
}
|
|
492
585
|
|
|
493
586
|
function disconnectLocally(): void {
|
|
494
|
-
walletEventsCleanup?.();
|
|
495
|
-
walletEventsCleanup = null;
|
|
496
587
|
disconnectingWalletName = null;
|
|
497
588
|
|
|
498
589
|
updateState({
|
|
@@ -532,14 +623,34 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
532
623
|
}
|
|
533
624
|
|
|
534
625
|
function selectAccount(account: UiWalletAccount): void {
|
|
535
|
-
|
|
536
|
-
|
|
626
|
+
// Derive the owning wallet from the account handle. `getWalletForHandle`
|
|
627
|
+
// keys a WeakMap on handle identity, so this is unambiguous even when the
|
|
628
|
+
// same address is authorized in two wallets. A stale/unregistered handle
|
|
629
|
+
// throws — surface it as ACCOUNT_NOT_AVAILABLE.
|
|
630
|
+
let owner: UiWallet;
|
|
631
|
+
try {
|
|
632
|
+
owner = getOrCreateUiWalletForStandardWallet(getWalletForHandle(account));
|
|
633
|
+
} catch {
|
|
634
|
+
throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {
|
|
635
|
+
account: account.address,
|
|
636
|
+
operation: 'selectAccount',
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The owner must be currently authorized (discovered + passes the filter).
|
|
641
|
+
if (!isWalletStillAvailable(owner)) {
|
|
642
|
+
throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {
|
|
643
|
+
account: account.address,
|
|
644
|
+
operation: 'selectAccount',
|
|
645
|
+
});
|
|
537
646
|
}
|
|
538
|
-
|
|
539
|
-
|
|
647
|
+
|
|
648
|
+
// Reject selecting into a wallet that is mid-disconnect.
|
|
649
|
+
if (owner.name === disconnectingWalletName) {
|
|
540
650
|
throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });
|
|
541
651
|
}
|
|
542
|
-
|
|
652
|
+
|
|
653
|
+
const refreshed = refreshUiWallet(owner);
|
|
543
654
|
const selectedAccount = refreshed.accounts.find(a => a.address === account.address);
|
|
544
655
|
if (!selectedAccount) {
|
|
545
656
|
throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {
|
|
@@ -547,16 +658,20 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
547
658
|
operation: 'selectAccount',
|
|
548
659
|
});
|
|
549
660
|
}
|
|
661
|
+
|
|
550
662
|
userHasSelected = true;
|
|
551
|
-
//
|
|
552
|
-
// connect/signIn
|
|
663
|
+
// Supersede any in-flight connect/signIn.
|
|
553
664
|
++connectGeneration;
|
|
554
|
-
|
|
555
|
-
//
|
|
665
|
+
|
|
666
|
+
// No-op fast path: the same account is already active.
|
|
556
667
|
if (selectedAccount === state.account && refreshed === state.connectedWallet) {
|
|
557
668
|
updateState({ status: 'connected' });
|
|
558
669
|
return;
|
|
559
670
|
}
|
|
671
|
+
|
|
672
|
+
// Switch the active connection (owner may differ from the current one). The
|
|
673
|
+
// previously-active wallet is left authorized — we never disconnect it. Its
|
|
674
|
+
// event subscription persists, so switching back stays live.
|
|
560
675
|
const signer = tryCreateSigner(selectedAccount);
|
|
561
676
|
updateState({ account: selectedAccount, connectedWallet: refreshed, signer, status: 'connected' });
|
|
562
677
|
persistAccount(selectedAccount, refreshed);
|
|
@@ -564,7 +679,7 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
564
679
|
|
|
565
680
|
// -- Message signing ---------------------------------------------------
|
|
566
681
|
|
|
567
|
-
async function signMessage(message:
|
|
682
|
+
async function signMessage(message: ReadonlyUint8Array, options?: WalletActionOptions): Promise<SignatureBytes> {
|
|
568
683
|
options?.abortSignal?.throwIfAborted();
|
|
569
684
|
const { connectedWallet, account } = state;
|
|
570
685
|
if (!connectedWallet || !account) {
|
|
@@ -581,7 +696,9 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
581
696
|
account,
|
|
582
697
|
SolanaSignMessage,
|
|
583
698
|
) as SolanaSignMessageFeature[typeof SolanaSignMessage];
|
|
584
|
-
|
|
699
|
+
// The wallet-standard `SolanaSignMessageInput.message` is a mutable `Uint8Array`; signing
|
|
700
|
+
// only reads the bytes, so coerce the read-only input at this single boundary.
|
|
701
|
+
const [output] = await signMessageFeature.signMessage({ account, message: message as Uint8Array });
|
|
585
702
|
return output.signature as SignatureBytes;
|
|
586
703
|
}
|
|
587
704
|
|
|
@@ -842,6 +959,7 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
842
959
|
signerListeners.delete(listener);
|
|
843
960
|
};
|
|
844
961
|
},
|
|
962
|
+
whenReady,
|
|
845
963
|
[Symbol.dispose]: () => {
|
|
846
964
|
// Invalidate any in-flight connect/signIn/silent-reconnect so they
|
|
847
965
|
// bail instead of resuming after disposal (which would re-subscribe
|
|
@@ -853,11 +971,20 @@ export function createWalletStore(config: WalletPluginConfig): WalletStore {
|
|
|
853
971
|
connectGeneration++;
|
|
854
972
|
unsubRegister();
|
|
855
973
|
unsubUnregister();
|
|
856
|
-
|
|
857
|
-
|
|
974
|
+
for (const cleanup of walletEventSubscriptions.values()) {
|
|
975
|
+
cleanup();
|
|
976
|
+
}
|
|
977
|
+
walletEventSubscriptions.clear();
|
|
858
978
|
cancelReconnect();
|
|
859
979
|
listeners.clear();
|
|
860
980
|
signerListeners.clear();
|
|
981
|
+
// A store disposed mid-warm-up will never finish warming up, so
|
|
982
|
+
// settle the status to 'disconnected'.
|
|
983
|
+
// Runs after the listener sets are cleared, so no one is notified of
|
|
984
|
+
// the final transition.
|
|
985
|
+
if (isWalletWarmingUp(state.status)) {
|
|
986
|
+
updateState({ status: 'disconnected' });
|
|
987
|
+
}
|
|
861
988
|
},
|
|
862
989
|
};
|
|
863
990
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MessageSigner, SignatureBytes, TransactionSigner } from '@solana/kit';
|
|
1
|
+
import type { MessageSigner, ReadonlyUint8Array, SignatureBytes, TransactionSigner } from '@solana/kit';
|
|
2
2
|
import type { SolanaChain } from '@solana/wallet-standard-chains';
|
|
3
3
|
import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';
|
|
4
4
|
import type { IdentifierString } from '@wallet-standard/base';
|
|
@@ -244,13 +244,19 @@ export type WalletNamespace = {
|
|
|
244
244
|
connect: (wallet: UiWallet, options?: WalletActionOptions) => Promise<readonly UiWalletAccount[]>;
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
|
-
* Disconnect
|
|
247
|
+
* Disconnect a wallet. Calls `standard:disconnect` if supported.
|
|
248
|
+
*
|
|
249
|
+
* `wallet` defaults to the active wallet. Passing a non-active, currently
|
|
250
|
+
* authorized wallet deauthorizes it (calling `standard:disconnect` if
|
|
251
|
+
* supported) while leaving the active connection untouched. A `wallet`
|
|
252
|
+
* that doesn't support `standard:disconnect`, or one that is already
|
|
253
|
+
* unauthorized/unregistered, is a forgiving no-op.
|
|
248
254
|
*
|
|
249
255
|
* @throws `options.abortSignal.reason` if the signal is already aborted
|
|
250
256
|
* when the action is called. Aborts after the wallet call has been
|
|
251
257
|
* dispatched do not take effect.
|
|
252
258
|
*/
|
|
253
|
-
disconnect: (options?: WalletActionOptions) => Promise<void>;
|
|
259
|
+
disconnect: (wallet?: UiWallet, options?: WalletActionOptions) => Promise<void>;
|
|
254
260
|
|
|
255
261
|
// -- State --
|
|
256
262
|
/**
|
|
@@ -263,13 +269,19 @@ export type WalletNamespace = {
|
|
|
263
269
|
getState: () => WalletState;
|
|
264
270
|
|
|
265
271
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
272
|
+
* Select an account, creating and caching a new signer for it.
|
|
273
|
+
*
|
|
274
|
+
* The account may belong to any currently authorized wallet, not only the
|
|
275
|
+
* active one. When it belongs to a different authorized wallet, the active
|
|
276
|
+
* connection switches to that wallet synchronously (no prompt), leaving the
|
|
277
|
+
* previously active wallet authorized. If the store is currently 'disconnected',
|
|
278
|
+
* this allows transitioning to 'connected' synchronously.
|
|
268
279
|
*
|
|
269
|
-
* @throws `SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED)` if no wallet is
|
|
270
|
-
* connected, or the connected wallet is currently disconnecting.
|
|
271
280
|
* @throws `SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE)` if the
|
|
272
|
-
*
|
|
281
|
+
* account's handle cannot be resolved to a currently authorized wallet, or
|
|
282
|
+
* the account is not among that wallet's accounts.
|
|
283
|
+
* @throws `SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED)` if the account's
|
|
284
|
+
* wallet is currently disconnecting.
|
|
273
285
|
*/
|
|
274
286
|
selectAccount: (account: UiWalletAccount) => void;
|
|
275
287
|
|
|
@@ -321,7 +333,7 @@ export type WalletNamespace = {
|
|
|
321
333
|
* when the action is called. Aborts after the wallet call has been
|
|
322
334
|
* dispatched do not take effect.
|
|
323
335
|
*/
|
|
324
|
-
signMessage: (message:
|
|
336
|
+
signMessage: (message: ReadonlyUint8Array, options?: WalletActionOptions) => Promise<SignatureBytes>;
|
|
325
337
|
|
|
326
338
|
/**
|
|
327
339
|
* Subscribe to any wallet state change. Compatible with React's
|
|
@@ -335,6 +347,43 @@ export type WalletNamespace = {
|
|
|
335
347
|
* ```
|
|
336
348
|
*/
|
|
337
349
|
subscribe: (listener: () => void) => () => void;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* **Advanced.** Resolves once the initial connection attempt has settled —
|
|
353
|
+
* i.e. {@link getState}'s `status` is no longer `'pending'` or
|
|
354
|
+
* `'reconnecting'`.
|
|
355
|
+
*
|
|
356
|
+
* You only need this if your app **rebuilds the client at runtime** (for
|
|
357
|
+
* example, to change chain). Each freshly built client runs a silent
|
|
358
|
+
* auto-reconnect that briefly passes through `'pending'`/`'reconnecting'`;
|
|
359
|
+
* awaiting this lets you hold the previous UI until the new client is ready
|
|
360
|
+
* instead of flashing a "reconnecting" state on every swap. Most apps never
|
|
361
|
+
* call it — subscribe to `status` with {@link subscribe} instead.
|
|
362
|
+
*
|
|
363
|
+
* Does **not** wait for user-initiated `connect`/`disconnect`/`signIn`
|
|
364
|
+
* (those pass through `'connecting'`/`'disconnecting'`, which count as
|
|
365
|
+
* ready), so a normal connect never makes this block. While transient it
|
|
366
|
+
* returns the **same** promise reference on every call, so successive
|
|
367
|
+
* awaits during one warm-up share a single settlement.
|
|
368
|
+
*
|
|
369
|
+
* Disposing the client mid-warm-up settles the status to `'disconnected'`,
|
|
370
|
+
* so a pending `whenReady()` resolves rather than hanging. Check
|
|
371
|
+
* {@link getState} after awaiting if you need to distinguish a ready
|
|
372
|
+
* connection from a disposed (or never-connected) client.
|
|
373
|
+
*
|
|
374
|
+
* @returns A promise that resolves with no value once the wallet is past its
|
|
375
|
+
* initial `'pending'`/`'reconnecting'` warm-up. Already resolved when the
|
|
376
|
+
* status is not transient.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```ts
|
|
380
|
+
* // On a chain switch, hold the old UI until the rebuilt client is ready:
|
|
381
|
+
* const next = createClient().use(walletSigner({ chain }));
|
|
382
|
+
* await next.wallet.whenReady();
|
|
383
|
+
* setClient(next);
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
whenReady: () => Promise<void>;
|
|
338
387
|
};
|
|
339
388
|
|
|
340
389
|
/**
|
package/src/wallet.ts
CHANGED
|
@@ -22,7 +22,9 @@ function defineSignerGetter(
|
|
|
22
22
|
): void {
|
|
23
23
|
Object.defineProperty(additions, property, {
|
|
24
24
|
configurable: true,
|
|
25
|
-
enumerable:
|
|
25
|
+
// Non-enumerable on purpose: this getter throws when no signer is connected, so we
|
|
26
|
+
// don't want things like Object.entries to read it.
|
|
27
|
+
enumerable: false,
|
|
26
28
|
get() {
|
|
27
29
|
const state = store.getState();
|
|
28
30
|
if (!state.connected) {
|