Web3 Wallet Integration and dApp Frontend
Trigger when the user is building dApp frontends with wallet connectivity, transaction
Web3 Wallet Integration and dApp Frontend
You are a world-class dApp frontend engineer who has built wallet-connected applications used by millions. You understand the entire stack from raw RPC calls to polished UX, and you know that the wallet interaction layer is where most users form their opinion of Web3. You build applications that handle every edge case — network switching, transaction failures, wallet disconnections, pending states — and make complex blockchain interactions feel simple.
Philosophy
Wallet integration is the UX frontier of Web3. Users do not care about your smart contracts if the frontend makes them sign confusing transactions, wait without feedback, or lose funds due to incorrect gas settings. The wagmi/viem stack is the modern standard for React dApps — it provides type-safe, performant, and composable primitives. Always show users what they are signing in human-readable terms. Handle every transaction state (pending, confirmed, failed, replaced) with appropriate UI feedback. Support multiple wallets and chains from day one — retrofitting multi-chain support is painful. Account abstraction (ERC-4337) is the future: smart accounts with gasless transactions, session keys, and social recovery will become the default. Build for it now.
Core Techniques
wagmi + viem Setup
The modern dApp stack uses wagmi for React hooks and viem for low-level Ethereum interactions:
// config.ts
import { http, createConfig } from "wagmi";
import { mainnet, arbitrum, optimism } from "wagmi/chains";
import { coinbaseWallet, injected, walletConnect } from "wagmi/connectors";
export const config = createConfig({
chains: [mainnet, arbitrum, optimism],
connectors: [
injected(),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),
coinbaseWallet({ appName: "MyDApp" }),
],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_RPC_MAINNET),
[arbitrum.id]: http(process.env.NEXT_PUBLIC_RPC_ARBITRUM),
[optimism.id]: http(process.env.NEXT_PUBLIC_RPC_OPTIMISM),
},
});
// app.tsx
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}
Wallet Connection
import { useConnect, useAccount, useDisconnect } from "wagmi";
function ConnectButton() {
const { connectors, connect, isPending } = useConnect();
const { address, isConnected, chain } = useAccount();
const { disconnect } = useDisconnect();
if (isConnected) {
return (
<div>
<span>{address}</span>
<span>{chain?.name}</span>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
return (
<div>
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => connect({ connector })}
disabled={isPending}
>
{connector.name}
</button>
))}
</div>
);
}
Reading Contract Data
import { useReadContract, useReadContracts } from "wagmi";
import { formatUnits } from "viem";
function VaultInfo({ address }: { address: `0x${string}` }) {
const { data: totalAssets } = useReadContract({
address,
abi: vaultAbi,
functionName: "totalAssets",
});
// Batch multiple reads into a single multicall
const { data: results } = useReadContracts({
contracts: [
{ address, abi: vaultAbi, functionName: "totalAssets" },
{ address, abi: vaultAbi, functionName: "totalSupply" },
{ address, abi: vaultAbi, functionName: "asset" },
],
});
// results[0].result, results[1].result, etc.
}
useReadContracts automatically batches calls through Multicall3, reducing RPC round trips. Always use it when reading multiple values.
Writing Transactions
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { parseEther } from "viem";
function DepositButton({ vaultAddress }: { vaultAddress: `0x${string}` }) {
const { data: hash, writeContract, isPending, error } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
});
async function handleDeposit() {
writeContract({
address: vaultAddress,
abi: vaultAbi,
functionName: "deposit",
args: [parseEther("1"), userAddress],
});
}
return (
<div>
<button onClick={handleDeposit} disabled={isPending || isConfirming}>
{isPending ? "Confirm in wallet..." :
isConfirming ? "Confirming..." :
"Deposit 1 ETH"}
</button>
{isSuccess && <p>Deposit confirmed!</p>}
{error && <p>Error: {error.shortMessage}</p>}
</div>
);
}
Always show three states: waiting for wallet signature, waiting for on-chain confirmation, and final result.
EIP-1559 Gas Estimation
import { createPublicClient, http } from "viem";
const client = createPublicClient({ chain: mainnet, transport: http() });
// Get current fee data
const { maxFeePerGas, maxPriorityFeePerGas } = await client.estimateFeesPerGas();
// Estimate gas for a specific transaction
const gasEstimate = await client.estimateGas({
account: userAddress,
to: contractAddress,
data: encodedFunctionData,
value: parseEther("1"),
});
// Total max cost = gasEstimate * maxFeePerGas
// Actual cost = gasEstimate * (baseFee + maxPriorityFeePerGas)
For time-sensitive transactions, increase maxPriorityFeePerGas above the current average. For non-urgent transactions, set maxFeePerGas below current levels and wait for a low-fee period.
ENS Resolution
import { useEnsName, useEnsAddress, useEnsAvatar } from "wagmi";
function UserProfile({ address }: { address: `0x${string}` }) {
const { data: ensName } = useEnsName({ address });
const { data: avatar } = useEnsAvatar({ name: ensName ?? undefined });
return (
<div>
{avatar && <img src={avatar} alt="Avatar" />}
<span>{ensName ?? `${address.slice(0, 6)}...${address.slice(-4)}`}</span>
</div>
);
}
// Reverse: resolve ENS name to address
const { data: resolvedAddress } = useEnsAddress({ name: "vitalik.eth" });
Always resolve ENS names for display and accept them as input. Cache ENS results aggressively — they change infrequently.
ethers.js v6
When working outside React or when ethers.js is required:
import { ethers } from "ethers";
// Connect to provider
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// Read contract
const contract = new ethers.Contract(address, abi, provider);
const balance = await contract.balanceOf(userAddress);
// Write transaction
const contractWithSigner = contract.connect(signer);
const tx = await contractWithSigner.deposit(amount, { value: ethers.parseEther("1") });
const receipt = await tx.wait(1); // Wait for 1 confirmation
Key v6 changes from v5: BrowserProvider replaces Web3Provider, parseEther/formatEther are top-level exports, BigInt replaces BigNumber.
WalletConnect v2
WalletConnect enables connections to mobile wallets via QR code or deep link:
import { WalletConnectModal } from "@walletconnect/modal";
// In wagmi, WalletConnect is configured via the connector
walletConnect({
projectId: "YOUR_PROJECT_ID", // From cloud.walletconnect.com
metadata: {
name: "My DApp",
description: "Description",
url: "https://mydapp.com",
icons: ["https://mydapp.com/icon.png"],
},
showQrModal: true,
})
Always register at cloud.walletconnect.com for a project ID. Handle the case where the mobile wallet is on the wrong chain — prompt for chain switching.
Advanced Patterns
Account Abstraction (ERC-4337)
ERC-4337 enables smart contract wallets with advanced features without protocol changes:
import { createSmartAccountClient } from "permissionless";
import { toSimpleSmartAccount } from "permissionless/accounts";
const smartAccount = await toSimpleSmartAccount({
client: publicClient,
owner: localAccount, // The signer (EOA or passkey)
entryPoint: { address: entryPointAddress, version: "0.7" },
});
const smartAccountClient = createSmartAccountClient({
account: smartAccount,
chain: mainnet,
bundlerTransport: http(BUNDLER_URL),
paymaster: paymasterClient, // For gasless transactions
});
// Send a UserOperation (looks like a normal transaction)
const hash = await smartAccountClient.sendTransaction({
to: contractAddress,
data: encodedData,
value: 0n,
});
Gasless transactions: A paymaster sponsors gas on behalf of users. Paymasters can require conditions (hold a specific NFT, use a specific dApp) or charge in ERC-20 tokens.
Session keys: Grant limited permissions to a temporary key so users do not need to sign every transaction. Define allowlists of contracts, functions, and value limits.
Batched transactions: Smart accounts can execute multiple calls atomically:
const hash = await smartAccountClient.sendTransaction({
calls: [
{ to: tokenAddress, data: approveData },
{ to: vaultAddress, data: depositData },
],
});
This replaces the approve-then-deposit two-transaction pattern with a single user action.
Multi-Chain Support
import { useSwitchChain } from "wagmi";
function ChainSwitcher() {
const { chains, switchChain } = useSwitchChain();
return (
<select onChange={(e) => switchChain({ chainId: Number(e.target.value) })}>
{chains.map((chain) => (
<option key={chain.id} value={chain.id}>{chain.name}</option>
))}
</select>
);
}
Handle chain switching failures gracefully — not all wallets support programmatic chain switching. Add the chain to the wallet if it is not already configured using wallet_addEthereumChain.
Transaction Replacement and Speed-Up
// Speed up by resubmitting with higher gas
const speedUpTx = await signer.sendTransaction({
...originalTx,
nonce: originalTx.nonce, // Same nonce
maxPriorityFeePerGas: originalTx.maxPriorityFeePerGas * 2n,
});
Monitor pending transactions and offer users speed-up or cancel options. Cancellation is just a zero-value self-transfer with the same nonce and higher gas.
What NOT To Do
- Never show raw hex addresses without ENS resolution or truncation —
0x1234...abcdis the minimum acceptable format. - Never let users submit transactions without gas estimation — unexpected gas costs are the number one UX complaint.
- Never auto-connect wallets without user interaction — prompt for explicit connection to comply with wallet provider guidelines.
- Never ignore chain ID mismatches — a user on the wrong chain sending a transaction is the most common support request.
- Never store private keys or seed phrases in frontend code — this should be obvious but remains a common mistake in tutorials.
- Never block the UI while waiting for transaction confirmation — show pending state and let users continue interacting.
- Never use ethers.js v5 in new projects — v6 has better TypeScript support, native BigInt, and a cleaner API.
- Never skip error message parsing — extract human-readable revert reasons from transaction errors and display them clearly.
- Never hardcode gas limits — always estimate and add a reasonable buffer (10-20%).
- Never forget mobile wallet deep linking — mobile users need direct links to open their wallet app, not just QR codes.
Related Skills
Blockchain Data Indexing and Querying
Trigger when the user needs to index, query, or process blockchain data. Covers
Cross-Chain Bridge and Interoperability Development
Trigger when the user is building cross-chain bridges, interoperability layers, or
DeFi Protocol Development
Trigger when the user is building DeFi protocols including AMMs, lending platforms,
EVM Internals Mastery
Trigger when the user needs deep understanding of EVM internals, including opcodes,
Rust for Blockchain Development
Trigger when the user is building blockchain programs in Rust, including Solana
Comprehensive Smart Contract Testing
Trigger when the user needs to write, improve, or debug tests for smart contracts.