-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| dist/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| dist/ | ||
| test-ledger/ | ||
| target/ | ||
| CHANGELOG.md |
| 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. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,211 @@ | ||||||
| # 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 a plugin that adds browser wallet support to your Kit clients using [wallet-standard](https://github.com/wallet-standard/wallet-standard). It handles wallet discovery, connection lifecycle, account selection, and signer creation — and syncs the connected wallet's signer to `client.payer` automatically. | ||||||
|
|
||||||
| ## Installation | ||||||
|
|
||||||
| ```sh | ||||||
| pnpm install @solana/kit-plugin-wallet | ||||||
| ``` | ||||||
|
|
||||||
| ## `wallet` plugin | ||||||
|
|
||||||
| The wallet plugin adds a `client.wallet` namespace with all wallet state and actions, and wires the connected wallet's signer to `client.payer`. | ||||||
|
|
||||||
| ### Setup | ||||||
|
|
||||||
| ```ts | ||||||
| import { createEmptyClient } from '@solana/kit'; | ||||||
| import { rpc } from '@solana/kit-plugin-rpc'; | ||||||
| import { wallet } from '@solana/kit-plugin-wallet'; | ||||||
|
|
||||||
| const client = createEmptyClient() | ||||||
| .use(rpc('https://api.mainnet-beta.solana.com')) | ||||||
| .use(wallet({ chain: 'solana:mainnet' })); | ||||||
| ``` | ||||||
|
|
||||||
| Once a wallet is connected, `client.payer` resolves to the wallet's signer and you can pass it directly to transaction instructions: | ||||||
|
|
||||||
| ```ts | ||||||
| import { getTransferSolInstruction } from '@solana-program/system'; | ||||||
| import { lamports } from '@solana/kit'; | ||||||
|
|
||||||
| // Read registered wallets | ||||||
| const selectedWallet = client.wallet.wallets[0]; | ||||||
|
|
||||||
| // Connect a wallet | ||||||
| await client.wallet.connect(selectedWallet); | ||||||
|
|
||||||
| // client.payer is now the connected wallet's signer | ||||||
| await client.sendTransaction( | ||||||
| getTransferSolInstruction({ | ||||||
| source: client.payer, | ||||||
| destination: recipientAddress, | ||||||
| amount: lamports(10_000_000n), | ||||||
| }), | ||||||
| ); | ||||||
| ``` | ||||||
|
|
||||||
| ### Features | ||||||
|
|
||||||
| - `client.wallet.wallets` — All discovered wallets that support the configured chain. | ||||||
|
|
||||||
| ```ts | ||||||
| for (const w of client.wallet.wallets) { | ||||||
| console.log(w.name, w.icon); | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| - `client.wallet.connected` — The active connection (wallet, account, and signer), or `null` when disconnected. | ||||||
|
|
||||||
| ```ts | ||||||
| const { wallet, account, signer } = client.wallet.connected ?? {}; | ||||||
| console.log(account?.address); | ||||||
| ``` | ||||||
|
|
||||||
| - `client.wallet.status` — The current connection status: `'pending'`, `'disconnected'`, `'connecting'`, `'connected'`, `'disconnecting'`, or `'reconnecting'`. | ||||||
|
|
||||||
| - `client.wallet.connect(wallet)` — Connect to a wallet and select the first newly authorized account. | ||||||
|
|
||||||
| ```ts | ||||||
| const accounts = await client.wallet.connect(selectedWallet); | ||||||
| ``` | ||||||
|
|
||||||
| - `client.wallet.disconnect()` — Disconnect the active wallet. | ||||||
|
|
||||||
| - `client.wallet.selectAccount(account)` — Switch to a different account within an already-authorized wallet without reconnecting. | ||||||
|
|
||||||
| ```ts | ||||||
| client.wallet.selectAccount(selectedWallet); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be account w/i the accounts returned from connected??
Suggested change
|
||||||
| ``` | ||||||
|
|
||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. client.wallet.selectAccount(selectedWallet);This example passes const accounts = await client.wallet.connect(selectedWallet);
client.wallet.selectAccount(accounts[1]); // switch to second account |
||||||
| - `client.wallet.signMessage(message)` — Sign a raw message with the connected account. | ||||||
|
|
||||||
| ```ts | ||||||
| const signature = await client.wallet.signMessage(new TextEncoder().encode('Hello')); | ||||||
| ``` | ||||||
|
|
||||||
| - `client.wallet.signIn(input?)` / `client.wallet.signIn(wallet, input?)` — Sign In With Solana (SIWS). The two-argument form connects the wallet implicitly. | ||||||
|
|
||||||
| ```ts | ||||||
| // Sign in with the already-connected wallet | ||||||
| const output = await client.wallet.signIn({ domain: window.location.host }); | ||||||
|
|
||||||
| // Sign in and connect in one step | ||||||
| const output = await client.wallet.signIn(selectedWallet, { domain: window.location.host }); | ||||||
| ``` | ||||||
|
|
||||||
| ### Framework integration | ||||||
|
|
||||||
| The plugin exposes `subscribe` and `getSnapshot` 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.getSnapshot); | ||||||
| } | ||||||
|
|
||||||
| function App() { | ||||||
| const { wallets, connected, status } = useWalletState(); | ||||||
|
|
||||||
| 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.getSnapshot()); | ||||||
| onMounted(() => { | ||||||
| const unsub = client.wallet.subscribe(() => { | ||||||
| state.value = client.wallet.getSnapshot(); | ||||||
| }); | ||||||
| onUnmounted(unsub); | ||||||
| }); | ||||||
| return state; | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| **Svelte** — wrap in a `readable` store: | ||||||
|
|
||||||
| ```ts | ||||||
| import { readable } from 'svelte/store'; | ||||||
|
|
||||||
| export const walletState = readable(client.wallet.getSnapshot(), set => { | ||||||
| return client.wallet.subscribe(() => set(client.wallet.getSnapshot())); | ||||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| ### 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): | ||||||
|
|
||||||
| ```ts | ||||||
| wallet({ | ||||||
| chain: 'solana:mainnet', | ||||||
| storage: sessionStorage, // use session storage instead | ||||||
| storageKey: 'my-app:wallet', // custom key (default: 'kit-wallet') | ||||||
| autoConnect: false, // disable silent reconnect | ||||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| ### SSR / server-side rendering | ||||||
|
|
||||||
| The plugin is safe to include in a shared client that runs on both server and browser. On the server, `status` stays `'pending'` permanently, all actions throw `WalletNotConnectedError`, and no registry listeners or storage reads are made. In the browser the plugin initializes normally. | ||||||
|
|
||||||
| ```ts | ||||||
| // This client chain works on both server and browser. | ||||||
| const client = createEmptyClient() | ||||||
| .use(rpc('https://api.mainnet-beta.solana.com')) | ||||||
| .use(payer(serverKeypair)) | ||||||
| .use(wallet({ chain: 'solana:mainnet' })); | ||||||
|
|
||||||
| // Server: client.wallet.status === 'pending', client.payer === serverKeypair | ||||||
| // Browser: auto-connects, client.payer becomes the wallet signer | ||||||
| ``` | ||||||
|
|
||||||
| ### Wallet discovery filtering | ||||||
|
|
||||||
| Use the `filter` option to restrict which wallets appear in `client.wallet.wallets`: | ||||||
|
|
||||||
| ```ts | ||||||
| wallet({ | ||||||
| chain: 'solana:mainnet', | ||||||
| // Only show wallets that support signAndSendTransaction | ||||||
| filter: w => w.features.includes('solana:signAndSendTransaction'), | ||||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| ### Cleanup | ||||||
|
|
||||||
| The plugin implements `[Symbol.dispose]`, so it integrates with the `using` declaration or explicit disposal: | ||||||
|
|
||||||
| ```ts | ||||||
| { | ||||||
| using client = createEmptyClient().use(wallet({ chain: 'solana:mainnet' })); | ||||||
| // registry listeners and storage subscriptions are cleaned up on scope exit | ||||||
| } | ||||||
| ``` | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| { | ||
| "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": [ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure how much these matter but might make sese to add "wallet-adapter" since this is a big piece of migrating from wallet adapter |
||
| "solana", | ||
| "kit", | ||
| "plugin", | ||
| "wallet", | ||
| "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": { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| "@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/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": "http://github.com/anza-xyz/kit-plugins/issues" | ||
| }, | ||
| "browserslist": [ | ||
| "supports bigint and not dead", | ||
| "maintained node versions" | ||
| ] | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this work w/o adding txn planner/executors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - you get a
TransactionSignerand can use it with any Kit APIs.I did mean to flag the decision not to have
client.wallet.signTransaction()though. I think we should just use the signer APIs and also offeringsignTransactionwould be confusing.I think most apps will want to just use their transaction plan/execute plugin
If you don't, you can just add
client.wallet.connected.signer(will probably move a little but same idea) as a signer on the transaction and sign/send it however you likeAt the lowest level you have
client.wallet.connected.signer.signAndModifyTransaction([...])which is basically the same API we'd be offering asclient.signTransaction, but just the signer interfaceThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this approach. We even have helpers like
signTransactionWithSigners(signers, tx)now so fully leaning into the Signer API makes total sense to me.