Post 11

Wallet Connection in 2026: EIP-6963 and the Connector Layer

window.ethereum was always a race condition. EIP-6963 fixed multi-wallet discovery, WalletConnect v2 handles mobile, and the connector abstraction hides both. What is actually happening when a user clicks 'Connect', and how to get the edge cases right.

Dec 22, 2025/10 min readWeb3
ShareY
Wallet Connection in 2026: EIP-6963 and the Connector Layer

For years, connecting a wallet meant reading window.ethereum and hoping. If the user had two extension wallets installed, one would silently win the global, and 'Connect MetaMask' might open Rabby. EIP-6963 replaced that global free-for-all with a proper announcement protocol, and the connector layer in libraries like wagmi now hides the whole mess. Worth understanding what it is hiding.

EIP-6963: wallets announce themselves

Instead of fighting over window.ethereum, each injected wallet dispatches an event with its own provider, a name, an icon, and a unique id. The page asks for announcements on load and gets back a list.

the raw discovery (what your library does for you)
1type Wallet = {2  info: { uuid: string; name: string; icon: string; rdns: string };3  provider: EIP1193Provider;4};5 6function discoverWallets(): Promise<Wallet[]> {7  return new Promise((resolve) => {8    const found: Wallet[] = [];9    window.addEventListener("eip6963:announceProvider", (e: any) => {10      found.push(e.detail);11    });12    window.dispatchEvent(new Event("eip6963:requestProvider"));13    setTimeout(() => resolve(found), 100);   // give wallets a tick to answer14  });15}16// now you can render a real list — each with its own provider,17// no more guessing which one window.ethereum points at

The connector abstraction

A connector is a uniform interface over 'a way to get an EIP-1193 provider and an account'. Injected (6963), WalletConnect (QR / deep link), Coinbase Wallet SDK, a Safe app context, a passkey-based smart account — all the same shape to your code.

lib/wagmi.ts
1import { createConfig, http } from "wagmi";2import { mainnet, base } from "wagmi/chains";3import { injected, walletConnect, coinbaseWallet } from "wagmi/connectors";4 5export const config = createConfig({6  chains: [mainnet, base],7  connectors: [8    injected(),   // picks up every EIP-6963 wallet automatically9    walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),10    coinbaseWallet({ appName: "My App" }),11  ],12  transports: {13    [mainnet.id]: http(),14    [base.id]: http(),15  },16  ssr: true,   // don't touch window during SSR17});
components/connect.tsx
1"use client";2import { useConnect, useAccount, useDisconnect } from "wagmi";3 4export function Connect() {5  const { connectors, connect, status, error } = useConnect();6  const { address } = useAccount();7  const { disconnect } = useDisconnect();8 9  if (address) {10    return <button onClick={() => disconnect()}>{short(address)}</button>;11  }12 13  return (14    <div>15      {connectors.map((c) => (16        <button key={c.uid} onClick={() => connect({ connector: c })}17                disabled={status === "pending"}>18          {c.icon && <img src={c.icon} alt="" width={20} />}19          {c.name}20        </button>21      ))}22      {error && <p>{friendlyConnectError(error)}</p>}23    </div>24  );25}

The edge cases that matter

SituationWhat to do
No wallet installedShow install links, not a broken 'Connect' button. Detect: connectors list is empty of injected.
User rejects the connectionerror.name === 'UserRejectedRequestError' — that's a normal outcome, not an error toast
Wallet lockedThe connect promise hangs until they unlock. Show 'Check your wallet' and a cancel.
Multiple accountsYou get the first. Listen for accountsChanged and update — wagmi does this.
Reconnect on reloadconfig.storage + wagmi's autoConnect. Don't prompt again if they were connected.
Running inside a Safe / FrameDetect the context and offer that connector first, or auto-connect.

Persist and reconnect without a prompt

app/providers.tsx
1"use client";2import { WagmiProvider, cookieToInitialState } from "wagmi";3import { config } from "@/lib/wagmi";4 5export function Providers({6  children, cookie,7}: { children: React.ReactNode; cookie: string | null }) {8  // hydrate connection state from the SSR cookie — no flash of9  // 'disconnected' on first paint, no re-prompt10  const initialState = cookieToInitialState(config, cookie);11  return (12    <WagmiProvider config={config} initialState={initialState}>13      {children}14    </WagmiProvider>15  );16}

On a phone, the user is in Safari and their wallet is a separate app. WalletConnect v2 handles the handoff — a QR on desktop, a deep link on mobile that opens the wallet app, they approve, and the connection is relayed back. The relay is a dependency; if projectId is missing or the relay is down, mobile connection silently fails, so surface that state.

The connector layer means you write one 'Connect' button and it works for a browser extension, a mobile wallet, a hardware wallet, a Safe, and a smart account. Understanding EIP-6963 underneath it is what lets you debug the day one of them misbehaves.

Found this useful? Pass it on.

All posts

Want a custom write-up for your team? Get in touch.

Building something like this?

If a post here maps to a problem on your roadmap, that's usually a good sign we should talk.