TechLead
Lesson 6 of 20
5 min read
Web3 & Blockchain

Connecting Wallets to dApps

Implement wallet connection in your decentralized application using MetaMask, WalletConnect, and modern wallet aggregation libraries

Understanding Wallet Connection

In Web3, the user's wallet serves as both their identity and their means of signing transactions. Unlike traditional web apps where you manage authentication with usernames and passwords, dApps delegate authentication entirely to the user's wallet. The wallet injects a provider object into the browser (typically window.ethereum) that your application uses to request the user's address, sign messages, and send transactions.

Common Wallets

  • MetaMask: The most popular browser extension wallet. Available as a Chrome/Firefox extension and mobile app.
  • Coinbase Wallet: Coinbase's self-custody wallet with a browser extension and mobile app.
  • WalletConnect: An open protocol that lets users connect mobile wallets by scanning a QR code.
  • Rainbow: A user-friendly Ethereum wallet focused on a great mobile experience.
  • Rabby: A browser extension wallet with built-in transaction simulation and security warnings.

Direct MetaMask Connection

The simplest approach connects directly to MetaMask using the injected window.ethereum provider. This is educational and useful for understanding the fundamentals, but production dApps should use a wallet aggregation library.

// Direct MetaMask connection (low-level approach)
import { ethers } from 'ethers';

async function connectMetaMask() {
  // Check if MetaMask is installed
  if (typeof window.ethereum === 'undefined') {
    throw new Error('MetaMask is not installed');
  }

  // Request account access — this triggers the MetaMask popup
  const accounts = await window.ethereum.request({
    method: 'eth_requestAccounts',
  });

  // Create ethers provider and signer
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const address = await signer.getAddress();
  const balance = await provider.getBalance(address);

  console.log('Connected:', address);
  console.log('Balance:', ethers.formatEther(balance), 'ETH');

  return { provider, signer, address };
}

// Listen for account and chain changes
window.ethereum.on('accountsChanged', (accounts: string[]) => {
  if (accounts.length === 0) {
    console.log('Wallet disconnected');
  } else {
    console.log('Switched to:', accounts[0]);
  }
});

window.ethereum.on('chainChanged', (chainId: string) => {
  console.log('Chain changed to:', parseInt(chainId, 16));
  // Recommended: reload the page on chain change
  window.location.reload();
});

// Request network switch
async function switchToSepolia() {
  await window.ethereum.request({
    method: 'wallet_switchEthereumChain',
    params: [{ chainId: '0xaa36a7' }], // Sepolia chainId in hex
  });
}

Using wagmi + viem (Recommended)

wagmi is the leading React hooks library for Ethereum. Combined with viem (a TypeScript-first alternative to ethers.js) and ConnectKit or RainbowKit for the UI, it provides a production-ready wallet connection experience.

// Install: npm install wagmi viem @tanstack/react-query connectkit

// config.ts — wagmi configuration
import { createConfig, http } from 'wagmi';
import { mainnet, sepolia } from 'wagmi/chains';
import { getDefaultConfig } from 'connectkit';

export const config = createConfig(
  getDefaultConfig({
    chains: [mainnet, sepolia],
    transports: {
      [mainnet.id]: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
      [sepolia.id]: http('https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'),
    },
    walletConnectProjectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
    appName: 'My Web3 App',
    appDescription: 'A decentralized application',
    appUrl: 'https://myapp.com',
  })
);
// providers.tsx — Wrap your app with providers
'use client';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ConnectKitProvider } from 'connectkit';
import { config } from './config';

const queryClient = new QueryClient();

export function Web3Provider({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <ConnectKitProvider theme="auto">
          {children}
        </ConnectKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}
// WalletButton.tsx — Connect/disconnect UI
'use client';
import { ConnectKitButton } from 'connectkit';

export function WalletButton() {
  return <ConnectKitButton />;
}

// Or build your own custom button
import { useAccount, useConnect, useDisconnect, useBalance } from 'wagmi';

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

  if (isConnected) {
    return (
      <div className="flex items-center gap-4">
        <span>{chain?.name}</span>
        <span>{balance?.formatted} {balance?.symbol}</span>
        <span>{address?.slice(0, 6)}...{address?.slice(-4)}</span>
        <button onClick={() => disconnect()}>Disconnect</button>
      </div>
    );
  }

  return (
    <div className="flex gap-2">
      {connectors.map((connector) => (
        <button key={connector.id} onClick={() => connect({ connector })}>
          {connector.name}
        </button>
      ))}
    </div>
  );
}

Reading and Writing Contract Data with wagmi

'use client';
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseEther, formatEther } from 'viem';

const contractConfig = {
  address: '0xContractAddress...' as const,
  abi: [
    {
      name: 'balanceOf',
      type: 'function',
      stateMutability: 'view',
      inputs: [{ name: 'account', type: 'address' }],
      outputs: [{ name: '', type: 'uint256' }],
    },
    {
      name: 'transfer',
      type: 'function',
      stateMutability: 'nonpayable',
      inputs: [
        { name: 'to', type: 'address' },
        { name: 'amount', type: 'uint256' },
      ],
      outputs: [{ name: '', type: 'bool' }],
    },
  ] as const,
};

export function TokenBalance({ address }: { address: string }) {
  const { data: balance, isLoading } = useReadContract({
    ...contractConfig,
    functionName: 'balanceOf',
    args: [address as `0x${string}`],
  });

  if (isLoading) return <span>Loading...</span>;
  return <span>{formatEther(balance ?? 0n)} tokens</span>;
}

export function TransferForm() {
  const { writeContract, data: hash, isPending } = useWriteContract();
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });

  function handleTransfer(to: string, amount: string) {
    writeContract({
      ...contractConfig,
      functionName: 'transfer',
      args: [to as `0x${string}`, parseEther(amount)],
    });
  }

  return (
    <div>
      <button
        onClick={() => handleTransfer('0xRecipient...', '10')}
        disabled={isPending || isConfirming}
      >
        {isPending ? 'Confirming in wallet...' : isConfirming ? 'Waiting for block...' : 'Send 10 Tokens'}
      </button>
      {isSuccess && <p>Transfer confirmed!</p>}
    </div>
  );
}

Wallet Connection Best Practices

  • Support multiple wallets: Use wagmi + ConnectKit/RainbowKit to support MetaMask, WalletConnect, Coinbase, and more
  • Handle disconnection: Always handle the case where the user disconnects or switches accounts
  • Show network info: Display the connected network and warn if the user is on the wrong chain
  • Persist connection: Use wagmi's built-in reconnect on page reload for a seamless experience
  • Never request the private key: Your dApp should only ask the wallet to sign messages/transactions — never request key export

Continue Learning