diff --git a/basic/78-wallet-connect/docs/guides/installation.md b/basic/78-wallet-connect/docs/guides/installation.md new file mode 100644 index 000000000..eb16342be --- /dev/null +++ b/basic/78-wallet-connect/docs/guides/installation.md @@ -0,0 +1,191 @@ +# WalletConnect 安装和使用指南 + +## 安装 + +在 DApp 中使用 WalletConnect 时,可以安装 WalletConnect JavaScript 客户端库: + +```bash +npm install @walletconnect/client +``` + +对于钱包提供方,需要集成 WalletConnect SDK(提供 iOS、Android 和 Web 版本)。 + +## 基本使用 + +### 步骤 1:初始化 WalletConnect 客户端 + +在 JavaScript/TypeScript 代码中,导入并初始化 WalletConnect 客户端: + +```javascript +import WalletConnect from "@walletconnect/client"; +import QRCodeModal from "@walletconnect/qrcode-modal"; + +// 创建 WalletConnect 客户端实例 +const connector = new WalletConnect({ + bridge: "https://bridge.walletconnect.org", // 必填项 + qrcodeModal: QRCodeModal, +}); +``` + +### 步骤 2:检查连接并创建会话 + +如果没有已建立的连接,则创建一个新会话,提示用户连接钱包。 + +```javascript +// 检查是否已连接 +if (!connector.connected) { + // 创建新会话 + await connector.createSession(); +} + +// 监听会话事件 +connector.on("connect", (error, payload) => { + if (error) { + throw error; + } + + // 连接成功 + const { accounts, chainId } = payload.params[0]; +}); + +connector.on("session_update", (error, payload) => { + if (error) { + throw error; + } + + // 更新的账户和链ID + const { accounts, chainId } = payload.params[0]; +}); + +connector.on("disconnect", (error, payload) => { + if (error) { + throw error; + } + + // 已断开连接 +}); +``` + +### 步骤 3:发送交易 + +连接成功后,即可与区块链进行交互。例如,发送一笔交易: + +```javascript +// 设置交易详情 +const tx = { + from: accounts[0], // 用户地址 + to: "0xRecipientAddress", + value: "0xAmountInWei", // 金额,单位为 wei + data: "0x", // 可选数据 +}; + +// 发送交易请求 +const result = await connector.sendTransaction(tx); +``` + +## 高级配置 + +### 多链支持 + +要支持多个区块链网络,可以在初始化时配置: + +```javascript +const connector = new WalletConnect({ + bridge: "https://bridge.walletconnect.org", + qrcodeModal: QRCodeModal, + chainId: 1, // 默认以太坊主网 + supportedChainIds: [1, 56, 137] // 支持的链ID:以太坊、BSC、Polygon +}); +``` + +### 自定义配置 + +```javascript +const connector = new WalletConnect({ + bridge: "https://your-custom-bridge.com", + qrcodeModal: QRCodeModal, + clientMeta: { + name: "Your DApp", + description: "Your DApp Description", + url: "https://your-dapp.com", + icons: ["https://your-dapp.com/icon.png"] + } +}); +``` + +## 错误处理最佳实践 + +### 连接错误处理 + +```javascript +connector.on("connect", (error, payload) => { + if (error) { + console.error("连接错误:", error); + // 实现重试逻辑 + return; + } + // 连接成功处理 +}); + +// 自定义错误处理 +try { + await connector.createSession(); +} catch (error) { + if (error.message.includes("User rejected")) { + console.log("用户拒绝连接"); + } else { + console.error("创建会话失败:", error); + } +} +``` + +### 交易错误处理 + +```javascript +try { + const result = await connector.sendTransaction(tx); +} catch (error) { + if (error.message.includes("insufficient funds")) { + console.error("余额不足"); + } else if (error.message.includes("gas")) { + console.error("Gas 费用估算失败"); + } else { + console.error("交易失败:", error); + } +} +``` + +## 安全性考虑 + +### 数据加密 + +- 确保所有敏感数据在传输前进行加密 +- 不要在客户端存储私钥或助记词 +- 使用安全的通信协议(HTTPS) + +### 交易安全 + +- 在发送交易前进行金额和地址验证 +- 实现交易确认对话框 +- 显示详细的交易信息供用户确认 + +### 会话管理 + +- 定期检查会话状态 +- 实现自动断开连接的超时机制 +- 在用户离开应用时清理会话数据 + +## 性能优化 + +- 使用 WebSocket 保持连接状态 +- 实现重连机制 +- 缓存常用数据 +- 批量处理交易请求 + +## 常见问题 + +- **二维码未显示**:请确保 `QRCodeModal` 正确导入并配置。 +- **连接问题**:请检查桥接 URL,默认桥接为 `"https://bridge.walletconnect.org"`,也支持自托管桥接。 +- **断开连接**:检查网络状态,确保 WebSocket 连接正常。 +- **交易失败**:验证账户余额、Gas 费用设置是否合理。 +- **兼容性问题**:确认钱包版本是否支持当前协议版本。 diff --git a/basic/78-wallet-connect/docs/introduction/overview.md b/basic/78-wallet-connect/docs/introduction/overview.md new file mode 100644 index 000000000..3a23f903a --- /dev/null +++ b/basic/78-wallet-connect/docs/introduction/overview.md @@ -0,0 +1,445 @@ +# WalletConnect 概述 + +## 简介 + +**WalletConnect** 是一个开放协议,允许用户将加密钱包安全地连接到去中心化应用(DApps),并支持多区块链网络的交互。用户只需扫描二维码或使用深度链接,即可与 DApp 进行交互,而无需暴露私钥,提供了安全、便捷的体验。 + +WalletConnect 通过加密消息在钱包与 DApp 之间传递信息,确保安全性。此外,WalletConnect 还支持多链和多会话连接,已广泛应用于 DeFi 和 NFT 平台等 Web3 领域。 + +## 功能 + +- **多链支持**:WalletConnect v2 支持跨多个区块链网络的连接,用户可以在单一会话中管理多链资产。 +- **安全消息传递**:所有通信均加密,确保钱包和 DApp 之间的交互安全。 +- **灵活的会话管理**:支持多会话,用户可以轻松在不同 DApps 和区块链网络之间切换。 +- **简单的集成方式**:DApp 和钱包开发者可以使用 WalletConnect SDK 快速集成,并支持广泛的钱包连接。 +- **跨平台兼容**:支持 iOS、Android 和 Web,确保用户在各种设备上都能访问。 + +## 技术架构 + +### 核心组件 + +1. **Bridge Server**: + - 作为中继服务器,负责在钱包和 DApp 之间传递加密消息 + - 采用分布式架构,提高可用性和性能 + - 支持 WebSocket 长连接,实现实时通信 + + 示例配置: + ```javascript + const bridge = new WalletConnectBridge({ + url: "wss://bridge.walletconnect.org", + pingInterval: 15000, + maxAttempts: 5 + }); + ``` + +2. **Client SDK**: + - 提供多语言支持(JavaScript、Swift、Kotlin等) + - 封装核心协议逻辑,简化开发流程 + - 内置安全机制和错误处理 + + JavaScript 示例: + ```javascript + import WalletConnect from "@walletconnect/client"; + + const connector = new WalletConnect({ + bridge: "https://bridge.walletconnect.org", + qrcodeModal: QRCodeModal, + chainId: 1 + }); + + // 监听连接事件 + connector.on("connect", (error, payload) => { + if (error) { + console.error(error); + return; + } + const { accounts, chainId } = payload.params[0]; + console.log("Connected to", accounts[0]); + }); + ``` + +3. **协议层**: + - 定义标准消息格式和通信流程 + - 实现跨平台兼容性 + - 支持协议版本升级和向后兼容 + + 消息格式示例: + ```json + { + "id": 1234567890, + "jsonrpc": "2.0", + "method": "eth_sendTransaction", + "params": [{ + "from": "0x...", + "to": "0x...", + "value": "0x..." + }] + } + +### 连接流程 + +1. DApp 创建连接请求并生成二维码 +2. 用户使用钱包扫描二维码 +3. 建立加密通道 +4. 进行身份验证和授权 +5. 开始安全通信 + +## 安全机制 + +### 加密方案 + +- **非对称加密**:使用 X25519 密钥交换 +- **对称加密**:采用 AES-256-CBC 加密消息内容 +- **消息认证**:使用 HMAC-SHA256 确保消息完整性 + +### 安全特性 + +1. **零信任架构**: + - 所有通信均端到端加密 + - Bridge Server 无法解密消息内容 + - 每个会话使用唯一的密钥对 + +2. **权限控制**: + - 细粒度的操作授权 + - 支持会话过期和主动断开 + - 可限制特定链和方法的访问 + +## 应用场景 + +### DeFi 应用 + +- 去中心化交易 +- 流动性提供 +- 收益耕作 +- 借贷平台 + +### NFT 交易 + +- 市场交易 +- 铸造操作 +- 拍卖参与 +- 版税管理 + +### GameFi + +- 游戏资产管理 +- 道具交易 +- 成就奖励 +- 跨游戏互操作 + +## 性能优化 + +### 连接优化 + +1. **快速重连**: + - 缓存会话信息 + - 支持自动重连 + - 优化重连流程 + + 重连实现示例: + ```javascript + class ConnectionManager { + constructor() { + this.reconnectAttempts = 0; + this.maxAttempts = 5; + this.baseDelay = 1000; // 1秒 + } + + async reconnect() { + if (this.reconnectAttempts >= this.maxAttempts) { + throw new Error("重连次数超过限制"); + } + + // 使用指数退避策略 + const delay = this.baseDelay * Math.pow(2, this.reconnectAttempts); + await new Promise(resolve => setTimeout(resolve, delay)); + + try { + // 尝试恢复缓存的会话 + const session = localStorage.getItem("walletconnect"); + if (session) { + await connector.connect(JSON.parse(session)); + } else { + await connector.createSession(); + } + this.reconnectAttempts = 0; + return true; + } catch (error) { + this.reconnectAttempts++; + return this.reconnect(); + } + } + } + ``` + +2. **网络适应**: + - 动态超时调整 + - 自动重试机制 + - 网络状态监控 + + 网络适应示例: + ```javascript + class NetworkMonitor { + constructor() { + this.baseTimeout = 5000; + this.maxTimeout = 30000; + this.latencyHistory = []; + } + + // 计算动态超时时间 + calculateTimeout() { + if (this.latencyHistory.length === 0) { + return this.baseTimeout; + } + + // 使用最近5次请求的平均延迟 + const recentLatencies = this.latencyHistory.slice(-5); + const avgLatency = recentLatencies.reduce((a, b) => a + b, 0) / recentLatencies.length; + + // 动态超时 = 平均延迟 * 3 + 标准差 + const timeout = (avgLatency * 3) + this.calculateStdDev(recentLatencies); + return Math.min(Math.max(timeout, this.baseTimeout), this.maxTimeout); + } + + // 计算标准差 + calculateStdDev(values) { + const avg = values.reduce((a, b) => a + b, 0) / values.length; + const squareDiffs = values.map(value => Math.pow(value - avg, 2)); + const avgSquareDiff = squareDiffs.reduce((a, b) => a + b, 0) / squareDiffs.length; + return Math.sqrt(avgSquareDiff); + } + + // 记录请求延迟 + recordLatency(latency) { + this.latencyHistory.push(latency); + if (this.latencyHistory.length > 10) { + this.latencyHistory.shift(); + } + } + } + ``` + +### 资源管理 + +- 优化内存使用 +- 减少网络请求 +- 高效的状态同步 + +资源管理示例: +```javascript +// 内存优化 +class ResourceManager { + constructor() { + this.subscriptions = new Map(); + this.messageQueue = []; + this.maxQueueSize = 100; + this.processingInterval = 1000; // 1秒处理一次队列 + } + + // 批量处理消息队列 + processMessageQueue() { + setInterval(() => { + const batch = this.messageQueue.splice(0, 10); + if (batch.length > 0) { + this.sendBatchMessage(batch); + } + }, this.processingInterval); + } + + // 清理过期订阅 + cleanupSubscriptions() { + const now = Date.now(); + for (const [key, sub] of this.subscriptions) { + if (now - sub.lastActive > 5 * 60 * 1000) { // 5分钟无活动 + sub.unsubscribe(); + this.subscriptions.delete(key); + } + } + } + + // 状态同步优化 + async syncState() { + const lastSync = localStorage.getItem("lastSync"); + const now = Date.now(); + + // 增量同步 + if (lastSync) { + const changes = await this.getStateChangesSince(lastSync); + this.applyStateChanges(changes); + } else { + // 全量同步 + const state = await this.getFullState(); + this.setState(state); + } + + localStorage.setItem("lastSync", now.toString()); + } + + // 性能指标监控 + measurePerformance() { + return { + memoryUsage: performance.memory?.usedJSHeapSize || 0, + activeSubscriptions: this.subscriptions.size, + queueLength: this.messageQueue.length, + processingLatency: this.calculateProcessingLatency() + }; + } +} +``` + +## 最佳实践 + +### 开发建议 + +1. **错误处理**: + - 实现完整的错误处理逻辑 + - 提供用户友好的错误提示 + - 记录详细的错误日志 + + 错误处理示例: + ```javascript + connector.on("error", (error) => { + // 网络错误处理 + if (error.message.includes("network")) { + showNetworkError(); + attemptReconnect(); + return; + } + + // 会话过期处理 + if (error.message.includes("session")) { + clearSession(); + requestNewConnection(); + return; + } + + // 通用错误处理 + console.error("WalletConnect错误:", error); + showUserFriendlyError(error); + }); + + function showUserFriendlyError(error) { + const errorMessages = { + "user_rejected": "用户拒绝了请求", + "chain_not_supported": "当前钱包不支持该链", + "invalid_parameters": "无效的交易参数" + }; + + const message = errorMessages[error.code] || "连接出现问题,请稍后重试"; + displayError(message); + } + ``` + +2. **用户体验**: + - 显示连接状态和进度 + - 提供清晰的操作引导 + - 实现平滑的断开重连 + + 状态管理示例: + ```javascript + const ConnectionState = { + CONNECTING: "connecting", + CONNECTED: "connected", + DISCONNECTED: "disconnected", + ERROR: "error" + }; + + function updateConnectionUI(state) { + const stateMessages = { + [ConnectionState.CONNECTING]: "正在连接钱包...", + [ConnectionState.CONNECTED]: "钱包已连接", + [ConnectionState.DISCONNECTED]: "请连接钱包", + [ConnectionState.ERROR]: "连接出错" + }; + + // 更新UI状态 + updateStatusText(stateMessages[state]); + updateConnectButton(state); + updateLoadingIndicator(state === ConnectionState.CONNECTING); + } + ``` + +### 安全建议 + +- 定期更新 SDK 版本 +- 实施请求频率限制 +- 验证所有用户输入 +- 实现会话超时机制 + +安全实践示例: +```javascript +// 请求频率限制 +const rateLimiter = { + requests: {}, + limit: 5, + interval: 60000, // 1分钟 + + checkLimit(method) { + const now = Date.now(); + const recentRequests = this.requests[method] || []; + + // 清理过期请求 + this.requests[method] = recentRequests.filter( + time => now - time < this.interval + ); + + if (this.requests[method].length >= this.limit) { + throw new Error("请求过于频繁,请稍后再试"); + } + + this.requests[method].push(now); + } +}; + +// 输入验证 +function validateTransactionParams(params) { + const required = ["to", "value"]; + for (const field of required) { + if (!params[field]) { + throw new Error(`缺少必要参数: ${field}`); + } + } + + // 验证地址格式 + if (!/^0x[a-fA-F0-9]{40}$/.test(params.to)) { + throw new Error("无效的接收地址"); + } + + // 验证金额 + if (!/^[0-9]+$/.test(params.value)) { + throw new Error("无效的转账金额"); + } +} + +// 会话超时检查 +const SESSION_TIMEOUT = 30 * 60 * 1000; // 30分钟 +function checkSessionTimeout() { + const lastActivity = connector.lastActivityTime; + if (Date.now() - lastActivity > SESSION_TIMEOUT) { + connector.killSession(); + return false; + } + return true; +} +``` + +## 未来发展 + +### 技术路线 + +1. **协议升级**: + - 支持更多加密算法 + - 优化消息传输效率 + - 增强安全性能 + +2. **功能扩展**: + - 支持更多区块链网络 + - 提供更丰富的API + - 增加开发者工具 + +### 生态建设 + +- 扩大合作伙伴网络 +- 建立开发者社区 +- 提供更多学习资源 +- 举办技术研讨会 diff --git a/basic/78-wallet-connect/v2/multichain-example/.env.example b/basic/78-wallet-connect/v2/multichain-example/.env.example new file mode 100644 index 000000000..a1af2b6e5 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/.env.example @@ -0,0 +1,5 @@ +# WalletConnect Project ID from https://cloud.walletconnect.com +REACT_APP_PROJECT_ID=your_project_id_here + +# Infura API Key for multiple chain support +REACT_APP_INFURA_KEY=your_infura_key_here \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/README.md b/basic/78-wallet-connect/v2/multichain-example/README.md new file mode 100644 index 000000000..cd21824a2 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/README.md @@ -0,0 +1,89 @@ +# WalletConnect 多链交互示例 + +这个示例项目展示了如何使用 WalletConnect v2 实现多链交互功能,包括跨链资产查看、多链钱包连接和链间切换等功能。 + +## 功能特点 + +- 支持多个区块链网络的同时连接 +- 实时显示多链资产余额 +- 支持在不同链之间无缝切换 +- 展示跨链交易历史 +- 集成多链交易签名功能 + +## 技术栈 + +- React.js - 前端框架 +- wagmi - 以太坊 React Hooks 库 +- web3modal - Web3钱包连接组件 +- ethers.js - 区块链交互库 +- viem - 现代以太坊开发工具包 + +## 支持的网络 + +- Ethereum Mainnet +- Polygon +- Arbitrum +- Optimism +- Binance Smart Chain + +## 开始使用 + +1. **安装依赖** + ```bash + yarn install + ``` + +2. **环境配置** + - 复制 `.env.example` 到 `.env` + - 在 [WalletConnect Cloud](https://cloud.walletconnect.com) 注册并获取项目ID + - 在 [Infura](https://infura.io) 注册并获取API密钥 + - 更新 `.env` 文件中的配置 + +3. **启动开发服务器** + ```bash + yarn start + ``` + +## 项目结构 + +``` +├── src/ +│ ├── components/ # React组件 +│ ├── hooks/ # 自定义Hooks +│ ├── config/ # 配置文件 +│ ├── utils/ # 工具函数 +│ └── services/ # 区块链服务 +``` + +## 主要功能演示 + +1. **多链钱包连接** + - 支持同时连接多个区块链网络 + - 显示每个网络的连接状态 + - 提供网络切换功能 + +2. **资产管理** + - 查看多链资产余额 + - 显示代币价格和市值 + - 支持资产转账功能 + +3. **交易功能** + - 发送跨链交易 + - 查看交易历史 + - 交易状态追踪 + +## 测试 + +```bash +# 运行单元测试 +yarn test + +# 运行E2E测试 +yarn test:e2e +``` + +## 参考资源 + +- [WalletConnect v2 文档](https://docs.walletconnect.com/2.0) +- [Wagmi 文档](https://wagmi.sh) +- [Web3Modal 文档](https://docs.walletconnect.com/2.0/web3modal/about) diff --git a/basic/78-wallet-connect/v2/multichain-example/package.json b/basic/78-wallet-connect/v2/multichain-example/package.json new file mode 100644 index 000000000..b6633f258 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/package.json @@ -0,0 +1,40 @@ +{ + "name": "multichain-example", + "version": "1.0.0", + "private": true, + "dependencies": { + "@wagmi/core": "^1.4.5", + "@web3modal/ethereum": "^2.7.1", + "@web3modal/react": "^2.7.1", + "ethers": "^5.7.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1", + "viem": "^1.18.1", + "wagmi": "^1.4.5" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/public/index.html b/basic/78-wallet-connect/v2/multichain-example/public/index.html new file mode 100644 index 000000000..32eba196d --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/public/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + WalletConnect Multichain Example + + + +
+ + \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/src/App.js b/basic/78-wallet-connect/v2/multichain-example/src/App.js new file mode 100644 index 000000000..edcebe548 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/src/App.js @@ -0,0 +1,46 @@ +import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum' +import { Web3Modal } from '@web3modal/react' +import { configureChains, createConfig, WagmiConfig } from 'wagmi' +import { arbitrum, mainnet, polygon } from 'wagmi/chains' +import { useAccount, useDisconnect } from 'wagmi' + +// 配置链和providers +const chains = [arbitrum, mainnet, polygon] +const projectId = 'YOUR_PROJECT_ID' // 需要替换为你的WalletConnect项目ID + +const { publicClient } = configureChains(chains, [w3mProvider({ projectId })]) +const wagmiConfig = createConfig({ + autoConnect: true, + connectors: w3mConnectors({ projectId, chains }), + publicClient +}) +const ethereumClient = new EthereumClient(wagmiConfig, chains) + +function Profile() { + const { address, isConnected } = useAccount() + const { disconnect } = useDisconnect() + + if (isConnected) + return ( +
+ 已连接到: {address} + +
+ ) + return
请连接钱包
+} + +export default function App() { + return ( + <> + + + + + + + ) +} \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/src/components/MultiChainWallet.js b/basic/78-wallet-connect/v2/multichain-example/src/components/MultiChainWallet.js new file mode 100644 index 000000000..c40ece582 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/src/components/MultiChainWallet.js @@ -0,0 +1,124 @@ +import React, { useState, useEffect } from 'react'; +import { useAccount, useNetwork, useBalance, useConnect, useDisconnect } from 'wagmi'; +import { SUPPORTED_CHAINS } from '../utils/chainUtils'; +import { isTransactionSafe, isHighRiskTransaction } from '../utils/securityUtils'; + +const MultiChainWallet = () => { + const { address, isConnected } = useAccount(); + const { chain } = useNetwork(); + const { connect, connectors } = useConnect(); + const { disconnect } = useDisconnect(); + const [selectedChain, setSelectedChain] = useState(null); + + // 获取当前链上的余额 + const { data: balance } = useBalance({ + address, + chainId: selectedChain?.id, + watch: true, + }); + + // 监听链切换 + useEffect(() => { + if (chain) { + const currentChain = Object.values(SUPPORTED_CHAINS).find( + (c) => c.id === chain.id + ); + setSelectedChain(currentChain); + } + }, [chain]); + + // 处理连接钱包 + const handleConnect = async (connector) => { + try { + await connect({ connector }); + } catch (error) { + console.error('连接钱包失败:', error); + } + }; + + // 处理断开连接 + const handleDisconnect = async () => { + try { + await disconnect(); + setSelectedChain(null); + } catch (error) { + console.error('断开连接失败:', error); + } + }; + + return ( +
+

多链钱包

+ + {/* 连接状态 */} +
+ {isConnected ? ( +
+

已连接地址: {address}

+

当前网络: {selectedChain?.name || '未知网络'}

+

余额: {balance?.formatted || '0'} {balance?.symbol}

+ +
+ ) : ( +
+

请连接钱包

+
+ {connectors.map((connector) => ( + + ))} +
+
+ )} +
+ + {/* 支持的链列表 */} + {isConnected && ( +
+

支持的网络

+
+ {Object.values(SUPPORTED_CHAINS).map((supportedChain) => ( +
+

{supportedChain.name}

+

+ Chain ID: {supportedChain.id} +

+
+ ))} +
+
+ )} + + {/* 安全提示 */} + {isConnected && ( +
+

安全提示

+
    +
  • 请确保在进行跨链操作时仔细检查网络和地址
  • +
  • 大额转账请使用小额测试
  • +
  • 谨防钓鱼网站和恶意合约
  • +
+
+ )} +
+ ); +}; + +export default MultiChainWallet; \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/src/index.css b/basic/78-wallet-connect/v2/multichain-example/src/index.css new file mode 100644 index 000000000..2bac0df99 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/src/index.css @@ -0,0 +1,34 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +.container { + padding: 20px; + max-width: 800px; + margin: 0 auto; +} + +button { + padding: 10px 20px; + margin: 10px; + border: none; + border-radius: 5px; + background-color: #646cff; + color: white; + cursor: pointer; + font-size: 16px; +} + +button:hover { + background-color: #535bf2; +} \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/src/index.js b/basic/78-wallet-connect/v2/multichain-example/src/index.js new file mode 100644 index 000000000..1e6f816cf --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/src/tests/chainUtils.test.js b/basic/78-wallet-connect/v2/multichain-example/src/tests/chainUtils.test.js new file mode 100644 index 000000000..d55a71193 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/src/tests/chainUtils.test.js @@ -0,0 +1,95 @@ +import { ethers } from 'ethers'; +import { + isValidAddress, + formatBalance, + validateTransaction, + SUPPORTED_CHAINS +} from '../utils/chainUtils'; + +describe('Chain Utilities Tests', () => { + describe('isValidAddress', () => { + test('应该验证有效的以太坊地址', () => { + const validAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; + expect(isValidAddress(validAddress)).toBe(true); + }); + + test('应该拒绝无效的地址', () => { + const invalidAddress = '0xinvalid'; + expect(isValidAddress(invalidAddress)).toBe(false); + }); + + test('应该拒绝非字符串输入', () => { + expect(isValidAddress(123)).toBe(false); + expect(isValidAddress(null)).toBe(false); + expect(isValidAddress(undefined)).toBe(false); + }); + }); + + describe('formatBalance', () => { + test('应该正确格式化Wei到Ether', () => { + const weiAmount = ethers.utils.parseEther('1.0'); + expect(formatBalance(weiAmount)).toBe('1.0'); + }); + + test('应该处理零值', () => { + expect(formatBalance('0')).toBe('0'); + }); + + test('应该处理自定义精度', () => { + const amount = '1000000'; // 1.0 with 6 decimals + expect(formatBalance(amount, 6)).toBe('1.0'); + }); + }); + + describe('validateTransaction', () => { + test('应该验证有效的交易对象', () => { + const validTx = { + to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + value: '0x0', + data: '0x' + }; + expect(validateTransaction(validTx)).toBe(true); + }); + + test('应该拒绝无效的接收地址', () => { + const invalidTx = { + to: '0xinvalid', + value: '0x0' + }; + expect(validateTransaction(invalidTx)).toBe(false); + }); + + test('应该拒绝无效的交易值', () => { + const invalidTx = { + to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + value: 'invalid' + }; + expect(validateTransaction(invalidTx)).toBe(false); + }); + }); + + describe('SUPPORTED_CHAINS', () => { + test('应该包含所有支持的链配置', () => { + expect(SUPPORTED_CHAINS).toHaveProperty('ethereum'); + expect(SUPPORTED_CHAINS).toHaveProperty('polygon'); + expect(SUPPORTED_CHAINS).toHaveProperty('arbitrum'); + expect(SUPPORTED_CHAINS).toHaveProperty('optimism'); + expect(SUPPORTED_CHAINS).toHaveProperty('bsc'); + }); + + test('每个链配置应该包含必要的字段', () => { + Object.values(SUPPORTED_CHAINS).forEach(chain => { + expect(chain).toHaveProperty('id'); + expect(chain).toHaveProperty('name'); + expect(chain).toHaveProperty('rpcUrl'); + expect(chain).toHaveProperty('currency'); + }); + }); + + test('链ID应该是唯一的', () => { + const chainIds = Object.values(SUPPORTED_CHAINS).map(chain => chain.id); + const uniqueChainIds = new Set(chainIds); + expect(chainIds.length).toBe(uniqueChainIds.size); + }); + }); +}); \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/src/utils/chainUtils.js b/basic/78-wallet-connect/v2/multichain-example/src/utils/chainUtils.js new file mode 100644 index 000000000..388a9c9e1 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/src/utils/chainUtils.js @@ -0,0 +1,122 @@ +import { ethers } from 'ethers'; + +/** + * 链配置信息 + */ +export const SUPPORTED_CHAINS = { + ethereum: { + id: 1, + name: 'Ethereum', + rpcUrl: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`, + currency: 'ETH' + }, + polygon: { + id: 137, + name: 'Polygon', + rpcUrl: `https://polygon-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`, + currency: 'MATIC' + }, + arbitrum: { + id: 42161, + name: 'Arbitrum', + rpcUrl: `https://arbitrum-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`, + currency: 'ETH' + }, + optimism: { + id: 10, + name: 'Optimism', + rpcUrl: `https://optimism-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`, + currency: 'ETH' + }, + bsc: { + id: 56, + name: 'BSC', + rpcUrl: 'https://bsc-dataseed.binance.org', + currency: 'BNB' + } +}; + +/** + * 获取链的Provider实例 + * @param {string} chainId 链ID + * @returns {ethers.providers.JsonRpcProvider} + */ +export const getChainProvider = (chainId) => { + const chain = Object.values(SUPPORTED_CHAINS).find(c => c.id === chainId); + if (!chain) throw new Error('不支持的链'); + return new ethers.providers.JsonRpcProvider(chain.rpcUrl); +}; + +/** + * 检查地址格式是否有效 + * @param {string} address 待检查的地址 + * @returns {boolean} + */ +export const isValidAddress = (address) => { + try { + return ethers.utils.isAddress(address); + } catch { + return false; + } +}; + +/** + * 格式化余额显示 + * @param {string|number} balance Wei单位的余额 + * @param {number} decimals 代币精度 + * @returns {string} + */ +export const formatBalance = (balance, decimals = 18) => { + try { + return ethers.utils.formatUnits(balance, decimals); + } catch { + return '0'; + } +}; + +/** + * 检查交易参数 + * @param {object} tx 交易对象 + * @returns {boolean} + */ +export const validateTransaction = (tx) => { + if (!tx || typeof tx !== 'object') return false; + if (!isValidAddress(tx.to)) return false; + if (tx.value && !ethers.utils.isHexString(tx.value)) return false; + return true; +}; + +/** + * 获取链上代币余额 + * @param {string} tokenAddress 代币合约地址 + * @param {string} walletAddress 钱包地址 + * @param {ethers.providers.Provider} provider Provider实例 + * @returns {Promise} + */ +export const getTokenBalance = async (tokenAddress, walletAddress, provider) => { + try { + const abi = ['function balanceOf(address) view returns (uint256)']; + const contract = new ethers.Contract(tokenAddress, abi, provider); + const balance = await contract.balanceOf(walletAddress); + return balance.toString(); + } catch (error) { + console.error('获取代币余额失败:', error); + return '0'; + } +}; + +/** + * 估算交易gas费用 + * @param {object} tx 交易对象 + * @param {ethers.providers.Provider} provider Provider实例 + * @returns {Promise} + */ +export const estimateGas = async (tx, provider) => { + try { + const gasEstimate = await provider.estimateGas(tx); + return gasEstimate.toString(); + } catch (error) { + console.error('估算gas失败:', error); + throw error; + } +}; \ No newline at end of file diff --git a/basic/78-wallet-connect/v2/multichain-example/src/utils/securityUtils.js b/basic/78-wallet-connect/v2/multichain-example/src/utils/securityUtils.js new file mode 100644 index 000000000..e1c7130c0 --- /dev/null +++ b/basic/78-wallet-connect/v2/multichain-example/src/utils/securityUtils.js @@ -0,0 +1,147 @@ +/** + * 安全工具函数集合 + * 提供交易安全检查和数据加密等功能 + */ + +import { ethers } from 'ethers'; + +/** + * 交易金额安全检查 + * @param {string} amount 交易金额 + * @param {string} balance 账户余额 + * @returns {boolean} + */ +export const isTransactionSafe = (amount, balance) => { + try { + const amountBN = ethers.BigNumber.from(amount); + const balanceBN = ethers.BigNumber.from(balance); + // 检查交易金额是否超过余额 + if (amountBN.gt(balanceBN)) return false; + // 检查是否为零或负数 + if (amountBN.lte(0)) return false; + return true; + } catch { + return false; + } +}; + +/** + * 检查交易gas限制 + * @param {string} gasLimit 交易gas限制 + * @returns {boolean} + */ +export const isGasLimitSafe = (gasLimit) => { + try { + const limit = ethers.BigNumber.from(gasLimit); + // 设置合理的gas限制范围 + const MIN_GAS = ethers.BigNumber.from('21000'); // 基础转账gas + const MAX_GAS = ethers.BigNumber.from('500000'); // 最大允许gas + return limit.gte(MIN_GAS) && limit.lte(MAX_GAS); + } catch { + return false; + } +}; + +/** + * 检查交易nonce值 + * @param {number} nonce 交易nonce + * @param {number} currentNonce 当前账户nonce + * @returns {boolean} + */ +export const isNonceSafe = (nonce, currentNonce) => { + return nonce >= currentNonce; +}; + +/** + * 检查合约调用数据 + * @param {string} data 合约调用数据 + * @returns {boolean} + */ +export const isContractDataSafe = (data) => { + try { + // 检查数据是否为有效的十六进制 + if (!ethers.utils.isHexString(data)) return false; + // 检查数据长度是否合理 + const length = ethers.utils.hexDataLength(data); + return length > 0 && length <= 24576; // 最大24KB + } catch { + return false; + } +}; + +/** + * 检查交易接收地址是否为已知的危险地址 + * @param {string} address 交易接收地址 + * @returns {boolean} + */ +export const isAddressSafe = (address) => { + // 这里可以维护一个已知的危险地址列表 + const DANGEROUS_ADDRESSES = [ + // 示例危险地址 + '0x0000000000000000000000000000000000000000' + ]; + return !DANGEROUS_ADDRESSES.includes(address.toLowerCase()); +}; + +/** + * 生成交易消息哈希 + * @param {object} tx 交易对象 + * @returns {string} + */ +export const generateTransactionHash = (tx) => { + try { + const encoded = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'bytes'], + [tx.to, tx.value || '0', tx.data || '0x'] + ); + return ethers.utils.keccak256(encoded); + } catch (error) { + console.error('生成交易哈希失败:', error); + throw error; + } +}; + +/** + * 验证交易签名 + * @param {string} message 消息 + * @param {string} signature 签名 + * @param {string} address 签名者地址 + * @returns {boolean} + */ +export const verifySignature = (message, signature, address) => { + try { + const recoveredAddress = ethers.utils.verifyMessage(message, signature); + return recoveredAddress.toLowerCase() === address.toLowerCase(); + } catch { + return false; + } +}; + +/** + * 检查交易是否为高风险操作 + * @param {object} tx 交易对象 + * @returns {boolean} + */ +export const isHighRiskTransaction = (tx) => { + // 定义高风险阈值(以ETH为单位) + const HIGH_VALUE_THRESHOLD = ethers.utils.parseEther('10'); + + try { + const value = ethers.BigNumber.from(tx.value || '0'); + // 检查是否为大额交易 + if (value.gte(HIGH_VALUE_THRESHOLD)) return true; + // 检查是否调用敏感合约方法 + if (tx.data && tx.data.length > 2) { + const methodId = tx.data.slice(0, 10); + const SENSITIVE_METHODS = [ + '0x23b872dd', // transferFrom + '0x095ea7b3', // approve + '0x42842e0e' // safeTransferFrom + ]; + if (SENSITIVE_METHODS.includes(methodId)) return true; + } + return false; + } catch { + return true; // 如有异常,视为高风险 + } +}; \ No newline at end of file