@solana/kit-plugin-wallet 0.13.0 → 0.13.1
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/dist/index.browser.mjs +5 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.node.mjs +5 -2
- package/dist/index.node.mjs.map +1 -1
- package/dist/index.react-native.mjs +5 -2
- package/dist/index.react-native.mjs.map +1 -1
- package/dist/react/index.browser.mjs +8 -1
- package/dist/react/index.browser.mjs.map +1 -1
- package/dist/react/index.node.mjs +8 -1
- package/dist/react/index.node.mjs.map +1 -1
- package/dist/react/index.react-native.mjs +8 -1
- package/dist/react/index.react-native.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.node.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/store.ts","../src/wallet.ts"],"names":["SolanaError"],"mappings":"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","file":"index.node.mjs","sourcesContent":["import {\n type ReadonlyUint8Array,\n type SignatureBytes,\n SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE,\n SOLANA_ERROR__WALLET__NOT_CONNECTED,\n SolanaError,\n} from '@solana/kit';\nimport { createSignerFromWalletAccount } from '@solana/wallet-account-signer';\nimport {\n SolanaSignIn,\n type SolanaSignInFeature,\n type SolanaSignInInput,\n type SolanaSignInOutput,\n SolanaSignMessage,\n type SolanaSignMessageFeature,\n} from '@solana/wallet-standard-features';\nimport { getWallets } from '@wallet-standard/app';\nimport {\n StandardConnect,\n type StandardConnectFeature,\n StandardDisconnect,\n type StandardDisconnectFeature,\n StandardEvents,\n type StandardEventsFeature,\n} from '@wallet-standard/features';\nimport type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';\nimport { getWalletAccountFeature, getWalletFeature } from '@wallet-standard/ui-features';\nimport { getOrCreateUiWalletForStandardWallet, getWalletForHandle } from '@wallet-standard/ui-registry';\n\nimport { isWalletWarmingUp } from './status';\nimport type {\n WalletActionOptions,\n WalletNamespace,\n WalletPluginConfig,\n WalletSigner,\n WalletState,\n WalletStatus,\n WalletStorage,\n} from './types';\n\n// -- Internal types ---------------------------------------------------------\n\n/** @internal */\nexport type WalletStore = WalletNamespace & {\n /**\n * Registers a listener notified only when the wallet signer may\n * have changed.\n * Returns an idempotent unsubscribe function.\n *\n * @internal\n */\n subscribeSigner: (listener: () => void) => () => void;\n [Symbol.dispose]: () => void;\n};\n\n// Flat internal state for easier updates\ntype WalletStoreState = {\n account: UiWalletAccount | null;\n connectedWallet: UiWallet | null;\n signer: WalletSigner | null;\n status: WalletStatus;\n wallets: readonly UiWallet[];\n};\n\n// -- Store ------------------------------------------------------------------\n\n/** @internal */\nexport function createWalletStore(config: WalletPluginConfig): WalletStore {\n // -- SSR guard: on the server, return an inert stub ---------------------\n\n if (!__BROWSER__ || __REACTNATIVE__) {\n const ssrSnapshot: WalletState = Object.freeze({\n connected: null,\n status: 'pending' as const,\n wallets: Object.freeze([]) as readonly UiWallet[],\n });\n return {\n connect: () =>\n Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' })),\n disconnect: () => Promise.resolve(),\n getState: () => ssrSnapshot,\n selectAccount: () => {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });\n },\n signIn: () => Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' })),\n signMessage: () =>\n Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' })),\n subscribe: () => () => {},\n subscribeSigner: () => () => {},\n whenReady: () => Promise.resolve(),\n [Symbol.dispose]: () => {},\n };\n }\n\n // -- Browser-only initialization below this point ----------------------\n\n let state: WalletStoreState = {\n account: null,\n connectedWallet: null,\n signer: null,\n status: 'pending',\n wallets: [],\n };\n\n const listeners = new Set<() => void>();\n const signerListeners = new Set<() => void>();\n // One `standard:events change` subscription per discovered wallet, keyed by\n // name (the stable identity; the UiWallet handle regenerates on changes). The\n // underlying subscription attaches to the raw wallet's events, so it survives\n // handle regeneration — no need to resubscribe when a wallet's accounts change.\n const walletEventSubscriptions = new Map<string, () => void>();\n let reconnectCleanup: (() => void) | null = null;\n let userHasSelected = false;\n let connectGeneration = 0;\n let disposed = false;\n // Wallet that is in the process of disconnecting, stored to guard against re-connecting to it if a future connect fails\n let disconnectingWalletName: string | null = null;\n // Deferred backing `whenReady`. Created lazily while the status is transient\n // (see `isWalletWarmingUp`) and resolved by `updateState` once the status\n // settles, so `whenReady` can hand out a stable promise for the duration of\n // one warm-up episode (required by React Suspense).\n let readyDeferred: PromiseWithResolvers<void> | null = null;\n\n // Resolve storage: default to localStorage in browser, null to disable.\n // Merely *accessing* `localStorage` throws a `SecurityError` in sandboxed\n // iframes or when third-party storage is blocked, so the default resolution\n // degrades to `null` rather than letting the whole plugin-install chain\n // throw. This matches the best-effort persistence applied to reads/writes.\n function resolveDefaultStorage(): WalletStorage | null {\n try {\n return localStorage;\n } catch {\n return null;\n }\n }\n\n const storage: WalletStorage | null = config.storage === null ? null : (config.storage ?? resolveDefaultStorage());\n const storageKey = config.storageKey ?? 'kit-wallet';\n\n // -- State management --------------------------------------------------\n\n function deriveSnapshot(s: WalletStoreState): WalletState {\n return Object.freeze({\n connected:\n s.connectedWallet && s.account\n ? Object.freeze({\n account: s.account,\n signer: s.signer,\n wallet: s.connectedWallet,\n })\n : null,\n status: s.status,\n wallets: s.wallets,\n });\n }\n\n let snapshot: WalletState = deriveSnapshot(state);\n\n // We set `client.{payer,identity}` to `getState().connected?.signer\n // This is `undefined` if disconnected, `null` if connected to a read-only wallet,\n // or the signer reference otherwise.\n function observableSigner(s: WalletStoreState): WalletSigner | null | undefined {\n return s.connectedWallet && s.account ? s.signer : undefined;\n }\n\n function updateState(updates: Partial<WalletStoreState>): void {\n const prev = state;\n state = { ...state, ...updates };\n\n // Resolve `whenReady` the moment the status leaves the transient\n // warm-up set. `readyDeferred` is only ever created while transient (in\n // `whenReady`), so clearing it here lets the next transient episode arm\n // a fresh one.\n if (isWalletWarmingUp(prev.status) && !isWalletWarmingUp(state.status)) {\n readyDeferred?.resolve();\n readyDeferred = null;\n }\n\n if (state.wallets !== prev.wallets) {\n syncWalletEventSubscriptions(state.wallets);\n }\n\n // Signer subscribers (`subscribeToPayer` / `subscribeToIdentity`) fire\n // only when the observed signer changes.\n const signerChanged = observableSigner(state) !== observableSigner(prev);\n\n if (\n state.connectedWallet !== prev.connectedWallet ||\n state.account !== prev.account ||\n state.status !== prev.status ||\n state.signer !== prev.signer ||\n state.wallets !== prev.wallets\n ) {\n snapshot = deriveSnapshot(state);\n listeners.forEach(l => l());\n }\n\n if (signerChanged) {\n signerListeners.forEach(l => l());\n }\n }\n\n // Advanced: see the `whenReady` docblock on `WalletNamespace`. Returns a\n // stable, already-resolved promise once the initial warm-up has settled;\n // while transient, returns the same pending promise on every call so it is\n // safe to throw for React Suspense.\n function whenReady(): Promise<void> {\n if (!isWalletWarmingUp(state.status)) {\n return Promise.resolve();\n }\n readyDeferred ??= Promise.withResolvers<void>();\n return readyDeferred.promise;\n }\n\n // -- Signer creation ---------------------------------------------------\n\n function tryCreateSigner(account: UiWalletAccount): WalletSigner | null {\n try {\n // `createSignerFromWalletAccount` throws for chains the wallet\n // doesn't support — which we catch below. Non-Solana chains\n // therefore degrade to `signer: null`, matching the read-only\n // wallet contract.\n return createSignerFromWalletAccount(account, config.chain);\n } catch {\n // Wallet doesn't support signing (read-only / watch wallet, or a\n // non-Solana chain).\n return null;\n }\n }\n\n // -- Wallet discovery --------------------------------------------------\n\n // `getWallets()` is a wallet-standard function that returns the `Wallets` API\n // This includes a `get()` function to get the current list of wallets, and\n // an `on()` function to subscribe to wallet registration/unregistration events.\n const registry = getWallets();\n\n function filterWallet(uiWallet: UiWallet): boolean {\n const supportsChain = uiWallet.chains.includes(config.chain);\n const supportsConnect = uiWallet.features.includes(StandardConnect);\n if (!supportsChain || !supportsConnect) return false;\n return config.filter ? config.filter(uiWallet) : true;\n }\n\n function buildWalletList(): readonly UiWallet[] {\n return Object.freeze(registry.get().map(getOrCreateUiWalletForStandardWallet).filter(filterWallet));\n }\n\n // Rebuilds the filtered wallet list but returns the *existing* `state.wallets`\n // reference when the contents are unchanged. `buildWalletList` always\n // allocates a fresh frozen array, so without this a registry event for a\n // wallet that gets filtered out (or any event that doesn't alter the visible\n // set) would still produce a new snapshot and notify every listener — a\n // spurious re-render. Wallets are compared by reference, in order: the\n // wallet-ui registry hands back a stable UiWallet for a given underlying\n // wallet until its accounts/features/chains change, so reference equality is\n // a sound same-contents check here.\n function reconcileWalletList(): readonly UiWallet[] {\n const next = buildWalletList();\n const prev = state.wallets;\n if (next.length === prev.length && next.every((w, i) => w === prev[i])) {\n return prev;\n }\n return next;\n }\n\n // Reconcile the per-wallet event subscriptions against the current wallet set:\n // subscribe wallets that just appeared, unsubscribe those that left. Called\n // whenever `state.wallets` changes reference. Subscribing fires nothing, so\n // this never causes re-render churn. `wallets` is the already-filtered visible\n // list, so wallets excluded by chain/feature/`config.filter` are intentionally\n // never subscribed — a `register` / `unregister` event reconciles them in or\n // out of the list (and thus in or out of this map) when their eligibility\n // changes.\n function syncWalletEventSubscriptions(wallets: readonly UiWallet[]): void {\n const names = new Set(wallets.map(w => w.name));\n for (const [name, cleanup] of walletEventSubscriptions) {\n if (!names.has(name)) {\n cleanup();\n walletEventSubscriptions.delete(name);\n }\n }\n for (const w of wallets) {\n if (!walletEventSubscriptions.has(w.name)) {\n walletEventSubscriptions.set(w.name, subscribeToWalletEvents(w));\n }\n }\n }\n\n // True if a wallet matching `uiWallet` (by name) is currently registered and\n // passes the filter. Used to re-check membership after an awaited connect /\n // sign-in / silent reconnect resolves: the wallet may have unregistered (or\n // dropped a required feature/chain) while its prompt was open, and the\n // unregister handler can't tear down a connection that hasn't been\n // established yet. Compared by name — the same identity the unregister\n // handler uses — because the wallet-ui registry hands back a fresh UiWallet\n // reference once connect adds accounts, so reference equality would not hold.\n function isWalletStillAvailable(uiWallet: UiWallet): boolean {\n return buildWalletList().some(w => w.name === uiWallet.name);\n }\n\n updateState({ wallets: buildWalletList() });\n\n // Listen for new wallets being registered\n const unsubRegister = registry.on('register', () => {\n updateState({ wallets: reconcileWalletList() });\n });\n\n // Listen for wallets being unregistered — if the connected wallet is removed, disconnect\n const unsubUnregister = registry.on('unregister', () => {\n const newWallets = reconcileWalletList();\n const updates: Partial<WalletStoreState> = { wallets: newWallets };\n\n // If the connected wallet was unregistered, disconnect locally.\n if (state.connectedWallet && !newWallets.some(w => w.name === state.connectedWallet!.name)) {\n updates.connectedWallet = null;\n updates.account = null;\n updates.signer = null;\n updates.status = 'disconnected';\n clearPersistedAccount();\n }\n\n updateState(updates);\n });\n\n // -- Wallet-initiated events -------------------------------------------\n\n function cancelReconnect(): void {\n reconnectCleanup?.();\n reconnectCleanup = null;\n }\n\n function setConnected(account: UiWalletAccount, wallet: UiWallet, options?: { persist?: boolean }): void {\n // The store may have been disposed while a connect / sign-in / silent\n // reconnect awaited. The `connectGeneration` guard at each call site\n // doesn't cover the auto-connect IIFE resuming from its storage read —\n // it captures a fresh generation in `attemptSilentReconnect` after\n // disposal. Bail here so a disposed store never re-subscribes to wallet\n // events (a listener the already-run disposer can't clean up) or reports\n // itself connected.\n if (disposed) return;\n const signer = tryCreateSigner(account);\n disconnectingWalletName = null;\n // Reconcile `wallets` alongside the connection: `connect`/`signIn`/silent\n // reconnect authorize accounts on the underlying wallet, which regenerates\n // its registry handle. Without rebuilding the list here, `getState().wallets`\n // would keep the pre-connect handle (empty `.accounts`) until an unrelated\n // `standard:events change` happened to fire — so consumers reading the\n // just-connected wallet from `wallets` (e.g. rendering its authorized\n // accounts) would see it as unconnected. `reconcileWalletList` returns the\n // existing reference when the visible set is unchanged, keeping the snapshot\n // churn-free when connect didn't actually add accounts.\n updateState({\n account,\n connectedWallet: wallet,\n signer,\n status: 'connected',\n wallets: reconcileWalletList(),\n });\n if (options?.persist !== false) {\n persistAccount(account, wallet);\n }\n }\n\n function refreshUiWallet(staleUiWallet: UiWallet): UiWallet {\n const rawWallet = getWalletForHandle(staleUiWallet);\n return getOrCreateUiWalletForStandardWallet(rawWallet);\n }\n\n function subscribeToWalletEvents(uiWallet: UiWallet): () => void {\n if (!uiWallet.features.includes(StandardEvents)) {\n return () => {};\n }\n const eventsFeature = getWalletFeature(\n uiWallet,\n StandardEvents,\n ) as StandardEventsFeature[typeof StandardEvents];\n const walletName = uiWallet.name;\n\n // The handler intentionally ignores the changed-property flags\n // (`accounts`, `chains`, `features`) the wallet passes and instead\n // reconciles against the refreshed wallet. The signer is derived\n // entirely from the active account's features and chains, and the\n // wallet-ui registry regenerates the account reference precisely when\n // one of those changes — so the account reference, not the flags, is\n // the single source of truth for whether the signer is affected. Branch\n // on the reconciled state below, never on `properties`.\n return eventsFeature.on('change', () => {\n // Refresh the discovered list so this wallet's (and any others') accounts\n // are current; `updateState` re-syncs subscriptions for added/removed wallets.\n const newWallets = reconcileWalletList();\n\n // Non-active wallet: reflect its new account/feature state in the list only.\n if (!state.connectedWallet || state.connectedWallet.name !== walletName) {\n updateState({ wallets: newWallets });\n return;\n }\n\n // Active wallet: the original reconcile logic, unchanged.\n const refreshed = refreshUiWallet(state.connectedWallet);\n if (!filterWallet(refreshed)) {\n disconnectLocally();\n return;\n }\n const result = reconcileActiveAccount(refreshed);\n if (result === null) {\n disconnectLocally();\n return;\n }\n updateState({ connectedWallet: refreshed, wallets: newWallets, ...result });\n if (result.account) {\n persistAccount(result.account, refreshed);\n }\n });\n }\n\n function reconcileActiveAccount(wallet: UiWallet): Partial<WalletStoreState> | null {\n const newAccounts = wallet.accounts;\n\n // No accounts left — the caller disconnects.\n if (newAccounts.length === 0) return null;\n\n const currentAddress = state.account?.address;\n const stillPresent = currentAddress ? newAccounts.find(a => a.address === currentAddress) : null;\n const activeAccount = stillPresent ?? newAccounts[0];\n\n // Same reference — nothing the signer depends on changed, so leave the\n // signer untouched to preserve referential stability.\n if (activeAccount === state.account) return {};\n\n // The active account changed (switched, removed, or regenerated because\n // its features/chains changed) — recreate the signer for it.\n return { account: activeAccount, signer: tryCreateSigner(activeAccount) };\n }\n\n // -- Connection lifecycle ----------------------------------------------\n\n async function connect(uiWallet: UiWallet, options?: WalletActionOptions): Promise<readonly UiWalletAccount[]> {\n options?.abortSignal?.throwIfAborted();\n\n // Resolve the feature before mutating any state. getWalletFeature throws\n // WalletStandardError synchronously when the wallet doesn't support\n // standard:connect — doing this first means an unsupported wallet is a\n // no-op that never tears down an existing connection or cancels a\n // pending reconnect.\n const connectFeature = getWalletFeature(\n uiWallet,\n StandardConnect,\n ) as StandardConnectFeature[typeof StandardConnect];\n\n userHasSelected = true;\n cancelReconnect();\n const generation = ++connectGeneration;\n updateState({ status: 'connecting' });\n\n try {\n // Snapshot existing accounts before connect.\n const existingAccounts = [...uiWallet.accounts];\n\n await connectFeature.connect();\n\n // A newer connect/signIn was started while we were awaiting. Reject\n // with an `AbortError` so this superseded call settles in the\n // standard, ignorable way (matching the abort-signal contract) while\n // the newer request owns the connection. The `catch` below leaves\n // that newer connection untouched, since `generation` no longer\n // matches `connectGeneration`.\n if (generation !== connectGeneration) {\n throw new DOMException('Wallet connect was superseded by a newer connect or sign-in', 'AbortError');\n }\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while its connect prompt was open. The unregister\n // handler only disconnects an already-connected wallet, so without\n // this re-check the store would be left connected to a wallet absent\n // from `wallets`. Throw so the connection failure surfaces as a\n // rejection; the `catch` below reverts to any prior connection.\n if (!isWalletStillAvailable(uiWallet)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' });\n }\n\n // Refresh UiWallet to get updated accounts after connect.\n const refreshedWallet = refreshUiWallet(uiWallet);\n const allAccounts = refreshedWallet.accounts;\n\n // No authorized accounts means there's nothing to connect to. Throw\n // rather than resolving so a caller can't mistake an empty result for\n // a successful connection; the `catch` reverts to any prior wallet.\n if (allAccounts.length === 0) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' });\n }\n\n // Prefer the first newly authorized account.\n const newAccount = allAccounts.find(a => !existingAccounts.some(e => e.address === a.address));\n const activeAccount = newAccount ?? allAccounts[0];\n\n setConnected(activeAccount, refreshedWallet);\n return allAccounts;\n } catch (error) {\n // Only act if we're still the active connection attempt; a\n // superseded attempt must leave the newer connection untouched.\n if (generation === connectGeneration) {\n revertToPreviousConnectionOrDisconnect();\n }\n throw error;\n }\n }\n\n async function disconnect(wallet?: UiWallet, options?: WalletActionOptions): Promise<void> {\n options?.abortSignal?.throwIfAborted();\n\n const target = wallet ?? state.connectedWallet;\n\n // Nothing to disconnect (no arg, nothing active): cancel any pending reconnect, and\n // supersede in-flight attempts.\n if (!target) {\n userHasSelected = true;\n cancelReconnect();\n connectGeneration++;\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const isActive = state.connectedWallet != null && state.connectedWallet.name === target.name;\n\n if (isActive) {\n const currentWallet = state.connectedWallet!;\n // Bump generation so this disconnect supersedes any in-flight connect/signIn.\n const generation = ++connectGeneration;\n disconnectingWalletName = currentWallet.name;\n updateState({ status: 'disconnecting' });\n try {\n if (currentWallet.features.includes(StandardDisconnect)) {\n const disconnectFeature = getWalletFeature(\n currentWallet,\n StandardDisconnect,\n ) as StandardDisconnectFeature[typeof StandardDisconnect];\n await disconnectFeature.disconnect();\n }\n } finally {\n if (generation === connectGeneration) {\n disconnectLocally();\n }\n }\n return;\n }\n\n // targeted disconnect but wallet gone: leave the active connection, status,\n // and persisted key untouched.\n if (!isWalletStillAvailable(target)) {\n return; // already gone / not authorized — forgiving no-op\n }\n const refreshedTarget = refreshUiWallet(target);\n if (!refreshedTarget.features.includes(StandardDisconnect)) {\n return; // can't deauthorize without standard:disconnect — no-op\n }\n // Guard against a concurrent select/connect into this wallet for the await;\n // restore the prior marker afterward (it may belong to an active disconnect).\n const prevDisconnecting = disconnectingWalletName;\n disconnectingWalletName = refreshedTarget.name;\n try {\n const disconnectFeature = getWalletFeature(\n refreshedTarget,\n StandardDisconnect,\n ) as StandardDisconnectFeature[typeof StandardDisconnect];\n await disconnectFeature.disconnect();\n } finally {\n if (disconnectingWalletName === refreshedTarget.name) {\n disconnectingWalletName = prevDisconnecting;\n }\n // Reflect the now-empty accounts immediately — but only if the store\n // is still live. If it was disposed while `standard:disconnect`\n // awaited, the disposer has already torn down every wallet-event\n // subscription and cleared the map; deauthorizing the target\n // regenerated its handle, so `reconcileWalletList` returns a fresh\n // array, and letting `updateState` run here would re-invoke\n // `syncWalletEventSubscriptions` and re-subscribe every wallet into\n // the cleared map — listeners the disposer can no longer clean up.\n if (!disposed) {\n updateState({ wallets: reconcileWalletList() });\n }\n }\n }\n\n function disconnectLocally(): void {\n disconnectingWalletName = null;\n\n updateState({\n account: null,\n connectedWallet: null,\n signer: null,\n status: 'disconnected',\n // Reconcile the list too: when this runs from the change handler\n // because the connected wallet dropped the configured chain or\n // failed the filter, that wallet must leave `wallets` immediately\n // rather than linger until an unrelated register/unregister event.\n // `reconcileWalletList` returns the existing reference when the\n // visible set is unchanged (the common case for a plain disconnect),\n // so this stays churn-free.\n wallets: reconcileWalletList(),\n });\n\n clearPersistedAccount();\n }\n\n // A `connect`/`signIn` attempt failed to establish a new connection. While\n // such an attempt is in flight only `status` is moved to 'connecting' — the\n // existing connection's account, signer, event subscription, and persisted\n // key are untouched until `setConnected` runs on success. So if a prior\n // connection is still intact, revert to it (declining the new wallet must not\n // log the user out of the one they had). Otherwise there's nothing to keep,\n // so disconnect cleanly. The active-attempt (`generation`) guard at each call\n // site ensures a superseded attempt never reverts the connection a newer one\n // owns.\n function revertToPreviousConnectionOrDisconnect(): void {\n // Don't revive a wallet with disconnect in progress\n if (state.connectedWallet && state.account && state.connectedWallet.name !== disconnectingWalletName) {\n updateState({ status: 'connected' });\n } else {\n disconnectLocally();\n }\n }\n\n function selectAccount(account: UiWalletAccount): void {\n // Derive the owning wallet from the account handle. `getWalletForHandle`\n // keys a WeakMap on handle identity, so this is unambiguous even when the\n // same address is authorized in two wallets. A stale/unregistered handle\n // throws — surface it as ACCOUNT_NOT_AVAILABLE.\n let owner: UiWallet;\n try {\n owner = getOrCreateUiWalletForStandardWallet(getWalletForHandle(account));\n } catch {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n // The owner must be currently authorized (discovered + passes the filter).\n if (!isWalletStillAvailable(owner)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n // Reject selecting into a wallet that is mid-disconnect.\n if (owner.name === disconnectingWalletName) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });\n }\n\n const refreshed = refreshUiWallet(owner);\n const selectedAccount = refreshed.accounts.find(a => a.address === account.address);\n if (!selectedAccount) {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n userHasSelected = true;\n // Supersede any in-flight connect/signIn.\n ++connectGeneration;\n\n // No-op fast path: the same account is already active.\n if (selectedAccount === state.account && refreshed === state.connectedWallet) {\n updateState({ status: 'connected' });\n return;\n }\n\n // Switch the active connection (owner may differ from the current one). The\n // previously-active wallet is left authorized — we never disconnect it. Its\n // event subscription persists, so switching back stays live.\n const signer = tryCreateSigner(selectedAccount);\n updateState({ account: selectedAccount, connectedWallet: refreshed, signer, status: 'connected' });\n persistAccount(selectedAccount, refreshed);\n }\n\n // -- Message signing ---------------------------------------------------\n\n async function signMessage(message: ReadonlyUint8Array, options?: WalletActionOptions): Promise<SignatureBytes> {\n options?.abortSignal?.throwIfAborted();\n const { connectedWallet, account } = state;\n if (!connectedWallet || !account) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' });\n }\n // Use the wallet feature directly rather than going through the cached\n // signer. This decouples message signing from transaction signing —\n // a wallet that supports solana:signMessage but not transaction signing\n // still works. solana:signMessage is an account-scoped feature, so this\n // resolves it against the active account: getWalletAccountFeature throws\n // a WalletStandardError when either the wallet or the account does not\n // support it.\n const signMessageFeature = getWalletAccountFeature(\n account,\n SolanaSignMessage,\n ) as SolanaSignMessageFeature[typeof SolanaSignMessage];\n // The wallet-standard `SolanaSignMessageInput.message` is a mutable `Uint8Array`; signing\n // only reads the bytes, so coerce the read-only input at this single boundary.\n const [output] = await signMessageFeature.signMessage({ account, message: message as Uint8Array });\n return output.signature as SignatureBytes;\n }\n\n // -- Sign In With Solana (SIWS-as-connect) ------------------------------\n\n async function signIn(\n uiWallet: UiWallet,\n input: SolanaSignInInput,\n options?: WalletActionOptions,\n ): Promise<SolanaSignInOutput> {\n options?.abortSignal?.throwIfAborted();\n\n // Resolve the feature before mutating any state. getWalletFeature throws\n // WalletStandardError synchronously when the wallet doesn't support\n // solana:signIn — doing this first means an unsupported wallet is a\n // no-op that never tears down an existing connection.\n const signInFeature = getWalletFeature(uiWallet, SolanaSignIn) as SolanaSignInFeature[typeof SolanaSignIn];\n\n userHasSelected = true;\n cancelReconnect();\n const generation = ++connectGeneration;\n updateState({ status: 'connecting' });\n\n try {\n const [result] = await signInFeature.signIn(input);\n\n // A newer connect/signIn was started while we were awaiting. Reject\n // with an `AbortError` (matching the abort-signal contract) so the\n // superseded call settles in the standard, ignorable way while the\n // newer request owns the connection.\n if (generation !== connectGeneration) {\n throw new DOMException('Wallet sign-in was superseded by a newer connect or sign-in', 'AbortError');\n }\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while its sign-in prompt was open. Throw rather than\n // resolving while the store is left connected to no wallet (or a\n // different one); the `catch` below reverts to any prior connection.\n if (!isWalletStillAvailable(uiWallet)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' });\n }\n\n // Set up full connection state using the account from the sign-in response.\n const refreshedWallet = refreshUiWallet(uiWallet);\n const activeAccount = refreshedWallet.accounts.find(a => a.address === result.account.address);\n\n if (!activeAccount) {\n // The wallet signed in with an account it doesn't expose — a\n // protocol violation that can't be mapped to a connection. Throw\n // so the failure surfaces as a rejection rather than resolving\n // with a sign-in result the store can't act on; the `catch`\n // reverts to any prior connection.\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' });\n }\n\n setConnected(activeAccount, refreshedWallet);\n return result;\n } catch (error) {\n if (generation === connectGeneration) {\n revertToPreviousConnectionOrDisconnect();\n }\n throw error;\n }\n }\n\n // -- Persistence -------------------------------------------------------\n\n function ignoreStorageFailure(operation: () => Promise<void> | void): void {\n void (async () => {\n try {\n await operation();\n } catch {\n // Persistence is best-effort; wallet state is the source of truth.\n }\n })();\n }\n\n function persistAccount(account: UiWalletAccount, wallet: UiWallet): void {\n if (!storage) return;\n ignoreStorageFailure(() => storage.setItem(storageKey, `${wallet.name}:${account.address}`));\n }\n\n function clearPersistedAccount(): void {\n if (!storage) return;\n ignoreStorageFailure(() => storage.removeItem(storageKey));\n }\n\n // -- Auto-connect ------------------------------------------------------\n\n if (config.autoConnect !== false && storage) {\n const generation = connectGeneration;\n (async () => {\n const savedKey = await storage.getItem(storageKey);\n // Don't auto-connect if the user has selected a wallet, or if the\n // store was disposed while the storage read was in flight — the\n // disposer has already torn down its listeners and bumped the\n // generation, but this IIFE hasn't captured one yet, so without this\n // check it would resume and silently reconnect into a disposed store.\n if (userHasSelected || disposed) return;\n\n if (!savedKey) {\n updateState({ status: 'disconnected' });\n return;\n }\n\n const separatorIndex = savedKey.lastIndexOf(':');\n if (separatorIndex === -1) {\n // Malformed saved key.\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const walletName = savedKey.slice(0, separatorIndex);\n const savedAddress = savedKey.slice(separatorIndex + 1);\n const existing = state.wallets.find(w => w.name === walletName);\n\n if (existing) {\n await attemptSilentReconnect(savedAddress, existing);\n } else if (\n registry.get().some(w => {\n const ui = getOrCreateUiWalletForStandardWallet(w);\n return ui.name === walletName;\n })\n ) {\n // Wallet registered but doesn't pass the filter.\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n } else {\n // Wallet not registered yet — wait for it to appear.\n updateState({ status: 'reconnecting' });\n\n // Change state to disconnected after 3s\n // Note that the listener stays alive until reconnect is cancelled, so this is\n // just affecting how long the UI shows reconnecting\n const statusTimeout = setTimeout(() => {\n if (!userHasSelected && state.status === 'reconnecting') {\n updateState({ status: 'disconnected' });\n }\n }, 3000);\n\n const unsubRegisterForReconnect = registry.on('register', () => {\n const generation = connectGeneration;\n void (async () => {\n if (userHasSelected) {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n return;\n }\n const found = buildWalletList().find(w => w.name === walletName);\n if (found) {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n await attemptSilentReconnect(savedAddress, found);\n } else if (\n registry.get().some(w => {\n const ui = getOrCreateUiWalletForStandardWallet(w);\n return ui.name === walletName;\n })\n ) {\n // Wallet registered but filtered out.\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n }\n })().catch(() => {\n // Reconnect failed — fall back to disconnected.\n if (generation === connectGeneration && !userHasSelected) {\n updateState({ status: 'disconnected' });\n }\n });\n });\n\n reconnectCleanup = () => {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n };\n }\n })().catch(() => {\n // Storage read failed — fall back to disconnected.\n if (generation === connectGeneration && !userHasSelected) {\n updateState({ status: 'disconnected' });\n }\n });\n } else {\n // No auto-connect: immediately transition from 'pending' to 'disconnected'.\n updateState({ status: 'disconnected' });\n }\n\n async function attemptSilentReconnect(savedAddress: string, uiWallet: UiWallet): Promise<void> {\n const generation = ++connectGeneration;\n updateState({ status: 'reconnecting' });\n\n try {\n const connectFeature = getWalletFeature(\n uiWallet,\n StandardConnect,\n ) as StandardConnectFeature[typeof StandardConnect];\n await connectFeature.connect({ silent: true });\n\n // A newer connect/signIn was started while we were awaiting — bail.\n if (generation !== connectGeneration) return;\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while the silent reconnect was in flight.\n if (!isWalletStillAvailable(uiWallet)) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const refreshedWallet = refreshUiWallet(uiWallet);\n const allAccounts = refreshedWallet.accounts;\n\n if (allAccounts.length === 0) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n // Restore the specific saved account, fall back to first from same wallet.\n const activeAccount = allAccounts.find(a => a.address === savedAddress) ?? allAccounts[0];\n\n // Persist only when we fell back to a different account\n setConnected(activeAccount, refreshedWallet, {\n persist: activeAccount.address !== savedAddress,\n });\n } catch {\n if (generation === connectGeneration) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n }\n }\n }\n\n // -- Public API --------------------------------------------------------\n\n return {\n connect,\n disconnect,\n getState: () => snapshot,\n selectAccount,\n signIn,\n signMessage,\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n subscribeSigner: (listener: () => void) => {\n signerListeners.add(listener);\n return () => {\n signerListeners.delete(listener);\n };\n },\n whenReady,\n [Symbol.dispose]: () => {\n // Invalidate any in-flight connect/signIn/silent-reconnect so they\n // bail instead of resuming after disposal (which would re-subscribe\n // to wallet events and re-arm cleanup that this disposer already\n // ran). The generation bump covers attempts that already captured a\n // generation; `disposed` covers the auto-connect IIFE, which resumes\n // from its storage read and captures a fresh generation afterward.\n disposed = true;\n connectGeneration++;\n unsubRegister();\n unsubUnregister();\n for (const cleanup of walletEventSubscriptions.values()) {\n cleanup();\n }\n walletEventSubscriptions.clear();\n cancelReconnect();\n listeners.clear();\n signerListeners.clear();\n // A store disposed mid-warm-up will never finish warming up, so\n // settle the status to 'disconnected'.\n // Runs after the listener sets are cleared, so no one is notified of\n // the final transition.\n if (isWalletWarmingUp(state.status)) {\n updateState({ status: 'disconnected' });\n }\n },\n };\n}\n","import {\n type ClientWithIdentity,\n type ClientWithPayer,\n type ClientWithSubscribeToIdentity,\n type ClientWithSubscribeToPayer,\n extendClient,\n SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED,\n SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE,\n SolanaError,\n withCleanup,\n} from '@solana/kit';\n\nimport { createWalletStore } from './store';\nimport type { ClientWithWallet, WalletPluginConfig } from './types';\n\n// -- Internal helpers ---------------------------------------------------------\n\nfunction defineSignerGetter(\n additions: Record<string, unknown>,\n property: string,\n store: ReturnType<typeof createWalletStore>,\n): void {\n Object.defineProperty(additions, property, {\n configurable: true,\n // Non-enumerable on purpose: this getter throws when no signer is connected, so we\n // don't want things like Object.entries to read it.\n enumerable: false,\n get() {\n const state = store.getState();\n if (!state.connected) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED, { status: state.status });\n }\n if (!state.connected.signer) {\n throw new SolanaError(SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE);\n }\n return state.connected.signer;\n },\n });\n}\n\ntype SignerProperties = 'payer' | 'identity';\n\n// Maps each signer capability to the Kit-convention reactive hook installed\n// alongside it (`@solana/plugin-interfaces`). A plugin whose `client.payer` /\n// `client.identity` is dynamic installs the matching `subscribeTo*` function\n// so reactive consumers can observe changes without naming this plugin.\nconst SUBSCRIBE_PROPERTY: Record<SignerProperties, 'subscribeToIdentity' | 'subscribeToPayer'> = {\n identity: 'subscribeToIdentity',\n payer: 'subscribeToPayer',\n};\n\nfunction createPlugin<TAdditions extends ClientWithWallet>(\n config: WalletPluginConfig,\n signerProperties: SignerProperties[],\n) {\n return <T extends object & { wallet?: never }>(client: T): Disposable & Omit<T, keyof TAdditions> & TAdditions => {\n if ('wallet' in client) {\n throw new Error(\n 'Only one wallet plugin can be used per client. ' +\n 'Use walletSigner, walletPayer, walletIdentity, or walletWithoutSigner — not multiple.',\n );\n }\n\n const store = createWalletStore(config);\n\n const additions: Record<string, unknown> = { wallet: store };\n for (const prop of signerProperties) {\n defineSignerGetter(additions, prop, store);\n additions[SUBSCRIBE_PROPERTY[prop]] = store.subscribeSigner;\n }\n\n return withCleanup(extendClient(client, additions), () => store[Symbol.dispose]()) as unknown as Disposable &\n Omit<T, keyof TAdditions> &\n TAdditions;\n };\n}\n\n// -- Public API ---------------------------------------------------------------\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to both `client.payer` and `client.identity`.\n *\n * This is the most common entrypoint for dApps. When a signing-capable\n * wallet is connected, `client.payer` and `client.identity` both return the\n * wallet signer. When disconnected or read-only, accessing either throws.\n *\n * Because the signer is dynamic, both `client.subscribeToPayer` and\n * `client.subscribeToIdentity` are installed so reactive consumers can observe\n * changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(walletSigner({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletPayer}\n * @see {@link walletIdentity}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletSigner(config: WalletPluginConfig) {\n return createPlugin<\n ClientWithIdentity &\n ClientWithPayer &\n ClientWithSubscribeToIdentity &\n ClientWithSubscribeToPayer &\n ClientWithWallet\n >(config, ['payer', 'identity']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to `client.identity`.\n *\n * Use this when `client.payer` is controlled by a separate `payer()` plugin\n * (e.g. a backend relayer pays fees, but the user's wallet is the identity).\n *\n * Because the signer is dynamic, `client.subscribeToIdentity` is installed so\n * reactive consumers can observe changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(payer(relayerKeypair))\n * .use(walletIdentity({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletPayer}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletIdentity(config: WalletPluginConfig) {\n return createPlugin<ClientWithIdentity & ClientWithSubscribeToIdentity & ClientWithWallet>(config, ['identity']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to `client.payer`.\n *\n * Use this when you need the wallet as the fee payer but don't need\n * `client.identity`. For most dApps, prefer {@link walletSigner} which\n * sets both.\n *\n * Because the signer is dynamic, `client.subscribeToPayer` is installed so\n * reactive consumers can observe changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(walletPayer({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletIdentity}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletPayer(config: WalletPluginConfig) {\n return createPlugin<ClientWithPayer & ClientWithSubscribeToPayer & ClientWithWallet>(config, ['payer']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard.\n *\n * Adds the `wallet` namespace to the client without setting `client.payer`\n * or `client.identity`. Use this alongside separate `payer()` and/or\n * `identity()` plugins, or when the wallet's signer is used explicitly in\n * instructions. For most dApps, prefer {@link walletSigner} instead.\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(payer(backendKeypair))\n * .use(walletWithoutSigner({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n *\n * // client.payer is always backendKeypair\n * // client.wallet.getState().connected?.signer for manual use\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletPayer}\n * @see {@link walletIdentity}\n * @see {@link WalletPluginConfig}\n */\nexport function walletWithoutSigner(config: WalletPluginConfig) {\n return createPlugin<ClientWithWallet>(config, []);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/status.ts","../src/store.ts","../src/wallet.ts"],"names":["SolanaError"],"mappings":"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","file":"index.node.mjs","sourcesContent":["import type { WalletStatus } from './types';\n\n/**\n * Reports whether a {@link WalletStatus} belongs to the initial auto-reconnect\n * \"warm-up\": `'pending'` (before auto-reconnect has resolved) and\n * `'reconnecting'` (silently re-establishing a persisted connection). During\n * this window a persisted wallet is on its way back, so wallet-dependent UI\n * should usually hold rather than flash a \"disconnected\" state.\n *\n * User-initiated `'connecting'` / `'disconnecting'` are deliberately excluded —\n * a normal connect or disconnect is not a warm-up and must stay interactive.\n *\n * This is the single source of truth for \"warm-up\" across the plugin: the\n * store's {@link WalletNamespace.whenReady} waits for the status to leave this set, and the\n * React `useIsWalletReady` hook and `WalletReadyGate` component gate on its\n * negation. Use it directly to gate on warm-up from a non-React consumer.\n *\n * @param status - A status from `client.wallet.getState().status`.\n * @returns `true` while the client is warming up, otherwise `false`.\n *\n * @example\n * ```ts\n * client.wallet.subscribe(() => {\n * const { status } = client.wallet.getState();\n * setLoading(isWalletWarmingUp(status));\n * });\n * ```\n *\n * @see {@link WalletNamespace.whenReady} — the imperative (promise-based) counterpart.\n */\nexport function isWalletWarmingUp(status: WalletStatus): boolean {\n return status === 'pending' || status === 'reconnecting';\n}\n","import {\n type ReadonlyUint8Array,\n type SignatureBytes,\n SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE,\n SOLANA_ERROR__WALLET__NOT_CONNECTED,\n SolanaError,\n} from '@solana/kit';\nimport { createSignerFromWalletAccount } from '@solana/wallet-account-signer';\nimport {\n SolanaSignIn,\n type SolanaSignInFeature,\n type SolanaSignInInput,\n type SolanaSignInOutput,\n SolanaSignMessage,\n type SolanaSignMessageFeature,\n} from '@solana/wallet-standard-features';\nimport { getWallets } from '@wallet-standard/app';\nimport {\n StandardConnect,\n type StandardConnectFeature,\n StandardDisconnect,\n type StandardDisconnectFeature,\n StandardEvents,\n type StandardEventsFeature,\n} from '@wallet-standard/features';\nimport type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';\nimport { getWalletAccountFeature, getWalletFeature } from '@wallet-standard/ui-features';\nimport { getOrCreateUiWalletForStandardWallet, getWalletForHandle } from '@wallet-standard/ui-registry';\n\nimport { isWalletWarmingUp } from './status';\nimport type {\n WalletActionOptions,\n WalletNamespace,\n WalletPluginConfig,\n WalletSigner,\n WalletState,\n WalletStatus,\n WalletStorage,\n} from './types';\n\n// -- Internal types ---------------------------------------------------------\n\n/** @internal */\nexport type WalletStore = WalletNamespace & {\n /**\n * Registers a listener notified only when the wallet signer may\n * have changed.\n * Returns an idempotent unsubscribe function.\n *\n * @internal\n */\n subscribeSigner: (listener: () => void) => () => void;\n [Symbol.dispose]: () => void;\n};\n\n// Flat internal state for easier updates\ntype WalletStoreState = {\n account: UiWalletAccount | null;\n connectedWallet: UiWallet | null;\n signer: WalletSigner | null;\n status: WalletStatus;\n wallets: readonly UiWallet[];\n};\n\n// -- Store ------------------------------------------------------------------\n\n/** @internal */\nexport function createWalletStore(config: WalletPluginConfig): WalletStore {\n // -- SSR guard: on the server, return an inert stub ---------------------\n\n if (!__BROWSER__ || __REACTNATIVE__) {\n const ssrSnapshot: WalletState = Object.freeze({\n connected: null,\n status: 'pending' as const,\n wallets: Object.freeze([]) as readonly UiWallet[],\n });\n return {\n connect: () =>\n Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' })),\n disconnect: () => Promise.resolve(),\n getState: () => ssrSnapshot,\n selectAccount: () => {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });\n },\n signIn: () => Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' })),\n signMessage: () =>\n Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' })),\n subscribe: () => () => {},\n subscribeSigner: () => () => {},\n whenReady: () => Promise.resolve(),\n [Symbol.dispose]: () => {},\n };\n }\n\n // -- Browser-only initialization below this point ----------------------\n\n let state: WalletStoreState = {\n account: null,\n connectedWallet: null,\n signer: null,\n status: 'pending',\n wallets: [],\n };\n\n const listeners = new Set<() => void>();\n const signerListeners = new Set<() => void>();\n // One `standard:events change` subscription per discovered wallet, keyed by\n // name (the stable identity; the UiWallet handle regenerates on changes). The\n // underlying subscription attaches to the raw wallet's events, so it survives\n // handle regeneration — no need to resubscribe when a wallet's accounts change.\n const walletEventSubscriptions = new Map<string, () => void>();\n let reconnectCleanup: (() => void) | null = null;\n let userHasSelected = false;\n let connectGeneration = 0;\n let disposed = false;\n // Wallet that is in the process of disconnecting, stored to guard against re-connecting to it if a future connect fails\n let disconnectingWalletName: string | null = null;\n // Deferred backing `whenReady`. Created lazily while the status is transient\n // (see `isWalletWarmingUp`) and resolved by `updateState` once the status\n // settles, so `whenReady` can hand out a stable promise for the duration of\n // one warm-up episode (required by React Suspense).\n let readyDeferred: PromiseWithResolvers<void> | null = null;\n\n // Resolve storage: default to localStorage in browser, null to disable.\n // Merely *accessing* `localStorage` throws a `SecurityError` in sandboxed\n // iframes or when third-party storage is blocked, so the default resolution\n // degrades to `null` rather than letting the whole plugin-install chain\n // throw. This matches the best-effort persistence applied to reads/writes.\n function resolveDefaultStorage(): WalletStorage | null {\n try {\n return localStorage;\n } catch {\n return null;\n }\n }\n\n const storage: WalletStorage | null = config.storage === null ? null : (config.storage ?? resolveDefaultStorage());\n const storageKey = config.storageKey ?? 'kit-wallet';\n\n // -- State management --------------------------------------------------\n\n function deriveSnapshot(s: WalletStoreState): WalletState {\n return Object.freeze({\n connected:\n s.connectedWallet && s.account\n ? Object.freeze({\n account: s.account,\n signer: s.signer,\n wallet: s.connectedWallet,\n })\n : null,\n status: s.status,\n wallets: s.wallets,\n });\n }\n\n let snapshot: WalletState = deriveSnapshot(state);\n\n // We set `client.{payer,identity}` to `getState().connected?.signer\n // This is `undefined` if disconnected, `null` if connected to a read-only wallet,\n // or the signer reference otherwise.\n function observableSigner(s: WalletStoreState): WalletSigner | null | undefined {\n return s.connectedWallet && s.account ? s.signer : undefined;\n }\n\n function updateState(updates: Partial<WalletStoreState>): void {\n const prev = state;\n state = { ...state, ...updates };\n\n // Resolve `whenReady` the moment the status leaves the transient\n // warm-up set. `readyDeferred` is only ever created while transient (in\n // `whenReady`), so clearing it here lets the next transient episode arm\n // a fresh one.\n if (isWalletWarmingUp(prev.status) && !isWalletWarmingUp(state.status)) {\n readyDeferred?.resolve();\n readyDeferred = null;\n }\n\n if (state.wallets !== prev.wallets) {\n syncWalletEventSubscriptions(state.wallets);\n }\n\n // Signer subscribers (`subscribeToPayer` / `subscribeToIdentity`) fire\n // only when the observed signer changes.\n const signerChanged = observableSigner(state) !== observableSigner(prev);\n\n if (\n state.connectedWallet !== prev.connectedWallet ||\n state.account !== prev.account ||\n state.status !== prev.status ||\n state.signer !== prev.signer ||\n state.wallets !== prev.wallets\n ) {\n snapshot = deriveSnapshot(state);\n listeners.forEach(l => l());\n }\n\n if (signerChanged) {\n signerListeners.forEach(l => l());\n }\n }\n\n // Advanced: see the `whenReady` docblock on `WalletNamespace`. Returns a\n // stable, already-resolved promise once the initial warm-up has settled;\n // while transient, returns the same pending promise on every call so it is\n // safe to throw for React Suspense.\n function whenReady(): Promise<void> {\n if (!isWalletWarmingUp(state.status)) {\n return Promise.resolve();\n }\n readyDeferred ??= Promise.withResolvers<void>();\n return readyDeferred.promise;\n }\n\n // -- Signer creation ---------------------------------------------------\n\n function tryCreateSigner(account: UiWalletAccount): WalletSigner | null {\n try {\n // `createSignerFromWalletAccount` throws for chains the wallet\n // doesn't support — which we catch below. Non-Solana chains\n // therefore degrade to `signer: null`, matching the read-only\n // wallet contract.\n return createSignerFromWalletAccount(account, config.chain);\n } catch {\n // Wallet doesn't support signing (read-only / watch wallet, or a\n // non-Solana chain).\n return null;\n }\n }\n\n // -- Wallet discovery --------------------------------------------------\n\n // `getWallets()` is a wallet-standard function that returns the `Wallets` API\n // This includes a `get()` function to get the current list of wallets, and\n // an `on()` function to subscribe to wallet registration/unregistration events.\n const registry = getWallets();\n\n function filterWallet(uiWallet: UiWallet): boolean {\n const supportsChain = uiWallet.chains.includes(config.chain);\n const supportsConnect = uiWallet.features.includes(StandardConnect);\n if (!supportsChain || !supportsConnect) return false;\n return config.filter ? config.filter(uiWallet) : true;\n }\n\n function buildWalletList(): readonly UiWallet[] {\n return Object.freeze(registry.get().map(getOrCreateUiWalletForStandardWallet).filter(filterWallet));\n }\n\n // Rebuilds the filtered wallet list but returns the *existing* `state.wallets`\n // reference when the contents are unchanged. `buildWalletList` always\n // allocates a fresh frozen array, so without this a registry event for a\n // wallet that gets filtered out (or any event that doesn't alter the visible\n // set) would still produce a new snapshot and notify every listener — a\n // spurious re-render. Wallets are compared by reference, in order: the\n // wallet-ui registry hands back a stable UiWallet for a given underlying\n // wallet until its accounts/features/chains change, so reference equality is\n // a sound same-contents check here.\n function reconcileWalletList(): readonly UiWallet[] {\n const next = buildWalletList();\n const prev = state.wallets;\n if (next.length === prev.length && next.every((w, i) => w === prev[i])) {\n return prev;\n }\n return next;\n }\n\n // Reconcile the per-wallet event subscriptions against the current wallet set:\n // subscribe wallets that just appeared, unsubscribe those that left. Called\n // whenever `state.wallets` changes reference. Subscribing fires nothing, so\n // this never causes re-render churn. `wallets` is the already-filtered visible\n // list, so wallets excluded by chain/feature/`config.filter` are intentionally\n // never subscribed — a `register` / `unregister` event reconciles them in or\n // out of the list (and thus in or out of this map) when their eligibility\n // changes.\n function syncWalletEventSubscriptions(wallets: readonly UiWallet[]): void {\n const names = new Set(wallets.map(w => w.name));\n for (const [name, cleanup] of walletEventSubscriptions) {\n if (!names.has(name)) {\n cleanup();\n walletEventSubscriptions.delete(name);\n }\n }\n for (const w of wallets) {\n if (!walletEventSubscriptions.has(w.name)) {\n walletEventSubscriptions.set(w.name, subscribeToWalletEvents(w));\n }\n }\n }\n\n // True if a wallet matching `uiWallet` (by name) is currently registered and\n // passes the filter. Used to re-check membership after an awaited connect /\n // sign-in / silent reconnect resolves: the wallet may have unregistered (or\n // dropped a required feature/chain) while its prompt was open, and the\n // unregister handler can't tear down a connection that hasn't been\n // established yet. Compared by name — the same identity the unregister\n // handler uses — because the wallet-ui registry hands back a fresh UiWallet\n // reference once connect adds accounts, so reference equality would not hold.\n function isWalletStillAvailable(uiWallet: UiWallet): boolean {\n return buildWalletList().some(w => w.name === uiWallet.name);\n }\n\n updateState({ wallets: buildWalletList() });\n\n // Listen for new wallets being registered\n const unsubRegister = registry.on('register', () => {\n updateState({ wallets: reconcileWalletList() });\n });\n\n // Listen for wallets being unregistered — if the connected wallet is removed, disconnect\n const unsubUnregister = registry.on('unregister', () => {\n const newWallets = reconcileWalletList();\n const updates: Partial<WalletStoreState> = { wallets: newWallets };\n\n // If the connected wallet was unregistered, disconnect locally.\n if (state.connectedWallet && !newWallets.some(w => w.name === state.connectedWallet!.name)) {\n updates.connectedWallet = null;\n updates.account = null;\n updates.signer = null;\n updates.status = 'disconnected';\n clearPersistedAccount();\n }\n\n updateState(updates);\n });\n\n // -- Wallet-initiated events -------------------------------------------\n\n function cancelReconnect(): void {\n reconnectCleanup?.();\n reconnectCleanup = null;\n }\n\n function setConnected(account: UiWalletAccount, wallet: UiWallet, options?: { persist?: boolean }): void {\n // The store may have been disposed while a connect / sign-in / silent\n // reconnect awaited. The `connectGeneration` guard at each call site\n // doesn't cover the auto-connect IIFE resuming from its storage read —\n // it captures a fresh generation in `attemptSilentReconnect` after\n // disposal. Bail here so a disposed store never re-subscribes to wallet\n // events (a listener the already-run disposer can't clean up) or reports\n // itself connected.\n if (disposed) return;\n const signer = tryCreateSigner(account);\n disconnectingWalletName = null;\n // Reconcile `wallets` alongside the connection: `connect`/`signIn`/silent\n // reconnect authorize accounts on the underlying wallet, which regenerates\n // its registry handle. Without rebuilding the list here, `getState().wallets`\n // would keep the pre-connect handle (empty `.accounts`) until an unrelated\n // `standard:events change` happened to fire — so consumers reading the\n // just-connected wallet from `wallets` (e.g. rendering its authorized\n // accounts) would see it as unconnected. `reconcileWalletList` returns the\n // existing reference when the visible set is unchanged, keeping the snapshot\n // churn-free when connect didn't actually add accounts.\n updateState({\n account,\n connectedWallet: wallet,\n signer,\n status: 'connected',\n wallets: reconcileWalletList(),\n });\n if (options?.persist !== false) {\n persistAccount(account, wallet);\n }\n }\n\n function refreshUiWallet(staleUiWallet: UiWallet): UiWallet {\n const rawWallet = getWalletForHandle(staleUiWallet);\n return getOrCreateUiWalletForStandardWallet(rawWallet);\n }\n\n function subscribeToWalletEvents(uiWallet: UiWallet): () => void {\n if (!uiWallet.features.includes(StandardEvents)) {\n return () => {};\n }\n const eventsFeature = getWalletFeature(\n uiWallet,\n StandardEvents,\n ) as StandardEventsFeature[typeof StandardEvents];\n const walletName = uiWallet.name;\n\n // The handler intentionally ignores the changed-property flags\n // (`accounts`, `chains`, `features`) the wallet passes and instead\n // reconciles against the refreshed wallet. The signer is derived\n // entirely from the active account's features and chains, and the\n // wallet-ui registry regenerates the account reference precisely when\n // one of those changes — so the account reference, not the flags, is\n // the single source of truth for whether the signer is affected. Branch\n // on the reconciled state below, never on `properties`.\n return eventsFeature.on('change', () => {\n // Refresh the discovered list so this wallet's (and any others') accounts\n // are current; `updateState` re-syncs subscriptions for added/removed wallets.\n const newWallets = reconcileWalletList();\n\n // Non-active wallet: reflect its new account/feature state in the list only.\n if (!state.connectedWallet || state.connectedWallet.name !== walletName) {\n updateState({ wallets: newWallets });\n return;\n }\n\n // Active wallet: the original reconcile logic, unchanged.\n const refreshed = refreshUiWallet(state.connectedWallet);\n if (!filterWallet(refreshed)) {\n disconnectLocally();\n return;\n }\n const result = reconcileActiveAccount(refreshed);\n if (result === null) {\n disconnectLocally();\n return;\n }\n updateState({ connectedWallet: refreshed, wallets: newWallets, ...result });\n if (result.account) {\n persistAccount(result.account, refreshed);\n }\n });\n }\n\n function reconcileActiveAccount(wallet: UiWallet): Partial<WalletStoreState> | null {\n const newAccounts = wallet.accounts;\n\n // No accounts left — the caller disconnects.\n if (newAccounts.length === 0) return null;\n\n const currentAddress = state.account?.address;\n const stillPresent = currentAddress ? newAccounts.find(a => a.address === currentAddress) : null;\n const activeAccount = stillPresent ?? newAccounts[0];\n\n // Same reference — nothing the signer depends on changed, so leave the\n // signer untouched to preserve referential stability.\n if (activeAccount === state.account) return {};\n\n // The active account changed (switched, removed, or regenerated because\n // its features/chains changed) — recreate the signer for it.\n return { account: activeAccount, signer: tryCreateSigner(activeAccount) };\n }\n\n // -- Connection lifecycle ----------------------------------------------\n\n async function connect(uiWallet: UiWallet, options?: WalletActionOptions): Promise<readonly UiWalletAccount[]> {\n options?.abortSignal?.throwIfAborted();\n\n // Resolve the feature before mutating any state. getWalletFeature throws\n // WalletStandardError synchronously when the wallet doesn't support\n // standard:connect — doing this first means an unsupported wallet is a\n // no-op that never tears down an existing connection or cancels a\n // pending reconnect.\n const connectFeature = getWalletFeature(\n uiWallet,\n StandardConnect,\n ) as StandardConnectFeature[typeof StandardConnect];\n\n userHasSelected = true;\n cancelReconnect();\n const generation = ++connectGeneration;\n updateState({ status: 'connecting' });\n\n try {\n // Snapshot existing accounts before connect.\n const existingAccounts = [...uiWallet.accounts];\n\n await connectFeature.connect();\n\n // A newer connect/signIn was started while we were awaiting. Reject\n // with an `AbortError` so this superseded call settles in the\n // standard, ignorable way (matching the abort-signal contract) while\n // the newer request owns the connection. The `catch` below leaves\n // that newer connection untouched, since `generation` no longer\n // matches `connectGeneration`.\n if (generation !== connectGeneration) {\n throw new DOMException('Wallet connect was superseded by a newer connect or sign-in', 'AbortError');\n }\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while its connect prompt was open. The unregister\n // handler only disconnects an already-connected wallet, so without\n // this re-check the store would be left connected to a wallet absent\n // from `wallets`. Throw so the connection failure surfaces as a\n // rejection; the `catch` below reverts to any prior connection.\n if (!isWalletStillAvailable(uiWallet)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' });\n }\n\n // Refresh UiWallet to get updated accounts after connect.\n const refreshedWallet = refreshUiWallet(uiWallet);\n const allAccounts = refreshedWallet.accounts;\n\n // No authorized accounts means there's nothing to connect to. Throw\n // rather than resolving so a caller can't mistake an empty result for\n // a successful connection; the `catch` reverts to any prior wallet.\n if (allAccounts.length === 0) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' });\n }\n\n // Prefer the first newly authorized account.\n const newAccount = allAccounts.find(a => !existingAccounts.some(e => e.address === a.address));\n const activeAccount = newAccount ?? allAccounts[0];\n\n setConnected(activeAccount, refreshedWallet);\n return allAccounts;\n } catch (error) {\n // Only act if we're still the active connection attempt; a\n // superseded attempt must leave the newer connection untouched.\n if (generation === connectGeneration) {\n revertToPreviousConnectionOrDisconnect();\n }\n throw error;\n }\n }\n\n async function disconnect(wallet?: UiWallet, options?: WalletActionOptions): Promise<void> {\n options?.abortSignal?.throwIfAborted();\n\n const target = wallet ?? state.connectedWallet;\n\n // Nothing to disconnect (no arg, nothing active): cancel any pending reconnect, and\n // supersede in-flight attempts.\n if (!target) {\n userHasSelected = true;\n cancelReconnect();\n connectGeneration++;\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const isActive = state.connectedWallet != null && state.connectedWallet.name === target.name;\n\n if (isActive) {\n const currentWallet = state.connectedWallet!;\n // Bump generation so this disconnect supersedes any in-flight connect/signIn.\n const generation = ++connectGeneration;\n disconnectingWalletName = currentWallet.name;\n updateState({ status: 'disconnecting' });\n try {\n if (currentWallet.features.includes(StandardDisconnect)) {\n const disconnectFeature = getWalletFeature(\n currentWallet,\n StandardDisconnect,\n ) as StandardDisconnectFeature[typeof StandardDisconnect];\n await disconnectFeature.disconnect();\n }\n } finally {\n if (generation === connectGeneration) {\n disconnectLocally();\n }\n }\n return;\n }\n\n // targeted disconnect but wallet gone: leave the active connection, status,\n // and persisted key untouched.\n if (!isWalletStillAvailable(target)) {\n return; // already gone / not authorized — forgiving no-op\n }\n const refreshedTarget = refreshUiWallet(target);\n if (!refreshedTarget.features.includes(StandardDisconnect)) {\n return; // can't deauthorize without standard:disconnect — no-op\n }\n // Guard against a concurrent select/connect into this wallet for the await;\n // restore the prior marker afterward (it may belong to an active disconnect).\n const prevDisconnecting = disconnectingWalletName;\n disconnectingWalletName = refreshedTarget.name;\n try {\n const disconnectFeature = getWalletFeature(\n refreshedTarget,\n StandardDisconnect,\n ) as StandardDisconnectFeature[typeof StandardDisconnect];\n await disconnectFeature.disconnect();\n } finally {\n if (disconnectingWalletName === refreshedTarget.name) {\n disconnectingWalletName = prevDisconnecting;\n }\n // Reflect the now-empty accounts immediately — but only if the store\n // is still live. If it was disposed while `standard:disconnect`\n // awaited, the disposer has already torn down every wallet-event\n // subscription and cleared the map; deauthorizing the target\n // regenerated its handle, so `reconcileWalletList` returns a fresh\n // array, and letting `updateState` run here would re-invoke\n // `syncWalletEventSubscriptions` and re-subscribe every wallet into\n // the cleared map — listeners the disposer can no longer clean up.\n if (!disposed) {\n updateState({ wallets: reconcileWalletList() });\n }\n }\n }\n\n function disconnectLocally(): void {\n disconnectingWalletName = null;\n\n updateState({\n account: null,\n connectedWallet: null,\n signer: null,\n status: 'disconnected',\n // Reconcile the list too: when this runs from the change handler\n // because the connected wallet dropped the configured chain or\n // failed the filter, that wallet must leave `wallets` immediately\n // rather than linger until an unrelated register/unregister event.\n // `reconcileWalletList` returns the existing reference when the\n // visible set is unchanged (the common case for a plain disconnect),\n // so this stays churn-free.\n wallets: reconcileWalletList(),\n });\n\n clearPersistedAccount();\n }\n\n // A `connect`/`signIn` attempt failed to establish a new connection. While\n // such an attempt is in flight only `status` is moved to 'connecting' — the\n // existing connection's account, signer, event subscription, and persisted\n // key are untouched until `setConnected` runs on success. So if a prior\n // connection is still intact, revert to it (declining the new wallet must not\n // log the user out of the one they had). Otherwise there's nothing to keep,\n // so disconnect cleanly. The active-attempt (`generation`) guard at each call\n // site ensures a superseded attempt never reverts the connection a newer one\n // owns.\n function revertToPreviousConnectionOrDisconnect(): void {\n // Don't revive a wallet with disconnect in progress\n if (state.connectedWallet && state.account && state.connectedWallet.name !== disconnectingWalletName) {\n updateState({ status: 'connected' });\n } else {\n disconnectLocally();\n }\n }\n\n function selectAccount(account: UiWalletAccount): void {\n // Derive the owning wallet from the account handle. `getWalletForHandle`\n // keys a WeakMap on handle identity, so this is unambiguous even when the\n // same address is authorized in two wallets. A stale/unregistered handle\n // throws — surface it as ACCOUNT_NOT_AVAILABLE.\n let owner: UiWallet;\n try {\n owner = getOrCreateUiWalletForStandardWallet(getWalletForHandle(account));\n } catch {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n // The owner must be currently authorized (discovered + passes the filter).\n if (!isWalletStillAvailable(owner)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n // Reject selecting into a wallet that is mid-disconnect.\n if (owner.name === disconnectingWalletName) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });\n }\n\n const refreshed = refreshUiWallet(owner);\n const selectedAccount = refreshed.accounts.find(a => a.address === account.address);\n if (!selectedAccount) {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n userHasSelected = true;\n // Supersede any in-flight connect/signIn.\n ++connectGeneration;\n\n // No-op fast path: the same account is already active.\n if (selectedAccount === state.account && refreshed === state.connectedWallet) {\n updateState({ status: 'connected' });\n return;\n }\n\n // Switch the active connection (owner may differ from the current one). The\n // previously-active wallet is left authorized — we never disconnect it. Its\n // event subscription persists, so switching back stays live.\n const signer = tryCreateSigner(selectedAccount);\n updateState({ account: selectedAccount, connectedWallet: refreshed, signer, status: 'connected' });\n persistAccount(selectedAccount, refreshed);\n }\n\n // -- Message signing ---------------------------------------------------\n\n async function signMessage(message: ReadonlyUint8Array, options?: WalletActionOptions): Promise<SignatureBytes> {\n options?.abortSignal?.throwIfAborted();\n const { connectedWallet, account } = state;\n if (!connectedWallet || !account) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' });\n }\n // Use the wallet feature directly rather than going through the cached\n // signer. This decouples message signing from transaction signing —\n // a wallet that supports solana:signMessage but not transaction signing\n // still works. solana:signMessage is an account-scoped feature, so this\n // resolves it against the active account: getWalletAccountFeature throws\n // a WalletStandardError when either the wallet or the account does not\n // support it.\n const signMessageFeature = getWalletAccountFeature(\n account,\n SolanaSignMessage,\n ) as SolanaSignMessageFeature[typeof SolanaSignMessage];\n // The wallet-standard `SolanaSignMessageInput.message` is a mutable `Uint8Array`; signing\n // only reads the bytes, so coerce the read-only input at this single boundary.\n const [output] = await signMessageFeature.signMessage({ account, message: message as Uint8Array });\n return output.signature as SignatureBytes;\n }\n\n // -- Sign In With Solana (SIWS-as-connect) ------------------------------\n\n async function signIn(\n uiWallet: UiWallet,\n input: SolanaSignInInput,\n options?: WalletActionOptions,\n ): Promise<SolanaSignInOutput> {\n options?.abortSignal?.throwIfAborted();\n\n // Resolve the feature before mutating any state. getWalletFeature throws\n // WalletStandardError synchronously when the wallet doesn't support\n // solana:signIn — doing this first means an unsupported wallet is a\n // no-op that never tears down an existing connection.\n const signInFeature = getWalletFeature(uiWallet, SolanaSignIn) as SolanaSignInFeature[typeof SolanaSignIn];\n\n userHasSelected = true;\n cancelReconnect();\n const generation = ++connectGeneration;\n updateState({ status: 'connecting' });\n\n try {\n const [result] = await signInFeature.signIn(input);\n\n // A newer connect/signIn was started while we were awaiting. Reject\n // with an `AbortError` (matching the abort-signal contract) so the\n // superseded call settles in the standard, ignorable way while the\n // newer request owns the connection.\n if (generation !== connectGeneration) {\n throw new DOMException('Wallet sign-in was superseded by a newer connect or sign-in', 'AbortError');\n }\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while its sign-in prompt was open. Throw rather than\n // resolving while the store is left connected to no wallet (or a\n // different one); the `catch` below reverts to any prior connection.\n if (!isWalletStillAvailable(uiWallet)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' });\n }\n\n // Set up full connection state using the account from the sign-in response.\n const refreshedWallet = refreshUiWallet(uiWallet);\n const activeAccount = refreshedWallet.accounts.find(a => a.address === result.account.address);\n\n if (!activeAccount) {\n // The wallet signed in with an account it doesn't expose — a\n // protocol violation that can't be mapped to a connection. Throw\n // so the failure surfaces as a rejection rather than resolving\n // with a sign-in result the store can't act on; the `catch`\n // reverts to any prior connection.\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' });\n }\n\n setConnected(activeAccount, refreshedWallet);\n return result;\n } catch (error) {\n if (generation === connectGeneration) {\n revertToPreviousConnectionOrDisconnect();\n }\n throw error;\n }\n }\n\n // -- Persistence -------------------------------------------------------\n\n function ignoreStorageFailure(operation: () => Promise<void> | void): void {\n void (async () => {\n try {\n await operation();\n } catch {\n // Persistence is best-effort; wallet state is the source of truth.\n }\n })();\n }\n\n function persistAccount(account: UiWalletAccount, wallet: UiWallet): void {\n if (!storage) return;\n ignoreStorageFailure(() => storage.setItem(storageKey, `${wallet.name}:${account.address}`));\n }\n\n function clearPersistedAccount(): void {\n if (!storage) return;\n ignoreStorageFailure(() => storage.removeItem(storageKey));\n }\n\n // -- Auto-connect ------------------------------------------------------\n\n if (config.autoConnect !== false && storage) {\n const generation = connectGeneration;\n (async () => {\n const savedKey = await storage.getItem(storageKey);\n // Don't auto-connect if the user has selected a wallet, or if the\n // store was disposed while the storage read was in flight — the\n // disposer has already torn down its listeners and bumped the\n // generation, but this IIFE hasn't captured one yet, so without this\n // check it would resume and silently reconnect into a disposed store.\n if (userHasSelected || disposed) return;\n\n if (!savedKey) {\n updateState({ status: 'disconnected' });\n return;\n }\n\n const separatorIndex = savedKey.lastIndexOf(':');\n if (separatorIndex === -1) {\n // Malformed saved key.\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const walletName = savedKey.slice(0, separatorIndex);\n const savedAddress = savedKey.slice(separatorIndex + 1);\n const existing = state.wallets.find(w => w.name === walletName);\n\n if (existing) {\n await attemptSilentReconnect(savedAddress, existing);\n } else if (\n registry.get().some(w => {\n const ui = getOrCreateUiWalletForStandardWallet(w);\n return ui.name === walletName;\n })\n ) {\n // Wallet registered but doesn't pass the filter.\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n } else {\n // Wallet not registered yet — wait for it to appear.\n updateState({ status: 'reconnecting' });\n\n // Change state to disconnected after 3s\n // Note that the listener stays alive until reconnect is cancelled, so this is\n // just affecting how long the UI shows reconnecting\n const statusTimeout = setTimeout(() => {\n if (!userHasSelected && state.status === 'reconnecting') {\n updateState({ status: 'disconnected' });\n }\n }, 3000);\n\n const unsubRegisterForReconnect = registry.on('register', () => {\n const generation = connectGeneration;\n void (async () => {\n if (userHasSelected) {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n return;\n }\n const found = buildWalletList().find(w => w.name === walletName);\n if (found) {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n await attemptSilentReconnect(savedAddress, found);\n } else if (\n registry.get().some(w => {\n const ui = getOrCreateUiWalletForStandardWallet(w);\n return ui.name === walletName;\n })\n ) {\n // Wallet registered but filtered out.\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n }\n })().catch(() => {\n // Reconnect failed — fall back to disconnected.\n if (generation === connectGeneration && !userHasSelected) {\n updateState({ status: 'disconnected' });\n }\n });\n });\n\n reconnectCleanup = () => {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n };\n }\n })().catch(() => {\n // Storage read failed — fall back to disconnected.\n if (generation === connectGeneration && !userHasSelected) {\n updateState({ status: 'disconnected' });\n }\n });\n } else {\n // No auto-connect: immediately transition from 'pending' to 'disconnected'.\n updateState({ status: 'disconnected' });\n }\n\n async function attemptSilentReconnect(savedAddress: string, uiWallet: UiWallet): Promise<void> {\n const generation = ++connectGeneration;\n updateState({ status: 'reconnecting' });\n\n try {\n const connectFeature = getWalletFeature(\n uiWallet,\n StandardConnect,\n ) as StandardConnectFeature[typeof StandardConnect];\n await connectFeature.connect({ silent: true });\n\n // A newer connect/signIn was started while we were awaiting — bail.\n if (generation !== connectGeneration) return;\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while the silent reconnect was in flight.\n if (!isWalletStillAvailable(uiWallet)) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const refreshedWallet = refreshUiWallet(uiWallet);\n const allAccounts = refreshedWallet.accounts;\n\n if (allAccounts.length === 0) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n // Restore the specific saved account, fall back to first from same wallet.\n const activeAccount = allAccounts.find(a => a.address === savedAddress) ?? allAccounts[0];\n\n // Persist only when we fell back to a different account\n setConnected(activeAccount, refreshedWallet, {\n persist: activeAccount.address !== savedAddress,\n });\n } catch {\n if (generation === connectGeneration) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n }\n }\n }\n\n // -- Public API --------------------------------------------------------\n\n return {\n connect,\n disconnect,\n getState: () => snapshot,\n selectAccount,\n signIn,\n signMessage,\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n subscribeSigner: (listener: () => void) => {\n signerListeners.add(listener);\n return () => {\n signerListeners.delete(listener);\n };\n },\n whenReady,\n [Symbol.dispose]: () => {\n // Invalidate any in-flight connect/signIn/silent-reconnect so they\n // bail instead of resuming after disposal (which would re-subscribe\n // to wallet events and re-arm cleanup that this disposer already\n // ran). The generation bump covers attempts that already captured a\n // generation; `disposed` covers the auto-connect IIFE, which resumes\n // from its storage read and captures a fresh generation afterward.\n disposed = true;\n connectGeneration++;\n unsubRegister();\n unsubUnregister();\n for (const cleanup of walletEventSubscriptions.values()) {\n cleanup();\n }\n walletEventSubscriptions.clear();\n cancelReconnect();\n listeners.clear();\n signerListeners.clear();\n // A store disposed mid-warm-up will never finish warming up, so\n // settle the status to 'disconnected'.\n // Runs after the listener sets are cleared, so no one is notified of\n // the final transition.\n if (isWalletWarmingUp(state.status)) {\n updateState({ status: 'disconnected' });\n }\n },\n };\n}\n","import {\n type ClientWithIdentity,\n type ClientWithPayer,\n type ClientWithSubscribeToIdentity,\n type ClientWithSubscribeToPayer,\n extendClient,\n SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED,\n SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE,\n SolanaError,\n withCleanup,\n} from '@solana/kit';\n\nimport { createWalletStore } from './store';\nimport type { ClientWithWallet, WalletPluginConfig } from './types';\n\n// -- Internal helpers ---------------------------------------------------------\n\nfunction defineSignerGetter(\n additions: Record<string, unknown>,\n property: string,\n store: ReturnType<typeof createWalletStore>,\n): void {\n Object.defineProperty(additions, property, {\n configurable: true,\n // Non-enumerable on purpose: this getter throws when no signer is connected, so we\n // don't want things like Object.entries to read it.\n enumerable: false,\n get() {\n const state = store.getState();\n if (!state.connected) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED, { status: state.status });\n }\n if (!state.connected.signer) {\n throw new SolanaError(SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE);\n }\n return state.connected.signer;\n },\n });\n}\n\ntype SignerProperties = 'payer' | 'identity';\n\n// Maps each signer capability to the Kit-convention reactive hook installed\n// alongside it (`@solana/plugin-interfaces`). A plugin whose `client.payer` /\n// `client.identity` is dynamic installs the matching `subscribeTo*` function\n// so reactive consumers can observe changes without naming this plugin.\nconst SUBSCRIBE_PROPERTY: Record<SignerProperties, 'subscribeToIdentity' | 'subscribeToPayer'> = {\n identity: 'subscribeToIdentity',\n payer: 'subscribeToPayer',\n};\n\nfunction createPlugin<TAdditions extends ClientWithWallet>(\n config: WalletPluginConfig,\n signerProperties: SignerProperties[],\n) {\n return <T extends object & { wallet?: never }>(client: T): Disposable & Omit<T, keyof TAdditions> & TAdditions => {\n if ('wallet' in client) {\n throw new Error(\n 'Only one wallet plugin can be used per client. ' +\n 'Use walletSigner, walletPayer, walletIdentity, or walletWithoutSigner — not multiple.',\n );\n }\n\n const store = createWalletStore(config);\n\n const additions: Record<string, unknown> = { wallet: store };\n for (const prop of signerProperties) {\n defineSignerGetter(additions, prop, store);\n additions[SUBSCRIBE_PROPERTY[prop]] = store.subscribeSigner;\n }\n\n return withCleanup(extendClient(client, additions), () => store[Symbol.dispose]()) as unknown as Disposable &\n Omit<T, keyof TAdditions> &\n TAdditions;\n };\n}\n\n// -- Public API ---------------------------------------------------------------\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to both `client.payer` and `client.identity`.\n *\n * This is the most common entrypoint for dApps. When a signing-capable\n * wallet is connected, `client.payer` and `client.identity` both return the\n * wallet signer. When disconnected or read-only, accessing either throws.\n *\n * Because the signer is dynamic, both `client.subscribeToPayer` and\n * `client.subscribeToIdentity` are installed so reactive consumers can observe\n * changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(walletSigner({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletPayer}\n * @see {@link walletIdentity}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletSigner(config: WalletPluginConfig) {\n return createPlugin<\n ClientWithIdentity &\n ClientWithPayer &\n ClientWithSubscribeToIdentity &\n ClientWithSubscribeToPayer &\n ClientWithWallet\n >(config, ['payer', 'identity']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to `client.identity`.\n *\n * Use this when `client.payer` is controlled by a separate `payer()` plugin\n * (e.g. a backend relayer pays fees, but the user's wallet is the identity).\n *\n * Because the signer is dynamic, `client.subscribeToIdentity` is installed so\n * reactive consumers can observe changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(payer(relayerKeypair))\n * .use(walletIdentity({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletPayer}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletIdentity(config: WalletPluginConfig) {\n return createPlugin<ClientWithIdentity & ClientWithSubscribeToIdentity & ClientWithWallet>(config, ['identity']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to `client.payer`.\n *\n * Use this when you need the wallet as the fee payer but don't need\n * `client.identity`. For most dApps, prefer {@link walletSigner} which\n * sets both.\n *\n * Because the signer is dynamic, `client.subscribeToPayer` is installed so\n * reactive consumers can observe changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(walletPayer({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletIdentity}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletPayer(config: WalletPluginConfig) {\n return createPlugin<ClientWithPayer & ClientWithSubscribeToPayer & ClientWithWallet>(config, ['payer']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard.\n *\n * Adds the `wallet` namespace to the client without setting `client.payer`\n * or `client.identity`. Use this alongside separate `payer()` and/or\n * `identity()` plugins, or when the wallet's signer is used explicitly in\n * instructions. For most dApps, prefer {@link walletSigner} instead.\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(payer(backendKeypair))\n * .use(walletWithoutSigner({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n *\n * // client.payer is always backendKeypair\n * // client.wallet.getState().connected?.signer for manual use\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletPayer}\n * @see {@link walletIdentity}\n * @see {@link WalletPluginConfig}\n */\nexport function walletWithoutSigner(config: WalletPluginConfig) {\n return createPlugin<ClientWithWallet>(config, []);\n}\n"]}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { isWalletWarmingUp } from './chunk-Z7IRL62Z.react-native.mjs';
|
|
2
1
|
import { withCleanup, extendClient, SolanaError, SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED, SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE, SOLANA_ERROR__WALLET__NOT_CONNECTED } from '@solana/kit';
|
|
3
2
|
import '@solana/wallet-account-signer';
|
|
4
3
|
import '@solana/wallet-standard-features';
|
|
@@ -7,6 +6,10 @@ import '@wallet-standard/features';
|
|
|
7
6
|
import '@wallet-standard/ui-features';
|
|
8
7
|
import '@wallet-standard/ui-registry';
|
|
9
8
|
|
|
9
|
+
// src/status.ts
|
|
10
|
+
function isWalletWarmingUp(status) {
|
|
11
|
+
return status === "pending" || status === "reconnecting";
|
|
12
|
+
}
|
|
10
13
|
function createWalletStore(config) {
|
|
11
14
|
{
|
|
12
15
|
const ssrSnapshot = Object.freeze({
|
|
@@ -86,6 +89,6 @@ function walletWithoutSigner(config) {
|
|
|
86
89
|
return createPlugin(config, []);
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
export { walletIdentity, walletPayer, walletSigner, walletWithoutSigner };
|
|
92
|
+
export { isWalletWarmingUp, walletIdentity, walletPayer, walletSigner, walletWithoutSigner };
|
|
90
93
|
//# sourceMappingURL=index.react-native.mjs.map
|
|
91
94
|
//# sourceMappingURL=index.react-native.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/store.ts","../src/wallet.ts"],"names":["SolanaError"],"mappings":"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","file":"index.react-native.mjs","sourcesContent":["import {\n type ReadonlyUint8Array,\n type SignatureBytes,\n SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE,\n SOLANA_ERROR__WALLET__NOT_CONNECTED,\n SolanaError,\n} from '@solana/kit';\nimport { createSignerFromWalletAccount } from '@solana/wallet-account-signer';\nimport {\n SolanaSignIn,\n type SolanaSignInFeature,\n type SolanaSignInInput,\n type SolanaSignInOutput,\n SolanaSignMessage,\n type SolanaSignMessageFeature,\n} from '@solana/wallet-standard-features';\nimport { getWallets } from '@wallet-standard/app';\nimport {\n StandardConnect,\n type StandardConnectFeature,\n StandardDisconnect,\n type StandardDisconnectFeature,\n StandardEvents,\n type StandardEventsFeature,\n} from '@wallet-standard/features';\nimport type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';\nimport { getWalletAccountFeature, getWalletFeature } from '@wallet-standard/ui-features';\nimport { getOrCreateUiWalletForStandardWallet, getWalletForHandle } from '@wallet-standard/ui-registry';\n\nimport { isWalletWarmingUp } from './status';\nimport type {\n WalletActionOptions,\n WalletNamespace,\n WalletPluginConfig,\n WalletSigner,\n WalletState,\n WalletStatus,\n WalletStorage,\n} from './types';\n\n// -- Internal types ---------------------------------------------------------\n\n/** @internal */\nexport type WalletStore = WalletNamespace & {\n /**\n * Registers a listener notified only when the wallet signer may\n * have changed.\n * Returns an idempotent unsubscribe function.\n *\n * @internal\n */\n subscribeSigner: (listener: () => void) => () => void;\n [Symbol.dispose]: () => void;\n};\n\n// Flat internal state for easier updates\ntype WalletStoreState = {\n account: UiWalletAccount | null;\n connectedWallet: UiWallet | null;\n signer: WalletSigner | null;\n status: WalletStatus;\n wallets: readonly UiWallet[];\n};\n\n// -- Store ------------------------------------------------------------------\n\n/** @internal */\nexport function createWalletStore(config: WalletPluginConfig): WalletStore {\n // -- SSR guard: on the server, return an inert stub ---------------------\n\n if (!__BROWSER__ || __REACTNATIVE__) {\n const ssrSnapshot: WalletState = Object.freeze({\n connected: null,\n status: 'pending' as const,\n wallets: Object.freeze([]) as readonly UiWallet[],\n });\n return {\n connect: () =>\n Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' })),\n disconnect: () => Promise.resolve(),\n getState: () => ssrSnapshot,\n selectAccount: () => {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });\n },\n signIn: () => Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' })),\n signMessage: () =>\n Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' })),\n subscribe: () => () => {},\n subscribeSigner: () => () => {},\n whenReady: () => Promise.resolve(),\n [Symbol.dispose]: () => {},\n };\n }\n\n // -- Browser-only initialization below this point ----------------------\n\n let state: WalletStoreState = {\n account: null,\n connectedWallet: null,\n signer: null,\n status: 'pending',\n wallets: [],\n };\n\n const listeners = new Set<() => void>();\n const signerListeners = new Set<() => void>();\n // One `standard:events change` subscription per discovered wallet, keyed by\n // name (the stable identity; the UiWallet handle regenerates on changes). The\n // underlying subscription attaches to the raw wallet's events, so it survives\n // handle regeneration — no need to resubscribe when a wallet's accounts change.\n const walletEventSubscriptions = new Map<string, () => void>();\n let reconnectCleanup: (() => void) | null = null;\n let userHasSelected = false;\n let connectGeneration = 0;\n let disposed = false;\n // Wallet that is in the process of disconnecting, stored to guard against re-connecting to it if a future connect fails\n let disconnectingWalletName: string | null = null;\n // Deferred backing `whenReady`. Created lazily while the status is transient\n // (see `isWalletWarmingUp`) and resolved by `updateState` once the status\n // settles, so `whenReady` can hand out a stable promise for the duration of\n // one warm-up episode (required by React Suspense).\n let readyDeferred: PromiseWithResolvers<void> | null = null;\n\n // Resolve storage: default to localStorage in browser, null to disable.\n // Merely *accessing* `localStorage` throws a `SecurityError` in sandboxed\n // iframes or when third-party storage is blocked, so the default resolution\n // degrades to `null` rather than letting the whole plugin-install chain\n // throw. This matches the best-effort persistence applied to reads/writes.\n function resolveDefaultStorage(): WalletStorage | null {\n try {\n return localStorage;\n } catch {\n return null;\n }\n }\n\n const storage: WalletStorage | null = config.storage === null ? null : (config.storage ?? resolveDefaultStorage());\n const storageKey = config.storageKey ?? 'kit-wallet';\n\n // -- State management --------------------------------------------------\n\n function deriveSnapshot(s: WalletStoreState): WalletState {\n return Object.freeze({\n connected:\n s.connectedWallet && s.account\n ? Object.freeze({\n account: s.account,\n signer: s.signer,\n wallet: s.connectedWallet,\n })\n : null,\n status: s.status,\n wallets: s.wallets,\n });\n }\n\n let snapshot: WalletState = deriveSnapshot(state);\n\n // We set `client.{payer,identity}` to `getState().connected?.signer\n // This is `undefined` if disconnected, `null` if connected to a read-only wallet,\n // or the signer reference otherwise.\n function observableSigner(s: WalletStoreState): WalletSigner | null | undefined {\n return s.connectedWallet && s.account ? s.signer : undefined;\n }\n\n function updateState(updates: Partial<WalletStoreState>): void {\n const prev = state;\n state = { ...state, ...updates };\n\n // Resolve `whenReady` the moment the status leaves the transient\n // warm-up set. `readyDeferred` is only ever created while transient (in\n // `whenReady`), so clearing it here lets the next transient episode arm\n // a fresh one.\n if (isWalletWarmingUp(prev.status) && !isWalletWarmingUp(state.status)) {\n readyDeferred?.resolve();\n readyDeferred = null;\n }\n\n if (state.wallets !== prev.wallets) {\n syncWalletEventSubscriptions(state.wallets);\n }\n\n // Signer subscribers (`subscribeToPayer` / `subscribeToIdentity`) fire\n // only when the observed signer changes.\n const signerChanged = observableSigner(state) !== observableSigner(prev);\n\n if (\n state.connectedWallet !== prev.connectedWallet ||\n state.account !== prev.account ||\n state.status !== prev.status ||\n state.signer !== prev.signer ||\n state.wallets !== prev.wallets\n ) {\n snapshot = deriveSnapshot(state);\n listeners.forEach(l => l());\n }\n\n if (signerChanged) {\n signerListeners.forEach(l => l());\n }\n }\n\n // Advanced: see the `whenReady` docblock on `WalletNamespace`. Returns a\n // stable, already-resolved promise once the initial warm-up has settled;\n // while transient, returns the same pending promise on every call so it is\n // safe to throw for React Suspense.\n function whenReady(): Promise<void> {\n if (!isWalletWarmingUp(state.status)) {\n return Promise.resolve();\n }\n readyDeferred ??= Promise.withResolvers<void>();\n return readyDeferred.promise;\n }\n\n // -- Signer creation ---------------------------------------------------\n\n function tryCreateSigner(account: UiWalletAccount): WalletSigner | null {\n try {\n // `createSignerFromWalletAccount` throws for chains the wallet\n // doesn't support — which we catch below. Non-Solana chains\n // therefore degrade to `signer: null`, matching the read-only\n // wallet contract.\n return createSignerFromWalletAccount(account, config.chain);\n } catch {\n // Wallet doesn't support signing (read-only / watch wallet, or a\n // non-Solana chain).\n return null;\n }\n }\n\n // -- Wallet discovery --------------------------------------------------\n\n // `getWallets()` is a wallet-standard function that returns the `Wallets` API\n // This includes a `get()` function to get the current list of wallets, and\n // an `on()` function to subscribe to wallet registration/unregistration events.\n const registry = getWallets();\n\n function filterWallet(uiWallet: UiWallet): boolean {\n const supportsChain = uiWallet.chains.includes(config.chain);\n const supportsConnect = uiWallet.features.includes(StandardConnect);\n if (!supportsChain || !supportsConnect) return false;\n return config.filter ? config.filter(uiWallet) : true;\n }\n\n function buildWalletList(): readonly UiWallet[] {\n return Object.freeze(registry.get().map(getOrCreateUiWalletForStandardWallet).filter(filterWallet));\n }\n\n // Rebuilds the filtered wallet list but returns the *existing* `state.wallets`\n // reference when the contents are unchanged. `buildWalletList` always\n // allocates a fresh frozen array, so without this a registry event for a\n // wallet that gets filtered out (or any event that doesn't alter the visible\n // set) would still produce a new snapshot and notify every listener — a\n // spurious re-render. Wallets are compared by reference, in order: the\n // wallet-ui registry hands back a stable UiWallet for a given underlying\n // wallet until its accounts/features/chains change, so reference equality is\n // a sound same-contents check here.\n function reconcileWalletList(): readonly UiWallet[] {\n const next = buildWalletList();\n const prev = state.wallets;\n if (next.length === prev.length && next.every((w, i) => w === prev[i])) {\n return prev;\n }\n return next;\n }\n\n // Reconcile the per-wallet event subscriptions against the current wallet set:\n // subscribe wallets that just appeared, unsubscribe those that left. Called\n // whenever `state.wallets` changes reference. Subscribing fires nothing, so\n // this never causes re-render churn. `wallets` is the already-filtered visible\n // list, so wallets excluded by chain/feature/`config.filter` are intentionally\n // never subscribed — a `register` / `unregister` event reconciles them in or\n // out of the list (and thus in or out of this map) when their eligibility\n // changes.\n function syncWalletEventSubscriptions(wallets: readonly UiWallet[]): void {\n const names = new Set(wallets.map(w => w.name));\n for (const [name, cleanup] of walletEventSubscriptions) {\n if (!names.has(name)) {\n cleanup();\n walletEventSubscriptions.delete(name);\n }\n }\n for (const w of wallets) {\n if (!walletEventSubscriptions.has(w.name)) {\n walletEventSubscriptions.set(w.name, subscribeToWalletEvents(w));\n }\n }\n }\n\n // True if a wallet matching `uiWallet` (by name) is currently registered and\n // passes the filter. Used to re-check membership after an awaited connect /\n // sign-in / silent reconnect resolves: the wallet may have unregistered (or\n // dropped a required feature/chain) while its prompt was open, and the\n // unregister handler can't tear down a connection that hasn't been\n // established yet. Compared by name — the same identity the unregister\n // handler uses — because the wallet-ui registry hands back a fresh UiWallet\n // reference once connect adds accounts, so reference equality would not hold.\n function isWalletStillAvailable(uiWallet: UiWallet): boolean {\n return buildWalletList().some(w => w.name === uiWallet.name);\n }\n\n updateState({ wallets: buildWalletList() });\n\n // Listen for new wallets being registered\n const unsubRegister = registry.on('register', () => {\n updateState({ wallets: reconcileWalletList() });\n });\n\n // Listen for wallets being unregistered — if the connected wallet is removed, disconnect\n const unsubUnregister = registry.on('unregister', () => {\n const newWallets = reconcileWalletList();\n const updates: Partial<WalletStoreState> = { wallets: newWallets };\n\n // If the connected wallet was unregistered, disconnect locally.\n if (state.connectedWallet && !newWallets.some(w => w.name === state.connectedWallet!.name)) {\n updates.connectedWallet = null;\n updates.account = null;\n updates.signer = null;\n updates.status = 'disconnected';\n clearPersistedAccount();\n }\n\n updateState(updates);\n });\n\n // -- Wallet-initiated events -------------------------------------------\n\n function cancelReconnect(): void {\n reconnectCleanup?.();\n reconnectCleanup = null;\n }\n\n function setConnected(account: UiWalletAccount, wallet: UiWallet, options?: { persist?: boolean }): void {\n // The store may have been disposed while a connect / sign-in / silent\n // reconnect awaited. The `connectGeneration` guard at each call site\n // doesn't cover the auto-connect IIFE resuming from its storage read —\n // it captures a fresh generation in `attemptSilentReconnect` after\n // disposal. Bail here so a disposed store never re-subscribes to wallet\n // events (a listener the already-run disposer can't clean up) or reports\n // itself connected.\n if (disposed) return;\n const signer = tryCreateSigner(account);\n disconnectingWalletName = null;\n // Reconcile `wallets` alongside the connection: `connect`/`signIn`/silent\n // reconnect authorize accounts on the underlying wallet, which regenerates\n // its registry handle. Without rebuilding the list here, `getState().wallets`\n // would keep the pre-connect handle (empty `.accounts`) until an unrelated\n // `standard:events change` happened to fire — so consumers reading the\n // just-connected wallet from `wallets` (e.g. rendering its authorized\n // accounts) would see it as unconnected. `reconcileWalletList` returns the\n // existing reference when the visible set is unchanged, keeping the snapshot\n // churn-free when connect didn't actually add accounts.\n updateState({\n account,\n connectedWallet: wallet,\n signer,\n status: 'connected',\n wallets: reconcileWalletList(),\n });\n if (options?.persist !== false) {\n persistAccount(account, wallet);\n }\n }\n\n function refreshUiWallet(staleUiWallet: UiWallet): UiWallet {\n const rawWallet = getWalletForHandle(staleUiWallet);\n return getOrCreateUiWalletForStandardWallet(rawWallet);\n }\n\n function subscribeToWalletEvents(uiWallet: UiWallet): () => void {\n if (!uiWallet.features.includes(StandardEvents)) {\n return () => {};\n }\n const eventsFeature = getWalletFeature(\n uiWallet,\n StandardEvents,\n ) as StandardEventsFeature[typeof StandardEvents];\n const walletName = uiWallet.name;\n\n // The handler intentionally ignores the changed-property flags\n // (`accounts`, `chains`, `features`) the wallet passes and instead\n // reconciles against the refreshed wallet. The signer is derived\n // entirely from the active account's features and chains, and the\n // wallet-ui registry regenerates the account reference precisely when\n // one of those changes — so the account reference, not the flags, is\n // the single source of truth for whether the signer is affected. Branch\n // on the reconciled state below, never on `properties`.\n return eventsFeature.on('change', () => {\n // Refresh the discovered list so this wallet's (and any others') accounts\n // are current; `updateState` re-syncs subscriptions for added/removed wallets.\n const newWallets = reconcileWalletList();\n\n // Non-active wallet: reflect its new account/feature state in the list only.\n if (!state.connectedWallet || state.connectedWallet.name !== walletName) {\n updateState({ wallets: newWallets });\n return;\n }\n\n // Active wallet: the original reconcile logic, unchanged.\n const refreshed = refreshUiWallet(state.connectedWallet);\n if (!filterWallet(refreshed)) {\n disconnectLocally();\n return;\n }\n const result = reconcileActiveAccount(refreshed);\n if (result === null) {\n disconnectLocally();\n return;\n }\n updateState({ connectedWallet: refreshed, wallets: newWallets, ...result });\n if (result.account) {\n persistAccount(result.account, refreshed);\n }\n });\n }\n\n function reconcileActiveAccount(wallet: UiWallet): Partial<WalletStoreState> | null {\n const newAccounts = wallet.accounts;\n\n // No accounts left — the caller disconnects.\n if (newAccounts.length === 0) return null;\n\n const currentAddress = state.account?.address;\n const stillPresent = currentAddress ? newAccounts.find(a => a.address === currentAddress) : null;\n const activeAccount = stillPresent ?? newAccounts[0];\n\n // Same reference — nothing the signer depends on changed, so leave the\n // signer untouched to preserve referential stability.\n if (activeAccount === state.account) return {};\n\n // The active account changed (switched, removed, or regenerated because\n // its features/chains changed) — recreate the signer for it.\n return { account: activeAccount, signer: tryCreateSigner(activeAccount) };\n }\n\n // -- Connection lifecycle ----------------------------------------------\n\n async function connect(uiWallet: UiWallet, options?: WalletActionOptions): Promise<readonly UiWalletAccount[]> {\n options?.abortSignal?.throwIfAborted();\n\n // Resolve the feature before mutating any state. getWalletFeature throws\n // WalletStandardError synchronously when the wallet doesn't support\n // standard:connect — doing this first means an unsupported wallet is a\n // no-op that never tears down an existing connection or cancels a\n // pending reconnect.\n const connectFeature = getWalletFeature(\n uiWallet,\n StandardConnect,\n ) as StandardConnectFeature[typeof StandardConnect];\n\n userHasSelected = true;\n cancelReconnect();\n const generation = ++connectGeneration;\n updateState({ status: 'connecting' });\n\n try {\n // Snapshot existing accounts before connect.\n const existingAccounts = [...uiWallet.accounts];\n\n await connectFeature.connect();\n\n // A newer connect/signIn was started while we were awaiting. Reject\n // with an `AbortError` so this superseded call settles in the\n // standard, ignorable way (matching the abort-signal contract) while\n // the newer request owns the connection. The `catch` below leaves\n // that newer connection untouched, since `generation` no longer\n // matches `connectGeneration`.\n if (generation !== connectGeneration) {\n throw new DOMException('Wallet connect was superseded by a newer connect or sign-in', 'AbortError');\n }\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while its connect prompt was open. The unregister\n // handler only disconnects an already-connected wallet, so without\n // this re-check the store would be left connected to a wallet absent\n // from `wallets`. Throw so the connection failure surfaces as a\n // rejection; the `catch` below reverts to any prior connection.\n if (!isWalletStillAvailable(uiWallet)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' });\n }\n\n // Refresh UiWallet to get updated accounts after connect.\n const refreshedWallet = refreshUiWallet(uiWallet);\n const allAccounts = refreshedWallet.accounts;\n\n // No authorized accounts means there's nothing to connect to. Throw\n // rather than resolving so a caller can't mistake an empty result for\n // a successful connection; the `catch` reverts to any prior wallet.\n if (allAccounts.length === 0) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' });\n }\n\n // Prefer the first newly authorized account.\n const newAccount = allAccounts.find(a => !existingAccounts.some(e => e.address === a.address));\n const activeAccount = newAccount ?? allAccounts[0];\n\n setConnected(activeAccount, refreshedWallet);\n return allAccounts;\n } catch (error) {\n // Only act if we're still the active connection attempt; a\n // superseded attempt must leave the newer connection untouched.\n if (generation === connectGeneration) {\n revertToPreviousConnectionOrDisconnect();\n }\n throw error;\n }\n }\n\n async function disconnect(wallet?: UiWallet, options?: WalletActionOptions): Promise<void> {\n options?.abortSignal?.throwIfAborted();\n\n const target = wallet ?? state.connectedWallet;\n\n // Nothing to disconnect (no arg, nothing active): cancel any pending reconnect, and\n // supersede in-flight attempts.\n if (!target) {\n userHasSelected = true;\n cancelReconnect();\n connectGeneration++;\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const isActive = state.connectedWallet != null && state.connectedWallet.name === target.name;\n\n if (isActive) {\n const currentWallet = state.connectedWallet!;\n // Bump generation so this disconnect supersedes any in-flight connect/signIn.\n const generation = ++connectGeneration;\n disconnectingWalletName = currentWallet.name;\n updateState({ status: 'disconnecting' });\n try {\n if (currentWallet.features.includes(StandardDisconnect)) {\n const disconnectFeature = getWalletFeature(\n currentWallet,\n StandardDisconnect,\n ) as StandardDisconnectFeature[typeof StandardDisconnect];\n await disconnectFeature.disconnect();\n }\n } finally {\n if (generation === connectGeneration) {\n disconnectLocally();\n }\n }\n return;\n }\n\n // targeted disconnect but wallet gone: leave the active connection, status,\n // and persisted key untouched.\n if (!isWalletStillAvailable(target)) {\n return; // already gone / not authorized — forgiving no-op\n }\n const refreshedTarget = refreshUiWallet(target);\n if (!refreshedTarget.features.includes(StandardDisconnect)) {\n return; // can't deauthorize without standard:disconnect — no-op\n }\n // Guard against a concurrent select/connect into this wallet for the await;\n // restore the prior marker afterward (it may belong to an active disconnect).\n const prevDisconnecting = disconnectingWalletName;\n disconnectingWalletName = refreshedTarget.name;\n try {\n const disconnectFeature = getWalletFeature(\n refreshedTarget,\n StandardDisconnect,\n ) as StandardDisconnectFeature[typeof StandardDisconnect];\n await disconnectFeature.disconnect();\n } finally {\n if (disconnectingWalletName === refreshedTarget.name) {\n disconnectingWalletName = prevDisconnecting;\n }\n // Reflect the now-empty accounts immediately — but only if the store\n // is still live. If it was disposed while `standard:disconnect`\n // awaited, the disposer has already torn down every wallet-event\n // subscription and cleared the map; deauthorizing the target\n // regenerated its handle, so `reconcileWalletList` returns a fresh\n // array, and letting `updateState` run here would re-invoke\n // `syncWalletEventSubscriptions` and re-subscribe every wallet into\n // the cleared map — listeners the disposer can no longer clean up.\n if (!disposed) {\n updateState({ wallets: reconcileWalletList() });\n }\n }\n }\n\n function disconnectLocally(): void {\n disconnectingWalletName = null;\n\n updateState({\n account: null,\n connectedWallet: null,\n signer: null,\n status: 'disconnected',\n // Reconcile the list too: when this runs from the change handler\n // because the connected wallet dropped the configured chain or\n // failed the filter, that wallet must leave `wallets` immediately\n // rather than linger until an unrelated register/unregister event.\n // `reconcileWalletList` returns the existing reference when the\n // visible set is unchanged (the common case for a plain disconnect),\n // so this stays churn-free.\n wallets: reconcileWalletList(),\n });\n\n clearPersistedAccount();\n }\n\n // A `connect`/`signIn` attempt failed to establish a new connection. While\n // such an attempt is in flight only `status` is moved to 'connecting' — the\n // existing connection's account, signer, event subscription, and persisted\n // key are untouched until `setConnected` runs on success. So if a prior\n // connection is still intact, revert to it (declining the new wallet must not\n // log the user out of the one they had). Otherwise there's nothing to keep,\n // so disconnect cleanly. The active-attempt (`generation`) guard at each call\n // site ensures a superseded attempt never reverts the connection a newer one\n // owns.\n function revertToPreviousConnectionOrDisconnect(): void {\n // Don't revive a wallet with disconnect in progress\n if (state.connectedWallet && state.account && state.connectedWallet.name !== disconnectingWalletName) {\n updateState({ status: 'connected' });\n } else {\n disconnectLocally();\n }\n }\n\n function selectAccount(account: UiWalletAccount): void {\n // Derive the owning wallet from the account handle. `getWalletForHandle`\n // keys a WeakMap on handle identity, so this is unambiguous even when the\n // same address is authorized in two wallets. A stale/unregistered handle\n // throws — surface it as ACCOUNT_NOT_AVAILABLE.\n let owner: UiWallet;\n try {\n owner = getOrCreateUiWalletForStandardWallet(getWalletForHandle(account));\n } catch {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n // The owner must be currently authorized (discovered + passes the filter).\n if (!isWalletStillAvailable(owner)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n // Reject selecting into a wallet that is mid-disconnect.\n if (owner.name === disconnectingWalletName) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });\n }\n\n const refreshed = refreshUiWallet(owner);\n const selectedAccount = refreshed.accounts.find(a => a.address === account.address);\n if (!selectedAccount) {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n userHasSelected = true;\n // Supersede any in-flight connect/signIn.\n ++connectGeneration;\n\n // No-op fast path: the same account is already active.\n if (selectedAccount === state.account && refreshed === state.connectedWallet) {\n updateState({ status: 'connected' });\n return;\n }\n\n // Switch the active connection (owner may differ from the current one). The\n // previously-active wallet is left authorized — we never disconnect it. Its\n // event subscription persists, so switching back stays live.\n const signer = tryCreateSigner(selectedAccount);\n updateState({ account: selectedAccount, connectedWallet: refreshed, signer, status: 'connected' });\n persistAccount(selectedAccount, refreshed);\n }\n\n // -- Message signing ---------------------------------------------------\n\n async function signMessage(message: ReadonlyUint8Array, options?: WalletActionOptions): Promise<SignatureBytes> {\n options?.abortSignal?.throwIfAborted();\n const { connectedWallet, account } = state;\n if (!connectedWallet || !account) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' });\n }\n // Use the wallet feature directly rather than going through the cached\n // signer. This decouples message signing from transaction signing —\n // a wallet that supports solana:signMessage but not transaction signing\n // still works. solana:signMessage is an account-scoped feature, so this\n // resolves it against the active account: getWalletAccountFeature throws\n // a WalletStandardError when either the wallet or the account does not\n // support it.\n const signMessageFeature = getWalletAccountFeature(\n account,\n SolanaSignMessage,\n ) as SolanaSignMessageFeature[typeof SolanaSignMessage];\n // The wallet-standard `SolanaSignMessageInput.message` is a mutable `Uint8Array`; signing\n // only reads the bytes, so coerce the read-only input at this single boundary.\n const [output] = await signMessageFeature.signMessage({ account, message: message as Uint8Array });\n return output.signature as SignatureBytes;\n }\n\n // -- Sign In With Solana (SIWS-as-connect) ------------------------------\n\n async function signIn(\n uiWallet: UiWallet,\n input: SolanaSignInInput,\n options?: WalletActionOptions,\n ): Promise<SolanaSignInOutput> {\n options?.abortSignal?.throwIfAborted();\n\n // Resolve the feature before mutating any state. getWalletFeature throws\n // WalletStandardError synchronously when the wallet doesn't support\n // solana:signIn — doing this first means an unsupported wallet is a\n // no-op that never tears down an existing connection.\n const signInFeature = getWalletFeature(uiWallet, SolanaSignIn) as SolanaSignInFeature[typeof SolanaSignIn];\n\n userHasSelected = true;\n cancelReconnect();\n const generation = ++connectGeneration;\n updateState({ status: 'connecting' });\n\n try {\n const [result] = await signInFeature.signIn(input);\n\n // A newer connect/signIn was started while we were awaiting. Reject\n // with an `AbortError` (matching the abort-signal contract) so the\n // superseded call settles in the standard, ignorable way while the\n // newer request owns the connection.\n if (generation !== connectGeneration) {\n throw new DOMException('Wallet sign-in was superseded by a newer connect or sign-in', 'AbortError');\n }\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while its sign-in prompt was open. Throw rather than\n // resolving while the store is left connected to no wallet (or a\n // different one); the `catch` below reverts to any prior connection.\n if (!isWalletStillAvailable(uiWallet)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' });\n }\n\n // Set up full connection state using the account from the sign-in response.\n const refreshedWallet = refreshUiWallet(uiWallet);\n const activeAccount = refreshedWallet.accounts.find(a => a.address === result.account.address);\n\n if (!activeAccount) {\n // The wallet signed in with an account it doesn't expose — a\n // protocol violation that can't be mapped to a connection. Throw\n // so the failure surfaces as a rejection rather than resolving\n // with a sign-in result the store can't act on; the `catch`\n // reverts to any prior connection.\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' });\n }\n\n setConnected(activeAccount, refreshedWallet);\n return result;\n } catch (error) {\n if (generation === connectGeneration) {\n revertToPreviousConnectionOrDisconnect();\n }\n throw error;\n }\n }\n\n // -- Persistence -------------------------------------------------------\n\n function ignoreStorageFailure(operation: () => Promise<void> | void): void {\n void (async () => {\n try {\n await operation();\n } catch {\n // Persistence is best-effort; wallet state is the source of truth.\n }\n })();\n }\n\n function persistAccount(account: UiWalletAccount, wallet: UiWallet): void {\n if (!storage) return;\n ignoreStorageFailure(() => storage.setItem(storageKey, `${wallet.name}:${account.address}`));\n }\n\n function clearPersistedAccount(): void {\n if (!storage) return;\n ignoreStorageFailure(() => storage.removeItem(storageKey));\n }\n\n // -- Auto-connect ------------------------------------------------------\n\n if (config.autoConnect !== false && storage) {\n const generation = connectGeneration;\n (async () => {\n const savedKey = await storage.getItem(storageKey);\n // Don't auto-connect if the user has selected a wallet, or if the\n // store was disposed while the storage read was in flight — the\n // disposer has already torn down its listeners and bumped the\n // generation, but this IIFE hasn't captured one yet, so without this\n // check it would resume and silently reconnect into a disposed store.\n if (userHasSelected || disposed) return;\n\n if (!savedKey) {\n updateState({ status: 'disconnected' });\n return;\n }\n\n const separatorIndex = savedKey.lastIndexOf(':');\n if (separatorIndex === -1) {\n // Malformed saved key.\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const walletName = savedKey.slice(0, separatorIndex);\n const savedAddress = savedKey.slice(separatorIndex + 1);\n const existing = state.wallets.find(w => w.name === walletName);\n\n if (existing) {\n await attemptSilentReconnect(savedAddress, existing);\n } else if (\n registry.get().some(w => {\n const ui = getOrCreateUiWalletForStandardWallet(w);\n return ui.name === walletName;\n })\n ) {\n // Wallet registered but doesn't pass the filter.\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n } else {\n // Wallet not registered yet — wait for it to appear.\n updateState({ status: 'reconnecting' });\n\n // Change state to disconnected after 3s\n // Note that the listener stays alive until reconnect is cancelled, so this is\n // just affecting how long the UI shows reconnecting\n const statusTimeout = setTimeout(() => {\n if (!userHasSelected && state.status === 'reconnecting') {\n updateState({ status: 'disconnected' });\n }\n }, 3000);\n\n const unsubRegisterForReconnect = registry.on('register', () => {\n const generation = connectGeneration;\n void (async () => {\n if (userHasSelected) {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n return;\n }\n const found = buildWalletList().find(w => w.name === walletName);\n if (found) {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n await attemptSilentReconnect(savedAddress, found);\n } else if (\n registry.get().some(w => {\n const ui = getOrCreateUiWalletForStandardWallet(w);\n return ui.name === walletName;\n })\n ) {\n // Wallet registered but filtered out.\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n }\n })().catch(() => {\n // Reconnect failed — fall back to disconnected.\n if (generation === connectGeneration && !userHasSelected) {\n updateState({ status: 'disconnected' });\n }\n });\n });\n\n reconnectCleanup = () => {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n };\n }\n })().catch(() => {\n // Storage read failed — fall back to disconnected.\n if (generation === connectGeneration && !userHasSelected) {\n updateState({ status: 'disconnected' });\n }\n });\n } else {\n // No auto-connect: immediately transition from 'pending' to 'disconnected'.\n updateState({ status: 'disconnected' });\n }\n\n async function attemptSilentReconnect(savedAddress: string, uiWallet: UiWallet): Promise<void> {\n const generation = ++connectGeneration;\n updateState({ status: 'reconnecting' });\n\n try {\n const connectFeature = getWalletFeature(\n uiWallet,\n StandardConnect,\n ) as StandardConnectFeature[typeof StandardConnect];\n await connectFeature.connect({ silent: true });\n\n // A newer connect/signIn was started while we were awaiting — bail.\n if (generation !== connectGeneration) return;\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while the silent reconnect was in flight.\n if (!isWalletStillAvailable(uiWallet)) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const refreshedWallet = refreshUiWallet(uiWallet);\n const allAccounts = refreshedWallet.accounts;\n\n if (allAccounts.length === 0) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n // Restore the specific saved account, fall back to first from same wallet.\n const activeAccount = allAccounts.find(a => a.address === savedAddress) ?? allAccounts[0];\n\n // Persist only when we fell back to a different account\n setConnected(activeAccount, refreshedWallet, {\n persist: activeAccount.address !== savedAddress,\n });\n } catch {\n if (generation === connectGeneration) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n }\n }\n }\n\n // -- Public API --------------------------------------------------------\n\n return {\n connect,\n disconnect,\n getState: () => snapshot,\n selectAccount,\n signIn,\n signMessage,\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n subscribeSigner: (listener: () => void) => {\n signerListeners.add(listener);\n return () => {\n signerListeners.delete(listener);\n };\n },\n whenReady,\n [Symbol.dispose]: () => {\n // Invalidate any in-flight connect/signIn/silent-reconnect so they\n // bail instead of resuming after disposal (which would re-subscribe\n // to wallet events and re-arm cleanup that this disposer already\n // ran). The generation bump covers attempts that already captured a\n // generation; `disposed` covers the auto-connect IIFE, which resumes\n // from its storage read and captures a fresh generation afterward.\n disposed = true;\n connectGeneration++;\n unsubRegister();\n unsubUnregister();\n for (const cleanup of walletEventSubscriptions.values()) {\n cleanup();\n }\n walletEventSubscriptions.clear();\n cancelReconnect();\n listeners.clear();\n signerListeners.clear();\n // A store disposed mid-warm-up will never finish warming up, so\n // settle the status to 'disconnected'.\n // Runs after the listener sets are cleared, so no one is notified of\n // the final transition.\n if (isWalletWarmingUp(state.status)) {\n updateState({ status: 'disconnected' });\n }\n },\n };\n}\n","import {\n type ClientWithIdentity,\n type ClientWithPayer,\n type ClientWithSubscribeToIdentity,\n type ClientWithSubscribeToPayer,\n extendClient,\n SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED,\n SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE,\n SolanaError,\n withCleanup,\n} from '@solana/kit';\n\nimport { createWalletStore } from './store';\nimport type { ClientWithWallet, WalletPluginConfig } from './types';\n\n// -- Internal helpers ---------------------------------------------------------\n\nfunction defineSignerGetter(\n additions: Record<string, unknown>,\n property: string,\n store: ReturnType<typeof createWalletStore>,\n): void {\n Object.defineProperty(additions, property, {\n configurable: true,\n // Non-enumerable on purpose: this getter throws when no signer is connected, so we\n // don't want things like Object.entries to read it.\n enumerable: false,\n get() {\n const state = store.getState();\n if (!state.connected) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED, { status: state.status });\n }\n if (!state.connected.signer) {\n throw new SolanaError(SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE);\n }\n return state.connected.signer;\n },\n });\n}\n\ntype SignerProperties = 'payer' | 'identity';\n\n// Maps each signer capability to the Kit-convention reactive hook installed\n// alongside it (`@solana/plugin-interfaces`). A plugin whose `client.payer` /\n// `client.identity` is dynamic installs the matching `subscribeTo*` function\n// so reactive consumers can observe changes without naming this plugin.\nconst SUBSCRIBE_PROPERTY: Record<SignerProperties, 'subscribeToIdentity' | 'subscribeToPayer'> = {\n identity: 'subscribeToIdentity',\n payer: 'subscribeToPayer',\n};\n\nfunction createPlugin<TAdditions extends ClientWithWallet>(\n config: WalletPluginConfig,\n signerProperties: SignerProperties[],\n) {\n return <T extends object & { wallet?: never }>(client: T): Disposable & Omit<T, keyof TAdditions> & TAdditions => {\n if ('wallet' in client) {\n throw new Error(\n 'Only one wallet plugin can be used per client. ' +\n 'Use walletSigner, walletPayer, walletIdentity, or walletWithoutSigner — not multiple.',\n );\n }\n\n const store = createWalletStore(config);\n\n const additions: Record<string, unknown> = { wallet: store };\n for (const prop of signerProperties) {\n defineSignerGetter(additions, prop, store);\n additions[SUBSCRIBE_PROPERTY[prop]] = store.subscribeSigner;\n }\n\n return withCleanup(extendClient(client, additions), () => store[Symbol.dispose]()) as unknown as Disposable &\n Omit<T, keyof TAdditions> &\n TAdditions;\n };\n}\n\n// -- Public API ---------------------------------------------------------------\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to both `client.payer` and `client.identity`.\n *\n * This is the most common entrypoint for dApps. When a signing-capable\n * wallet is connected, `client.payer` and `client.identity` both return the\n * wallet signer. When disconnected or read-only, accessing either throws.\n *\n * Because the signer is dynamic, both `client.subscribeToPayer` and\n * `client.subscribeToIdentity` are installed so reactive consumers can observe\n * changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(walletSigner({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletPayer}\n * @see {@link walletIdentity}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletSigner(config: WalletPluginConfig) {\n return createPlugin<\n ClientWithIdentity &\n ClientWithPayer &\n ClientWithSubscribeToIdentity &\n ClientWithSubscribeToPayer &\n ClientWithWallet\n >(config, ['payer', 'identity']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to `client.identity`.\n *\n * Use this when `client.payer` is controlled by a separate `payer()` plugin\n * (e.g. a backend relayer pays fees, but the user's wallet is the identity).\n *\n * Because the signer is dynamic, `client.subscribeToIdentity` is installed so\n * reactive consumers can observe changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(payer(relayerKeypair))\n * .use(walletIdentity({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletPayer}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletIdentity(config: WalletPluginConfig) {\n return createPlugin<ClientWithIdentity & ClientWithSubscribeToIdentity & ClientWithWallet>(config, ['identity']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to `client.payer`.\n *\n * Use this when you need the wallet as the fee payer but don't need\n * `client.identity`. For most dApps, prefer {@link walletSigner} which\n * sets both.\n *\n * Because the signer is dynamic, `client.subscribeToPayer` is installed so\n * reactive consumers can observe changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(walletPayer({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletIdentity}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletPayer(config: WalletPluginConfig) {\n return createPlugin<ClientWithPayer & ClientWithSubscribeToPayer & ClientWithWallet>(config, ['payer']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard.\n *\n * Adds the `wallet` namespace to the client without setting `client.payer`\n * or `client.identity`. Use this alongside separate `payer()` and/or\n * `identity()` plugins, or when the wallet's signer is used explicitly in\n * instructions. For most dApps, prefer {@link walletSigner} instead.\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(payer(backendKeypair))\n * .use(walletWithoutSigner({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n *\n * // client.payer is always backendKeypair\n * // client.wallet.getState().connected?.signer for manual use\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletPayer}\n * @see {@link walletIdentity}\n * @see {@link WalletPluginConfig}\n */\nexport function walletWithoutSigner(config: WalletPluginConfig) {\n return createPlugin<ClientWithWallet>(config, []);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/status.ts","../src/store.ts","../src/wallet.ts"],"names":["SolanaError"],"mappings":";;;;;;;;;AA8BO,SAAS,kBAAkB,MAAA,EAA+B;AAC7D,EAAA,OAAO,MAAA,KAAW,aAAa,MAAA,KAAW,cAAA;AAC9C;ACmCO,SAAS,kBAAkB,MAAA,EAAyC;AAGvE,EAAqC;AACjC,IAAA,MAAM,WAAA,GAA2B,OAAO,MAAA,CAAO;AAAA,MAC3C,SAAA,EAAW,IAAA;AAAA,MACX,MAAA,EAAQ,SAAA;AAAA,MACR,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,EAAE;AAAA,KAC5B,CAAA;AACD,IAAA,OAAO;AAAA,MACH,OAAA,EAAS,MACL,OAAA,CAAQ,MAAA,CAAO,IAAI,WAAA,CAAY,mCAAA,EAAqC,EAAE,SAAA,EAAW,SAAA,EAAW,CAAC,CAAA;AAAA,MACjG,UAAA,EAAY,MAAM,OAAA,CAAQ,OAAA,EAAQ;AAAA,MAClC,UAAU,MAAM,WAAA;AAAA,MAChB,eAAe,MAAM;AACjB,QAAA,MAAM,IAAI,WAAA,CAAY,mCAAA,EAAqC,EAAE,SAAA,EAAW,iBAAiB,CAAA;AAAA,MAC7F,CAAA;AAAA,MACA,MAAA,EAAQ,MAAM,OAAA,CAAQ,MAAA,CAAO,IAAI,WAAA,CAAY,mCAAA,EAAqC,EAAE,SAAA,EAAW,QAAA,EAAU,CAAC,CAAA;AAAA,MAC1G,WAAA,EAAa,MACT,OAAA,CAAQ,MAAA,CAAO,IAAI,WAAA,CAAY,mCAAA,EAAqC,EAAE,SAAA,EAAW,aAAA,EAAe,CAAC,CAAA;AAAA,MACrG,SAAA,EAAW,MAAM,MAAM;AAAA,MAAC,CAAA;AAAA,MACxB,eAAA,EAAiB,MAAM,MAAM;AAAA,MAAC,CAAA;AAAA,MAC9B,SAAA,EAAW,MAAM,OAAA,CAAQ,OAAA,EAAQ;AAAA,MACjC,CAAC,MAAA,CAAO,OAAO,GAAG,MAAM;AAAA,MAAC;AAAA,KAC7B;AAAA,EACJ;AAi4BJ;;;AC58BA,SAAS,kBAAA,CACL,SAAA,EACA,QAAA,EACA,KAAA,EACI;AACJ,EAAA,MAAA,CAAO,cAAA,CAAe,WAAW,QAAA,EAAU;AAAA,IACvC,YAAA,EAAc,IAAA;AAAA;AAAA;AAAA,IAGd,UAAA,EAAY,KAAA;AAAA,IACZ,GAAA,GAAM;AACF,MAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,EAAS;AAC7B,MAAA,IAAI,CAAC,MAAM,SAAA,EAAW;AAClB,QAAA,MAAM,IAAIA,WAAAA,CAAY,yCAAA,EAA2C,EAAE,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA;AAAA,MAC7F;AACA,MAAA,IAAI,CAAC,KAAA,CAAM,SAAA,CAAU,MAAA,EAAQ;AACzB,QAAA,MAAM,IAAIA,YAAY,0CAA0C,CAAA;AAAA,MACpE;AACA,MAAA,OAAO,MAAM,SAAA,CAAU,MAAA;AAAA,IAC3B;AAAA,GACH,CAAA;AACL;AAQA,IAAM,kBAAA,GAA2F;AAAA,EAC7F,QAAA,EAAU,qBAAA;AAAA,EACV,KAAA,EAAO;AACX,CAAA;AAEA,SAAS,YAAA,CACL,QACA,gBAAA,EACF;AACE,EAAA,OAAO,CAAwC,MAAA,KAAmE;AAC9G,IAAA,IAAI,YAAY,MAAA,EAAQ;AACpB,MAAA,MAAM,IAAI,KAAA;AAAA,QACN;AAAA,OAEJ;AAAA,IACJ;AAEA,IAAA,MAAM,KAAA,GAAQ,kBAAwB,CAAA;AAEtC,IAAA,MAAM,SAAA,GAAqC,EAAE,MAAA,EAAQ,KAAA,EAAM;AAC3D,IAAA,KAAA,MAAW,QAAQ,gBAAA,EAAkB;AACjC,MAAA,kBAAA,CAAmB,SAAA,EAAW,MAAM,KAAK,CAAA;AACzC,MAAA,SAAA,CAAU,kBAAA,CAAmB,IAAI,CAAC,CAAA,GAAI,KAAA,CAAM,eAAA;AAAA,IAChD;AAEA,IAAA,OAAO,WAAA,CAAY,YAAA,CAAa,MAAA,EAAQ,SAAS,CAAA,EAAG,MAAM,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA,EAAG,CAAA;AAAA,EAGrF,CAAA;AACJ;AAkCO,SAAS,aAAa,MAAA,EAA4B;AACrD,EAAA,OAAO,YAAA,CAML,MAAA,EAAQ,CAAC,OAAA,EAAS,UAAU,CAAC,CAAA;AACnC;AA+BO,SAAS,eAAe,MAAA,EAA4B;AACvD,EAAA,OAAO,YAAA,CAAoF,MAAA,EAAQ,CAAC,UAAU,CAAC,CAAA;AACnH;AA+BO,SAAS,YAAY,MAAA,EAA4B;AACpD,EAAA,OAAO,YAAA,CAA8E,MAAA,EAAQ,CAAC,OAAO,CAAC,CAAA;AAC1G;AAgCO,SAAS,oBAAoB,MAAA,EAA4B;AAC5D,EAAA,OAAO,YAAA,CAA+B,MAAA,EAAQ,EAAE,CAAA;AACpD","file":"index.react-native.mjs","sourcesContent":["import type { WalletStatus } from './types';\n\n/**\n * Reports whether a {@link WalletStatus} belongs to the initial auto-reconnect\n * \"warm-up\": `'pending'` (before auto-reconnect has resolved) and\n * `'reconnecting'` (silently re-establishing a persisted connection). During\n * this window a persisted wallet is on its way back, so wallet-dependent UI\n * should usually hold rather than flash a \"disconnected\" state.\n *\n * User-initiated `'connecting'` / `'disconnecting'` are deliberately excluded —\n * a normal connect or disconnect is not a warm-up and must stay interactive.\n *\n * This is the single source of truth for \"warm-up\" across the plugin: the\n * store's {@link WalletNamespace.whenReady} waits for the status to leave this set, and the\n * React `useIsWalletReady` hook and `WalletReadyGate` component gate on its\n * negation. Use it directly to gate on warm-up from a non-React consumer.\n *\n * @param status - A status from `client.wallet.getState().status`.\n * @returns `true` while the client is warming up, otherwise `false`.\n *\n * @example\n * ```ts\n * client.wallet.subscribe(() => {\n * const { status } = client.wallet.getState();\n * setLoading(isWalletWarmingUp(status));\n * });\n * ```\n *\n * @see {@link WalletNamespace.whenReady} — the imperative (promise-based) counterpart.\n */\nexport function isWalletWarmingUp(status: WalletStatus): boolean {\n return status === 'pending' || status === 'reconnecting';\n}\n","import {\n type ReadonlyUint8Array,\n type SignatureBytes,\n SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE,\n SOLANA_ERROR__WALLET__NOT_CONNECTED,\n SolanaError,\n} from '@solana/kit';\nimport { createSignerFromWalletAccount } from '@solana/wallet-account-signer';\nimport {\n SolanaSignIn,\n type SolanaSignInFeature,\n type SolanaSignInInput,\n type SolanaSignInOutput,\n SolanaSignMessage,\n type SolanaSignMessageFeature,\n} from '@solana/wallet-standard-features';\nimport { getWallets } from '@wallet-standard/app';\nimport {\n StandardConnect,\n type StandardConnectFeature,\n StandardDisconnect,\n type StandardDisconnectFeature,\n StandardEvents,\n type StandardEventsFeature,\n} from '@wallet-standard/features';\nimport type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';\nimport { getWalletAccountFeature, getWalletFeature } from '@wallet-standard/ui-features';\nimport { getOrCreateUiWalletForStandardWallet, getWalletForHandle } from '@wallet-standard/ui-registry';\n\nimport { isWalletWarmingUp } from './status';\nimport type {\n WalletActionOptions,\n WalletNamespace,\n WalletPluginConfig,\n WalletSigner,\n WalletState,\n WalletStatus,\n WalletStorage,\n} from './types';\n\n// -- Internal types ---------------------------------------------------------\n\n/** @internal */\nexport type WalletStore = WalletNamespace & {\n /**\n * Registers a listener notified only when the wallet signer may\n * have changed.\n * Returns an idempotent unsubscribe function.\n *\n * @internal\n */\n subscribeSigner: (listener: () => void) => () => void;\n [Symbol.dispose]: () => void;\n};\n\n// Flat internal state for easier updates\ntype WalletStoreState = {\n account: UiWalletAccount | null;\n connectedWallet: UiWallet | null;\n signer: WalletSigner | null;\n status: WalletStatus;\n wallets: readonly UiWallet[];\n};\n\n// -- Store ------------------------------------------------------------------\n\n/** @internal */\nexport function createWalletStore(config: WalletPluginConfig): WalletStore {\n // -- SSR guard: on the server, return an inert stub ---------------------\n\n if (!__BROWSER__ || __REACTNATIVE__) {\n const ssrSnapshot: WalletState = Object.freeze({\n connected: null,\n status: 'pending' as const,\n wallets: Object.freeze([]) as readonly UiWallet[],\n });\n return {\n connect: () =>\n Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' })),\n disconnect: () => Promise.resolve(),\n getState: () => ssrSnapshot,\n selectAccount: () => {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });\n },\n signIn: () => Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' })),\n signMessage: () =>\n Promise.reject(new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' })),\n subscribe: () => () => {},\n subscribeSigner: () => () => {},\n whenReady: () => Promise.resolve(),\n [Symbol.dispose]: () => {},\n };\n }\n\n // -- Browser-only initialization below this point ----------------------\n\n let state: WalletStoreState = {\n account: null,\n connectedWallet: null,\n signer: null,\n status: 'pending',\n wallets: [],\n };\n\n const listeners = new Set<() => void>();\n const signerListeners = new Set<() => void>();\n // One `standard:events change` subscription per discovered wallet, keyed by\n // name (the stable identity; the UiWallet handle regenerates on changes). The\n // underlying subscription attaches to the raw wallet's events, so it survives\n // handle regeneration — no need to resubscribe when a wallet's accounts change.\n const walletEventSubscriptions = new Map<string, () => void>();\n let reconnectCleanup: (() => void) | null = null;\n let userHasSelected = false;\n let connectGeneration = 0;\n let disposed = false;\n // Wallet that is in the process of disconnecting, stored to guard against re-connecting to it if a future connect fails\n let disconnectingWalletName: string | null = null;\n // Deferred backing `whenReady`. Created lazily while the status is transient\n // (see `isWalletWarmingUp`) and resolved by `updateState` once the status\n // settles, so `whenReady` can hand out a stable promise for the duration of\n // one warm-up episode (required by React Suspense).\n let readyDeferred: PromiseWithResolvers<void> | null = null;\n\n // Resolve storage: default to localStorage in browser, null to disable.\n // Merely *accessing* `localStorage` throws a `SecurityError` in sandboxed\n // iframes or when third-party storage is blocked, so the default resolution\n // degrades to `null` rather than letting the whole plugin-install chain\n // throw. This matches the best-effort persistence applied to reads/writes.\n function resolveDefaultStorage(): WalletStorage | null {\n try {\n return localStorage;\n } catch {\n return null;\n }\n }\n\n const storage: WalletStorage | null = config.storage === null ? null : (config.storage ?? resolveDefaultStorage());\n const storageKey = config.storageKey ?? 'kit-wallet';\n\n // -- State management --------------------------------------------------\n\n function deriveSnapshot(s: WalletStoreState): WalletState {\n return Object.freeze({\n connected:\n s.connectedWallet && s.account\n ? Object.freeze({\n account: s.account,\n signer: s.signer,\n wallet: s.connectedWallet,\n })\n : null,\n status: s.status,\n wallets: s.wallets,\n });\n }\n\n let snapshot: WalletState = deriveSnapshot(state);\n\n // We set `client.{payer,identity}` to `getState().connected?.signer\n // This is `undefined` if disconnected, `null` if connected to a read-only wallet,\n // or the signer reference otherwise.\n function observableSigner(s: WalletStoreState): WalletSigner | null | undefined {\n return s.connectedWallet && s.account ? s.signer : undefined;\n }\n\n function updateState(updates: Partial<WalletStoreState>): void {\n const prev = state;\n state = { ...state, ...updates };\n\n // Resolve `whenReady` the moment the status leaves the transient\n // warm-up set. `readyDeferred` is only ever created while transient (in\n // `whenReady`), so clearing it here lets the next transient episode arm\n // a fresh one.\n if (isWalletWarmingUp(prev.status) && !isWalletWarmingUp(state.status)) {\n readyDeferred?.resolve();\n readyDeferred = null;\n }\n\n if (state.wallets !== prev.wallets) {\n syncWalletEventSubscriptions(state.wallets);\n }\n\n // Signer subscribers (`subscribeToPayer` / `subscribeToIdentity`) fire\n // only when the observed signer changes.\n const signerChanged = observableSigner(state) !== observableSigner(prev);\n\n if (\n state.connectedWallet !== prev.connectedWallet ||\n state.account !== prev.account ||\n state.status !== prev.status ||\n state.signer !== prev.signer ||\n state.wallets !== prev.wallets\n ) {\n snapshot = deriveSnapshot(state);\n listeners.forEach(l => l());\n }\n\n if (signerChanged) {\n signerListeners.forEach(l => l());\n }\n }\n\n // Advanced: see the `whenReady` docblock on `WalletNamespace`. Returns a\n // stable, already-resolved promise once the initial warm-up has settled;\n // while transient, returns the same pending promise on every call so it is\n // safe to throw for React Suspense.\n function whenReady(): Promise<void> {\n if (!isWalletWarmingUp(state.status)) {\n return Promise.resolve();\n }\n readyDeferred ??= Promise.withResolvers<void>();\n return readyDeferred.promise;\n }\n\n // -- Signer creation ---------------------------------------------------\n\n function tryCreateSigner(account: UiWalletAccount): WalletSigner | null {\n try {\n // `createSignerFromWalletAccount` throws for chains the wallet\n // doesn't support — which we catch below. Non-Solana chains\n // therefore degrade to `signer: null`, matching the read-only\n // wallet contract.\n return createSignerFromWalletAccount(account, config.chain);\n } catch {\n // Wallet doesn't support signing (read-only / watch wallet, or a\n // non-Solana chain).\n return null;\n }\n }\n\n // -- Wallet discovery --------------------------------------------------\n\n // `getWallets()` is a wallet-standard function that returns the `Wallets` API\n // This includes a `get()` function to get the current list of wallets, and\n // an `on()` function to subscribe to wallet registration/unregistration events.\n const registry = getWallets();\n\n function filterWallet(uiWallet: UiWallet): boolean {\n const supportsChain = uiWallet.chains.includes(config.chain);\n const supportsConnect = uiWallet.features.includes(StandardConnect);\n if (!supportsChain || !supportsConnect) return false;\n return config.filter ? config.filter(uiWallet) : true;\n }\n\n function buildWalletList(): readonly UiWallet[] {\n return Object.freeze(registry.get().map(getOrCreateUiWalletForStandardWallet).filter(filterWallet));\n }\n\n // Rebuilds the filtered wallet list but returns the *existing* `state.wallets`\n // reference when the contents are unchanged. `buildWalletList` always\n // allocates a fresh frozen array, so without this a registry event for a\n // wallet that gets filtered out (or any event that doesn't alter the visible\n // set) would still produce a new snapshot and notify every listener — a\n // spurious re-render. Wallets are compared by reference, in order: the\n // wallet-ui registry hands back a stable UiWallet for a given underlying\n // wallet until its accounts/features/chains change, so reference equality is\n // a sound same-contents check here.\n function reconcileWalletList(): readonly UiWallet[] {\n const next = buildWalletList();\n const prev = state.wallets;\n if (next.length === prev.length && next.every((w, i) => w === prev[i])) {\n return prev;\n }\n return next;\n }\n\n // Reconcile the per-wallet event subscriptions against the current wallet set:\n // subscribe wallets that just appeared, unsubscribe those that left. Called\n // whenever `state.wallets` changes reference. Subscribing fires nothing, so\n // this never causes re-render churn. `wallets` is the already-filtered visible\n // list, so wallets excluded by chain/feature/`config.filter` are intentionally\n // never subscribed — a `register` / `unregister` event reconciles them in or\n // out of the list (and thus in or out of this map) when their eligibility\n // changes.\n function syncWalletEventSubscriptions(wallets: readonly UiWallet[]): void {\n const names = new Set(wallets.map(w => w.name));\n for (const [name, cleanup] of walletEventSubscriptions) {\n if (!names.has(name)) {\n cleanup();\n walletEventSubscriptions.delete(name);\n }\n }\n for (const w of wallets) {\n if (!walletEventSubscriptions.has(w.name)) {\n walletEventSubscriptions.set(w.name, subscribeToWalletEvents(w));\n }\n }\n }\n\n // True if a wallet matching `uiWallet` (by name) is currently registered and\n // passes the filter. Used to re-check membership after an awaited connect /\n // sign-in / silent reconnect resolves: the wallet may have unregistered (or\n // dropped a required feature/chain) while its prompt was open, and the\n // unregister handler can't tear down a connection that hasn't been\n // established yet. Compared by name — the same identity the unregister\n // handler uses — because the wallet-ui registry hands back a fresh UiWallet\n // reference once connect adds accounts, so reference equality would not hold.\n function isWalletStillAvailable(uiWallet: UiWallet): boolean {\n return buildWalletList().some(w => w.name === uiWallet.name);\n }\n\n updateState({ wallets: buildWalletList() });\n\n // Listen for new wallets being registered\n const unsubRegister = registry.on('register', () => {\n updateState({ wallets: reconcileWalletList() });\n });\n\n // Listen for wallets being unregistered — if the connected wallet is removed, disconnect\n const unsubUnregister = registry.on('unregister', () => {\n const newWallets = reconcileWalletList();\n const updates: Partial<WalletStoreState> = { wallets: newWallets };\n\n // If the connected wallet was unregistered, disconnect locally.\n if (state.connectedWallet && !newWallets.some(w => w.name === state.connectedWallet!.name)) {\n updates.connectedWallet = null;\n updates.account = null;\n updates.signer = null;\n updates.status = 'disconnected';\n clearPersistedAccount();\n }\n\n updateState(updates);\n });\n\n // -- Wallet-initiated events -------------------------------------------\n\n function cancelReconnect(): void {\n reconnectCleanup?.();\n reconnectCleanup = null;\n }\n\n function setConnected(account: UiWalletAccount, wallet: UiWallet, options?: { persist?: boolean }): void {\n // The store may have been disposed while a connect / sign-in / silent\n // reconnect awaited. The `connectGeneration` guard at each call site\n // doesn't cover the auto-connect IIFE resuming from its storage read —\n // it captures a fresh generation in `attemptSilentReconnect` after\n // disposal. Bail here so a disposed store never re-subscribes to wallet\n // events (a listener the already-run disposer can't clean up) or reports\n // itself connected.\n if (disposed) return;\n const signer = tryCreateSigner(account);\n disconnectingWalletName = null;\n // Reconcile `wallets` alongside the connection: `connect`/`signIn`/silent\n // reconnect authorize accounts on the underlying wallet, which regenerates\n // its registry handle. Without rebuilding the list here, `getState().wallets`\n // would keep the pre-connect handle (empty `.accounts`) until an unrelated\n // `standard:events change` happened to fire — so consumers reading the\n // just-connected wallet from `wallets` (e.g. rendering its authorized\n // accounts) would see it as unconnected. `reconcileWalletList` returns the\n // existing reference when the visible set is unchanged, keeping the snapshot\n // churn-free when connect didn't actually add accounts.\n updateState({\n account,\n connectedWallet: wallet,\n signer,\n status: 'connected',\n wallets: reconcileWalletList(),\n });\n if (options?.persist !== false) {\n persistAccount(account, wallet);\n }\n }\n\n function refreshUiWallet(staleUiWallet: UiWallet): UiWallet {\n const rawWallet = getWalletForHandle(staleUiWallet);\n return getOrCreateUiWalletForStandardWallet(rawWallet);\n }\n\n function subscribeToWalletEvents(uiWallet: UiWallet): () => void {\n if (!uiWallet.features.includes(StandardEvents)) {\n return () => {};\n }\n const eventsFeature = getWalletFeature(\n uiWallet,\n StandardEvents,\n ) as StandardEventsFeature[typeof StandardEvents];\n const walletName = uiWallet.name;\n\n // The handler intentionally ignores the changed-property flags\n // (`accounts`, `chains`, `features`) the wallet passes and instead\n // reconciles against the refreshed wallet. The signer is derived\n // entirely from the active account's features and chains, and the\n // wallet-ui registry regenerates the account reference precisely when\n // one of those changes — so the account reference, not the flags, is\n // the single source of truth for whether the signer is affected. Branch\n // on the reconciled state below, never on `properties`.\n return eventsFeature.on('change', () => {\n // Refresh the discovered list so this wallet's (and any others') accounts\n // are current; `updateState` re-syncs subscriptions for added/removed wallets.\n const newWallets = reconcileWalletList();\n\n // Non-active wallet: reflect its new account/feature state in the list only.\n if (!state.connectedWallet || state.connectedWallet.name !== walletName) {\n updateState({ wallets: newWallets });\n return;\n }\n\n // Active wallet: the original reconcile logic, unchanged.\n const refreshed = refreshUiWallet(state.connectedWallet);\n if (!filterWallet(refreshed)) {\n disconnectLocally();\n return;\n }\n const result = reconcileActiveAccount(refreshed);\n if (result === null) {\n disconnectLocally();\n return;\n }\n updateState({ connectedWallet: refreshed, wallets: newWallets, ...result });\n if (result.account) {\n persistAccount(result.account, refreshed);\n }\n });\n }\n\n function reconcileActiveAccount(wallet: UiWallet): Partial<WalletStoreState> | null {\n const newAccounts = wallet.accounts;\n\n // No accounts left — the caller disconnects.\n if (newAccounts.length === 0) return null;\n\n const currentAddress = state.account?.address;\n const stillPresent = currentAddress ? newAccounts.find(a => a.address === currentAddress) : null;\n const activeAccount = stillPresent ?? newAccounts[0];\n\n // Same reference — nothing the signer depends on changed, so leave the\n // signer untouched to preserve referential stability.\n if (activeAccount === state.account) return {};\n\n // The active account changed (switched, removed, or regenerated because\n // its features/chains changed) — recreate the signer for it.\n return { account: activeAccount, signer: tryCreateSigner(activeAccount) };\n }\n\n // -- Connection lifecycle ----------------------------------------------\n\n async function connect(uiWallet: UiWallet, options?: WalletActionOptions): Promise<readonly UiWalletAccount[]> {\n options?.abortSignal?.throwIfAborted();\n\n // Resolve the feature before mutating any state. getWalletFeature throws\n // WalletStandardError synchronously when the wallet doesn't support\n // standard:connect — doing this first means an unsupported wallet is a\n // no-op that never tears down an existing connection or cancels a\n // pending reconnect.\n const connectFeature = getWalletFeature(\n uiWallet,\n StandardConnect,\n ) as StandardConnectFeature[typeof StandardConnect];\n\n userHasSelected = true;\n cancelReconnect();\n const generation = ++connectGeneration;\n updateState({ status: 'connecting' });\n\n try {\n // Snapshot existing accounts before connect.\n const existingAccounts = [...uiWallet.accounts];\n\n await connectFeature.connect();\n\n // A newer connect/signIn was started while we were awaiting. Reject\n // with an `AbortError` so this superseded call settles in the\n // standard, ignorable way (matching the abort-signal contract) while\n // the newer request owns the connection. The `catch` below leaves\n // that newer connection untouched, since `generation` no longer\n // matches `connectGeneration`.\n if (generation !== connectGeneration) {\n throw new DOMException('Wallet connect was superseded by a newer connect or sign-in', 'AbortError');\n }\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while its connect prompt was open. The unregister\n // handler only disconnects an already-connected wallet, so without\n // this re-check the store would be left connected to a wallet absent\n // from `wallets`. Throw so the connection failure surfaces as a\n // rejection; the `catch` below reverts to any prior connection.\n if (!isWalletStillAvailable(uiWallet)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' });\n }\n\n // Refresh UiWallet to get updated accounts after connect.\n const refreshedWallet = refreshUiWallet(uiWallet);\n const allAccounts = refreshedWallet.accounts;\n\n // No authorized accounts means there's nothing to connect to. Throw\n // rather than resolving so a caller can't mistake an empty result for\n // a successful connection; the `catch` reverts to any prior wallet.\n if (allAccounts.length === 0) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'connect' });\n }\n\n // Prefer the first newly authorized account.\n const newAccount = allAccounts.find(a => !existingAccounts.some(e => e.address === a.address));\n const activeAccount = newAccount ?? allAccounts[0];\n\n setConnected(activeAccount, refreshedWallet);\n return allAccounts;\n } catch (error) {\n // Only act if we're still the active connection attempt; a\n // superseded attempt must leave the newer connection untouched.\n if (generation === connectGeneration) {\n revertToPreviousConnectionOrDisconnect();\n }\n throw error;\n }\n }\n\n async function disconnect(wallet?: UiWallet, options?: WalletActionOptions): Promise<void> {\n options?.abortSignal?.throwIfAborted();\n\n const target = wallet ?? state.connectedWallet;\n\n // Nothing to disconnect (no arg, nothing active): cancel any pending reconnect, and\n // supersede in-flight attempts.\n if (!target) {\n userHasSelected = true;\n cancelReconnect();\n connectGeneration++;\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const isActive = state.connectedWallet != null && state.connectedWallet.name === target.name;\n\n if (isActive) {\n const currentWallet = state.connectedWallet!;\n // Bump generation so this disconnect supersedes any in-flight connect/signIn.\n const generation = ++connectGeneration;\n disconnectingWalletName = currentWallet.name;\n updateState({ status: 'disconnecting' });\n try {\n if (currentWallet.features.includes(StandardDisconnect)) {\n const disconnectFeature = getWalletFeature(\n currentWallet,\n StandardDisconnect,\n ) as StandardDisconnectFeature[typeof StandardDisconnect];\n await disconnectFeature.disconnect();\n }\n } finally {\n if (generation === connectGeneration) {\n disconnectLocally();\n }\n }\n return;\n }\n\n // targeted disconnect but wallet gone: leave the active connection, status,\n // and persisted key untouched.\n if (!isWalletStillAvailable(target)) {\n return; // already gone / not authorized — forgiving no-op\n }\n const refreshedTarget = refreshUiWallet(target);\n if (!refreshedTarget.features.includes(StandardDisconnect)) {\n return; // can't deauthorize without standard:disconnect — no-op\n }\n // Guard against a concurrent select/connect into this wallet for the await;\n // restore the prior marker afterward (it may belong to an active disconnect).\n const prevDisconnecting = disconnectingWalletName;\n disconnectingWalletName = refreshedTarget.name;\n try {\n const disconnectFeature = getWalletFeature(\n refreshedTarget,\n StandardDisconnect,\n ) as StandardDisconnectFeature[typeof StandardDisconnect];\n await disconnectFeature.disconnect();\n } finally {\n if (disconnectingWalletName === refreshedTarget.name) {\n disconnectingWalletName = prevDisconnecting;\n }\n // Reflect the now-empty accounts immediately — but only if the store\n // is still live. If it was disposed while `standard:disconnect`\n // awaited, the disposer has already torn down every wallet-event\n // subscription and cleared the map; deauthorizing the target\n // regenerated its handle, so `reconcileWalletList` returns a fresh\n // array, and letting `updateState` run here would re-invoke\n // `syncWalletEventSubscriptions` and re-subscribe every wallet into\n // the cleared map — listeners the disposer can no longer clean up.\n if (!disposed) {\n updateState({ wallets: reconcileWalletList() });\n }\n }\n }\n\n function disconnectLocally(): void {\n disconnectingWalletName = null;\n\n updateState({\n account: null,\n connectedWallet: null,\n signer: null,\n status: 'disconnected',\n // Reconcile the list too: when this runs from the change handler\n // because the connected wallet dropped the configured chain or\n // failed the filter, that wallet must leave `wallets` immediately\n // rather than linger until an unrelated register/unregister event.\n // `reconcileWalletList` returns the existing reference when the\n // visible set is unchanged (the common case for a plain disconnect),\n // so this stays churn-free.\n wallets: reconcileWalletList(),\n });\n\n clearPersistedAccount();\n }\n\n // A `connect`/`signIn` attempt failed to establish a new connection. While\n // such an attempt is in flight only `status` is moved to 'connecting' — the\n // existing connection's account, signer, event subscription, and persisted\n // key are untouched until `setConnected` runs on success. So if a prior\n // connection is still intact, revert to it (declining the new wallet must not\n // log the user out of the one they had). Otherwise there's nothing to keep,\n // so disconnect cleanly. The active-attempt (`generation`) guard at each call\n // site ensures a superseded attempt never reverts the connection a newer one\n // owns.\n function revertToPreviousConnectionOrDisconnect(): void {\n // Don't revive a wallet with disconnect in progress\n if (state.connectedWallet && state.account && state.connectedWallet.name !== disconnectingWalletName) {\n updateState({ status: 'connected' });\n } else {\n disconnectLocally();\n }\n }\n\n function selectAccount(account: UiWalletAccount): void {\n // Derive the owning wallet from the account handle. `getWalletForHandle`\n // keys a WeakMap on handle identity, so this is unambiguous even when the\n // same address is authorized in two wallets. A stale/unregistered handle\n // throws — surface it as ACCOUNT_NOT_AVAILABLE.\n let owner: UiWallet;\n try {\n owner = getOrCreateUiWalletForStandardWallet(getWalletForHandle(account));\n } catch {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n // The owner must be currently authorized (discovered + passes the filter).\n if (!isWalletStillAvailable(owner)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n // Reject selecting into a wallet that is mid-disconnect.\n if (owner.name === disconnectingWalletName) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'selectAccount' });\n }\n\n const refreshed = refreshUiWallet(owner);\n const selectedAccount = refreshed.accounts.find(a => a.address === account.address);\n if (!selectedAccount) {\n throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_AVAILABLE, {\n account: account.address,\n operation: 'selectAccount',\n });\n }\n\n userHasSelected = true;\n // Supersede any in-flight connect/signIn.\n ++connectGeneration;\n\n // No-op fast path: the same account is already active.\n if (selectedAccount === state.account && refreshed === state.connectedWallet) {\n updateState({ status: 'connected' });\n return;\n }\n\n // Switch the active connection (owner may differ from the current one). The\n // previously-active wallet is left authorized — we never disconnect it. Its\n // event subscription persists, so switching back stays live.\n const signer = tryCreateSigner(selectedAccount);\n updateState({ account: selectedAccount, connectedWallet: refreshed, signer, status: 'connected' });\n persistAccount(selectedAccount, refreshed);\n }\n\n // -- Message signing ---------------------------------------------------\n\n async function signMessage(message: ReadonlyUint8Array, options?: WalletActionOptions): Promise<SignatureBytes> {\n options?.abortSignal?.throwIfAborted();\n const { connectedWallet, account } = state;\n if (!connectedWallet || !account) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signMessage' });\n }\n // Use the wallet feature directly rather than going through the cached\n // signer. This decouples message signing from transaction signing —\n // a wallet that supports solana:signMessage but not transaction signing\n // still works. solana:signMessage is an account-scoped feature, so this\n // resolves it against the active account: getWalletAccountFeature throws\n // a WalletStandardError when either the wallet or the account does not\n // support it.\n const signMessageFeature = getWalletAccountFeature(\n account,\n SolanaSignMessage,\n ) as SolanaSignMessageFeature[typeof SolanaSignMessage];\n // The wallet-standard `SolanaSignMessageInput.message` is a mutable `Uint8Array`; signing\n // only reads the bytes, so coerce the read-only input at this single boundary.\n const [output] = await signMessageFeature.signMessage({ account, message: message as Uint8Array });\n return output.signature as SignatureBytes;\n }\n\n // -- Sign In With Solana (SIWS-as-connect) ------------------------------\n\n async function signIn(\n uiWallet: UiWallet,\n input: SolanaSignInInput,\n options?: WalletActionOptions,\n ): Promise<SolanaSignInOutput> {\n options?.abortSignal?.throwIfAborted();\n\n // Resolve the feature before mutating any state. getWalletFeature throws\n // WalletStandardError synchronously when the wallet doesn't support\n // solana:signIn — doing this first means an unsupported wallet is a\n // no-op that never tears down an existing connection.\n const signInFeature = getWalletFeature(uiWallet, SolanaSignIn) as SolanaSignInFeature[typeof SolanaSignIn];\n\n userHasSelected = true;\n cancelReconnect();\n const generation = ++connectGeneration;\n updateState({ status: 'connecting' });\n\n try {\n const [result] = await signInFeature.signIn(input);\n\n // A newer connect/signIn was started while we were awaiting. Reject\n // with an `AbortError` (matching the abort-signal contract) so the\n // superseded call settles in the standard, ignorable way while the\n // newer request owns the connection.\n if (generation !== connectGeneration) {\n throw new DOMException('Wallet sign-in was superseded by a newer connect or sign-in', 'AbortError');\n }\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while its sign-in prompt was open. Throw rather than\n // resolving while the store is left connected to no wallet (or a\n // different one); the `catch` below reverts to any prior connection.\n if (!isWalletStillAvailable(uiWallet)) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' });\n }\n\n // Set up full connection state using the account from the sign-in response.\n const refreshedWallet = refreshUiWallet(uiWallet);\n const activeAccount = refreshedWallet.accounts.find(a => a.address === result.account.address);\n\n if (!activeAccount) {\n // The wallet signed in with an account it doesn't expose — a\n // protocol violation that can't be mapped to a connection. Throw\n // so the failure surfaces as a rejection rather than resolving\n // with a sign-in result the store can't act on; the `catch`\n // reverts to any prior connection.\n throw new SolanaError(SOLANA_ERROR__WALLET__NOT_CONNECTED, { operation: 'signIn' });\n }\n\n setConnected(activeAccount, refreshedWallet);\n return result;\n } catch (error) {\n if (generation === connectGeneration) {\n revertToPreviousConnectionOrDisconnect();\n }\n throw error;\n }\n }\n\n // -- Persistence -------------------------------------------------------\n\n function ignoreStorageFailure(operation: () => Promise<void> | void): void {\n void (async () => {\n try {\n await operation();\n } catch {\n // Persistence is best-effort; wallet state is the source of truth.\n }\n })();\n }\n\n function persistAccount(account: UiWalletAccount, wallet: UiWallet): void {\n if (!storage) return;\n ignoreStorageFailure(() => storage.setItem(storageKey, `${wallet.name}:${account.address}`));\n }\n\n function clearPersistedAccount(): void {\n if (!storage) return;\n ignoreStorageFailure(() => storage.removeItem(storageKey));\n }\n\n // -- Auto-connect ------------------------------------------------------\n\n if (config.autoConnect !== false && storage) {\n const generation = connectGeneration;\n (async () => {\n const savedKey = await storage.getItem(storageKey);\n // Don't auto-connect if the user has selected a wallet, or if the\n // store was disposed while the storage read was in flight — the\n // disposer has already torn down its listeners and bumped the\n // generation, but this IIFE hasn't captured one yet, so without this\n // check it would resume and silently reconnect into a disposed store.\n if (userHasSelected || disposed) return;\n\n if (!savedKey) {\n updateState({ status: 'disconnected' });\n return;\n }\n\n const separatorIndex = savedKey.lastIndexOf(':');\n if (separatorIndex === -1) {\n // Malformed saved key.\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const walletName = savedKey.slice(0, separatorIndex);\n const savedAddress = savedKey.slice(separatorIndex + 1);\n const existing = state.wallets.find(w => w.name === walletName);\n\n if (existing) {\n await attemptSilentReconnect(savedAddress, existing);\n } else if (\n registry.get().some(w => {\n const ui = getOrCreateUiWalletForStandardWallet(w);\n return ui.name === walletName;\n })\n ) {\n // Wallet registered but doesn't pass the filter.\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n } else {\n // Wallet not registered yet — wait for it to appear.\n updateState({ status: 'reconnecting' });\n\n // Change state to disconnected after 3s\n // Note that the listener stays alive until reconnect is cancelled, so this is\n // just affecting how long the UI shows reconnecting\n const statusTimeout = setTimeout(() => {\n if (!userHasSelected && state.status === 'reconnecting') {\n updateState({ status: 'disconnected' });\n }\n }, 3000);\n\n const unsubRegisterForReconnect = registry.on('register', () => {\n const generation = connectGeneration;\n void (async () => {\n if (userHasSelected) {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n return;\n }\n const found = buildWalletList().find(w => w.name === walletName);\n if (found) {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n await attemptSilentReconnect(savedAddress, found);\n } else if (\n registry.get().some(w => {\n const ui = getOrCreateUiWalletForStandardWallet(w);\n return ui.name === walletName;\n })\n ) {\n // Wallet registered but filtered out.\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n reconnectCleanup = null;\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n }\n })().catch(() => {\n // Reconnect failed — fall back to disconnected.\n if (generation === connectGeneration && !userHasSelected) {\n updateState({ status: 'disconnected' });\n }\n });\n });\n\n reconnectCleanup = () => {\n clearTimeout(statusTimeout);\n unsubRegisterForReconnect();\n };\n }\n })().catch(() => {\n // Storage read failed — fall back to disconnected.\n if (generation === connectGeneration && !userHasSelected) {\n updateState({ status: 'disconnected' });\n }\n });\n } else {\n // No auto-connect: immediately transition from 'pending' to 'disconnected'.\n updateState({ status: 'disconnected' });\n }\n\n async function attemptSilentReconnect(savedAddress: string, uiWallet: UiWallet): Promise<void> {\n const generation = ++connectGeneration;\n updateState({ status: 'reconnecting' });\n\n try {\n const connectFeature = getWalletFeature(\n uiWallet,\n StandardConnect,\n ) as StandardConnectFeature[typeof StandardConnect];\n await connectFeature.connect({ silent: true });\n\n // A newer connect/signIn was started while we were awaiting — bail.\n if (generation !== connectGeneration) return;\n\n // The wallet may have unregistered (or dropped a required\n // feature/chain) while the silent reconnect was in flight.\n if (!isWalletStillAvailable(uiWallet)) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n const refreshedWallet = refreshUiWallet(uiWallet);\n const allAccounts = refreshedWallet.accounts;\n\n if (allAccounts.length === 0) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n return;\n }\n\n // Restore the specific saved account, fall back to first from same wallet.\n const activeAccount = allAccounts.find(a => a.address === savedAddress) ?? allAccounts[0];\n\n // Persist only when we fell back to a different account\n setConnected(activeAccount, refreshedWallet, {\n persist: activeAccount.address !== savedAddress,\n });\n } catch {\n if (generation === connectGeneration) {\n updateState({ status: 'disconnected' });\n clearPersistedAccount();\n }\n }\n }\n\n // -- Public API --------------------------------------------------------\n\n return {\n connect,\n disconnect,\n getState: () => snapshot,\n selectAccount,\n signIn,\n signMessage,\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n subscribeSigner: (listener: () => void) => {\n signerListeners.add(listener);\n return () => {\n signerListeners.delete(listener);\n };\n },\n whenReady,\n [Symbol.dispose]: () => {\n // Invalidate any in-flight connect/signIn/silent-reconnect so they\n // bail instead of resuming after disposal (which would re-subscribe\n // to wallet events and re-arm cleanup that this disposer already\n // ran). The generation bump covers attempts that already captured a\n // generation; `disposed` covers the auto-connect IIFE, which resumes\n // from its storage read and captures a fresh generation afterward.\n disposed = true;\n connectGeneration++;\n unsubRegister();\n unsubUnregister();\n for (const cleanup of walletEventSubscriptions.values()) {\n cleanup();\n }\n walletEventSubscriptions.clear();\n cancelReconnect();\n listeners.clear();\n signerListeners.clear();\n // A store disposed mid-warm-up will never finish warming up, so\n // settle the status to 'disconnected'.\n // Runs after the listener sets are cleared, so no one is notified of\n // the final transition.\n if (isWalletWarmingUp(state.status)) {\n updateState({ status: 'disconnected' });\n }\n },\n };\n}\n","import {\n type ClientWithIdentity,\n type ClientWithPayer,\n type ClientWithSubscribeToIdentity,\n type ClientWithSubscribeToPayer,\n extendClient,\n SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED,\n SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE,\n SolanaError,\n withCleanup,\n} from '@solana/kit';\n\nimport { createWalletStore } from './store';\nimport type { ClientWithWallet, WalletPluginConfig } from './types';\n\n// -- Internal helpers ---------------------------------------------------------\n\nfunction defineSignerGetter(\n additions: Record<string, unknown>,\n property: string,\n store: ReturnType<typeof createWalletStore>,\n): void {\n Object.defineProperty(additions, property, {\n configurable: true,\n // Non-enumerable on purpose: this getter throws when no signer is connected, so we\n // don't want things like Object.entries to read it.\n enumerable: false,\n get() {\n const state = store.getState();\n if (!state.connected) {\n throw new SolanaError(SOLANA_ERROR__WALLET__NO_SIGNER_CONNECTED, { status: state.status });\n }\n if (!state.connected.signer) {\n throw new SolanaError(SOLANA_ERROR__WALLET__SIGNER_NOT_AVAILABLE);\n }\n return state.connected.signer;\n },\n });\n}\n\ntype SignerProperties = 'payer' | 'identity';\n\n// Maps each signer capability to the Kit-convention reactive hook installed\n// alongside it (`@solana/plugin-interfaces`). A plugin whose `client.payer` /\n// `client.identity` is dynamic installs the matching `subscribeTo*` function\n// so reactive consumers can observe changes without naming this plugin.\nconst SUBSCRIBE_PROPERTY: Record<SignerProperties, 'subscribeToIdentity' | 'subscribeToPayer'> = {\n identity: 'subscribeToIdentity',\n payer: 'subscribeToPayer',\n};\n\nfunction createPlugin<TAdditions extends ClientWithWallet>(\n config: WalletPluginConfig,\n signerProperties: SignerProperties[],\n) {\n return <T extends object & { wallet?: never }>(client: T): Disposable & Omit<T, keyof TAdditions> & TAdditions => {\n if ('wallet' in client) {\n throw new Error(\n 'Only one wallet plugin can be used per client. ' +\n 'Use walletSigner, walletPayer, walletIdentity, or walletWithoutSigner — not multiple.',\n );\n }\n\n const store = createWalletStore(config);\n\n const additions: Record<string, unknown> = { wallet: store };\n for (const prop of signerProperties) {\n defineSignerGetter(additions, prop, store);\n additions[SUBSCRIBE_PROPERTY[prop]] = store.subscribeSigner;\n }\n\n return withCleanup(extendClient(client, additions), () => store[Symbol.dispose]()) as unknown as Disposable &\n Omit<T, keyof TAdditions> &\n TAdditions;\n };\n}\n\n// -- Public API ---------------------------------------------------------------\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to both `client.payer` and `client.identity`.\n *\n * This is the most common entrypoint for dApps. When a signing-capable\n * wallet is connected, `client.payer` and `client.identity` both return the\n * wallet signer. When disconnected or read-only, accessing either throws.\n *\n * Because the signer is dynamic, both `client.subscribeToPayer` and\n * `client.subscribeToIdentity` are installed so reactive consumers can observe\n * changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(walletSigner({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletPayer}\n * @see {@link walletIdentity}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletSigner(config: WalletPluginConfig) {\n return createPlugin<\n ClientWithIdentity &\n ClientWithPayer &\n ClientWithSubscribeToIdentity &\n ClientWithSubscribeToPayer &\n ClientWithWallet\n >(config, ['payer', 'identity']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to `client.identity`.\n *\n * Use this when `client.payer` is controlled by a separate `payer()` plugin\n * (e.g. a backend relayer pays fees, but the user's wallet is the identity).\n *\n * Because the signer is dynamic, `client.subscribeToIdentity` is installed so\n * reactive consumers can observe changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(payer(relayerKeypair))\n * .use(walletIdentity({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletPayer}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletIdentity(config: WalletPluginConfig) {\n return createPlugin<ClientWithIdentity & ClientWithSubscribeToIdentity & ClientWithWallet>(config, ['identity']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard — and syncs the\n * connected wallet's signer to `client.payer`.\n *\n * Use this when you need the wallet as the fee payer but don't need\n * `client.identity`. For most dApps, prefer {@link walletSigner} which\n * sets both.\n *\n * Because the signer is dynamic, `client.subscribeToPayer` is installed so\n * reactive consumers can observe changes (the Kit reactive-capability convention).\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(walletPayer({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletIdentity}\n * @see {@link walletWithoutSigner}\n * @see {@link WalletPluginConfig}\n */\nexport function walletPayer(config: WalletPluginConfig) {\n return createPlugin<ClientWithPayer & ClientWithSubscribeToPayer & ClientWithWallet>(config, ['payer']);\n}\n\n/**\n * A framework-agnostic Kit plugin that manages wallet discovery, connection\n * lifecycle, and signer creation using wallet-standard.\n *\n * Adds the `wallet` namespace to the client without setting `client.payer`\n * or `client.identity`. Use this alongside separate `payer()` and/or\n * `identity()` plugins, or when the wallet's signer is used explicitly in\n * instructions. For most dApps, prefer {@link walletSigner} instead.\n *\n * **SSR-safe.** Can be included in a shared client chain that runs on both\n * server and browser.\n *\n * ```ts\n * const client = createClient()\n * .use(payer(backendKeypair))\n * .use(walletWithoutSigner({ chain: 'solana:mainnet' }))\n * .use(solanaRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' }))\n * .use(planAndSendTransactions());\n *\n * // client.payer is always backendKeypair\n * // client.wallet.getState().connected?.signer for manual use\n * ```\n *\n * @param config - Plugin configuration.\n *\n * @see {@link walletSigner}\n * @see {@link walletPayer}\n * @see {@link walletIdentity}\n * @see {@link WalletPluginConfig}\n */\nexport function walletWithoutSigner(config: WalletPluginConfig) {\n return createPlugin<ClientWithWallet>(config, []);\n}\n"]}
|