@solana/kit-plugin-wallet 0.0.0 → 0.13.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.
Files changed (44) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +401 -0
  3. package/dist/index.browser.cjs +622 -0
  4. package/dist/index.browser.cjs.map +1 -0
  5. package/dist/index.browser.mjs +614 -0
  6. package/dist/index.browser.mjs.map +1 -0
  7. package/dist/index.node.cjs +100 -0
  8. package/dist/index.node.cjs.map +1 -0
  9. package/dist/index.node.mjs +91 -0
  10. package/dist/index.node.mjs.map +1 -0
  11. package/dist/index.react-native.mjs +91 -0
  12. package/dist/index.react-native.mjs.map +1 -0
  13. package/dist/react/index.browser.cjs +77 -0
  14. package/dist/react/index.browser.cjs.map +1 -0
  15. package/dist/react/index.browser.mjs +59 -0
  16. package/dist/react/index.browser.mjs.map +1 -0
  17. package/dist/react/index.node.cjs +77 -0
  18. package/dist/react/index.node.cjs.map +1 -0
  19. package/dist/react/index.node.mjs +59 -0
  20. package/dist/react/index.node.mjs.map +1 -0
  21. package/dist/react/index.react-native.mjs +59 -0
  22. package/dist/react/index.react-native.mjs.map +1 -0
  23. package/dist/types/index.d.ts +4 -0
  24. package/dist/types/index.d.ts.map +1 -0
  25. package/dist/types/react/index.d.ts +235 -0
  26. package/dist/types/react/index.d.ts.map +1 -0
  27. package/dist/types/status.d.ts +31 -0
  28. package/dist/types/status.d.ts.map +1 -0
  29. package/dist/types/store.d.ts +16 -0
  30. package/dist/types/store.d.ts.map +1 -0
  31. package/dist/types/types.d.ts +384 -0
  32. package/dist/types/types.d.ts.map +1 -0
  33. package/dist/types/wallet.d.ts +133 -0
  34. package/dist/types/wallet.d.ts.map +1 -0
  35. package/package.json +103 -7
  36. package/src/__typetests__/wallet-typetest.ts +129 -0
  37. package/src/index.ts +3 -0
  38. package/src/react/__typetests__/index.ts +108 -0
  39. package/src/react/index.ts +301 -0
  40. package/src/status.ts +33 -0
  41. package/src/store.ts +990 -0
  42. package/src/types/global.d.ts +6 -0
  43. package/src/types.ts +407 -0
  44. package/src/wallet.ts +218 -0
@@ -0,0 +1,6 @@
1
+ declare const __BROWSER__: boolean;
2
+ declare const __ESM__: boolean;
3
+ declare const __NODEJS__: boolean;
4
+ declare const __REACTNATIVE__: boolean;
5
+ declare const __TEST__: boolean;
6
+ declare const __VERSION__: string;
package/src/types.ts ADDED
@@ -0,0 +1,407 @@
1
+ import type { MessageSigner, ReadonlyUint8Array, SignatureBytes, TransactionSigner } from '@solana/kit';
2
+ import type { SolanaChain } from '@solana/wallet-standard-chains';
3
+ import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';
4
+ import type { IdentifierString } from '@wallet-standard/base';
5
+ import type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';
6
+
7
+ /**
8
+ * The signer type for a connected wallet account.
9
+ *
10
+ * Always satisfies `TransactionSigner`. Additionally implements `MessageSigner`
11
+ * when the wallet supports `solana:signMessage`.
12
+ */
13
+ export type WalletSigner = TransactionSigner | (MessageSigner & TransactionSigner);
14
+
15
+ // -- Public types -----------------------------------------------------------
16
+
17
+ /**
18
+ * The connection status of the wallet plugin.
19
+ *
20
+ * - `pending` — not yet initialized. Initial state on both server and browser.
21
+ * On the server — and in React Native, which is treated the same way — this
22
+ * state is permanent. In the browser it resolves to `disconnected` or
23
+ * `reconnecting` once the storage check completes.
24
+ * - `disconnected` — initialized, no wallet connected.
25
+ * - `connecting` — a user-initiated connection request is in progress. When
26
+ * connecting to a different wallet while one is already connected, the
27
+ * existing connection is preserved until the new one succeeds — so
28
+ * {@link WalletState.connected} can be non-null during this state, and a
29
+ * failed attempt reverts to the previous connection.
30
+ * - `connected` — a wallet is connected.
31
+ * - `disconnecting` — a user-initiated disconnection request is in progress.
32
+ * - `reconnecting` — auto-connect in progress (connecting to persisted wallet).
33
+ */
34
+ export type WalletStatus = 'connected' | 'connecting' | 'disconnected' | 'disconnecting' | 'pending' | 'reconnecting';
35
+
36
+ /**
37
+ * A snapshot of the wallet plugin state at a point in time.
38
+ *
39
+ * Returned by {@link WalletNamespace.getState}. The same object reference
40
+ * is returned on successive calls as long as nothing has changed — a new
41
+ * object is only created when a field actually changes. This ensures
42
+ * `useSyncExternalStore` only triggers re-renders on meaningful state changes.
43
+ *
44
+ * @see {@link WalletNamespace.getState}
45
+ */
46
+ export type WalletState = {
47
+ /**
48
+ * The active connection, or `null` when disconnected.
49
+ *
50
+ * `signer` is `null` for read-only / watch-only wallets that do not
51
+ * support any signing feature.
52
+ *
53
+ * Independent of {@link status}: when {@link connect} or {@link signIn} is
54
+ * called for a different wallet while one is already connected, `connected`
55
+ * keeps describing the existing connection while `status` is `'connecting'`,
56
+ * and a failed attempt leaves it in place.
57
+ */
58
+ readonly connected: {
59
+ readonly account: UiWalletAccount;
60
+ /** The signer for the active account, or `null` for read-only wallets. */
61
+ readonly signer: WalletSigner | null;
62
+ readonly wallet: UiWallet;
63
+ } | null;
64
+ /** The current connection status. */
65
+ readonly status: WalletStatus;
66
+ /** All discovered wallets matching the configured chain and filter. */
67
+ readonly wallets: readonly UiWallet[];
68
+ };
69
+
70
+ /**
71
+ * Options accepted by each async wallet action.
72
+ *
73
+ * Currently only carries an `abortSignal`, but is kept as an object for
74
+ * consistency with the rest of the Kit ecosystem and to allow future
75
+ * additions without breaking the call-site shape.
76
+ *
77
+ * @see {@link WalletNamespace.connect}
78
+ * @see {@link WalletNamespace.disconnect}
79
+ * @see {@link WalletNamespace.signMessage}
80
+ * @see {@link WalletNamespace.signIn}
81
+ */
82
+ export type WalletActionOptions = {
83
+ /**
84
+ * An optional `AbortSignal` used to cancel the operation.
85
+ *
86
+ * Cancellation is pre-call only: the plugin calls
87
+ * `abortSignal.throwIfAborted()` at the start of each action and bails
88
+ * out before invoking the wallet. Once the underlying wallet-standard
89
+ * call has been dispatched, its result is returned even if the signal
90
+ * is aborted mid-flight — the wallet's side effect (an approved
91
+ * signature, a live connection, a broadcast transaction) is the source
92
+ * of truth, and throwing here would discard real user work without
93
+ * undoing what the wallet already did.
94
+ */
95
+ abortSignal?: AbortSignal;
96
+ };
97
+
98
+ /**
99
+ * A pluggable storage adapter for persisting the selected wallet account.
100
+ *
101
+ * Follows the Web Storage API shape (`getItem`/`setItem`/`removeItem`).
102
+ * `localStorage` and `sessionStorage` satisfy this interface directly.
103
+ * Async backends (IndexedDB, encrypted storage) may return `Promise`s.
104
+ *
105
+ * Writes are fire-and-forget: the plugin does not await `setItem`/`removeItem`
106
+ * and swallows any rejection (the live wallet connection is the source of
107
+ * truth, so a failed persist is non-fatal). A resolved action therefore does
108
+ * not guarantee the write has landed, and rapid successive writes are not
109
+ * ordered. This is intentional — persistence only records which account to
110
+ * silently reconnect to on the next load.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * // Use sessionStorage
115
+ * walletSigner({ chain: 'solana:mainnet', storage: sessionStorage });
116
+ *
117
+ * // Custom async adapter
118
+ * walletSigner({
119
+ * chain: 'solana:mainnet',
120
+ * storage: {
121
+ * getItem: (key) => myStore.get(key),
122
+ * setItem: (key, value) => myStore.set(key, value),
123
+ * removeItem: (key) => myStore.delete(key),
124
+ * },
125
+ * });
126
+ * ```
127
+ */
128
+ export type WalletStorage = {
129
+ getItem(key: string): Promise<string | null> | string | null;
130
+ removeItem(key: string): Promise<void> | void;
131
+ setItem(key: string, value: string): Promise<void> | void;
132
+ };
133
+
134
+ /**
135
+ * Configuration for the wallet plugins.
136
+ *
137
+ * @see {@link walletSigner}
138
+ * @see {@link walletIdentity}
139
+ * @see {@link walletPayer}
140
+ * @see {@link walletWithoutSigner}
141
+ */
142
+ export type WalletPluginConfig = {
143
+ /**
144
+ * Whether to attempt silent reconnection on startup using the persisted
145
+ * wallet account from `storage`.
146
+ *
147
+ * Has no effect if `storage` is `null`.
148
+ *
149
+ * @default true
150
+ */
151
+ autoConnect?: boolean;
152
+
153
+ /**
154
+ * The chain this client targets (e.g. `'solana:mainnet'`).
155
+ *
156
+ * Accepts any {@link SolanaChain} (with literal autocomplete) and, as an
157
+ * escape hatch, any wallet-standard {@link IdentifierString} shape
158
+ * (`${string}:${string}`) for custom chains or non-Solana L2s. The plugin's
159
+ * runtime behavior is chain-agnostic — it passes the identifier to
160
+ * wallet-standard discovery (`uiWallet.chains.includes(chain)`) and to
161
+ * `createSignerFromWalletAccount`. Wallets that don't advertise the chain
162
+ * are filtered out, and accounts that can't produce a signer for the chain
163
+ * resolve to `signer: null` (matching the read-only-wallet contract).
164
+ *
165
+ * One client = one chain. To switch networks, create a separate client
166
+ * with a different chain and RPC endpoint.
167
+ */
168
+ chain: SolanaChain | (IdentifierString & {});
169
+
170
+ /**
171
+ * Optional filter function for wallet discovery. Called for each wallet
172
+ * that supports the configured chain and `standard:connect`. Return `true`
173
+ * to include the wallet, `false` to exclude it.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * // Require signAndSendTransaction
178
+ * filter: (w) => w.features.includes('solana:signAndSendTransaction')
179
+ *
180
+ * // Whitelist specific wallets
181
+ * filter: (w) => ['SomeWallet', 'SomeOtherWallet'].includes(w.name)
182
+ * ```
183
+ */
184
+ filter?: (wallet: UiWallet) => boolean;
185
+
186
+ /**
187
+ * Storage adapter for persisting the selected wallet account across page
188
+ * loads. Pass `null` to disable persistence entirely.
189
+ *
190
+ * When omitted in a browser environment, `localStorage` is used by default.
191
+ * On the server, storage is always skipped regardless of this option.
192
+ *
193
+ * @default localStorage (in browser)
194
+ * @see {@link WalletStorage}
195
+ */
196
+ storage?: WalletStorage | null;
197
+
198
+ /**
199
+ * Storage key used for persistence.
200
+ *
201
+ * @default 'kit-wallet'
202
+ */
203
+ storageKey?: string;
204
+ };
205
+
206
+ /**
207
+ * The `wallet` namespace exposed on the client as `client.wallet`.
208
+ *
209
+ * All wallet state is accessed via {@link getState}. Use {@link subscribe}
210
+ * to be notified of changes and integrate with framework primitives such as
211
+ * React's `useSyncExternalStore`.
212
+ *
213
+ * @see {@link ClientWithWallet}
214
+ */
215
+ export type WalletNamespace = {
216
+ // -- Actions --
217
+
218
+ /**
219
+ * Connect to a wallet. Calls `standard:connect`, then selects the first
220
+ * newly authorized account (or the first account if reconnecting). Creates
221
+ * and caches a signer for the active account.
222
+ *
223
+ * Resolving means the wallet is connected; any failure rejects. If a wallet
224
+ * is already connected, it stays connected until the new one is established:
225
+ * a failed attempt — a rejected prompt, no authorized accounts, or the
226
+ * wallet becoming unavailable — leaves the previous connection in place
227
+ * rather than disconnecting it.
228
+ *
229
+ * @returns All accounts from the wallet after connection.
230
+ * @throws The wallet's rejection error if the user declines the prompt.
231
+ * @throws `SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED)` if the wallet
232
+ * authorizes no accounts, or unregisters (or drops a required
233
+ * feature/chain) while its connect prompt is open. Any previously
234
+ * connected wallet is left in place.
235
+ * @throws `DOMException` with `name: 'AbortError'` if a newer `connect` or
236
+ * `signIn` is started before this call resolves. The newer request wins
237
+ * and owns the resulting connection; this superseded call rejects so it
238
+ * can be ignored like any other aborted operation (e.g. an accidental
239
+ * double-click still connects — only the orphaned first promise rejects).
240
+ * @throws `options.abortSignal.reason` if the signal is already aborted
241
+ * when the action is called. Aborts after the wallet call has been
242
+ * dispatched do not take effect.
243
+ */
244
+ connect: (wallet: UiWallet, options?: WalletActionOptions) => Promise<readonly UiWalletAccount[]>;
245
+
246
+ /**
247
+ * Disconnect a wallet. Calls `standard:disconnect` if supported.
248
+ *
249
+ * `wallet` defaults to the active wallet. Passing a non-active, currently
250
+ * authorized wallet deauthorizes it (calling `standard:disconnect` if
251
+ * supported) while leaving the active connection untouched. A `wallet`
252
+ * that doesn't support `standard:disconnect`, or one that is already
253
+ * unauthorized/unregistered, is a forgiving no-op.
254
+ *
255
+ * @throws `options.abortSignal.reason` if the signal is already aborted
256
+ * when the action is called. Aborts after the wallet call has been
257
+ * dispatched do not take effect.
258
+ */
259
+ disconnect: (wallet?: UiWallet, options?: WalletActionOptions) => Promise<void>;
260
+
261
+ // -- State --
262
+ /**
263
+ * Get the current wallet state. Referentially stable — a new object is
264
+ * only created when a field actually changes, so React's
265
+ * `useSyncExternalStore` skips re-renders when nothing meaningful changed.
266
+ *
267
+ * @see {@link WalletState}
268
+ */
269
+ getState: () => WalletState;
270
+
271
+ /**
272
+ * Select an account, creating and caching a new signer for it.
273
+ *
274
+ * The account may belong to any currently authorized wallet, not only the
275
+ * active one. When it belongs to a different authorized wallet, the active
276
+ * connection switches to that wallet synchronously (no prompt), leaving the
277
+ * previously active wallet authorized. If the store is currently 'disconnected',
278
+ * this allows transitioning to 'connected' synchronously.
279
+ *
280
+ * @throws `SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE)` if the
281
+ * account's handle cannot be resolved to a currently authorized wallet, or
282
+ * the account is not among that wallet's accounts.
283
+ * @throws `SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED)` if the account's
284
+ * wallet is currently disconnecting.
285
+ */
286
+ selectAccount: (account: UiWalletAccount) => void;
287
+
288
+ /**
289
+ * Sign In With Solana (SIWS-as-connect).
290
+ *
291
+ * Connects the wallet, calls `solana:signIn`, sets the returned account as
292
+ * active, and creates a signer. Resolving means the client is in the same
293
+ * state as if {@link connect} had been called; any failure to connect
294
+ * rejects rather than resolving while disconnected.
295
+ *
296
+ * All fields on `SolanaSignInInput` are optional — pass `{}` if no sign-in
297
+ * customization is needed.
298
+ *
299
+ * To sign in with the already-connected wallet, pass
300
+ * `getState().connected.wallet`.
301
+ *
302
+ * Like {@link connect}, a failed sign-in to a different wallet rejects and
303
+ * leaves any existing connection in place rather than disconnecting it.
304
+ *
305
+ * @returns The wallet's sign-in output, once the connection is established.
306
+ * @throws `WalletStandardError(WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED)`
307
+ * if the wallet does not support `solana:signIn`.
308
+ * @throws `SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED)` if the wallet
309
+ * unregisters (or drops a required feature/chain) while its sign-in prompt
310
+ * is open, or signs in with an account it does not expose. Any previously
311
+ * connected wallet is left in place.
312
+ * @throws `DOMException` with `name: 'AbortError'` if a newer `connect` or
313
+ * `signIn` is started before this call resolves. The newer request wins
314
+ * and owns the resulting connection; this superseded call rejects so it
315
+ * can be ignored like any other aborted operation.
316
+ * @throws `options.abortSignal.reason` if the signal is already aborted
317
+ * when the action is called. Aborts after the wallet call has been
318
+ * dispatched do not take effect.
319
+ */
320
+ signIn: (wallet: UiWallet, input: SolanaSignInInput, options?: WalletActionOptions) => Promise<SolanaSignInOutput>;
321
+
322
+ /**
323
+ * Sign an arbitrary message with the connected account.
324
+ *
325
+ * Calls the wallet's `solana:signMessage` feature directly (does not go
326
+ * through the cached signer), so message signing works even for wallets
327
+ * that don't support transaction signing.
328
+ *
329
+ * @throws `SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED)` if no wallet is connected.
330
+ * @throws `WalletStandardError(WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED)`
331
+ * if the wallet does not support `solana:signMessage`.
332
+ * @throws `options.abortSignal.reason` if the signal is already aborted
333
+ * when the action is called. Aborts after the wallet call has been
334
+ * dispatched do not take effect.
335
+ */
336
+ signMessage: (message: ReadonlyUint8Array, options?: WalletActionOptions) => Promise<SignatureBytes>;
337
+
338
+ /**
339
+ * Subscribe to any wallet state change. Compatible with React's
340
+ * `useSyncExternalStore` and similar framework primitives.
341
+ *
342
+ * @returns An unsubscribe function.
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * const state = useSyncExternalStore(client.wallet.subscribe, client.wallet.getState);
347
+ * ```
348
+ */
349
+ subscribe: (listener: () => void) => () => void;
350
+
351
+ /**
352
+ * **Advanced.** Resolves once the initial connection attempt has settled —
353
+ * i.e. {@link getState}'s `status` is no longer `'pending'` or
354
+ * `'reconnecting'`.
355
+ *
356
+ * You only need this if your app **rebuilds the client at runtime** (for
357
+ * example, to change chain). Each freshly built client runs a silent
358
+ * auto-reconnect that briefly passes through `'pending'`/`'reconnecting'`;
359
+ * awaiting this lets you hold the previous UI until the new client is ready
360
+ * instead of flashing a "reconnecting" state on every swap. Most apps never
361
+ * call it — subscribe to `status` with {@link subscribe} instead.
362
+ *
363
+ * Does **not** wait for user-initiated `connect`/`disconnect`/`signIn`
364
+ * (those pass through `'connecting'`/`'disconnecting'`, which count as
365
+ * ready), so a normal connect never makes this block. While transient it
366
+ * returns the **same** promise reference on every call, so successive
367
+ * awaits during one warm-up share a single settlement.
368
+ *
369
+ * Disposing the client mid-warm-up settles the status to `'disconnected'`,
370
+ * so a pending `whenReady()` resolves rather than hanging. Check
371
+ * {@link getState} after awaiting if you need to distinguish a ready
372
+ * connection from a disposed (or never-connected) client.
373
+ *
374
+ * @returns A promise that resolves with no value once the wallet is past its
375
+ * initial `'pending'`/`'reconnecting'` warm-up. Already resolved when the
376
+ * status is not transient.
377
+ *
378
+ * @example
379
+ * ```ts
380
+ * // On a chain switch, hold the old UI until the rebuilt client is ready:
381
+ * const next = createClient().use(walletSigner({ chain }));
382
+ * await next.wallet.whenReady();
383
+ * setClient(next);
384
+ * ```
385
+ */
386
+ whenReady: () => Promise<void>;
387
+ };
388
+
389
+ /**
390
+ * Properties added to the client by the wallet plugins.
391
+ *
392
+ * All wallet state and actions are namespaced under `client.wallet`.
393
+ * Depending on which plugin variant is used, `client.payer` and/or
394
+ * `client.identity` may also be set to the connected wallet's signer.
395
+ * This is not part of Kit plugin-interfaces, as it depends on wallet-standard types
396
+ *
397
+ * @see {@link walletSigner}
398
+ * @see {@link walletPayer}
399
+ * @see {@link walletIdentity}
400
+ * @see {@link walletWithoutSigner}
401
+ * @see {@link WalletNamespace}
402
+ *
403
+ */
404
+ export type ClientWithWallet = {
405
+ /** The wallet namespace — state, actions, and framework integration. */
406
+ readonly wallet: WalletNamespace;
407
+ };
package/src/wallet.ts ADDED
@@ -0,0 +1,218 @@
1
+ import {
2
+ type ClientWithIdentity,
3
+ type ClientWithPayer,
4
+ type ClientWithSubscribeToIdentity,
5
+ type ClientWithSubscribeToPayer,
6
+ extendClient,
7
+ SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED,
8
+ SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE,
9
+ SolanaError,
10
+ withCleanup,
11
+ } from '@solana/kit';
12
+
13
+ import { createWalletStore } from './store';
14
+ import type { ClientWithWallet, WalletPluginConfig } from './types';
15
+
16
+ // -- Internal helpers ---------------------------------------------------------
17
+
18
+ function defineSignerGetter(
19
+ additions: Record<string, unknown>,
20
+ property: string,
21
+ store: ReturnType<typeof createWalletStore>,
22
+ ): void {
23
+ Object.defineProperty(additions, property, {
24
+ configurable: true,
25
+ // Non-enumerable on purpose: this getter throws when no signer is connected, so we
26
+ // don't want things like Object.entries to read it.
27
+ enumerable: false,
28
+ get() {
29
+ const state = store.getState();
30
+ if (!state.connected) {
31
+ throw new SolanaError(SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED, { status: state.status });
32
+ }
33
+ if (!state.connected.signer) {
34
+ throw new SolanaError(SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE);
35
+ }
36
+ return state.connected.signer;
37
+ },
38
+ });
39
+ }
40
+
41
+ type SignerProperties = 'payer' | 'identity';
42
+
43
+ // Maps each signer capability to the Kit-convention reactive hook installed
44
+ // alongside it (`@solana/plugin-interfaces`). A plugin whose `client.payer` /
45
+ // `client.identity` is dynamic installs the matching `subscribeTo*` function
46
+ // so reactive consumers can observe changes without naming this plugin.
47
+ const SUBSCRIBE_PROPERTY: Record<SignerProperties, 'subscribeToIdentity' | 'subscribeToPayer'> = {
48
+ identity: 'subscribeToIdentity',
49
+ payer: 'subscribeToPayer',
50
+ };
51
+
52
+ function createPlugin<TAdditions extends ClientWithWallet>(
53
+ config: WalletPluginConfig,
54
+ signerProperties: SignerProperties[],
55
+ ) {
56
+ return <T extends object & { wallet?: never }>(client: T): Disposable & Omit<T, keyof TAdditions> & TAdditions => {
57
+ if ('wallet' in client) {
58
+ throw new Error(
59
+ 'Only one wallet plugin can be used per client. ' +
60
+ 'Use walletSigner, walletPayer, walletIdentity, or walletWithoutSigner — not multiple.',
61
+ );
62
+ }
63
+
64
+ const store = createWalletStore(config);
65
+
66
+ const additions: Record<string, unknown> = { wallet: store };
67
+ for (const prop of signerProperties) {
68
+ defineSignerGetter(additions, prop, store);
69
+ additions[SUBSCRIBE_PROPERTY[prop]] = store.subscribeSigner;
70
+ }
71
+
72
+ return withCleanup(extendClient(client, additions), () => store[Symbol.dispose]()) as unknown as Disposable &
73
+ Omit<T, keyof TAdditions> &
74
+ TAdditions;
75
+ };
76
+ }
77
+
78
+ // -- Public API ---------------------------------------------------------------
79
+
80
+ /**
81
+ * A framework-agnostic Kit plugin that manages wallet discovery, connection
82
+ * lifecycle, and signer creation using wallet-standard — and syncs the
83
+ * connected wallet's signer to both `client.payer` and `client.identity`.
84
+ *
85
+ * This is the most common entrypoint for dApps. When a signing-capable
86
+ * wallet is connected, `client.payer` and `client.identity` both return the
87
+ * wallet signer. When disconnected or read-only, accessing either throws.
88
+ *
89
+ * Because the signer is dynamic, both `client.subscribeToPayer` and
90
+ * `client.subscribeToIdentity` are installed so reactive consumers can observe
91
+ * changes (the Kit reactive-capability convention).
92
+ *
93
+ * **SSR-safe.** Can be included in a shared client chain that runs on both
94
+ * server and browser.
95
+ *
96
+ * ```ts
97
+ * const client = createClient()
98
+ * .use(walletSigner({ chain: 'solana:mainnet' }))
99
+ * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))
100
+ * .use(planAndSendTransactions());
101
+ * ```
102
+ *
103
+ * @param config - Plugin configuration.
104
+ *
105
+ * @see {@link walletPayer}
106
+ * @see {@link walletIdentity}
107
+ * @see {@link walletWithoutSigner}
108
+ * @see {@link WalletPluginConfig}
109
+ */
110
+ export function walletSigner(config: WalletPluginConfig) {
111
+ return createPlugin<
112
+ ClientWithIdentity &
113
+ ClientWithPayer &
114
+ ClientWithSubscribeToIdentity &
115
+ ClientWithSubscribeToPayer &
116
+ ClientWithWallet
117
+ >(config, ['payer', 'identity']);
118
+ }
119
+
120
+ /**
121
+ * A framework-agnostic Kit plugin that manages wallet discovery, connection
122
+ * lifecycle, and signer creation using wallet-standard — and syncs the
123
+ * connected wallet's signer to `client.identity`.
124
+ *
125
+ * Use this when `client.payer` is controlled by a separate `payer()` plugin
126
+ * (e.g. a backend relayer pays fees, but the user's wallet is the identity).
127
+ *
128
+ * Because the signer is dynamic, `client.subscribeToIdentity` is installed so
129
+ * reactive consumers can observe changes (the Kit reactive-capability convention).
130
+ *
131
+ * **SSR-safe.** Can be included in a shared client chain that runs on both
132
+ * server and browser.
133
+ *
134
+ * ```ts
135
+ * const client = createClient()
136
+ * .use(payer(relayerKeypair))
137
+ * .use(walletIdentity({ chain: 'solana:mainnet' }))
138
+ * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))
139
+ * .use(planAndSendTransactions());
140
+ * ```
141
+ *
142
+ * @param config - Plugin configuration.
143
+ *
144
+ * @see {@link walletSigner}
145
+ * @see {@link walletPayer}
146
+ * @see {@link walletWithoutSigner}
147
+ * @see {@link WalletPluginConfig}
148
+ */
149
+ export function walletIdentity(config: WalletPluginConfig) {
150
+ return createPlugin<ClientWithIdentity & ClientWithSubscribeToIdentity & ClientWithWallet>(config, ['identity']);
151
+ }
152
+
153
+ /**
154
+ * A framework-agnostic Kit plugin that manages wallet discovery, connection
155
+ * lifecycle, and signer creation using wallet-standard — and syncs the
156
+ * connected wallet's signer to `client.payer`.
157
+ *
158
+ * Use this when you need the wallet as the fee payer but don't need
159
+ * `client.identity`. For most dApps, prefer {@link walletSigner} which
160
+ * sets both.
161
+ *
162
+ * Because the signer is dynamic, `client.subscribeToPayer` is installed so
163
+ * reactive consumers can observe changes (the Kit reactive-capability convention).
164
+ *
165
+ * **SSR-safe.** Can be included in a shared client chain that runs on both
166
+ * server and browser.
167
+ *
168
+ * ```ts
169
+ * const client = createClient()
170
+ * .use(walletPayer({ chain: 'solana:mainnet' }))
171
+ * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))
172
+ * .use(planAndSendTransactions());
173
+ * ```
174
+ *
175
+ * @param config - Plugin configuration.
176
+ *
177
+ * @see {@link walletSigner}
178
+ * @see {@link walletIdentity}
179
+ * @see {@link walletWithoutSigner}
180
+ * @see {@link WalletPluginConfig}
181
+ */
182
+ export function walletPayer(config: WalletPluginConfig) {
183
+ return createPlugin<ClientWithPayer & ClientWithSubscribeToPayer & ClientWithWallet>(config, ['payer']);
184
+ }
185
+
186
+ /**
187
+ * A framework-agnostic Kit plugin that manages wallet discovery, connection
188
+ * lifecycle, and signer creation using wallet-standard.
189
+ *
190
+ * Adds the `wallet` namespace to the client without setting `client.payer`
191
+ * or `client.identity`. Use this alongside separate `payer()` and/or
192
+ * `identity()` plugins, or when the wallet's signer is used explicitly in
193
+ * instructions. For most dApps, prefer {@link walletSigner} instead.
194
+ *
195
+ * **SSR-safe.** Can be included in a shared client chain that runs on both
196
+ * server and browser.
197
+ *
198
+ * ```ts
199
+ * const client = createClient()
200
+ * .use(payer(backendKeypair))
201
+ * .use(walletWithoutSigner({ chain: 'solana:mainnet' }))
202
+ * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))
203
+ * .use(planAndSendTransactions());
204
+ *
205
+ * // client.payer is always backendKeypair
206
+ * // client.wallet.getState().connected?.signer for manual use
207
+ * ```
208
+ *
209
+ * @param config - Plugin configuration.
210
+ *
211
+ * @see {@link walletSigner}
212
+ * @see {@link walletPayer}
213
+ * @see {@link walletIdentity}
214
+ * @see {@link WalletPluginConfig}
215
+ */
216
+ export function walletWithoutSigner(config: WalletPluginConfig) {
217
+ return createPlugin<ClientWithWallet>(config, []);
218
+ }