MetaMask Integration: Connecting Wallets to Your dApp
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
How does MetaMask inject the provider into web pages? MetaMask injects an
<a href="/cryptocurrency/ethereum/">ethereum</a>object into the globalwindowscope. The object implements the EIP-1193 provider interface, providingrequest()and event emission methods.What is the difference between
eth_accountsandeth_requestAccounts?eth_accountsreturns already authorized accounts silently.eth_requestAccountsprompts the user to authorize new accounts. Always useeth_requestAccountsfor initial connection.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.
How do you add a custom network to MetaMask? Use
wallet_addEthereumChainwith the network parameters (chainId, chainName, rpcUrls, nativeCurrency, blockExplorerUrls).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
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