钱包连接与认证

核心知识点讲解

钱包连接的基本原理

钱包连接是Web3应用与用户交互的第一步:

  • 钱包检测:检测用户是否安装了钱包
  • 连接请求:请求用户授权连接钱包
  • 地址获取:获取用户的钱包地址
  • 网络检测:检测用户是否在正确的网络上
  • 状态管理:管理钱包连接状态

签名认证的工作原理

签名认证是一种基于密码学的认证方法:

  • 消息生成:生成一个随机消息或挑战
  • 签名请求:请求用户签名消息
  • 签名验证:验证签名是否来自声称的地址
  • 会话管理:管理用户认证状态

钱包连接方法

常见的钱包连接方法:

  • MetaMask直接连接:使用window.ethereum API
  • WalletConnect:支持多种钱包的连接协议
  • Coinbase Wallet SDK:Coinbase钱包的连接SDK
  • 多钱包集成:同时支持多种钱包

认证流程

完整的认证流程包括:

  1. 钱包连接:连接用户的钱包
  2. 地址获取:获取用户的钱包地址
  3. 消息签名:请求用户签名消息
  4. 签名验证:验证签名的有效性
  5. 会话创建:创建用户会话
  6. 会话管理:管理会话状态和过期

实用案例分析

MetaMask钱包连接

// MetaMask钱包连接组件
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

function MetaMaskConnector() {
  const [provider, setProvider] = useState(null);
  const [signer, setSigner] = useState(null);
  const [address, setAddress] = useState('');
  const [balance, setBalance] = useState('0');
  const [network, setNetwork] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  // 检查钱包是否安装
  useEffect(() => {
    if (window.ethereum) {
      console.log('MetaMask detected');
    } else {
      setError('MetaMask not detected');
    }
  }, []);

  // 连接钱包
  const connectWallet = async () => {
    try {
      if (!window.ethereum) {
        throw new Error('MetaMask not detected');
      }

      setLoading(true);
      setError('');

      // 请求账户访问
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const address = accounts[0];

      // 创建Provider和Signer
      const provider = new ethers.BrowserProvider(window.ethereum);
      const signer = await provider.getSigner();

      // 获取网络信息
      const network = await provider.getNetwork();

      // 获取余额
      const balance = await provider.getBalance(address);
      const balanceInEther = ethers.formatEther(balance);

      setProvider(provider);
      setSigner(signer);
      setAddress(address);
      setBalance(balanceInEther);
      setNetwork(network);
    } catch (err) {
      setError('Failed to connect wallet: ' + err.message);
      console.error('Wallet connection error:', err);
    } finally {
      setLoading(false);
    }
  };

  // 断开钱包
  const disconnectWallet = () => {
    setProvider(null);
    setSigner(null);
    setAddress('');
    setBalance('0');
    setNetwork(null);
    setError('');
  };

  // 监听账户变化
  useEffect(() => {
    if (window.ethereum) {
      const handleAccountsChanged = (accounts) => {
        if (accounts.length === 0) {
          disconnectWallet();
        } else {
          setAddress(accounts[0]);
        }
      };

      const handleNetworkChanged = async (networkId) => {
        if (provider) {
          try {
            const network = await provider.getNetwork();
            setNetwork(network);
          } catch (err) {
            console.error('Error getting network:', err);
          }
        }
      };

      window.ethereum.on('accountsChanged', handleAccountsChanged);
      window.ethereum.on('networkChanged', handleNetworkChanged);

      return () => {
        window.ethereum.off('accountsChanged', handleAccountsChanged);
        window.ethereum.off('networkChanged', handleNetworkChanged);
      };
    }
  }, [provider]);

  return (
    <div className="metamask-connector">
      <h2>MetaMask Wallet Connection</h2>
      
      {!provider ? (
        <button onClick={connectWallet} disabled={loading}>
          {loading ? 'Connecting...' : 'Connect MetaMask'}
        </button>
      ) : (
        <div>
          <p><strong>Address:</strong> {address}</p>
          <p><strong>Balance:</strong> {balance} ETH</p>
          <p><strong>Network:</strong> {network?.name} (Chain ID: {network?.chainId})</p>
          <button onClick={disconnectWallet}>Disconnect</button>
        </div>
      )}
      
      {error && (
        <div className="error">
          <p>{error}</p>
        </div>
      )}
    </div>
  );
}

export default MetaMaskConnector;

钱包签名认证

// 钱包签名认证组件
import React, { useState } from 'react';
import { ethers } from 'ethers';

function WalletAuthentication({ signer, address }) {
  const [message, setMessage] = useState('');
  const [signature, setSignature] = useState('');
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  // 生成随机消息
  const generateMessage = () => {
    const randomMessage = `Sign this message to authenticate: ${Math.random().toString(36).substring(2)}`;
    setMessage(randomMessage);
    setSignature('');
    setIsAuthenticated(false);
    setError('');
  };

  // 签名消息
  const signMessage = async () => {
    if (!signer || !message) {
      setError('Please connect wallet and generate message first');
      return;
    }

    try {
      setLoading(true);
      setError('');

      const signature = await signer.signMessage(message);
      setSignature(signature);
      
      // 验证签名
      const recoveredAddress = ethers.verifyMessage(message, signature);
      if (recoveredAddress.toLowerCase() === address.toLowerCase()) {
        setIsAuthenticated(true);
        console.log('Authentication successful');
      } else {
        setError('Signature verification failed');
        console.error('Signature verification failed');
      }
    } catch (err) {
      setError('Failed to sign message: ' + err.message);
      console.error('Sign message error:', err);
    } finally {
      setLoading(false);
    }
  };

  // 验证签名
  const verifySignature = () => {
    if (!message || !signature || !address) {
      setError('Please sign a message first');
      return;
    }

    try {
      const recoveredAddress = ethers.verifyMessage(message, signature);
      if (recoveredAddress.toLowerCase() === address.toLowerCase()) {
        setIsAuthenticated(true);
        setError('');
        console.log('Signature verified successfully');
      } else {
        setIsAuthenticated(false);
        setError('Signature verification failed');
        console.error('Signature verification failed');
      }
    } catch (err) {
      setError('Failed to verify signature: ' + err.message);
      console.error('Verify signature error:', err);
    }
  };

  return (
    <div className="wallet-authentication">
      <h2>Wallet Authentication</h2>
      
      {!signer ? (
        <p>Please connect wallet first</p>
      ) : (
        <div>
          <div className="message-section">
            <button onClick={generateMessage}>Generate Message</button>
            {message && (
              <div>
                <h3>Message:</h3>
                <p>{message}</p>
              </div>
            )}
          </div>
          
          <div className="signature-section">
            <button onClick={signMessage} disabled={!message || loading}>
              {loading ? 'Signing...' : 'Sign Message'}
            </button>
            {signature && (
              <div>
                <h3>Signature:</h3>
                <p>{signature}</p>
                <button onClick={verifySignature}>Verify Signature</button>
              </div>
            )}
          </div>
          
          <div className="auth-status">
            {isAuthenticated ? (
              <p className="success">Authentication successful!</p>
            ) : (
              <p className="pending">Not authenticated</p>
            )}
          </div>
        </div>
      )}
      
      {error && (
        <div className="error">
          <p>{error}</p>
        </div>
      )}
    </div>
  );
}

export default WalletAuthentication;

WalletConnect集成

// WalletConnect集成组件
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import WalletConnectProvider from '@walletconnect/web3-provider';

function WalletConnectConnector() {
  const [provider, setProvider] = useState(null);
  const [signer, setSigner] = useState(null);
  const [address, setAddress] = useState('');
  const [balance, setBalance] = useState('0');
  const [network, setNetwork] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [connector, setConnector] = useState(null);

  // 连接WalletConnect
  const connectWalletConnect = async () => {
    try {
      setLoading(true);
      setError('');

      // 创建WalletConnect提供者
      const walletConnectProvider = new WalletConnectProvider({
        rpc: {
          1: 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          11155111: 'https://sepolia.infura.io/v3/YOUR_INFURA_API_KEY'
        }
      });

      // 连接
      await walletConnectProvider.enable();

      // 创建ethers提供者
      const ethersProvider = new ethers.BrowserProvider(walletConnectProvider);
      const signer = await ethersProvider.getSigner();

      // 获取地址
      const address = await signer.getAddress();

      // 获取网络信息
      const network = await ethersProvider.getNetwork();

      // 获取余额
      const balance = await ethersProvider.getBalance(address);
      const balanceInEther = ethers.formatEther(balance);

      setProvider(ethersProvider);
      setSigner(signer);
      setAddress(address);
      setBalance(balanceInEther);
      setNetwork(network);
      setConnector(walletConnectProvider);
    } catch (err) {
      setError('Failed to connect WalletConnect: ' + err.message);
      console.error('WalletConnect connection error:', err);
    } finally {
      setLoading(false);
    }
  };

  // 断开WalletConnect
  const disconnectWalletConnect = async () => {
    if (connector) {
      await connector.disconnect();
    }
    setProvider(null);
    setSigner(null);
    setAddress('');
    setBalance('0');
    setNetwork(null);
    setConnector(null);
    setError('');
  };

  // 监听连接状态
  useEffect(() => {
    if (connector) {
      connector.on('disconnect', () => {
        disconnectWalletConnect();
      });

      connector.on('accountsChanged', (accounts) => {
        if (accounts.length > 0) {
          setAddress(accounts[0]);
        } else {
          disconnectWalletConnect();
        }
      });

      connector.on('chainChanged', async (chainId) => {
        if (provider) {
          try {
            const network = await provider.getNetwork();
            setNetwork(network);
          } catch (err) {
            console.error('Error getting network:', err);
          }
        }
      });

      return () => {
        if (connector) {
          connector.off('disconnect');
          connector.off('accountsChanged');
          connector.off('chainChanged');
        }
      };
    }
  }, [connector, provider]);

  return (
    <div className="walletconnect-connector">
      <h2>WalletConnect</h2>
      
      {!provider ? (
        <button onClick={connectWalletConnect} disabled={loading}>
          {loading ? 'Connecting...' : 'Connect via WalletConnect'}
        </button>
      ) : (
        <div>
          <p><strong>Address:</strong> {address}</p>
          <p><strong>Balance:</strong> {balance} ETH</p>
          <p><strong>Network:</strong> {network?.name} (Chain ID: {network?.chainId})</p>
          <button onClick={disconnectWalletConnect}>Disconnect</button>
        </div>
      )}
      
      {error && (
        <div className="error">
          <p>{error}</p>
        </div>
      )}
    </div>
  );
}

export default WalletConnectConnector;

多钱包集成

// 多钱包集成组件
import React, { useState } from 'react';
import MetaMaskConnector from './MetaMaskConnector';
import WalletConnectConnector from './WalletConnectConnector';

function MultiWalletConnector() {
  const [selectedWallet, setSelectedWallet] = useState('metaMask');

  return (
    <div className="multi-wallet-connector">
      <h2>Connect Wallet</h2>
      
      <div className="wallet-selector">
        <button 
          className={selectedWallet === 'metaMask' ? 'active' : ''}
          onClick={() => setSelectedWallet('metaMask')}
        >
          MetaMask
        </button>
        <button 
          className={selectedWallet === 'walletConnect' ? 'active' : ''}
          onClick={() => setSelectedWallet('walletConnect')}
        >
          WalletConnect
        </button>
      </div>
      
      <div className="wallet-connector">
        {selectedWallet === 'metaMask' ? (
          <MetaMaskConnector />
        ) : (
          <WalletConnectConnector />
        )}
      </div>
    </div>
  );
}

export default MultiWalletConnector;

会话管理

// 会话管理组件
import React, { useState, useEffect } from 'react';

function SessionManager({ isAuthenticated, address }) {
  const [session, setSession] = useState(null);
  const [sessionExpiry, setSessionExpiry] = useState(null);

  // 创建会话
  useEffect(() => {
    if (isAuthenticated && address) {
      const now = new Date();
      const expiry = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24小时过期
      
      const newSession = {
        address,
        createdAt: now,
        expiresAt: expiry
      };
      
      // 存储会话到localStorage
      localStorage.setItem('web3Session', JSON.stringify(newSession));
      setSession(newSession);
      setSessionExpiry(expiry);
    }
  }, [isAuthenticated, address]);

  // 检查会话
  useEffect(() => {
    const storedSession = localStorage.getItem('web3Session');
    if (storedSession) {
      const session = JSON.parse(storedSession);
      const now = new Date();
      const expiresAt = new Date(session.expiresAt);
      
      if (now < expiresAt) {
        setSession(session);
        setSessionExpiry(expiresAt);
      } else {
        // 会话过期
        localStorage.removeItem('web3Session');
      }
    }
  }, []);

  // 清除会话
  const clearSession = () => {
    localStorage.removeItem('web3Session');
    setSession(null);
    setSessionExpiry(null);
  };

  return (
    <div className="session-manager">
      <h2>Session Management</h2>
      
      {session ? (
        <div>
          <p><strong>Session active for address:</strong> {session.address}</p>
          <p><strong>Expires at:</strong> {sessionExpiry?.toLocaleString()}</p>
          <button onClick={clearSession}>Clear Session</button>
        </div>
      ) : (
        <p>No active session</p>
      )}
    </div>
  );
}

export default SessionManager;

实践练习

  1. 实现MetaMask钱包连接

    • 检测MetaMask是否安装
    • 实现连接和断开功能
    • 处理账户和网络变化
    • 显示账户信息和余额
  2. 实现钱包签名认证

    • 生成随机消息
    • 请求用户签名
    • 验证签名
    • 管理认证状态
  3. 集成WalletConnect

    • 安装WalletConnect库
    • 实现WalletConnect连接
    • 处理连接状态
    • 与MetaMask连接集成
  4. 实现多钱包集成

    • 创建钱包选择界面
    • 集成多种钱包连接方法
    • 统一处理钱包状态
    • 提供一致的用户体验
  5. 实现会话管理

    • 创建和存储会话
    • 检查会话过期
    • 清除会话
    • 保持用户认证状态

总结

钱包连接与认证是Web3应用的重要组成部分,它为用户提供了安全、便捷的登录和认证方式。通过本集的学习,你应该能够:

  • 连接钱包:使用MetaMask、WalletConnect等方式连接用户钱包
  • 获取地址:获取用户的钱包地址和账户信息
  • 签名认证:使用密码学签名进行用户认证
  • 验证签名:验证签名的有效性和真实性
  • 管理会话:创建和管理用户会话

在实际开发中,钱包连接与认证需要考虑用户体验、安全性和可靠性。通过合理的错误处理和状态管理,可以创建出安全、用户友好的Web3应用。

钱包连接与认证是Web3应用与用户交互的基础,它使得去中心化应用能够识别用户身份,为用户提供个性化的服务和体验。

« 上一篇 前端与智能合约交互 下一篇 » 前端状态管理