-
Notifications
You must be signed in to change notification settings - Fork 7
Scaffold/design for the wallet plugin #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mcintyre94
wants to merge
14
commits into
main
Choose a base branch
from
wallet-plugin-scaffold
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
67d8b85
Scaffold/design for the wallet plugin
mcintyre94 c406383
Update wallet plugin scaffold
mcintyre94 6821061
Refactor to use withCleanup for plugin cleanup
mcintyre94 7815ccd
Update wallet plugin scaffold design
mcintyre94 a6ec04e
Split code into store/types/wallet
mcintyre94 dece063
Minor tweaks
mcintyre94 167170f
Update spec to address issues found when implementing
mcintyre94 df60be2
Use WalletNotConnectedError from Kit
mcintyre94 c81c821
Refactor so `walletAsPayer` returns a `ClientWithPayer` and throws if…
mcintyre94 1e5a58f
Add walletIdentity and walletSigner
mcintyre94 2e0288b
Update readme for scaffold
mcintyre94 579c6fc
Fix types of wallet plugins + add typetests
mcintyre94 4d120ee
Loosen the type of `chain` to allow non-solana chains
mcintyre94 996e912
Add abort signals to async wallet operations
mcintyre94 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| dist/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| dist/ | ||
| test-ledger/ | ||
| target/ | ||
| CHANGELOG.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2025 Anza | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining | ||
| a copy of this software and associated documentation files (the | ||
| "Software"), to deal in the Software without restriction, including | ||
| without limitation the rights to use, copy, modify, merge, publish, | ||
| distribute, sublicense, and/or sell copies of the Software, and to | ||
| permit persons to whom the Software is furnished to do so, subject to | ||
| the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be | ||
| included in all copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
| LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
| OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,265 @@ | ||
| # Kit Plugins ➤ Wallet | ||
|
|
||
| [![npm][npm-image]][npm-url] | ||
| [![npm-downloads][npm-downloads-image]][npm-url] | ||
|
|
||
| [npm-downloads-image]: https://img.shields.io/npm/dm/@solana/kit-plugin-wallet.svg?style=flat | ||
| [npm-image]: https://img.shields.io/npm/v/@solana/kit-plugin-wallet.svg?style=flat&label=%40solana%2Fkit-plugin-wallet | ||
| [npm-url]: https://www.npmjs.com/package/@solana/kit-plugin-wallet | ||
|
|
||
| This package provides plugins that add browser wallet support to your Kit clients using [wallet-standard](https://github.com/wallet-standard/wallet-standard). They handle wallet discovery, connection lifecycle, account selection, and signer creation. | ||
|
|
||
| Four plugin functions are exported — each adds a `client.wallet` namespace, but they differ in how the connected wallet's signer is exposed on the client: | ||
|
|
||
| | Plugin | `client.payer` | `client.identity` | Use case | | ||
| | --------------------- | -------------- | ----------------- | ------------------------------------------------------------- | | ||
| | `walletSigner` | wallet signer | wallet signer | Most dApps — wallet pays fees and signs as the user identity. | | ||
| | `walletPayer` | wallet signer | — | Wallet pays fees; identity is managed separately. | | ||
| | `walletIdentity` | — | wallet signer | Backend relayer pays fees; wallet provides user identity. | | ||
| | `walletWithoutSigner` | — | — | Wallet state only; payer and identity are managed separately. | | ||
|
|
||
| ## Installation | ||
|
|
||
| ```sh | ||
| pnpm install @solana/kit-plugin-wallet | ||
| ``` | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```ts | ||
| import { createClient } from '@solana/kit'; | ||
| import { solanaRpc } from '@solana/kit-plugin-rpc'; | ||
| import { walletSigner } from '@solana/kit-plugin-wallet'; | ||
| import { planAndSendTransactions } from '@solana/kit-plugin-instruction-plan'; | ||
|
|
||
| const client = createClient() | ||
| .use(walletSigner({ chain: 'solana:mainnet' })) | ||
| .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) | ||
| .use(planAndSendTransactions()); | ||
|
|
||
| // Read discovered wallets from state | ||
| const { wallets } = client.wallet.getState(); | ||
|
|
||
| // Connect a wallet | ||
| await client.wallet.connect(wallets[0]); | ||
|
|
||
| // client.payer and client.identity are now the connected wallet's signer | ||
| await client.sendTransaction([myInstruction]); | ||
| ``` | ||
|
|
||
| ## `walletSigner` plugin | ||
|
|
||
| Syncs the connected wallet's signer to both `client.payer` and `client.identity`. This is the most common choice for dApps where the user's wallet pays fees and signs as the transaction identity. | ||
|
|
||
| ```ts | ||
| import { walletSigner } from '@solana/kit-plugin-wallet'; | ||
|
|
||
| const client = createClient() | ||
| .use(walletSigner({ chain: 'solana:mainnet' })) | ||
| .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) | ||
| .use(planAndSendTransactions()); | ||
| ``` | ||
|
|
||
| ## `walletPayer` plugin | ||
|
|
||
| Syncs the connected wallet's signer to `client.payer` only. Use this when you need the wallet as the fee payer but don't need `client.identity`. For most dApps, prefer `walletSigner` which sets both. | ||
|
|
||
| ```ts | ||
| import { walletPayer } from '@solana/kit-plugin-wallet'; | ||
|
|
||
| const client = createClient() | ||
| .use(walletPayer({ chain: 'solana:mainnet' })) | ||
| .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) | ||
| .use(planAndSendTransactions()); | ||
| ``` | ||
|
|
||
| ## `walletIdentity` plugin | ||
|
|
||
| Syncs the connected wallet's signer to `client.identity` only. Use this when a separate payer (e.g. a backend relayer) pays transaction fees, but the user's wallet is needed as the identity signer. | ||
|
|
||
| ```ts | ||
| import { payer } from '@solana/kit-plugin-signer'; | ||
| import { walletIdentity } from '@solana/kit-plugin-wallet'; | ||
|
|
||
| const client = createClient() | ||
| .use(payer(relayerKeypair)) | ||
| .use(walletIdentity({ chain: 'solana:mainnet' })) | ||
| .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) | ||
| .use(planAndSendTransactions()); | ||
|
|
||
| // client.payer is always relayerKeypair | ||
| // client.identity is the connected wallet's signer | ||
| ``` | ||
|
|
||
| ## `walletWithoutSigner` plugin | ||
|
|
||
| Adds `client.wallet` without setting `client.payer` or `client.identity`. Use this alongside separate `payer()` and/or `identity()` plugins, or when the wallet's signer is used explicitly in instructions. | ||
|
|
||
| ```ts | ||
| import { payer } from '@solana/kit-plugin-signer'; | ||
| import { walletWithoutSigner } from '@solana/kit-plugin-wallet'; | ||
|
|
||
| const client = createClient() | ||
| .use(payer(backendKeypair)) | ||
| .use(walletWithoutSigner({ chain: 'solana:mainnet' })) | ||
| .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) | ||
| .use(planAndSendTransactions()); | ||
|
|
||
| // client.payer is always backendKeypair | ||
| // client.wallet.getState().connected?.signer for manual use | ||
| ``` | ||
|
|
||
| ## State and actions | ||
|
|
||
| All wallet state is accessed via `client.wallet.getState()`, which returns a referentially stable `WalletState` object (new reference only when something changes). | ||
|
|
||
| - **`getState().wallets`** — All discovered wallets that support the configured chain. | ||
|
|
||
| ```ts | ||
| const { wallets } = client.wallet.getState(); | ||
| for (const w of wallets) { | ||
| console.log(w.name, w.icon); | ||
| } | ||
| ``` | ||
|
|
||
| - **`getState().connected`** — The active connection (wallet, account, and signer), or `null` when disconnected. | ||
|
|
||
| ```ts | ||
| const { connected } = client.wallet.getState(); | ||
| console.log(connected?.account.address); | ||
| ``` | ||
|
|
||
| - **`getState().status`** — The current connection status: `'pending'`, `'disconnected'`, `'connecting'`, `'connected'`, `'disconnecting'`, or `'reconnecting'`. | ||
|
|
||
| - **`connect(wallet)`** — Connect to a wallet and select the first newly authorized account. | ||
|
|
||
| ```ts | ||
| const accounts = await client.wallet.connect(selectedWallet); | ||
| ``` | ||
|
|
||
| - **`disconnect()`** — Disconnect the active wallet. | ||
|
|
||
| - **`selectAccount(account)`** — Switch to a different account within an already-authorized wallet without reconnecting. | ||
|
|
||
| ```ts | ||
| client.wallet.selectAccount(accounts[0]); | ||
| ``` | ||
|
|
||
| - **`signMessage(message)`** — Sign a raw message with the connected account. | ||
|
|
||
| ```ts | ||
| const signature = await client.wallet.signMessage(new TextEncoder().encode('Hello')); | ||
| ``` | ||
|
|
||
| - **`signIn(wallet, input)`** — Sign In With Solana (SIWS-as-connect). Connects the wallet, calls `solana:signIn`, and sets up full connection state. Pass `{}` for `input` if no sign-in customization is needed. To sign in with the already-connected wallet, pass `getState().connected.wallet`. | ||
|
|
||
| ```ts | ||
| const output = await client.wallet.signIn(selectedWallet, { domain: window.location.host }); | ||
| ``` | ||
|
|
||
| ## Framework integration | ||
|
|
||
| The plugin exposes `subscribe` and `getState` for binding wallet state to any UI framework. | ||
|
|
||
| **React** — use `useSyncExternalStore` for concurrent-mode-safe rendering: | ||
|
|
||
| ```tsx | ||
| import { useSyncExternalStore } from 'react'; | ||
|
|
||
| function useWalletState() { | ||
| return useSyncExternalStore(client.wallet.subscribe, client.wallet.getState); | ||
| } | ||
|
|
||
| function App() { | ||
| const { wallets, connected, status } = useWalletState(); | ||
|
mcintyre94 marked this conversation as resolved.
|
||
|
|
||
| if (status === 'pending') return null; // avoid flashing a connect button before auto-reconnect | ||
|
|
||
| if (!connected) { | ||
| return wallets.map(w => ( | ||
| <button key={w.name} onClick={() => client.wallet.connect(w)}> | ||
| {w.name} | ||
| </button> | ||
| )); | ||
| } | ||
|
|
||
| return <p>Connected: {connected.account.address}</p>; | ||
| } | ||
| ``` | ||
|
|
||
| **Vue** — use a `shallowRef` composable: | ||
|
|
||
| ```ts | ||
| import { onMounted, onUnmounted, shallowRef } from 'vue'; | ||
|
|
||
| function useWalletState() { | ||
| const state = shallowRef(client.wallet.getState()); | ||
| onMounted(() => { | ||
| const unsub = client.wallet.subscribe(() => { | ||
| state.value = client.wallet.getState(); | ||
| }); | ||
| onUnmounted(unsub); | ||
| }); | ||
| return state; | ||
| } | ||
| ``` | ||
|
|
||
| **Svelte** — wrap in a `readable` store: | ||
|
|
||
| ```ts | ||
| import { readable } from 'svelte/store'; | ||
|
|
||
| export const walletState = readable(client.wallet.getState(), set => { | ||
| return client.wallet.subscribe(() => set(client.wallet.getState())); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| ```ts | ||
| walletSigner({ | ||
| chain: 'solana:mainnet', // required | ||
| storage: sessionStorage, // default: localStorage (null to disable) | ||
| storageKey: 'my-app:wallet', // default: 'kit-wallet' | ||
| autoConnect: false, // default: true (disable silent reconnect) | ||
| filter: w => w.features.includes('solana:signAndSendTransaction'), // optional | ||
| }); | ||
| ``` | ||
|
|
||
| ## Persistence | ||
|
|
||
| By default the plugin uses `localStorage` to remember the last connected wallet and auto-reconnects on the next page load. Pass `storage: null` to disable, or provide a custom adapter (e.g. `sessionStorage` or an IndexedDB wrapper). | ||
|
|
||
| ## SSR / server-side rendering | ||
|
|
||
| All four wallet plugins are safe to include in a shared client that runs on both server and browser. On the server, `status` stays `'pending'` permanently, all actions throw, and no registry listeners or storage reads are made. In the browser the plugin initializes normally. | ||
|
|
||
| ```ts | ||
| const client = createClient() | ||
| .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) | ||
| .use(walletSigner({ chain: 'solana:mainnet' })) | ||
| .use(planAndSendTransactions()); | ||
|
|
||
| // Server: status === 'pending', client.payer throws | ||
| // Browser: auto-connects, client.payer becomes the wallet signer | ||
| ``` | ||
|
|
||
| ## Cleanup | ||
|
|
||
| The plugin implements `[Symbol.dispose]`, so it integrates with the `using` declaration or explicit disposal: | ||
|
|
||
| ```ts | ||
| { | ||
| using client = createClient().use(walletSigner({ chain: 'solana:mainnet' })); | ||
| // registry listeners and storage subscriptions are cleaned up on scope exit | ||
| } | ||
| ``` | ||
|
|
||
| Or call `[Symbol.dispose]()` explicitly when you're done with the client: | ||
|
|
||
| ```ts | ||
| const client = createClient().use(walletSigner({ chain: 'solana:mainnet' })); | ||
|
|
||
| // ... later, when the client is no longer needed | ||
| client[Symbol.dispose](); | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| { | ||
| "name": "@solana/kit-plugin-wallet", | ||
| "version": "0.1.0", | ||
| "description": "Wallet connection plugin for Kit clients", | ||
| "exports": { | ||
| "types": "./dist/types/index.d.ts", | ||
| "react-native": "./dist/index.react-native.mjs", | ||
| "browser": { | ||
| "import": "./dist/index.browser.mjs", | ||
| "require": "./dist/index.browser.cjs" | ||
| }, | ||
| "node": { | ||
| "import": "./dist/index.node.mjs", | ||
| "require": "./dist/index.node.cjs" | ||
| } | ||
| }, | ||
| "browser": { | ||
| "./dist/index.node.cjs": "./dist/index.browser.cjs", | ||
| "./dist/index.node.mjs": "./dist/index.browser.mjs" | ||
| }, | ||
| "main": "./dist/index.node.cjs", | ||
| "module": "./dist/index.node.mjs", | ||
| "react-native": "./dist/index.react-native.mjs", | ||
| "types": "./dist/types/index.d.ts", | ||
| "type": "commonjs", | ||
| "files": [ | ||
| "./dist/types", | ||
| "./dist/index.*", | ||
| "./src/" | ||
| ], | ||
| "sideEffects": false, | ||
| "keywords": [ | ||
|
mcintyre94 marked this conversation as resolved.
|
||
| "solana", | ||
| "kit", | ||
| "plugin", | ||
| "wallet", | ||
| "wallet-adapter", | ||
| "wallet-standard", | ||
| "signer" | ||
| ], | ||
| "scripts": { | ||
| "build": "rimraf dist && tsup && tsc -p ./tsconfig.declarations.json", | ||
| "dev": "vitest --project node", | ||
| "lint": "eslint . && prettier --check .", | ||
| "lint:fix": "eslint --fix . && prettier --write .", | ||
| "test": "pnpm test:types && pnpm test:treeshakability", | ||
| "test:treeshakability": "for file in dist/index.*.mjs; do agadoo $file; done", | ||
| "test:types": "tsc --noEmit" | ||
| }, | ||
| "peerDependencies": { | ||
|
mcintyre94 marked this conversation as resolved.
|
||
| "@solana/kit": "^6.6.0" | ||
| }, | ||
| "dependencies": { | ||
| "@solana/wallet-account-signer": "^6.6.0", | ||
| "@solana/wallet-standard-chains": "^1.1.1", | ||
| "@solana/wallet-standard-features": "^1.3.0", | ||
| "@wallet-standard/app": "^1.1.0", | ||
| "@wallet-standard/base": "^1.1.0", | ||
| "@wallet-standard/errors": "^0.1.1", | ||
| "@wallet-standard/features": "^1.1.0", | ||
| "@wallet-standard/ui": "^1.0.1", | ||
| "@wallet-standard/ui-features": "^1.0.1", | ||
| "@wallet-standard/ui-registry": "^1.0.1" | ||
| }, | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/anza-xyz/kit-plugins" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/anza-xyz/kit-plugins/issues" | ||
| }, | ||
| "browserslist": [ | ||
| "supports bigint and not dead", | ||
| "maintained node versions" | ||
| ] | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.