@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.
- package/LICENSE +22 -0
- package/README.md +401 -0
- package/dist/index.browser.cjs +622 -0
- package/dist/index.browser.cjs.map +1 -0
- package/dist/index.browser.mjs +614 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.node.cjs +100 -0
- package/dist/index.node.cjs.map +1 -0
- package/dist/index.node.mjs +91 -0
- package/dist/index.node.mjs.map +1 -0
- package/dist/index.react-native.mjs +91 -0
- package/dist/index.react-native.mjs.map +1 -0
- package/dist/react/index.browser.cjs +77 -0
- package/dist/react/index.browser.cjs.map +1 -0
- package/dist/react/index.browser.mjs +59 -0
- package/dist/react/index.browser.mjs.map +1 -0
- package/dist/react/index.node.cjs +77 -0
- package/dist/react/index.node.cjs.map +1 -0
- package/dist/react/index.node.mjs +59 -0
- package/dist/react/index.node.mjs.map +1 -0
- package/dist/react/index.react-native.mjs +59 -0
- package/dist/react/index.react-native.mjs.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/react/index.d.ts +235 -0
- package/dist/types/react/index.d.ts.map +1 -0
- package/dist/types/status.d.ts +31 -0
- package/dist/types/status.d.ts.map +1 -0
- package/dist/types/store.d.ts +16 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/types.d.ts +384 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/wallet.d.ts +133 -0
- package/dist/types/wallet.d.ts.map +1 -0
- package/package.json +103 -7
- package/src/__typetests__/wallet-typetest.ts +129 -0
- package/src/index.ts +3 -0
- package/src/react/__typetests__/index.ts +108 -0
- package/src/react/index.ts +301 -0
- package/src/status.ts +33 -0
- package/src/store.ts +990 -0
- package/src/types/global.d.ts +6 -0
- package/src/types.ts +407 -0
- package/src/wallet.ts +218 -0
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
|
+
}
|