Skip to content

switchChainAsync resolves inconsistently with MetaMask chain switch state #4600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
1 task done
matteocelani opened this issue Mar 27, 2025 · 5 comments
Open
1 task done

Comments

@matteocelani
Copy link

Check existing issues

Describe the bug

There is an inconsistency in the behavior of switchChainAsync when used with MetaMask. The function resolves before the actual chain switch is completed, leading to a mismatch between the Promise resolution and the actual chain state.

Key observations:

  1. Timing:

    • switchChainAsync resolves immediately after sending the request to MetaMask
    • The actual chain switch in MetaMask happens several seconds later
    • This creates a gap between Promise resolution and chain switch completion
  2. State Management:

    • UI updates show the new chain immediately
    • Hook values (useChainId, useAccount) maintain previous chain values
    • Console logs demonstrate the state mismatch during the switch process

Link to Minimal Reproducible Example

https://stackblitz.com/edit/new-wagmi-hrpyucft?file=src%2FApp.tsx

Steps To Reproduce

import { useState, useEffect } from 'react';
import { useChainId, useSwitchChain, useAccount } from 'wagmi';
import { arbitrum, base } from 'wagmi/chains';

export default function App() {
  const chainId = useChainId();
  const { chain: accountChain } = useAccount();
  const { switchChainAsync } = useSwitchChain();
  const [isLoading, setIsLoading] = useState(false);

  // Monitor chain changes
  useEffect(() => {
    console.log('Chain ID from useChainId:', chainId);
    console.log('Chain ID from useAccount:', accountChain?.id);
  }, [chainId, accountChain]);

  const testAsyncBehavior = async (targetChainId: number) => {
    console.log('=== Test Async Behavior Start ===');
    console.log('T0 - Initial state:');
    console.log('- ChainId from useChainId:', chainId);
    console.log('- ChainId from useAccount:', accountChain?.id);

    const switchPromise = switchChainAsync({ chainId: targetChainId });
    console.log('T1 - Switch request sent to wallet');

    const result = await switchPromise;
    console.log('T3 - switchChainAsync resolved with:', result);

    console.log('T4 - State after await:');
    console.log('- ChainId from useChainId:', chainId);
    console.log('- ChainId from useAccount:', accountChain?.id);

    return result;
  };

  const handleSwitchChain = async () => {
    try {
      setIsLoading(true);
      if (!chainId) return;

      const targetChainId = chainId === base.id ? arbitrum.id : base.id;
      await testAsyncBehavior(targetChainId);
    } catch (error) {
      console.error('Chain switch error:', error);
    } finally {
      setTimeout(() => {
        if (isLoading) setIsLoading(false);
      }, 5000);
    }
  };

  return (
    <div>
      <button onClick={handleSwitchChain} disabled={isLoading}>
        {isLoading
          ? 'Switching...'
          : `Switch to ${chainId === base.id ? 'Arbitrum' : 'Base'}`}
      </button>
      <div>
        <p>Current Chain (useChainId): {chainId === base.id ? 'Base' : 'Arbitrum'}</p>
        <p>Current Chain (useAccount): {accountChain?.id === base.id ? 'Base' : 'Arbitrum'}</p>
      </div>
    </div>
  );
}

Console output showing the issue:

=== Test Async Behavior Start ===
T0 - Initial state:
- ChainId from useChainId: 8453
- ChainId from useAccount: 8453
T1 - Switch request sent to wallet
T3 - switchChainAsync resolved with: {id: 42161, name: 'Arbitrum One', ...}
T4 - State after await:
- ChainId from useChainId: 8453    // Still showing old chain
- ChainId from useAccount: 8453    // Still showing old chain

What Wagmi package(s) are you using?

wagmi

Wagmi Package(s) Version(s)

[email protected]

Viem Version

2.24.1

TypeScript Version

5.7.3

Anything else?

Using:

  • MetaMask as wallet
  • Testing performed with Base (8453) and Arbitrum (42161) networks

The issue affects the reliability of chain-switching logic in dApps, as developers cannot depend on the Promise resolution to accurately reflect the actual chain state.

@tmm tmm closed this as not planned Won't fix, can't repro, duplicate, stale Mar 27, 2025
@tmm
Copy link
Member

tmm commented Mar 27, 2025

What connector are you using? MetaMask, Injected, EIP-6963, etc.

@matteocelani
Copy link
Author

As specified in the description and code, we are using MetaMask as the connector.
The issue has been verified in two scenarios:

  1. Direct MetaMask integration (as shown in the reproduction)
  2. Using RainbowKit with MetaMask as connector

The behavior is consistent in both cases: switchChainAsync resolves before MetaMask completes the actual chain switch.

@Chris0150
Copy link

This is an ongoing critical bug, why has this issue been closed?

@tmm tmm marked this as a duplicate of #4601 Mar 28, 2025
@tmm
Copy link
Member

tmm commented Mar 28, 2025

Nothing has changed with the metaMask connector code recently so this is likely an upstream wallet issue. Some questions:

  • What browser are you using?
  • What MetaMask version? Mobile or browser extension?

This is an ongoing critical bug, why has this issue been closed?

Not reproducible. (It's also not a security issue so not a critical bug as far as we are concerned.)

@matteocelani
Copy link
Author

matteocelani commented Mar 28, 2025

Hello @tmm

I see the issue was closed as "not reproducible," but the problem is easily observable using the provided example.

To clarify further:

  • Connector used: MetaMask (desktop browser extension version 12.13.1)
  • Browsers used: Mainly Arc (a Chromium-based browser), but the issue is identical on other Chromium-based browsers such as Chrome.

Please directly verify the problematic behavior using the reproducible example provided at this link:

https://stackblitz.com/edit/new-wagmi-hrpyucft?file=src%2FApp.tsx

Image

When you open the browser console after running this example, the problematic behavior becomes evident. Here's exactly what happens during the chain switch:

Detailed console log example during the switch from Base (chainId 8453) to Arbitrum (chainId 42161):

=== Test Async Behavior Start ===
T0 - Initial state:
- ChainId from useChainId: 8453
- ChainId from useAccount: 8453
T1 - Switch request sent to wallet
T2 - State immediately after request:
- ChainId from useChainId: 8453
- ChainId from useAccount: 8453
Chain ID from useChainId: 42161
Chain ID from useAccount: 42161
T3 - switchChainAsync resolved with: { id: 42161, name: 'Arbitrum One', … }
T4 - State after await:
- ChainId from useChainId: 8453  // still showing the previous value
- ChainId from useAccount: 8453  // still showing the previous value
=== Test Async Behavior End ===
T5 - State after 1 second:
- ChainId from useChainId: 8453
- ChainId from useAccount: 8453
T6 - State after 3 seconds:
- ChainId from useChainId: 8453
- ChainId from useAccount: 8453

The promise (switchChainAsync) resolves immediately, suggesting the chain switch is complete, but the actual chain state in wagmi remains unchanged for several more seconds, causing a significant inconsistency in our application's state management.

I'm available for further clarification or additional testing if needed.

Thank you.

@tmm tmm reopened this Mar 28, 2025
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

No branches or pull requests

3 participants