提示:以下项目均基于测试网进行,千万不要基于主网搭建项目,如造成虚拟财产损失,概不负责呀,一定要谨慎!!!
使用 React + ethers.js + Vite 创建项目。
实现“连接MetaMask钱包”功能。
显示用户的Sepolia USDC 余额和地址。
实现ERC-20代币余额查询和转账功能。
网络名称:
SepoliaRPC URL:
https://sepolia.infura.io/v3/链ID:
11155111货币符号:
ETH区块浏览器:
https://sepolia.etherscan.io/
打开终端,进入你希望创建项目的目录,然后运行:
npm create vite@latest sepolia-wallet -- --template reactcd sepolia-walletnpm install
sepolia-wallet 的新React项目。
安装必要的依赖库:
npm install ethersnpm install vite-plugin-environment --save-dev
项目结构和代码:基本如下
sepolia-wallet/├── public/├── src/│ ├── App.jsx (或 App.tsx)│ ├── main.jsx│ └── ...├── package.json└── ...
合约地址:
0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238符号: USDC
小数位: 6
src/App.jsx
// src/App.jsximport { useState, useEffect } from 'react';import { ethers } from 'ethers';import './App.css';// 使用Sepolia USDC合约地址const TOKEN_ADDRESS = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";// 简化的ERC-20 ABI - 只包含我们需要的函数const ERC20_ABI = ["function name() view returns (string)","function symbol() view returns (string)","function decimals() view returns (uint8)","function balanceOf(address) view returns (uint256)","function transfer(address to, uint amount) returns (bool)"];function App() {const [hasProvider, setHasProvider] = useState(!!window.ethereum);const [account, setAccount] = useState(null);const [balance, setBalance] = useState({ eth: "0", token: "0" });const [tokenInfo, setTokenInfo] = useState({name: "USD Coin",symbol: "USDC",decimals: 6});const [isLoading, setIsLoading] = useState(false);const [toAddress, setToAddress] = useState("");const [transferAmount, setTransferAmount] = useState("");// 检查用户是否安装了MetaMaskuseEffect(() => {const checkProvider = () => {setHasProvider(!!window.ethereum);};checkProvider();return () => {// 清理函数if (window.ethereum) {window.ethereum.removeAllListeners('accountsChanged');}};}, []);// 连接钱包函数const connectWallet = async () => {if (!window.ethereum) {alert("请安装MetaMask!");return;}try {setIsLoading(true);// 请求连接账户const accounts = await window.ethereum.request({method: "eth_requestAccounts",});const account = accounts[0];setAccount(account);// 获取余额和代币信息await updateBalances(account);await fetchTokenInfo();} catch (error) {console.error("连接钱包失败:", error);alert("连接失败: " + (error.message || "未知错误"));} finally {setIsLoading(false);}};// 获取代币信息const fetchTokenInfo = async () => {if (!window.ethereum) return;try {const provider = new ethers.BrowserProvider(window.ethereum);const tokenContract = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, provider);const [name, symbol, decimals] = await Promise.all([tokenContract.name(),tokenContract.symbol(),tokenContract.decimals()]);setTokenInfo({ name, symbol, decimals });} catch (error) {console.error("获取代币信息失败:", error);}};// 更新ETH和代币余额const updateBalances = async (account) => {if (!account || !window.ethereum) return;try {// 创建Providerconst provider = new ethers.BrowserProvider(window.ethereum);// 获取ETH余额const ethBalance = await provider.getBalance(account);const formattedEth = ethers.formatEther(ethBalance);// 获取代币余额const tokenContract = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, provider);const tokenBalance = await tokenContract.balanceOf(account);const formattedToken = ethers.formatUnits(tokenBalance, tokenInfo.decimals);setBalance({eth: parseFloat(formattedEth).toFixed(6),token: parseFloat(formattedToken).toFixed(2)});} catch (error) {console.error("获取余额失败:", error);}};// 转账函数const handleTransfer = async (e) => {e.preventDefault();if (!account || !toAddress || !transferAmount) {alert("请填写完整信息");return;}// 验证地址格式if (!ethers.isAddress(toAddress)) {alert("请输入有效的以太坊地址");return;}try {setIsLoading(true);const provider = new ethers.BrowserProvider(window.ethereum);const signer = await provider.getSigner();const tokenContract = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, signer);// 将金额转换为合约所需格式const amountInUnits = ethers.parseUnits(transferAmount, tokenInfo.decimals);// 发送转账交易const transaction = await tokenContract.transfer(toAddress, amountInUnits);console.log("交易已发送,哈希:", transaction.hash);alert(`转账已提交!\n交易哈希: ${transaction.hash}\n请等待确认...`);// 等待交易确认const receipt = await transaction.wait();console.log("交易已确认:", receipt);alert("✅ 转账成功!");// 更新余额await updateBalances(account);// 清空表单setToAddress("");setTransferAmount("");} catch (error) {console.error("转账失败:", error);let errorMessage = "转账失败: ";if (error.reason) {errorMessage += error.reason;} else if (error.message) {errorMessage += error.message;} else {errorMessage += "未知错误";}alert(errorMessage);} finally {setIsLoading(false);}};// 复制地址功能const copyAddress = () => {navigator.clipboard.writeText(account);alert("地址已复制到剪贴板!");};return (<div className="app"><header className="app-header"><h1>🪙 Sepolia USDC 钱包</h1><p>简单的ERC-20代币钱包演示</p></header><main className="app-main">{!account ? (<div className="connect-section"><buttononClick={connectWallet}disabled={isLoading || !hasProvider}className="connect-button">{isLoading ? "连接中..." : "连接MetaMask钱包"}</button>{!hasProvider && (<p className="warning">⚠️ 检测到未安装MetaMask,请先安装扩展程序。</p>)}</div>) : (<div className="wallet-section">{/* 账户信息 */}<div className="account-info"><h2>👤 我的账户</h2><p><strong>地址:</strong><span className="address" onClick={copyAddress}>{`${account.slice(0, 8)}...${account.slice(-6)}`}</span></p><p><strong>ETH余额:</strong> {balance.eth} ETH</p><p><strong>USDC余额:</strong> {balance.token} USDC</p></div>{/* 代币信息 */}<div className="token-info-simple"><h3>📊 当前代币</h3><p><strong>{tokenInfo.name}</strong> ({tokenInfo.symbol})</p><p>小数位: {tokenInfo.decimals}</p></div>{/* 转账表单 */}<div className="transfer-section"><h2>💸 转账 USDC</h2><form onSubmit={handleTransfer}><div className="form-group"><label>收款地址:</label><inputtype="text"value={toAddress}onChange={(e) => setToAddress(e.target.value)}placeholder="0x..."disabled={isLoading}/></div><div className="form-group"><label>金额 (USDC):</label><inputtype="number"step="any"value={transferAmount}onChange={(e) => setTransferAmount(e.target.value)}placeholder="0.00"disabled={isLoading}/></div><buttontype="submit"disabled={isLoading || !toAddress || !transferAmount}className="transfer-button">{isLoading ? "处理中..." : "发送 USDC"}</button></form></div>{/* 操作按钮 */}<div className="action-buttons"><buttononClick={() => updateBalances(account)}disabled={isLoading}className="refresh-button">🔄 刷新余额</button></div>{/* 使用提示 */}<div className="tips"><h3>💡 使用提示</h3><ul><li>确保MetaMask连接到<strong>Sepolia测试网</strong></li><li>需要Sepolia ETH来支付Gas费</li><li>可以在Uniswap测试网用ETH兑换USDC</li><li>所有操作都在测试网,没有真实价值</li></ul></div></div>)}</main></div>);}export default App;
src/App.css
/* src/App.css */* {box-sizing: border-box;margin: 0;padding: 0;}body {font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);min-height: 100vh;color: #333;line-height: 1.6;}.app {max-width: 500px;margin: 0 auto;padding: 20px;min-height: 100vh;}.app-header {text-align: center;margin-bottom: 30px;color: white;}.app-header h1 {font-size: 2.2rem;margin-bottom: 8px;}.app-header p {opacity: 0.9;font-size: 1.1rem;}.connect-section, .wallet-section {background: white;border-radius: 16px;padding: 25px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);}.connect-button, .transfer-button, .refresh-button {width: 100%;padding: 15px;border: none;border-radius: 12px;font-size: 1.1rem;font-weight: 600;cursor: pointer;transition: all 0.3s ease;margin-bottom: 15px;}.connect-button {background: linear-gradient(135deg, #4CAF50, #45a049);color: white;}.connect-button:disabled {background: #cccccc;cursor: not-allowed;}.connect-button:hover:not(:disabled) {transform: translateY(-2px);box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);}.warning {color: #ff6b6b;text-align: center;margin-top: 15px;font-weight: 500;}.account-info {background: #f8f9fa;border-radius: 12px;padding: 20px;margin-bottom: 20px;}.account-info h2 {color: #495057;margin-bottom: 15px;font-size: 1.3rem;}.account-info p {margin: 10px 0;font-size: 1rem;}.address {background: #e9ecef;padding: 4px 8px;border-radius: 6px;font-family: monospace;margin-left: 8px;cursor: pointer;user-select: none;border: 1px dashed #adb5bd;}.address:hover {background: #dee2e6;}.token-info-simple {background: #e3f2fd;border: 1px solid #90caf9;border-radius: 12px;padding: 15px;margin-bottom: 20px;}.token-info-simple h3 {color: #1565c0;margin-bottom: 10px;font-size: 1.1rem;}.token-info-simple p {margin: 5px 0;font-size: 0.95rem;}.transfer-section {border-top: 2px solid #e9ecef;padding-top: 20px;margin-bottom: 20px;}.transfer-section h2 {color: #495057;margin-bottom: 20px;font-size: 1.3rem;}.form-group {margin-bottom: 20px;}.form-group label {display: block;margin-bottom: 8px;font-weight: 600;color: #495057;}.form-group input {width: 100%;padding: 12px 15px;border: 2px solid #e9ecef;border-radius: 8px;font-size: 1rem;transition: border-color 0.3s ease;}.form-group input:focus {outline: none;border-color: #667eea;}.form-group input:disabled {background-color: #f8f9fa;cursor: not-allowed;}.transfer-button {background: linear-gradient(135deg, #667eea, #764ba2);color: white;}.transfer-button:disabled {background: #cccccc;cursor: not-allowed;}.transfer-button:hover:not(:disabled) {transform: translateY(-2px);box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);}.refresh-button {background: #6c757d;color: white;}.refresh-button:hover:not(:disabled) {background: #5a6268;transform: translateY(-1px);}.tips {background: #fff3cd;border: 1px solid #ffeaa7;border-radius: 12px;padding: 20px;margin-top: 20px;}.tips h3 {color: #856404;margin-bottom: 10px;font-size: 1.1rem;}.tips ul {margin-left: 20px;}.tips li {margin: 8px 0;font-size: 0.9rem;color: #856404;}/* 响应式设计 */@media (max-width: 480px) {.app {padding: 15px;}.app-header h1 {font-size: 1.8rem;}.connect-section, .wallet-section {padding: 20px;}}
npm run dev

