Skip to content

fix(store): do not restore a NotExist wallet state from localStorage - #51

Merged
pyramation merged 1 commit into
hyperweb-io:mainfrom
busydonna:fix/store-restore-not-exist
Oct 23, 2026
Merged

pyramation merged 1 commit into
hyperweb-io:mainfrom
busydonna:fix/store-restore-not-exist

Conversation

@busydonna

Copy link
Copy Markdown
Contributor

Problem

If a browser extension wallet hasn't injected its client by the time document.readyState reaches complete, getClientFromExtension() throws Client not exist. This happens with Keplr's MV3 build, which adds window.keplr from a service-worker-registered script, typically on a cold browser start. When it does, the wallet stays in that state on that browser for good:

  1. WalletStore.init() catches the error and sets walletState: NotExist for that wallet on every chain.
  2. The store subscriber saves this to localStorage (interchain-kit-store).
  3. On every later page load, restore() brings the saved chainWalletStates back as they are. A successful init() never resets them.
  4. While the entry is NotExist, ChainWalletStore.connect() returns early, so connecting does nothing.

The modal shows " Not Installed" and asks the user to refresh, but refreshing can't fix it. The only way out is clearing the site's storage.

Separately, ChainWalletStore's walletState and errorMessage getters call getChainWalletState(this.chain.chainName, this.wallet.info.name). The store's signature is getChainWalletState(walletName, chainName), so the lookup never matches: walletState always returns NotExist and errorMessage always returns ''. The existing unit test asserted the swapped order, so it never caught this.

Fix

  • restore() turns a saved NotExist entry into Disconnected (and clears its error message). If the extension really is missing, init() sets NotExist again on that load, so the "not installed" view still works.
  • The two getters now pass (walletName, chainName).

Tests

  • chain-wallet-store.test.ts: the walletState test now expects ('keplr', 'cosmoshub'), and the errorMessage test checks the arguments too.
  • wallet-manager-store.test.ts: new test, "should restore a saved NotExist state as Disconnected". It checks the first setState call, which is the one that writes the restored state.

packages/store: 178/178 tests pass. Without the source change, these 3 tests fail.

Reproduce

  1. Open a dapp using interchain-kit, with Keplr installed.
  2. In DevTools, set walletState to "NotExist" for the keplr-extension entries in localStorage interchain-kit-store.
  3. Reload. Before this change, Keplr stays "Not Installed" and connect does nothing. After it, Keplr shows as Disconnected and connects normally.

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).
@pyramation
pyramation merged commit 6ff764e into hyperweb-io:main Oct 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants