batch-bsc-sender/app.js
2026-01-24 22:31:49 +08:00

713 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
console.log('获取到的Gas价格 (Gwei):', gasPrice);
this.gasPrice.value = window.TransferUtils.formatNumber(parseFloat(gasPrice), 2);
console.log('设置的Gas价格值:', this.gasPrice.value);
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 && parseFloat(this.gasPrice.value) > 0) {
const gasPrice = parseFloat(this.gasPrice.value);
const gasLimit = parseInt(this.gasLimit.value);
console.log('开始计算Gas费用:');
console.log('- 转账数量:', this.transferData.length);
console.log('- Gas价格 (Gwei):', gasPrice);
console.log('- Gas限制:', gasLimit);
try {
const cost = await window.BatchTransfer.calculateTotalCost(
this.transferData,
gasLimit,
gasPrice,
this.isTokenTransfer
);
console.log('计算结果:');
console.log('- 总Gas费用 (BNB):', cost.totalGasCost);
console.log('- 总成本 (BNB):', cost.totalCost);
this.estimatedGas.textContent = window.TransferUtils.formatNumber(cost.totalGasCost, 8);
this.totalCost.textContent = window.TransferUtils.formatNumber(cost.totalCost, 8);
} catch (error) {
console.error('计算Gas费用失败:', error);
}
} else {
console.log('无法计算Gas费用:');
console.log('- Provider存在:', !!window.BatchTransfer.provider);
console.log('- Gas价格值:', this.gasPrice.value);
console.log('- Gas价格>0:', parseFloat(this.gasPrice.value) > 0);
}
// 启用开始按钮
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();
});