batch-bsc-sender/batchTransfer.js
2026-01-24 22:22:11 +08:00

435 lines
14 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 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();