大数跨境
0
0

Web3日记——简单钱包DApp实现

Web3日记——简单钱包DApp实现 David的跨境日记
2025-10-23
10
导读:“日拱一卒,功不唐捐”提示:以下项目均基于测试网进行,如有基于主网搭建项目,造成虚拟财产损失,概不负责呀!
“日拱一卒,功不唐捐”


提示:以下项目均基于测试网进行,千万不要基于主网搭建项目,如造成虚拟财产损失,概不负责呀,一定要谨慎!!!

今天我们简单构建一个网页版Sepolia代币钱包,实现连接钱包、查询余额、转账等简单功能。提供相对友好的UI界面。纯干货,无保留。
以后有机会再做更复杂些的功能。
基本流程:
  • 使用 React + ethers.js + Vite 创建项目。

  • 实现“连接MetaMask钱包”功能。

  • 显示用户的Sepolia USDC 余额和地址。

  • 实现ERC-20代币余额查询和转账功能。



一、环境准备
学习目的,基于Sepolia USDC测试网代币搭建测试项目。
1、安装Node.js、Visual Studio Code
2、确认浏览器中安装了Metamask扩展程序。
⚠️:在MetaMask网络下拉菜单中选择Sepolia网络,如果没有,可以手动添加:
  • 网络名称: Sepolia

  • RPC URL: https://sepolia.infura.io/v3/

  • 链ID: 11155111

  • 货币符号: ETH

  • 区块浏览器: https://sepolia.etherscan.io/


3、从水龙头(Faucets)获取测试ETH
这一步是必要的,因为App创建后,需要连接钱包,实现USDC转账功能,需要支付交易费Transaction Fee.怎么获得?可以看下我之前智能合约部署的文章,里面有提到。
二、创建React项目并安装依赖

打开终端,进入你希望创建项目的目录,然后运行:

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└── ...


三、代码编写
本次使用的测试代币合约是:Sepolia USDC
  • 合约地址: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238

  • 符号: USDC

  • 小数位: 6

可以在https://sepolia.etherscan.io/(公网测试网)中查看到该合约:
下面开始写代码:
Visual Studio code打开如下文件并替换代码:
1、核心文件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"    decimals6   });  const [isLoading, setIsLoading] = useState(false);  const [toAddress, setToAddress] = useState("");  const [transferAmount, setTransferAmount] = useState("");
  // 检查用户是否安装了MetaMask  useEffect(() => {    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.ethereumreturn;
    try {      const provider = new ethers.BrowserProvider(window.ethereum);      const tokenContract = new ethers.Contract(TOKEN_ADDRESSERC20_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.ethereumreturn;
    try {      // 创建Provider      const provider = new ethers.BrowserProvider(window.ethereum);
      // 获取ETH余额      const ethBalance = await provider.getBalance(account);      const formattedEth = ethers.formatEther(ethBalance);
      // 获取代币余额      const tokenContract = new ethers.Contract(TOKEN_ADDRESSERC20_ABI, provider);      const tokenBalance = await tokenContract.balanceOf(account);      const formattedToken = ethers.formatUnits(tokenBalance, tokenInfo.decimals);
      setBalance({        ethparseFloat(formattedEth).toFixed(6),        tokenparseFloat(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_ADDRESSERC20_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">            <button               onClick={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>                  <input                    type="text"                    value={toAddress}                    onChange={(e) => setToAddress(e.target.value)}                    placeholder="0x..."                    disabled={isLoading}                  />                </div>                <div className="form-group">                  <label>金额 (USDC):</label>                  <input                    type="number"                    step="any"                    value={transferAmount}                    onChange={(e) => setTransferAmount(e.target.value)}                    placeholder="0.00"                    disabled={isLoading}                  />                </div>                <button                   type="submit"                   disabled={isLoading || !toAddress || !transferAmount}                  className="transfer-button"                >                  {isLoading ? "处理中..." : "发送 USDC"}                </button>              </form>            </div>
            {/* 操作按钮 */}            <div className="action-buttons">              <button                 onClick={() => 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;
2、简化版 src/App.css
/* src/App.css */* {  box-sizing: border-box;  margin0;  padding0;}
body {  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI''Roboto', sans-serif;  backgroundlinear-gradient(135deg#667eea 0%#764ba2 100%);  min-height100vh;  color#333;  line-height1.6;}
.app {  max-width500px;  margin0 auto;  padding20px;  min-height100vh;}
.app-header {  text-align: center;  margin-bottom30px;  color: white;}
.app-header h1 {  font-size2.2rem;  margin-bottom8px;}
.app-header p {  opacity0.9;  font-size1.1rem;}
.connect-section.wallet-section {  background: white;  border-radius16px;  padding25px;  box-shadow0 10px 30px rgba(0000.2);}
.connect-button.transfer-button.refresh-button {  width100%;  padding15px;  border: none;  border-radius12px;  font-size1.1rem;  font-weight600;  cursor: pointer;  transition: all 0.3s ease;  margin-bottom15px;}
.connect-button {  backgroundlinear-gradient(135deg#4CAF50#45a049);  color: white;}
.connect-button:disabled {  background#cccccc;  cursor: not-allowed;}
.connect-button:hover:not(:disabled) {  transformtranslateY(-2px);  box-shadow0 5px 15px rgba(0000.2);}
.warning {  color#ff6b6b;  text-align: center;  margin-top15px;  font-weight500;}
.account-info {  background#f8f9fa;  border-radius12px;  padding20px;  margin-bottom20px;}
.account-info h2 {  color#495057;  margin-bottom15px;  font-size1.3rem;}
.account-info p {  margin10px 0;  font-size1rem;}
.address {  background#e9ecef;  padding4px 8px;  border-radius6px;  font-family: monospace;  margin-left8px;  cursor: pointer;  user-select: none;  border1px dashed #adb5bd;}
.address:hover {  background#dee2e6;}
.token-info-simple {  background#e3f2fd;  border1px solid #90caf9;  border-radius12px;  padding15px;  margin-bottom20px;}
.token-info-simple h3 {  color#1565c0;  margin-bottom10px;  font-size1.1rem;}
.token-info-simple p {  margin5px 0;  font-size0.95rem;}
.transfer-section {  border-top2px solid #e9ecef;  padding-top20px;  margin-bottom20px;}
.transfer-section h2 {  color#495057;  margin-bottom20px;  font-size1.3rem;}
.form-group {  margin-bottom20px;}
.form-group label {  display: block;  margin-bottom8px;  font-weight600;  color#495057;}
.form-group input {  width100%;  padding12px 15px;  border2px solid #e9ecef;  border-radius8px;  font-size1rem;  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 {  backgroundlinear-gradient(135deg#667eea#764ba2);  color: white;}
.transfer-button:disabled {  background#cccccc;  cursor: not-allowed;}
.transfer-button:hover:not(:disabled) {  transformtranslateY(-2px);  box-shadow0 5px 15px rgba(1021262340.4);}
.refresh-button {  background#6c757d;  color: white;}
.refresh-button:hover:not(:disabled) {  background#5a6268;  transformtranslateY(-1px);}
.tips {  background#fff3cd;  border1px solid #ffeaa7;  border-radius12px;  padding20px;  margin-top20px;}
.tips h3 {  color#856404;  margin-bottom10px;  font-size1.1rem;}
.tips ul {  margin-left20px;}
.tips li {  margin8px 0;  font-size0.9rem;  color#856404;}
/* 响应式设计 */@media (max-width480px) {  .app {    padding15px;  }
  .app-header h1 {    font-size1.8rem;  }
  .connect-section.wallet-section {    padding20px;  }}

四、运行你的DApp
在项目根目录下运行:
npm run dev
可能的问题,Node.js的版本不对,用nvm切换到最新版本就行
运行成功后:
在浏览器中访问提示的local地址,可以看到如下界面:

五、连接钱包
点击连接MetaMask钱包,授权后,钱包选择Sepolia网络
此时UI显示如下:

六、测试转账Sepolia USDC
要测试转账,必须获取Sepolia USDC代币,目前我找到的水龙头,注意选择正确的网络和输入你的钱包地址,可以免费获取10USDC,不知是否有限制。我获取了20个USDC,只是测试币,无任何价值
获取测试代币后,可以在开发的DApp界面看到收到的USDC代币个数,可以尝试给另一个人转账。不再赘述。

总结:至此,我们完整开发了一个简单的DApp,大家也可以根据测试合约地址,找到更多ABI函数使用,开发更多功能。
本次学习了DApp的简单开发流程,了解了智能合约和DApp的关系,希望能和大家一起进步,继续学习中。有任何问题或者错误也希望大家指正。

【声明】内容源于网络
0
0
David的跨境日记
跨境分享营 | 持续分享跨境心得
内容 46537
粉丝 1
David的跨境日记 跨境分享营 | 持续分享跨境心得
总阅读266.3k
粉丝1
内容46.5k