Web3 Integration Guide Part 1: Connecting Crypto Wallets

Learn how to integrate cryptocurrency wallets into your Vue 3 applications. Complete guide covering MetaMask, WalletConnect, and building a wallet connection interface with Vite and Ethers.js.

By GodFake Team15 min read
Web3Vue 3CryptocurrencyBlockchainMetaMaskEthereumFrontend Development
Web3 Integration Guide

Web3 Integration Guide Part 1: Connecting Crypto Wallets

Introduction

Web3 integration enables your web applications to interact with blockchain networks, accept cryptocurrency payments, and leverage decentralized technologies. This comprehensive five-part guide will teach you how to build a fully-functional Web3 application using Vue 3 and Vite.

In Part 1, we'll focus on the foundation: connecting cryptocurrency wallets to your web application. By the end of this lesson, you'll have a working Vue 3 app that can connect to MetaMask and WalletConnect, display user addresses, and handle network switching.

What You'll Learn

  • Understanding Web3 wallets and providers
  • Setting up a Vue 3 + Vite project for Web3
  • Installing and configuring Ethers.js
  • Implementing MetaMask connection
  • Adding WalletConnect support
  • Building a wallet connection UI component
  • Handling wallet events (account changes, disconnections)
  • Detecting and switching networks

Prerequisites

  • Basic knowledge of Vue 3 Composition API
  • Node.js 18+ and npm/yarn installed
  • MetaMask browser extension (for testing)
  • Basic understanding of JavaScript async/await

Understanding Web3 Wallets

What is a Web3 Wallet?

A Web3 wallet is a digital tool that stores your cryptocurrency private keys and enables interaction with blockchain networks. Unlike traditional apps where users create accounts with username/password, Web3 apps authenticate users through their wallet addresses.

Key Concepts:

  • Public Address: Your wallet's public identifier (e.g., 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4)
  • Private Key: Secret key that proves ownership (never share this!)
  • Provider: Software that connects your app to the blockchain
  • Signer: Interface to sign transactions with your private key

Popular Wallet Options

Wallet Type Best For Platforms
MetaMask Browser Extension Desktop users, beginners Chrome, Firefox, Brave
WalletConnect Protocol Mobile users, multiple wallets 200+ mobile wallets
Coinbase Wallet Browser + Mobile Coinbase users iOS, Android, Chrome
Rainbow Mobile NFT collectors iOS, Android

For this guide, we'll implement MetaMask (most popular) and WalletConnect (best mobile support).

Project Setup

Step 1: Create Vue 3 + Vite Project

Let's start by creating a new Vue 3 project using Vite:

npm create vite@latest web3-wallet-app -- --template vue
cd web3-wallet-app
npm install

Step 2: Install Web3 Dependencies

We'll use Ethers.js v6 as our Web3 library. It's lightweight, well-documented, and actively maintained:

npm install ethers@^6.0.0

For WalletConnect support, we'll also need:

npm install @web3modal/ethers@^5.0.0

Step 3: Project Structure

Organize your project like this:

web3-wallet-app/
├── src/
│   ├── components/
│   │   ├── WalletConnect.vue      # Main wallet connection component
│   │   └── NetworkSelector.vue    # Network switching component
│   ├── composables/
│   │   └── useWallet.js          # Wallet state management
│   ├── utils/
│   │   └── chains.js             # Chain configurations
│   ├── App.vue
│   └── main.js
├── package.json
└── vite.config.js

Building the Wallet Composable

Vue 3's Composition API allows us to create reusable logic with composables. Let's create useWallet.js to manage wallet state:

Create src/composables/useWallet.js

import { ref, computed, readonly } from 'vue'
import { BrowserProvider } from 'ethers'

// Reactive state
const account = ref(null)
const chainId = ref(null)
const provider = ref(null)
const isConnected = ref(false)
const isConnecting = ref(false)
const error = ref(null)

export function useWallet() {
  /**
   * Connect to MetaMask wallet
   */
  const connectMetaMask = async () => {
    try {
      isConnecting.value = true
      error.value = null

      // Check if MetaMask is installed
      if (!window.ethereum) {
        throw new Error('MetaMask is not installed. Please install it from metamask.io')
      }

      // Request account access
      const accounts = await window.ethereum.request({
        method: 'eth_requestAccounts'
      })

      if (!accounts || accounts.length === 0) {
        throw new Error('No accounts found. Please unlock MetaMask.')
      }

      // Create Ethers provider
      provider.value = new BrowserProvider(window.ethereum)
      
      // Get signer and account details
      const signer = await provider.value.getSigner()
      account.value = await signer.getAddress()

      // Get network/chain ID
      const network = await provider.value.getNetwork()
      chainId.value = Number(network.chainId)

      isConnected.value = true

      // Setup event listeners
      setupEventListeners()

      console.log('✅ Connected to MetaMask:', account.value)
      
    } catch (err) {
      console.error('MetaMask connection error:', err)
      error.value = err.message
      disconnect()
    } finally {
      isConnecting.value = false
    }
  }

  /**
   * Disconnect wallet
   */
  const disconnect = () => {
    account.value = null
    chainId.value = null
    provider.value = null
    isConnected.value = false
    error.value = null

    // Remove event listeners
    if (window.ethereum) {
      window.ethereum.removeAllListeners()
    }

    console.log('👋 Wallet disconnected')
  }

  /**
   * Setup wallet event listeners
   */
  const setupEventListeners = () => {
    if (!window.ethereum) return

    // Handle account changes
    window.ethereum.on('accountsChanged', (accounts) => {
      if (accounts.length === 0) {
        // User disconnected wallet
        disconnect()
      } else if (accounts[0] !== account.value) {
        // User switched accounts
        account.value = accounts[0]
        console.log('🔄 Account changed:', account.value)
      }
    })

    // Handle chain/network changes
    window.ethereum.on('chainChanged', (newChainId) => {
      chainId.value = parseInt(newChainId, 16)
      console.log('🔄 Chain changed:', chainId.value)
      // Reload the page on chain change (recommended by MetaMask)
      window.location.reload()
    })

    // Handle disconnection
    window.ethereum.on('disconnect', () => {
      disconnect()
    })
  }

  /**
   * Switch to a different network
   */
  const switchNetwork = async (targetChainId) => {
    try {
      error.value = null
      
      const chainIdHex = `0x${targetChainId.toString(16)}`
      
      await window.ethereum.request({
        method: 'wallet_switchEthereumChain',
        params: [{ chainId: chainIdHex }]
      })

      console.log('✅ Switched to chain:', targetChainId)
      
    } catch (err) {
      // This error code indicates that the chain has not been added to MetaMask
      if (err.code === 4902) {
        error.value = 'This network is not available in your MetaMask. Please add it manually.'
      } else {
        error.value = err.message
      }
      console.error('Network switch error:', err)
    }
  }

  /**
   * Get balance of connected account
   */
  const getBalance = async () => {
    if (!provider.value || !account.value) {
      return '0'
    }

    try {
      const balance = await provider.value.getBalance(account.value)
      // Convert from wei to ether
      return (Number(balance) / 1e18).toFixed(4)
    } catch (err) {
      console.error('Error fetching balance:', err)
      return '0'
    }
  }

  /**
   * Format address for display (0x1234...5678)
   */
  const formatAddress = (address) => {
    if (!address) return ''
    return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`
  }

  // Computed properties
  const shortAddress = computed(() => formatAddress(account.value))
  const networkName = computed(() => {
    const networks = {
      1: 'Ethereum Mainnet',
      5: 'Goerli Testnet',
      11155111: 'Sepolia Testnet',
      137: 'Polygon',
      80001: 'Mumbai Testnet',
      56: 'BSC Mainnet',
      97: 'BSC Testnet'
    }
    return networks[chainId.value] || `Chain ${chainId.value}`
  })

  return {
    // State (readonly to prevent external mutations)
    account: readonly(account),
    chainId: readonly(chainId),
    provider: readonly(provider),
    isConnected: readonly(isConnected),
    isConnecting: readonly(isConnecting),
    error: readonly(error),
    
    // Computed
    shortAddress,
    networkName,
    
    // Methods
    connectMetaMask,
    disconnect,
    switchNetwork,
    getBalance,
    formatAddress
  }
}

Key Features Explained

  • BrowserProvider: Ethers.js wrapper around window.ethereum that provides blockchain access
  • Reactive State: Vue refs track wallet connection status, account, and chain ID
  • Event Listeners: Automatically update when user switches accounts or networks
  • Error Handling: Gracefully handle connection failures and user rejections
  • Readonly Exports: Prevent components from accidentally mutating wallet state

Building the Wallet Connection UI

Now let's create a component that uses our wallet composable:

Create src/components/WalletConnect.vue

<template>
  <div class="wallet-connect">
    <!-- Error Display -->
    <div v-if="error" class="error-banner">
      ⚠️ {{ error }}
      <button @click="error = null" class="close-btn">×</button>
    </div>

    <!-- Not Connected State -->
    <div v-if="!isConnected" class="connect-container">
      <h2>Connect Your Wallet</h2>
      <p>Connect your crypto wallet to interact with Web3 features</p>

      <button 
        @click="connectMetaMask"
        :disabled="isConnecting"
        class="connect-btn metamask"
      >
        <img src="/metamask-icon.svg" alt="MetaMask" />
        {{ isConnecting ? 'Connecting...' : 'Connect MetaMask' }}
      </button>

      <p class="install-note" v-if="!hasMetaMask">
        Don't have MetaMask? 
        <a href="https://metamask.io/download/" target="_blank">Install it here</a>
      </p>
    </div>

    <!-- Connected State -->
    <div v-else class="connected-container">
      <div class="wallet-info">
        <div class="info-row">
          <span class="label">Connected Account:</span>
          <span class="value">
            {{ shortAddress }}
            <button @click="copyAddress" class="copy-btn" title="Copy full address">
              {{ copied ? '✓' : '📋' }}
            </button>
          </span>
        </div>

        <div class="info-row">
          <span class="label">Network:</span>
          <span class="value network">
            <span class="network-dot"></span>
            {{ networkName }}
          </span>
        </div>

        <div class="info-row">
          <span class="label">Balance:</span>
          <span class="value">{{ balance }} ETH</span>
        </div>
      </div>

      <button @click="disconnect" class="disconnect-btn">
        Disconnect Wallet
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useWallet } from '../composables/useWallet'

const {
  account,
  chainId,
  isConnected,
  isConnecting,
  error,
  shortAddress,
  networkName,
  connectMetaMask,
  disconnect,
  getBalance
} = useWallet()

const balance = ref('0.0000')
const copied = ref(false)

// Check if MetaMask is installed
const hasMetaMask = computed(() => {
  return typeof window !== 'undefined' && typeof window.ethereum !== 'undefined'
})

// Update balance when connected
const updateBalance = async () => {
  if (isConnected.value) {
    balance.value = await getBalance()
  }
}

// Copy address to clipboard
const copyAddress = async () => {
  if (!account.value) return
  
  try {
    await navigator.clipboard.writeText(account.value)
    copied.value = true
    setTimeout(() => copied.value = false, 2000)
  } catch (err) {
    console.error('Failed to copy address:', err)
  }
}

// Update balance on mount and when connected
onMounted(() => {
  if (isConnected.value) {
    updateBalance()
  }
})

// Watch for connection changes
import { watch } from 'vue'
watch(isConnected, (connected) => {
  if (connected) {
    updateBalance()
  } else {
    balance.value = '0.0000'
  }
})
</script>

<style scoped>
.wallet-connect {
  max-width: 500px;
  margin: 0 auto;
  padding: 2rem;
}

.error-banner {
  background: #fee;
  border: 1px solid #fcc;
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: #c00;
}

.close-btn {
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: #c00;
}

.connect-container {
  text-align: center;
}

.connect-container h2 {
  margin-bottom: 0.5rem;
  color: #333;
}

.connect-container p {
  color: #666;
  margin-bottom: 2rem;
}

.connect-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem 2rem;
  font-size: 1.1rem;
  font-weight: 600;
  border: none;
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.2s;
}

.connect-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.connect-btn.metamask {
  background: linear-gradient(135deg, #f6851b 0%, #e2761b 100%);
  color: white;
}

.connect-btn.metamask:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(246, 133, 27, 0.4);
}

.connect-btn img {
  width: 24px;
  height: 24px;
}

.install-note {
  margin-top: 1rem;
  font-size: 0.9rem;
  color: #666;
}

.install-note a {
  color: #f6851b;
  font-weight: 600;
}

.connected-container {
  background: #f8f9fa;
  border-radius: 12px;
  padding: 1.5rem;
}

.wallet-info {
  margin-bottom: 1.5rem;
}

.info-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem 0;
  border-bottom: 1px solid #e0e0e0;
}

.info-row:last-child {
  border-bottom: none;
}

.label {
  font-weight: 600;
  color: #666;
}

.value {
  font-family: monospace;
  color: #333;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.network {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.network-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #4caf50;
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.copy-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1rem;
  padding: 0.25rem;
  transition: transform 0.2s;
}

.copy-btn:hover {
  transform: scale(1.2);
}

.disconnect-btn {
  width: 100%;
  padding: 0.75rem;
  background: #dc3545;
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.disconnect-btn:hover {
  background: #c82333;
}
</style>

Component Features

  • Conditional Rendering: Shows connect button when disconnected, wallet info when connected
  • MetaMask Detection: Displays install prompt if MetaMask isn't found
  • Address Copying: One-click copy of full wallet address to clipboard
  • Live Balance: Fetches and displays ETH balance
  • Network Indicator: Shows current blockchain network with visual indicator
  • Error Display: User-friendly error messages with dismissal

Creating the Network Selector

Let's add a component that allows users to switch between different networks:

Create src/utils/chains.js

export const supportedChains = [
  {
    id: 1,
    name: 'Ethereum Mainnet',
    rpcUrl: 'https://eth.llamarpc.com',
    currency: 'ETH',
    explorer: 'https://etherscan.io'
  },
  {
    id: 11155111,
    name: 'Sepolia Testnet',
    rpcUrl: 'https://rpc.sepolia.org',
    currency: 'SepoliaETH',
    explorer: 'https://sepolia.etherscan.io'
  },
  {
    id: 137,
    name: 'Polygon Mainnet',
    rpcUrl: 'https://polygon-rpc.com',
    currency: 'MATIC',
    explorer: 'https://polygonscan.com'
  },
  {
    id: 80001,
    name: 'Mumbai Testnet',
    rpcUrl: 'https://rpc-mumbai.maticvigil.com',
    currency: 'MATIC',
    explorer: 'https://mumbai.polygonscan.com'
  }
]

export const getChainById = (chainId) => {
  return supportedChains.find(chain => chain.id === chainId)
}

Create src/components/NetworkSelector.vue

<template>
  <div class="network-selector" v-if="isConnected">
    <label for="network">Select Network:</label>
    <select 
      id="network"
      :value="chainId" 
      @change="handleNetworkChange"
      class="network-select"
    >
      <option 
        v-for="chain in supportedChains" 
        :key="chain.id"
        :value="chain.id"
      >
        {{ chain.name }}
      </option>
    </select>
  </div>
</template>

<script setup>
import { useWallet } from '../composables/useWallet'
import { supportedChains } from '../utils/chains'

const { chainId, isConnected, switchNetwork } = useWallet()

const handleNetworkChange = (event) => {
  const targetChainId = parseInt(event.target.value, 10)
  if (targetChainId !== chainId.value) {
    switchNetwork(targetChainId)
  }
}
</script>

<style scoped>
.network-selector {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem;
  background: #f8f9fa;
  border-radius: 8px;
  margin: 1rem 0;
}

label {
  font-weight: 600;
  color: #333;
}

.network-select {
  flex: 1;
  padding: 0.5rem 1rem;
  border: 2px solid #ddd;
  border-radius: 6px;
  font-size: 1rem;
  cursor: pointer;
  background: white;
  transition: border-color 0.2s;
}

.network-select:hover {
  border-color: #f6851b;
}

.network-select:focus {
  outline: none;
  border-color: #f6851b;
}
</style>

Putting It All Together

Update src/App.vue

<template>
  <div id="app">
    <header>
      <h1>🔗 Web3 Wallet Integration</h1>
      <p>Learn how to connect crypto wallets to your Vue 3 app</p>
    </header>

    <main>
      <WalletConnect />
      <NetworkSelector />
    </main>

    <footer>
      <p>Part 1: Wallet Connection | <a href="/guides/web3-integration-guide-2025-part-2">Next: Smart Contracts →</a></p>
    </footer>
  </div>
</template>

<script setup>
import WalletConnect from './components/WalletConnect.vue'
import NetworkSelector from './components/NetworkSelector.vue'
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding: 2rem;
}

#app {
  max-width: 1200px;
  margin: 0 auto;
}

header {
  text-align: center;
  color: white;
  margin-bottom: 3rem;
}

header h1 {
  font-size: 2.5rem;
  margin-bottom: 0.5rem;
}

header p {
  font-size: 1.2rem;
  opacity: 0.9;
}

main {
  background: white;
  border-radius: 16px;
  padding: 2rem;
  box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}

footer {
  text-align: center;
  margin-top: 2rem;
  color: white;
}

footer a {
  color: #ffd700;
  font-weight: 600;
  text-decoration: none;
}

footer a:hover {
  text-decoration: underline;
}
</style>

Testing Your Application

Step 1: Run the Development Server

npm run dev

Your app should now be running at http://localhost:5173

Step 2: Test Wallet Connection

  1. Open the app in your browser
  2. Click "Connect MetaMask"
  3. Approve the connection in MetaMask popup
  4. Verify your address and balance are displayed
  5. Try switching networks using the network selector
  6. Test disconnecting and reconnecting

Step 3: Test Edge Cases

  • No MetaMask: Test in a browser without MetaMask installed
  • User Rejection: Click "Reject" when MetaMask asks for connection
  • Account Switching: Switch accounts in MetaMask and verify UI updates
  • Network Switching: Change networks and check balance updates

Common Issues and Solutions

Issue: "MetaMask is not installed" error

Solution: Install MetaMask browser extension from metamask.io/download

Issue: Connection request doesn't appear

Solutions:

  • Check if MetaMask popup is blocked by browser
  • Unlock MetaMask wallet
  • Refresh the page and try again

Issue: Balance shows as 0

Solutions:

Issue: "ChainId not supported" error

Solution: Add the network to MetaMask manually or implement the wallet_addEthereumChain method (covered in Part 5)

Best Practices

Security

  • ✅ Never store private keys in your code or frontend
  • ✅ Always validate addresses before sending transactions
  • ✅ Use HTTPS in production
  • ✅ Implement rate limiting for RPC calls
  • ✅ Display clear transaction details before user confirmation

User Experience

  • ✅ Show loading states during connection
  • ✅ Provide clear error messages
  • ✅ Save connection preference in localStorage
  • ✅ Auto-reconnect on page refresh if previously connected
  • ✅ Support multiple wallets (MetaMask, WalletConnect, Coinbase)

Performance

  • ✅ Cache provider instances
  • ✅ Debounce balance updates
  • ✅ Use event listeners instead of polling
  • ✅ Lazy-load Web3 libraries

What's Next?

Congratulations! You now have a working wallet connection system. In the next parts of this guide, we'll build on this foundation:

  • Part 2: Interacting with smart contracts - reading data, writing transactions, and handling events
  • Part 3: Accepting cryptocurrency payments and ERC-20 tokens
  • Part 4: Sending payments to users, batch transfers, and gas optimization
  • Part 5: Security best practices, testing, and production deployment

Resources

Complete Code Repository

The complete working code for this guide is available on GitHub:

git clone https://github.com/godfake/web3-wallet-integration-vue3.git
cd web3-wallet-integration-vue3
git checkout part-1-wallet-connection
npm install
npm run dev

Continue to Part 2:

→ Smart Contract Interaction