Skip to content

Conversation

noahsaso
Copy link
Collaborator

@noahsaso noahsaso commented Aug 7, 2025

Related: #393

When you connect to chains, Cosmos Kit saves the accounts in local storage (in cosmos-kit@2:core//current-wallet). When visiting the site again and the wallet connection needs re-approval, it currently spams separate connection popups for each of the accounts stored in local storage, instead of doing a bulk connection like most wallets support. This PR aims to fix that to prevent the wallet spam.

The problem

Relevant code:

private _restoreAccounts = async () => {
const walletName =
// If Cosmiframe enabled, use it by default instead of stored wallet.
this.cosmiframeEnabled
? COSMIFRAME_WALLET_ID
: window.localStorage.getItem('cosmos-kit@2:core//current-wallet');
if (walletName) {
try {
const mainWallet = this.getMainWallet(walletName);
mainWallet.activate();
if (mainWallet.clientMutable.state === State.Done) {
const accountsStr = window.localStorage.getItem(
'cosmos-kit@2:core//accounts'
);
if (accountsStr && accountsStr !== '[]') {
const accounts: SimpleAccount[] = JSON.parse(accountsStr);
accounts.forEach((data) => {
const chainWallet = mainWallet
.getChainWalletList(false)
.find(
(w) =>
w.chainRecord.chain?.chain_id === data.chainId &&
w.namespace === data.namespace
);
chainWallet?.activate();
if (mainWallet.walletInfo.mode === 'wallet-connect') {
chainWallet?.setData(data);
chainWallet?.setState(State.Done);
}
});
mainWallet.setState(State.Done);
}
}
if (mainWallet.walletInfo.mode !== 'wallet-connect') {
await this._reconnect(walletName);
}
} catch (error) {
if (error instanceof WalletNotProvidedError) {
this.logger?.warn(error.message);
} else {
throw error;
}
}
}
};

and:
private _reconnect = async (
walletName: WalletName,
checkConnection = false
) => {
if (
checkConnection &&
this.getMainWallet(walletName)?.isWalletDisconnected
) {
return;
}
this.logger?.debug('[Event Emit] `refresh_connection` (manager)');
this.coreEmitter.emit('refresh_connection');
await this.getMainWallet(walletName).connect();
await this.getMainWallet(walletName)
.getChainWalletList(true)[0]
?.connect(true);
};

The problem with this code is that _restoreAccounts does the following:

  1. loads accounts from local storage
  2. activates all the chain wallets
  3. calls _reconnect

_reconnect then:

  1. calls connect on the wallet
  2. calls connect(sync = true) on a single chain wallet, broadcasting to all the other chain wallets that were activated in _restoreAccounts to connect

Each chain wallet individually receives this connection sync and fires off a connect call, triggering a bunch of individual pop ups for every single account in storage.

How useChains bulk connect works

The useChains hook knows how to connect to chains in bulk by setting beforeConnect and connectChains callbacks that intercept typical wallet connection:

walletRepo.wallets.forEach((wallet) => {
if (wallet.isModeExtension) {
wallet.callbacks.beforeConnect = async () => {
try {
await wallet.client?.enable?.(ids);
} catch (e) {
for (const repo of repos) {
await wallet.client?.addChain?.(repo.chainRecord)
}
await wallet.client?.enable?.(ids);
}
}
}
if (wallet.isModeWalletConnect) {
wallet.connectChains = async () => {
await wallet?.client?.connect?.(ids);
for (const name of names.filter((name) => name !== chainName)) {
await wallet.mainWallet
.getChainWallet(name)
.update({ connect: false });
}
};
}
});

That hook:

  1. attempts to enable all chains at once
  2. attempts to addChain for every chain in case a missing chain was the reason for the enable failure
  3. attempts to enable again

This works in the best case scenario that enable succeeds or is missing a chain and then succeeds, but it also leads to double prompting if the user rejects the initial attempt.

The solution

I took the logic from useChains, made it a bit more fail-safe (since account reconnection should be a seamless background procedure that doesn't cause the user any issues), and added it to the manager's account reconnection logic.

Now the manager:

  1. loads accounts from local storage
  2. activates all found chain wallets
  3. activates all wallet repos for the chain wallets (i have no clue what activate does but it seems necessary)
  4. attempts to enable all stored account chains at once with a valid chain wallet / wallet repo, BAILING IMMEDIATELY if the user rejects the attempt
  5. attempts to addChain if the enable failed due to any error other than a user rejection
  6. attempts to enable again if all the addChain calls succeeded, BAILING IMMEDIATELY if the user rejects the attempt
  7. calls _reconnect, ONLY if the user did not reject either of the enable attempts above, and ONLY with the chains loaded from the account storage

There is no way to predict/detect the error message when enable fails due to a missing chain, since wallet clients can throw whatever they want, so I think it's best case to just try to addChain as long as the user did not reject the enable (which we can predict/detect).

It definitely shouldn't call _reconnect if enable fails or else a user rejection will lead to the individual spam calls of each chain trying to connect again. And If enable succeeds, _reconnect will seamlessly set up the clients and sync connection state to the appropriate wallet clients.

I added the _reconnect logic that only connects to the specific chain wallets WITHOUT sync because I noticed a bug where if components on the page that use the useChain hook load first (before the manager's account restoration function runs), they activate these chain wallets, which then spams individual connection attempts for each of those other activated chains when restore runs, since they weren't in the restored accounts and thus were not part of the bulk enable call.

Questions

  1. Am I missing anything in the chain and/or wallet and/or chain wallet client activation/setup? I'm not exactly sure what's required when it comes to all the activate, setData, setState, and connect calls. It seems to work as-is, so I assume the answer is no.

  2. Is the final _reconnect still necessary? I assume yes because enable just prepares the wallet itself, and then _reconnect will call connect on the necessary internal clients which should seamlessly succeed since the wallet is already enabled.

    I have confirmed that _reconnect is a necessary final step.

  3. Like the _reconnect call at the end, should we only be running this autoconnection logic if the mode isn't wallet-connect?

@pyramation pyramation merged commit 7b74969 into main Aug 12, 2025
1 check 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