Skip to content

MetaMask Integration: Connecting Wallets to Your dApp

DodaTech Updated 2026-06-22 8 min read

In this tutorial, you'll learn MetaMask integration including wallet connection, account management, network switching, transaction signing, and handling user interactions in your dApp. Why it matters: MetaMask is the most widely used Ethereum wallet with over 30 million monthly active users, serving as the primary gateway for users to interact with decentralized applications. By the end, you'll build a robust wallet connection system.

MetaMask is a browser extension wallet that injects an Ethereum provider into web pages, enabling dApps to request accounts, sign transactions, and interact with the blockchain through a familiar interface.

Checking for MetaMask

Before interacting with MetaMask, check if the extension is installed and handle the case where it is not.

import { BrowserProvider } from "ethers";

function isMetaMaskInstalled() {
  return typeof window.ethereum !== "undefined" && window.ethereum.isMetaMask;
}

function detectMetaMask() {
  if (!isMetaMaskInstalled()) {
    window.open("https://metamask.io/download.html", "_blank");
    return false;
  }
  return true;
}

async function initializeProvider() {
  if (!detectMetaMask()) {
    console.log("Please install MetaMask");
    return null;
  }
  
  const provider = new BrowserProvider(window.ethereum);
  console.log("Provider chain ID:", (await provider.getNetwork()).chainId);
  return provider;
}

// Expected output:
// Provider chain ID: 1n  (or 11155111n for Sepolia)
flowchart LR
  A[User visits dApp] --> B{MetaMask installed?}
  B -->|No| C[Show install prompt]
  B -->|Yes| D[ethereum provider available]
  C --> E[Link to MetaMask download]
  D --> F[Request account access]
  F --> G{User approves?}
  G -->|Yes| H[Connect to dApp]
  G -->|No| I[Show connection error]
  H --> J[Read/Wite blockchain]

Connecting to MetaMask

Request account access and handle connection properly.

import { BrowserProvider, Contract } from "ethers";

async function connectMetaMask() {
  if (!window.ethereum) {
    return { success: false, error: "MetaMask not installed" };
  }
  
  try {
    // Request account access
    const accounts = await window.ethereum.request({
      method: "eth_requestAccounts",
    });
    
    const provider = new BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const network = await provider.getNetwork();
    
    return {
      success: true,
      account: accounts[0],
      provider,
      signer,
      chainId: network.chainId,
      chainName: network.name,
    };
  } catch (error) {
    console.error("MetaMask connection error:", error);
    
    let message = "Failed to connect";
    if (error.code === 4001) {
      message = "User rejected the connection request";
    } else if (error.code === -32002) {
      message = "Connection request already pending. Check MetaMask.";
    } else if (error.code === -32603) {
      message = "Internal error. Try refreshing the page.";
    }
    
    return { success: false, error: message };
  }
}

// Expected output on success:
// { success: true, account: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", ... }
// Expected output on rejection:
// { success: false, error: "User rejected the connection request" }

Handling Account and Network Changes

MetaMask emits events when the user switches accounts or networks. Your dApp must listen and respond.

import { useState, useEffect } from "react";
import { BrowserProvider } from "ethers";

function useMetaMask() {
  const [account, setAccount] = useState(null);
  const [chainId, setChainId] = useState(null);
  const [provider, setProvider] = useState(null);
  const [signer, setSigner] = useState(null);
  const [isConnected, setIsConnected] = useState(false);
  
  useEffect(() => {
    if (!window.ethereum) return;
    
    // Handle account changes
    const handleAccountsChanged = async (accounts) => {
      if (accounts.length === 0) {
        // User disconnected
        setAccount(null);
        setIsConnected(false);
        console.log("Please connect to MetaMask");
      } else {
        setAccount(accounts[0]);
        const provider = new BrowserProvider(window.ethereum);
        const signer = await provider.getSigner();
        setProvider(provider);
        setSigner(signer);
        setIsConnected(true);
        console.log("Account changed to:", accounts[0]);
      }
    };
    
    // Handle network changes
    const handleChainChanged = (chainId) => {
      // Reload page on network change (recommended by MetaMask)
      window.location.reload();
    };
    
    // Handle disconnect
    const handleDisconnect = (error) => {
      console.log("MetaMask disconnected:", error);
      setAccount(null);
      setIsConnected(false);
    };
    
    window.ethereum.on("accountsChanged", handleAccountsChanged);
    window.ethereum.on("chainChanged", handleChainChanged);
    window.ethereum.on("disconnect", handleDisconnect);
    
    // Load initial connection
    if (window.ethereum.selectedAddress) {
      handleAccountsChanged([window.ethereum.selectedAddress]);
    }
    
    return () => {
      window.ethereum.removeListener("accountsChanged", handleAccountsChanged);
      window.ethereum.removeListener("chainChanged", handleChainChanged);
      window.ethereum.removeListener("disconnect", handleDisconnect);
    };
  }, []);
  
  return { account, chainId, provider, signer, isConnected };
}

Switching Networks

Prompt the user to switch to the correct network or add a custom network.

async function switchToNetwork(chainId) {
  if (!window.ethereum) return false;
  
  const chainIdHex = "0x" + chainId.toString(16);
  
  try {
    await window.ethereum.request({
      method: "wallet_switchEthereumChain",
      params: [{ chainId: chainIdHex }],
    });
    return true;
  } catch (error) {
    // Chain not added yet
    if (error.code === 4902) {
      return await addNetwork(chainId);
    }
    console.error("Switch network error:", error);
    return false;
  }
}

async function addNetwork(chainId) {
  const networks = {
    11155111: {
      chainId: "0xaa36a7",
      chainName: "Sepolia Testnet",
      nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
      rpcUrls: ["https://rpc.sepolia.org"],
      blockExplorerUrls: ["https://sepolia.etherscan.io"],
    },
    137: {
      chainId: "0x89",
      chainName: "Polygon Mainnet",
      nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
      rpcUrls: ["https://polygon-rpc.com"],
      blockExplorerUrls: ["https://polygonscan.com"],
    },
  };
  
  const network = networks[chainId];
  if (!network) throw new Error("Unknown network");
  
  try {
    await window.ethereum.request({
      method: "wallet_addEthereumChain",
      params: [network],
    });
    return true;
  } catch (error) {
    console.error("Add network error:", error);
    return false;
  }
}

// Usage
async function ensureCorrectNetwork() {
  const desiredChainId = 11155111; // Sepolia
  const currentChainId = await window.ethereum.request({
    method: "eth_chainId",
  });
  
  if (currentChainId !== "0x" + desiredChainId.toString(16)) {
    const switched = await switchToNetwork(desiredChainId);
    if (!switched) {
      console.error("Please switch to Sepolia network");
    }
  }
}

Signing Transactions and Messages

Send transactions and sign messages using MetaMask.

import { BrowserProvider } from "ethers";

async function sendETH(provider, to, amountInEth) {
  const signer = await provider.getSigner();
  
  const tx = await signer.sendTransaction({
    to: to,
    value: ethers.parseEther(amountInEth),
  });
  
  console.log("Transaction sent:", tx.hash);
  console.log("Waiting for confirmation...");
  
  const receipt = await tx.wait();
  console.log("Confirmed in block:", receipt.blockNumber);
  console.log("Gas used:", receipt.gasUsed.toString());
  
  return receipt;
}

async function signMessage(provider, message) {
  const signer = await provider.getSigner();
  
  const signature = await signer.signMessage(message);
  console.log("Signature:", signature);
  
  // Verify the signature
  const signerAddress = await signer.getAddress();
  const recoveredAddress = ethers.verifyMessage(message, signature);
  const isValid = recoveredAddress.toLowerCase() === signerAddress.toLowerCase();
  
  console.log("Signer address:", signerAddress);
  console.log("Recovered address:", recoveredAddress);
  console.log("Signature valid:", isValid);
  
  return { signature, isValid };
}

// Expected output:
// Transaction sent: 0x7a3f...c9e2
// Confirmed in block: 19472345
// Gas used: 21000
// Signature: 0xabc123...
// Signature valid: true

Building a Connection Button

Create a reusable React component for MetaMask connection.

import { useState } from "react";
import { BrowserProvider } from "ethers";

function ConnectButton({ onConnect, onError }) {
  const [connecting, setConnecting] = useState(false);
  const [account, setAccount] = useState(null);
  
  async function handleConnect() {
    setConnecting(true);
    
    try {
      if (!window.ethereum) {
        throw new Error("Please install MetaMask");
      }
      
      const accounts = await window.ethereum.request({
        method: "eth_requestAccounts",
      });
      
      const provider = new BrowserProvider(window.ethereum);
      const network = await provider.getNetwork();
      
      const connectionInfo = {
        account: accounts[0],
        provider,
        chainId: network.chainId,
      };
      
      setAccount(accounts[0]);
      onConnect?.(connectionInfo);
    } catch (error) {
      onError?.(error.message);
    } finally {
      setConnecting(false);
    }
  }
  
  async function handleDisconnect() {
    setAccount(null);
    // MetaMask doesn't support programmatic disconnect
    // Clear local state
  }
  
  if (account) {
    return (
      <div>
        <span>
          Connected: {account.slice(0, 6)}...{account.slice(-4)}
        </span>
        <button onClick={handleDisconnect}>Disconnect</button>
      </div>
    );
  }
  
  return (
    <button onClick={handleConnect} disabled={connecting}>
      {connecting ? "Connecting..." : "Connect MetaMask"}
    </button>
  );
}

export default ConnectButton;
flowchart TD
  A[Connect Button] --> B{User clicks}
  B --> C[MetaMask opens prompt]
  C --> D{User action}
  D -->|Select account + Approve| E[Connection successful]
  D -->|Cancel| F[Error: User rejected]
  E --> G[Display account + balance]
  G --> H[Monitor changes]
  H --> I{Account changed?}
  I -->|Yes| J[Update state]
  H --> K{Network changed?}
  K -->|Yes| L[Reload or switch]

Common Errors and Misunderstandings

1. eth_requestAccounts Fails Silently

The eth_requestAccounts call can fail if MetaMask is locked. Always prompt the user to unlock MetaMask first.

2. Not Handling Multiple Requests

Do not call eth_requestAccounts repeatedly. Check <a href="/cryptocurrency/ethereum/">ethereum</a>.selectedAddress first. If the user has already connected, skip the request.

3. Forgetting Chain ID Validation

Always verify the user is on the correct network before sending transactions. Incorrect chain ID causes transaction failures.

4. Confusing MetaMask Events

accountsChanged fires different events: empty array when disconnected, new array when changed. Handle both cases.

5. Not Cleaning Up Event Listeners

React hooks without cleanup cause memory leaks and duplicate event handlers. Always return cleanup functions in useEffect.

Practice Questions

  1. How does MetaMask inject the provider into web pages? MetaMask injects an <a href="/cryptocurrency/ethereum/">ethereum</a> object into the global window scope. The object implements the EIP-1193 provider interface, providing request() and event emission methods.

  2. What is the difference between eth_accounts and eth_requestAccounts? eth_accounts returns already authorized accounts silently. eth_requestAccounts prompts the user to authorize new accounts. Always use eth_requestAccounts for initial connection.

  3. Why does MetaMask recommend reloading on chain changes? Reloading ensures all dApp state is fresh and network-dependent operations are re-initialized. Partial state updates can lead to inconsistent behavior.

  4. How do you add a custom network to MetaMask? Use wallet_addEthereumChain with the network parameters (chainId, chainName, rpcUrls, nativeCurrency, blockExplorerUrls).

  5. What happens when a user rejects a signature request? MetaMask throws an error with code 4001 ("User rejected the request"). Always handle this error gracefully without crashing the dApp.

Challenge

Build a dApp connection manager that supports MetaMask, WalletConnect, and Coinbase Wallet using a provider-agnostic abstraction layer. Implement auto-connect (reconnect on page reload), network switching with fallback RPCs, and transaction queue management with nonce handling.

Real-World Task

Create a complete wallet integration component for a production dApp using React and ethers.js. Include connection to MetaMask, balance display (ETH and ERC-20 tokens), transaction history, network indicator with switch capability, and a disconnect button that clears all sensitive state from the dApp.

Frequently Asked Questions

Does MetaMask work on mobile devices?

Yes. MetaMask has a mobile app with a built-in browser. dApps can detect mobile MetaMask through window.<a href="/cryptocurrency/ethereum/">ethereum</a>.isMetaMask and adjust the UI accordingly. WalletConnect is also available for mobile-to-desktop pairing.

Can I use MetaMask without a browser extension?

On desktop, you need the browser extension. On mobile, use the MetaMask app browser. For programmatic access without a browser, use the private key directly with new ethers.Wallet(privateKey, provider) for backend scripts.

How do I handle multiple MetaMask accounts?

MetaMask can manage multiple accounts. The dApp accesses them through eth_accounts. You can switch accounts via eth_requestAccounts or by listening to accountsChanged events when the user switches in the extension.

Next Steps

After integrating MetaMask, explore WalletConnect for mobile wallet support, then build DeFi dApps that leverage connected wallets for lending, swapping, and yield farming.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro