merge transactions and update

This commit is contained in:
lzx
2025-10-31 13:46:58 +08:00
parent 056bc05b75
commit 8d7da5d345
12 changed files with 1154 additions and 975 deletions

View File

@@ -1,213 +0,0 @@
package eth
import (
"crypto/ecdsa"
"fmt"
"log"
"m2pool-payment/internal/utils"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)
// BatchTransferItem 批量转账单项
type BatchTransferItem struct {
ToAddress string // 接收地址
Amount float64 // 转账金额
}
// BatchTransferResult 批量转账结果
type BatchTransferResult struct {
TxHash string // 交易哈希
Success bool // 是否成功
TotalAmount float64 // 总转账金额
Count int // 转账笔数
}
// usdt_batch_transfer 批量转账ERC20-USDT
// from: 发送地址
// items: 批量转账列表
// returns: 交易哈希和错误信息
func (e *ETHNode) USDTBatchTransfer(from string, items []BatchTransferItem) (*BatchTransferResult, error) {
if len(items) == 0 {
return nil, fmt.Errorf("批量转账列表不能为空")
}
// 统一转换为小写
from = strings.ToLower(from)
// 计算总金额
var totalAmount float64
for _, item := range items {
if item.Amount <= 0 {
return nil, fmt.Errorf("转账金额必须大于0")
}
totalAmount += item.Amount
}
// 1. 校验钱包USDT余额
balance, err := e.getUSDTBalance(from)
log.Printf("🔄 批量转账 - 检测钱包=%s余额=%.2f USDT", from, balance)
if err != nil {
return nil, fmt.Errorf("获取余额失败: %w", err)
}
if balance < totalAmount {
return nil, fmt.Errorf("余额不足: 余额=%.2f USDT < 需要=%.2f USDT", balance, totalAmount)
}
// 2. 通过from地址前往数据库查找出对应加密后的私钥并解密真实的私钥
originalKey := e.decodePrivatekey(from)
if originalKey == "" {
return nil, fmt.Errorf("无法获取私钥")
}
privateKey, err := crypto.HexToECDSA(originalKey)
if err != nil {
return nil, fmt.Errorf("解析私钥失败: %w", err)
}
// 3. 获得nonce
nonce, err := e.RpcClient.PendingNonceAt(e.Ctx, common.HexToAddress(from))
if err != nil {
return nil, fmt.Errorf("获取nonce失败: %w", err)
}
// 4. 构造批量转账数据
// 使用 transfer(address[], uint256[]) 或多次transfer调用
// 这里使用多次transfer调用的方式因为标准ERC20没有批量转账方法
// 方法1: 构造多次transfer调用适合少量转账
if len(items) <= 3 {
return e.batchTransferMultipleCalls(from, privateKey, nonce, items)
}
// 方法2: 使用合约批量转账(需要部署代理合约,这里简化处理)
// 注意这里实现的是多次transaction方式
return e.batchTransferSeparateTransactions(from, privateKey, nonce, items)
}
// batchTransferMultipleCalls 使用一个交易多次调用transfer需要gas优化
func (e *ETHNode) batchTransferMultipleCalls(from string, privateKey *ecdsa.PrivateKey, nonce uint64, items []BatchTransferItem) (*BatchTransferResult, error) {
// 注意标准ERC20不支持批量transfer这里需要自定义合约
// 或者使用多次独立交易
log.Printf("⚠️ 标准ERC20不支持批量transfer改用多次独立交易")
// 回退到多次独立交易
return e.batchTransferSeparateTransactions(from, privateKey, nonce, items)
}
// batchTransferSeparateTransactions 执行多次独立的transfer交易
func (e *ETHNode) batchTransferSeparateTransactions(from string, privateKey *ecdsa.PrivateKey, nonce uint64, items []BatchTransferItem) (*BatchTransferResult, error) {
var totalAmount float64
var txHashes []string
var allSuccess bool = true
for i, item := range items {
// 构造单个transfer交易
amountBigInt := utils.Float64ToBigIntUSDT(item.Amount)
data, err := e.USDT.ABI.Pack("transfer", common.HexToAddress(strings.ToLower(item.ToAddress)), amountBigInt)
if err != nil {
log.Printf("❌ 批量转账第%d笔打包失败: %v", i+1, err)
allSuccess = false
continue
}
// 获取gas limit
gasLimit, err := e.getGasLimit()
if err != nil {
log.Printf("❌ 批量转账第%d笔获取gasLimit失败: %v", i+1, err)
allSuccess = false
continue
}
// 获取gas费用
maxFeePerGas, maxPriorityFeePerGas, err := e.getEIP1559GasFees()
var txHash string
if err != nil {
// 回退到传统gas price
gasPrice, err := e.getSuggestGasPrice()
if err != nil {
log.Printf("❌ 批量转账第%d笔获取gasPrice失败: %v", i+1, err)
allSuccess = false
continue
}
tx := types.NewTransaction(nonce+uint64(i), e.USDT.Address, big.NewInt(0), gasLimit, gasPrice, data)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
if err != nil {
log.Printf("❌ 批量转账第%d笔签名失败: %v", i+1, err)
allSuccess = false
continue
}
txHash = signedTx.Hash().Hex()
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
log.Printf("❌ 批量转账第%d笔发送失败: %v", i+1, err)
allSuccess = false
continue
}
} else {
// 使用EIP-1559交易
ethBalance, err := e.getETHBlance(from)
if err != nil {
log.Printf("❌ 批量转账第%d笔获取ETH余额失败: %v", i+1, err)
allSuccess = false
continue
}
maxGasCost := new(big.Int).Mul(new(big.Int).SetUint64(gasLimit), maxFeePerGas)
if ethBalance.Cmp(maxGasCost) == -1 {
log.Printf("❌ 批量转账第%d笔ETH余额不足", i+1)
allSuccess = false
continue
}
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: e.NetId,
Nonce: nonce + uint64(i),
GasTipCap: maxPriorityFeePerGas,
GasFeeCap: maxFeePerGas,
Gas: gasLimit,
To: &e.USDT.Address,
Value: big.NewInt(0),
Data: data,
})
signedTx, err := types.SignTx(tx, types.NewLondonSigner(e.NetId), privateKey)
if err != nil {
log.Printf("❌ 批量转账第%d笔签名失败: %v", i+1, err)
allSuccess = false
continue
}
txHash = signedTx.Hash().Hex()
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
log.Printf("❌ 批量转账第%d笔发送失败: %v", i+1, err)
allSuccess = false
continue
}
}
txHashes = append(txHashes, txHash)
totalAmount += item.Amount
log.Printf("✅ 批量转账第%d笔已提交: %s, 金额=%.2f USDT, 收款地址=%s",
i+1, txHash, item.Amount, strings.ToLower(item.ToAddress))
}
log.Printf("📊 批量转账完成: 总计%d笔, 成功%d笔, 总金额=%.2f USDT",
len(items), len(txHashes), totalAmount)
return &BatchTransferResult{
TxHash: strings.Join(txHashes, ","),
Success: allSuccess && len(txHashes) == len(items),
TotalAmount: totalAmount,
Count: len(txHashes),
}, nil
}

View File

@@ -1,101 +0,0 @@
# ERC20-USDT 批量转账功能
## 功能说明
该文件 `batch_transfer.go` 提供了 ERC20-USDT 的批量转账功能,支持从同一个发送地址向多个不同的接收地址转账。
## 主要功能
### 1. 批量转账类型
```go
type BatchTransferItem struct {
ToAddress string // 接收地址
Amount float64 // 转账金额
}
type BatchTransferResult struct {
TxHash string // 交易哈希(多个用逗号分隔)
Success bool // 是否成功
TotalAmount float64 // 总转账金额
Count int // 转账笔数
}
```
### 2. 使用方法
```go
// 1. 准备批量转账列表
items := []eth.BatchTransferItem{
{ToAddress: "0xRecipient1", Amount: 100.0},
{ToAddress: "0xRecipient2", Amount: 200.0},
{ToAddress: "0xRecipient3", Amount: 50.0},
}
// 2. 调用批量转账
fromAddress := "0xYourAddress"
result, err := ethNode.USDTBatchTransfer(fromAddress, items)
if err != nil {
log.Fatalf("批量转账失败: %v", err)
}
// 3. 处理结果
fmt.Printf("批量转账完成: %d笔, 总金额: %.2f USDT", result.Count, result.TotalAmount)
fmt.Printf("交易哈希: %s", result.TxHash)
```
## 工作原理
由于标准 ERC20 合约不支持批量转账,本实现采用以下策略:
1. **多次独立交易**:对每笔转账创建一个独立的 ERC20 `transfer` 交易
2. **Nonce 管理**:自动管理 nonce确保交易按顺序广播
3. **Gas 费用**:支持 EIP-1559 动态费用和传统 gas price
4. **错误处理**:单笔失败不影响其他交易,返回成功和失败的详细统计
## 注意事项
### 1. Gas 费用
- 每笔转账需要独立的 gas 费用(约 65,000 gas
- 批量转账 10 笔需要约 650,000 gas
- 确保发送地址有足够的 ETH 作为 gas 费用
### 2. 余额检查
- 函数会自动检查 USDT 余额是否足够
- 如果余额不足,会返回错误并终止转账
### 3. 部分成功
- 如果某些转账失败,函数会继续执行其他转账
- 返回结果中包含成功笔数和详细交易哈希
### 4. 网络拥堵
- 在高网络拥堵时,某些交易可能被推迟
- 建议监控所有交易状态
## 性能优化建议
如果需要更高效的批量转账,考虑:
1. **部署批量转账代理合约**:实现一个合约方法 `batchTransfer(address[] to, uint256[] amounts)`
2. **使用多签钱包**:减少私钥管理风险
3. **Gas 优化**:使用更低的 gas price 分批发送
## 示例输出
```
🔄 批量转账 - 检测钱包=0x...,余额=1000.00 USDT
✅ 批量转账第1笔已提交: 0xabc123..., 金额=100.00 USDT, 收款地址=0x...
✅ 批量转账第2笔已提交: 0xdef456..., 金额=200.00 USDT, 收款地址=0x...
✅ 批量转账第3笔已提交: 0x789ghi..., 金额=50.00 USDT, 收款地址=0x...
📊 批量转账完成: 总计3笔, 成功3笔, 总金额=350.00 USDT
```
## 限制
- 标准 ERC20 不支持真正的批量转账(单笔交易)
- 需要确保发送地址有足够的 ETH 作为 gas 费用
- 交易按顺序发送,可能在高负载时较慢

View File

@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MultiSend {
function multiTransfer(address[] calldata to, uint256[] calldata amounts) external payable {
uint256 length = to.length;
require(length == amounts.length, "Arrays must have the same length");
for (uint256 i = 0; i < length; i++) {
payable(to[i]).transfer(amounts[i]);
}
}
}

View File

@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function transfer(address recipient, uint256 amount) external returns (bool);
}
contract MultiSendUSDT {
address public tokenAddress;
constructor(address _tokenAddress) {
tokenAddress = _tokenAddress;
}
function multiTransfer(address[] calldata to, uint256[] calldata amounts) external {
IERC20 token = IERC20(tokenAddress);
uint256 length = to.length;
require(length == amounts.length, "Arrays must have the same length");
for (uint256 i = 0; i < length; i++) {
token.transfer(to[i], amounts[i]);
}
}
}

File diff suppressed because it is too large Load Diff