|
| 1 | +# Kit Plugins ➤ Wallet |
| 2 | + |
| 3 | +[![npm][npm-image]][npm-url] |
| 4 | +[![npm-downloads][npm-downloads-image]][npm-url] |
| 5 | + |
| 6 | +[npm-downloads-image]: https://img.shields.io/npm/dm/@solana/kit-plugin-wallet.svg?style=flat |
| 7 | +[npm-image]: https://img.shields.io/npm/v/@solana/kit-plugin-wallet.svg?style=flat&label=%40solana%2Fkit-plugin-wallet |
| 8 | +[npm-url]: https://www.npmjs.com/package/@solana/kit-plugin-wallet |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +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: |
| 13 | + |
| 14 | +| Plugin | `client.payer` | `client.identity` | Use case | |
| 15 | +| --------------------- | -------------- | ----------------- | ------------------------------------------------------------- | |
| 16 | +| `walletSigner` | wallet signer | wallet signer | Most dApps — wallet pays fees and signs as the user identity. | |
| 17 | +| `walletPayer` | wallet signer | — | Wallet pays fees; identity is managed separately. | |
| 18 | +| `walletIdentity` | — | wallet signer | Backend relayer pays fees; wallet provides user identity. | |
| 19 | +| `walletWithoutSigner` | — | — | Wallet state only; payer and identity are managed separately. | |
| 20 | + |
| 21 | +## Installation |
| 22 | + |
| 23 | +```sh |
| 24 | +pnpm install @solana/kit-plugin-wallet |
| 25 | +``` |
| 26 | + |
| 27 | +## Quick start |
| 28 | + |
| 29 | +```ts |
| 30 | +import { createClient } from '@solana/kit'; |
| 31 | +import { solanaRpc } from '@solana/kit-plugin-rpc'; |
| 32 | +import { walletSigner } from '@solana/kit-plugin-wallet'; |
| 33 | +import { planAndSendTransactions } from '@solana/kit-plugin-instruction-plan'; |
| 34 | + |
| 35 | +const client = createClient() |
| 36 | + .use(walletSigner({ chain: 'solana:mainnet' })) |
| 37 | + .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) |
| 38 | + .use(planAndSendTransactions()); |
| 39 | + |
| 40 | +// Read discovered wallets from state |
| 41 | +const { wallets } = client.wallet.getState(); |
| 42 | + |
| 43 | +// Connect a wallet |
| 44 | +await client.wallet.connect(wallets[0]); |
| 45 | + |
| 46 | +// client.payer and client.identity are now the connected wallet's signer |
| 47 | +await client.sendTransaction([myInstruction]); |
| 48 | +``` |
| 49 | + |
| 50 | +## `walletSigner` plugin |
| 51 | + |
| 52 | +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. |
| 53 | + |
| 54 | +```ts |
| 55 | +import { walletSigner } from '@solana/kit-plugin-wallet'; |
| 56 | + |
| 57 | +const client = createClient() |
| 58 | + .use(walletSigner({ chain: 'solana:mainnet' })) |
| 59 | + .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) |
| 60 | + .use(planAndSendTransactions()); |
| 61 | +``` |
| 62 | + |
| 63 | +## `walletPayer` plugin |
| 64 | + |
| 65 | +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. |
| 66 | + |
| 67 | +```ts |
| 68 | +import { walletPayer } from '@solana/kit-plugin-wallet'; |
| 69 | + |
| 70 | +const client = createClient() |
| 71 | + .use(walletPayer({ chain: 'solana:mainnet' })) |
| 72 | + .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) |
| 73 | + .use(planAndSendTransactions()); |
| 74 | +``` |
| 75 | + |
| 76 | +## `walletIdentity` plugin |
| 77 | + |
| 78 | +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. |
| 79 | + |
| 80 | +```ts |
| 81 | +import { payer } from '@solana/kit-plugin-signer'; |
| 82 | +import { walletIdentity } from '@solana/kit-plugin-wallet'; |
| 83 | + |
| 84 | +const client = createClient() |
| 85 | + .use(payer(relayerKeypair)) |
| 86 | + .use(walletIdentity({ chain: 'solana:mainnet' })) |
| 87 | + .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) |
| 88 | + .use(planAndSendTransactions()); |
| 89 | + |
| 90 | +// client.payer is always relayerKeypair |
| 91 | +// client.identity is the connected wallet's signer |
| 92 | +``` |
| 93 | + |
| 94 | +## `walletWithoutSigner` plugin |
| 95 | + |
| 96 | +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. |
| 97 | + |
| 98 | +```ts |
| 99 | +import { payer } from '@solana/kit-plugin-signer'; |
| 100 | +import { walletWithoutSigner } from '@solana/kit-plugin-wallet'; |
| 101 | + |
| 102 | +const client = createClient() |
| 103 | + .use(payer(backendKeypair)) |
| 104 | + .use(walletWithoutSigner({ chain: 'solana:mainnet' })) |
| 105 | + .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) |
| 106 | + .use(planAndSendTransactions()); |
| 107 | + |
| 108 | +// client.payer is always backendKeypair |
| 109 | +// client.wallet.getState().connected?.signer for manual use |
| 110 | +``` |
| 111 | + |
| 112 | +## State and actions |
| 113 | + |
| 114 | +All wallet state is accessed via `client.wallet.getState()`, which returns a referentially stable `WalletState` object (new reference only when something changes). |
| 115 | + |
| 116 | +- **`getState().wallets`** — All discovered wallets that support the configured chain. |
| 117 | + |
| 118 | + ```ts |
| 119 | + const { wallets } = client.wallet.getState(); |
| 120 | + for (const w of wallets) { |
| 121 | + console.log(w.name, w.icon); |
| 122 | + } |
| 123 | + ``` |
| 124 | + |
| 125 | +- **`getState().connected`** — The active connection (wallet, account, and signer), or `null` when disconnected. |
| 126 | + |
| 127 | + ```ts |
| 128 | + const { connected } = client.wallet.getState(); |
| 129 | + console.log(connected?.account.address); |
| 130 | + ``` |
| 131 | + |
| 132 | +- **`getState().status`** — The current connection status: `'pending'`, `'disconnected'`, `'connecting'`, `'connected'`, `'disconnecting'`, or `'reconnecting'`. |
| 133 | + |
| 134 | +- **`connect(wallet)`** — Connect to a wallet and select the first newly authorized account. |
| 135 | + |
| 136 | + ```ts |
| 137 | + const accounts = await client.wallet.connect(selectedWallet); |
| 138 | + ``` |
| 139 | + |
| 140 | +- **`disconnect()`** — Disconnect the active wallet. |
| 141 | + |
| 142 | +- **`selectAccount(account)`** — Switch to a different account within an already-authorized wallet without reconnecting. |
| 143 | + |
| 144 | + ```ts |
| 145 | + client.wallet.selectAccount(accounts[0]); |
| 146 | + ``` |
| 147 | + |
| 148 | +- **`signMessage(message)`** — Sign a raw message with the connected account. |
| 149 | + |
| 150 | + ```ts |
| 151 | + const signature = await client.wallet.signMessage(new TextEncoder().encode('Hello')); |
| 152 | + ``` |
| 153 | + |
| 154 | +- **`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`. |
| 155 | + |
| 156 | + ```ts |
| 157 | + const output = await client.wallet.signIn(selectedWallet, { domain: window.location.host }); |
| 158 | + ``` |
| 159 | + |
| 160 | +## Framework integration |
| 161 | + |
| 162 | +The plugin exposes `subscribe` and `getState` for binding wallet state to any UI framework. |
| 163 | + |
| 164 | +**React** — use `useSyncExternalStore` for concurrent-mode-safe rendering: |
| 165 | + |
| 166 | +```tsx |
| 167 | +import { useSyncExternalStore } from 'react'; |
| 168 | +
|
| 169 | +function useWalletState() { |
| 170 | + return useSyncExternalStore(client.wallet.subscribe, client.wallet.getState); |
| 171 | +} |
| 172 | +
|
| 173 | +function App() { |
| 174 | + const { wallets, connected, status } = useWalletState(); |
| 175 | +
|
| 176 | + if (status === 'pending') return null; // avoid flashing a connect button before auto-reconnect |
| 177 | +
|
| 178 | + if (!connected) { |
| 179 | + return wallets.map(w => ( |
| 180 | + <button key={w.name} onClick={() => client.wallet.connect(w)}> |
| 181 | + {w.name} |
| 182 | + </button> |
| 183 | + )); |
| 184 | + } |
| 185 | +
|
| 186 | + return <p>Connected: {connected.account.address}</p>; |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +**Vue** — use a `shallowRef` composable: |
| 191 | + |
| 192 | +```ts |
| 193 | +import { onMounted, onUnmounted, shallowRef } from 'vue'; |
| 194 | +
|
| 195 | +function useWalletState() { |
| 196 | + const state = shallowRef(client.wallet.getState()); |
| 197 | + onMounted(() => { |
| 198 | + const unsub = client.wallet.subscribe(() => { |
| 199 | + state.value = client.wallet.getState(); |
| 200 | + }); |
| 201 | + onUnmounted(unsub); |
| 202 | + }); |
| 203 | + return state; |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +**Svelte** — wrap in a `readable` store: |
| 208 | + |
| 209 | +```ts |
| 210 | +import { readable } from 'svelte/store'; |
| 211 | +
|
| 212 | +export const walletState = readable(client.wallet.getState(), set => { |
| 213 | + return client.wallet.subscribe(() => set(client.wallet.getState())); |
| 214 | +}); |
| 215 | +``` |
| 216 | + |
| 217 | +## Configuration |
| 218 | + |
| 219 | +```ts |
| 220 | +walletSigner({ |
| 221 | + chain: 'solana:mainnet', // required |
| 222 | + storage: sessionStorage, // default: localStorage (null to disable) |
| 223 | + storageKey: 'my-app:wallet', // default: 'kit-wallet' |
| 224 | + autoConnect: false, // default: true (disable silent reconnect) |
| 225 | + filter: w => w.features.includes('solana:signAndSendTransaction'), // optional |
| 226 | +}); |
| 227 | +``` |
| 228 | + |
| 229 | +## Persistence |
| 230 | + |
| 231 | +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). |
| 232 | + |
| 233 | +## SSR / server-side rendering |
| 234 | + |
| 235 | +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. |
| 236 | + |
| 237 | +```ts |
| 238 | +const client = createClient() |
| 239 | + .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) |
| 240 | + .use(walletSigner({ chain: 'solana:mainnet' })) |
| 241 | + .use(planAndSendTransactions()); |
| 242 | +
|
| 243 | +// Server: status === 'pending', client.payer throws |
| 244 | +// Browser: auto-connects, client.payer becomes the wallet signer |
| 245 | +``` |
| 246 | + |
| 247 | +## Cleanup |
| 248 | + |
| 249 | +The plugin implements `[Symbol.dispose]`, so it integrates with the `using` declaration or explicit disposal: |
| 250 | + |
| 251 | +```ts |
| 252 | +{ |
| 253 | + using client = createClient().use(walletSigner({ chain: 'solana:mainnet' })); |
| 254 | + // registry listeners and storage subscriptions are cleaned up on scope exit |
| 255 | +} |
| 256 | +``` |
| 257 | + |
| 258 | +Or call `[Symbol.dispose]()` explicitly when you're done with the client: |
| 259 | + |
| 260 | +```ts |
| 261 | +const client = createClient().use(walletSigner({ chain: 'solana:mainnet' })); |
| 262 | +
|
| 263 | +// ... later, when the client is no longer needed |
| 264 | +client[Symbol.dispose](); |
| 265 | +``` |
0 commit comments