Files
eth_transfer_go/utils/ethtransferhelper/transfer_helper.go

283 lines
9.0 KiB
Go
Raw Normal View History

2025-05-13 15:44:44 +08:00
package ethtransferhelper
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
2025-05-15 18:39:19 +08:00
"regexp"
2025-05-13 15:44:44 +08:00
"strings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
2025-05-15 18:39:19 +08:00
"github.com/shopspring/decimal"
2025-05-13 15:44:44 +08:00
"golang.org/x/crypto/sha3"
)
2025-05-15 18:39:19 +08:00
// TransferErc20 发送 ERC-20 代币交易。
// tokenaddress: ERC-20 代币的合约地址。 如果为空则转移ETH
func TransferErc20(
client *ethclient.Client,
fromPrivateKey string,
tokenAddress string,
toAddress string,
tokenAmount decimal.Decimal,
tokenDecimals uint8) (*types.Transaction, error) {
switch tokenAddress {
2025-05-17 09:10:14 +08:00
case "0":
2025-05-15 18:39:19 +08:00
return TransferEth(client, fromPrivateKey, toAddress, tokenAmount)
default:
return TransferErc20Token(client, fromPrivateKey, tokenAddress, toAddress, tokenAmount, tokenDecimals)
}
}
// TransferEth 发送 ETH 交易。
func TransferEth(client *ethclient.Client, fromPrivateKey string, toAddress string, tokenAmount decimal.Decimal) (*types.Transaction, error) {
// 1. 解析私钥
privateKey, fromAddressCommon, err := GetAddressFromPrivateKey(fromPrivateKey)
if err != nil {
return nil, err
}
// 3. 获取发送者的 nonce交易序号
nonce, err := client.PendingNonceAt(context.Background(), fromAddressCommon)
if err != nil {
return nil, fmt.Errorf("获取 nonce 失败: %w", err)
}
// 4. 设置交易的 value (对于代币转账value 是 0)
value, _ := convertDecimalToBigInt(tokenAmount, 18)
// 5. 获取 Gas 价格
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
return nil, fmt.Errorf("获取 Gas 价格失败: %w", err)
}
gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(11))
gasPrice = new(big.Int).Div(gasPrice, big.NewInt(10))
2025-05-15 18:39:19 +08:00
// 6. 将地址字符串转换为 common.Address 类型
toAddressCommon := common.HexToAddress(toAddress)
// 7. 构造 ERC-20 transfer 函数的调用数据
// 7.1 函数签名transfer(address,uint256)
transferFnSignature := "transfer(address,uint256)" // 已经是标准化的
hash := sha3.NewLegacyKeccak256() // 或 sha3.NewLegacyKeccak256()
hash.Write([]byte(transferFnSignature))
// 7.4 拼接调用数据
var data []byte
// 8. 估算 Gas 消耗
gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
From: fromAddressCommon,
To: &toAddressCommon,
Data: data,
})
if err != nil {
return nil, fmt.Errorf("估算 Gas 消耗失败: %w", err)
}
// 9. (预估gas+基础费用)*1.1 作为 GasLimit
2025-05-17 09:10:14 +08:00
gasLimit = (gasLimit + 21000) * 19 / 10 // 增加 90%
2025-05-15 18:39:19 +08:00
if gasLimit < 23000 {
gasLimit = 23000 // 最小 Gas 限制
}
// 10. 创建交易
tx := types.NewTransaction(nonce, toAddressCommon, value, gasLimit, gasPrice, data)
// 11. 获取链 ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
return nil, fmt.Errorf("获取链 ID 失败: %w", err)
}
// 12. 签名交易
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
return nil, fmt.Errorf("签名交易失败: %w", err)
}
// 13. 发送交易
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
return nil, fmt.Errorf("发送交易失败: %w", err)
}
return signedTx, nil
}
2025-05-13 15:44:44 +08:00
// transferErc20Token 发送 ERC-20 代币交易。
// tokenAmount: 要发送的代币数量 (例如1.0 代表 1 个代币)。
// fromPrivateKey: 发送者的私钥。
// tokenAddress: ERC-20 代币的合约地址。
// toAddress: 接收者的地址。
// tokenDecimals: 代币的小数位数 (例如USDC 是 6很多其他代币是 18)。
2025-05-15 18:39:19 +08:00
func TransferErc20Token(
2025-05-13 15:44:44 +08:00
client *ethclient.Client,
fromPrivateKey string,
tokenAddress string,
toAddress string,
2025-05-15 18:39:19 +08:00
tokenAmount decimal.Decimal,
2025-05-13 15:44:44 +08:00
tokenDecimals uint8,
) (*types.Transaction, error) {
// 1. 解析私钥
privateKey, fromAddressCommon, err := GetAddressFromPrivateKey(fromPrivateKey)
if err != nil {
return nil, err
}
// 3. 获取发送者的 nonce交易序号
nonce, err := client.PendingNonceAt(context.Background(), fromAddressCommon)
if err != nil {
return nil, fmt.Errorf("获取 nonce 失败: %w", err)
}
// 4. 设置交易的 value (对于代币转账value 是 0)
value := big.NewInt(0) // 不发送 ETH
// 5. 获取 Gas 价格
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
return nil, fmt.Errorf("获取 Gas 价格失败: %w", err)
}
gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(11))
gasPrice = new(big.Int).Div(gasPrice, big.NewInt(10))
2025-05-13 15:44:44 +08:00
// 6. 将地址字符串转换为 common.Address 类型
toAddressCommon := common.HexToAddress(toAddress)
tokenAddressCommon := common.HexToAddress(tokenAddress)
// 7. 构造 ERC-20 transfer 函数的调用数据
// 7.1 函数签名transfer(address,uint256)
2025-05-15 18:39:19 +08:00
transferFnSignature := "transfer(address,uint256)" // 已经是标准化的
hash := sha3.NewLegacyKeccak256() // 或 sha3.NewLegacyKeccak256()
hash.Write([]byte(transferFnSignature))
methodID := hash.Sum(nil)[:4]
2025-05-13 15:44:44 +08:00
// 7.2 填充接收者地址和转账金额
paddedAddress := common.LeftPadBytes(toAddressCommon.Bytes(), 32)
// 7.3 将代币数量转换为最小单位
2025-05-15 18:39:19 +08:00
amountBigInt, err := convertDecimalToBigInt(tokenAmount, tokenDecimals)
2025-05-13 15:44:44 +08:00
if err != nil {
return nil, fmt.Errorf("转换代币数量失败: %w", err)
}
paddedAmount := common.LeftPadBytes(amountBigInt.Bytes(), 32)
// 7.4 拼接调用数据
var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)
// 8. 估算 Gas 消耗
gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
From: fromAddressCommon,
2025-05-15 18:39:19 +08:00
To: &toAddressCommon,
2025-05-13 15:44:44 +08:00
Data: data,
})
if err != nil {
return nil, fmt.Errorf("估算 Gas 消耗失败: %w", err)
}
2025-05-15 18:39:19 +08:00
// 9. (预估gas+基础费用)*1.1 作为 GasLimit
2025-05-17 09:10:14 +08:00
gasLimit = (gasLimit + 21000) * 19 / 10 // 增加 90%
2025-05-13 15:44:44 +08:00
if gasLimit < 23000 {
gasLimit = 23000 // 最小 Gas 限制
}
// 10. 创建交易
tx := types.NewTransaction(nonce, tokenAddressCommon, value, gasLimit, gasPrice, data)
// 11. 获取链 ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
return nil, fmt.Errorf("获取链 ID 失败: %w", err)
}
// 12. 签名交易
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
return nil, fmt.Errorf("签名交易失败: %w", err)
}
// 13. 发送交易
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
return nil, fmt.Errorf("发送交易失败: %w", err)
}
return signedTx, nil
}
// getAddressFromPrivateKey 从私钥获取公钥和地址。
func GetAddressFromPrivateKey(fromPrivateKey string) (*ecdsa.PrivateKey, common.Address, error) {
// 1. 移除 "0x" 前缀(如果存在)
if strings.HasPrefix(fromPrivateKey, "0x") {
fromPrivateKey = fromPrivateKey[2:]
}
privateKey, err := crypto.HexToECDSA(fromPrivateKey)
if err != nil {
return nil, common.Address{}, fmt.Errorf("解析私钥失败: %w", err)
}
// 2. 从私钥获取公钥和发送者地址
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return nil, common.Address{}, errors.New("无法将公钥转换为 ECDSA 类型")
}
fromAddressCommon := crypto.PubkeyToAddress(*publicKeyECDSA)
return privateKey, fromAddressCommon, nil
}
// convertTokenAmountToBigInt 将代币数量 (例如1.0) 转换为最小单位的整数表示 (例如USDC 的 1000000)。
2025-05-15 18:39:19 +08:00
func convertDecimalToBigInt(amountDecimal decimal.Decimal, tokenDecimals uint8) (*big.Int, error) {
// 1. 创建一个与 token decimals 精度相同的 10 的幂
exponent := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenDecimals)), nil)
// 2. 将 decimal 乘以该 10 的幂
amountScaled := amountDecimal.Mul(decimal.NewFromBigInt(exponent, 0))
// 3. 将 scaled decimal 转换为 big.Int
amountBigInt := amountScaled.BigInt()
2025-05-13 15:44:44 +08:00
return amountBigInt, nil
}
2025-05-15 18:39:19 +08:00
// GetTransactionByHash 获取交易的状态。
// status 0:失败 1-成功
// error: 交易未确认或不存在
func GetTransactionByHash(client *ethclient.Client, hash string) (int, error) {
txHash := common.HexToHash(hash)
// 获取交易收据(包含状态)
receipt, err := client.TransactionReceipt(context.Background(), txHash)
if err != nil {
return 0, errors.New("交易未确认或不存在")
}
return int(receipt.Status), nil
}
// 校验钱包地址是否合法
func IsValidAddress(address string) bool {
if !strings.HasPrefix(address, "0x") {
address = "0x" + address
}
if len(address) != 42 {
return false
}
if !regexp.MustCompile(`^0x[0-9a-fA-F]{40}$`).MatchString(address) {
return false
}
checksumAddress := common.HexToAddress(address).String()
2025-05-17 09:10:14 +08:00
return strings.EqualFold(address, checksumAddress)
2025-05-15 18:39:19 +08:00
}