279 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			279 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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 "":
 | ||
| 		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) * 12 / 10 // 增加 20%
 | ||
| 	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) * 12 / 10 // 增加 20%
 | ||
| 	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 address == checksumAddress
 | ||
| }
 |