Skip to content

Scaffold/design for the wallet plugin#173

Open
mcintyre94 wants to merge 13 commits intomainfrom
wallet-plugin-scaffold
Open

Scaffold/design for the wallet plugin#173
mcintyre94 wants to merge 13 commits intomainfrom
wallet-plugin-scaffold

Conversation

@mcintyre94
Copy link
Copy Markdown
Member

@mcintyre94 mcintyre94 commented Mar 31, 2026

This PR adds the scaffolding and design of the proposed wallet plugin, without yet implementing it

The plugin is designed to expose the interface expected by reactive frameworks, eg React's useSyncExternalStore.

Example (Vanilla JS)

const client = createLocalClient().use(walletSigner{ chain: 'solana:localnet' })

// call render whenever something changes
client.wallet.subscribe(render)

function render() {
  const walletState = client.wallet.getState()

  // get all registered wallets
  const wallets = walletState.wallets

  // connect to one
  const someWallet = wallets[0];
  client.wallet.connect(someWallet)

  // are we connected?
  if(walletState.connected) {}

  // do we have a signer?
  if(walletState.connected?.signer) {}

  // if we do, send a transaction
  await client.sendTransaction(
    getTransferSolInstruction({
      destination: DEMO_RECIPIENT,
      amount: lamports(10_000_000n), // 0.01 SOL
   }),
);

// transaction fee payer was selected wallet, set to client.payer
// transferSol source was selected wallet, set to client.identity

// can use `walletWithoutSigner` to not set these, or `walletPayer` or `walletIdentity` to just set one

Key functionality

  • Access all registered wallets. This filters by chain and standard:connect feature, as well as an optional filter that can be passed to the plugin. Returns UiWallet[]
  • After connect(wallet: UiWallet), we derive a Signer for the connected wallet account. This will include all available signer features of modify + send tx, sign + send tx, modify + sign message. See Kit function: https://github.com/anza-xyz/kit/blob/f055201c2dd3a4a69b9894d66b622ae81c13b8cd/packages/wallet-account-signer/src/wallet-account-signer.ts#L68. If the wallet account is read-only, we don't set the signer but do set wallet + account
  • Four plugins: walletSigner, walletIdentity, walletPayer, walletWithoutSigner. All provide an identical client.wallet interface. The only difference is which of client.identity and client.payer they control.
  • The connected wallet + account is persisted for autoconnect. By default it persists to localstorage, but anything that matches a subset of the WebStorage API works. Can be disabled by passing storage: null, and is always disabled on the server. autoConnect can be independently disabled while using storage too.
  • State is exposed through the getState() and subscribe() APIs. getState returns a referentially stable object describing the state, and subscribe allows registering listeners that are notified whenever state changes. Together these enable a reactive store like React's useSyncExternalStore, Svelte's readable and Vue's reactive state.
  • The plugin is SSR safe. On the server it will always be in a pending state, and all functions will throw. But consumers can safely include it in their SSR pass for simplicity. The pending state is intentionally distinct from disconnected to allow eg not rendering/rendering a skeleton and avoid a UI flash from disconnected -> connected

Implementation notes/TODOs

  • We haven't published Kit since merging withCleanup, so for now this plugin implements [Symbol.Dispose] directly.

  • ~~I think we will need one new error added to Kit. For now this is just included as an error class in the plugin. I think it's justifiable in Kit because it'd be useful to any similar wallet functionality. ~~

SOLANA_ERROR__WALLET__NOT_CONNECTED
// context: { operation: string }
// message: "Cannot $operation: no wallet connected"
  • We'd also want to add ClientWithWallet to kit plugin-interfaces later, and move some of the types there (I noticed we can't do this because of wallet-standard types, so ClientWithWallet will be exported from the plugin)

More examples

I've had Claude generate a simple example app for React, Svelte, Vue and Vanilla JS: https://github.com/anza-xyz/kit-plugins/tree/wallet-plugin-demos-1/examples/wallet-demo/src . These are intended to show how different frameworks can bind to the state in this plugin and expose it to apps. They are not intended to be representative of how actual apps would be written with kit-plugins, but hopefully show how thin the framework-specific layer for wallets can be. One of my design goals is to never again upset someone who doesn't want to use React!


For full transparency, this PR includes a spec file. This is long and the result of lots of back and forth with an LLM, I don't expect anyone to review it and will later remove it from the repo. But wanted to include it here to give more context about how this will all work!

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 31, 2026

⚠️ No Changeset found

Latest commit: c3c284e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch from d88512c to 2435b0e Compare March 31, 2026 17:40
Copy link
Copy Markdown
Contributor

@amilz amilz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. Great work w/ this!

Comment thread packages/kit-plugin-wallet/package.json
Comment thread packages/kit-plugin-wallet/src/index.ts Outdated
Comment thread packages/kit-plugin-wallet/README.md Outdated
Comment thread wallet-plugin-spec.md
Comment thread wallet-plugin-spec.md Outdated
Comment thread wallet-plugin-spec.md Outdated
Comment thread packages/kit-plugin-wallet/src/index.ts Outdated
Comment thread packages/kit-plugin-wallet/src/index.ts Outdated
Comment thread packages/kit-plugin-wallet/src/index.ts
Comment thread packages/kit-plugin-wallet/README.md Outdated
@lorisleiva
Copy link
Copy Markdown
Member

@trevor-cortex

Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR scaffolds the @solana/kit-plugin-wallet package — a framework-agnostic wallet plugin that manages wallet discovery, connection lifecycle, signer creation, and payer integration via wallet-standard. The implementation is stub-only (createWalletStore throws not implemented), so this is purely the type system, API surface, configuration design, and package scaffolding.

The design is very well thought out. The API surface is clean, the subscribe/getSnapshot pattern for framework integration is the right call, and the SSR story with pending status is elegant. The spec included in wallet-plugin-spec.md provides excellent context for the implementation that will follow.

Key observations

Things that look great

  • hasSigner in snapshot vs signer in WalletConnection — smart separation to avoid unnecessary re-renders while still exposing the actual signer for instruction building.
  • pending vs disconnected distinction — avoids the UI flash problem on SSR/auto-reconnect. This is a common pain point in wallet adapters.
  • usePayer with fallback capture — the dynamic getter approach with fallback to the previous client.payer is clean. Plugin ordering (payer before wallet) is well-documented.
  • WalletStorage interface — supporting async backends while being compatible with localStorage/sessionStorage directly is practical.
  • signIn overloads — the two-argument form (SIWS-as-connect) is a nice ergonomic touch.

Things to watch during implementation

  • Race conditions in connect/disconnect — concurrent calls (user spam-clicks connect, or connects during auto-reconnect) need careful state machine transitions. The spec mentions this but it'll be the trickiest part of createWalletStore.
  • Signer caching invalidation — when a wallet's accounts change externally (via standard:events), cached signers for removed accounts need cleanup. The WalletStoreState has a single signer field, so this should be straightforward, but worth verifying during implementation.
  • fallbackPayer capture timing — the spec shows capturing client.payer at plugin install time. If the prior payer is itself a dynamic getter (from another plugin), you'll want to capture the descriptor, not the value, to preserve the getter chain. Or at least document that the fallback is evaluated once.

Minor items

See inline comments below.

Notes for subsequent reviewers

  • The wallet-plugin-spec.md (1299 lines) is context-only and not intended for permanent inclusion in the repo — author has noted this.
  • The createWalletStore function is the heart of this plugin and is entirely stubbed. The real review work comes when that's implemented.
  • @solana/kit is listed as both a peerDependency and resolved as a regular dependency in the lockfile — worth checking if this is intentional or if it should only be a peer dep.
  • The extendClient import from @solana/kit is present but unused in the current scaffold (the plugin function implementation is truncated in the diff, but the store is a stub anyway).

Comment thread packages/kit-plugin-wallet/src/index.ts Outdated
Comment thread packages/kit-plugin-wallet/src/index.ts Outdated
Comment thread packages/kit-plugin-wallet/src/index.ts Outdated
Comment thread packages/kit-plugin-wallet/package.json
Comment thread packages/kit-plugin-wallet/README.md
Comment thread packages/kit-plugin-wallet/src/index.ts Outdated
Comment thread packages/kit-plugin-wallet/tsconfig.json
@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch 3 times, most recently from 9ff1b02 to ca6a002 Compare April 2, 2026 14:42
@mcintyre94
Copy link
Copy Markdown
Member Author

I've updated this PR with a new commit: ca6a002

This is a large update and addresses a lot of what we've talked about:

  • All state is removed from client.wallet and is only available on the snapshot: client.wallet.getSnapshot()
  • The signer is available on the snapshot
  • Split into two plugins, wallet and walletWithPayer. Both provide the same client.wallet interface, walletAsPayer also controls client.payer. There is no fallback to a previous payer any more

I've also updated the description, and the demos - which already mostly used the reactive subscribe/snapshot APIs so have minimal changes.

Also dependabot bumped us to Kit 6.6.0, so I've updated that dependency here and used withCleanup for the cleanup function.

Comment thread wallet-plugin-spec.md Outdated
@mcintyre94
Copy link
Copy Markdown
Member Author

I've made another update:

  • getSnapshot renamed to getState as discussed
  • the internal store is simplified to only cover the public API, meaning we just return it as wallet. The only extra thing it includes is the implementation of Disposable which is harmless. We remove buildWalletNamespace which mapped from the store to the wallet public API
  • signer is now only defined in one place: connected?.signer, was previously duplicated in the store

@mcintyre94
Copy link
Copy Markdown
Member Author

To flag, I've realised that ClientWithWallet cannot be moved to Kit @solana/plugin-interfaces as I planned, because of its dependencies on types from @wallet-standard/ui. Technically this is all just types and won't affect bundles, but it'd add an NPM dependency that we probably don't want.

For now I think it'll be best to just leave these types in the plugin itself, as the current version of this PR does.

@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch from b6c41e3 to 963206a Compare April 7, 2026 16:53
@mcintyre94
Copy link
Copy Markdown
Member Author

mcintyre94 commented Apr 7, 2026

I've split the code into three different files (types/store/wallet). This is annoying for the diff, but better to do it here than in the implementation. Note that types and wallet are exported from the package, store is not.

Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR scaffolds the @solana/kit-plugin-wallet package, establishing the type system, plugin entry points (wallet and walletAsPayer), and a stub store — without yet implementing the wallet discovery/connection logic. The design is well thought out: clean separation between types, store, and plugin wiring; a subscribe/getState API that maps directly onto framework primitives (useSyncExternalStore, Svelte readable, Vue shallowRef); SSR safety via the pending state; and a pluggable storage adapter for persistence.

Key observations

Design looks solid. The split into wallet() vs walletAsPayer() is clean and gives users the right level of control. The WalletSigner type alias is a nice addition. The Object.defineProperty approach for the dynamic payer getter in walletAsPayer is the right call.

Types are thorough and well-documented. The JSDoc throughout types.ts is excellent — each type, field, and method has clear documentation with examples where helpful.

Things to watch for in subsequent reviews / during implementation

  • The WalletNamespace JSDoc references {@link getSnapshot} (line 137 of types.ts) but the method is called getState. Should be {@link getState}.
  • The payer getter in walletAsPayer coerces null signer to undefined via ?? undefined. This is correct for the type (TransactionSigner | undefined) but worth a comment explaining why — connected?.signer can be null for read-only wallets, while the payer slot expects undefined for "no payer".
  • @solana/kit appears as a peerDependency but also gets resolved as a regular dependency in the lockfile (the packages/kit-plugin-wallet section in pnpm-lock.yaml lists it under dependencies). This seems to be a pnpm workspace resolution artifact, but worth double-checking it doesn't cause duplicate bundling.
  • The spec file (wallet-plugin-spec.md) is at the repo root. The PR description mentions it'll be removed later — might be worth adding a TODO comment or tracking issue so it doesn't get forgotten.
  • tsconfig.json uses lib: [] which is consistent with the existing kit-plugins package. The implementation will need DOM types (for localStorage, window checks, etc.) — this will presumably be handled via the __BROWSER__ global declarations and conditional compilation through tsup, matching the existing pattern.

Comment thread packages/kit-plugin-wallet/src/types.ts
Comment thread packages/kit-plugin-wallet/src/wallet.ts Outdated
Comment thread packages/kit-plugin-wallet/src/types.ts Outdated
Comment thread packages/kit-plugin-wallet/src/types.ts
@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch from 2acdb76 to 548685c Compare April 10, 2026 09:53
Copy link
Copy Markdown
Member Author

mcintyre94 commented Apr 10, 2026

@mcintyre94
Copy link
Copy Markdown
Member Author

Added another commit: cb6f652

Addresses issues found in the spec/scaffold while implementing the plugin:

  • signMessage now uses the feature directly, instead of using the cached connected.signer. Academically this means that a wallet that has the signMessage feature but no transaction feature, works correctly. But more practically this simplifies the implementation and removes an error case that doesn't cleanly fit the existing errors
  • SIWS is simplified to require passing UiWallet, the same as connect. Previously we had a version that automatically signs in an account on the connected wallet. This doesn't really need a special case (you can just pass connected.wallet) and removing it simplifies the code a lot. I think SIWS as connect is the majority use case too.
  • Fixes a bug where we try to extend a frozen object from extendClient

@mcintyre94
Copy link
Copy Markdown
Member Author

@trevor-cortex

Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — scaffold looks good

This is a re-review after changes since my last pass. The previous issues I raised (stale getSnapshot JSDoc link, missing comment on the ?? undefined coercion) have both been addressed. The code is in good shape for a scaffold PR.

What changed

  • File structure split into types.ts, wallet.ts, store.ts — much cleaner than the single-file version.
  • The wallet.ts plugin functions now properly use withCleanup wrapping extendClient rather than spread.
  • store.ts is a clean stub with the right shape (WalletStore = WalletNamespace & { [Symbol.dispose] }).

New observations

  • README signIn docs vs types mismatch: The README shows two forms — signIn(input?) (connected wallet) and signIn(wallet, input?) (connect-and-sign-in). But WalletNamespace.signIn in types.ts only has the two-argument form (wallet: UiWallet, input: SolanaSignInInput). The README implies an overload that doesn't exist in the types. The spec also only has the two-argument form and explains the design choice. I'd update the README to match.
  • store.ts not exported from index.ts — intentional and correct (marked @internal), just confirming I noticed.

Previously raised — still relevant

  • Error class resilience: WalletNotConnectedError still relies on this.name = 'WalletNotConnectedError' which can break under minification if someone does error.name === 'WalletNotConnectedError'. A static code property (like Kit's SolanaError) would make this robust. The TODO comment about moving this to Kit proper addresses this long-term.
  • @solana/kit in lockfile: Still appears as a resolved regular dep under packages/kit-plugin-wallet in the lockfile despite being listed only as a peerDependency in package.json. Likely a pnpm workspace artifact, but worth verifying it doesn't cause duplicate bundling.

For subsequent reviewers

  • The spec file at the repo root (wallet-plugin-spec.md) is intended to be temporary — the PR description mentions removing it later.
  • The tsconfig.json uses lib: [] — this is consistent with the repo pattern; DOM types come in via the __BROWSER__ compile-time constants and tsup's multi-target build.
  • The store implementation is entirely stubbed — the interesting review will be when createWalletStore gets its real implementation.

Comment thread packages/kit-plugin-wallet/README.md
@mcintyre94 mcintyre94 requested review from amilz and lorisleiva April 13, 2026 18:01
Comment thread wallet-plugin-spec.md Outdated
Comment thread wallet-plugin-spec.md Outdated
Comment on lines +344 to +346
get() {
return store.getState().connected?.signer ?? undefined;
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a plugin that adds Partial<ClientWithPayer> to the client is going to be very useful because none of the plugin requiring ClientWithPayer will accept it.

I think it's best to return a ClientWithPayer directly and, when the wallet isn't connected, throw an error when trying to access client.payer with a message showing the wallet status.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with doing it that way too. I didn't for the same concern you mentioned above - claiming to implement the type but then throwing a runtime error.

I'm not sure if this changes in your open PRs but I based this decision on the fact that plugins like rpcTransactionPlanner were using Partial<ClientWithPayer> as their input client type, so it would be accepted for that.

I don't feel strongly either way though, especially if that has changed/will change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That rpcTransactionPlanner design is more of an exception than the rule and to be honest, now that we're fully leaning into the plugin design (i.e. not using client factories), I should totally change that plugin requirement so it requests ClientWithPayer instead of throwing a runtime error when both are not provided.

Just about every other plugin (including the program plugins) will end up requesting ClientWithPayer so Partial<ClientWithPayer> isn't gonna take us very far.

I think if the goal is making sure SSR hydration works but really you're designing the client plugin, then I think we'll make the developer experience more enjoyable with ClientWithPayer.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I'm happy to update to set ClientWithPayer (and also ClientWithIdentity when we add that)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a quick PR for the Partial<ClientWithPayer> removal: #192.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this, it now returns ClientWithPayer. I'll open a PR on Kit with the errors proposed for the case where it isn't set.

Comment thread wallet-plugin-spec.md Outdated
Comment on lines +127 to +142
// Wallet as payer — most dApps
const client = createEmptyClient()
.use(rpc('https://api.mainnet-beta.solana.com'))
.use(walletAsPayer({ chain: 'solana:mainnet' }))
.use(planAndSendTransactions());
// client.payer is TransactionSigner | undefined

// Wallet alongside a static payer
const client = createEmptyClient()
.use(rpc('https://api.mainnet-beta.solana.com'))
.use(payer(backendKeypair))
.use(wallet({ chain: 'solana:mainnet' }))
.use(planAndSendTransactions());
// client.payer is TransactionSigner (from payer plugin, untouched)
// client.wallet.getState().connected?.signer for manual use
```
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline, the new @solana/kit-plugin-signer package provides a family of helpers for each kind of signer strategy. For instance, generatedPayer/generatedIdentity/generatedSigner all add a new generated signer to the client such that the first one sets client.payer, the second sets client.identity and the last sets both using the same signer object (the one most users will end up using).

As such, we should aim to unify these APIs since the wallet plugin is able to set signers on the client too.

The main difference is the wallet plugin also sets client.wallet which acts as a "control centre" for managing connected wallets. Therefore we could go with something like:

  • walletSigner: Sets client.wallet, client.payer and client.identity (the most popular entrypoint).
  • walletIdentity: Sets client.wallet, client.identity (the second most popular entrypoint for dApps that have a different payer signer).
  • walletPayer: Sets client.wallet, client.payer.
  • walletWithoutSigner: Sets client.wallet only (more verbose to discourage users to default to using it).

One other thing to consider is: could we technically select two different wallets as the client.identity and client.payer signers? If so, we should consider if it'd make sense for client.identity and client.payer to own their own client.wallet stores internally (or a subset of it).

Copy link
Copy Markdown
Member Author

@mcintyre94 mcintyre94 Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that if we add all these entrypoints then we should make the walletWithoutSigner more verbose.

I'd lean against exposing different client.wallet stores for identity and payer though. Technically they could be independent signers, both implemented by a wallet. But that doesn't require different client.wallet stores, the Signer contains everything they need, the fact they internally call to a wallet is implementation detail.

While the client.wallet store only stores one selected wallet/account/signer in client.wallet.connected, you have access to all connected accounts for all wallets in client.wallet.getState().wallets. And you can use createSignerFromWalletAccount from Kit to get a signer for any of those.

So I think in the unusual case that you want to have signers for multiple wallet accounts and set them as payer/identity, you have everything you need with just one client.wallet store, and your own custom identity/payer logic.

The one thing that would be arguably useful is to allow them to store the equivalent of client.wallet.connected, ie the wallet/account/signer. But I'm not sure how we could structure payer/identity to handle that without complicating all the non-wallet use cases, so I think app specific logic is probably the right way to deal with that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that having a single client.wallet store makes sense even if you have multiple selected wallets as client.payer and client.identity. It's just the connected state I'm unsure of (if we end up supporting multiple wallet selected). Theoretically there's more information you could store inside a client.payer and client.identity so that could move there. I'm just not sure what the high level API looks like for connecting two different wallets. Is that two plugins: walletPayer and walletIdentity and the second call doesn't override the existing client.wallet? How do you then select a payer wallet versus an identity wallet?

Maybe this is out-of-scope for now though and, if this is needed in the future, it would materialise as a separate plugin. I just wanted to make sure we were not missing a design piece by not analysing this use-case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice an app juggling multiple connected wallets would probably want to just use wallet-standard directly:

const connectFeature = getWalletFeature(
    uiWallet,
    StandardConnect,
) as StandardConnectFeature[typeof StandardConnect];

// Snapshot existing accounts before connect.
const existingAccounts = [...uiWallet.accounts];

const updatedAccounts = await connectFeature.connect();

// persist connected accounts / convert to signer if needed

So you'd bypass client.wallet.getState().connected, and just be using the wallet plugin for tracking registered wallets/all their connected accounts/event handlers etc. and handle mapping them to signers yourself.

There is a missing piece there for setting those signers to identity/payer though, but I think that being out of scope is ok. If you wanted to do it yourself you'd probably create a custom plugin that exposes a setter for each of them, and then in your app logic where you handle connecting wallets you'd call those setters.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to the suggestion at the top of this thread:

  • walletSigner: Sets client.wallet, client.payer and client.identity (the most popular entrypoint).
  • walletIdentity: Sets client.wallet, client.identity (the second most popular entrypoint for dApps that have a different payer signer).
  • walletPayer: Sets client.wallet, client.payer.
  • walletWithoutSigner: Sets client.wallet only (more verbose to discourage users to default to using it).

Comment thread wallet-plugin-spec.md

**Descriptor-preserving composition.** The plugin uses `extendClient` from plugin-core to build the client, preserving getters and symbol-keyed properties from previous plugins. The updated `addUse` also preserves descriptors through `.use()` calls. Downstream plugins should use `extendClient` rather than spread to avoid flattening the dynamic payer getter.

**Single chain per client.** Signers are bound to a specific chain at creation time. Switching chains requires a different RPC endpoint too. One client = one network.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See for me if we have the design principle "1 client = 1 network", then I don't think it's unexpected to also have the design principle "1 client = 1 environment" (i.e. client vs server).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the same as #173 (comment) - treating SSR as an environment where you should be able to use the same client as the browser for developer convenience.

@mcintyre94 mcintyre94 changed the base branch from main to graphite-base/173 April 14, 2026 17:28
@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch from cb6f652 to 20a2e6b Compare April 14, 2026 17:29
@mcintyre94 mcintyre94 changed the base branch from graphite-base/173 to 04-12-bump_solana_kit_peer_dependency_to_6.8.0 April 14, 2026 17:29
@lorisleiva lorisleiva changed the base branch from 04-12-bump_solana_kit_peer_dependency_to_6.8.0 to graphite-base/173 April 14, 2026 17:32
@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch from f41ae68 to 4e36bfb Compare April 15, 2026 11:09
@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch from 4e36bfb to 67b7f09 Compare April 15, 2026 14:28
@mcintyre94 mcintyre94 changed the base branch from graphite-base/173 to main April 15, 2026 14:28
@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch from 4362565 to 536a1d3 Compare April 15, 2026 15:29
Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — Scaffold/design for the wallet plugin

What changed since last review

Big design evolution: the two plugins (wallet, walletAsPayer) are now four (walletSigner, walletPayer, walletIdentity, walletWithoutSigner). This is a much cleaner design — the 4-way matrix covering payer × identity gives users exactly the right level of control without needing to understand the internals.

The createPlugin + defineSignerGetter pattern in wallet.ts is elegant — one generic factory with the signer properties parameterized. The two distinct error cases (no wallet connected vs. read-only wallet) in the getter are a nice touch.

Issues

🐛 Type bug: Omit<T, 'identity' | 'payer' | 'wallet'> is over-broad — see inline comment. This will strip payer/identity from the input type even for plugins that don't replace them.

📝 Stale JSDoc references — a couple of {@link wallet} / {@link walletAsPayer} references survived the rename. See inline comments.

Still relevant from prior reviews

  • @solana/kit appearing as a resolved regular dependency in the lockfile despite being declared as peerDependencies only in package.json. The packages/kit-plugin-wallet section in pnpm-lock.yaml lists it under dependencies. This is likely a pnpm workspace resolution artifact, but worth verifying it doesn't cause duplicate bundling for consumers.

client: T,
): Disposable & Omit<T, 'identity' | 'payer' | 'wallet'> & TAdditions => {
if ('wallet' in client) {
throw new Error(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Omit<T, 'identity' | 'payer' | 'wallet'> always strips both identity and payer from the incoming type, even for plugins that don't set them. This means:

const client = createClient()
  .use(identity(myKeypair))           // sets client.identity
  .use(walletWithoutSigner({ ... })); // should only add wallet

client.identity; // ← type error, but the value is still there at runtime

Same issue with walletPayer stripping identity and walletIdentity stripping payer from the preceding chain.

The Omit should only strip properties that the plugin actually re-adds. One approach:

function createPlugin<TAdditions extends ClientWithWallet>(
  config: WalletPluginConfig,
  signerProperties: string[],
) {
  return <T extends object & { wallet?: never }>(
    client: T,
  ): Disposable & Omit<T, keyof TAdditions> & TAdditions => { ... };
}

That way walletWithoutSigner only omits wallet, walletPayer omits wallet + payer, etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed + added typetests to verify the behaviour of types of these plugins (🤞 these won't summon copilot spam)

Comment thread packages/kit-plugin-wallet/src/types.ts
Comment thread packages/kit-plugin-wallet/src/types.ts
Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — Scaffold/design for the wallet plugin

Both issues from my last review have been addressed:

  • Omit type bug — Fixed. createPlugin now correctly uses Omit<T, keyof TAdditions>, so each variant only strips the properties it actually adds. ✅
  • Stale JSDoc references — Fixed. WalletPluginConfig docs now correctly reference walletSigner, walletPayer, walletIdentity, walletWithoutSigner. ✅

Type tests

Great addition. The new wallet-typetest.ts covers all four plugin variants and validates:

  • Each plugin adds the expected properties (payer, identity, wallet)
  • Plugins don't strip previously-set properties they don't own (the Omit fix)
  • Only one wallet plugin can be used per client (via { wallet?: never } constraint)

The scaffold looks good to me — no new issues found.

Still noting (non-blocking)

  • @solana/kit appearing as a resolved regular dependency in pnpm-lock.yaml despite being declared peer-only in package.json. Likely a pnpm workspace artifact, but worth a quick check that consumers don't get a duplicate bundle.

@mcintyre94 mcintyre94 requested a review from lorisleiva April 15, 2026 18:16
Copy link
Copy Markdown
Member

@lorisleiva lorisleiva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for making the walletIdentity/walletPayer/walletSigner changes to be consistent with @solana/kit-plugin-signers.

@mcintyre94
Copy link
Copy Markdown
Member Author

Pushed a small change to widen the type of chain to SolanaChain | IdentifierString & {}. Ie you can pass any IdentifierString, matching wallet-standard, and SolanaChain ones will still autocomplete in IDEs. For now we'll need to cast to SolanaChain when we pass it to Kit, but that's just a type issue too. I have a PR open to widen the type equivalently in Kit, but we can land that independently. anza-xyz/kit#1548

@mcintyre94 mcintyre94 force-pushed the wallet-plugin-scaffold branch from bd0ca1c to c3c284e Compare April 20, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants