This commit is contained in:
lzx
2025-10-27 16:27:33 +08:00
parent b7c84fd101
commit b1d3e07c36
14 changed files with 2982 additions and 948 deletions

View File

@@ -0,0 +1,213 @@
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

@@ -0,0 +1,101 @@
# 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

@@ -125,27 +125,25 @@ func NewETHNode(cfg message.ETHConfig, decodeKey string) (*ETHNode, error) {
}
// ============================ 抽象接口 ============================
func (e *ETHNode) AddAddress(address string, rmq_msg any) {
func (e *ETHNode) AddAddress(address string, rmq_msg any) error {
// 统一转换为小写
address = strings.ToLower(address)
log.Printf("新增钱包监听消息:%v", rmq_msg)
e.ListenAddresses.Store(address, true)
e.mu.Lock()
if len(e.RmqMsgs[address]) == 0 {
e.RmqMsgs[address] = []any{rmq_msg}
} else {
e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg)
}
e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg)
e.mu.Unlock()
return nil
}
func (e *ETHNode) RemoveAddress(address string) {
func (e *ETHNode) RemoveAddress(address string) error {
// 统一转换为小写
address = strings.ToLower(address)
e.ListenAddresses.Delete(address)
e.mu.Lock()
delete(e.RmqMsgs, address)
e.mu.Unlock()
return nil
}
func (e *ETHNode) Listen(symbol string, ch chan any) {
@@ -174,6 +172,10 @@ func (e *ETHNode) Transfer(symbol string, msg any) error {
return nil
}
func (e *ETHNode) Stop() {
e.Cancel()
}
// ============================ rpc节点方法 ============================
func (e *ETHNode) getETHBlance(address string) (*big.Int, error) {
@@ -221,12 +223,11 @@ func (e *ETHNode) getUSDTBalance(address string) (float64, error) {
return bal, nil
}
func (e *ETHNode) getBlockHeight() (uint64, error) {
header, err := e.RpcClient.HeaderByNumber(e.Ctx, nil)
if err != nil {
return 0, fmt.Errorf("failed to get latest block header: %w", err)
}
return header.Number.Uint64(), nil
func (e *ETHNode) getGasLimit() (uint64, error) {
// 对于ERC20转账使用固定的gas limit
// 通常ERC20 transfer需要约65,000-100,000 gas
// 这里设置为80,000足够覆盖大部分情况
return 80000, nil
}
func (e *ETHNode) getSuggestGasPrice() (*big.Int, error) {
@@ -235,9 +236,56 @@ func (e *ETHNode) getSuggestGasPrice() (*big.Int, error) {
if err != nil {
return nil, fmt.Errorf("get suggest-gasprice error:%v", err)
}
// 设置gas price上限避免在网络拥堵时费用过高
// 这里设置为20 Gwei (20 * 10^9 wei)
maxGasPrice := new(big.Int).SetUint64(20000000000) // 20 Gwei
if gasPrice.Cmp(maxGasPrice) > 0 {
log.Printf("⚠️ 建议gas price过高 (%v Gwei),使用上限 20 Gwei", new(big.Int).Div(gasPrice, big.NewInt(1000000000)))
return maxGasPrice, nil
}
log.Printf("✅ 使用建议gas price: %v Gwei", new(big.Int).Div(gasPrice, big.NewInt(1000000000)))
return gasPrice, nil
}
// getEIP1559GasFees 获取EIP-1559的gas费用参数
func (e *ETHNode) getEIP1559GasFees() (*big.Int, *big.Int, error) {
ctx := context.Background()
// 获取基础费用
latestBlock, err := e.RpcClient.BlockByNumber(ctx, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to get latest block: %w", err)
}
baseFee := latestBlock.BaseFee()
if baseFee == nil {
return nil, nil, fmt.Errorf("base fee not available")
}
// 设置优先级费用tip这里设置为2 Gwei
maxPriorityFeePerGas := new(big.Int).SetUint64(2000000000) // 2 Gwei
// 计算最大费用 = 基础费用 + 优先级费用
maxFeePerGas := new(big.Int).Add(baseFee, maxPriorityFeePerGas)
// 设置最大费用上限为30 Gwei
maxFeeLimit := new(big.Int).SetUint64(30000000000) // 30 Gwei
if maxFeePerGas.Cmp(maxFeeLimit) > 0 {
log.Printf("⚠️ 计算的最大费用过高 (%v Gwei),使用上限 30 Gwei", new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)))
maxFeePerGas = maxFeeLimit
}
log.Printf("✅ EIP-1559 Gas费用: BaseFee=%v Gwei, MaxPriorityFee=%v Gwei, MaxFee=%v Gwei",
new(big.Int).Div(baseFee, big.NewInt(1000000000)),
new(big.Int).Div(maxPriorityFeePerGas, big.NewInt(1000000000)),
new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)))
return maxFeePerGas, maxPriorityFeePerGas, nil
}
// ============================ 业务方法 ============================
func (e *ETHNode) listen_usdt(ch chan any) error {
fmt.Println("🔍 ETH 开始监听 USDT Transfer 事件...")
@@ -257,6 +305,7 @@ func (e *ETHNode) listen_usdt(ch chan any) error {
fmt.Println("✅ 订阅成功")
// 处理事件
for {
select {
case err := <-sub.Err():
fmt.Println("⚠️ 订阅异常,准备重连:", err)
@@ -578,7 +627,6 @@ func (e *ETHNode) decodePrivatekey(address string) string {
}
// 使用key解密
privateKey := encryptedKey // 实际使用时替换成具体的解密代码
// fmt.Println(privateKey)
return privateKey
}
@@ -624,7 +672,6 @@ func (e *ETHNode) usdt_transfer(msg any) error {
if originalKey == "" {
return fmt.Errorf("failed to query privatekey")
}
fmt.Println(originalKey)
privateKey, err := crypto.HexToECDSA(originalKey)
if err != nil {
return fmt.Errorf("failed to parse private key: %w", err)
@@ -640,41 +687,125 @@ func (e *ETHNode) usdt_transfer(msg any) error {
if err != nil {
return fmt.Errorf("failed to pack transfer data: %w", err)
}
gasPrice, err := e.getSuggestGasPrice() // 获得当前建议gasPrice
gasLimit, err := e.getGasLimit() // 获得gasLimit
if err != nil {
return fmt.Errorf("get suggest-gasprice error:%v", err)
return fmt.Errorf("get gas limit error:%v", err)
}
eth_balance, err := e.getETHBlance(final_from) // 获得钱包eth余额
// 获取EIP-1559 gas费用参数
maxFeePerGas, maxPriorityFeePerGas, err := e.getEIP1559GasFees()
if err != nil {
log.Printf("⚠️ 获取EIP-1559费用失败回退到传统gas price: %v", err)
// 回退到传统gas price
gasPrice, err := e.getSuggestGasPrice()
if err != nil {
return fmt.Errorf("get suggest-gasprice error:%v", err)
}
eth_balance, err := e.getETHBlance(final_from)
if err != nil {
return fmt.Errorf("%w", err)
}
gasLimit_b := new(big.Int).SetUint64(gasLimit)
gas := new(big.Int).Mul(gasLimit_b, gasPrice)
// 计算gas费用以ETH为单位
gasInETH := new(big.Float).SetInt(gas)
gasInETH.Quo(gasInETH, new(big.Float).SetInt64(1000000000000000000))
log.Printf("💰 传统Gas费用预估: Limit=%d, Price=%v Gwei, 总费用=%.6f ETH",
gasLimit,
new(big.Int).Div(gasPrice, big.NewInt(1000000000)),
gasInETH)
// 判断钱包eth是否支持本次交易gas费用
if eth_balance.Cmp(gas) == -1 {
ethBalanceInETH := new(big.Float).SetInt(eth_balance)
ethBalanceInETH.Quo(ethBalanceInETH, new(big.Float).SetInt64(1000000000000000000))
return fmt.Errorf("❌ 地址 %s ETH余额不足: %.6f ETH < %.6f ETH (gas费用)",
final_from, ethBalanceInETH, gasInETH)
}
// 构造传统交易
tx := types.NewTransaction(
nonce,
e.USDT.Address,
big.NewInt(0),
gasLimit,
gasPrice,
data,
)
// 签名并发送传统交易
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
if err != nil {
return fmt.Errorf("failed to sign transaction: %w", err)
}
txHash := signedTx.Hash().Hex()
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
return fmt.Errorf("failed to send transaction: %w", err)
}
log.Printf("✅ 传统交易已提交至mempool%s金额:%.2f USDT, 手续费:%.6f ETH", txHash, amount, gasInETH)
return nil
}
// 使用EIP-1559交易
eth_balance, err := e.getETHBlance(final_from)
if err != nil {
return fmt.Errorf("%w", err)
}
var gasLimit uint64 = 100000
gasLimit_b := new(big.Int).SetUint64(gasLimit)
gas := new(big.Int).Mul(gasLimit_b, gasPrice)
// 计算最大可能的gas费用
maxGasCost := new(big.Int).Mul(new(big.Int).SetUint64(gasLimit), maxFeePerGas)
// 计算gas费用以ETH为单位
maxGasCostInETH := new(big.Float).SetInt(maxGasCost)
maxGasCostInETH.Quo(maxGasCostInETH, new(big.Float).SetInt64(1000000000000000000))
log.Printf("💰 EIP-1559 Gas费用预估: Limit=%d, MaxFee=%v Gwei, MaxPriorityFee=%v Gwei, 最大费用=%.6f ETH",
gasLimit,
new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)),
new(big.Int).Div(maxPriorityFeePerGas, big.NewInt(1000000000)),
maxGasCostInETH)
// 判断钱包eth是否支持本次交易gas费用
if eth_balance.Cmp(gas) == -1 {
return fmt.Errorf("address=%s balance less than gas=%v(wei)", final_from, eth_balance)
if eth_balance.Cmp(maxGasCost) == -1 {
ethBalanceInETH := new(big.Float).SetInt(eth_balance)
ethBalanceInETH.Quo(ethBalanceInETH, new(big.Float).SetInt64(1000000000000000000))
return fmt.Errorf("❌ 地址 %s ETH余额不足: %.6f ETH < %.6f ETH (最大gas费用)",
final_from, ethBalanceInETH, maxGasCostInETH)
}
// 构造发送到 USDT 合约地址的交易
tx := types.NewTransaction(
nonce,
e.USDT.Address, // 发送到USDT合约地址
big.NewInt(0), // value为0ERC20转账不需要ETH
gasLimit, // GasLimit设置为100000ERC20转账需要更多gas
gasPrice, // GasPrice: 20 Gwei
data, // 附加数据transfer方法调用
)
// 6, 签名交易并获得txHash
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
// txHash := signedTx.Hash().Hex() // 通过签名信息解析出交易hash
// 构造EIP-1559交易
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: e.NetId,
Nonce: nonce,
GasTipCap: maxPriorityFeePerGas,
GasFeeCap: maxFeePerGas,
Gas: gasLimit,
To: &e.USDT.Address,
Value: big.NewInt(0),
Data: data,
})
// 6, 签名EIP-1559交易并获得txHash
signedTx, err := types.SignTx(tx, types.NewLondonSigner(e.NetId), privateKey)
if err != nil {
return fmt.Errorf("failed to sign transaction: %w", err)
return fmt.Errorf("failed to sign EIP-1559 transaction: %w", err)
}
// 7, 发送交易
txHash := signedTx.Hash().Hex()
// 7, 发送EIP-1559交易
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
return fmt.Errorf("failed to send transaction: %w", err)
return fmt.Errorf("failed to send EIP-1559 transaction: %w", err)
}
log.Printf("✅ EIP-1559交易已提交至mempool%s金额:%.2f USDT, 最大手续费:%.6f ETH", txHash, amount, maxGasCostInETH)
// // 8, 构造交易消息
// tx_msg := message.Tx_msg{
// TxType: tx_type,
@@ -692,7 +823,3 @@ func (e *ETHNode) usdt_transfer(msg any) error {
// e.UnConfirmTxs[txHash] = tx_msg
return nil
}
func (e *ETHNode) Stop() {
e.Cancel()
}