Files
eth_transfer_go/utils/ethtransferhelper/transfer_helper.go

279 lines
8.8 KiB
Go
Raw 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.

package ethtransferhelper
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
"regexp"
"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"
"github.com/shopspring/decimal"
"golang.org/x/crypto/sha3"
)
// 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 {
case "0":
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)
}
// 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
gasLimit = (gasLimit + 21000) * 19 / 10 // 增加 90%
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
}
// transferErc20Token 发送 ERC-20 代币交易。
// tokenAmount: 要发送的代币数量 (例如1.0 代表 1 个代币)。
// fromPrivateKey: 发送者的私钥。
// tokenAddress: ERC-20 代币的合约地址。
// toAddress: 接收者的地址。
// tokenDecimals: 代币的小数位数 (例如USDC 是 6很多其他代币是 18)。
func TransferErc20Token(
client *ethclient.Client,
fromPrivateKey string,
tokenAddress string,
toAddress string,
tokenAmount decimal.Decimal,
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)
}
// 6. 将地址字符串转换为 common.Address 类型
toAddressCommon := common.HexToAddress(toAddress)
tokenAddressCommon := common.HexToAddress(tokenAddress)
// 7. 构造 ERC-20 transfer 函数的调用数据
// 7.1 函数签名transfer(address,uint256)
transferFnSignature := "transfer(address,uint256)" // 已经是标准化的
hash := sha3.NewLegacyKeccak256() // 或 sha3.NewLegacyKeccak256()
hash.Write([]byte(transferFnSignature))
methodID := hash.Sum(nil)[:4]
// 7.2 填充接收者地址和转账金额
paddedAddress := common.LeftPadBytes(toAddressCommon.Bytes(), 32)
// 7.3 将代币数量转换为最小单位
amountBigInt, err := convertDecimalToBigInt(tokenAmount, tokenDecimals)
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,
To: &toAddressCommon,
Data: data,
})
if err != nil {
return nil, fmt.Errorf("估算 Gas 消耗失败: %w", err)
}
// 9. (预估gas+基础费用)*1.1 作为 GasLimit
gasLimit = (gasLimit + 21000) * 19 / 10 // 增加 90%
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)。
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()
return amountBigInt, nil
}
// 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()
return strings.EqualFold(address, checksumAddress)
}