@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.
@@ -0,0 +1,533 @@
1
+ import { withCleanup, extendClient, SolanaError, SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED, SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE, SOLANA_ERROR__WALLET__NOT_CONNECTED, SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE } from '@solana/kit';
2
+ import { createSignerFromWalletAccount } from '@solana/wallet-account-signer';
3
+ import { SolanaSignMessage, SolanaSignIn } from '@solana/wallet-standard-features';
4
+ import { getWallets } from '@wallet-standard/app';
5
+ import { StandardConnect, StandardDisconnect, StandardEvents } from '@wallet-standard/features';
6
+ import { getWalletFeature, getWalletAccountFeature } from '@wallet-standard/ui-features';
7
+ import { getOrCreateUiWalletForStandardWallet, getWalletForHandle } from '@wallet-standard/ui-registry';
8
+
9
+ // src/wallet.ts
10
+ function createWalletStore(config) {
11
+ let state = {
12
+ account: null,
13
+ connectedWallet: null,
14
+ signer: null,
15
+ status: "pending",
16
+ wallets: []
17
+ };
18
+ const listeners = /* @__PURE__ */ new Set();
19
+ const signerListeners = /* @__PURE__ */ new Set();
20
+ let walletEventsCleanup = null;
21
+ let reconnectCleanup = null;
22
+ let userHasSelected = false;
23
+ let connectGeneration = 0;
24
+ let disposed = false;
25
+ let disconnectingWalletName = null;
26
+ function resolveDefaultStorage() {
27
+ try {
28
+ return localStorage;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ const storage = config.storage === null ? null : config.storage ?? resolveDefaultStorage();
34
+ const storageKey = config.storageKey ?? "kit-wallet";
35
+ function deriveSnapshot(s) {
36
+ return Object.freeze({
37
+ connected: s.connectedWallet && s.account ? Object.freeze({
38
+ account: s.account,
39
+ signer: s.signer,
40
+ wallet: s.connectedWallet
41
+ }) : null,
42
+ status: s.status,
43
+ wallets: s.wallets
44
+ });
45
+ }
46
+ let snapshot = deriveSnapshot(state);
47
+ function observableSigner(s) {
48
+ return s.connectedWallet && s.account ? s.signer : void 0;
49
+ }
50
+ function updateState(updates) {
51
+ const prev = state;
52
+ state = { ...state, ...updates };
53
+ const signerChanged = observableSigner(state) !== observableSigner(prev);
54
+ if (state.connectedWallet !== prev.connectedWallet || state.account !== prev.account || state.status !== prev.status || state.signer !== prev.signer || state.wallets !== prev.wallets) {
55
+ snapshot = deriveSnapshot(state);
56
+ listeners.forEach((l) => l());
57
+ }
58
+ if (signerChanged) {
59
+ signerListeners.forEach((l) => l());
60
+ }
61
+ }
62
+ function tryCreateSigner(account) {
63
+ try {
64
+ return createSignerFromWalletAccount(account, config.chain);
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ const registry = getWallets();
70
+ function filterWallet(uiWallet) {
71
+ const supportsChain = uiWallet.chains.includes(config.chain);
72
+ const supportsConnect = uiWallet.features.includes(StandardConnect);
73
+ if (!supportsChain || !supportsConnect) return false;
74
+ return config.filter ? config.filter(uiWallet) : true;
75
+ }
76
+ function buildWalletList() {
77
+ return Object.freeze(registry.get().map(getOrCreateUiWalletForStandardWallet).filter(filterWallet));
78
+ }
79
+ function reconcileWalletList() {
80
+ const next = buildWalletList();
81
+ const prev = state.wallets;
82
+ if (next.length === prev.length && next.every((w, i) => w === prev[i])) {
83
+ return prev;
84
+ }
85
+ return next;
86
+ }
87
+ function isWalletStillAvailable(uiWallet) {
88
+ return buildWalletList().some((w) => w.name === uiWallet.name);
89
+ }
90
+ updateState({ wallets: buildWalletList() });
91
+ const unsubRegister = registry.on("register", () => {
92
+ updateState({ wallets: reconcileWalletList() });
93
+ });
94
+ const unsubUnregister = registry.on("unregister", () => {
95
+ const newWallets = reconcileWalletList();
96
+ const updates = { wallets: newWallets };
97
+ if (state.connectedWallet && !newWallets.some((w) => w.name === state.connectedWallet.name)) {
98
+ walletEventsCleanup?.();
99
+ walletEventsCleanup = null;
100
+ updates.connectedWallet = null;
101
+ updates.account = null;
102
+ updates.signer = null;
103
+ updates.status = "disconnected";
104
+ clearPersistedAccount();
105
+ }
106
+ updateState(updates);
107
+ });
108
+ function cancelReconnect() {
109
+ reconnectCleanup?.();
110
+ reconnectCleanup = null;
111
+ }
112
+ function resubscribeToWalletEvents(uiWallet) {
113
+ walletEventsCleanup?.();
114
+ walletEventsCleanup = subscribeToWalletEvents(uiWallet);
115
+ }
116
+ function setConnected(account, wallet, options) {
117
+ if (disposed) return;
118
+ const signer = tryCreateSigner(account);
119
+ resubscribeToWalletEvents(wallet);
120
+ disconnectingWalletName = null;
121
+ updateState({ account, connectedWallet: wallet, signer, status: "connected" });
122
+ if (options?.persist !== false) {
123
+ persistAccount(account, wallet);
124
+ }
125
+ }
126
+ function refreshUiWallet(staleUiWallet) {
127
+ const rawWallet = getWalletForHandle(staleUiWallet);
128
+ return getOrCreateUiWalletForStandardWallet(rawWallet);
129
+ }
130
+ function subscribeToWalletEvents(uiWallet) {
131
+ if (!uiWallet.features.includes(StandardEvents)) {
132
+ return () => {
133
+ };
134
+ }
135
+ const eventsFeature = getWalletFeature(
136
+ uiWallet,
137
+ StandardEvents
138
+ );
139
+ return eventsFeature.on("change", () => {
140
+ const refreshed = refreshUiWallet(uiWallet);
141
+ if (!filterWallet(refreshed)) {
142
+ disconnectLocally();
143
+ return;
144
+ }
145
+ const result = reconcileActiveAccount(refreshed);
146
+ if (result === null) {
147
+ disconnectLocally();
148
+ return;
149
+ }
150
+ updateState({ connectedWallet: refreshed, wallets: reconcileWalletList(), ...result });
151
+ if (result.account) {
152
+ persistAccount(result.account, refreshed);
153
+ }
154
+ });
155
+ }
156
+ function reconcileActiveAccount(wallet) {
157
+ const newAccounts = wallet.accounts;
158
+ if (newAccounts.length === 0) return null;
159
+ const currentAddress = state.account?.address;
160
+ const stillPresent = currentAddress ? newAccounts.find((a) => a.address === currentAddress) : null;
161
+ const activeAccount = stillPresent ?? newAccounts[0];
162
+ if (activeAccount === state.account) return {};
163
+ return { account: activeAccount, signer: tryCreateSigner(activeAccount) };
164
+ }
165
+ async function connect(uiWallet, options) {
166
+ options?.abortSignal?.throwIfAborted();
167
+ const connectFeature = getWalletFeature(
168
+ uiWallet,
169
+ StandardConnect
170
+ );
171
+ userHasSelected = true;
172
+ cancelReconnect();
173
+ const generation = ++connectGeneration;
174
+ updateState({ status: "connecting" });
175
+ try {
176
+ const existingAccounts = [...uiWallet.accounts];
177
+ await connectFeature.connect();
178
+ if (generation !== connectGeneration) {
179
+ throw new DOMException("Wallet connect was superseded by a newer connect or sign-in", "AbortError");
180
+ }
181
+ if (!isWalletStillAvailable(uiWallet)) {
182
+ throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: "connect" });
183
+ }
184
+ const refreshedWallet = refreshUiWallet(uiWallet);
185
+ const allAccounts = refreshedWallet.accounts;
186
+ if (allAccounts.length === 0) {
187
+ throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: "connect" });
188
+ }
189
+ const newAccount = allAccounts.find((a) => !existingAccounts.some((e) => e.address === a.address));
190
+ const activeAccount = newAccount ?? allAccounts[0];
191
+ setConnected(activeAccount, refreshedWallet);
192
+ return allAccounts;
193
+ } catch (error) {
194
+ if (generation === connectGeneration) {
195
+ revertToPreviousConnectionOrDisconnect();
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+ async function disconnect(options) {
201
+ options?.abortSignal?.throwIfAborted();
202
+ if (!state.connectedWallet) {
203
+ userHasSelected = true;
204
+ cancelReconnect();
205
+ connectGeneration++;
206
+ updateState({ status: "disconnected" });
207
+ clearPersistedAccount();
208
+ return;
209
+ }
210
+ const currentWallet = state.connectedWallet;
211
+ const generation = ++connectGeneration;
212
+ disconnectingWalletName = currentWallet.name;
213
+ updateState({ status: "disconnecting" });
214
+ try {
215
+ if (currentWallet.features.includes(StandardDisconnect)) {
216
+ const disconnectFeature = getWalletFeature(
217
+ currentWallet,
218
+ StandardDisconnect
219
+ );
220
+ await disconnectFeature.disconnect();
221
+ }
222
+ } finally {
223
+ if (generation === connectGeneration) {
224
+ disconnectLocally();
225
+ }
226
+ }
227
+ }
228
+ function disconnectLocally() {
229
+ walletEventsCleanup?.();
230
+ walletEventsCleanup = null;
231
+ disconnectingWalletName = null;
232
+ updateState({
233
+ account: null,
234
+ connectedWallet: null,
235
+ signer: null,
236
+ status: "disconnected",
237
+ // Reconcile the list too: when this runs from the change handler
238
+ // because the connected wallet dropped the configured chain or
239
+ // failed the filter, that wallet must leave `wallets` immediately
240
+ // rather than linger until an unrelated register/unregister event.
241
+ // `reconcileWalletList` returns the existing reference when the
242
+ // visible set is unchanged (the common case for a plain disconnect),
243
+ // so this stays churn-free.
244
+ wallets: reconcileWalletList()
245
+ });
246
+ clearPersistedAccount();
247
+ }
248
+ function revertToPreviousConnectionOrDisconnect() {
249
+ if (state.connectedWallet && state.account && state.connectedWallet.name !== disconnectingWalletName) {
250
+ updateState({ status: "connected" });
251
+ } else {
252
+ disconnectLocally();
253
+ }
254
+ }
255
+ function selectAccount(account) {
256
+ if (!state.connectedWallet) {
257
+ throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: "selectAccount" });
258
+ }
259
+ if (state.connectedWallet.name === disconnectingWalletName) {
260
+ throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: "selectAccount" });
261
+ }
262
+ const refreshed = refreshUiWallet(state.connectedWallet);
263
+ const selectedAccount = refreshed.accounts.find((a) => a.address === account.address);
264
+ if (!selectedAccount) {
265
+ throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {
266
+ account: account.address,
267
+ operation: "selectAccount"
268
+ });
269
+ }
270
+ userHasSelected = true;
271
+ ++connectGeneration;
272
+ if (selectedAccount === state.account && refreshed === state.connectedWallet) {
273
+ updateState({ status: "connected" });
274
+ return;
275
+ }
276
+ const signer = tryCreateSigner(selectedAccount);
277
+ updateState({ account: selectedAccount, connectedWallet: refreshed, signer, status: "connected" });
278
+ persistAccount(selectedAccount, refreshed);
279
+ }
280
+ async function signMessage(message, options) {
281
+ options?.abortSignal?.throwIfAborted();
282
+ const { connectedWallet, account } = state;
283
+ if (!connectedWallet || !account) {
284
+ throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: "signMessage" });
285
+ }
286
+ const signMessageFeature = getWalletAccountFeature(
287
+ account,
288
+ SolanaSignMessage
289
+ );
290
+ const [output] = await signMessageFeature.signMessage({ account, message });
291
+ return output.signature;
292
+ }
293
+ async function signIn(uiWallet, input, options) {
294
+ options?.abortSignal?.throwIfAborted();
295
+ const signInFeature = getWalletFeature(uiWallet, SolanaSignIn);
296
+ userHasSelected = true;
297
+ cancelReconnect();
298
+ const generation = ++connectGeneration;
299
+ updateState({ status: "connecting" });
300
+ try {
301
+ const [result] = await signInFeature.signIn(input);
302
+ if (generation !== connectGeneration) {
303
+ throw new DOMException("Wallet sign-in was superseded by a newer connect or sign-in", "AbortError");
304
+ }
305
+ if (!isWalletStillAvailable(uiWallet)) {
306
+ throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: "signIn" });
307
+ }
308
+ const refreshedWallet = refreshUiWallet(uiWallet);
309
+ const activeAccount = refreshedWallet.accounts.find((a) => a.address === result.account.address);
310
+ if (!activeAccount) {
311
+ throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: "signIn" });
312
+ }
313
+ setConnected(activeAccount, refreshedWallet);
314
+ return result;
315
+ } catch (error) {
316
+ if (generation === connectGeneration) {
317
+ revertToPreviousConnectionOrDisconnect();
318
+ }
319
+ throw error;
320
+ }
321
+ }
322
+ function ignoreStorageFailure(operation) {
323
+ void (async () => {
324
+ try {
325
+ await operation();
326
+ } catch {
327
+ }
328
+ })();
329
+ }
330
+ function persistAccount(account, wallet) {
331
+ if (!storage) return;
332
+ ignoreStorageFailure(() => storage.setItem(storageKey, `${wallet.name}:${account.address}`));
333
+ }
334
+ function clearPersistedAccount() {
335
+ if (!storage) return;
336
+ ignoreStorageFailure(() => storage.removeItem(storageKey));
337
+ }
338
+ if (config.autoConnect !== false && storage) {
339
+ const generation = connectGeneration;
340
+ (async () => {
341
+ const savedKey = await storage.getItem(storageKey);
342
+ if (userHasSelected || disposed) return;
343
+ if (!savedKey) {
344
+ updateState({ status: "disconnected" });
345
+ return;
346
+ }
347
+ const separatorIndex = savedKey.lastIndexOf(":");
348
+ if (separatorIndex === -1) {
349
+ updateState({ status: "disconnected" });
350
+ clearPersistedAccount();
351
+ return;
352
+ }
353
+ const walletName = savedKey.slice(0, separatorIndex);
354
+ const savedAddress = savedKey.slice(separatorIndex + 1);
355
+ const existing = state.wallets.find((w) => w.name === walletName);
356
+ if (existing) {
357
+ await attemptSilentReconnect(savedAddress, existing);
358
+ } else if (registry.get().some((w) => {
359
+ const ui = getOrCreateUiWalletForStandardWallet(w);
360
+ return ui.name === walletName;
361
+ })) {
362
+ updateState({ status: "disconnected" });
363
+ clearPersistedAccount();
364
+ } else {
365
+ updateState({ status: "reconnecting" });
366
+ const statusTimeout = setTimeout(() => {
367
+ if (!userHasSelected && state.status === "reconnecting") {
368
+ updateState({ status: "disconnected" });
369
+ }
370
+ }, 3e3);
371
+ const unsubRegisterForReconnect = registry.on("register", () => {
372
+ const generation2 = connectGeneration;
373
+ void (async () => {
374
+ if (userHasSelected) {
375
+ clearTimeout(statusTimeout);
376
+ unsubRegisterForReconnect();
377
+ reconnectCleanup = null;
378
+ return;
379
+ }
380
+ const found = buildWalletList().find((w) => w.name === walletName);
381
+ if (found) {
382
+ clearTimeout(statusTimeout);
383
+ unsubRegisterForReconnect();
384
+ reconnectCleanup = null;
385
+ await attemptSilentReconnect(savedAddress, found);
386
+ } else if (registry.get().some((w) => {
387
+ const ui = getOrCreateUiWalletForStandardWallet(w);
388
+ return ui.name === walletName;
389
+ })) {
390
+ clearTimeout(statusTimeout);
391
+ unsubRegisterForReconnect();
392
+ reconnectCleanup = null;
393
+ updateState({ status: "disconnected" });
394
+ clearPersistedAccount();
395
+ }
396
+ })().catch(() => {
397
+ if (generation2 === connectGeneration && !userHasSelected) {
398
+ updateState({ status: "disconnected" });
399
+ }
400
+ });
401
+ });
402
+ reconnectCleanup = () => {
403
+ clearTimeout(statusTimeout);
404
+ unsubRegisterForReconnect();
405
+ };
406
+ }
407
+ })().catch(() => {
408
+ if (generation === connectGeneration && !userHasSelected) {
409
+ updateState({ status: "disconnected" });
410
+ }
411
+ });
412
+ } else {
413
+ updateState({ status: "disconnected" });
414
+ }
415
+ async function attemptSilentReconnect(savedAddress, uiWallet) {
416
+ const generation = ++connectGeneration;
417
+ updateState({ status: "reconnecting" });
418
+ try {
419
+ const connectFeature = getWalletFeature(
420
+ uiWallet,
421
+ StandardConnect
422
+ );
423
+ await connectFeature.connect({ silent: true });
424
+ if (generation !== connectGeneration) return;
425
+ if (!isWalletStillAvailable(uiWallet)) {
426
+ updateState({ status: "disconnected" });
427
+ clearPersistedAccount();
428
+ return;
429
+ }
430
+ const refreshedWallet = refreshUiWallet(uiWallet);
431
+ const allAccounts = refreshedWallet.accounts;
432
+ if (allAccounts.length === 0) {
433
+ updateState({ status: "disconnected" });
434
+ clearPersistedAccount();
435
+ return;
436
+ }
437
+ const activeAccount = allAccounts.find((a) => a.address === savedAddress) ?? allAccounts[0];
438
+ setConnected(activeAccount, refreshedWallet, {
439
+ persist: activeAccount.address !== savedAddress
440
+ });
441
+ } catch {
442
+ if (generation === connectGeneration) {
443
+ updateState({ status: "disconnected" });
444
+ clearPersistedAccount();
445
+ }
446
+ }
447
+ }
448
+ return {
449
+ connect,
450
+ disconnect,
451
+ getState: () => snapshot,
452
+ selectAccount,
453
+ signIn,
454
+ signMessage,
455
+ subscribe: (listener) => {
456
+ listeners.add(listener);
457
+ return () => {
458
+ listeners.delete(listener);
459
+ };
460
+ },
461
+ subscribeSigner: (listener) => {
462
+ signerListeners.add(listener);
463
+ return () => {
464
+ signerListeners.delete(listener);
465
+ };
466
+ },
467
+ [Symbol.dispose]: () => {
468
+ disposed = true;
469
+ connectGeneration++;
470
+ unsubRegister();
471
+ unsubUnregister();
472
+ walletEventsCleanup?.();
473
+ walletEventsCleanup = null;
474
+ cancelReconnect();
475
+ listeners.clear();
476
+ signerListeners.clear();
477
+ }
478
+ };
479
+ }
480
+
481
+ // src/wallet.ts
482
+ function defineSignerGetter(additions, property, store) {
483
+ Object.defineProperty(additions, property, {
484
+ configurable: true,
485
+ enumerable: true,
486
+ get() {
487
+ const state = store.getState();
488
+ if (!state.connected) {
489
+ throw new SolanaError(SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED, { status: state.status });
490
+ }
491
+ if (!state.connected.signer) {
492
+ throw new SolanaError(SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE);
493
+ }
494
+ return state.connected.signer;
495
+ }
496
+ });
497
+ }
498
+ var SUBSCRIBE_PROPERTY = {
499
+ identity: "subscribeToIdentity",
500
+ payer: "subscribeToPayer"
501
+ };
502
+ function createPlugin(config, signerProperties) {
503
+ return (client) => {
504
+ if ("wallet" in client) {
505
+ throw new Error(
506
+ "Only one wallet plugin can be used per client. Use walletSigner, walletPayer, walletIdentity, or walletWithoutSigner \u2014 not multiple."
507
+ );
508
+ }
509
+ const store = createWalletStore(config);
510
+ const additions = { wallet: store };
511
+ for (const prop of signerProperties) {
512
+ defineSignerGetter(additions, prop, store);
513
+ additions[SUBSCRIBE_PROPERTY[prop]] = store.subscribeSigner;
514
+ }
515
+ return withCleanup(extendClient(client, additions), () => store[Symbol.dispose]());
516
+ };
517
+ }
518
+ function walletSigner(config) {
519
+ return createPlugin(config, ["payer", "identity"]);
520
+ }
521
+ function walletIdentity(config) {
522
+ return createPlugin(config, ["identity"]);
523
+ }
524
+ function walletPayer(config) {
525
+ return createPlugin(config, ["payer"]);
526
+ }
527
+ function walletWithoutSigner(config) {
528
+ return createPlugin(config, []);
529
+ }
530
+
531
+ export { walletIdentity, walletPayer, walletSigner, walletWithoutSigner };
532
+ //# sourceMappingURL=index.browser.mjs.map
533
+ //# sourceMappingURL=index.browser.mjs.map