@solana/kit-plugin-wallet 0.0.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,303 @@
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
+ A successful call resolves only once the wallet is connected; any failure rejects. Connecting to a different wallet keeps the current one connected until the new connection succeeds — so if the attempt fails (the user declines the prompt, the wallet authorizes no accounts, or it becomes unavailable), the previous connection is left untouched rather than torn down. The same holds for `signIn`, which rejects rather than resolving with its sign-in output when the connection can't be established. While the attempt is in flight, `status` is `'connecting'` but `getState().connected` still describes the existing connection.
141
+
142
+ If another `connect` or `signIn` starts before this one resolves (e.g. an accidental double-click), the newer request wins and owns the connection — the superseded call rejects with a `DOMException` whose `name` is `'AbortError'`. Ignore it the way you'd ignore any aborted operation:
143
+
144
+ ```ts
145
+ try {
146
+ await client.wallet.connect(selectedWallet);
147
+ } catch (e) {
148
+ if ((e as Error).name !== 'AbortError') throw e; // a real failure
149
+ }
150
+ ```
151
+
152
+ - **`disconnect()`** — Disconnect the active wallet.
153
+
154
+ - **`selectAccount(account)`** — Switch to a different account within an already-authorized wallet without reconnecting.
155
+
156
+ ```ts
157
+ client.wallet.selectAccount(accounts[0]);
158
+ ```
159
+
160
+ - **`signMessage(message)`** — Sign a raw message with the connected account.
161
+
162
+ ```ts
163
+ const signature = await client.wallet.signMessage(new TextEncoder().encode('Hello'));
164
+ ```
165
+
166
+ - **`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`.
167
+
168
+ ```ts
169
+ const output = await client.wallet.signIn(selectedWallet, { domain: window.location.host });
170
+ ```
171
+
172
+ ## Framework integration
173
+
174
+ The plugin exposes `subscribe` and `getState` for binding wallet state to any UI framework.
175
+
176
+ **React** — use `useSyncExternalStore` for concurrent-mode-safe rendering:
177
+
178
+ ```tsx
179
+ import { useSyncExternalStore } from 'react';
180
+
181
+ function useWalletState() {
182
+ return useSyncExternalStore(client.wallet.subscribe, client.wallet.getState);
183
+ }
184
+
185
+ function App() {
186
+ const { wallets, connected, status } = useWalletState();
187
+
188
+ if (status === 'pending') return null; // avoid flashing a connect button before auto-reconnect
189
+
190
+ if (!connected) {
191
+ return wallets.map(w => (
192
+ <button key={w.name} onClick={() => client.wallet.connect(w)}>
193
+ {w.name}
194
+ </button>
195
+ ));
196
+ }
197
+
198
+ return <p>Connected: {connected.account.address}</p>;
199
+ }
200
+ ```
201
+
202
+ **Vue** — use a `shallowRef` composable:
203
+
204
+ ```ts
205
+ import { onMounted, onUnmounted, shallowRef } from 'vue';
206
+
207
+ function useWalletState() {
208
+ const state = shallowRef(client.wallet.getState());
209
+ onMounted(() => {
210
+ const unsub = client.wallet.subscribe(() => {
211
+ state.value = client.wallet.getState();
212
+ });
213
+ onUnmounted(unsub);
214
+ });
215
+ return state;
216
+ }
217
+ ```
218
+
219
+ **Svelte** — wrap in a `readable` store:
220
+
221
+ ```ts
222
+ import { readable } from 'svelte/store';
223
+
224
+ export const walletState = readable(client.wallet.getState(), set => {
225
+ return client.wallet.subscribe(() => set(client.wallet.getState()));
226
+ });
227
+ ```
228
+
229
+ ### Reactivity: observing `payer` / `identity`
230
+
231
+ Because the connected wallet's signer is dynamic, the plugins follow Kit's
232
+ reactive-capability convention. The variants that set `client.payer` /
233
+ `client.identity` also install a sibling `subscribeToPayer` /
234
+ `subscribeToIdentity` function, so reactive consumers can observe signer
235
+ changes without naming the wallet plugin:
236
+
237
+ ```ts
238
+ const client = createClient().use(walletSigner({ chain: 'solana:mainnet' }));
239
+
240
+ const unsubscribe = client.subscribeToPayer(() => {
241
+ // Re-read the current value; it may now throw if disconnected.
242
+ console.log('payer is now', client.payer);
243
+ });
244
+ ```
245
+
246
+ `walletSigner` installs both hooks, `walletPayer` installs `subscribeToPayer`,
247
+ `walletIdentity` installs `subscribeToIdentity`, and `walletWithoutSigner`
248
+ installs neither. The listener fires on connect, account switch, and
249
+ disconnect. For the full
250
+ wallet state (including the discovered-wallet list), use
251
+ `client.wallet.subscribe` instead.
252
+
253
+ ## Configuration
254
+
255
+ ```ts
256
+ walletSigner({
257
+ chain: 'solana:mainnet', // required
258
+ storage: sessionStorage, // default: localStorage (null to disable)
259
+ storageKey: 'my-app:wallet', // default: 'kit-wallet'
260
+ autoConnect: false, // default: true (disable silent reconnect)
261
+ filter: w => w.features.includes('solana:signAndSendTransaction'), // optional
262
+ });
263
+ ```
264
+
265
+ ## Persistence
266
+
267
+ 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).
268
+
269
+ ## SSR / server-side rendering
270
+
271
+ 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.
272
+
273
+ React Native is treated the same way as the server: wallet-standard browser discovery is not available, so the plugin returns the same inert stub (`status` stays `'pending'`, actions throw). Native wallet integration would need a different discovery mechanism and is out of scope for this plugin.
274
+
275
+ ```ts
276
+ const client = createClient()
277
+ .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))
278
+ .use(walletSigner({ chain: 'solana:mainnet' }))
279
+ .use(planAndSendTransactions());
280
+
281
+ // Server: status === 'pending', client.payer throws
282
+ // Browser: auto-connects, client.payer becomes the wallet signer
283
+ ```
284
+
285
+ ## Cleanup
286
+
287
+ The plugin implements `[Symbol.dispose]`, so it integrates with the `using` declaration or explicit disposal:
288
+
289
+ ```ts
290
+ {
291
+ using client = createClient().use(walletSigner({ chain: 'solana:mainnet' }));
292
+ // registry listeners and storage subscriptions are cleaned up on scope exit
293
+ }
294
+ ```
295
+
296
+ Or call `[Symbol.dispose]()` explicitly when you're done with the client:
297
+
298
+ ```ts
299
+ const client = createClient().use(walletSigner({ chain: 'solana:mainnet' }));
300
+
301
+ // ... later, when the client is no longer needed
302
+ client[Symbol.dispose]();
303
+ ```