├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── USAGE.md ├── contracts └── DeFiToken.sol ├── env.example ├── hardhat.config.js ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts └── deploy.js ├── src ├── app │ ├── api │ │ ├── assets │ │ │ └── route.ts │ │ ├── balance │ │ │ └── route.ts │ │ ├── prices │ │ │ └── route.ts │ │ └── transactions │ │ │ └── route.ts │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── providers.tsx ├── components │ ├── ApiStatus.tsx │ ├── AssetCard.tsx │ ├── ErrorMessage.tsx │ ├── Header.tsx │ ├── LoadingSpinner.tsx │ ├── Toast.tsx │ └── TransactionCard.tsx ├── hooks │ └── useToast.ts ├── services │ ├── api.ts │ └── clientApi.ts ├── store │ └── useWalletStore.ts ├── types │ └── index.ts └── utils │ ├── constants.ts │ └── format.ts ├── start.sh ├── tailwind.config.js ├── test └── DeFiToken.test.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Blockchain Network Configuration 2 | ALCHEMY_API_KEY=your_alchemy_api_key_here 3 | NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_wallet_connect_project_id_here 4 | 5 | # Contract Deployment (for Hardhat) 6 | SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_KEY 7 | PRIVATE_KEY=your_private_key_here 8 | ETHERSCAN_API_KEY=your_etherscan_api_key_here 9 | 10 | # 支持的网络配置 - 默认sepolia测试网 11 | ETHERSCAN_NETWORK=https://api-sepolia.etherscan.io/api 12 | 13 | # Development 14 | NEXT_PUBLIC_ENVIRONMENT=development -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | 4 | .*.local 5 | 6 | ui.html 7 | 8 | BLOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeFi Dashboard 2 | 3 | 现代化的去中心化金融(DeFi)资产管理面板,支持连接钱包查看真实的资产余额和交易记录。 4 | 5 | ## ✨ 功能特性 6 | 7 | - 🔗 **钱包连接**: 支持 MetaMask、WalletConnect 等主流钱包 8 | - 💰 **真实资产数据**: 显示用户的 ETH 和 ERC20 代币余额 9 | - 📊 **投资组合概览**: 实时计算总资产价值和持仓分布 10 | - 📝 **交易记录**: 显示最近的 ETH 转账和代币转账记录 11 | - 🌙 **暗色主题**: 现代化的深色界面设计 12 | - 📱 **响应式设计**: 完美适配桌面和移动设备 13 | - ⚡ **实时数据**: 集成 Etherscan 和 CoinGecko API 14 | 15 | ## 🛠 技术栈 16 | 17 | - **前端框架**: Next.js + TypeScript 18 | - **样式**: Tailwind CSS 19 | - **钱包集成**: wagmi + RainbowKit 20 | - **状态管理**: Zustand 21 | - **区块链数据**: Etherscan API 22 | - **价格数据**: CoinGecko API 23 | - **图标**: Lucide React 24 | 25 | ## 🚀 快速开始 26 | 27 | ### 1. 克隆项目 28 | 29 | ```bash 30 | git clone 31 | cd dapp-demo 32 | ``` 33 | 34 | ### 2. 安装依赖 35 | 36 | ```bash 37 | npm install 38 | ``` 39 | 40 | ### 3. 配置环境变量 41 | 42 | 复制环境变量示例文件: 43 | 44 | ```bash 45 | cp env.example .env.local 46 | ``` 47 | 48 | 编辑 `.env.local` 文件,填入必要的 API 密钥: 49 | 50 | ```env 51 | # WalletConnect Project ID (必需) 52 | NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_wallet_connect_project_id 53 | 54 | # Alchemy API Key (必需) 55 | ALCHEMY_API_KEY=your_alchemy_api_key 56 | 57 | # Etherscan API Key (必需) 58 | ETHERSCAN_API_KEY=your_etherscan_api_key 59 | 60 | # CoinGecko API Key (可选) 61 | COINGECKO_API_KEY=your_coingecko_api_key 62 | ``` 63 | 64 | ### 4. 获取 API 密钥 65 | 66 | #### WalletConnect Project ID 67 | 1. 访问 [WalletConnect Cloud](https://cloud.walletconnect.com/) 68 | 2. 创建新项目 69 | 3. 复制 Project ID 70 | 71 | #### Alchemy API Key 72 | 1. 访问 [Alchemy](https://www.alchemy.com/) 73 | 2. 创建免费账户 74 | 3. 创建新的 App (选择 Ethereum Mainnet) 75 | 4. 复制 API Key 76 | 77 | #### Etherscan API Key 78 | 1. 访问 [Etherscan](https://etherscan.io/apis) 79 | 2. 创建免费账户 80 | 3. 生成 API Key 81 | 82 | #### CoinGecko API Key (可选) 83 | 1. 访问 [CoinGecko](https://www.coingecko.com/en/api) 84 | 2. 免费版本无需 API Key 85 | 3. 付费版本可获得更高的请求限制 86 | 87 | ### 5. 启动开发服务器 88 | 89 | ```bash 90 | npm run dev 91 | # 或使用启动脚本 92 | ./start.sh 93 | ``` 94 | 95 | 访问 [http://localhost:3000](http://localhost:3000) 查看应用。 96 | 97 | ## 📱 使用说明 98 | 99 | ### 连接钱包 100 | 1. 点击右上角的"连接钱包"按钮 101 | 2. 选择您的钱包(推荐 MetaMask) 102 | 3. 确认连接请求 103 | 104 | ### 查看资产 105 | - 连接钱包后,系统会自动获取您的资产数据 106 | - 支持显示 ETH、USDC、UNI、LINK 等主要代币 107 | - 实时显示代币数量、美元价值和24小时涨跌幅 108 | 109 | ### 查看交易记录 110 | - 显示最近的 ETH 转账记录 111 | - 显示 ERC20 代币转账记录 112 | - 点击交易哈希可跳转到 Etherscan 查看详情 113 | 114 | ### 刷新数据 115 | - 点击投资组合概览中的"刷新"按钮 116 | - 系统会重新获取最新的资产和交易数据 117 | 118 | ## 🔧 项目结构 119 | 120 | ``` 121 | src/ 122 | ├── app/ # Next.js 应用路由 123 | ├── components/ # React 组件 124 | │ ├── AssetCard.tsx # 资产卡片组件 125 | │ ├── TransactionCard.tsx # 交易记录组件 126 | │ ├── Header.tsx # 头部导航 127 | │ ├── LoadingSpinner.tsx # 加载动画 128 | │ └── ErrorMessage.tsx # 错误提示 129 | ├── services/ # API 服务 130 | │ └── api.ts # 区块链数据服务 131 | ├── store/ # 状态管理 132 | │ └── useWalletStore.ts # 钱包状态 133 | ├── utils/ # 工具函数 134 | │ ├── constants.ts # 常量配置 135 | │ └── format.ts # 格式化函数 136 | └── providers/ # 上下文提供者 137 | └── Providers.tsx # 应用提供者 138 | ``` 139 | 140 | ## 🌐 支持的网络 141 | 142 | - **Ethereum Mainnet**: 主要支持网络 143 | - **Sepolia Testnet**: 测试网络支持 144 | 145 | ## 🪙 支持的代币 146 | 147 | - **ETH**: 以太坊原生代币 148 | - **USDC**: USD Coin 稳定币 149 | - **UNI**: Uniswap 治理代币 150 | - **LINK**: Chainlink 预言机代币 151 | - **USDT**: Tether 稳定币 152 | - **DAI**: MakerDAO 稳定币 153 | - **WBTC**: Wrapped Bitcoin 154 | 155 | ## 🔒 安全说明 156 | 157 | - 本应用只读取区块链数据,不会请求私钥或助记词 158 | - 所有交易都需要通过您的钱包确认 159 | - API 密钥仅用于获取公开的区块链数据 160 | - 建议在主网使用前先在测试网测试 161 | 162 | ## 🚨 故障排除 163 | 164 | ### 钱包连接失败 165 | - 确保已安装 MetaMask 或其他支持的钱包 166 | - 检查钱包是否已解锁 167 | - 尝试刷新页面重新连接 168 | 169 | ### 数据加载失败 170 | - 检查网络连接 171 | - 确认 API 密钥配置正确 172 | - 查看浏览器控制台的错误信息 173 | 174 | ### API 请求限制 175 | - Etherscan 免费版有请求频率限制 176 | - CoinGecko 免费版有请求次数限制 177 | - 考虑升级到付费版本获得更高限制 178 | 179 | ## 🤝 贡献 180 | 181 | 欢迎提交 Issue 和 Pull Request! 182 | 183 | ## 📄 许可证 184 | 185 | MIT License 186 | 187 | ## 🔗 相关链接 188 | 189 | - [Next.js 文档](https://nextjs.org/docs) 190 | - [wagmi 文档](https://wagmi.sh/) 191 | - [RainbowKit 文档](https://www.rainbowkit.com/) 192 | - [Etherscan API 文档](https://docs.etherscan.io/) 193 | - [CoinGecko API 文档](https://www.coingecko.com/en/api/documentation) -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # DeFi Dashboard 使用指南 2 | 3 | ## 🚀 快速开始 4 | 5 | ### 1. 访问应用 6 | 打开浏览器访问:http://localhost:3000 7 | 8 | ### 2. 连接钱包 9 | - 点击右上角的蓝色"连接钱包"按钮 10 | - 选择您的钱包(推荐使用 MetaMask) 11 | - 在钱包中确认连接请求 12 | 13 | ### 3. 查看数据 14 | 连接成功后,应用会自动: 15 | - 获取您的真实资产余额 16 | - 显示投资组合总价值 17 | - 加载最近的交易记录 18 | 19 | ## 🎯 主要功能 20 | 21 | ### 📊 投资组合概览 22 | - **总资产价值**: 实时计算所有代币的美元价值 23 | - **持有代币数量**: 显示您拥有的不同代币种类 24 | - **最近交易**: 显示最新的交易记录数量 25 | - **刷新按钮**: 手动更新最新数据 26 | 27 | ### 💰 资产管理 28 | - **支持的代币**: ETH、USDC、UNI、LINK 等主要代币 29 | - **实时价格**: 显示当前市场价格和24小时涨跌幅 30 | - **余额显示**: 显示您的实际持有数量 31 | - **价值计算**: 自动计算每种代币的美元价值 32 | 33 | ### 📝 交易记录 34 | - **ETH转账**: 显示以太坊原生代币的转入转出记录 35 | - **代币转账**: 显示ERC20代币的转账记录 36 | - **交易状态**: 成功、失败、待确认状态显示 37 | - **交易详情**: 点击交易哈希可跳转到Etherscan查看详情 38 | 39 | ### 🔗 钱包管理 40 | - **网络显示**: 显示当前连接的区块链网络 41 | - **账户信息**: 显示钱包地址和余额 42 | - **快捷操作**: 43 | - 复制钱包地址 44 | - 在Etherscan中查看地址 45 | - 断开钱包连接 46 | 47 | ## 🛠 高级功能 48 | 49 | ### 🌙 主题切换 50 | - 点击右上角的月亮/太阳图标 51 | - 支持明暗两种主题模式 52 | - 自动保存用户偏好设置 53 | 54 | ### 🔄 数据刷新 55 | - **自动刷新**: 连接钱包时自动获取数据 56 | - **手动刷新**: 点击"刷新"按钮更新数据 57 | - **实时价格**: 价格数据来自CoinGecko API 58 | 59 | ### 📱 响应式设计 60 | - 完美适配桌面和移动设备 61 | - 自适应布局和字体大小 62 | - 触摸友好的交互设计 63 | 64 | ## 🔒 安全说明 65 | 66 | ### ✅ 安全特性 67 | - **只读访问**: 应用只读取区块链数据,不会请求私钥 68 | - **无资金风险**: 不涉及任何资金转移或交易操作 69 | - **公开数据**: 只获取公开的区块链信息 70 | 71 | ### ⚠️ 注意事项 72 | - 确保使用官方钱包应用 73 | - 不要在不安全的网络环境下使用 74 | - 定期检查钱包连接状态 75 | 76 | ## 🚨 故障排除 77 | 78 | ### 连接问题 79 | **问题**: 钱包连接失败 80 | **解决方案**: 81 | 1. 确保已安装MetaMask或其他支持的钱包 82 | 2. 检查钱包是否已解锁 83 | 3. 刷新页面重新尝试连接 84 | 4. 检查网络连接是否正常 85 | 86 | ### 数据加载问题 87 | **问题**: 资产或交易数据不显示 88 | **解决方案**: 89 | 1. 点击"刷新"按钮重新加载数据 90 | 2. 检查网络连接 91 | 3. 确认钱包地址是否有资产 92 | 4. 查看浏览器控制台是否有错误信息 93 | 94 | ### 网络问题 95 | **问题**: 显示"错误网络" 96 | **解决方案**: 97 | 1. 点击网络按钮切换到支持的网络 98 | 2. 确保钱包连接到以太坊主网 99 | 3. 如需使用测试网,请联系开发团队 100 | 101 | ### API限制问题 102 | **问题**: 数据加载缓慢或失败 103 | **解决方案**: 104 | 1. 等待一段时间后重试 105 | 2. 检查API密钥配置是否正确 106 | 3. 考虑升级到付费API服务 107 | 108 | ## 📞 技术支持 109 | 110 | ### 常见问题 111 | - **Q**: 支持哪些钱包? 112 | - **A**: 支持MetaMask、WalletConnect等主流钱包 113 | 114 | - **Q**: 支持哪些代币? 115 | - **A**: 目前支持ETH、USDC、UNI、LINK等主要代币 116 | 117 | - **Q**: 数据多久更新一次? 118 | - **A**: 连接时自动获取,可手动刷新获取最新数据 119 | 120 | ### 联系方式 121 | 如果遇到问题,请: 122 | 1. 查看浏览器控制台错误信息 123 | 2. 检查网络连接和API配置 124 | 3. 提交Issue到项目仓库 125 | 126 | ## 🔄 更新日志 127 | 128 | ### v1.0.0 (当前版本) 129 | - ✅ 真实区块链数据集成 130 | - ✅ 钱包连接和管理 131 | - ✅ 资产余额显示 132 | - ✅ 交易记录查看 133 | - ✅ 响应式设计 134 | - ✅ 主题切换功能 135 | 136 | ### 计划功能 137 | - 🔄 更多代币支持 138 | - 🔄 价格图表显示 139 | - 🔄 交易功能集成 140 | - 🔄 DeFi协议集成 141 | 142 | --- 143 | 144 | **注意**: 这是一个演示项目,请在充分了解风险的情况下使用。 -------------------------------------------------------------------------------- /contracts/DeFiToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | /** 9 | * @title DeFiToken 10 | * @dev A simple ERC20 token for DeFi Dashboard demonstration 11 | */ 12 | contract DeFiToken is ERC20, ERC20Permit, Ownable { 13 | uint256 public constant MAX_SUPPLY = 1000000 * 10**18; // 1 million tokens 14 | 15 | event TokensMinted(address indexed to, uint256 amount); 16 | event TokensBurned(address indexed from, uint256 amount); 17 | 18 | constructor( 19 | string memory name, 20 | string memory symbol, 21 | address initialOwner 22 | ) ERC20(name, symbol) ERC20Permit(name) Ownable(initialOwner) { 23 | // Mint initial supply to owner 24 | _mint(initialOwner, 100000 * 10**18); // 100k tokens 25 | } 26 | 27 | /** 28 | * @dev Mint new tokens (only owner) 29 | * @param to Address to mint tokens to 30 | * @param amount Amount of tokens to mint 31 | */ 32 | function mint(address to, uint256 amount) external onlyOwner { 33 | require(totalSupply() + amount <= MAX_SUPPLY, "DeFiToken: Max supply exceeded"); 34 | _mint(to, amount); 35 | emit TokensMinted(to, amount); 36 | } 37 | 38 | /** 39 | * @dev Burn tokens from caller's balance 40 | * @param amount Amount of tokens to burn 41 | */ 42 | function burn(uint256 amount) external { 43 | _burn(msg.sender, amount); 44 | emit TokensBurned(msg.sender, amount); 45 | } 46 | 47 | /** 48 | * @dev Burn tokens from specified account (with allowance) 49 | * @param from Address to burn tokens from 50 | * @param amount Amount of tokens to burn 51 | */ 52 | function burnFrom(address from, uint256 amount) external { 53 | _spendAllowance(from, msg.sender, amount); 54 | _burn(from, amount); 55 | emit TokensBurned(from, amount); 56 | } 57 | 58 | /** 59 | * @dev Get token information 60 | */ 61 | function getTokenInfo() external view returns ( 62 | string memory tokenName, 63 | string memory tokenSymbol, 64 | uint256 tokenDecimals, 65 | uint256 tokenTotalSupply, 66 | uint256 tokenMaxSupply 67 | ) { 68 | return ( 69 | name(), 70 | symbol(), 71 | decimals(), 72 | totalSupply(), 73 | MAX_SUPPLY 74 | ); 75 | } 76 | } -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # DeFi Dashboard 环境变量配置 2 | 3 | # 应用程序URL (生产环境需要) 4 | # 用于API中间层的内部请求 5 | NEXT_PUBLIC_APP_URL=https://your-app-domain.com 6 | 7 | # WalletConnect Project ID (必需) 8 | # 获取地址: https://cloud.walletconnect.com/ 9 | NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_wallet_connect_project_id 10 | 11 | # Alchemy API Key (可选,用于更稳定的RPC连接) 12 | # 获取地址: https://dashboard.alchemy.com/ 13 | ALCHEMY_API_KEY=your_alchemy_api_key 14 | 15 | # Etherscan API Key (强烈推荐) 16 | # 获取地址: https://etherscan.io/apis 17 | # 没有此密钥可能导致余额和交易数据无法加载 18 | # 🔐 此密钥仅在服务端使用,保护客户端安全 19 | ETHERSCAN_API_KEY=your_etherscan_api_key 20 | 21 | # 支持的网络配置 - 默认sepolia测试网 22 | ETHERSCAN_NETWORK=https://api-sepolia.etherscan.io/api 23 | 24 | # CoinGecko API Key (可选,免费版本有限制) 25 | # 获取地址: https://www.coingecko.com/en/api/pricing 26 | # 免费版本每分钟限制10-50次请求 27 | # 🔐 此密钥仅在服务端使用,保护客户端安全 28 | COINGECKO_API_KEY=your_coingecko_api_key 29 | 30 | # 注意事项: 31 | # 1. 复制此文件为 .env.local 32 | # 2. 填入真实的API密钥 33 | # 3. 不要将 .env.local 提交到版本控制 34 | # 4. API密钥现在安全地存储在服务端,不会暴露给客户端 35 | # 5. 生产环境需要设置 NEXT_PUBLIC_APP_URL 36 | 37 | # Infura API Key (可选 - 备用RPC提供商) 38 | NEXT_PUBLIC_INFURA_API_KEY=your_infura_api_key 39 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | require("dotenv").config(); 3 | 4 | /** @type import('hardhat/config').HardhatUserConfig */ 5 | module.exports = { 6 | solidity: { 7 | version: "0.8.20", 8 | settings: { 9 | optimizer: { 10 | enabled: true, 11 | runs: 200, 12 | }, 13 | }, 14 | }, 15 | networks: { 16 | hardhat: { 17 | chainId: 1337, 18 | }, 19 | sepolia: { 20 | url: process.env.SEPOLIA_RPC_URL, 21 | accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], 22 | chainId: 11155111, 23 | }, 24 | }, 25 | etherscan: { 26 | apiKey: process.env.ETHERSCAN_API_KEY, 27 | }, 28 | paths: { 29 | sources: "./contracts", 30 | tests: "./test", 31 | cache: "./cache", 32 | artifacts: "./artifacts", 33 | }, 34 | }; -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | experimental: { 5 | optimizePackageImports: ['lucide-react'], 6 | }, 7 | devIndicators: false, 8 | } 9 | 10 | module.exports = nextConfig -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dapp-demo", 3 | "version": "0.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/GDYG/DeFi-Dashboard.git" 7 | }, 8 | "author": "GDYG", 9 | "license": "MIT", 10 | "bugs": { 11 | "url": "https://github.com/GDYG/DeFi-Dashboard/issues" 12 | }, 13 | "homepage": "https://github.com/GDYG/DeFi-Dashboard#readme", 14 | "scripts": { 15 | "dev": "next dev", 16 | "build": "next build", 17 | "start": "next start", 18 | "lint": "next lint", 19 | "hardhat:compile": "npx hardhat compile", 20 | "hardhat:deploy": "npx hardhat run scripts/deploy.js --network sepolia", 21 | "hardhat:test": "npx hardhat test" 22 | }, 23 | "dependencies": { 24 | "@apollo/client": "^3.11.8", 25 | "@openzeppelin/contracts": "^5.0.0", 26 | "@rainbow-me/rainbowkit": "^2.1.6", 27 | "@tanstack/react-query": "^5.59.0", 28 | "clsx": "^2.1.1", 29 | "ethers": "^6.13.2", 30 | "graphql": "^16.9.0", 31 | "https-proxy-agent": "^7.0.6", 32 | "lucide-react": "^0.451.0", 33 | "next": "^15.0.3", 34 | "node-fetch": "^3.3.2", 35 | "react": "^19.1.0", 36 | "react-dom": "^19.1.0", 37 | "tailwind-merge": "^2.5.3", 38 | "viem": "^2.21.19", 39 | "wagmi": "^2.12.17", 40 | "zustand": "^5.0.0" 41 | }, 42 | "devDependencies": { 43 | "@nomicfoundation/hardhat-toolbox": "^4.0.0", 44 | "@types/node": "^22.7.9", 45 | "@types/react": "^19.1.6", 46 | "@types/react-dom": "^19.1.5", 47 | "autoprefixer": "^10.4.20", 48 | "dotenv": "^16.3.1", 49 | "eslint": "^8.57.1", 50 | "eslint-config-next": "^15.0.3", 51 | "hardhat": "^2.19.0", 52 | "pino-pretty": "^13.0.0", 53 | "postcss": "^8.4.47", 54 | "tailwindcss": "^3.4.14", 55 | "typescript": "^5.6.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat"); 2 | 3 | async function main() { 4 | console.log("Starting deployment..."); 5 | 6 | // Get the deployer account 7 | const [deployer] = await ethers.getSigners(); 8 | console.log("Deploying contracts with account:", deployer.address); 9 | 10 | // Get account balance 11 | const balance = await ethers.provider.getBalance(deployer.address); 12 | console.log("Account balance:", ethers.formatEther(balance), "ETH"); 13 | 14 | // Deploy DeFiToken 15 | console.log("\nDeploying DeFiToken..."); 16 | const DeFiToken = await ethers.getContractFactory("DeFiToken"); 17 | const defiToken = await DeFiToken.deploy( 18 | "DeFi Dashboard Token", 19 | "DDT", 20 | deployer.address 21 | ); 22 | 23 | await defiToken.waitForDeployment(); 24 | const defiTokenAddress = await defiToken.getAddress(); 25 | 26 | console.log("DeFiToken deployed to:", defiTokenAddress); 27 | 28 | // Verify deployment 29 | const tokenInfo = await defiToken.getTokenInfo(); 30 | console.log("\nToken Information:"); 31 | console.log("Name:", tokenInfo.tokenName); 32 | console.log("Symbol:", tokenInfo.tokenSymbol); 33 | console.log("Decimals:", tokenInfo.tokenDecimals.toString()); 34 | console.log("Total Supply:", ethers.formatEther(tokenInfo.tokenTotalSupply)); 35 | console.log("Max Supply:", ethers.formatEther(tokenInfo.tokenMaxSupply)); 36 | 37 | // Save deployment info 38 | const deploymentInfo = { 39 | network: await ethers.provider.getNetwork(), 40 | deployer: deployer.address, 41 | contracts: { 42 | DeFiToken: { 43 | address: defiTokenAddress, 44 | name: tokenInfo.tokenName, 45 | symbol: tokenInfo.tokenSymbol, 46 | decimals: tokenInfo.tokenDecimals.toString(), 47 | totalSupply: ethers.formatEther(tokenInfo.tokenTotalSupply), 48 | maxSupply: ethers.formatEther(tokenInfo.tokenMaxSupply) 49 | } 50 | }, 51 | timestamp: new Date().toISOString() 52 | }; 53 | 54 | console.log("\nDeployment completed successfully!"); 55 | console.log("Deployment info:", JSON.stringify(deploymentInfo, null, 2)); 56 | } 57 | 58 | main() 59 | .then(() => process.exit(0)) 60 | .catch((error) => { 61 | console.error("Deployment failed:", error); 62 | process.exit(1); 63 | }); -------------------------------------------------------------------------------- /src/app/api/assets/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { EtherscanService, CoinGeckoService } from '@/services/api' 3 | 4 | export async function GET(request: NextRequest) { 5 | try { 6 | const { searchParams } = new URL(request.url) 7 | const address = searchParams.get('address') 8 | 9 | if (!address) { 10 | return NextResponse.json( 11 | { error: '地址参数是必需的' }, 12 | { status: 400 } 13 | ) 14 | } 15 | 16 | // 在服务端创建服务实例,API密钥只在服务端使用 17 | const etherscanService = new EtherscanService() 18 | const coinGeckoService = new CoinGeckoService() 19 | 20 | // 获取ETH余额 21 | const ethBalance = await etherscanService.getAccountBalance(address) 22 | const ethBalanceInEther = parseFloat(ethBalance) / 1e18 23 | 24 | // 获取ETH价格 25 | const ethPrice = await coinGeckoService.getEthereumPrice() 26 | 27 | // 主要ERC20代币合约地址 28 | const tokenContracts = { 29 | USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 30 | UNI: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', 31 | LINK: '0x514910771AF9Ca656af840dff83E8264EcF986CA', 32 | } 33 | 34 | // 获取代币余额 35 | const tokenBalances = await Promise.all([ 36 | etherscanService.getTokenBalance(address, tokenContracts.USDC), 37 | etherscanService.getTokenBalance(address, tokenContracts.UNI), 38 | etherscanService.getTokenBalance(address, tokenContracts.LINK), 39 | ]) 40 | 41 | // 获取代币价格 42 | const tokenPrices = await coinGeckoService.getTokenPrices(['uniswap', 'chainlink']) 43 | 44 | const assets = [ 45 | { 46 | symbol: 'ETH', 47 | name: 'Ethereum', 48 | amount: ethBalanceInEther.toFixed(4), 49 | usd: ethPrice ? `$${(ethBalanceInEther * ethPrice.usd).toFixed(2)}` : '$0.00', 50 | icon: 'Ξ', 51 | change: ethPrice ? `${ethPrice.usd_24h_change >= 0 ? '+' : ''}${ethPrice.usd_24h_change.toFixed(2)}%` : '0%', 52 | address: '0x0000000000000000000000000000000000000000' 53 | }, 54 | { 55 | symbol: 'USDC', 56 | name: 'USD Coin', 57 | amount: (parseFloat(tokenBalances[0]) / 1e6).toFixed(2), 58 | usd: `$${(parseFloat(tokenBalances[0]) / 1e6).toFixed(2)}`, 59 | icon: '$', 60 | change: '+0.1%', 61 | address: tokenContracts.USDC 62 | }, 63 | { 64 | symbol: 'UNI', 65 | name: 'Uniswap', 66 | amount: (parseFloat(tokenBalances[1]) / 1e18).toFixed(2), 67 | usd: tokenPrices.uniswap ? `$${((parseFloat(tokenBalances[1]) / 1e18) * tokenPrices.uniswap.usd).toFixed(2)}` : '$0.00', 68 | icon: '🦄', 69 | change: tokenPrices.uniswap ? `${tokenPrices.uniswap.usd_24h_change >= 0 ? '+' : ''}${tokenPrices.uniswap.usd_24h_change.toFixed(2)}%` : '0%', 70 | address: tokenContracts.UNI 71 | }, 72 | { 73 | symbol: 'LINK', 74 | name: 'Chainlink', 75 | amount: (parseFloat(tokenBalances[2]) / 1e18).toFixed(2), 76 | usd: tokenPrices.chainlink ? `$${((parseFloat(tokenBalances[2]) / 1e18) * tokenPrices.chainlink.usd).toFixed(2)}` : '$0.00', 77 | icon: '🔗', 78 | change: tokenPrices.chainlink ? `${tokenPrices.chainlink.usd_24h_change >= 0 ? '+' : ''}${tokenPrices.chainlink.usd_24h_change.toFixed(2)}%` : '0%', 79 | address: tokenContracts.LINK 80 | } 81 | ] 82 | 83 | const filteredAssets = assets.filter(asset => parseFloat(asset.amount) > 0) 84 | 85 | return NextResponse.json({ assets: filteredAssets }) 86 | } catch (error) { 87 | console.error('获取资产数据失败:', error) 88 | return NextResponse.json( 89 | { error: '获取资产数据失败' }, 90 | { status: 500 } 91 | ) 92 | } 93 | } -------------------------------------------------------------------------------- /src/app/api/balance/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { EtherscanService } from '@/services/api' 3 | 4 | export async function GET(request: NextRequest) { 5 | try { 6 | const { searchParams } = new URL(request.url) 7 | const address = searchParams.get('address') 8 | const contractAddress = searchParams.get('contractAddress') 9 | 10 | if (!address) { 11 | return NextResponse.json( 12 | { error: '地址参数是必需的' }, 13 | { status: 400 } 14 | ) 15 | } 16 | 17 | // 在服务端创建服务实例,API密钥只在服务端使用 18 | const etherscanService = new EtherscanService() 19 | 20 | if (contractAddress) { 21 | // 获取代币余额 22 | const tokenBalance = await etherscanService.getTokenBalance(address, contractAddress) 23 | return NextResponse.json({ balance: tokenBalance }) 24 | } else { 25 | // 获取ETH余额 26 | const ethBalance = await etherscanService.getAccountBalance(address) 27 | return NextResponse.json({ balance: ethBalance }) 28 | } 29 | } catch (error) { 30 | console.error('获取余额数据失败:', error) 31 | return NextResponse.json( 32 | { error: '获取余额数据失败' }, 33 | { status: 500 } 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /src/app/api/prices/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { CoinGeckoService } from '@/services/api' 3 | 4 | export async function GET(request: NextRequest) { 5 | try { 6 | const { searchParams } = new URL(request.url) 7 | const tokenIds = searchParams.get('tokenIds') 8 | const contractAddress = searchParams.get('contractAddress') 9 | 10 | // 在服务端创建服务实例,API密钥只在服务端使用 11 | const coinGeckoService = new CoinGeckoService() 12 | 13 | if (contractAddress) { 14 | // 根据合约地址获取代币价格 15 | const tokenPrice = await coinGeckoService.getTokenPriceByContract(contractAddress) 16 | return NextResponse.json({ price: tokenPrice }) 17 | } else if (tokenIds) { 18 | // 根据代币ID列表获取价格 19 | const tokenIdArray = tokenIds.split(',') 20 | const prices = await coinGeckoService.getTokenPrices(tokenIdArray) 21 | return NextResponse.json({ prices }) 22 | } else { 23 | // 获取以太坊价格 24 | const ethereumPrice = await coinGeckoService.getEthereumPrice() 25 | return NextResponse.json({ ethereumPrice }) 26 | } 27 | } catch (error) { 28 | console.error('获取价格数据失败:', error) 29 | return NextResponse.json( 30 | { error: '获取价格数据失败' }, 31 | { status: 500 } 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /src/app/api/transactions/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { EtherscanService } from '@/services/api' 3 | 4 | export async function GET(request: NextRequest) { 5 | try { 6 | const { searchParams } = new URL(request.url) 7 | const address = searchParams.get('address') 8 | 9 | if (!address) { 10 | return NextResponse.json( 11 | { error: '地址参数是必需的' }, 12 | { status: 400 } 13 | ) 14 | } 15 | 16 | // 在服务端创建服务实例,API密钥只在服务端使用 17 | const etherscanService = new EtherscanService() 18 | 19 | // 获取ETH交易记录 20 | const ethTransactions = await etherscanService.getAccountTransactions(address, 1, 5) 21 | 22 | // 获取代币转账记录 23 | const tokenTransfers = await etherscanService.getTokenTransfers(address, undefined, 1, 5) 24 | 25 | // 格式化时间为相对时间 26 | const formatTimeAgo = (timestamp: number): string => { 27 | const now = Date.now() 28 | const diffInSeconds = Math.floor((now - timestamp) / 1000) 29 | 30 | if (diffInSeconds < 60) { 31 | return `${diffInSeconds} 秒前` 32 | } 33 | 34 | const diffInMinutes = Math.floor(diffInSeconds / 60) 35 | if (diffInMinutes < 60) { 36 | return `${diffInMinutes} 分钟前` 37 | } 38 | 39 | const diffInHours = Math.floor(diffInMinutes / 60) 40 | if (diffInHours < 24) { 41 | return `${diffInHours} 小时前` 42 | } 43 | 44 | const diffInDays = Math.floor(diffInHours / 24) 45 | if (diffInDays < 30) { 46 | return `${diffInDays} 天前` 47 | } 48 | 49 | const diffInMonths = Math.floor(diffInDays / 30) 50 | if (diffInMonths < 12) { 51 | return `${diffInMonths} 个月前` 52 | } 53 | 54 | const diffInYears = Math.floor(diffInMonths / 12) 55 | return `${diffInYears} 年前` 56 | } 57 | 58 | // 合并并格式化交易记录 59 | const allTransactions = [ 60 | ...ethTransactions.map(tx => ({ 61 | hash: tx.hash, 62 | type: 'Transfer', 63 | amount: `${tx.from.toLowerCase() === address.toLowerCase() ? '-' : '+'}${(parseFloat(tx.value) / 1e18).toFixed(4)} ETH`, 64 | status: tx.isError === '0' ? 'success' as const : 'failed' as const, 65 | time: formatTimeAgo(parseInt(tx.timeStamp) * 1000), 66 | from: tx.from, 67 | to: tx.to, 68 | timestamp: parseInt(tx.timeStamp) 69 | })), 70 | ...tokenTransfers.map(tx => ({ 71 | hash: tx.hash, 72 | type: 'Token Transfer', 73 | amount: `${tx.from.toLowerCase() === address.toLowerCase() ? '-' : '+'}${(parseFloat(tx.value) / Math.pow(10, parseInt(tx.tokenDecimal))).toFixed(4)} ${tx.tokenSymbol}`, 74 | status: 'success' as const, 75 | time: formatTimeAgo(parseInt(tx.timeStamp) * 1000), 76 | from: tx.from, 77 | to: tx.to, 78 | timestamp: parseInt(tx.timeStamp) 79 | })) 80 | ] 81 | 82 | // 按时间排序 83 | const sortedTransactions = allTransactions 84 | .sort((a, b) => b.timestamp - a.timestamp) 85 | .slice(0, 10) 86 | 87 | return NextResponse.json({ transactions: sortedTransactions }) 88 | } catch (error) { 89 | console.error('获取交易记录失败:', error) 90 | return NextResponse.json( 91 | { error: '获取交易记录失败' }, 92 | { status: 500 } 93 | ) 94 | } 95 | } -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient(to bottom, 22 | transparent, 23 | rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); 24 | } 25 | 26 | /* Custom scrollbar */ 27 | ::-webkit-scrollbar { 28 | width: 6px; 29 | } 30 | 31 | ::-webkit-scrollbar-track { 32 | background: transparent; 33 | } 34 | 35 | ::-webkit-scrollbar-thumb { 36 | background: #888; 37 | border-radius: 3px; 38 | } 39 | 40 | ::-webkit-scrollbar-thumb:hover { 41 | background: #555; 42 | } 43 | 44 | /* Animation classes */ 45 | .animate-fade-in { 46 | animation: fadeIn 0.5s ease-in; 47 | } 48 | 49 | @keyframes fadeIn { 50 | from { 51 | opacity: 0; 52 | transform: translateY(20px); 53 | } 54 | 55 | to { 56 | opacity: 1; 57 | transform: translateY(0); 58 | } 59 | } 60 | 61 | /* Loading spinner */ 62 | .spinner { 63 | border: 2px solid #f3f3f3; 64 | border-top: 2px solid #ff007a; 65 | border-radius: 50%; 66 | width: 20px; 67 | height: 20px; 68 | animation: spin 1s linear infinite; 69 | } 70 | 71 | @keyframes spin { 72 | 0% { 73 | transform: rotate(0deg); 74 | } 75 | 76 | 100% { 77 | transform: rotate(360deg); 78 | } 79 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | import { Providers } from './providers' 5 | import { Header } from '@/components/Header' 6 | 7 | const inter = Inter({ subsets: ['latin'] }) 8 | 9 | export const metadata: Metadata = { 10 | title: 'DeFi Dashboard', 11 | description: '现代化的去中心化金融仪表板', 12 | } 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode 18 | }) { 19 | return ( 20 | 21 | 22 | 23 |
24 |
25 | {children} 26 |
27 |
28 | 29 | 30 | ) 31 | } -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect } from 'react' 4 | import { useAccount, useChainId } from 'wagmi' 5 | import { useWalletStore } from '@/store/useWalletStore' 6 | import AssetCard from '@/components/AssetCard' 7 | import TransactionCard from '@/components/TransactionCard' 8 | import LoadingSpinner from '@/components/LoadingSpinner' 9 | import ErrorMessage from '@/components/ErrorMessage' 10 | import { ApiStatus } from '@/components/ApiStatus' 11 | 12 | export default function Home() { 13 | const { address, isConnected } = useAccount() 14 | const chainId = useChainId() 15 | 16 | const { 17 | assets, 18 | transactions, 19 | totalValue, 20 | isLoadingAssets, 21 | isLoadingTransactions, 22 | error, 23 | setConnection, 24 | refresh 25 | } = useWalletStore() 26 | 27 | // 监听钱包连接状态变化 28 | useEffect(() => { 29 | setConnection(isConnected, address, chainId) 30 | }, [isConnected, address, chainId, setConnection]) 31 | 32 | // 如果钱包未连接,显示连接提示 33 | if (!isConnected) { 34 | return ( 35 |
36 |
37 |
38 |
39 |
🔗
40 |

41 | 欢迎使用 DeFi Dashboard 42 |

43 |

44 | 连接您的钱包以查看资产余额、交易记录和投资组合概览 45 |

46 |
47 | 请点击右上角的"连接钱包"按钮开始使用 48 |
49 |
50 |
51 |
52 |
53 | ) 54 | } 55 | 56 | return ( 57 |
58 |
59 | {/* API状态提醒 */} 60 | 61 | 62 | {/* 错误提示 */} 63 | {error && ( 64 |
65 | 69 |
70 | )} 71 | 72 | {/* 投资组合概览 */} 73 |
74 |
75 |
76 |

投资组合概览

77 | 94 |
95 | 96 |
97 |
98 |
99 | {isLoadingAssets ? ( 100 | 101 | ) : ( 102 | totalValue 103 | )} 104 |
105 |
总资产价值
106 |
107 | 108 |
109 |
110 | {isLoadingAssets ? ( 111 | 112 | ) : ( 113 | assets.length 114 | )} 115 |
116 |
持有代币
117 |
118 | 119 |
120 |
121 | {isLoadingTransactions ? ( 122 | 123 | ) : ( 124 | transactions.length 125 | )} 126 |
127 |
最近交易
128 |
129 |
130 | 131 | {/* 钱包地址显示 */} 132 |
133 |
134 | 当前钱包: {address} 135 |
136 |
137 |
138 |
139 | 140 |
141 | {/* 资产列表 */} 142 |
143 |
144 |

我的资产

145 | {isLoadingAssets && } 146 |
147 | 148 |
149 | {isLoadingAssets ? ( 150 | // 加载状态 151 | Array.from({ length: 3 }).map((_, index) => ( 152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | )) 168 | ) : assets.length > 0 ? ( 169 | assets.map((asset, index) => ( 170 | 171 | )) 172 | ) : ( 173 |
174 |
💰
175 |
暂无资产数据
176 |
177 | 请确保您的钱包中有资产或稍后重试 178 |
179 |
180 | )} 181 |
182 |
183 | 184 | {/* 交易记录 */} 185 |
186 |
187 |

最近交易

188 | {isLoadingTransactions && } 189 |
190 | 191 |
192 | {isLoadingTransactions ? ( 193 | // 加载状态 194 | Array.from({ length: 4 }).map((_, index) => ( 195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 | )) 211 | ) : transactions.length > 0 ? ( 212 | transactions.map((transaction, index) => ( 213 | 214 | )) 215 | ) : ( 216 |
217 |
📝
218 |
暂无交易记录
219 |
220 | 您的交易记录将在这里显示 221 |
222 |
223 | )} 224 |
225 |
226 |
227 |
228 |
229 | ) 230 | } -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ReactNode } from 'react' 4 | import { WagmiProvider, http } from 'wagmi' 5 | import { sepolia, mainnet } from 'wagmi/chains' 6 | import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit' 7 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 8 | import '@rainbow-me/rainbowkit/styles.css' 9 | 10 | // Create wagmi config using the new v2 API 11 | const config = getDefaultConfig({ 12 | appName: 'DeFi-Dashboard', 13 | projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || 'demo-project-id', 14 | chains: [sepolia, mainnet], 15 | transports: { 16 | [sepolia.id]: http( 17 | process.env.ALCHEMY_API_KEY 18 | ? `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}` 19 | : undefined 20 | ), 21 | [mainnet.id]: http( 22 | process.env.ALCHEMY_API_KEY 23 | ? `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}` 24 | : undefined 25 | ), 26 | }, 27 | ssr: true, 28 | }) 29 | 30 | // Create query client 31 | const queryClient = new QueryClient({ 32 | defaultOptions: { 33 | queries: { 34 | staleTime: 1000 * 60 * 5, // 5 minutes 35 | gcTime: 1000 * 60 * 10, // 10 minutes (renamed from cacheTime) 36 | }, 37 | }, 38 | }) 39 | 40 | interface ProvidersProps { 41 | children: ReactNode 42 | } 43 | 44 | export function Providers({ children }: ProvidersProps) { 45 | return ( 46 | 47 | 48 | 49 | {children} 50 | 51 | 52 | 53 | ) 54 | } -------------------------------------------------------------------------------- /src/components/ApiStatus.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useState } from 'react' 4 | import { AlertTriangle, CheckCircle, XCircle } from 'lucide-react' 5 | 6 | interface ApiStatusProps { 7 | className?: string 8 | } 9 | 10 | interface ApiStatus { 11 | etherscan: 'checking' | 'ok' | 'error' 12 | coingecko: 'checking' | 'ok' | 'error' 13 | } 14 | 15 | export function ApiStatus({ className = '' }: ApiStatusProps) { 16 | const [status, setStatus] = useState({ 17 | etherscan: 'checking', 18 | coingecko: 'checking' 19 | }) 20 | const [showDetails, setShowDetails] = useState(false) 21 | 22 | useEffect(() => { 23 | checkApiStatus() 24 | }, []) 25 | 26 | const checkApiStatus = async () => { 27 | // 检查环境变量 28 | setStatus({ 29 | etherscan: 'ok', // etherscan免费配额 30 | coingecko: 'ok' // CoinGecko 免费版不需要API Key 31 | }) 32 | } 33 | 34 | const getStatusIcon = (apiStatus: 'checking' | 'ok' | 'error') => { 35 | switch (apiStatus) { 36 | case 'ok': 37 | return 38 | case 'error': 39 | return 40 | default: 41 | return 42 | } 43 | } 44 | 45 | const hasErrors = status.etherscan === 'error' || status.coingecko === 'error' 46 | 47 | if (!hasErrors) return null 48 | 49 | return ( 50 |
51 |
52 |
53 | 54 | 55 | API 配置提醒 56 | 57 |
58 | 64 |
65 | 66 | {showDetails && ( 67 |
68 |
69 | 为了获得最佳体验,请配置以下API密钥: 70 |
71 | 72 |
73 |
74 | {getStatusIcon(status.etherscan)} 75 | 76 | Etherscan API Key {status.etherscan === 'error' && '(未配置 - 数据可能无法加载)'} 77 | 78 |
79 | 80 |
81 | {getStatusIcon(status.coingecko)} 82 | 83 | CoinGecko API (免费版本可用) 84 | 85 |
86 |
87 | 88 |
89 | 请查看 README.md 了解如何获取和配置API密钥 90 |
91 |
92 | )} 93 |
94 | ) 95 | } -------------------------------------------------------------------------------- /src/components/AssetCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { formatTxHash } from '@/utils/format' 4 | 5 | interface Asset { 6 | symbol: string 7 | name: string 8 | amount: string 9 | usd: string 10 | icon: string 11 | change: string 12 | address: string 13 | } 14 | 15 | interface AssetCardProps { 16 | asset: Asset 17 | } 18 | 19 | export default function AssetCard({ asset }: AssetCardProps) { 20 | const isPositiveChange = asset.change.startsWith('+') 21 | 22 | return ( 23 |
24 |
25 |
26 |
27 | {asset.icon} 28 |
29 |
30 |

{asset.symbol}

31 |

{asset.name}

32 |
33 |
34 | 35 |
36 |

{asset.amount}

37 |
38 |

{asset.usd}

39 | 44 | {asset.change} 45 | 46 |
47 |
48 |
49 | 50 | {/* 合约地址 */} 51 |
52 |
53 | 合约: {formatTxHash(asset.address)} 54 |
55 |
56 |
57 | ) 58 | } -------------------------------------------------------------------------------- /src/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | interface ErrorMessageProps { 2 | message: string 3 | onRetry?: () => void 4 | className?: string 5 | } 6 | 7 | export default function ErrorMessage({ message, onRetry, className = '' }: ErrorMessageProps) { 8 | return ( 9 |
10 |
11 |
12 |
⚠️
13 |
14 |
出现错误
15 |
{message}
16 |
17 |
18 | 19 | {onRetry && ( 20 | 26 | )} 27 |
28 |
29 | ) 30 | } -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState, useEffect } from 'react' 4 | import { ConnectButton } from '@rainbow-me/rainbowkit' 5 | import { useDisconnect } from 'wagmi' 6 | import { Moon, Sun, LogOut, Copy, ExternalLink } from 'lucide-react' 7 | import { useToast } from '@/hooks/useToast' 8 | import { ToastManager } from '@/components/Toast' 9 | 10 | export function Header() { 11 | const [isDark, setIsDark] = useState(false) 12 | const [showAccountMenu, setShowAccountMenu] = useState(false) 13 | const { disconnect } = useDisconnect() 14 | const { toasts, removeToast, success } = useToast() 15 | 16 | useEffect(() => { 17 | // Check for saved theme preference or default to dark mode 18 | const savedTheme = localStorage.getItem('theme') 19 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches 20 | 21 | if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { 22 | setIsDark(true) 23 | document.documentElement.classList.add('dark') 24 | } 25 | }, []) 26 | 27 | const toggleTheme = () => { 28 | const newTheme = !isDark 29 | setIsDark(newTheme) 30 | 31 | if (newTheme) { 32 | document.documentElement.classList.add('dark') 33 | localStorage.setItem('theme', 'dark') 34 | } else { 35 | document.documentElement.classList.remove('dark') 36 | localStorage.setItem('theme', 'light') 37 | } 38 | } 39 | 40 | const copyAddress = (address: string) => { 41 | navigator.clipboard.writeText(address) 42 | success('地址已复制到剪贴板') 43 | setShowAccountMenu(false) 44 | } 45 | 46 | const openEtherscan = (address: string) => { 47 | window.open(`https://etherscan.io/address/${address}`, '_blank') 48 | setShowAccountMenu(false) 49 | } 50 | 51 | const handleDisconnect = () => { 52 | disconnect() 53 | setShowAccountMenu(false) 54 | success('钱包已断开连接') 55 | } 56 | 57 | return ( 58 | <> 59 |
60 |
61 |
62 | {/* Logo */} 63 |
64 |
65 | D 66 |
67 |

68 | DeFi Dashboard 69 |

70 |
71 | 72 | {/* Actions */} 73 |
74 | {/* Theme Toggle */} 75 | 86 | 87 | {/* Wallet Connect Button */} 88 |
89 | 90 | {({ 91 | account, 92 | chain, 93 | openAccountModal, 94 | openChainModal, 95 | openConnectModal, 96 | authenticationStatus, 97 | mounted, 98 | }) => { 99 | const ready = mounted && authenticationStatus !== 'loading'; 100 | const connected = 101 | ready && 102 | account && 103 | chain && 104 | (!authenticationStatus || 105 | authenticationStatus === 'authenticated'); 106 | 107 | return ( 108 |
118 | {(() => { 119 | if (!connected) { 120 | return ( 121 | 128 | ); 129 | } 130 | 131 | if (chain.unsupported) { 132 | return ( 133 | 140 | ); 141 | } 142 | 143 | return ( 144 |
145 | {/* Network Button */} 146 | 174 | 175 | {/* Account Button with Dropdown */} 176 |
177 | 189 | 190 | {/* Dropdown Menu */} 191 | {showAccountMenu && ( 192 |
193 |
194 |
钱包地址
195 |
196 | {account.address} 197 |
198 |
199 | 200 | 207 | 208 | 215 | 216 |
217 | 224 |
225 |
226 | )} 227 |
228 |
229 | ); 230 | })()} 231 |
232 | ); 233 | }} 234 |
235 |
236 |
237 |
238 |
239 | 240 | {/* Click outside to close menu */} 241 | {showAccountMenu && ( 242 |
setShowAccountMenu(false)} 245 | /> 246 | )} 247 |
248 | 249 | {/* Toast Manager */} 250 | 251 | 252 | ) 253 | } -------------------------------------------------------------------------------- /src/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | interface LoadingSpinnerProps { 2 | size?: 'sm' | 'md' | 'lg' 3 | className?: string 4 | } 5 | 6 | export default function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) { 7 | const sizeClasses = { 8 | sm: 'w-4 h-4', 9 | md: 'w-6 h-6', 10 | lg: 'w-8 h-8' 11 | } 12 | 13 | return ( 14 |
15 | 加载中... 16 |
17 | ) 18 | } -------------------------------------------------------------------------------- /src/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { CheckCircle, X } from 'lucide-react' 3 | 4 | interface ToastProps { 5 | message: string 6 | type?: 'success' | 'error' | 'info' 7 | duration?: number 8 | onClose: () => void 9 | } 10 | 11 | export function Toast({ message, type = 'success', duration = 3000, onClose }: ToastProps) { 12 | const [isVisible, setIsVisible] = useState(true) 13 | 14 | useEffect(() => { 15 | const timer = setTimeout(() => { 16 | setIsVisible(false) 17 | setTimeout(onClose, 300) // Wait for animation to complete 18 | }, duration) 19 | 20 | return () => clearTimeout(timer) 21 | }, [duration, onClose]) 22 | 23 | const getIcon = () => { 24 | switch (type) { 25 | case 'success': 26 | return 27 | case 'error': 28 | return 29 | default: 30 | return 31 | } 32 | } 33 | 34 | const getBackgroundColor = () => { 35 | switch (type) { 36 | case 'success': 37 | return 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800' 38 | case 'error': 39 | return 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800' 40 | default: 41 | return 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800' 42 | } 43 | } 44 | 45 | return ( 46 |
51 |
52 | {getIcon()} 53 | 54 | {message} 55 | 56 | 65 |
66 |
67 | ) 68 | } 69 | 70 | // Toast管理器 71 | interface ToastManagerProps { 72 | toasts: Array<{ 73 | id: string 74 | message: string 75 | type?: 'success' | 'error' | 'info' 76 | duration?: number 77 | }> 78 | removeToast: (id: string) => void 79 | } 80 | 81 | export function ToastManager({ toasts, removeToast }: ToastManagerProps) { 82 | return ( 83 | <> 84 | {toasts.map((toast, index) => ( 85 |
90 | removeToast(toast.id)} 95 | /> 96 |
97 | ))} 98 | 99 | ) 100 | } -------------------------------------------------------------------------------- /src/components/TransactionCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { formatTxHash } from '@/utils/format' 4 | import { CheckCircle, Clock, XCircle, ExternalLink } from 'lucide-react' 5 | 6 | interface Transaction { 7 | hash: string 8 | type: string 9 | amount: string 10 | status: 'success' | 'pending' | 'failed' 11 | time: string 12 | from: string 13 | to: string 14 | } 15 | 16 | interface TransactionCardProps { 17 | transaction: Transaction 18 | } 19 | 20 | const getStatusIcon = (status: string) => { 21 | switch (status) { 22 | case 'success': 23 | return 24 | case 'pending': 25 | return 26 | case 'failed': 27 | return 28 | default: 29 | return 30 | } 31 | } 32 | 33 | const getStatusColor = (status: string) => { 34 | switch (status) { 35 | case 'success': 36 | return 'bg-green-500/20 text-green-400' 37 | case 'pending': 38 | return 'bg-yellow-500/20 text-yellow-400' 39 | case 'failed': 40 | return 'bg-red-500/20 text-red-400' 41 | default: 42 | return 'bg-gray-500/20 text-gray-400' 43 | } 44 | } 45 | 46 | export default function TransactionCard({ transaction }: TransactionCardProps) { 47 | const isPositive = transaction.amount.startsWith('+') 48 | const isNegative = transaction.amount.startsWith('-') 49 | 50 | return ( 51 |
52 |
53 |
54 |
55 | {getStatusIcon(transaction.status)} 56 |
57 |
58 | 69 |
70 | 71 | {transaction.type} 72 | 73 | {transaction.time} 74 |
75 |
76 |
77 | 78 |
79 |

86 | {transaction.amount} 87 |

88 |

89 | {transaction.status} 90 |

91 |
92 |
93 | 94 | {/* 交易详情 */} 95 |
96 |
97 |
98 | 从: {formatTxHash(transaction.from)} 99 |
100 |
101 | 到: {formatTxHash(transaction.to)} 102 |
103 |
104 |
105 |
106 | ) 107 | } -------------------------------------------------------------------------------- /src/hooks/useToast.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react' 2 | 3 | interface Toast { 4 | id: string 5 | message: string 6 | type?: 'success' | 'error' | 'info' 7 | duration?: number 8 | } 9 | 10 | export function useToast() { 11 | const [toasts, setToasts] = useState([]) 12 | 13 | const addToast = useCallback((toast: Omit) => { 14 | const id = Math.random().toString(36).substr(2, 9) 15 | setToasts(prev => [...prev, { ...toast, id }]) 16 | }, []) 17 | 18 | const removeToast = useCallback((id: string) => { 19 | setToasts(prev => prev.filter(toast => toast.id !== id)) 20 | }, []) 21 | 22 | const success = useCallback((message: string, duration?: number) => { 23 | addToast({ message, type: 'success', duration }) 24 | }, [addToast]) 25 | 26 | const error = useCallback((message: string, duration?: number) => { 27 | addToast({ message, type: 'error', duration }) 28 | }, [addToast]) 29 | 30 | const info = useCallback((message: string, duration?: number) => { 31 | addToast({ message, type: 'info', duration }) 32 | }, [addToast]) 33 | 34 | return { 35 | toasts, 36 | addToast, 37 | removeToast, 38 | success, 39 | error, 40 | info 41 | } 42 | } -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import { API_ENDPOINTS } from '@/utils/constants' 2 | import { HttpsProxyAgent } from 'https-proxy-agent'; 3 | import fetch from 'node-fetch'; 4 | 5 | // API响应类型定义 6 | interface EtherscanResponse { 7 | status: string 8 | message: string 9 | result: T 10 | } 11 | 12 | interface CoinGeckoTokenPrice { 13 | usd: number 14 | usd_24h_change: number 15 | } 16 | 17 | interface EtherscanTransaction { 18 | hash: string 19 | from: string 20 | to: string 21 | value: string 22 | gas: string 23 | gasPrice: string 24 | gasUsed: string 25 | timeStamp: string 26 | confirmations: string 27 | isError: string 28 | } 29 | 30 | interface EtherscanTokenTransfer { 31 | hash: string 32 | from: string 33 | to: string 34 | value: string 35 | tokenName: string 36 | tokenSymbol: string 37 | tokenDecimal: string 38 | contractAddress: string 39 | timeStamp: string 40 | } 41 | 42 | /** 43 | * 基础API客户端 44 | */ 45 | class ApiClient { 46 | private baseUrl: string 47 | 48 | constructor(baseUrl: string) { 49 | this.baseUrl = baseUrl 50 | } 51 | 52 | async get(endpoint: string, params?: Record): Promise { 53 | const url = new URL(endpoint, this.baseUrl) 54 | 55 | if (params) { 56 | Object.entries(params).forEach(([key, value]) => { 57 | url.searchParams.append(key, value) 58 | }) 59 | } 60 | const proxyAgent = new HttpsProxyAgent('http://127.0.0.1:7890'); 61 | const _rest = process.env.NODE_ENV === 'production' ? {} : { 62 | agent: proxyAgent, 63 | headers: { 64 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 65 | 'Accept': 'application/json', 66 | } 67 | } 68 | const response = await fetch(url.toString(), { 69 | ..._rest, 70 | }) 71 | 72 | if (!response.ok) { 73 | throw new Error(`API request failed: ${response.status} ${response.statusText}`) 74 | } 75 | 76 | return response.json() as Promise 77 | } 78 | } 79 | 80 | /** 81 | * CoinGecko API 服务 82 | * 🔐 仅用于服务端 - 包含敏感API密钥 83 | */ 84 | export class CoinGeckoService { 85 | private client: ApiClient 86 | 87 | constructor() { 88 | this.client = new ApiClient(API_ENDPOINTS.COINGECKO) 89 | } 90 | 91 | /** 92 | * 获取代币价格 93 | */ 94 | async getTokenPrices(tokenIds: string[]): Promise> { 95 | try { 96 | const params: Record = { 97 | ids: tokenIds.join(','), 98 | vs_currencies: 'usd', 99 | include_24hr_change: 'true' 100 | } 101 | 102 | // 如果有API Key,添加到参数中 103 | const apiKey = process.env.COINGECKO_API_KEY 104 | if (apiKey) { 105 | params.x_cg_demo_api_key = apiKey 106 | } 107 | 108 | const response = await this.client.get>('simple/price', params) 109 | return response 110 | } catch (error) { 111 | console.warn('⚠️ CoinGecko API 请求失败,可能是网络问题或请求频率限制:', error) 112 | 113 | // 返回默认价格数据以避免应用崩溃 114 | const fallbackPrices: Record = {} 115 | tokenIds.forEach(id => { 116 | fallbackPrices[id] = { 117 | usd: 0, 118 | usd_24h_change: 0 119 | } 120 | }) 121 | return fallbackPrices 122 | } 123 | } 124 | 125 | /** 126 | * 获取以太坊价格 127 | */ 128 | async getEthereumPrice(): Promise { 129 | try { 130 | const prices = await this.getTokenPrices(['ethereum']) 131 | return prices.ethereum || null 132 | } catch (error) { 133 | console.error('Failed to fetch Ethereum price:', error) 134 | return null 135 | } 136 | } 137 | 138 | /** 139 | * 根据合约地址获取代币价格 140 | */ 141 | async getTokenPriceByContract(contractAddress: string): Promise { 142 | try { 143 | const params: Record = { 144 | contract_addresses: contractAddress, 145 | vs_currencies: 'usd', 146 | include_24hr_change: 'true' 147 | } 148 | 149 | const apiKey = process.env.COINGECKO_API_KEY 150 | if (apiKey) { 151 | params.x_cg_demo_api_key = apiKey 152 | } 153 | 154 | const response = await this.client.get>('simple/token_price/ethereum', params) 155 | return response[contractAddress.toLowerCase()] || null 156 | } catch (error) { 157 | console.error('Failed to fetch token price by contract:', error) 158 | return null 159 | } 160 | } 161 | } 162 | 163 | /** 164 | * Etherscan API 服务 165 | * 🔐 仅用于服务端 - 包含敏感API密钥 166 | */ 167 | export class EtherscanService { 168 | private client: ApiClient 169 | private apiKey: string 170 | 171 | constructor() { 172 | this.client = new ApiClient(API_ENDPOINTS.ETHERSCAN) 173 | this.apiKey = process.env.ETHERSCAN_API_KEY || '' 174 | 175 | if (!this.apiKey) { 176 | console.warn('⚠️ Etherscan API Key 未配置,将使用免费限制版本') 177 | } 178 | } 179 | 180 | /** 181 | * 获取账户ETH余额 182 | */ 183 | async getAccountBalance(address: string): Promise { 184 | try { 185 | const response = await this.client.get>('', { 186 | module: 'account', 187 | action: 'balance', 188 | address, 189 | tag: 'latest', 190 | apikey: this.apiKey 191 | }) 192 | if (response.status === '1') { 193 | return response.result 194 | } 195 | 196 | // 处理API错误 197 | if (response.message === 'NOTOK') { 198 | console.warn('⚠️ Etherscan API 请求失败,可能是API密钥问题或请求频率限制') 199 | return '0' 200 | } 201 | 202 | throw new Error(response.message) 203 | } catch (error) { 204 | console.error('Failed to fetch account balance:', error) 205 | return '0' 206 | } 207 | } 208 | 209 | /** 210 | * 获取账户交易记录 211 | */ 212 | async getAccountTransactions( 213 | address: string, 214 | page: number = 1, 215 | offset: number = 10 216 | ): Promise { 217 | try { 218 | const response = await this.client.get>('', { 219 | module: 'account', 220 | action: 'txlist', 221 | address, 222 | startblock: '0', 223 | endblock: '99999999', 224 | page: page.toString(), 225 | offset: offset.toString(), 226 | sort: 'desc', 227 | apikey: this.apiKey 228 | }) 229 | 230 | if (response.status === '1') { 231 | return response.result 232 | } 233 | return [] 234 | } catch (error) { 235 | console.error('Failed to fetch account transactions:', error) 236 | return [] 237 | } 238 | } 239 | 240 | /** 241 | * 获取ERC20代币转账记录 242 | */ 243 | async getTokenTransfers( 244 | address: string, 245 | contractAddress?: string, 246 | page: number = 1, 247 | offset: number = 10 248 | ): Promise { 249 | try { 250 | const params: Record = { 251 | module: 'account', 252 | action: 'tokentx', 253 | address, 254 | page: page.toString(), 255 | offset: offset.toString(), 256 | sort: 'desc', 257 | apikey: this.apiKey 258 | } 259 | 260 | if (contractAddress) { 261 | params.contractaddress = contractAddress 262 | } 263 | 264 | const response = await this.client.get>('', params) 265 | 266 | if (response.status === '1') { 267 | return response.result 268 | } 269 | return [] 270 | } catch (error) { 271 | console.error('Failed to fetch token transfers:', error) 272 | return [] 273 | } 274 | } 275 | 276 | /** 277 | * 获取账户ERC20代币余额 278 | */ 279 | async getTokenBalance(address: string, contractAddress: string): Promise { 280 | try { 281 | const response = await this.client.get>('', { 282 | module: 'account', 283 | action: 'tokenbalance', 284 | contractaddress: contractAddress, 285 | address, 286 | tag: 'latest', 287 | apikey: this.apiKey 288 | }) 289 | 290 | if (response.status === '1') { 291 | return response.result 292 | } 293 | return '0' 294 | } catch (error) { 295 | console.error('Failed to fetch token balance:', error) 296 | return '0' 297 | } 298 | } 299 | } 300 | 301 | // 导出服务实例(仅用于服务端) 302 | export const coinGeckoService = new CoinGeckoService() 303 | export const etherscanService = new EtherscanService() 304 | 305 | // 注意: 306 | // - 以上服务类包含API密钥,仅应在服务端使用 307 | // - 客户端应该使用 ClientApiService 通过内部API路由获取数据 308 | // - 这样可以保护敏感信息不被暴露在客户端代码中 -------------------------------------------------------------------------------- /src/services/clientApi.ts: -------------------------------------------------------------------------------- 1 | // 客户端API服务 - 通过内部API路由获取数据 2 | export class ClientApiService { 3 | private baseUrl: string 4 | 5 | constructor() { 6 | this.baseUrl = process.env.NODE_ENV === 'production' 7 | ? process.env.NEXT_PUBLIC_APP_URL || '' 8 | : 'http://localhost:3000' 9 | } 10 | 11 | /** 12 | * 获取用户资产数据 13 | */ 14 | async getUserAssets(address: string) { 15 | try { 16 | const ret = await fetch( 17 | `${this.baseUrl}/api/assets?address=${encodeURIComponent(address)}` 18 | ) 19 | const data = await ret.json() 20 | return data.assets || [] 21 | } catch (error) { 22 | console.error('获取用户资产失败:', error) 23 | return [] 24 | } 25 | } 26 | 27 | /** 28 | * 获取用户交易记录 29 | */ 30 | async getUserTransactions(address: string) { 31 | try { 32 | const ret = await fetch( 33 | `${this.baseUrl}/api/transactions?address=${encodeURIComponent(address)}` 34 | ) 35 | const data = await ret.json() 36 | 37 | return data.transactions || [] 38 | } catch (error) { 39 | console.error('获取交易记录失败:', error) 40 | return [] 41 | } 42 | } 43 | 44 | /** 45 | * 获取以太坊价格 46 | */ 47 | async getEthereumPrice() { 48 | try { 49 | const ret = await fetch(`${this.baseUrl}/api/prices`) 50 | const data = await ret.json() 51 | return data.ethereumPrice 52 | } catch (error) { 53 | console.error('获取以太坊价格失败:', error) 54 | return null 55 | } 56 | } 57 | 58 | /** 59 | * 获取代币价格 60 | */ 61 | async getTokenPrices(tokenIds: string[]) { 62 | try { 63 | const ret = await fetch( 64 | `${this.baseUrl}/api/prices?tokenIds=${tokenIds.join(',')}` 65 | ) 66 | const data = await ret.json() 67 | return data.prices || {} 68 | } catch (error) { 69 | console.error('获取代币价格失败:', error) 70 | return {} 71 | } 72 | } 73 | 74 | /** 75 | * 根据合约地址获取代币价格 76 | */ 77 | async getTokenPriceByContract(contractAddress: string) { 78 | try { 79 | const ret = await fetch( 80 | `${this.baseUrl}/api/prices?contractAddress=${encodeURIComponent(contractAddress)}` 81 | ) 82 | const data = await ret.json() 83 | return data.price 84 | } catch (error) { 85 | console.error('根据合约获取代币价格失败:', error) 86 | return null 87 | } 88 | } 89 | 90 | /** 91 | * 获取账户ETH余额 92 | */ 93 | async getAccountBalance(address: string) { 94 | try { 95 | const ret = await fetch( 96 | `${this.baseUrl}/api/balance?address=${encodeURIComponent(address)}` 97 | ) 98 | const data = await ret.json() 99 | return data.balance || '0' 100 | } catch (error) { 101 | console.error('获取账户余额失败:', error) 102 | return '0' 103 | } 104 | } 105 | 106 | /** 107 | * 获取代币余额 108 | */ 109 | async getTokenBalance(address: string, contractAddress: string) { 110 | try { 111 | const ret = await fetch( 112 | `${this.baseUrl}/api/balance?address=${encodeURIComponent(address)}&contractAddress=${encodeURIComponent(contractAddress)}` 113 | ) 114 | const data = await ret.json() 115 | return data.balance || '0' 116 | } catch (error) { 117 | console.error('获取代币余额失败:', error) 118 | return '0' 119 | } 120 | } 121 | } 122 | 123 | // 导出客户端服务实例 124 | export const clientApiService = new ClientApiService() -------------------------------------------------------------------------------- /src/store/useWalletStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { clientApiService } from '@/services/clientApi' 3 | 4 | interface Asset { 5 | symbol: string 6 | name: string 7 | amount: string 8 | usd: string 9 | icon: string 10 | change: string 11 | address: string 12 | } 13 | 14 | interface Transaction { 15 | hash: string 16 | type: string 17 | amount: string 18 | status: 'success' | 'pending' | 'failed' 19 | time: string 20 | from: string 21 | to: string 22 | } 23 | 24 | interface WalletState { 25 | // 连接状态 26 | isConnected: boolean 27 | address: string | null 28 | chainId: number | null 29 | 30 | // 数据状态 31 | assets: Asset[] 32 | transactions: Transaction[] 33 | totalValue: string 34 | 35 | // 加载状态 36 | isLoadingAssets: boolean 37 | isLoadingTransactions: boolean 38 | 39 | // 错误状态 40 | error: string | null 41 | 42 | // Actions 43 | setConnection: (isConnected: boolean, address?: string, chainId?: number) => void 44 | setAssets: (assets: Asset[]) => void 45 | setTransactions: (transactions: Transaction[]) => void 46 | setLoading: (type: 'assets' | 'transactions', loading: boolean) => void 47 | setError: (error: string | null) => void 48 | 49 | // 数据获取方法 50 | fetchUserData: (address: string) => Promise 51 | fetchAssets: (address: string) => Promise 52 | fetchTransactions: (address: string) => Promise 53 | 54 | // 重置方法 55 | reset: () => void 56 | refresh: () => Promise 57 | } 58 | 59 | export const useWalletStore = create((set, get) => ({ 60 | // 初始状态 61 | isConnected: false, 62 | address: null, 63 | chainId: null, 64 | 65 | assets: [], 66 | transactions: [], 67 | totalValue: '$0.00', 68 | 69 | isLoadingAssets: false, 70 | isLoadingTransactions: false, 71 | 72 | error: null, 73 | 74 | // 设置连接状态 75 | setConnection: (isConnected, address, chainId) => { 76 | set({ 77 | isConnected, 78 | address: address || null, 79 | chainId: chainId || null, 80 | error: null 81 | }) 82 | 83 | // 如果连接成功,获取用户数据 84 | if (isConnected && address) { 85 | get().fetchUserData(address) 86 | } else { 87 | // 如果断开连接,重置数据 88 | get().reset() 89 | } 90 | }, 91 | 92 | // 设置资产数据 93 | setAssets: (assets) => { 94 | // 计算总价值 95 | const totalValue = assets.reduce((total, asset) => { 96 | const value = parseFloat(asset.usd.replace(/[$,]/g, '')) 97 | return total + (isNaN(value) ? 0 : value) 98 | }, 0) 99 | 100 | set({ 101 | assets, 102 | totalValue: `$${totalValue.toLocaleString('en-US', { 103 | minimumFractionDigits: 2, 104 | maximumFractionDigits: 2 105 | })}` 106 | }) 107 | }, 108 | 109 | // 设置交易数据 110 | setTransactions: (transactions) => { 111 | set({ transactions }) 112 | }, 113 | 114 | // 设置加载状态 115 | setLoading: (type, loading) => { 116 | if (type === 'assets') { 117 | set({ isLoadingAssets: loading }) 118 | } else { 119 | set({ isLoadingTransactions: loading }) 120 | } 121 | }, 122 | 123 | // 设置错误状态 124 | setError: (error) => { 125 | set({ error }) 126 | }, 127 | 128 | // 获取用户完整数据 129 | fetchUserData: async (address: string) => { 130 | const { fetchAssets, fetchTransactions } = get() 131 | 132 | try { 133 | set({ error: null }) 134 | 135 | // 并行获取资产和交易数据 136 | await Promise.all([ 137 | fetchAssets(address), 138 | fetchTransactions(address) 139 | ]) 140 | } catch (error) { 141 | console.error('Failed to fetch user data:', error) 142 | set({ error: '获取用户数据失败,请稍后重试' }) 143 | } 144 | }, 145 | 146 | // 获取资产数据 147 | fetchAssets: async (address: string) => { 148 | const { setAssets, setLoading, setError } = get() 149 | 150 | try { 151 | setLoading('assets', true) 152 | setError(null) 153 | 154 | const assets = await clientApiService.getUserAssets(address) 155 | setAssets(assets) 156 | } catch (error) { 157 | console.error('Failed to fetch assets:', error) 158 | setError('获取资产数据失败') 159 | setAssets([]) 160 | } finally { 161 | setLoading('assets', false) 162 | } 163 | }, 164 | 165 | // 获取交易数据 166 | fetchTransactions: async (address: string) => { 167 | const { setTransactions, setLoading, setError } = get() 168 | 169 | try { 170 | setLoading('transactions', true) 171 | 172 | const transactions = await clientApiService.getUserTransactions(address) 173 | setTransactions(transactions) 174 | } catch (error) { 175 | console.error('Failed to fetch transactions:', error) 176 | setError('获取交易记录失败') 177 | setTransactions([]) 178 | } finally { 179 | setLoading('transactions', false) 180 | } 181 | }, 182 | 183 | // 重置所有数据 184 | reset: () => { 185 | set({ 186 | isConnected: false, 187 | address: null, 188 | chainId: null, 189 | assets: [], 190 | transactions: [], 191 | totalValue: '$0.00', 192 | isLoadingAssets: false, 193 | isLoadingTransactions: false, 194 | error: null 195 | }) 196 | }, 197 | 198 | // 刷新数据 199 | refresh: async () => { 200 | const { address, fetchUserData } = get() 201 | 202 | if (address) { 203 | await fetchUserData(address) 204 | } 205 | } 206 | })) 207 | 208 | // 导出类型 209 | export type { Asset, Transaction, WalletState } -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Asset { 2 | symbol: string 3 | name: string 4 | amount: string 5 | usd: string 6 | icon: string 7 | address?: string 8 | change?: string 9 | } 10 | 11 | export interface Transaction { 12 | hash: string 13 | type: string 14 | amount: string 15 | status: 'success' | 'pending' | 'failed' 16 | time: string 17 | from?: string 18 | to?: string 19 | } 20 | 21 | export interface WalletState { 22 | isConnected: boolean 23 | address: string | null 24 | balance: string 25 | assets: Asset[] 26 | transactions: Transaction[] 27 | portfolioValue: string 28 | dailyChange: string 29 | isLoading: boolean 30 | 31 | // Actions 32 | setConnected: (connected: boolean) => void 33 | setAddress: (address: string | null) => void 34 | setBalance: (balance: string) => void 35 | setAssets: (assets: Asset[]) => void 36 | setTransactions: (transactions: Transaction[]) => void 37 | setPortfolioValue: (value: string) => void 38 | setDailyChange: (change: string) => void 39 | setLoading: (loading: boolean) => void 40 | reset: () => void 41 | } 42 | 43 | export interface ContractInfo { 44 | address: string 45 | name: string 46 | symbol: string 47 | decimals: string 48 | totalSupply: string 49 | maxSupply: string 50 | } 51 | 52 | export interface DeploymentInfo { 53 | network: any 54 | deployer: string 55 | contracts: { 56 | DeFiToken: ContractInfo 57 | } 58 | timestamp: string 59 | } -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // 网络配置 2 | export const NETWORKS = { 3 | MAINNET: { 4 | id: 1, 5 | name: 'Ethereum Mainnet', 6 | rpcUrl: 'https://mainnet.infura.io/v3/', 7 | blockExplorer: 'https://etherscan.io', 8 | }, 9 | SEPOLIA: { 10 | id: 11155111, 11 | name: 'Sepolia Testnet', 12 | rpcUrl: 'https://sepolia.infura.io/v3/', 13 | blockExplorer: 'https://sepolia.etherscan.io', 14 | }, 15 | } as const 16 | 17 | // API 端点配置 18 | export const API_ENDPOINTS = { 19 | COINGECKO: 'https://api.coingecko.com/api/v3/', 20 | ETHERSCAN: process.env.ETHERSCAN_NETWORK || 'https://api-sepolia.etherscan.io/api', 21 | ALCHEMY: 'https://eth-mainnet.g.alchemy.com/v2', 22 | INFURA: 'https://mainnet.infura.io/v3' 23 | } as const 24 | 25 | // 支持的网络配置 26 | export const SUPPORTED_CHAINS = { 27 | ETHEREUM: { 28 | id: 1, 29 | name: 'Ethereum', 30 | symbol: 'ETH', 31 | rpcUrl: `${API_ENDPOINTS.ALCHEMY}/${process.env.ALCHEMY_API_KEY}`, 32 | blockExplorer: 'https://etherscan.io' 33 | }, 34 | SEPOLIA: { 35 | id: 11155111, 36 | name: 'Sepolia', 37 | symbol: 'ETH', 38 | rpcUrl: `${API_ENDPOINTS.ALCHEMY}/${process.env.ALCHEMY_API_KEY}`, 39 | blockExplorer: 'https://sepolia.etherscan.io' 40 | } 41 | } as const 42 | 43 | // 主要代币合约地址 (Ethereum Mainnet) 44 | export const TOKEN_CONTRACTS = { 45 | USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 46 | USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7', 47 | UNI: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', 48 | LINK: '0x514910771AF9Ca656af840dff83E8264EcF986CA', 49 | WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 50 | DAI: '0x6B175474E89094C44Da98b954EedeAC495271d0F', 51 | WBTC: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' 52 | } as const 53 | 54 | // CoinGecko 代币ID映射 55 | export const COINGECKO_TOKEN_IDS = { 56 | [TOKEN_CONTRACTS.USDC]: 'usd-coin', 57 | [TOKEN_CONTRACTS.USDT]: 'tether', 58 | [TOKEN_CONTRACTS.UNI]: 'uniswap', 59 | [TOKEN_CONTRACTS.LINK]: 'chainlink', 60 | [TOKEN_CONTRACTS.WETH]: 'weth', 61 | [TOKEN_CONTRACTS.DAI]: 'dai', 62 | [TOKEN_CONTRACTS.WBTC]: 'wrapped-bitcoin' 63 | } as const 64 | 65 | // 代币信息配置 66 | export const TOKEN_INFO = { 67 | ETH: { 68 | symbol: 'ETH', 69 | name: 'Ethereum', 70 | decimals: 18, 71 | icon: 'Ξ', 72 | coingeckoId: 'ethereum' 73 | }, 74 | [TOKEN_CONTRACTS.USDC]: { 75 | symbol: 'USDC', 76 | name: 'USD Coin', 77 | decimals: 6, 78 | icon: '$', 79 | coingeckoId: 'usd-coin' 80 | }, 81 | [TOKEN_CONTRACTS.USDT]: { 82 | symbol: 'USDT', 83 | name: 'Tether USD', 84 | decimals: 6, 85 | icon: '₮', 86 | coingeckoId: 'tether' 87 | }, 88 | [TOKEN_CONTRACTS.UNI]: { 89 | symbol: 'UNI', 90 | name: 'Uniswap', 91 | decimals: 18, 92 | icon: '🦄', 93 | coingeckoId: 'uniswap' 94 | }, 95 | [TOKEN_CONTRACTS.LINK]: { 96 | symbol: 'LINK', 97 | name: 'Chainlink', 98 | decimals: 18, 99 | icon: '🔗', 100 | coingeckoId: 'chainlink' 101 | }, 102 | [TOKEN_CONTRACTS.WETH]: { 103 | symbol: 'WETH', 104 | name: 'Wrapped Ether', 105 | decimals: 18, 106 | icon: 'Ξ', 107 | coingeckoId: 'weth' 108 | }, 109 | [TOKEN_CONTRACTS.DAI]: { 110 | symbol: 'DAI', 111 | name: 'Dai Stablecoin', 112 | decimals: 18, 113 | icon: '◈', 114 | coingeckoId: 'dai' 115 | }, 116 | [TOKEN_CONTRACTS.WBTC]: { 117 | symbol: 'WBTC', 118 | name: 'Wrapped BTC', 119 | decimals: 8, 120 | icon: '₿', 121 | coingeckoId: 'wrapped-bitcoin' 122 | } 123 | } as const 124 | 125 | // 交易状态配置 126 | export const TRANSACTION_STATUS = { 127 | SUCCESS: 'success', 128 | PENDING: 'pending', 129 | FAILED: 'failed' 130 | } as const 131 | 132 | // 交易类型配置 133 | export const TRANSACTION_TYPES = { 134 | TRANSFER: 'Transfer', 135 | TOKEN_TRANSFER: 'Token Transfer', 136 | SWAP: 'Swap', 137 | APPROVE: 'Approve', 138 | DEPOSIT: 'Deposit', 139 | WITHDRAW: 'Withdraw' 140 | } as const 141 | 142 | // 应用配置 143 | export const APP_CONFIG = { 144 | NAME: 'DeFi Dashboard', 145 | DESCRIPTION: '现代化的DeFi资产管理面板', 146 | VERSION: '1.0.0', 147 | GITHUB_URL: 'https://github.com/your-username/defi-dashboard', 148 | DOCS_URL: 'https://docs.your-domain.com' 149 | } as const 150 | 151 | // 默认设置 152 | export const DEFAULT_SETTINGS = { 153 | THEME: 'dark', 154 | CURRENCY: 'USD', 155 | LANGUAGE: 'zh-CN', 156 | REFRESH_INTERVAL: 30000, // 30秒 157 | ITEMS_PER_PAGE: 10 158 | } as const 159 | 160 | // 错误消息 161 | export const ERROR_MESSAGES = { 162 | WALLET_NOT_CONNECTED: '请先连接钱包', 163 | NETWORK_NOT_SUPPORTED: '不支持的网络', 164 | INSUFFICIENT_BALANCE: '余额不足', 165 | TRANSACTION_FAILED: '交易失败', 166 | API_ERROR: 'API请求失败', 167 | INVALID_ADDRESS: '无效的地址格式' 168 | } as const 169 | 170 | // 本地存储键 171 | export const STORAGE_KEYS = { 172 | THEME: 'theme', 173 | WALLET_STORAGE: 'wallet-storage', 174 | USER_PREFERENCES: 'user-preferences', 175 | } as const 176 | 177 | // 动画配置 178 | export const ANIMATION_CONFIG = { 179 | DURATION: { 180 | FAST: 150, 181 | NORMAL: 300, 182 | SLOW: 500, 183 | }, 184 | EASING: { 185 | EASE_IN: 'ease-in', 186 | EASE_OUT: 'ease-out', 187 | EASE_IN_OUT: 'ease-in-out', 188 | }, 189 | } as const -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 格式化数字为货币格式 3 | */ 4 | export function formatCurrency( 5 | value: number | string, 6 | currency: string = 'USD', 7 | locale: string = 'en-US' 8 | ): string { 9 | const numValue = typeof value === 'string' ? parseFloat(value) : value 10 | 11 | if (isNaN(numValue)) return '$0.00' 12 | 13 | return new Intl.NumberFormat(locale, { 14 | style: 'currency', 15 | currency, 16 | minimumFractionDigits: 2, 17 | maximumFractionDigits: 2, 18 | }).format(numValue) 19 | } 20 | 21 | /** 22 | * 格式化大数字(K, M, B) 23 | */ 24 | export function formatLargeNumber(value: number | string): string { 25 | const numValue = typeof value === 'string' ? parseFloat(value) : value 26 | 27 | if (isNaN(numValue)) return '0' 28 | 29 | if (numValue >= 1e9) { 30 | return (numValue / 1e9).toFixed(1) + 'B' 31 | } 32 | if (numValue >= 1e6) { 33 | return (numValue / 1e6).toFixed(1) + 'M' 34 | } 35 | if (numValue >= 1e3) { 36 | return (numValue / 1e3).toFixed(1) + 'K' 37 | } 38 | 39 | return numValue.toFixed(2) 40 | } 41 | 42 | /** 43 | * 格式化百分比 44 | */ 45 | export function formatPercentage(value: number | string): string { 46 | const numValue = typeof value === 'string' ? parseFloat(value) : value 47 | 48 | if (isNaN(numValue)) return '0%' 49 | 50 | const sign = numValue >= 0 ? '+' : '' 51 | return `${sign}${numValue.toFixed(2)}%` 52 | } 53 | 54 | /** 55 | * 格式化以太坊地址 56 | */ 57 | export function formatAddress( 58 | address: string, 59 | startLength: number = 6, 60 | endLength: number = 4 61 | ): string { 62 | if (!address) return '' 63 | 64 | if (address.length <= startLength + endLength) { 65 | return address 66 | } 67 | 68 | return `${address.slice(0, startLength)}...${address.slice(-endLength)}` 69 | } 70 | 71 | /** 72 | * 格式化交易哈希 73 | */ 74 | export function formatTxHash(hash: string): string { 75 | return formatAddress(hash, 10, 8) 76 | } 77 | 78 | /** 79 | * 格式化时间为相对时间 80 | */ 81 | export function formatRelativeTime(timestamp: number | string | Date): string { 82 | const date = new Date(timestamp) 83 | const now = new Date() 84 | const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000) 85 | 86 | if (diffInSeconds < 60) { 87 | return `${diffInSeconds} 秒前` 88 | } 89 | 90 | const diffInMinutes = Math.floor(diffInSeconds / 60) 91 | if (diffInMinutes < 60) { 92 | return `${diffInMinutes} 分钟前` 93 | } 94 | 95 | const diffInHours = Math.floor(diffInMinutes / 60) 96 | if (diffInHours < 24) { 97 | return `${diffInHours} 小时前` 98 | } 99 | 100 | const diffInDays = Math.floor(diffInHours / 24) 101 | if (diffInDays < 30) { 102 | return `${diffInDays} 天前` 103 | } 104 | 105 | const diffInMonths = Math.floor(diffInDays / 30) 106 | if (diffInMonths < 12) { 107 | return `${diffInMonths} 个月前` 108 | } 109 | 110 | const diffInYears = Math.floor(diffInMonths / 12) 111 | return `${diffInYears} 年前` 112 | } 113 | 114 | /** 115 | * 格式化代币数量 116 | */ 117 | export function formatTokenAmount( 118 | amount: string | number, 119 | decimals: number = 18, 120 | displayDecimals: number = 4 121 | ): string { 122 | const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount 123 | 124 | if (isNaN(numAmount)) return '0' 125 | 126 | // 如果是wei单位,转换为ether 127 | const etherAmount = decimals === 18 ? numAmount / 1e18 : numAmount 128 | 129 | return etherAmount.toFixed(displayDecimals) 130 | } 131 | 132 | /** 133 | * 验证以太坊地址 134 | */ 135 | export function isValidAddress(address: string): boolean { 136 | return /^0x[a-fA-F0-9]{40}$/.test(address) 137 | } 138 | 139 | /** 140 | * 验证交易哈希 141 | */ 142 | export function isValidTxHash(hash: string): boolean { 143 | return /^0x[a-fA-F0-9]{64}$/.test(hash) 144 | } -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "🚀 启动 DeFi Dashboard 项目..." 4 | echo "" 5 | 6 | # 检查 Node.js 版本 7 | echo "📋 检查环境..." 8 | node_version=$(node -v 2>/dev/null) 9 | if [ $? -eq 0 ]; then 10 | echo "✅ Node.js 版本: $node_version" 11 | else 12 | echo "❌ 未找到 Node.js,请先安装 Node.js" 13 | exit 1 14 | fi 15 | 16 | echo "" 17 | 18 | # 检查依赖是否已安装 19 | if [ ! -d "node_modules" ]; then 20 | echo "📦 安装项目依赖..." 21 | npm install 22 | if [ $? -ne 0 ]; then 23 | echo "❌ 依赖安装失败" 24 | exit 1 25 | fi 26 | echo "✅ 依赖安装完成" 27 | else 28 | echo "✅ 依赖已安装" 29 | fi 30 | 31 | echo "" 32 | echo "🎉 准备工作完成!" 33 | echo "" 34 | echo "🌐 启动开发服务器..." 35 | echo "📱 应用将在 http://localhost:3000 启动" 36 | echo "" 37 | echo "💡 使用说明:" 38 | echo " 1. 打开浏览器访问 http://localhost:3000" 39 | echo " 2. 点击右上角的 '连接钱包' 按钮" 40 | echo " 3. 选择您的钱包(如 MetaMask)进行连接" 41 | echo " 4. 连接后即可查看资产和交易记录" 42 | echo "" 43 | echo "🛑 按 Ctrl+C 停止服务器" 44 | echo "" 45 | 46 | # 启动开发服务器 47 | npm run dev -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | darkMode: 'class', 9 | theme: { 10 | extend: { 11 | colors: { 12 | primary: { 13 | 50: '#fdf2f8', 14 | 100: '#fce7f3', 15 | 200: '#fbcfe8', 16 | 300: '#f9a8d4', 17 | 400: '#f472b6', 18 | 500: '#ec4899', 19 | 600: '#db2777', 20 | 700: '#be185d', 21 | 800: '#9d174d', 22 | 900: '#831843', 23 | }, 24 | accent: '#ff007a', 25 | success: '#10b981', 26 | warning: '#f59e0b', 27 | error: '#ef4444', 28 | }, 29 | fontFamily: { 30 | sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'], 31 | mono: ['Monaco', 'Menlo', 'monospace'], 32 | }, 33 | animation: { 34 | 'fade-in': 'fadeIn 0.5s ease-in', 35 | 'pulse': 'pulse 2s infinite', 36 | 'spin': 'spin 1s linear infinite', 37 | }, 38 | keyframes: { 39 | fadeIn: { 40 | '0%': { opacity: '0', transform: 'translateY(20px)' }, 41 | '100%': { opacity: '1', transform: 'translateY(0)' }, 42 | }, 43 | }, 44 | boxShadow: { 45 | 'card': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', 46 | 'card-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', 47 | }, 48 | }, 49 | }, 50 | plugins: [], 51 | } -------------------------------------------------------------------------------- /test/DeFiToken.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { ethers } = require("hardhat"); 3 | 4 | describe("DeFiToken", function () { 5 | let DeFiToken; 6 | let defiToken; 7 | let owner; 8 | let addr1; 9 | let addr2; 10 | 11 | beforeEach(async function () { 12 | // Get signers 13 | [owner, addr1, addr2] = await ethers.getSigners(); 14 | 15 | // Deploy contract 16 | DeFiToken = await ethers.getContractFactory("DeFiToken"); 17 | defiToken = await DeFiToken.deploy( 18 | "DeFi Dashboard Token", 19 | "DDT", 20 | owner.address 21 | ); 22 | await defiToken.waitForDeployment(); 23 | }); 24 | 25 | describe("Deployment", function () { 26 | it("Should set the right owner", async function () { 27 | expect(await defiToken.owner()).to.equal(owner.address); 28 | }); 29 | 30 | it("Should assign the initial supply to the owner", async function () { 31 | const ownerBalance = await defiToken.balanceOf(owner.address); 32 | expect(await defiToken.totalSupply()).to.equal(ownerBalance); 33 | }); 34 | 35 | it("Should have correct token info", async function () { 36 | const tokenInfo = await defiToken.getTokenInfo(); 37 | expect(tokenInfo.tokenName).to.equal("DeFi Dashboard Token"); 38 | expect(tokenInfo.tokenSymbol).to.equal("DDT"); 39 | expect(tokenInfo.tokenDecimals).to.equal(18); 40 | }); 41 | }); 42 | 43 | describe("Minting", function () { 44 | it("Should allow owner to mint tokens", async function () { 45 | const mintAmount = ethers.parseEther("1000"); 46 | await defiToken.mint(addr1.address, mintAmount); 47 | 48 | expect(await defiToken.balanceOf(addr1.address)).to.equal(mintAmount); 49 | }); 50 | 51 | it("Should not allow non-owner to mint tokens", async function () { 52 | const mintAmount = ethers.parseEther("1000"); 53 | 54 | await expect( 55 | defiToken.connect(addr1).mint(addr1.address, mintAmount) 56 | ).to.be.revertedWithCustomError(defiToken, "OwnableUnauthorizedAccount"); 57 | }); 58 | 59 | it("Should not allow minting beyond max supply", async function () { 60 | const maxSupply = await defiToken.MAX_SUPPLY(); 61 | const currentSupply = await defiToken.totalSupply(); 62 | const excessAmount = maxSupply - currentSupply + ethers.parseEther("1"); 63 | 64 | await expect( 65 | defiToken.mint(addr1.address, excessAmount) 66 | ).to.be.revertedWith("DeFiToken: Max supply exceeded"); 67 | }); 68 | 69 | it("Should emit TokensMinted event", async function () { 70 | const mintAmount = ethers.parseEther("1000"); 71 | 72 | await expect(defiToken.mint(addr1.address, mintAmount)) 73 | .to.emit(defiToken, "TokensMinted") 74 | .withArgs(addr1.address, mintAmount); 75 | }); 76 | }); 77 | 78 | describe("Burning", function () { 79 | beforeEach(async function () { 80 | // Mint some tokens to addr1 for testing 81 | await defiToken.mint(addr1.address, ethers.parseEther("1000")); 82 | }); 83 | 84 | it("Should allow users to burn their tokens", async function () { 85 | const burnAmount = ethers.parseEther("500"); 86 | const initialBalance = await defiToken.balanceOf(addr1.address); 87 | 88 | await defiToken.connect(addr1).burn(burnAmount); 89 | 90 | expect(await defiToken.balanceOf(addr1.address)).to.equal( 91 | initialBalance - burnAmount 92 | ); 93 | }); 94 | 95 | it("Should emit TokensBurned event", async function () { 96 | const burnAmount = ethers.parseEther("500"); 97 | 98 | await expect(defiToken.connect(addr1).burn(burnAmount)) 99 | .to.emit(defiToken, "TokensBurned") 100 | .withArgs(addr1.address, burnAmount); 101 | }); 102 | 103 | it("Should allow burning with allowance", async function () { 104 | const burnAmount = ethers.parseEther("500"); 105 | 106 | // Approve addr2 to spend addr1's tokens 107 | await defiToken.connect(addr1).approve(addr2.address, burnAmount); 108 | 109 | // addr2 burns addr1's tokens 110 | await defiToken.connect(addr2).burnFrom(addr1.address, burnAmount); 111 | 112 | expect(await defiToken.balanceOf(addr1.address)).to.equal( 113 | ethers.parseEther("500") 114 | ); 115 | }); 116 | }); 117 | 118 | describe("Transfers", function () { 119 | beforeEach(async function () { 120 | // Mint some tokens to addr1 for testing 121 | await defiToken.mint(addr1.address, ethers.parseEther("1000")); 122 | }); 123 | 124 | it("Should transfer tokens between accounts", async function () { 125 | const transferAmount = ethers.parseEther("500"); 126 | 127 | await defiToken.connect(addr1).transfer(addr2.address, transferAmount); 128 | 129 | expect(await defiToken.balanceOf(addr1.address)).to.equal( 130 | ethers.parseEther("500") 131 | ); 132 | expect(await defiToken.balanceOf(addr2.address)).to.equal(transferAmount); 133 | }); 134 | 135 | it("Should fail if sender doesn't have enough tokens", async function () { 136 | const transferAmount = ethers.parseEther("2000"); 137 | 138 | await expect( 139 | defiToken.connect(addr1).transfer(addr2.address, transferAmount) 140 | ).to.be.revertedWithCustomError(defiToken, "ERC20InsufficientBalance"); 141 | }); 142 | }); 143 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "ES6" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "baseUrl": ".", 26 | "paths": { 27 | "@/*": [ 28 | "./src/*" 29 | ], 30 | "@/components/*": [ 31 | "./src/components/*" 32 | ], 33 | "@/hooks/*": [ 34 | "./src/hooks/*" 35 | ], 36 | "@/store/*": [ 37 | "./src/store/*" 38 | ], 39 | "@/utils/*": [ 40 | "./src/utils/*" 41 | ], 42 | "@/types/*": [ 43 | "./src/types/*" 44 | ] 45 | } 46 | }, 47 | "include": [ 48 | "next-env.d.ts", 49 | "**/*.ts", 50 | "**/*.tsx", 51 | ".next/types/**/*.ts" 52 | ], 53 | "exclude": [ 54 | "node_modules", 55 | "contracts" 56 | ] 57 | } --------------------------------------------------------------------------------