From a3905af8a6b1ab0a61c8e07cd8c05a6369f102d1 Mon Sep 17 00:00:00 2001 From: Donna <308550712+busydonna@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:58:04 +0000 Subject: [PATCH] fix(store): do not restore a NotExist wallet state from localStorage When an extension wallet is injected after document.readyState reaches 'complete' (e.g. Keplr's MV3 build on a cold browser start), init() throws 'Client not exist' and WalletStore.init() marks the wallet NotExist on every chain. That state is persisted, and restore() brought it back on every later load while a successful init() never cleared it. Since ChainWalletStore.connect() returns early for NotExist, the wallet stayed 'not installed' and connect() did nothing until the user cleared site data. restore() now maps a saved NotExist to Disconnected; init() still sets NotExist on a load where the extension is really missing. Also fix the swapped getChainWalletState(walletName, chainName) arguments in the ChainWalletStore walletState/errorMessage getters, which made them always fall back to NotExist / '' (the existing test asserted the swapped order). --- .../__tests__/chain-wallet-store.test.ts | 3 +- .../__tests__/wallet-manager-store.test.ts | 33 +++++++++++++++++++ .../src/wallet-manager/chain-wallet-store.ts | 4 +-- .../wallet-manager/wallet-manager-store.ts | 7 +++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/store/__tests__/chain-wallet-store.test.ts b/packages/store/__tests__/chain-wallet-store.test.ts index 7f005171b..087e46599 100644 --- a/packages/store/__tests__/chain-wallet-store.test.ts +++ b/packages/store/__tests__/chain-wallet-store.test.ts @@ -128,7 +128,7 @@ describe('ChainWalletStore', () => { const walletState = chainWalletStore.walletState; expect(walletState).toBe(WalletState.Connected); - expect(mockInterchainStore.getChainWalletState).toHaveBeenCalledWith('cosmoshub', 'keplr'); + expect(mockInterchainStore.getChainWalletState).toHaveBeenCalledWith('keplr', 'cosmoshub'); }); it('should return NotExist when no state exists', () => { @@ -161,6 +161,7 @@ describe('ChainWalletStore', () => { const errorMessage = chainWalletStore.errorMessage; expect(errorMessage).toBe('Connection failed'); + expect(mockInterchainStore.getChainWalletState).toHaveBeenCalledWith('keplr', 'cosmoshub'); }); it('should return empty string when no state exists', () => { diff --git a/packages/store/__tests__/wallet-manager-store.test.ts b/packages/store/__tests__/wallet-manager-store.test.ts index 1fb41f5d0..d83717c5d 100644 --- a/packages/store/__tests__/wallet-manager-store.test.ts +++ b/packages/store/__tests__/wallet-manager-store.test.ts @@ -217,6 +217,39 @@ describe('WalletManagerStore', () => { currentChainName: savedState.currentChainName, }); }); + it('should restore a saved NotExist state as Disconnected', async () => { + const savedState: { chainWalletStates: ChainWalletState[]; currentWalletName: string; currentChainName: string } = { + chainWalletStates: [ + { + chainName: 'cosmoshub', + walletName: 'keplr', + walletState: WalletState.NotExist, + rpcEndpoint: '', + errorMessage: 'Client not exist', + account: undefined, + }, + ], + currentWalletName: '', + currentChainName: '', + }; + + mockLocalStorage.load.mockReturnValue(savedState); + + await walletManagerStore.init(); + + // restore() writes the saved state first; later calls only add or drop entries. + expect(mockInterchainStore.setState).toHaveBeenNthCalledWith(1, { + chainWalletStates: [ + { + ...savedState.chainWalletStates[0], + walletState: WalletState.Disconnected, + errorMessage: '', + }, + ], + currentWalletName: '', + currentChainName: '', + }); + }); }); describe('State Management', () => { diff --git a/packages/store/src/wallet-manager/chain-wallet-store.ts b/packages/store/src/wallet-manager/chain-wallet-store.ts index 9dada020f..f51244da3 100644 --- a/packages/store/src/wallet-manager/chain-wallet-store.ts +++ b/packages/store/src/wallet-manager/chain-wallet-store.ts @@ -25,11 +25,11 @@ export class ChainWalletStore extends BaseWallet { } get walletState(): WalletState { - return this.store.getChainWalletState(this.chain.chainName, this.wallet.info.name)?.walletState || WalletState.NotExist; + return this.store.getChainWalletState(this.wallet.info.name, this.chain.chainName)?.walletState || WalletState.NotExist; } get errorMessage(): string { - return this.store.getChainWalletState(this.chain.chainName, this.wallet.info.name)?.errorMessage || ''; + return this.store.getChainWalletState(this.wallet.info.name, this.chain.chainName)?.errorMessage || ''; } async init(): Promise { diff --git a/packages/store/src/wallet-manager/wallet-manager-store.ts b/packages/store/src/wallet-manager/wallet-manager-store.ts index 3a26ac587..1c1c05931 100644 --- a/packages/store/src/wallet-manager/wallet-manager-store.ts +++ b/packages/store/src/wallet-manager/wallet-manager-store.ts @@ -72,7 +72,12 @@ export class WalletManagerStore implements WalletManager { const oldState = this.localStorage.load(); this.store.setState({ - chainWalletStates: oldState.chainWalletStates || [], + // NotExist only means the extension was not injected yet on the load that saved it. + // Restoring it would block connect() for good, so start that wallet over as Disconnected; + // init() sets NotExist again if the extension is really missing. + chainWalletStates: (oldState.chainWalletStates || []).map(cws => cws.walletState === WalletState.NotExist + ? { ...cws, walletState: WalletState.Disconnected, errorMessage: '' } + : cws), currentWalletName: oldState.currentWalletName || '', currentChainName: oldState.currentChainName || '' });