DEV Community

Cover image for Connecting Wallets the Right Way: wagmi v2 and EIP-6963
Danny Holloran
Danny Holloran

Posted on • Originally published at danholloran.me

Connecting Wallets the Right Way: wagmi v2 and EIP-6963

Originally published on danholloran.me


If you've ever built a dApp and had a user complain that "your site only shows MetaMask even though I have Coinbase Wallet installed," you've run headfirst into the window.ethereum problem. For years, every wallet browser extension raced to claim the same global, and whichever one loaded last won. The result was a constant arms race between wallet vendors and a genuinely terrible user experience.

EIP-6963 fixed that — and wagmi v2 makes the fix trivially easy to adopt. If you're still rolling your own wallet detection or using a library that hasn't caught up yet, this post is for you.

The window.ethereum Problem and How EIP-6963 Solves It

The original EIP-1193 standard gave wallets a single attachment point: window.ethereum. One slot, many wallets, inevitable collisions. MetaMask and Coinbase Wallet both inject themselves there; whoever ran last overwrites the other.

EIP-6963 introduces an event-based discovery protocol instead. On page load, your dApp fires a eip6963:requestProvider event. Each installed wallet extension that supports the spec responds by emitting an eip6963:announceProvider event carrying its own provider object and metadata (icon, name, RDNS identifier). Your app collects the responses and builds a list — every wallet, no conflicts.

You can implement this yourself, but the raw event wiring is boilerplate you don't need to own:

// Low-level EIP-6963 — illustrative only, use wagmi instead
const providers: EIP6963ProviderDetail[] = [];

window.addEventListener("eip6963:announceProvider", (event: CustomEvent) => {
  providers.push(event.detail);
});

window.dispatchEvent(new Event("eip6963:requestProvider"));
Enter fullscreen mode Exit fullscreen mode

The real-world issue is that you also need to handle providers announced after your request, de-duplicate by RDNS, manage lifecycle — it adds up. wagmi v2 handles all of it.

Setting Up wagmi v2 with Multi-Wallet Support

wagmi v2 replaced its ethers.js dependency with viem — a lightweight, fully typed Ethereum library — and rebuilt its connector system from scratch. EIP-6963 discovery is enabled by default with the injected() connector.

Install the stack:

npm install wagmi viem @tanstack/react-query
Enter fullscreen mode Exit fullscreen mode

Create your config:

// lib/wagmi.ts
import { createConfig, http } from "wagmi";
import { mainnet, sepolia } from "wagmi/chains";
import { injected, walletConnect, coinbaseWallet } from "wagmi/connectors";

export const config = createConfig({
  chains: [mainnet, sepolia],
  connectors: [
    injected(), // discovers ALL EIP-6963 wallets automatically
    walletConnect({ projectId: import.meta.env.VITE_WC_PROJECT_ID }),
    coinbaseWallet({ appName: "My dApp" }),
  ],
  transports: {
    [mainnet.id]: http(),
    [sepolia.id]: http(),
  },
});
Enter fullscreen mode Exit fullscreen mode

Wrap your app with the providers:

// main.tsx
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { config } from "./lib/wagmi";

const queryClient = new QueryClient();

export function App() {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <YourApp />
      </QueryClientProvider>
    </WagmiProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

That injected() connector does the EIP-6963 legwork automatically. When a user has three wallets installed, your connection UI will show all three.

Building a Wallet Connection Component

With the config in place, wagmi's hooks give you everything you need. Here's a minimal but fully functional wallet picker:

// components/WalletConnect.tsx
import { useConnect, useAccount, useDisconnect, useBalance } from "wagmi";
import { formatEther } from "viem";

export function WalletConnect() {
  const { connectors, connect, isPending } = useConnect();
  const { address, isConnected, chain } = useAccount();
  const { disconnect } = useDisconnect();
  const { data: balance } = useBalance({ address });

  if (isConnected && address) {
    return (
      <div>
        <p>
          {address.slice(0, 6)}{address.slice(-4)} on {chain?.name}
        </p>
        {balance && (
          <p>{parseFloat(formatEther(balance.value)).toFixed(4)} ETH</p>
        )}
        <button onClick={() => disconnect()}>Disconnect</button>
      </div>
    );
  }

  return (
    <div>
      {connectors.map((connector) => (
        <button
          key={connector.uid}
          onClick={() => connect({ connector })}
          disabled={isPending}
        >
          {connector.name}
        </button>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

A few things worth noting here. connectors is populated at runtime based on what's actually available in the browser — if the user has MetaMask and Phantom installed, both show up. The connector.uid is stable across renders so it's safe as a React key. And useBalance returns a bigint value, which is where viem's formatEther utility comes in — it handles the wei-to-ether conversion without floating-point surprises.

For sending transactions, viem's type-safe primitives plug directly into wagmi's useSendTransaction hook:

import { useSendTransaction } from "wagmi";
import { parseEther } from "viem";

const { sendTransaction } = useSendTransaction();

sendTransaction({
  to: "0xRecipientAddress",
  value: parseEther("0.01"),
});
Enter fullscreen mode Exit fullscreen mode

Where This Lands in Practice

EIP-6963 browser support is now effectively universal among modern wallet extensions — MetaMask, Coinbase Wallet, Phantom, Rabby, and most others have shipped it. Users with multiple wallets get a proper choice UI instead of a silent clobber. wagmi v2's default injected() connector handles the discovery without any extra configuration on your end.

If you're maintaining an older dApp that still reaches for window.ethereum directly, migrating to wagmi v2 is the cleanest upgrade path. The wagmi docs are thorough, and the TypeScript types throughout viem make the contract interactions significantly less error-prone than working with ethers.js or raw JSON-RPC calls.


This post was originally published on danholloran.me. Follow along there for more frontend and dev content.

Top comments (0)