MetaMask Wallet Integration
MetaMask injects window.ethereum into the page — and direct work with the provider usually ends there. In 2024, writing window.ethereum.request({ method: 'eth_requestAccounts' }) directly — this is either a prototype or technical debt. Proper integration via wagmi + viem handles all edge cases: multiple wallets in browser, network switching, disconnection, mobile MetaMask via deeplink.
Stack and Connection
wagmi v2 + viem — the standard for modern dApps. Wagmi manages connection state, auto-reconnect, events. Viem — a typed low-level client instead of ethers.js.
import { createConfig, http } from 'wagmi'
import { mainnet, base } from 'wagmi/chains'
import { metaMask } from 'wagmi/connectors'
export const config = createConfig({
chains: [mainnet, base],
connectors: [metaMask()],
transports: {
[mainnet.id]: http(),
[base.id]: http(),
},
})
Connect button:
import { useConnect, useAccount, useDisconnect } from 'wagmi'
function ConnectButton() {
const { address, isConnected } = useAccount()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
if (isConnected) {
return (
<button onClick={() => disconnect()}>
{address?.slice(0, 6)}...{address?.slice(-4)}
</button>
)
}
const metamask = connectors.find(c => c.id === 'metaMaskSDK')
return (
<button onClick={() => connect({ connector: metamask! })}>
Connect MetaMask
</button>
)
}
Network Switching and Adding Custom Network
Common problem: user on Ethereum mainnet, dApp works on Base. Need to request switch, and if network not added — add it.
import { useSwitchChain } from 'wagmi'
function NetworkGuard({ children, requiredChainId }: Props) {
const { chainId } = useAccount()
const { switchChain, isPending } = useSwitchChain()
if (chainId !== requiredChainId) {
return (
<button
onClick={() => switchChain({ chainId: requiredChainId })}
disabled={isPending}
>
Switch to Base
</button>
)
}
return children
}
wagmi automatically calls wallet_addEthereumChain if wallet_switchEthereumChain returns error 4902 (chain not found).
Message Signing and Transactions
EIP-712 typed data signing — for structured signatures (permit, orders, auth tokens):
import { useSignTypedData } from 'wagmi'
const { signTypedData } = useSignTypedData()
signTypedData({
domain: { name: 'MyApp', version: '1', chainId: 8453 },
types: {
Login: [
{ name: 'address', type: 'address' },
{ name: 'nonce', type: 'string' },
],
},
primaryType: 'Login',
message: { address: userAddress, nonce: sessionNonce },
})
For SIWE (Sign-In with Ethereum) — siwe library generates standardized message, backend verifies via SiweMessage.verify().
Error Handling
MetaMask returns specific error codes:
-
4001— user rejected request -
4902— network not found -
-32002— request already pending (second connect call while first hangs) -
-32603— internal JSON-RPC error (usually insufficient gas or revert)
const { error } = useConnect()
useEffect(() => {
if (error?.name === 'UserRejectedRequestError') {
toast('Connection cancelled')
} else if (error?.name === 'ConnectorAlreadyConnectedError') {
// already connected, ignore
}
}, [error])
Mobile Deeplink
On mobile MetaMask opens via deeplink metamask://dapp/<your-url>. wagmi MetaMask connector automatically handles this via MetaMask SDK. For correct work on iOS need HTTPS (not HTTP) and properly configured WagmiProvider at top app level.







