first commit
This commit is contained in:
commit
2ff09f91b0
142
README.md
Normal file
142
README.md
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# BSC批量转账工具
|
||||||
|
|
||||||
|
一个现代化、简洁、美观的BSC链批量转账工具,支持BNB和BEP-20代币的批量转账。
|
||||||
|
|
||||||
|
## ✨ 界面特色
|
||||||
|
|
||||||
|
- 🎨 **现代化设计** - 采用BSC品牌金黄色系,简洁大方
|
||||||
|
- 🌈 **优雅配色** - 浅色主题,护眼舒适
|
||||||
|
- 📱 **响应式布局** - 完美适配桌面和移动设备
|
||||||
|
- ⚡ **流畅动画** - 微交互动画,提升用户体验
|
||||||
|
- 🎯 **清晰层次** - 卡片式设计,信息层次分明
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
✅ 支持BNB原生代币批量转账
|
||||||
|
✅ 支持BEP-20代币批量转账
|
||||||
|
✅ 文本输入批量数据
|
||||||
|
✅ 实时转账进度显示
|
||||||
|
✅ 转账历史记录查看
|
||||||
|
✅ 交易记录导出
|
||||||
|
✅ Gas费用预估
|
||||||
|
|
||||||
|
## 运行方法
|
||||||
|
|
||||||
|
### 方式1:直接打开
|
||||||
|
```bash
|
||||||
|
open index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式2:本地服务器(推荐)
|
||||||
|
```bash
|
||||||
|
# 使用Python
|
||||||
|
python3 -m http.server 8000
|
||||||
|
|
||||||
|
# 或使用Node.js
|
||||||
|
npx http-server -p 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
然后访问 `http://localhost:8000`
|
||||||
|
|
||||||
|
## 使用步骤
|
||||||
|
|
||||||
|
### 1. 连接钱包
|
||||||
|
- 点击"连接钱包"按钮
|
||||||
|
- 确保已安装MetaMask
|
||||||
|
- 自动切换到BSC主网
|
||||||
|
|
||||||
|
### 2. 选择转账类型
|
||||||
|
|
||||||
|
#### BNB转账
|
||||||
|
1. 选择"BNB (原生代币)"
|
||||||
|
2. Gas Limit默认:21000
|
||||||
|
|
||||||
|
#### BEP-20 Token转账
|
||||||
|
1. 选择"BEP-20 Token"
|
||||||
|
2. 输入Token合约地址
|
||||||
|
3. 点击"加载Token信息"
|
||||||
|
4. 确认Token信息正确
|
||||||
|
5. Gas Limit默认:65000
|
||||||
|
|
||||||
|
### 3. 准备转账数据
|
||||||
|
|
||||||
|
格式:`地址,金额,备注`(每行一条)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```
|
||||||
|
0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb,0.001,用户A
|
||||||
|
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,0.002,用户B
|
||||||
|
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,0.003,用户C
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:**
|
||||||
|
- BNB转账:金额单位为BNB(如0.1表示0.1个BNB)
|
||||||
|
- Token转账:金额为实际数量(如100表示100个token)
|
||||||
|
- 备注为可选项,可以留空
|
||||||
|
|
||||||
|
### 4. 输入转账数据
|
||||||
|
- 在文本框中输入或粘贴转账数据
|
||||||
|
- 每行一条记录,格式:地址,金额,备注
|
||||||
|
- 点击"解析数据"按钮
|
||||||
|
- 或使用快捷键 Ctrl/Cmd + Enter 快速解析
|
||||||
|
|
||||||
|
### 5. 配置参数
|
||||||
|
- **发送间隔**:每笔交易之间的延迟(毫秒)
|
||||||
|
- **Gas Limit**:每笔交易的Gas限制
|
||||||
|
|
||||||
|
### 6. 开始转账
|
||||||
|
1. 点击"开始批量转账"
|
||||||
|
2. 确认转账信息
|
||||||
|
3. 勾选确认框
|
||||||
|
4. 点击"开始执行"
|
||||||
|
|
||||||
|
### 7. 查看转账历史
|
||||||
|
1. 点击右上角"转账历史"按钮
|
||||||
|
2. 查看所有历史转账记录
|
||||||
|
3. 可按状态、类型筛选
|
||||||
|
4. 支持搜索地址或交易哈希
|
||||||
|
5. 可导出历史记录为CSV
|
||||||
|
6. 点击"查看详情"查看单笔转账详情
|
||||||
|
7. 历史记录保存在浏览器本地存储中
|
||||||
|
|
||||||
|
## 常见Token合约地址(BSC主网)
|
||||||
|
|
||||||
|
- **USDT**: 0x55d398326f99059fF775485246999027B3197955
|
||||||
|
- **USDC**: 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d
|
||||||
|
- **BUSD**: 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56
|
||||||
|
- **CAKE**: 0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
⚠️ **安全提示**
|
||||||
|
- 转账前请仔细核对收款地址
|
||||||
|
- 确保钱包中有足够的BNB支付Gas费
|
||||||
|
- Token转账需要足够的Token余额
|
||||||
|
- 建议先小额测试
|
||||||
|
|
||||||
|
⚠️ **Gas费用**
|
||||||
|
- BNB转账:约21000 Gas
|
||||||
|
- Token转账:约65000 Gas
|
||||||
|
- 实际费用取决于网络拥堵情况
|
||||||
|
|
||||||
|
⚠️ **网络要求**
|
||||||
|
- 必须连接BSC主网(Chain ID: 56)
|
||||||
|
- RPC节点:https://bsc-dataseed.binance.org/
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- HTML/CSS/JavaScript
|
||||||
|
- ethers.js v5.7.2
|
||||||
|
- MetaMask Web3 Provider
|
||||||
|
- BSC (Binance Smart Chain)
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
- `index.html` - 主页面
|
||||||
|
- `history.html` - 转账历史页面
|
||||||
|
- `app.js` - 应用逻辑
|
||||||
|
- `batchTransfer.js` - 转账核心功能
|
||||||
|
- `history.js` - 历史记录管理
|
||||||
|
- `utils.js` - 工具函数
|
||||||
|
- `style.css` - 样式文件
|
||||||
|
- `example.csv` - CSV示例文件
|
||||||
696
app.js
Normal file
696
app.js
Normal file
@ -0,0 +1,696 @@
|
|||||||
|
class BatchTransferApp {
|
||||||
|
constructor() {
|
||||||
|
this.transferData = [];
|
||||||
|
this.transferResults = [];
|
||||||
|
this.isTransferring = false;
|
||||||
|
this.isTokenTransfer = false;
|
||||||
|
this.tokenInfo = null;
|
||||||
|
|
||||||
|
this.initElements();
|
||||||
|
this.initEventListeners();
|
||||||
|
this.initUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
initElements() {
|
||||||
|
// 钱包相关
|
||||||
|
this.connectWalletBtn = document.getElementById('connectWallet');
|
||||||
|
this.disconnectWalletBtn = document.getElementById('disconnectWallet');
|
||||||
|
this.walletInfo = document.getElementById('connectedInfo');
|
||||||
|
this.accountAddress = document.getElementById('accountAddress');
|
||||||
|
this.walletBalance = document.getElementById('walletBalance');
|
||||||
|
this.networkInfo = document.getElementById('networkInfo');
|
||||||
|
|
||||||
|
// 转账类型
|
||||||
|
this.transferTypeRadios = document.querySelectorAll('input[name="transferType"]');
|
||||||
|
this.tokenConfig = document.getElementById('tokenConfig');
|
||||||
|
this.tokenAddress = document.getElementById('tokenAddress');
|
||||||
|
this.loadTokenInfoBtn = document.getElementById('loadTokenInfo');
|
||||||
|
this.tokenInfoElement = document.getElementById('tokenInfo');
|
||||||
|
this.tokenName = document.getElementById('tokenName');
|
||||||
|
this.tokenSymbol = document.getElementById('tokenSymbol');
|
||||||
|
this.tokenBalance = document.getElementById('tokenBalance');
|
||||||
|
|
||||||
|
// 上传相关
|
||||||
|
this.transferInput = document.getElementById('transferInput');
|
||||||
|
this.parseDataBtn = document.getElementById('parseData');
|
||||||
|
this.clearInputBtn = document.getElementById('clearInput');
|
||||||
|
this.dataPreview = document.getElementById('dataPreview');
|
||||||
|
this.dataTable = document.getElementById('dataTable');
|
||||||
|
this.dataCount = document.getElementById('dataCount');
|
||||||
|
this.clearDataBtn = document.getElementById('clearData');
|
||||||
|
|
||||||
|
// 汇总信息
|
||||||
|
this.totalCount = document.getElementById('totalCount');
|
||||||
|
this.totalAmount = document.getElementById('totalAmount');
|
||||||
|
this.amountUnit = document.getElementById('amountUnit');
|
||||||
|
this.estimatedGas = document.getElementById('estimatedGas');
|
||||||
|
this.totalCost = document.getElementById('totalCost');
|
||||||
|
|
||||||
|
// 高级设置
|
||||||
|
this.delayTime = document.getElementById('delayTime');
|
||||||
|
this.gasLimit = document.getElementById('gasLimit');
|
||||||
|
this.gasPrice = document.getElementById('gasPrice');
|
||||||
|
|
||||||
|
// 转账按钮
|
||||||
|
this.startTransferBtn = document.getElementById('startTransfer');
|
||||||
|
this.stopTransferBtn = document.getElementById('stopTransfer');
|
||||||
|
|
||||||
|
// 进度相关
|
||||||
|
this.progressSection = document.getElementById('progressSection');
|
||||||
|
this.progressFill = document.getElementById('progressFill');
|
||||||
|
this.progressInfo = document.getElementById('progressInfo');
|
||||||
|
this.progressPercent = document.getElementById('progressPercent');
|
||||||
|
this.successCount = document.getElementById('successCount');
|
||||||
|
this.failedCount = document.getElementById('failedCount');
|
||||||
|
this.pendingCount = document.getElementById('pendingCount');
|
||||||
|
|
||||||
|
// 日志相关
|
||||||
|
this.transferLog = document.getElementById('transferLog');
|
||||||
|
this.clearLogBtn = document.getElementById('clearLog');
|
||||||
|
|
||||||
|
// 模态框
|
||||||
|
this.confirmModal = document.getElementById('confirmModal');
|
||||||
|
this.confirmCount = document.getElementById('confirmCount');
|
||||||
|
this.confirmAmount = document.getElementById('confirmAmount');
|
||||||
|
this.confirmGas = document.getElementById('confirmGas');
|
||||||
|
this.confirmBalance = document.getElementById('confirmBalance');
|
||||||
|
this.confirmCheck = document.getElementById('confirmCheck');
|
||||||
|
this.cancelTransferBtn = document.getElementById('cancelTransfer');
|
||||||
|
this.executeTransferBtn = document.getElementById('executeTransfer');
|
||||||
|
this.modalCloseBtn = document.querySelector('.modal-close');
|
||||||
|
}
|
||||||
|
|
||||||
|
initEventListeners() {
|
||||||
|
// 钱包连接
|
||||||
|
this.connectWalletBtn.addEventListener('click', () => this.connectWallet());
|
||||||
|
this.disconnectWalletBtn.addEventListener('click', () => this.disconnectWallet());
|
||||||
|
|
||||||
|
// 转账类型切换
|
||||||
|
this.transferTypeRadios.forEach(radio => {
|
||||||
|
radio.addEventListener('change', (e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
this.onTransferTypeChange(e.target.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Token相关
|
||||||
|
this.loadTokenInfoBtn.addEventListener('click', () => this.loadTokenInfo());
|
||||||
|
this.tokenAddress.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.loadTokenInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据输入
|
||||||
|
this.parseDataBtn.addEventListener('click', () => this.parseTransferData());
|
||||||
|
this.clearInputBtn.addEventListener('click', () => this.clearInput());
|
||||||
|
this.transferInput.addEventListener('keydown', (e) => {
|
||||||
|
// Ctrl/Cmd + Enter 快捷键解析数据
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
this.parseTransferData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空数据
|
||||||
|
if (this.clearDataBtn) {
|
||||||
|
this.clearDataBtn.addEventListener('click', () => this.clearData());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转账控制
|
||||||
|
this.startTransferBtn.addEventListener('click', () => this.showConfirmModal());
|
||||||
|
this.stopTransferBtn.addEventListener('click', () => this.stopTransfer());
|
||||||
|
|
||||||
|
// 日志控制
|
||||||
|
this.clearLogBtn.addEventListener('click', () => this.clearLog());
|
||||||
|
|
||||||
|
// 配置变化
|
||||||
|
this.delayTime.addEventListener('change', () => this.updateSummary());
|
||||||
|
this.gasLimit.addEventListener('change', () => this.updateSummary());
|
||||||
|
|
||||||
|
// 模态框
|
||||||
|
this.confirmCheck.addEventListener('change', (e) => {
|
||||||
|
this.executeTransferBtn.disabled = !e.target.checked;
|
||||||
|
});
|
||||||
|
this.cancelTransferBtn.addEventListener('click', () => this.hideModal());
|
||||||
|
this.executeTransferBtn.addEventListener('click', () => this.executeTransfer());
|
||||||
|
this.modalCloseBtn.addEventListener('click', () => this.hideModal());
|
||||||
|
|
||||||
|
// 点击模态框外部关闭
|
||||||
|
this.confirmModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === this.confirmModal) {
|
||||||
|
this.hideModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initUI() {
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
// 尝试恢复钱包连接
|
||||||
|
this.restoreWalletConnection();
|
||||||
|
|
||||||
|
// 定期更新Gas价格
|
||||||
|
setInterval(() => {
|
||||||
|
if (window.BatchTransfer.provider) {
|
||||||
|
this.updateGasPrice();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreWalletConnection() {
|
||||||
|
try {
|
||||||
|
// 检查是否有保存的钱包连接状态
|
||||||
|
const wasConnected = localStorage.getItem('walletConnected');
|
||||||
|
if (wasConnected === 'true' && window.ethereum) {
|
||||||
|
// 尝试自动连接
|
||||||
|
const accounts = await window.ethereum.request({
|
||||||
|
method: 'eth_accounts'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (accounts && accounts.length > 0) {
|
||||||
|
// 钱包已授权,自动连接
|
||||||
|
await this.connectWallet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('无法恢复钱包连接:', error);
|
||||||
|
localStorage.removeItem('walletConnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectWallet() {
|
||||||
|
try {
|
||||||
|
this.addLog('正在连接钱包...', 'info');
|
||||||
|
|
||||||
|
const account = await window.BatchTransfer.connectWallet();
|
||||||
|
|
||||||
|
// 保存连接状态到localStorage
|
||||||
|
localStorage.setItem('walletConnected', 'true');
|
||||||
|
|
||||||
|
// 更新UI
|
||||||
|
this.connectWalletBtn.style.display = 'none';
|
||||||
|
this.walletInfo.style.display = 'flex';
|
||||||
|
this.accountAddress.textContent = window.TransferUtils.formatAddress(account);
|
||||||
|
|
||||||
|
// 获取余额和网络信息
|
||||||
|
await this.updateWalletInfo();
|
||||||
|
|
||||||
|
this.addLog('钱包连接成功', 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.addLog(`连接失败: ${error.message}`, 'error');
|
||||||
|
alert(`连接钱包失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnectWallet() {
|
||||||
|
await window.BatchTransfer.disconnectWallet();
|
||||||
|
|
||||||
|
// 清除保存的连接状态
|
||||||
|
localStorage.removeItem('walletConnected');
|
||||||
|
|
||||||
|
// 重置UI
|
||||||
|
this.connectWalletBtn.style.display = 'block';
|
||||||
|
this.walletInfo.style.display = 'none';
|
||||||
|
|
||||||
|
// 检查networkInfo元素是否存在(因为我们已经从页面移除了它)
|
||||||
|
if (this.networkInfo) {
|
||||||
|
this.networkInfo.textContent = '未连接';
|
||||||
|
}
|
||||||
|
this.gasPrice.value = '0';
|
||||||
|
|
||||||
|
this.addLog('已断开钱包连接', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWalletInfo() {
|
||||||
|
try {
|
||||||
|
// 更新余额
|
||||||
|
const balance = await window.BatchTransfer.getWalletBalance();
|
||||||
|
this.walletBalance.textContent = window.TransferUtils.formatNumber(balance, 4);
|
||||||
|
|
||||||
|
// 更新网络信息(如果元素存在)
|
||||||
|
if (this.networkInfo) {
|
||||||
|
const network = await window.BatchTransfer.getNetworkInfo();
|
||||||
|
this.networkInfo.textContent = `${network.name} (Chain ID: ${network.chainId})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新Gas价格
|
||||||
|
await this.updateGasPrice();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新钱包信息失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGasPrice() {
|
||||||
|
try {
|
||||||
|
const gasPrice = await window.BatchTransfer.getGasPrice();
|
||||||
|
this.gasPrice.value = window.TransferUtils.formatNumber(parseFloat(gasPrice), 2);
|
||||||
|
this.updateSummary();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取Gas价格失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTransferTypeChange(type) {
|
||||||
|
this.isTokenTransfer = (type === 'token');
|
||||||
|
|
||||||
|
if (this.isTokenTransfer) {
|
||||||
|
// 显示Token配置
|
||||||
|
this.tokenConfig.style.display = 'block';
|
||||||
|
// 更新Gas Limit默认值
|
||||||
|
this.gasLimit.value = '65000';
|
||||||
|
this.amountUnit.textContent = 'Token';
|
||||||
|
} else {
|
||||||
|
// 隐藏Token配置
|
||||||
|
this.tokenConfig.style.display = 'none';
|
||||||
|
this.tokenInfoElement.style.display = 'none';
|
||||||
|
// 恢复BNB转账的Gas Limit
|
||||||
|
this.gasLimit.value = '21000';
|
||||||
|
this.amountUnit.textContent = 'BNB';
|
||||||
|
// 清空token信息
|
||||||
|
this.tokenInfo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTokenInfo() {
|
||||||
|
const address = this.tokenAddress.value.trim();
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
alert('请输入Token合约地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.BatchTransfer.provider) {
|
||||||
|
alert('请先连接钱包');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.addLog('正在加载Token信息...', 'info');
|
||||||
|
this.loadTokenInfoBtn.disabled = true;
|
||||||
|
this.loadTokenInfoBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 加载中...';
|
||||||
|
|
||||||
|
// 加载Token合约
|
||||||
|
const tokenInfo = await window.BatchTransfer.loadTokenContract(address);
|
||||||
|
|
||||||
|
// 获取Token余额
|
||||||
|
const balance = await window.BatchTransfer.getTokenBalance();
|
||||||
|
|
||||||
|
// 更新UI
|
||||||
|
this.tokenName.textContent = tokenInfo.name;
|
||||||
|
this.tokenSymbol.textContent = tokenInfo.symbol;
|
||||||
|
this.tokenBalance.textContent = window.TransferUtils.formatNumber(parseFloat(balance), 4);
|
||||||
|
|
||||||
|
this.tokenInfoElement.style.display = 'block';
|
||||||
|
this.tokenInfo = tokenInfo;
|
||||||
|
|
||||||
|
// 更新金额单位
|
||||||
|
this.amountUnit.textContent = tokenInfo.symbol;
|
||||||
|
|
||||||
|
this.addLog(`Token加载成功: ${tokenInfo.name} (${tokenInfo.symbol})`, 'success');
|
||||||
|
|
||||||
|
// 更新汇总信息
|
||||||
|
this.updateSummary();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.addLog(`加载Token失败: ${error.message}`, 'error');
|
||||||
|
alert(`加载Token失败: ${error.message}`);
|
||||||
|
this.tokenInfoElement.style.display = 'none';
|
||||||
|
} finally {
|
||||||
|
this.loadTokenInfoBtn.disabled = false;
|
||||||
|
this.loadTokenInfoBtn.innerHTML = '<i class="fas fa-sync"></i> 加载';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCSVFile(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const csvText = e.target.result;
|
||||||
|
this.transferData = window.TransferUtils.parseCSV(csvText);
|
||||||
|
|
||||||
|
// 验证数据
|
||||||
|
const errors = window.BatchTransfer.validateTransferData(this.transferData);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
this.addLog('CSV文件解析错误:', 'error');
|
||||||
|
errors.forEach(error => this.addLog(error, 'error'));
|
||||||
|
alert(`发现${errors.length}个错误,请检查CSV文件格式`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示数据预览
|
||||||
|
this.dataPreview.style.display = 'block';
|
||||||
|
|
||||||
|
// 更新表格显示
|
||||||
|
this.updateDataTable();
|
||||||
|
|
||||||
|
// 更新汇总信息
|
||||||
|
this.updateSummary();
|
||||||
|
|
||||||
|
this.addLog(`成功加载 ${this.transferData.length} 条转账记录`, 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.addLog(`解析CSV文件失败: ${error.message}`, 'error');
|
||||||
|
alert('解析CSV文件失败,请检查文件格式');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
this.addLog('读取文件失败', 'error');
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTransferData() {
|
||||||
|
const inputText = this.transferInput.value.trim();
|
||||||
|
|
||||||
|
if (!inputText) {
|
||||||
|
alert('请输入转账数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用相同的CSV解析函数
|
||||||
|
this.transferData = window.TransferUtils.parseCSV(inputText);
|
||||||
|
|
||||||
|
// 验证数据
|
||||||
|
const errors = window.BatchTransfer.validateTransferData(this.transferData);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
this.addLog('数据解析错误:', 'error');
|
||||||
|
errors.forEach(error => this.addLog(error, 'error'));
|
||||||
|
alert(`发现${errors.length}个错误,请检查数据格式\n\n格式:地址,金额,备注\n示例:0x123...,0.1,用户A`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示数据预览
|
||||||
|
this.dataPreview.style.display = 'block';
|
||||||
|
|
||||||
|
// 更新表格显示
|
||||||
|
this.updateDataTable();
|
||||||
|
|
||||||
|
// 更新汇总信息
|
||||||
|
this.updateSummary();
|
||||||
|
|
||||||
|
this.addLog(`成功解析 ${this.transferData.length} 条转账记录`, 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.addLog(`解析数据失败: ${error.message}`, 'error');
|
||||||
|
alert('解析数据失败,请检查数据格式\n\n格式:地址,金额,备注\n示例:0x123...,0.1,用户A');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInput() {
|
||||||
|
this.transferInput.value = '';
|
||||||
|
this.clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDataTable() {
|
||||||
|
const tbody = this.dataTable.querySelector('tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
this.transferData.forEach((item, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td title="${item.address}">${window.TransferUtils.formatAddress(item.address)}</td>
|
||||||
|
<td>${item.amount}</td>
|
||||||
|
<td>${item.note || '-'}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataCount.textContent = this.transferData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearData() {
|
||||||
|
this.transferData = [];
|
||||||
|
this.dataPreview.style.display = 'none';
|
||||||
|
this.updateSummary();
|
||||||
|
this.addLog('已清空转账数据', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSummary() {
|
||||||
|
if (this.transferData.length === 0) {
|
||||||
|
this.totalCount.textContent = '0';
|
||||||
|
this.totalAmount.textContent = '0';
|
||||||
|
this.estimatedGas.textContent = '0';
|
||||||
|
this.totalCost.textContent = '0';
|
||||||
|
this.startTransferBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查Token转账模式下是否已加载Token信息
|
||||||
|
if (this.isTokenTransfer && !this.tokenInfo) {
|
||||||
|
this.startTransferBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
const totalAmount = this.transferData.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
this.totalCount.textContent = this.transferData.length;
|
||||||
|
this.totalAmount.textContent = window.TransferUtils.formatNumber(totalAmount, 4);
|
||||||
|
|
||||||
|
// 计算Gas费用
|
||||||
|
if (window.BatchTransfer.provider && this.gasPrice.value) {
|
||||||
|
const gasPrice = parseFloat(this.gasPrice.value);
|
||||||
|
const gasLimit = parseInt(this.gasLimit.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cost = await window.BatchTransfer.calculateTotalCost(
|
||||||
|
this.transferData,
|
||||||
|
gasLimit,
|
||||||
|
gasPrice,
|
||||||
|
this.isTokenTransfer
|
||||||
|
);
|
||||||
|
|
||||||
|
this.estimatedGas.textContent = window.TransferUtils.formatNumber(cost.totalGasCost, 4);
|
||||||
|
this.totalCost.textContent = window.TransferUtils.formatNumber(cost.totalCost, 4);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('计算Gas费用失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用开始按钮
|
||||||
|
this.startTransferBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showConfirmModal() {
|
||||||
|
if (!window.BatchTransfer.provider) {
|
||||||
|
alert('请先连接钱包');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.transferData.length === 0) {
|
||||||
|
alert('请先上传转账数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新确认信息
|
||||||
|
this.confirmCount.textContent = this.transferData.length;
|
||||||
|
this.confirmAmount.textContent = this.totalAmount.textContent + ' ' + this.amountUnit.textContent;
|
||||||
|
this.confirmGas.textContent = this.estimatedGas.textContent;
|
||||||
|
this.confirmBalance.textContent = this.walletBalance.textContent;
|
||||||
|
|
||||||
|
// 重置确认勾选
|
||||||
|
this.confirmCheck.checked = false;
|
||||||
|
this.executeTransferBtn.disabled = true;
|
||||||
|
|
||||||
|
// 显示模态框
|
||||||
|
this.confirmModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
hideModal() {
|
||||||
|
this.confirmModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeTransfer() {
|
||||||
|
this.hideModal();
|
||||||
|
|
||||||
|
if (!window.BatchTransfer.provider) {
|
||||||
|
this.addLog('请先连接钱包', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token转账时检查Token余额
|
||||||
|
if (this.isTokenTransfer) {
|
||||||
|
if (!this.tokenInfo) {
|
||||||
|
this.addLog('请先加载Token信息', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenBalance = parseFloat(this.tokenBalance.textContent);
|
||||||
|
const requiredAmount = parseFloat(this.totalAmount.textContent);
|
||||||
|
|
||||||
|
if (tokenBalance < requiredAmount) {
|
||||||
|
this.addLog(`Token余额不足:需要 ${requiredAmount} ${this.tokenInfo.symbol},当前余额 ${tokenBalance} ${this.tokenInfo.symbol}`, 'error');
|
||||||
|
alert(`Token余额不足,请确保钱包中有足够的${this.tokenInfo.symbol}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查BNB余额是否足够支付Gas费
|
||||||
|
const bnbBalance = parseFloat(this.walletBalance.textContent);
|
||||||
|
const requiredGas = parseFloat(this.estimatedGas.textContent);
|
||||||
|
|
||||||
|
if (bnbBalance < requiredGas) {
|
||||||
|
this.addLog(`BNB余额不足以支付Gas费:需要 ${requiredGas} BNB,当前余额 ${bnbBalance} BNB`, 'error');
|
||||||
|
alert('BNB余额不足以支付Gas费用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// BNB转账时检查余额
|
||||||
|
const balance = parseFloat(this.walletBalance.textContent);
|
||||||
|
const required = parseFloat(this.totalCost.textContent);
|
||||||
|
|
||||||
|
if (balance < required) {
|
||||||
|
this.addLog(`余额不足:需要 ${required} BNB,当前余额 ${balance} BNB`, 'error');
|
||||||
|
alert('余额不足,请确保钱包中有足够的BNB支付转账费用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示进度区域
|
||||||
|
this.progressSection.style.display = 'block';
|
||||||
|
this.progressSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
|
||||||
|
// 开始转账
|
||||||
|
this.isTransferring = true;
|
||||||
|
this.startTransferBtn.style.display = 'none';
|
||||||
|
this.stopTransferBtn.style.display = 'block';
|
||||||
|
|
||||||
|
// 重置进度
|
||||||
|
this.resetProgress();
|
||||||
|
|
||||||
|
const transferType = this.isTokenTransfer ? `Token (${this.tokenInfo.symbol})` : 'BNB';
|
||||||
|
this.addLog(`开始批量转账 ${transferType}...`, 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
delay: parseInt(this.delayTime.value),
|
||||||
|
gasLimit: parseInt(this.gasLimit.value),
|
||||||
|
isTokenTransfer: this.isTokenTransfer,
|
||||||
|
onProgress: (progress) => this.updateProgress(progress),
|
||||||
|
onComplete: (results) => this.onTransferComplete(results),
|
||||||
|
onError: (error) => this.onTransferError(error)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.transferResults = await window.BatchTransfer.executeBatchTransfer(
|
||||||
|
this.transferData,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.addLog(`转账过程出错: ${error.message}`, 'error');
|
||||||
|
this.stopTransfer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTransfer() {
|
||||||
|
window.BatchTransfer.stopTransfer();
|
||||||
|
this.isTransferring = false;
|
||||||
|
this.startTransferBtn.style.display = 'block';
|
||||||
|
this.stopTransferBtn.style.display = 'none';
|
||||||
|
this.addLog('转账已停止', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
resetProgress() {
|
||||||
|
this.progressFill.style.width = '0%';
|
||||||
|
this.progressInfo.textContent = '0/0';
|
||||||
|
this.progressPercent.textContent = '0%';
|
||||||
|
this.successCount.textContent = '0';
|
||||||
|
this.failedCount.textContent = '0';
|
||||||
|
this.pendingCount.textContent = this.transferData.length.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(progress) {
|
||||||
|
const percent = Math.round((progress.current / progress.total) * 100);
|
||||||
|
|
||||||
|
this.progressFill.style.width = `${percent}%`;
|
||||||
|
this.progressInfo.textContent = `${progress.current}/${progress.total}`;
|
||||||
|
this.progressPercent.textContent = `${percent}%`;
|
||||||
|
this.successCount.textContent = progress.success.toString();
|
||||||
|
this.failedCount.textContent = progress.failed.toString();
|
||||||
|
this.pendingCount.textContent = (progress.total - progress.current).toString();
|
||||||
|
|
||||||
|
// 更新余额(每5笔更新一次)
|
||||||
|
if (progress.current % 5 === 0) {
|
||||||
|
this.updateWalletInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTransferComplete(results) {
|
||||||
|
this.isTransferring = false;
|
||||||
|
this.startTransferBtn.style.display = 'block';
|
||||||
|
this.stopTransferBtn.style.display = 'none';
|
||||||
|
|
||||||
|
const successCount = results.filter(r => r.success).length;
|
||||||
|
const failedCount = results.filter(r => !r.success).length;
|
||||||
|
|
||||||
|
this.addLog(`转账完成!成功: ${successCount},失败: ${failedCount}`, 'success');
|
||||||
|
|
||||||
|
// 更新钱包余额
|
||||||
|
this.updateWalletInfo();
|
||||||
|
|
||||||
|
// 询问是否导出结果
|
||||||
|
if (confirm('转账完成,是否导出详细结果?')) {
|
||||||
|
window.TransferUtils.exportToCSV(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTransferError(error) {
|
||||||
|
this.addLog(`转账过程中出错: ${error.message}`, 'error');
|
||||||
|
this.isTransferring = false;
|
||||||
|
this.startTransferBtn.style.display = 'block';
|
||||||
|
this.stopTransferBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog(message, type = 'info') {
|
||||||
|
const log = window.TransferUtils.generateLogMessage(type, message);
|
||||||
|
|
||||||
|
// 移除空状态
|
||||||
|
const emptyState = this.transferLog.querySelector('.log-empty');
|
||||||
|
if (emptyState) {
|
||||||
|
emptyState.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到日志顶部
|
||||||
|
this.transferLog.insertAdjacentHTML('afterbegin', log.html);
|
||||||
|
|
||||||
|
// 限制日志数量
|
||||||
|
const logItems = this.transferLog.querySelectorAll('.log-item');
|
||||||
|
if (logItems.length > 100) {
|
||||||
|
logItems[logItems.length - 1].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLog() {
|
||||||
|
this.transferLog.innerHTML = `
|
||||||
|
<div class="log-empty">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<p>暂无日志</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
// 初始化UI状态
|
||||||
|
this.startTransferBtn.disabled = true;
|
||||||
|
this.stopTransferBtn.style.display = 'none';
|
||||||
|
this.progressSection.style.display = 'none';
|
||||||
|
this.dataPreview.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.app = new BatchTransferApp();
|
||||||
|
});
|
||||||
435
batchTransfer.js
Normal file
435
batchTransfer.js
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
class BSCBatchTransfer {
|
||||||
|
constructor() {
|
||||||
|
this.provider = null;
|
||||||
|
this.signer = null;
|
||||||
|
this.account = '';
|
||||||
|
this.isTransferring = false;
|
||||||
|
this.currentBatch = 0;
|
||||||
|
this.totalBatches = 0;
|
||||||
|
|
||||||
|
// BEP-20 Token ABI (与ERC-20兼容)
|
||||||
|
this.BEP20_ABI = [
|
||||||
|
"function name() view returns (string)",
|
||||||
|
"function symbol() view returns (string)",
|
||||||
|
"function decimals() view returns (uint8)",
|
||||||
|
"function totalSupply() view returns (uint256)",
|
||||||
|
"function balanceOf(address) view returns (uint256)",
|
||||||
|
"function transfer(address to, uint256 amount) returns (bool)",
|
||||||
|
"function allowance(address owner, address spender) view returns (uint256)",
|
||||||
|
"function approve(address spender, uint256 amount) returns (bool)"
|
||||||
|
];
|
||||||
|
|
||||||
|
this.tokenContract = null;
|
||||||
|
this.tokenInfo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForEthereum(maxWait = 3000) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (window.ethereum) {
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let waited = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (window.ethereum) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve(true);
|
||||||
|
} else if (waited >= maxWait) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve(false);
|
||||||
|
} else {
|
||||||
|
waited += 100;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectWallet() {
|
||||||
|
try {
|
||||||
|
console.log('开始连接钱包...');
|
||||||
|
|
||||||
|
// 等待MetaMask注入,最多等待3秒
|
||||||
|
const ethereumAvailable = await this.waitForEthereum();
|
||||||
|
|
||||||
|
if (!ethereumAvailable || !window.ethereum) {
|
||||||
|
console.error('未检测到MetaMask');
|
||||||
|
throw new Error('请安装MetaMask或支持BSC的钱包插件,刷新页面后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('检测到钱包:', window.ethereum.isMetaMask ? 'MetaMask' : '其他钱包');
|
||||||
|
|
||||||
|
// 请求连接钱包
|
||||||
|
const accounts = await window.ethereum.request({
|
||||||
|
method: 'eth_requestAccounts'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.account = accounts[0];
|
||||||
|
|
||||||
|
// 切换到BSC网络
|
||||||
|
await this.switchToBSCNetwork();
|
||||||
|
|
||||||
|
// 创建Provider和Signer
|
||||||
|
this.provider = new ethers.providers.Web3Provider(window.ethereum);
|
||||||
|
this.signer = this.provider.getSigner();
|
||||||
|
|
||||||
|
// 监听账户变化
|
||||||
|
window.ethereum.on('accountsChanged', (accounts) => {
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
this.disconnectWallet();
|
||||||
|
} else {
|
||||||
|
this.account = accounts[0];
|
||||||
|
this.updateWalletInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听网络变化
|
||||||
|
window.ethereum.on('chainChanged', (chainId) => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.account;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('连接钱包失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnectWallet() {
|
||||||
|
this.provider = null;
|
||||||
|
this.signer = null;
|
||||||
|
this.account = '';
|
||||||
|
this.isTransferring = false;
|
||||||
|
|
||||||
|
// 移除事件监听
|
||||||
|
if (window.ethereum) {
|
||||||
|
window.ethereum.removeAllListeners('accountsChanged');
|
||||||
|
window.ethereum.removeAllListeners('chainChanged');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchToBSCNetwork() {
|
||||||
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
|
||||||
|
// BSC主网chainId: 0x38 (56)
|
||||||
|
if (chainId !== '0x38') {
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: '0x38' }]
|
||||||
|
});
|
||||||
|
} catch (switchError) {
|
||||||
|
// 如果网络不存在,添加网络
|
||||||
|
if (switchError.code === 4902) {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [{
|
||||||
|
chainId: '0x38',
|
||||||
|
chainName: 'Binance Smart Chain',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'BNB',
|
||||||
|
symbol: 'BNB',
|
||||||
|
decimals: 18
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://bsc-dataseed.binance.org/'],
|
||||||
|
blockExplorerUrls: ['https://bscscan.com/']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw switchError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWalletBalance() {
|
||||||
|
if (!this.provider) throw new Error('请先连接钱包');
|
||||||
|
|
||||||
|
const balance = await this.provider.getBalance(this.account);
|
||||||
|
return ethers.utils.formatEther(balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTokenContract(tokenAddress) {
|
||||||
|
if (!this.provider) throw new Error('请先连接钱包');
|
||||||
|
|
||||||
|
if (!ethers.utils.isAddress(tokenAddress)) {
|
||||||
|
throw new Error('无效的Token合约地址');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.tokenContract = new ethers.Contract(tokenAddress, this.BEP20_ABI, this.signer);
|
||||||
|
|
||||||
|
// 获取Token信息
|
||||||
|
const [name, symbol, decimals] = await Promise.all([
|
||||||
|
this.tokenContract.name(),
|
||||||
|
this.tokenContract.symbol(),
|
||||||
|
this.tokenContract.decimals()
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.tokenInfo = {
|
||||||
|
address: tokenAddress,
|
||||||
|
name,
|
||||||
|
symbol,
|
||||||
|
decimals
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.tokenInfo;
|
||||||
|
} catch (error) {
|
||||||
|
this.tokenContract = null;
|
||||||
|
this.tokenInfo = null;
|
||||||
|
throw new Error('无法加载Token信息,请检查合约地址是否正确');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTokenBalance() {
|
||||||
|
if (!this.tokenContract || !this.tokenInfo) {
|
||||||
|
throw new Error('请先加载Token信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
const balance = await this.tokenContract.balanceOf(this.account);
|
||||||
|
return ethers.utils.formatUnits(balance, this.tokenInfo.decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGasPrice() {
|
||||||
|
if (!this.provider) throw new Error('请先连接钱包');
|
||||||
|
|
||||||
|
const gasPrice = await this.provider.getGasPrice();
|
||||||
|
return ethers.utils.formatUnits(gasPrice, 'gwei');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNetworkInfo() {
|
||||||
|
if (!this.provider) throw new Error('请先连接钱包');
|
||||||
|
|
||||||
|
const network = await this.provider.getNetwork();
|
||||||
|
return {
|
||||||
|
name: network.name,
|
||||||
|
chainId: network.chainId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateTransferData(transferData) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < transferData.length; i++) {
|
||||||
|
const item = transferData[i];
|
||||||
|
|
||||||
|
// 验证地址
|
||||||
|
if (!ethers.utils.isAddress(item.address)) {
|
||||||
|
errors.push(`第${i + 1}行:无效的地址 ${item.address}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证金额
|
||||||
|
try {
|
||||||
|
const amount = parseFloat(item.amount);
|
||||||
|
if (isNaN(amount) || amount <= 0) {
|
||||||
|
errors.push(`第${i + 1}行:无效的金额 ${item.amount}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`第${i + 1}行:无效的金额格式`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateTotalCost(transferData, gasLimit, gasPrice, isTokenTransfer = false) {
|
||||||
|
if (!this.provider) throw new Error('请先连接钱包');
|
||||||
|
|
||||||
|
const totalAmount = transferData.reduce((sum, item) => {
|
||||||
|
return sum + parseFloat(item.amount);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// 计算Gas费用
|
||||||
|
const gasCostPerTx = ethers.utils.parseUnits(gasPrice.toString(), 'gwei')
|
||||||
|
.mul(gasLimit)
|
||||||
|
.mul(transferData.length);
|
||||||
|
|
||||||
|
const totalGasCost = parseFloat(ethers.utils.formatEther(gasCostPerTx));
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalAmount,
|
||||||
|
totalGasCost,
|
||||||
|
// Token转账时,总成本只包含Gas费用(因为token金额不是BNB)
|
||||||
|
totalCost: isTokenTransfer ? totalGasCost : (totalAmount + totalGasCost)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeBatchTransfer(transferData, options) {
|
||||||
|
if (!this.signer) throw new Error('请先连接钱包');
|
||||||
|
|
||||||
|
const {
|
||||||
|
delay = 1000,
|
||||||
|
gasLimit = 21000,
|
||||||
|
isTokenTransfer = false,
|
||||||
|
onProgress,
|
||||||
|
onComplete,
|
||||||
|
onError
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// 如果是Token转账,检查是否已加载Token合约
|
||||||
|
if (isTokenTransfer && !this.tokenContract) {
|
||||||
|
throw new Error('请先加载Token合约信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isTransferring = true;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < transferData.length; i++) {
|
||||||
|
if (!this.isTransferring) {
|
||||||
|
throw new Error('转账被用户停止');
|
||||||
|
}
|
||||||
|
|
||||||
|
const transfer = transferData[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前Gas价格
|
||||||
|
const currentGasPrice = await this.provider.getGasPrice();
|
||||||
|
|
||||||
|
let txResponse;
|
||||||
|
|
||||||
|
if (isTokenTransfer) {
|
||||||
|
// Token转账
|
||||||
|
const amount = ethers.utils.parseUnits(
|
||||||
|
transfer.amount.toString(),
|
||||||
|
this.tokenInfo.decimals
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发送Token转账交易
|
||||||
|
txResponse = await this.tokenContract.transfer(
|
||||||
|
transfer.address,
|
||||||
|
amount,
|
||||||
|
{
|
||||||
|
gasLimit: gasLimit,
|
||||||
|
gasPrice: currentGasPrice
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// BNB转账
|
||||||
|
const tx = {
|
||||||
|
to: transfer.address,
|
||||||
|
value: ethers.utils.parseEther(transfer.amount.toString()),
|
||||||
|
gasLimit: gasLimit,
|
||||||
|
gasPrice: currentGasPrice
|
||||||
|
};
|
||||||
|
|
||||||
|
txResponse = await this.signer.sendTransaction(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待交易确认
|
||||||
|
const receipt = await txResponse.wait();
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
index: i,
|
||||||
|
success: true,
|
||||||
|
hash: txResponse.hash,
|
||||||
|
to: transfer.address,
|
||||||
|
amount: transfer.amount,
|
||||||
|
receipt: receipt
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存到历史记录
|
||||||
|
this.saveTransferToHistory(transfer, results[results.length - 1], isTokenTransfer);
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
current: i + 1,
|
||||||
|
total: transferData.length,
|
||||||
|
success: results.filter(r => r.success).length,
|
||||||
|
failed: results.filter(r => !r.success).length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
results.push({
|
||||||
|
index: i,
|
||||||
|
success: false,
|
||||||
|
to: transfer.address,
|
||||||
|
amount: transfer.amount,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存到历史记录(包括失败的)
|
||||||
|
this.saveTransferToHistory(transfer, results[results.length - 1], isTokenTransfer);
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
current: i + 1,
|
||||||
|
total: transferData.length,
|
||||||
|
success: results.filter(r => r.success).length,
|
||||||
|
failed: results.filter(r => !r.success).length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟处理下一笔交易
|
||||||
|
if (i < transferData.length - 1) {
|
||||||
|
await this.delay(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成回调
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.isTransferring = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTransfer() {
|
||||||
|
this.isTransferring = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存转账记录到历史
|
||||||
|
saveTransferToHistory(transferData, result, isTokenTransfer = false) {
|
||||||
|
try {
|
||||||
|
const storageKey = 'bsc_transfer_history';
|
||||||
|
const history = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: isTokenTransfer ? 'token' : 'bnb',
|
||||||
|
address: result.to,
|
||||||
|
amount: result.amount,
|
||||||
|
status: result.success ? 'success' : 'failed',
|
||||||
|
txHash: result.hash || null,
|
||||||
|
note: transferData.note || '',
|
||||||
|
gasCost: result.receipt ? ethers.utils.formatEther(
|
||||||
|
result.receipt.gasUsed.mul(result.receipt.effectiveGasPrice)
|
||||||
|
) : null,
|
||||||
|
tokenAddress: isTokenTransfer && this.tokenInfo ? this.tokenInfo.address : null,
|
||||||
|
tokenSymbol: isTokenTransfer && this.tokenInfo ? this.tokenInfo.symbol : null,
|
||||||
|
error: result.error || null
|
||||||
|
};
|
||||||
|
|
||||||
|
history.push(record);
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(history));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存转账历史失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWalletInfo() {
|
||||||
|
// 这个方法需要在UI层实现,这里只提供接口
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
window.BatchTransfer = new BSCBatchTransfer();
|
||||||
367
developer.html
Normal file
367
developer.html
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>开发者介绍 | 区块链龙哥 出品</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
.developer-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto var(--spacing-lg);
|
||||||
|
font-size: 48px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-bio {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.8;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card h2 i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
margin-right: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-action {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-action:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-card h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-card h2 i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link i {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-right: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="developer-container">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<a href="index.html" class="back-button">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
返回主页
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- 个人资料卡片 -->
|
||||||
|
<div class="profile-card">
|
||||||
|
<div class="profile-avatar">
|
||||||
|
<i class="fas fa-dragon"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="profile-name">区块链龙哥</h1>
|
||||||
|
<p class="profile-title">区块链开发者 · Web3工具开发</p>
|
||||||
|
<p class="profile-bio">
|
||||||
|
专注于区块链技术开发和Web3工具创建,致力于为用户提供简单、安全、高效的区块链应用解决方案。
|
||||||
|
擅长智能合约开发、DApp开发以及区块链工具开发。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 联系方式卡片 -->
|
||||||
|
<div class="contact-card">
|
||||||
|
<h2><i class="fas fa-address-book"></i> 联系方式</h2>
|
||||||
|
<div class="contact-list">
|
||||||
|
<!-- 微信 -->
|
||||||
|
<div class="contact-item">
|
||||||
|
<div class="contact-icon">
|
||||||
|
<i class="fab fa-weixin"></i>
|
||||||
|
</div>
|
||||||
|
<div class="contact-info">
|
||||||
|
<div class="contact-label">微信号</div>
|
||||||
|
<div class="contact-value">aaronlzhou</div>
|
||||||
|
</div>
|
||||||
|
<button class="contact-action" onclick="copyToClipboard('aaronlzhou', '微信号')">
|
||||||
|
<i class="fas fa-copy"></i> 复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮箱 -->
|
||||||
|
<div class="contact-item">
|
||||||
|
<div class="contact-icon">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</div>
|
||||||
|
<div class="contact-info">
|
||||||
|
<div class="contact-label">电子邮箱</div>
|
||||||
|
<div class="contact-value">aaron.l.zhou@gmail.com</div>
|
||||||
|
</div>
|
||||||
|
<button class="contact-action" onclick="copyToClipboard('aaron.l.zhou@gmail.com', '邮箱地址')">
|
||||||
|
<i class="fas fa-copy"></i> 复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部 -->
|
||||||
|
<div class="footer">
|
||||||
|
<p><i class="fas fa-code"></i> 使用区块链技术构建更好的未来</p>
|
||||||
|
<p style="margin-top: 8px; color: var(--text-tertiary);">© 2026 区块链龙哥 · All Rights Reserved</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 复制到剪贴板功能
|
||||||
|
function copyToClipboard(text, label) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// 创建提示消息
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.textContent = `${label}已复制到剪贴板`;
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: var(--success-color);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 9999;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// 3秒后移除
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}).catch(err => {
|
||||||
|
alert('复制失败,请手动复制');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加动画样式
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
example.csv
Normal file
3
example.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb,0.001,测试地址1
|
||||||
|
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,0.002,测试地址2
|
||||||
|
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,0.003,测试地址3
|
||||||
|
202
history.html
Normal file
202
history.html
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>转账历史 | 区块链龙哥 出品</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/ethers@5.7.2/dist/ethers.umd.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1><i class="fas fa-history"></i> 转账历史</h1>
|
||||||
|
<p class="subtitle">查看所有批量转账记录</p>
|
||||||
|
</div>
|
||||||
|
<div class="wallet-section">
|
||||||
|
<a href="index.html" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> 返回主页
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主内容 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 筛选和操作栏 -->
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="history-toolbar">
|
||||||
|
<div class="filter-group">
|
||||||
|
<div class="filter-item">
|
||||||
|
<label>状态筛选</label>
|
||||||
|
<select id="statusFilter" class="form-control">
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="success">成功</option>
|
||||||
|
<option value="failed">失败</option>
|
||||||
|
<option value="pending">待确认</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-item">
|
||||||
|
<label>类型筛选</label>
|
||||||
|
<select id="typeFilter" class="form-control">
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="bnb">BNB</option>
|
||||||
|
<option value="token">Token</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-item">
|
||||||
|
<label>搜索</label>
|
||||||
|
<input type="text" id="searchInput" class="form-control" placeholder="搜索地址或交易哈希...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-group">
|
||||||
|
<button id="exportHistory" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-download"></i> 导出CSV
|
||||||
|
</button>
|
||||||
|
<button id="clearHistory" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash"></i> 清空历史
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon success">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-label">成功转账</span>
|
||||||
|
<span class="stat-value" id="totalSuccess">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon failed">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-label">失败转账</span>
|
||||||
|
<span class="stat-value" id="totalFailed">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon pending">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-label">待确认</span>
|
||||||
|
<span class="stat-value" id="totalPending">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon total">
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-label">总转账数</span>
|
||||||
|
<span class="stat-value" id="totalTransfers">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 历史记录表格 -->
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="history-header">
|
||||||
|
<h3><i class="fas fa-list"></i> 转账记录</h3>
|
||||||
|
<span class="record-count">共 <strong id="recordCount">0</strong> 条记录</span>
|
||||||
|
</div>
|
||||||
|
<div class="history-table-container">
|
||||||
|
<table class="history-table" id="historyTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>时间</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>收款地址</th>
|
||||||
|
<th>金额</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>交易哈希</th>
|
||||||
|
<th>备注</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="historyTableBody">
|
||||||
|
<tr class="empty-state">
|
||||||
|
<td colspan="8">
|
||||||
|
<div class="empty-content">
|
||||||
|
<i class="fas fa-inbox"></i>
|
||||||
|
<p>暂无转账记录</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 底部信息 -->
|
||||||
|
<footer class="footer">
|
||||||
|
<p><i class="fas fa-info-circle"></i> 历史记录保存在浏览器本地存储中</p>
|
||||||
|
<p style="margin-top: 12px;"><a href="developer.html" style="color: var(--primary-color); text-decoration: none;"><i class="fas fa-user-circle"></i> 关于开发者</a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情模态框 -->
|
||||||
|
<div id="detailModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fas fa-info-circle"></i> 转账详情</h3>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">时间</span>
|
||||||
|
<span class="value" id="detailTime">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">类型</span>
|
||||||
|
<span class="value" id="detailType">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">收款地址</span>
|
||||||
|
<span class="value" id="detailAddress">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">金额</span>
|
||||||
|
<span class="value" id="detailAmount">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">状态</span>
|
||||||
|
<span class="value" id="detailStatus">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">交易哈希</span>
|
||||||
|
<span class="value" id="detailTxHash">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">备注</span>
|
||||||
|
<span class="value" id="detailNote">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">Gas费用</span>
|
||||||
|
<span class="value" id="detailGas">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="closeDetail" class="btn btn-secondary">关闭</button>
|
||||||
|
<a id="viewOnBscscan" href="#" target="_blank" class="btn btn-primary">
|
||||||
|
<i class="fas fa-external-link-alt"></i> 在BSCScan查看
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="utils.js"></script>
|
||||||
|
<script src="history.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
322
history.js
Normal file
322
history.js
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
// 转账历史管理
|
||||||
|
class TransferHistory {
|
||||||
|
constructor() {
|
||||||
|
this.storageKey = 'bsc_transfer_history';
|
||||||
|
this.history = this.loadHistory();
|
||||||
|
this.filteredHistory = [...this.history];
|
||||||
|
this.initElements();
|
||||||
|
this.bindEvents();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
initElements() {
|
||||||
|
// 筛选器
|
||||||
|
this.statusFilter = document.getElementById('statusFilter');
|
||||||
|
this.typeFilter = document.getElementById('typeFilter');
|
||||||
|
this.searchInput = document.getElementById('searchInput');
|
||||||
|
|
||||||
|
// 操作按钮
|
||||||
|
this.exportBtn = document.getElementById('exportHistory');
|
||||||
|
this.clearBtn = document.getElementById('clearHistory');
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
this.totalSuccess = document.getElementById('totalSuccess');
|
||||||
|
this.totalFailed = document.getElementById('totalFailed');
|
||||||
|
this.totalPending = document.getElementById('totalPending');
|
||||||
|
this.totalTransfers = document.getElementById('totalTransfers');
|
||||||
|
this.recordCount = document.getElementById('recordCount');
|
||||||
|
|
||||||
|
// 表格
|
||||||
|
this.tableBody = document.getElementById('historyTableBody');
|
||||||
|
|
||||||
|
// 模态框
|
||||||
|
this.detailModal = document.getElementById('detailModal');
|
||||||
|
this.closeDetailBtn = document.getElementById('closeDetail');
|
||||||
|
this.viewOnBscscanBtn = document.getElementById('viewOnBscscan');
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// 筛选器事件
|
||||||
|
this.statusFilter.addEventListener('change', () => this.applyFilters());
|
||||||
|
this.typeFilter.addEventListener('change', () => this.applyFilters());
|
||||||
|
this.searchInput.addEventListener('input', () => this.applyFilters());
|
||||||
|
|
||||||
|
// 操作按钮
|
||||||
|
this.exportBtn.addEventListener('click', () => this.exportToCSV());
|
||||||
|
this.clearBtn.addEventListener('click', () => this.clearHistory());
|
||||||
|
|
||||||
|
// 模态框
|
||||||
|
this.closeDetailBtn.addEventListener('click', () => this.closeModal());
|
||||||
|
this.detailModal.querySelector('.modal-close').addEventListener('click', () => this.closeModal());
|
||||||
|
this.detailModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === this.detailModal) this.closeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从localStorage加载历史记录
|
||||||
|
loadHistory() {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(this.storageKey);
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载历史记录失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存历史记录到localStorage
|
||||||
|
saveHistory() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(this.storageKey, JSON.stringify(this.history));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存历史记录失败:', error);
|
||||||
|
showNotification('保存失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用筛选
|
||||||
|
applyFilters() {
|
||||||
|
const status = this.statusFilter.value;
|
||||||
|
const type = this.typeFilter.value;
|
||||||
|
const search = this.searchInput.value.toLowerCase().trim();
|
||||||
|
|
||||||
|
this.filteredHistory = this.history.filter(record => {
|
||||||
|
// 状态筛选
|
||||||
|
if (status !== 'all' && record.status !== status) return false;
|
||||||
|
|
||||||
|
// 类型筛选
|
||||||
|
if (type !== 'all' && record.type !== type) return false;
|
||||||
|
|
||||||
|
// 搜索筛选
|
||||||
|
if (search) {
|
||||||
|
const searchableText = [
|
||||||
|
record.address,
|
||||||
|
record.txHash,
|
||||||
|
record.note
|
||||||
|
].join(' ').toLowerCase();
|
||||||
|
|
||||||
|
if (!searchableText.includes(search)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染页面
|
||||||
|
render() {
|
||||||
|
this.renderStats();
|
||||||
|
this.renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染统计数据
|
||||||
|
renderStats() {
|
||||||
|
const stats = {
|
||||||
|
success: 0,
|
||||||
|
failed: 0,
|
||||||
|
pending: 0,
|
||||||
|
total: this.history.length
|
||||||
|
};
|
||||||
|
|
||||||
|
this.history.forEach(record => {
|
||||||
|
if (record.status === 'success') stats.success++;
|
||||||
|
else if (record.status === 'failed') stats.failed++;
|
||||||
|
else if (record.status === 'pending') stats.pending++;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.totalSuccess.textContent = stats.success;
|
||||||
|
this.totalFailed.textContent = stats.failed;
|
||||||
|
this.totalPending.textContent = stats.pending;
|
||||||
|
this.totalTransfers.textContent = stats.total;
|
||||||
|
this.recordCount.textContent = this.filteredHistory.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染表格
|
||||||
|
renderTable() {
|
||||||
|
if (this.filteredHistory.length === 0) {
|
||||||
|
this.tableBody.innerHTML = `
|
||||||
|
<tr class="empty-state">
|
||||||
|
<td colspan="8">
|
||||||
|
<div class="empty-content">
|
||||||
|
<i class="fas fa-inbox"></i>
|
||||||
|
<p>${this.history.length === 0 ? '暂无转账记录' : '没有符合条件的记录'}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间倒序排列
|
||||||
|
const sortedHistory = [...this.filteredHistory].sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
|
||||||
|
this.tableBody.innerHTML = sortedHistory.map(record => `
|
||||||
|
<tr>
|
||||||
|
<td>${this.formatTime(record.timestamp)}</td>
|
||||||
|
<td>
|
||||||
|
<span class="type-badge ${record.type}">
|
||||||
|
${record.type === 'bnb' ? 'BNB' : record.tokenSymbol || 'Token'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="address-cell" title="${record.address}">
|
||||||
|
${this.formatAddress(record.address)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>${record.amount}</strong> ${record.type === 'bnb' ? 'BNB' : record.tokenSymbol || ''}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge ${record.status}">
|
||||||
|
${this.getStatusText(record.status)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${record.txHash ? `
|
||||||
|
<a href="https://bscscan.com/tx/${record.txHash}" target="_blank" class="tx-link" title="${record.txHash}">
|
||||||
|
${this.formatTxHash(record.txHash)}
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
</a>
|
||||||
|
` : '-'}
|
||||||
|
</td>
|
||||||
|
<td>${record.note || '-'}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn-icon" onclick="history.showDetail('${record.id}')" title="查看详情">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示详情模态框
|
||||||
|
showDetail(recordId) {
|
||||||
|
const record = this.history.find(r => r.id === recordId);
|
||||||
|
if (!record) return;
|
||||||
|
|
||||||
|
document.getElementById('detailTime').textContent = this.formatTime(record.timestamp, true);
|
||||||
|
document.getElementById('detailType').textContent = record.type === 'bnb' ? 'BNB' : `${record.tokenSymbol || 'Token'} (${record.tokenAddress})`;
|
||||||
|
document.getElementById('detailAddress').textContent = record.address;
|
||||||
|
document.getElementById('detailAmount').textContent = `${record.amount} ${record.type === 'bnb' ? 'BNB' : record.tokenSymbol || ''}`;
|
||||||
|
document.getElementById('detailStatus').innerHTML = `<span class="status-badge ${record.status}">${this.getStatusText(record.status)}</span>`;
|
||||||
|
document.getElementById('detailTxHash').textContent = record.txHash || '-';
|
||||||
|
document.getElementById('detailNote').textContent = record.note || '-';
|
||||||
|
document.getElementById('detailGas').textContent = record.gasCost ? `${record.gasCost} BNB` : '-';
|
||||||
|
|
||||||
|
if (record.txHash) {
|
||||||
|
this.viewOnBscscanBtn.href = `https://bscscan.com/tx/${record.txHash}`;
|
||||||
|
this.viewOnBscscanBtn.style.display = 'inline-flex';
|
||||||
|
} else {
|
||||||
|
this.viewOnBscscanBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.detailModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
closeModal() {
|
||||||
|
this.detailModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出为CSV
|
||||||
|
exportToCSV() {
|
||||||
|
if (this.filteredHistory.length === 0) {
|
||||||
|
showNotification('没有可导出的记录', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = ['时间', '类型', '收款地址', '金额', '状态', '交易哈希', '备注', 'Gas费用'];
|
||||||
|
const rows = this.filteredHistory.map(record => [
|
||||||
|
this.formatTime(record.timestamp, true),
|
||||||
|
record.type === 'bnb' ? 'BNB' : record.tokenSymbol || 'Token',
|
||||||
|
record.address,
|
||||||
|
`${record.amount} ${record.type === 'bnb' ? 'BNB' : record.tokenSymbol || ''}`,
|
||||||
|
this.getStatusText(record.status),
|
||||||
|
record.txHash || '',
|
||||||
|
record.note || '',
|
||||||
|
record.gasCost || ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
const csvContent = [headers, ...rows]
|
||||||
|
.map(row => row.map(cell => `"${cell}"`).join(','))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `transfer_history_${Date.now()}.csv`;
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
showNotification('导出成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空历史记录
|
||||||
|
clearHistory() {
|
||||||
|
if (this.history.length === 0) {
|
||||||
|
showNotification('没有可清空的记录', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('确定要清空所有历史记录吗?此操作不可恢复!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.history = [];
|
||||||
|
this.filteredHistory = [];
|
||||||
|
this.saveHistory();
|
||||||
|
this.render();
|
||||||
|
showNotification('历史记录已清空', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
formatTime(timestamp, full = false) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
if (full) {
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化地址
|
||||||
|
formatAddress(address) {
|
||||||
|
if (!address) return '-';
|
||||||
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化交易哈希
|
||||||
|
formatTxHash(txHash) {
|
||||||
|
if (!txHash) return '-';
|
||||||
|
return `${txHash.slice(0, 8)}...${txHash.slice(-6)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
getStatusText(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'success': '成功',
|
||||||
|
'failed': '失败',
|
||||||
|
'pending': '待确认'
|
||||||
|
};
|
||||||
|
return statusMap[status] || status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
let historyManager;
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
historyManager = new TransferHistory();
|
||||||
|
// 将实例暴露到全局,供onclick使用
|
||||||
|
window.history = historyManager;
|
||||||
|
});
|
||||||
308
index.html
Normal file
308
index.html
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BSC批量转账工具 | 区块链龙哥 出品</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/ethers@5.7.2/dist/ethers.umd.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1><i class="fas fa-paper-plane"></i> BSC批量转账</h1>
|
||||||
|
<p class="subtitle">简单、快速、安全的批量转账工具</p>
|
||||||
|
<div style="margin-top: 12px; display: flex; gap: 8px;">
|
||||||
|
<a href="developer.html" class="btn btn-secondary" style="font-size: 13px; padding: 6px 12px; text-decoration: none;">
|
||||||
|
<i class="fas fa-user-circle"></i> 联系开发者
|
||||||
|
</a>
|
||||||
|
<a href="history.html" class="btn btn-secondary" style="font-size: 13px; padding: 6px 12px; text-decoration: none;">
|
||||||
|
<i class="fas fa-history"></i> 转账历史
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wallet-section">
|
||||||
|
<button id="connectWallet" class="btn btn-primary">
|
||||||
|
<i class="fas fa-wallet"></i> 连接钱包
|
||||||
|
</button>
|
||||||
|
<div id="connectedInfo" class="wallet-connected" style="display: none;">
|
||||||
|
<div class="wallet-details">
|
||||||
|
<span class="wallet-address" id="accountAddress"></span>
|
||||||
|
<span class="wallet-balance"><i class="fas fa-coins"></i> <span id="walletBalance">0</span> BNB</span>
|
||||||
|
</div>
|
||||||
|
<button id="disconnectWallet" class="btn-icon" title="断开连接">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主内容 - 步骤式布局 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 步骤1: 选择转账类型 -->
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="step-header">
|
||||||
|
<span class="step-number">1</span>
|
||||||
|
<h2>选择转账类型</h2>
|
||||||
|
</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="transfer-type-selector">
|
||||||
|
<label class="type-option" data-type="bnb">
|
||||||
|
<input type="radio" name="transferType" value="bnb" checked>
|
||||||
|
<div class="type-card">
|
||||||
|
<i class="fas fa-coins"></i>
|
||||||
|
<h3>BNB</h3>
|
||||||
|
<p>原生代币转账</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="type-option" data-type="token">
|
||||||
|
<input type="radio" name="transferType" value="token">
|
||||||
|
<div class="type-card">
|
||||||
|
<i class="fas fa-certificate"></i>
|
||||||
|
<h3>BEP-20 Token</h3>
|
||||||
|
<p>代币合约转账</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token配置区域 -->
|
||||||
|
<div id="tokenConfig" class="token-config" style="display: none;">
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Token合约地址</label>
|
||||||
|
<div class="input-with-button">
|
||||||
|
<input type="text" id="tokenAddress" class="form-control" placeholder="0x...">
|
||||||
|
<button id="loadTokenInfo" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-sync"></i> 加载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tokenInfo" class="token-info" style="display: none;">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">名称</span>
|
||||||
|
<span class="value" id="tokenName">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">符号</span>
|
||||||
|
<span class="value" id="tokenSymbol">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">余额</span>
|
||||||
|
<span class="value" id="tokenBalance">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤2: 输入转账数据 -->
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="step-header">
|
||||||
|
<span class="step-number">2</span>
|
||||||
|
<h2>输入转账数据</h2>
|
||||||
|
</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="input-group">
|
||||||
|
<label>
|
||||||
|
转账列表
|
||||||
|
<span class="format-hint-inline">格式:地址,金额,备注(每行一条)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="transferInput"
|
||||||
|
class="transfer-textarea"
|
||||||
|
placeholder="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb,0.001,测试地址1 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,0.002,测试地址2 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,0.003,测试地址3"
|
||||||
|
rows="8"
|
||||||
|
></textarea>
|
||||||
|
<div class="input-actions">
|
||||||
|
<button id="parseData" class="btn btn-primary">
|
||||||
|
<i class="fas fa-check"></i> 解析数据
|
||||||
|
</button>
|
||||||
|
<button id="clearInput" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-eraser"></i> 清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据预览 -->
|
||||||
|
<div id="dataPreview" class="data-preview" style="display: none;">
|
||||||
|
<div class="preview-header">
|
||||||
|
<h3>数据预览 <span id="dataCount" class="count-badge">0</span></h3>
|
||||||
|
<button id="clearData" class="btn-text">
|
||||||
|
<i class="fas fa-times"></i> 清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-table">
|
||||||
|
<table id="dataTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>序号</th>
|
||||||
|
<th>收款地址</th>
|
||||||
|
<th>金额</th>
|
||||||
|
<th>备注</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤3: 确认并转账 -->
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="step-header">
|
||||||
|
<span class="step-number">3</span>
|
||||||
|
<h2>确认并开始转账</h2>
|
||||||
|
</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<!-- 转账汇总 -->
|
||||||
|
<div class="transfer-summary">
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>转账数量</span>
|
||||||
|
<strong><span id="totalCount">0</span> 笔</strong>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>转账总额</span>
|
||||||
|
<strong><span id="totalAmount">0</span> <span id="amountUnit">BNB</span></strong>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>预计Gas费</span>
|
||||||
|
<strong><span id="estimatedGas">0</span> BNB</strong>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row total">
|
||||||
|
<span>总计费用</span>
|
||||||
|
<strong><span id="totalCost">0</span> BNB</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 高级设置 -->
|
||||||
|
<details class="advanced-settings">
|
||||||
|
<summary>
|
||||||
|
<i class="fas fa-cog"></i> 高级设置
|
||||||
|
</summary>
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>发送间隔 (毫秒)</label>
|
||||||
|
<input type="number" id="delayTime" class="form-control" value="1000" min="500" max="5000">
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>Gas Limit</label>
|
||||||
|
<input type="number" id="gasLimit" class="form-control" value="21000" min="21000" max="100000">
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>Gas Price</label>
|
||||||
|
<div class="input-with-unit">
|
||||||
|
<input type="text" id="gasPrice" class="form-control" readonly value="0">
|
||||||
|
<span>Gwei</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button id="startTransfer" class="btn btn-primary btn-large" disabled>
|
||||||
|
<i class="fas fa-rocket"></i> 开始批量转账
|
||||||
|
</button>
|
||||||
|
<button id="stopTransfer" class="btn btn-danger btn-large" style="display: none;">
|
||||||
|
<i class="fas fa-stop"></i> 停止转账
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 转账进度 (只在转账时显示) -->
|
||||||
|
<div id="progressSection" class="progress-section" style="display: none;">
|
||||||
|
<div class="progress-card">
|
||||||
|
<h3><i class="fas fa-tasks"></i> 转账进度</h3>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progressFill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<span id="progressInfo">0/0</span>
|
||||||
|
<span id="progressPercent">0%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-stats">
|
||||||
|
<div class="stat success">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span>成功 <strong id="successCount">0</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="stat failed">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
<span>失败 <strong id="failedCount">0</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="stat pending">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
<span>等待 <strong id="pendingCount">0</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 实时日志 -->
|
||||||
|
<div class="log-card">
|
||||||
|
<div class="log-header">
|
||||||
|
<h3><i class="fas fa-list"></i> 转账日志</h3>
|
||||||
|
<div class="log-actions">
|
||||||
|
<button id="clearLog" class="btn-text">
|
||||||
|
<i class="fas fa-trash"></i> 清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="log-content" id="transferLog">
|
||||||
|
<div class="log-empty">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<p>暂无日志</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 底部信息 -->
|
||||||
|
<footer class="footer">
|
||||||
|
<p style="color: var(--text-tertiary); font-size: 13px;">© 2026 区块链龙哥 · All Rights Reserved</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认模态框 -->
|
||||||
|
<div id="confirmModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fas fa-exclamation-triangle"></i> 确认转账</h3>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>请仔细核对以下信息:</p>
|
||||||
|
<div class="confirm-details">
|
||||||
|
<p><span>转账数量</span><strong id="confirmCount">0</strong> 笔</p>
|
||||||
|
<p><span>转账金额</span><strong id="confirmAmount">0</strong></p>
|
||||||
|
<p><span>Gas费用</span><strong id="confirmGas">0</strong> BNB</p>
|
||||||
|
<p><span>账户余额</span><strong id="confirmBalance">0</strong> BNB</p>
|
||||||
|
</div>
|
||||||
|
<div class="warning">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
转账一旦开始将无法撤销,请确保信息正确!
|
||||||
|
</div>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="confirmCheck">
|
||||||
|
<span>我已核对所有信息并确认转账</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="cancelTransfer" class="btn btn-secondary">取消</button>
|
||||||
|
<button id="executeTransfer" class="btn btn-primary" disabled>确认转账</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="utils.js"></script>
|
||||||
|
<script src="batchTransfer.js"></script>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
117
utils.js
Normal file
117
utils.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
class TransferUtils {
|
||||||
|
static parseCSV(csvText) {
|
||||||
|
const lines = csvText.split('\n');
|
||||||
|
const transfers = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
// 支持逗号分隔或制表符分隔
|
||||||
|
const parts = line.includes('\t') ? line.split('\t') : line.split(',');
|
||||||
|
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const address = parts[0].trim();
|
||||||
|
const amount = parts[1].trim();
|
||||||
|
const note = parts[2] ? parts[2].trim() : '';
|
||||||
|
|
||||||
|
// 基本验证
|
||||||
|
if (address && amount) {
|
||||||
|
transfers.push({
|
||||||
|
address,
|
||||||
|
amount: parseFloat(amount),
|
||||||
|
note
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transfers;
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatAddress(address) {
|
||||||
|
if (!address || address.length < 10) return address;
|
||||||
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatNumber(num, decimals = 4) {
|
||||||
|
if (isNaN(num)) return '0';
|
||||||
|
return parseFloat(num.toFixed(decimals)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateLogMessage(type, message) {
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
const types = {
|
||||||
|
success: { icon: '✅', class: 'success' },
|
||||||
|
error: { icon: '❌', class: 'error' },
|
||||||
|
info: { icon: 'ℹ️', class: 'info' },
|
||||||
|
warning: { icon: '⚠️', class: 'warning' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeInfo = types[type] || types.info;
|
||||||
|
|
||||||
|
return {
|
||||||
|
time,
|
||||||
|
type: typeInfo.class,
|
||||||
|
message: `${typeInfo.icon} ${message}`,
|
||||||
|
html: `
|
||||||
|
<div class="log-item ${typeInfo.class}">
|
||||||
|
<div class="log-time">${time}</div>
|
||||||
|
<div class="log-message">${typeInfo.icon} ${message}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static exportToCSV(data, filename = 'transfer-results.csv') {
|
||||||
|
const headers = ['序号', '收款地址', '金额(BNB)', '状态', '交易哈希', '备注'];
|
||||||
|
const rows = data.map(item => [
|
||||||
|
item.index + 1,
|
||||||
|
item.to,
|
||||||
|
item.amount,
|
||||||
|
item.success ? '成功' : '失败',
|
||||||
|
item.hash || '',
|
||||||
|
item.error || ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
const csvContent = [headers, ...rows]
|
||||||
|
.map(row => row.map(cell => `"${cell}"`).join(','))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
|
||||||
|
if (navigator.msSaveBlob) {
|
||||||
|
navigator.msSaveBlob(blob, filename);
|
||||||
|
} else {
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = filename;
|
||||||
|
link.style.display = 'none';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static validateEthereumAddress(address) {
|
||||||
|
try {
|
||||||
|
return ethers.utils.isAddress(address);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async checkBalanceSufficient(provider, address, requiredAmount) {
|
||||||
|
try {
|
||||||
|
const balance = await provider.getBalance(address);
|
||||||
|
const required = ethers.utils.parseEther(requiredAmount.toString());
|
||||||
|
return balance.gte(required);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查余额失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出工具类
|
||||||
|
window.TransferUtils = TransferUtils;
|
||||||
Loading…
Reference in New Issue
Block a user