Connecting a crypto wallet is the first entry point to any Web3 application. The user clicks "Connect Wallet", the browser opens MetaMask or a WalletConnect QR, and the application receives an address and a signature. It sounds simple, but under the hood there are three different protocols, a dozen wallet providers, and a number of state-related issues that need to be solved correctly from the start. For example, a typical mistake is calling eth_requestAccounts on every component mount, which leads to constant popup requests and degrades UX. We have been working with this for years: we have 30+ projects with wallet integration on React, Next.js, and Vue under our belt.
Which wallets do we support?
We implement connections for MetaMask (injected EIP-1193), WalletConnect v2 (QR code/WebSocket for mobile wallets), Coinbase Wallet (both methods), Rabby, and Brave Wallet. The table below shows the specifics:
| Wallet | Connection type | Peculiarities |
|---|---|---|
| MetaMask | EIP-1193 injection | Popular, browser extension, test network |
| WalletConnect | QR code / WebSocket | Any mobile wallet (Trust, Rainbow, OKX) |
| Coinbase Wallet | Injection + WalletConnect | Integration with Coinbase, dapp browser support |
| Rabby | EIP-1193 injection | Multi-chain, user-friendly interface, open source |
For unification we use adapter libraries: @web3-onboard or wagmi with viem. Compared to a manual implementation via ethers.js, these solutions reduce code volume by 40% and lower the probability of errors when switching wallets. Which stack to choose depends on the requirements for bundle size and flexibility. We will evaluate your project and offer the optimal solution.
Minimal implementation via ethers.js
Below is a basic connection implementation using ethers.js (v6). This code works for custom interfaces when full control is needed.
// lib/wallet.ts
import { BrowserProvider, JsonRpcSigner } from 'ethers';
export interface WalletState {
address: string | null;
chainId: number | null;
provider: BrowserProvider | null;
signer: JsonRpcSigner | null;
}
export async function connectWallet(): Promise<WalletState> {
if (!window.ethereum) {
throw new Error('No injected wallet found. Install MetaMask.');
}
const provider = new BrowserProvider(window.ethereum);
const accounts = await provider.send('eth_requestAccounts', []);
const network = await provider.getNetwork();
const signer = await provider.getSigner();
return {
address: accounts[0],
chainId: Number(network.chainId),
provider,
signer,
};
}
export async function switchChain(chainId: number): Promise<void> {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${chainId.toString(16)}` }],
});
}
How to handle wallet events without memory leaks?
The wallet can change the account or network without notifying the application – you need to subscribe to events. Proper subscription with cleanup on unmount is key to stability.
// hooks/useWalletEvents.ts
import { useEffect } from 'react';
import { useWalletStore } from '@/store/wallet';
export function useWalletEvents() {
const { disconnect, setAddress, setChainId } = useWalletStore();
useEffect(() => {
if (!window.ethereum) return;
const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
disconnect();
} else {
setAddress(accounts[0]);
}
};
const handleChainChanged = (chainIdHex: string) => {
setChainId(parseInt(chainIdHex, 16));
// Don't reload the page — update state
};
window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('chainChanged', handleChainChanged);
window.ethereum.on('disconnect', disconnect);
return () => {
window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
window.ethereum.removeListener('chainChanged', handleChainChanged);
window.ethereum.removeListener('disconnect', disconnect);
};
}, [disconnect, setAddress, setChainId]);
}
Why is SIWE the passwordless authentication standard?
A wallet address is not a user identifier – it can easily be spoofed in an HTTP request. SIWE (Sign-In with Ethereum) solves this: the user signs a message, the backend verifies the signature via ecrecover. A nonce with a 5-minute TTL protects against replay attacks. This approach provides a secure session without storing passwords.
// lib/siwe.ts
import { SiweMessage } from 'siwe';
export async function signInWithEthereum(
address: string,
chainId: number,
signer: JsonRpcSigner,
): Promise<{ message: string; signature: string }> {
const nonce = await fetch('/api/auth/nonce').then(r => r.text());
const message = new SiweMessage({
domain: window.location.host,
address,
statement: 'Sign in to MyApp',
uri: window.location.origin,
version: '1',
chainId,
nonce,
});
const messageStr = message.prepareMessage();
const signature = await signer.signMessage(messageStr);
return { message: messageStr, signature };
}
The backend verifies the signature via the siwe package (Node.js) or any ecrecover implementation. More about the standard can be read in EIP-4361 or on Wikipedia.
How to unify connection of multiple wallets?
To support multiple providers without custom logic, we use adapter libraries. Manual implementation via ethers.js gives full control but requires writing an abstraction for each wallet. wagmi with viem offers React hooks and a type-safe API, reducing code by 30%, but is not suitable for Vue. @web3-onboard is the most universal: built-in UI and support for 20+ wallets, but adds about 50 KB to the bundle. The choice depends on priorities: lightness vs. development speed.
Example configuration with @web3-onboard:
import Onboard from '@web3-onboard/core';
import injectedModule from '@web3-onboard/injected-wallets';
import walletConnectModule from '@web3-onboard/walletconnect';
const injected = injectedModule();
const walletConnect = walletConnectModule({
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
requiredChains: [1, 137],
});
export const onboard = Onboard({
wallets: [injected, walletConnect],
chains: [
{ id: '0x1', token: 'ETH', label: 'Ethereum Mainnet', rpcUrl: process.env.ETH_RPC_URL! },
{ id: '0x89', token: 'MATIC', label: 'Polygon', rpcUrl: process.env.POLYGON_RPC_URL! },
],
appMetadata: {
name: 'MyApp',
icon: '/logo.svg',
description: 'DeFi platform',
},
});
Session restoration after reload
Without persistence, the user would have to go through the entire flow every time. We use eth_accounts – it returns addresses without a popup if the wallet is already authorized.
// Check on initialization
async function restoreConnection(): Promise<void> {
if (!window.ethereum) return;
const accounts: string[] = await window.ethereum.request({
method: 'eth_accounts',
});
if (accounts.length > 0) {
const provider = new BrowserProvider(window.ethereum);
const network = await provider.getNetwork();
walletStore.set({ address: accounts[0], chainId: Number(network.chainId) });
}
}
Typical mistakes and how to avoid them
- Using
eth_requestAccountson every mount – causes a popup even if the wallet is already connected. Solution: checketh_accountsfirst. - Lack of handling
disconnect– can lead to memory leaks. Always subscribe and clean up listeners. - Incorrect nonce calculation in SIWE – if the nonce is not saved on the server, the signature cannot be verified. Use a unique nonce with TTL.
What's included in the work
We deliver a complete package:
- Integration of 3-4 wallets (MetaMask, WalletConnect, Coinbase, Rabby) via an adapter
- SIWE authentication with backend part (Node.js/Python)
- Event handling (accountsChanged, chainChanged, disconnect)
- Session restoration and error handling (rejection, no wallet)
- Network switching and balance checking
- Integration documentation and Readme
- Consultation on library selection for your stack
Contact us for a free assessment of your project – we will respond within a day. Order a wallet integration right now and get clean code with full case coverage.
Timelines and cost
Basic integration of one wallet with SIWE – from 2 to 3 days. A full turnkey solution with support for 4-5 wallets, handling all states, and documentation – from 5 to 7 days. The cost is calculated individually depending on backend complexity and the number of wallets. Get a consultation – we will respond within a day.







