1
This commit is contained in:
@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
@ -13,21 +14,114 @@ import (
|
||||
"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(
|
||||
func TransferErc20Token(
|
||||
client *ethclient.Client,
|
||||
fromPrivateKey string,
|
||||
tokenAddress string,
|
||||
toAddress string,
|
||||
tokenAmount float64,
|
||||
tokenAmount decimal.Decimal,
|
||||
tokenDecimals uint8,
|
||||
) (*types.Transaction, error) {
|
||||
// 1. 解析私钥
|
||||
@ -57,15 +151,15 @@ func transferErc20Token(
|
||||
|
||||
// 7. 构造 ERC-20 transfer 函数的调用数据
|
||||
// 7.1 函数签名:transfer(address,uint256)
|
||||
transferFnSignature := []byte("transfer(address,uint256)")
|
||||
hash := sha3.New256()
|
||||
hash.Write(transferFnSignature)
|
||||
methodID := hash.Sum(nil)[:4] // 取前 4 个字节作为方法 ID
|
||||
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 := convertTokenAmountToBigInt(tokenAmount, tokenDecimals)
|
||||
amountBigInt, err := convertDecimalToBigInt(tokenAmount, tokenDecimals)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("转换代币数量失败: %w", err)
|
||||
}
|
||||
@ -80,15 +174,15 @@ func transferErc20Token(
|
||||
// 8. 估算 Gas 消耗
|
||||
gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
|
||||
From: fromAddressCommon,
|
||||
To: &tokenAddressCommon,
|
||||
To: &toAddressCommon,
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("估算 Gas 消耗失败: %w", err)
|
||||
}
|
||||
|
||||
// 9. 增加 Gas 限制的安全边际
|
||||
gasLimit = gasLimit * 11 / 10 // 增加 10%
|
||||
// 9. (预估gas+基础费用)*1.1 作为 GasLimit
|
||||
gasLimit = (gasLimit + 21000) * 12 / 10 // 增加 20%
|
||||
if gasLimit < 23000 {
|
||||
gasLimit = 23000 // 最小 Gas 限制
|
||||
}
|
||||
@ -140,36 +234,45 @@ func GetAddressFromPrivateKey(fromPrivateKey string) (*ecdsa.PrivateKey, common.
|
||||
}
|
||||
|
||||
// convertTokenAmountToBigInt 将代币数量 (例如,1.0) 转换为最小单位的整数表示 (例如,USDC 的 1000000)。
|
||||
func convertTokenAmountToBigInt(tokenAmount float64, tokenDecimals uint8) (*big.Int, error) {
|
||||
// 1. 使用最大精度格式化浮点数
|
||||
amountStr := fmt.Sprintf("%.18f", tokenAmount) // 使用最大精度
|
||||
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. 找到小数点的位置
|
||||
decimalPointIndex := strings.Index(amountStr, ".")
|
||||
// 2. 将 decimal 乘以该 10 的幂
|
||||
amountScaled := amountDecimal.Mul(decimal.NewFromBigInt(exponent, 0))
|
||||
|
||||
// 3. 如果没有小数点,则添加足够的 0
|
||||
if decimalPointIndex == -1 {
|
||||
amountStr += "." + strings.Repeat("0", int(tokenDecimals))
|
||||
} else {
|
||||
// 4. 计算需要填充的 0 的数量
|
||||
paddingNeeded := int(tokenDecimals) - (len(amountStr) - decimalPointIndex - 1)
|
||||
|
||||
// 5. 填充 0 或截断多余的小数位
|
||||
if paddingNeeded > 0 {
|
||||
amountStr += strings.Repeat("0", paddingNeeded)
|
||||
} else if paddingNeeded < 0 {
|
||||
amountStr = amountStr[:decimalPointIndex+int(tokenDecimals)+1]
|
||||
}
|
||||
// 6. 移除小数点
|
||||
amountStr = strings.ReplaceAll(amountStr, ".", "")
|
||||
}
|
||||
|
||||
// 7. 将字符串转换为 big.Int
|
||||
amountBigInt := new(big.Int)
|
||||
amountBigInt, ok := amountBigInt.SetString(amountStr, 10)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("将金额字符串转换为 big.Int 失败: %s", amountStr)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user