Implement kit-plugin-wallet store, types, and tests#191
Conversation
🦋 Changeset detectedLatest commit: 0a1f210 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
trevor-cortex
left a comment
There was a problem hiding this comment.
Summary
This PR implements the wallet plugin's core store (createWalletStore) with wallet discovery via wallet-standard, connection lifecycle management, signer creation, auto-connect persistence, subscribable state for UI frameworks, and signIn (SIWS-as-connect). Tests are thorough and well-structured with a clean mock setup.
Overall this is solid work — the architecture is clean, the SSR guard is well thought out, and the test coverage is excellent. I have a few things to flag, most notably around the signIn error handling and some concurrency edge cases.
Key Things to Watch
-
signInis missing error handling and status transition — Unlikeconnect(),signIn()doesn't setstatus: 'connecting'before the async call, and has no try/catch to calldisconnectLocally()on failure. IfsignInthrows, the store status is never reset. -
setStatenotifies listeners even when nothing changed — The listener notification runs unconditionally outside the snapshot-change check. ForuseSyncExternalStorethis is mostly fine (React does its ownObject.ischeck), but other frameworks that re-render on every listener call will get unnecessary updates. -
No concurrency guard on
connect/signIn— Callingconnect(walletA)thenconnect(walletB)before the first resolves can leave leaked event subscriptions from walletA, sincesetConnectedfor walletA runs and sets upwalletEventsCleanupbefore walletB overwrites it. -
Late registration handler fires concurrent async IIFEs — If multiple wallets register in quick succession, multiple
attemptSilentReconnectcalls can race.
Notes for Subsequent Reviewers
- The persistence format
walletName:accountAddressuseslastIndexOf(':')for parsing, which correctly handles wallet names containing colons (base58 addresses don't contain:). The malformed-key case is tested. - The SSR stub returns a frozen singleton snapshot, which is important for
useSyncExternalStorereferential stability. - The
walletAsPayerplugin usesObject.definePropertyfor a dynamicpayergetter sinceextendClientfreezes the result — clever approach. - Auto-connect tests use fake timers to control the async flow — good pattern.
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
94a2553 to
509369e
Compare
trevor-cortex
left a comment
There was a problem hiding this comment.
Re-review
Great progress — all four issues from my first review have been addressed:
- ✅
signInnow has properstatus: 'connecting'transition, try/catch, anddisconnectLocally()on error - ✅
setStateonly notifies listeners when snapshot-relevant fields actually change - ✅
connectGenerationcounter guards against concurrent connect/signIn races (with thorough tests) - ✅ Late-registration reconnect handler has
.catch()for error recovery
The concurrency test coverage is excellent — stale rejection, concurrent connects, cross-connect/signIn superseding, and auto-reconnect vs explicit connect are all well covered.
Remaining items
I found one state consistency bug and a couple of smaller things:
-
Signer/account mismatch when both
accountsandfeatureschange in a single event —handleFeaturesChangedreadsstate.account(the old account) instead of the potentially-updated account fromhandleAccountsChanged, so the signer can be computed for the wrong account. (See inline comment.) -
disconnect()finally block can clobber a concurrentconnect()— Sincedisconnect()doesn't participate in the generation counter, itsfinally { disconnectLocally() }can wipe out aconnect()that completed while the disconnect was in flight. Low priority since it's an unusual user interaction pattern, but worth noting. -
No validation in
selectAccount— AnyUiWalletAccountis accepted regardless of whether it belongs to the connected wallet. If a foreign account is passed, the persisted key will have a mismatched wallet name + address. Not a crash, but surprising behavior.
None of these are blockers — #1 is the most impactful but only triggers on simultaneous account+feature wallet events (uncommon). Overall the implementation is clean and well-tested.
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
509369e to
8036d5b
Compare
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
8036d5b to
0388316
Compare
cb6f652 to
20a2e6b
Compare
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
4e36bfb to
67b7f09
Compare
0388316 to
6036391
Compare
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
6036391 to
31ee110
Compare
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
31ee110 to
c43e4ab
Compare
4362565 to
536a1d3
Compare
trevor-cortex
left a comment
There was a problem hiding this comment.
Re-review (3rd pass)
All 7 issues from the previous two reviews are now addressed with corresponding test coverage:
- ✅
signInerror handling and status transition (from review 1) - ✅
setStateonly notifies on actual changes (from review 1) - ✅
connectGenerationguards connect/signIn/disconnect concurrency (from review 1) - ✅ Late-registration reconnect has
.catch()recovery (from review 1) - ✅ Signer/account mismatch fixed —
handleFeaturesChangedis now skipped when accounts also changed in the same event, so the signer is always computed for the correct account (from review 2) - ✅
disconnect()participates in generation counter — capturesconnectGenerationbefore the async call and checks it infinally, preventing a stale disconnect from clobbering a concurrent connect (from review 2) - ✅
selectAccountvalidates account belongs to wallet — refreshes the wallet and checks the address exists in the account list before accepting (from review 2)
The new tests are thorough — 'disconnect does not clobber concurrent connect', 'uses new account signer when both accounts and features change', 'selectAccount throws for account not in wallet', and 'still reconnects if wallet appears after timeout' all directly verify the fixes.
The wallet plugin architecture is clean: four plugin variants (walletSigner, walletPayer, walletIdentity, walletWithoutSigner) built on a shared createPlugin helper with defineSignerGetter for dynamic payer/identity getters. SSR stub, persistence, auto-reconnect with timeout, and the concurrency model are all solid.
LGTM 🚢
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
c43e4ab to
0f56ab2
Compare
Addresses review feedback from #191: - signIn now sets status: 'connecting' and wraps in try/catch with disconnectLocally on failure, matching connect's error handling pattern. - setState only notifies listeners when the snapshot actually changed, avoiding unnecessary re-renders in non-React frameworks. - Added connectGeneration counter to prevent concurrent connect/signIn/attemptSilentReconnect calls from leaking event subscriptions or overwriting each other's state. - Removed redundant currentWallet && guard in disconnect. New tests for signIn status transitions, signIn rejection, listener suppression on no-op setState, and concurrent connect/signIn race conditions.
aac9881 to
bac08de
Compare
bac08de to
9986851
Compare
0257fd6 to
0a1f210
Compare
| '@solana/kit-plugin-wallet': minor | ||
| --- | ||
|
|
||
| Add `walletSigner`, `walletIdentity`, `walletPayer`, and `walletWithoutSigner` plugins for framework-agnostic wallet management using wallet-standard. Provides wallet discovery, connection lifecycle, signer creation, auto-connect persistence, subscribable state for UI frameworks, and dynamic payer/identity integration. |
There was a problem hiding this comment.
Note to self: make this changeset more of a new plugin announcement, with code examples

Implements the wallet plugin's core store (createWalletStore) with wallet discovery, connection lifecycle, signer creation, and subscribable state management.
Tests are split into store tests (state management, lifecycle) and wallet tests (plugin integration, payer getter).
Also add to list of plugins in root readme and contributing