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) }