网页中 MetaMask 钱包钱包交互核心功能详解

发布于:2025-08-08 ⋅ 阅读:(20) ⋅ 点赞:(0)

下面将详细讲解 MetaMask 钱包交互的四个核心功能,并提供独立的代码实现。

一、钱包检测与连接

钱包检测与连接是所有交互的基础,需要确认用户是否安装了 MetaMask 并建立连接。

// 钱包检测与连接功能
class WalletDetector {
  constructor() {
    this.provider = null;
    this.isConnected = false;
  }

  // 检测MetaMask是否安装
  detectMetaMask() {
    // 检查浏览器环境
    if (typeof window === 'undefined') {
      throw new Error('请在浏览器环境中运行');
    }

    // 检测window.ethereum对象
    if (window.ethereum) {
      this.provider = window.ethereum;
      return this.provider.isMetaMask;
    } else if (window.web3) {
      // 兼容旧版本web3
      this.provider = window.web3.currentProvider;
      return this.provider.isMetaMask;
    }

    return false;
  }

  // 连接钱包
  async connect() {
    if (!this.provider) {
      throw new Error('未检测到MetaMask钱包');
    }

    try {
      // 请求账户访问权限
      const accounts = await this.provider.request({ 
        method: 'eth_requestAccounts' 
      });

      this.isConnected = accounts.length > 0;
      return {
        success: this.isConnected,
        accounts: accounts,
        message: this.isConnected ? '连接成功' : '未获取到账户'
      };
    } catch (error) {
      this.isConnected = false;
      return {
        success: false,
        error: this.handleError(error),
        message: '连接失败'
      };
    }
  }

  // 断开连接
  disconnect() {
    this.isConnected = false;
    // MetaMask没有真正的断开方法,主要是前端状态重置
    return { success: true, message: '已断开连接' };
  }

  // 错误处理
  handleError(error) {
    switch (error.code) {
      case 4001:
        return '用户拒绝了访问请求';
      case -32002:
        return '请在MetaMask中完成操作';
      default:
        return error.message || '未知错误';
    }
  }
}

// 使用示例
const walletDetector = new WalletDetector();

// 检测钱包
if (walletDetector.detectMetaMask()) {
  console.log('检测到MetaMask钱包');
  
  // 连接钱包
  document.getElementById('connectBtn').addEventListener('click', async () => {
    const result = await walletDetector.connect();
    if (result.success) {
      console.log('连接成功,账户:', result.accounts[0]);
    } else {
      console.error('连接失败:', result.error);
    }
  });
  
  // 断开连接
  document.getElementById('disconnectBtn').addEventListener('click', () => {
    const result = walletDetector.disconnect();
    console.log(result.message);
  });
} else {
  console.log('未检测到MetaMask钱包');
}

核心要点

  • 通过检测window.ethereum对象判断 MetaMask 是否安装
  • 使用eth_requestAccounts方法请求账户访问权限
  • 实现完善的错误处理,针对不同错误代码提供明确信息
  • 维护连接状态,便于后续操作判断

二、账户授权与信息获取

成功连接后,需要获取账户详细信息如地址、余额等,并处理授权状态变化。

// 账户信息管理
class AccountManager {
  constructor(provider) {
    this.provider = provider;
    this.web3 = new Web3(provider);
    this.currentAccount = null;
    this.accountInfo = {};
  }

  // 获取账户列表
  async getAccounts() {
    try {
      return await this.provider.request({ method: 'eth_accounts' });
    } catch (error) {
      console.error('获取账户列表失败:', error);
      return [];
    }
  }

  // 获取账户详情
  async getAccountDetails(account) {
    if (!account) return null;

    try {
      // 获取余额
      const balanceWei = await this.web3.eth.getBalance(account);
      const balanceEth = this.web3.utils.fromWei(balanceWei, 'ether');
      
      // 获取链信息
      const chainId = await this.provider.request({ method: 'eth_chainId' });
      
      return {
        address: account,
        balance: parseFloat(balanceEth).toFixed(4),
        chainId: chainId,
        chainName: this.getChainName(chainId)
      };
    } catch (error) {
      console.error('获取账户详情失败:', error);
      return {
        address: account,
        balance: '获取失败',
        chainId: '获取失败',
        chainName: '未知'
      };
    }
  }

  // 格式化账户地址显示
  formatAddress(address, prefix = 6, suffix = 4) {
    if (!address) return '';
    return `${address.slice(0, prefix)}...${address.slice(-suffix)}`;
  }

  // 链ID与链名称映射
  getChainName(chainId) {
    const chainNames = {
      '0x1': '以太坊主网',
      '0x3': 'Ropsten 测试网',
      '0x4': 'Rinkeby 测试网',
      '0x5': 'Goerli 测试网',
      '0x2a': 'Kovan 测试网',
      '0x89': 'Polygon 主网',
      '0x38': '币安智能链主网'
    };
    return chainNames[chainId] || `未知网络 (${chainId})`;
  }

  // 刷新账户信息
  async refreshAccountInfo() {
    const accounts = await this.getAccounts();
    if (accounts.length === 0) {
      this.currentAccount = null;
      this.accountInfo = {};
      return null;
    }

    this.currentAccount = accounts[0];
    this.accountInfo = await this.getAccountDetails(this.currentAccount);
    return this.accountInfo;
  }
}

// 使用示例
// 假设已通过WalletDetector获取provider
if (window.ethereum) {
  const accountManager = new AccountManager(window.ethereum);
  
  // 刷新账户信息按钮
  document.getElementById('refreshBtn').addEventListener('click', async () => {
    const info = await accountManager.refreshAccountInfo();
    if (info) {
      // 更新UI显示
      document.getElementById('accountAddress').textContent = info.address;
      document.getElementById('formattedAddress').textContent = accountManager.formatAddress(info.address);
      document.getElementById('ethBalance').textContent = `${info.balance} ETH`;
      document.getElementById('chainName').textContent = info.chainName;
    }
  });
}

核心要点

  • 使用eth_accounts获取已授权账户列表
  • 通过eth_getBalance获取账户余额,并使用 Web3.js 工具转换为 ETH
  • 实现地址格式化,保护用户隐私同时展示关键信息
  • 提供链 ID 与链名称的映射,增强用户可读性
  • 支持信息刷新,应对数据可能的变化

三、区块链网络切换监听

监听网络切换事件,及时更新应用状态以适应新的网络环境。

// 网络状态管理器
class NetworkManager {
  constructor(provider, accountManager) {
    this.provider = provider;
    this.accountManager = accountManager;
    this.currentChainId = null;
    this.listeners = [];
    this.init();
  }

  // 初始化
  async init() {
    this.currentChainId = await this.getChainId();
    this.setupEventListeners();
  }

  // 获取当前链ID
  async getChainId() {
    try {
      return await this.provider.request({ method: 'eth_chainId' });
    } catch (error) {
      console.error('获取链ID失败:', error);
      return null;
    }
  }

  // 设置事件监听
  setupEventListeners() {
    // 监听链变化事件
    this.provider.on('chainChanged', this.handleChainChanged.bind(this));
    
    // 监听断开连接事件
    this.provider.on('disconnect', this.handleDisconnect.bind(this));
  }

  // 处理链变化
  async handleChainChanged(chainId) {
    console.log('网络已切换,新链ID:', chainId);
    this.currentChainId = chainId;
    
    // 更新账户信息(因为不同网络余额可能不同)
    if (this.accountManager) {
      await this.accountManager.refreshAccountInfo();
    }
    
    // 通知所有监听器
    this.notifyListeners({
      type: 'chainChanged',
      chainId: chainId,
      chainName: this.accountManager ? this.accountManager.getChainName(chainId) : null
    });
  }

  // 处理断开连接
  handleDisconnect(error) {
    console.log('钱包已断开连接:', error);
    this.notifyListeners({
      type: 'disconnect',
      error: error
    });
  }

  // 添加监听器
  addListener(listener) {
    if (typeof listener === 'function') {
      this.listeners.push(listener);
    }
  }

  // 移除监听器
  removeListener(listener) {
    this.listeners = this.listeners.filter(l => l !== listener);
  }

  // 通知所有监听器
  notifyListeners(event) {
    this.listeners.forEach(listener => {
      try {
        listener(event);
      } catch (error) {
        console.error('监听器处理事件失败:', error);
      }
    });
  }

  // 切换到指定网络
  async switchNetwork(chainId) {
    try {
      await this.provider.request({
        method: 'wallet_switchEthereumChain',
        params: [{ chainId: chainId }],
      });
      return { success: true, chainId: chainId };
    } catch (error) {
      console.error('切换网络失败:', error);
      return { 
        success: false, 
        error: error.message,
        // 如果是未添加的网络,错误代码为4902
        needAddNetwork: error.code === 4902
      };
    }
  }

  // 添加自定义网络
  async addNetwork(networkParams) {
    try {
      await this.provider.request({
        method: 'wallet_addEthereumChain',
        params: [networkParams],
      });
      return { success: true };
    } catch (error) {
      console.error('添加网络失败:', error);
      return { success: false, error: error.message };
    }
  }
}

// 使用示例
// 假设已有accountManager实例
if (window.ethereum && accountManager) {
  const networkManager = new NetworkManager(window.ethereum, accountManager);
  
  // 添加网络变化监听器
  networkManager.addListener(event => {
    if (event.type === 'chainChanged') {
      // 更新UI显示网络变化
      document.getElementById('networkStatus').textContent = `当前网络: ${event.chainName}`;
      showMessage(`已切换到${event.chainName}`, 'success');
    } else if (event.type === 'disconnect') {
      showMessage('钱包连接已断开,请重新连接', 'error');
    }
  });
  
  // 切换到以太坊主网按钮
  document.getElementById('switchToMainnet').addEventListener('click', async () => {
    const result = await networkManager.switchNetwork('0x1');
    if (!result.success && result.needAddNetwork) {
      // 如果需要添加网络,定义网络参数
      const networkParams = {
        chainId: '0x1',
        chainName: 'Ethereum Mainnet',
        rpcUrls: ['https://mainnet.infura.io/v3/YOUR_INFURA_KEY'],
        nativeCurrency: {
          name: 'Ether',
          symbol: 'ETH',
          decimals: 18
        },
        blockExplorerUrls: ['https://etherscan.io']
      };
      await networkManager.addNetwork(networkParams);
    }
  });
}

核心要点

  • 监听chainChanged事件捕获网络切换
  • 实现网络切换通知机制,便于应用各部分响应变化
  • 提供主动切换网络的功能wallet_switchEthereumChain
  • 支持添加自定义网络wallet_addEthereumChain,处理未添加网络的情况
  • 网络切换后自动更新账户信息,保持数据一致性

四、交易签名与发送

交易签名与发送是与区块链交互的核心功能,用于执行转账、合约交互等操作。

// 交易处理器
class TransactionHandler {
  constructor(provider, accountManager) {
    this.provider = provider;
    this.accountManager = accountManager;
    this.web3 = new Web3(provider);
  }

  // 发送ETH转账交易
  async sendEthTransaction(toAddress, amountEth, gasOptions = {}) {
    // 验证参数
    if (!this.web3.utils.isAddress(toAddress)) {
      return { success: false, error: '无效的接收地址' };
    }

    if (isNaN(amountEth) || amountEth <= 0) {
      return { success: false, error: '请输入有效的转账金额' };
    }

    // 检查账户是否已连接
    const accounts = await this.accountManager.getAccounts();
    if (accounts.length === 0) {
      return { success: false, error: '请先连接钱包' };
    }
    const fromAddress = accounts[0];

    try {
      // 转换金额为wei
      const amountWei = this.web3.utils.toWei(amountEth.toString(), 'ether');
      
      // 构建交易参数
      const txParams = {
        from: fromAddress,
        to: toAddress,
        value: amountWei,
        // 可选的gas参数
        gas: gasOptions.gas || await this.estimateGas(fromAddress, toAddress, amountWei),
        gasPrice: gasOptions.gasPrice || await this.web3.eth.getGasPrice()
      };

      // 发送交易
      const txHash = await this.provider.request({
        method: 'eth_sendTransaction',
        params: [txParams]
      });

      return {
        success: true,
        txHash: txHash,
        message: '交易已发送,等待确认',
        explorerUrl: this.getTransactionExplorerUrl(txHash)
      };
    } catch (error) {
      console.error('发送交易失败:', error);
      return {
        success: false,
        error: this.handleTransactionError(error),
        txHash: null
      };
    }
  }

  // 估算gas
  async estimateGas(from, to, value) {
    try {
      return await this.web3.eth.estimateGas({ from, to, value });
    } catch (error) {
      console.warn('估算gas失败,使用默认值:', error);
      return '21000'; // 简单转账的默认gas
    }
  }

  // 处理交易错误
  handleTransactionError(error) {
    switch (error.code) {
      case 4001:
        return '用户取消了交易';
      case -32000:
        return '余额不足,无法完成交易';
      case -32003:
        return '交易被拒绝,可能是gas不足';
      default:
        return error.message || '交易失败,请重试';
    }
  }

  // 获取交易在区块浏览器的链接
  getTransactionExplorerUrl(txHash) {
    if (!this.accountManager || !this.accountManager.accountInfo) return null;
    
    const chainId = this.accountManager.accountInfo.chainId;
    const explorers = {
      '0x1': `https://etherscan.io/tx/${txHash}`,
      '0x3': `https://ropsten.etherscan.io/tx/${txHash}`,
      '0x4': `https://rinkeby.etherscan.io/tx/${txHash}`,
      '0x5': `https://goerli.etherscan.io/tx/${txHash}`,
      '0x89': `https://polygonscan.com/tx/${txHash}`,
      '0x38': `https://bscscan.com/tx/${txHash}`
    };
    
    return explorers[chainId] || null;
  }

  // 签名消息
  async signMessage(message) {
    // 检查账户是否已连接
    const accounts = await this.accountManager.getAccounts();
    if (accounts.length === 0) {
      return { success: false, error: '请先连接钱包' };
    }
    const fromAddress = accounts[0];

    try {
      // 对于MetaMask,建议使用personal_sign
      const signature = await this.provider.request({
        method: 'personal_sign',
        params: [
          this.web3.utils.utf8ToHex(message),
          fromAddress
        ]
      });

      return {
        success: true,
        message: message,
        signature: signature,
        address: fromAddress
      };
    } catch (error) {
      console.error('签名失败:', error);
      return {
        success: false,
        error: error.code === 4001 ? '用户取消了签名' : error.message,
        signature: null
      };
    }
  }

  // 调用合约方法
  async callContractMethod(contractAddress, abi, methodName, params = [], options = {}) {
    // 检查账户是否已连接
    const accounts = await this.accountManager.getAccounts();
    if (accounts.length === 0) {
      return { success: false, error: '请先连接钱包' };
    }
    const fromAddress = accounts[0];

    try {
      // 创建合约实例
      const contract = new this.web3.eth.Contract(abi, contractAddress);
      
      // 准备调用参数
      const method = contract.methods[methodName](...params);
      
      // 如果是只读方法,直接调用
      if (method.call) {
        const result = await method.call({ from: fromAddress });
        return {
          success: true,
          isReadonly: true,
          result: result
        };
      }
      
      // 如果是需要发送交易的方法
      const gas = options.gas || await method.estimateGas({ from: fromAddress });
      const gasPrice = options.gasPrice || await this.web3.eth.getGasPrice();
      
      const txHash = await method.send({
        from: fromAddress,
        gas: gas,
        gasPrice: gasPrice,
        value: options.value || 0
      });
      
      return {
        success: true,
        isReadonly: false,
        txHash: txHash.transactionHash,
        explorerUrl: this.getTransactionExplorerUrl(txHash.transactionHash)
      };
    } catch (error) {
      console.error('合约方法调用失败:', error);
      return {
        success: false,
        error: error.message || '合约调用失败',
        txHash: null
      };
    }
  }
}

// 使用示例
// 假设已有accountManager实例
if (window.ethereum && accountManager) {
  const txHandler = new TransactionHandler(window.ethereum, accountManager);
  
  // 发送ETH按钮
  document.getElementById('sendEthBtn').addEventListener('click', async () => {
    const toAddress = document.getElementById('toAddress').value;
    const amount = document.getElementById('ethAmount').value;
    
    const result = await txHandler.sendEthTransaction(toAddress, amount);
    
    if (result.success) {
      showMessage(`交易已发送: ${result.txHash}`, 'success');
      if (result.explorerUrl) {
        document.getElementById('txLink').href = result.explorerUrl;
        document.getElementById('txLink').textContent = '查看交易详情';
        document.getElementById('txLink').style.display = 'block';
      }
    } else {
      showMessage(`交易失败: ${result.error}`, 'error');
    }
  });
  
  // 签名消息按钮
  document.getElementById('signMsgBtn').addEventListener('click', async () => {
    const message = document.getElementById('messageToSign').value || '请确认这是我的签名';
    
    const result = await txHandler.signMessage(message);
    
    if (result.success) {
      document.getElementById('signatureResult').textContent = result.signature;
      showMessage('消息签名成功', 'success');
    } else {
      showMessage(`签名失败: ${result.error}`, 'error');
    }
  });
}

核心要点

  • 交易发送使用eth_sendTransaction方法,需要构建包含发送方、接收方、金额等信息的交易参数
  • 实现 gas 估算机制,避免 gas 不足导致交易失败
  • 提供完善的交易错误处理,针对不同错误类型给出明确提示
  • 消息签名使用personal_sign方法,用于身份验证等场景
  • 支持合约交互,区分只读方法和需要发送交易的方法
  • 提供交易在区块浏览器的查询链接,方便用户追踪交易状态

五、消息的签名与验证

消息签名是用私钥对一段数据生成唯一的“证明”,验证是用公钥(地址)检查这个证明是否真的是那个人签的、而且内容没被改过。

// 签名(浏览器端)
const message = "登录验证: 你好,123";
const hexMessage = web3.utils.utf8ToHex(message); // 正确编码(多字节安全)
const signature = await window.ethereum.request({
  method: 'personal_sign',
  params: [hexMessage, account] // MetaMask 要求 [hexMessage, address]
});

// 验证(浏览器或后端)
const recovered = web3.eth.accounts.recover(message, signature);
console.log(recovered.toLowerCase() === account.toLowerCase()); // true 即验证通过

核心要点

  • 签名不可伪造 —— 只有私钥持有人能生成该签名
  • 内容防篡改 —— 签名绑定了原始消息,任何改动都会导致验证失败
  • 可公开验证 —— 任何人都能用签名+消息恢复出签名者的公钥/地址
  • 无需泄露私钥 —— 验证过程只用公钥(区块链地址),私钥始终安全保管
  • 常用于身份认证与交易授权 —— 登录、授权操作、链上链下数据证明等场景

以上五个核心功能涵盖了与 MetaMask 钱包交互的主要场景,实际应用中可以根据需求进行组合和扩展。每个功能模块都保持了相对独立性,便于维护和复用。


网站公告

今日签到

点亮在社区的每一天
去签到