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