diff --git a/packages/client/src/client/actions.test.ts b/packages/client/src/client/actions.test.ts index ca00da0..b3a57cc 100644 --- a/packages/client/src/client/actions.test.ts +++ b/packages/client/src/client/actions.test.ts @@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { SolanaClientRuntime } from '../rpc/types'; import type { ClientActions } from '../types'; import { createWalletRegistry } from '../wallet/registry'; -import type { WalletConnector, WalletRegistry } from '../wallet/types'; +import type { WalletAccount, WalletConnector, WalletRegistry } from '../wallet/types'; import { createActions } from './actions'; import { createDefaultClientStore } from './createClientStore'; @@ -256,4 +256,57 @@ describe('client actions', () => { expect(signature).toBe(AIRDROP_SIGNATURE); expect(airdropFactoryMock).toHaveBeenCalled(); }); + + it('updates the store account when the user switches accounts in their wallet', async () => { + let accountsChangedListener: ((accounts: WalletAccount[]) => void) | undefined; + walletConnector.connect = vi.fn(async () => ({ + account: { address: ACCOUNT_ADDRESS, publicKey: new Uint8Array(32) }, + connector: { id: 'wallet-1', name: 'Wallet 1' }, + disconnect: vi.fn(async () => undefined), + signTransaction: vi.fn(), + onAccountsChanged: (listener: (accounts: WalletAccount[]) => void) => { + accountsChangedListener = listener; + return () => { + accountsChangedListener = undefined; + }; + }, + })); + + await actions.connectWallet('wallet-1'); + expect(store.getState().wallet.status).toBe('connected'); + + const NEW_ADDRESS = 'new-addr' as Address; + accountsChangedListener?.([{ address: NEW_ADDRESS, publicKey: new Uint8Array(32) }]); + + const state = store.getState(); + expect(state.wallet.status).toBe('connected'); + if (state.wallet.status === 'connected') { + expect(state.wallet.session.account.address).toBe(NEW_ADDRESS); + } + }); + + it('disconnects when the wallet reports no accounts via onAccountsChanged', async () => { + let accountsChangedListener: ((accounts: WalletAccount[]) => void) | undefined; + walletConnector.connect = vi.fn(async () => ({ + account: { address: ACCOUNT_ADDRESS, publicKey: new Uint8Array(32) }, + connector: { id: 'wallet-1', name: 'Wallet 1' }, + disconnect: vi.fn(async () => undefined), + signTransaction: vi.fn(), + onAccountsChanged: (listener: (accounts: WalletAccount[]) => void) => { + accountsChangedListener = listener; + return () => { + accountsChangedListener = undefined; + }; + }, + })); + + await actions.connectWallet('wallet-1'); + expect(store.getState().wallet.status).toBe('connected'); + + accountsChangedListener?.([]); + // disconnectWallet is called with void — drain all pending microtasks + await new Promise((resolve) => setTimeout(resolve)); + + expect(store.getState().wallet.status).toBe('disconnected'); + }); }); diff --git a/packages/client/src/client/actions.ts b/packages/client/src/client/actions.ts index a9ad435..5d6a8b7 100644 --- a/packages/client/src/client/actions.ts +++ b/packages/client/src/client/actions.ts @@ -199,6 +199,20 @@ export function createActions({ connectors, logger: inputLogger, runtime, store walletEventsCleanup?.(); walletEventsCleanup = undefined; void disconnectWallet(); + } else { + updateState(store, { + wallet: { + autoConnect: autoConnectPreference, + connectorId: resolvedConnectorId, + session: { ...session, account: accounts[0] }, + status: 'connected', + }, + }); + logger({ + data: { address: accounts[0].address.toString(), connectorId: resolvedConnectorId }, + level: 'info', + message: 'wallet account changed', + }); } }); } diff --git a/packages/client/src/wallet/standard.ts b/packages/client/src/wallet/standard.ts index fdee54c..61e1c6a 100644 --- a/packages/client/src/wallet/standard.ts +++ b/packages/client/src/wallet/standard.ts @@ -113,8 +113,14 @@ export type WalletStandardSessionOptions = Readonly<{ export function createWalletStandardSession(options: WalletStandardSessionOptions): WalletSession { const { account, defaultChain, disconnect, metadata, onAccountsChanged, wallet } = options; - let currentAccount = account; - let sessionAccount = toSessionAccount(currentAccount); + + // Mutable session state: updated by account change listeners. + // All signing operations read from this object to ensure they always + // use the most recent account after a wallet-side account switch. + const sessionState = { + currentAccount: account, + sessionAccount: toSessionAccount(account), + }; const signMessageFeature = wallet.features[SolanaSignMessage] as | SolanaSignMessageFeature[typeof SolanaSignMessage] @@ -126,12 +132,12 @@ export function createWalletStandardSession(options: WalletStandardSessionOption | SolanaSignAndSendTransactionFeature[typeof SolanaSignAndSendTransaction] | undefined; - const resolvedChain = defaultChain ?? getChain(currentAccount); + const resolvedChain = defaultChain ?? getChain(sessionState.currentAccount); const signMessage = signMessageFeature ? async (message: Uint8Array) => { const [output] = await signMessageFeature.signMessage({ - account: currentAccount, + account: sessionState.currentAccount, message, }); return output.signature; @@ -143,12 +149,12 @@ export function createWalletStandardSession(options: WalletStandardSessionOption const wireBytes = new Uint8Array(transactionEncoder.encode(transaction)); const request = resolvedChain ? { - account: currentAccount, + account: sessionState.currentAccount, chain: resolvedChain, transaction: wireBytes, } : { - account: currentAccount, + account: sessionState.currentAccount, transaction: wireBytes, }; const [output] = await signTransactionFeature.signTransaction(request); @@ -159,9 +165,9 @@ export function createWalletStandardSession(options: WalletStandardSessionOption const sendTransaction = signAndSendFeature ? async (transaction: SendableTransaction & Transaction, config?: Readonly<{ commitment?: Commitment }>) => { const wireBytes = new Uint8Array(transactionEncoder.encode(transaction)); - const chain: IdentifierString = defaultChain ?? getChain(currentAccount) ?? 'solana:mainnet-beta'; + const chain: IdentifierString = defaultChain ?? getChain(sessionState.currentAccount) ?? 'solana:mainnet-beta'; const [output] = await signAndSendFeature.signAndSendTransaction({ - account: currentAccount, + account: sessionState.currentAccount, chain, options: { commitment: mapCommitment(config?.commitment), @@ -187,8 +193,8 @@ export function createWalletStandardSession(options: WalletStandardSessionOption listener([]); return; } - currentAccount = accounts[0]; - sessionAccount = toSessionAccount(currentAccount); + sessionState.currentAccount = accounts[0]; + sessionState.sessionAccount = toSessionAccount(sessionState.currentAccount); listener(accounts.map(toSessionAccount)); }); changeUnsubscribe = off; @@ -200,7 +206,9 @@ export function createWalletStandardSession(options: WalletStandardSessionOption : undefined; return { - account: sessionAccount, + get account() { + return sessionState.sessionAccount; + }, connector: metadata, disconnect: disconnectSession, onAccountsChanged: handleAccountsChanged,