Add SubscribeToPayer/Identity, and use for wallet plugin#200
Conversation
🦋 Changeset detectedLatest commit: 107a603 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
e2dbe51 to
f4d3f27
Compare
7025c19 to
7808e13
Compare
trevor-cortex
left a comment
There was a problem hiding this comment.
Clean, well-scoped addition. The core design — defining the subscribeTo<Capability> convention in kit-plugin-signer so consumers can duck-type on it without binding to any specific plugin — is the right call, and the alternatives section in the PR description convinced me too. This looks good to me pending a couple of minor observations below.
What this does well
- Filtering is done at the source on the signer identity itself (
connected?.signer ?? null), so unrelated store notifications (wallet discovery, status transitions likepending → disconnected) don't spuriously wake listeners. That's the important correctness property and the tests verify it. - Each subscriber tracks its own
lastbaseline, so an unsubscribe/re-subscribe cycle correctly resets. No shared mutable state between consumers. - Types are threaded through all three variants (
walletSigner,walletPayer,walletIdentity) correctly, andwalletWithoutSignerdeliberately installs nothing — the type tests pin this down with@ts-expect-error. - Docblocks explicitly document over-notification as acceptable and point at
useSyncExternalStore's snapshot-equality behavior as the backstop. Good expectation-setting for the contract. - Changeset is
minoron both packages, which is correct for pre-1.0 feature additions under your convention.
Things for subsequent reviewers to verify
- The store's
listeners.forEach(l => l())runs listeners synchronously; a throwing user listener would still propagate and potentially skip siblings. That's pre-existing behavior fromstore.tsand not introduced here, but worth being aware of now that subscribe hooks are exposed as a public API surface. - On
walletSigner, bothsubscribeToPayerandsubscribeToIdentitysubscribe to the same underlying signer identity, so a single account switch fires both listeners — which matches the contract since bothclient.payerandclient.identityresolve to the same signer. ThewalletSigner fires both subscriptionstest covers this. - No test currently exercises
selectAccountfiring the listener, but the filter is purely on signer reference identity so it should behave correctly by construction. - The convention is structural: anything shaped like
{ subscribeToPayer: (listener) => () => void }satisfiesClientWithSubscribeToPayer. That's intentional per the docblock — worth keeping in mind if a future plugin wants to opt into the same contract.
Minor nit: the changeset file is missing a trailing newline (\ No newline at end of file in the diff). Not worth blocking on; Prettier or a follow-up will catch it.
| last = curr; | ||
| listener(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Small observation, no change requested: the subscribe factory is allocated per-property inside the loop. With only two capabilities that's fine, but if the signerProperties list ever grows or if you add more subscribeTo* capabilities, it might be worth hoisting this into a named helper (e.g. createSignerChangeSubscriber(store)) so the branching and closure allocation live in one place. Purely stylistic.
| * | ||
| * Calling the returned unsubscribe more than once is safe — it must be | ||
| * idempotent. | ||
| */ |
There was a problem hiding this comment.
The docblock guarantees idempotent unsubscribe ("Calling the returned unsubscribe more than once is safe"). The current wallet-plugin implementation delegates to store.subscribe, whose unsubscribe is a listeners.delete(listener) — naturally idempotent. Worth noting that any future plugin adopting this convention needs to uphold that contract; consider whether you want a type-level or test-level marker to make that obvious. Not blocking.
| unsubscribe(); | ||
| await client.wallet.disconnect(); | ||
| expect(listener).toHaveBeenCalledTimes(1); | ||
| }); |
There was a problem hiding this comment.
Consider adding a test that selectAccount (switching to a different account on the same wallet) fires the listener — it exercises a code path distinct from connect/disconnect and would lock in the "fires on any signer identity change" contract. Not blocking; the implementation filters on signer reference equality so it should Just Work.
There was a problem hiding this comment.
Good idea, added this test
7808e13 to
f821c81
Compare
|
Draft for now. Types moved to anza-xyz/kit#1551, this PR would just use them in the wallet plugin. Will require a Kit release + dependency bump here |
f4d3f27 to
d59d768
Compare
02d3f6f to
9940096
Compare
d59d768 to
2495858
Compare
2495858 to
6d8785c
Compare
9940096 to
107a603
Compare

This PR adds new types
ClientWithSubscribeToPayerandClientWithSubscribeToIdentity. These allow a plugin (such as the wallet plugin) to notify subscribers when they modify these fields, without those consumers needing to know that a particular plugin has made them reactive.It's already the case that
client.payercan be made dynamic by the wallet plugin, by using aget()function. This retains compatibility with thepayer: TransactionSignertype. But in order to make this useful for a reactive UI that may for example render based on the current value ofclient.payer, we also need to be able to subscribe to changes.This PR wires up these subscribers in the wallet plugin, so that it notifies them when it updates the signer.
Reactive consumers can then use
client.subscribeToPayer(() => {})to receive these notifications.Alternatives considered:
kit-plugin-signerand allow consumers to be agnostic to how the fields are made dynamicpayerandidentityactually act as a reactive store (with subscribe and getValue functions), this would be a breaking change and annoying for most uses. This PR provides the same functionality only to consumers that need to react to changes to these values. This is likely only reactive UI - anything usingclient.payeralready gets the correct updated value