Web3 Wallet Integration and dApp Frontend

Crypto & Web3 Advanced crypto-dev-skills universal
0 Upvotes
6 Views
0 Downloads
1,469 Words

Description

Build wallet-connected dApp frontends with multi-wallet support, network switching, and robust transaction UX using wagmi/viem.

When to Use

I’m building a dApp with wallet connectivity. | How do I integrate multiple wallets in a React dApp? | Show me wagmi/viem wallet setup steps. | I need UX for wallet transaction states. | Help with network switching and signing txs in a dApp.

Use Cases

Connect wallets (MetaMask, WalletConnect, Coinbase) and sign txs. | Handle network switching prompts with clear UX. | Show pending, confirmed, failed, or replaced tx states. | Support multi-chain frontends and ERC-4337 concepts. | Debug wallet disconnects and nonce-related issues gracefully.

SKILL.md Content

---
name: wallet-integration
description: "Build wallet-connected dApp frontends with multi-wallet support, network switching, and robust transaction UX using wagmi/viem."
metadata:
  tags: "web3, wallet-integration, dapp-frontend, wagmi-viem, multi-wallet, wallet-connectors, network-switching, transaction-ux"
  source: "https://skilldb.dev/skills/crypto-dev-skills/wallet-integration"
  pack: "crypto-dev-skills"
  category: "Crypto & Web3"
---

# Web3 Wallet Integration and dApp Frontend

## When to use this skill
Use when the user says things like:
- "I’m building a dApp with wallet connectivity."
- "How do I integrate multiple wallets in a React dApp?"
- "Show me wagmi/viem wallet setup steps."
- "I need UX for wallet transaction states."
- "Help with network switching and signing txs in a dApp."


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:

```typescript
// 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),
  },
});
```

```tsx
// 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

```tsx
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

```tsx
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

```tsx
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

```typescript
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

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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

```typescript
// 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...abcd` is 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.