下面将详细讲解 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 钱包交互的主要场景,实际应用中可以根据需求进行组合和扩展。每个功能模块都保持了相对独立性,便于维护和复用。