ort { createConfig, http } from 'wagmi';
import { mainnet, sepolia, polygon } from 'wagmi/chains';
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors';
const RPC_ENDPOINTS = {
[mainnet.id]: process.env.NEXT_PUBLIC_MAINNET_RPC ?? 'https://eth.llamarpc.com',
[sepolia.id]: process.env.NEXT_PUBLIC_SEPOLIA_RPC ?? 'https://rpc.sepolia.org',
[polygon.id]: process.env.NEXT_PUBLIC_POLYGON_RPC ?? 'https://polygon-rpc.com',
};
export const chainConfig = createConfig({
chains: [mainnet, sepolia, polygon],
connectors: [
injected(),
walletConnect({
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID ?? '',
metadata: {
name: 'Enterprise dApp',
description: 'Production-grade wallet interface',
url: 'https://app.example.com',
icons: ['https://app.example.com/icon.png'],
},
}),
coinbaseWallet({
appName: 'Enterprise dApp',
appLogoUrl: 'https://app.example.com/logo.svg',
}),
],
transports: {
[mainnet.id]: http(RPC_ENDPOINTS[mainnet.id]),
[sepolia.id]: http(RPC_ENDPOINTS[sepolia.id]),
[polygon.id]: http(RPC_ENDPOINTS[polygon.id]),
},
});
**Architecture Rationale:**
- `injected()` automatically subscribes to `eip6963:requestProvider` and `eip6963:announceProvider` events. It handles asynchronous provider arrival, RDNS-based deduplication, and cleanup on unmount.
- Explicit transport mapping prevents wagmi from falling back to default public RPCs, which often rate-limit or lack historical data.
- Environment variable fallbacks ensure local development remains functional without breaking production builds.
### Step 3: Wrap the Application Root
React Query and wagmi must share the same context tree to enable automatic cache invalidation on chain switches or account changes.
```typescript
// src/app/providers.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WagmiProvider } from 'wagmi';
import { chainConfig } from '@/ethereum/config';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 30,
retry: 2,
},
},
});
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={chainConfig}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}
Step 4: Build the Connection Interface
The UI layer should separate provider discovery from account state. A custom hook encapsulates the discovery logic, while the component handles rendering and user interaction.
// src/hooks/useProviderRegistry.ts
import { useConnect } from 'wagmi';
import { useMemo } from 'react';
export function useProviderRegistry() {
const { connectors, connect, isPending, error } = useConnect();
const availableProviders = useMemo(() => {
return connectors
.filter((c) => c.id !== 'walletConnect' && c.id !== 'coinbaseWalletSDK')
.map((c) => ({
id: c.uid,
label: c.name,
icon: c.icon,
rdns: c.id,
}));
}, [connectors]);
return {
providers: availableProviders,
connectProvider: (rdns: string) => {
const target = connectors.find((c) => c.id === rdns);
if (target) connect({ connector: target });
},
isLoading: isPending,
connectionError: error,
};
}
// src/components/NetworkSelector.tsx
import { useAccount, useDisconnect, useBalance } from 'wagmi';
import { formatEther } from 'viem';
import { useProviderRegistry } from '@/hooks/useProviderRegistry';
export function NetworkSelector() {
const { address, isConnected, chain } = useAccount();
const { disconnect } = useDisconnect();
const { data: nativeBalance } = useBalance({ address });
const { providers, connectProvider, isLoading, connectionError } = useProviderRegistry();
if (isConnected && address) {
return (
<section className="account-panel">
<header>
<span className="address">{address.slice(0, 6)}β¦{address.slice(-4)}</span>
<span className="network">{chain?.name ?? 'Unknown'}</span>
</header>
<div className="balance">
{nativeBalance && (
<span>{parseFloat(formatEther(nativeBalance.value)).toFixed(4)} ETH</span>
)}
</div>
<button onClick={() => disconnect()} className="btn-danger">
Terminate Session
</button>
</section>
);
}
return (
<section className="connection-panel">
<h2>Select Wallet</h2>
{connectionError && <p className="error">{connectionError.message}</p>}
<div className="provider-grid">
{providers.map((p) => (
<button
key={p.id}
onClick={() => connectProvider(p.rdns)}
disabled={isLoading}
className="provider-card"
>
<img src={p.icon} alt={p.label} width={24} height={24} />
<span>{p.label}</span>
</button>
))}
</div>
</section>
);
}
Why this structure works:
useProviderRegistry isolates wagmi's connector array from UI rendering, preventing unnecessary re-renders when non-injected connectors update.
- RDNS filtering (
c.id !== 'walletConnect') ensures the UI only displays EIP-6963-compliant injected wallets, keeping the interface clean.
formatEther from viem safely converts bigint wei values to decimal strings without floating-point precision loss, a common source of balance display bugs.
Pitfall Guide
1. Assuming Synchronous Provider Availability
Explanation: EIP-6963 relies on browser extension event listeners. Extensions may announce themselves 100β500ms after page load, especially on slower devices or when multiple extensions compete for CPU time. Polling window.ethereum or expecting immediate connector population will miss late announcers.
Fix: Rely on wagmi's injected() connector, which maintains a persistent event listener and updates the connectors array reactively. Never cache the provider list outside of React state.
2. Ignoring RDNS-Based Deduplication
Explanation: Some extensions announce multiple times during hot-reloads or tab restores. Without deduplication, the UI renders duplicate buttons, confusing users and triggering multiple connection attempts.
Fix: Use the connector's uid or id (RDNS format like io.metamask) as the React key and filter duplicates using a Set or Map before rendering. wagmi v2 handles this internally, but custom wrappers must replicate the logic.
3. Mishandling Wei/ETH Unit Conversions
Explanation: Blockchain balances and transaction values use bigint to preserve precision. JavaScript's Number type loses precision beyond 2^53 - 1, causing silent truncation in large balances or micro-transactions.
Fix: Always use viem's formatEther and parseEther utilities. Never pass raw bigint values directly to UI components or arithmetic operations without explicit conversion.
4. Hardcoding Fallback RPCs Without Transport Configuration
Explanation: wagmi defaults to public RPC endpoints when transports aren't explicitly defined. Public nodes frequently rate-limit, drop historical queries, or return stale blocks, causing useBalance or useReadContract to fail intermittently.
Fix: Map every chain ID to a dedicated transport using http() or webSocket(). Implement fallback chains in your transport configuration to maintain uptime during provider outages.
5. Not Handling Connector Deactivation Gracefully
Explanation: Wallet extensions can disconnect due to network changes, account switches, or security policies. If the dApp doesn't listen to disconnect events, the UI remains in a "connected" state while RPC calls silently fail.
Fix: Subscribe to useAccount's isConnected flag and implement a cleanup effect that resets local application state when the flag flips to false. Avoid manual window.ethereum.removeListener calls; let wagmi manage the lifecycle.
6. Overlooking Mobile Deep-Linking Requirements
Explanation: EIP-6963 only applies to desktop browser extensions. Mobile browsers lack extension injection, meaning injected() returns an empty array on iOS/Android. Users on mobile will see a blank connection screen.
Fix: Always pair injected() with walletConnect() and coinbaseWallet(). Implement device detection to conditionally render mobile-optimized QR codes or deep-link buttons alongside desktop extension lists.
7. Assuming Universal EIP-6963 Compliance
Explanation: While major wallets support the spec, niche or enterprise-grade extensions may still rely on legacy injection patterns. Blindly filtering out non-EIP-6963 providers can alienate institutional users.
Fix: Maintain a fallback detection layer that checks for window.ethereum only when the EIP-6963 registry is empty. Log a deprecation warning to console for debugging, but prioritize the event-driven path in production.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Desktop-only dApp with known user base | injected() only | Minimal bundle size, native UX, zero API keys | $0 (no third-party costs) |
| Cross-platform dApp (desktop + mobile) | injected() + walletConnect() | Covers extension and mobile deep-linking flows | ~$50β200/mo (WC project limits) |
| Enterprise/B2B application | injected() + coinbaseWallet() + custom SSO bridge | Supports institutional wallets and compliance requirements | Higher dev overhead, potential licensing |
| High-frequency trading interface | injected() + WebSocket transports | Reduces RPC latency, enables real-time block streaming | Increased node infrastructure costs |
Configuration Template
// src/ethereum/config.ts
import { createConfig, http, webSocket } from 'wagmi';
import { mainnet, sepolia, arbitrum, optimism } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
const isDev = process.env.NODE_ENV === 'development';
export const chainConfig = createConfig({
chains: [mainnet, sepolia, arbitrum, optimism],
connectors: [
injected(),
walletConnect({
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID ?? 'placeholder',
metadata: {
name: 'Production dApp',
description: 'EIP-6963 compliant wallet interface',
url: isDev ? 'http://localhost:3000' : 'https://app.production.com',
icons: ['https://app.production.com/favicon.ico'],
},
}),
],
transports: {
[mainnet.id]: isDev ? http() : webSocket(process.env.NEXT_PUBLIC_MAINNET_WS ?? ''),
[sepolia.id]: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC ?? ''),
[arbitrum.id]: http(process.env.NEXT_PUBLIC_ARB_RPC ?? ''),
[optimism.id]: http(process.env.NEXT_PUBLIC_OP_RPC ?? ''),
},
});
Quick Start Guide
- Initialize Project: Run
npm create vite@latest my-dapp -- --template react-ts and navigate into the directory.
- Install Core Dependencies: Execute
npm install wagmi viem @tanstack/react-query.
- Create Configuration File: Copy the Configuration Template into
src/ethereum/config.ts and populate environment variables in .env.local.
- Wrap Application: Import
AppProviders into your root component (main.tsx or app/layout.tsx) and wrap your application tree.
- Render Connection UI: Drop
NetworkSelector into your layout. Run npm run dev and verify that multiple installed extensions appear as distinct buttons without manual polling.