Web3钱包开发:使用Ethers.js集成MetaMask
大家好,我是欧阳瑞(Rich Own)。今天想和大家聊聊Web3钱包开发这个话题。作为一个Web3探索者,我经常需要在DApp中集成钱包功能。今天就来分享一下如何使用Ethers.js与MetaMask进行集成。
为什么需要钱包集成?
在Web3世界中,钱包是用户与区块链交互的桥梁。通过钱包,用户可以:
- 管理加密货币资产
- 签署交易
- 与智能合约交互
- 验证身份
什么是MetaMask?
MetaMask是目前最流行的以太坊钱包浏览器扩展。它允许用户安全地管理以太坊账户,并与DApp进行交互。
Ethers.js简介
Ethers.js是一个强大的以太坊JavaScript库,提供了与以太坊区块链交互的所有功能。
环境准备
bash
# 安装Ethers.js
npm install ethers
# 或使用yarn
yarn add ethers
基础集成
1. 检测MetaMask是否安装
javascript
async function checkMetaMask() {
if (typeof window.ethereum !== 'undefined') {
console.log('MetaMask is installed!');
return true;
} else {
console.log('MetaMask is not installed');
return false;
}
}
2. 连接钱包
javascript
async function connectWallet() {
try {
if (!await checkMetaMask()) {
alert('请先安装MetaMask');
return;
}
// 请求连接钱包
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const account = accounts[0];
console.log('Connected account:', account);
return account;
} catch (error) {
console.error('连接失败:', error);
throw error;
}
}
3. 获取账户信息
javascript
async function getAccountInfo() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
// 获取账户地址
const address = await signer.getAddress();
console.log('地址:', address);
// 获取余额
const balance = await provider.getBalance(address);
console.log('余额:', ethers.utils.formatEther(balance), 'ETH');
// 获取链ID
const chainId = await provider.getNetwork();
console.log('链ID:', chainId.chainId);
return { address, balance, chainId };
}
监听账户和链变化
javascript
// 监听账户变化
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
console.log('用户已断开连接');
} else {
console.log('账户已切换:', accounts[0]);
}
});
// 监听链变化
window.ethereum.on('chainChanged', (chainId) => {
console.log('链已切换:', chainId);
// 刷新页面以适配新链
window.location.reload();
});
// 监听连接状态
window.ethereum.on('connect', (info) => {
console.log('已连接:', info);
});
window.ethereum.on('disconnect', (error) => {
console.log('连接已断开:', error);
});
与智能合约交互
1. 读取合约数据
javascript
async function readFromContract() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
// 合约地址和ABI
const contractAddress = '0x...';
const contractABI = [
'function balanceOf(address owner) view returns (uint256)',
'function name() view returns (string)',
'function symbol() view returns (string)'
];
// 创建合约实例
const contract = new ethers.Contract(contractAddress, contractABI, provider);
// 调用合约方法
const name = await contract.name();
const symbol = await contract.symbol();
const balance = await contract.balanceOf('0x...');
console.log('代币名称:', name);
console.log('代币符号:', symbol);
console.log('余额:', ethers.utils.formatUnits(balance, 18));
}
2. 写入合约数据
javascript
async function writeToContract() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contractAddress = '0x...';
const contractABI = [
'function transfer(address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)'
];
// 使用signer创建合约实例
const contract = new ethers.Contract(contractAddress, contractABI, signer);
try {
// 构建交易
const tx = await contract.transfer(
'0xRecipientAddress',
ethers.utils.parseEther('1.0')
);
console.log('交易哈希:', tx.hash);
// 等待交易确认
const receipt = await tx.wait();
console.log('交易已确认:', receipt);
return receipt;
} catch (error) {
console.error('交易失败:', error);
throw error;
}
}
发送原生代币
javascript
async function sendEther(to, amount) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
try {
const tx = await signer.sendTransaction({
to: to,
value: ethers.utils.parseEther(amount)
});
console.log('交易哈希:', tx.hash);
const receipt = await tx.wait();
console.log('交易已确认:', receipt);
return receipt;
} catch (error) {
console.error('发送失败:', error);
throw error;
}
}
处理不同的网络
javascript
const NETWORKS = {
1: 'mainnet',
5: 'goerli',
11155111: 'sepolia',
137: 'polygon',
80001: 'polygon-mumbai'
};
async function getCurrentNetwork() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const network = await provider.getNetwork();
return NETWORKS[network.chainId] || 'unknown';
}
async function switchNetwork(chainId) {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: ethers.utils.hexValue(chainId) }]
});
} catch (error) {
// 如果链不存在,需要添加
if (error.code === 4902) {
await addNetwork(chainId);
} else {
throw error;
}
}
}
async function addNetwork(chainId) {
const networkConfig = {
137: {
chainId: '0x89',
chainName: 'Polygon Mainnet',
rpcUrls: ['https://polygon-rpc.com'],
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18
},
blockExplorerUrls: ['https://polygonscan.com']
}
// 添加更多网络配置...
};
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [networkConfig[chainId]]
});
}
完整的钱包组件示例
tsx
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
export default function WalletConnector() {
const [account, setAccount] = useState<string | null>(null);
const [balance, setBalance] = useState<string>('0');
const [network, setNetwork] = useState<string>('');
useEffect(() => {
// 检查是否已连接
checkConnection();
// 设置监听
window.ethereum?.on('accountsChanged', handleAccountsChanged);
window.ethereum?.on('chainChanged', handleChainChanged);
return () => {
window.ethereum?.removeListener('accountsChanged', handleAccountsChanged);
window.ethereum?.removeListener('chainChanged', handleChainChanged);
};
}, []);
const checkConnection = async () => {
if (!window.ethereum) return;
const provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await provider.listAccounts();
if (accounts.length > 0) {
setAccount(accounts[0]);
await updateBalance(accounts[0]);
await updateNetwork();
}
};
const handleAccountsChanged = async (accounts: string[]) => {
if (accounts.length > 0) {
setAccount(accounts[0]);
await updateBalance(accounts[0]);
} else {
setAccount(null);
setBalance('0');
}
};
const handleChainChanged = async () => {
await updateNetwork();
};
const updateBalance = async (addr: string) => {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const bal = await provider.getBalance(addr);
setBalance(ethers.utils.formatEther(bal));
};
const updateNetwork = async () => {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const net = await provider.getNetwork();
setNetwork(net.name || 'Unknown');
};
const connect = async () => {
try {
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send('eth_requestAccounts', []);
} catch (error) {
console.error('连接失败:', error);
}
};
const disconnect = async () => {
// MetaMask不支持直接断开连接
// 用户需要手动在MetaMask中断开
alert('请在MetaMask中手动断开连接');
};
const switchToPolygon = async () => {
await switchNetwork(137);
};
if (!window.ethereum) {
return <div>请安装MetaMask</div>;
}
return (
<div className="wallet-connector">
{account ? (
<div>
<p>账户: {account}</p>
<p>余额: {balance} ETH</p>
<p>网络: {network}</p>
<button onClick={disconnect}>断开连接</button>
<button onClick={switchToPolygon}>切换到Polygon</button>
</div>
) : (
<button onClick={connect}>连接钱包</button>
)}
</div>
);
}
安全最佳实践
1. 验证交易参数
javascript
async function safeTransfer(to, amount) {
// 验证地址格式
if (!ethers.utils.isAddress(to)) {
throw new Error('无效的地址');
}
// 验证金额
if (ethers.utils.parseEther(amount).isZero()) {
throw new Error('金额不能为0');
}
// 检查余额
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const balance = await signer.getBalance();
if (balance.lt(ethers.utils.parseEther(amount))) {
throw new Error('余额不足');
}
// 执行交易
const tx = await signer.sendTransaction({ to, value: ethers.utils.parseEther(amount) });
return tx;
}
2. 使用硬件钱包
javascript
// 使用Ledger或Trezor
async function connectHardwareWallet() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
// 请求硬件钱包账户
const accounts = await provider.send('eth_requestAccounts', []);
return accounts[0];
}
3. 显示交易确认
javascript
async function confirmTransaction(txData) {
const confirmation = window.confirm(
`确认交易:\n` +
`目标地址: ${txData.to}\n` +
`金额: ${ethers.utils.formatEther(txData.value)} ETH\n` +
`Gas费用: ${ethers.utils.formatEther(txData.gasLimit * txData.gasPrice)} ETH`
);
if (!confirmation) {
throw new Error('用户取消交易');
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const tx = await signer.sendTransaction(txData);
return tx;
}
常见问题处理
1. MetaMask未安装
javascript
function handleNoMetaMask() {
const installUrl = 'https://metamask.io/download/';
if (confirm('请安装MetaMask以继续使用此DApp')) {
window.open(installUrl, '_blank');
}
}
2. 用户拒绝连接
javascript
async function connectWithErrorHandling() {
try {
await connectWallet();
} catch (error) {
if (error.code === 4001) {
console.log('用户拒绝连接');
} else {
console.error('连接错误:', error);
}
}
}
3. 交易失败
javascript
async function handleTransactionError(error) {
if (error.code === 4001) {
console.log('用户拒绝交易');
} else if (error.code === -32000) {
console.log('交易失败:', error.message);
} else {
console.error('未知错误:', error);
}
}
总结
使用Ethers.js集成MetaMask是Web3开发的基础技能。掌握这些知识后,你就可以开发出功能丰富的DApp了。
我的鬃狮蜥Hash对Web3也很感兴趣------它每天看着我在MetaMask中管理资产,似乎也想拥有自己的加密钱包。也许有一天,我会为它创建一个专属的NFT收藏系统。
如果你有Web3开发方面的问题,欢迎留言交流!我是欧阳瑞,Web3探索之路,我们一起前行!
技术栈:Ethers.js · MetaMask · Ethereum · DApp