1
This commit is contained in:
88
utils/aeshelper/aes256.go
Normal file
88
utils/aeshelper/aes256.go
Normal file
@ -0,0 +1,88 @@
|
||||
package aeshelper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Encrypt text with the passphrase
|
||||
func Encrypt(text string, passphrase string) string {
|
||||
salt := make([]byte, 8)
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
key, iv := DeriveKeyAndIv(passphrase, string(salt))
|
||||
|
||||
block, err := aes.NewCipher([]byte(key))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pad := PKCS7Padding([]byte(text), block.BlockSize())
|
||||
ecb := cipher.NewCBCEncrypter(block, []byte(iv))
|
||||
encrypted := make([]byte, len(pad))
|
||||
ecb.CryptBlocks(encrypted, pad)
|
||||
|
||||
return base64.StdEncoding.EncodeToString([]byte("Salted__" + string(salt) + string(encrypted)))
|
||||
}
|
||||
|
||||
// Decrypt encrypted text with the passphrase
|
||||
func Decrypt(encrypted string, passphrase string) string {
|
||||
ct, _ := base64.StdEncoding.DecodeString(encrypted)
|
||||
if len(ct) < 16 || string(ct[:8]) != "Salted__" {
|
||||
return ""
|
||||
}
|
||||
|
||||
salt := ct[8:16]
|
||||
ct = ct[16:]
|
||||
key, iv := DeriveKeyAndIv(passphrase, string(salt))
|
||||
|
||||
block, err := aes.NewCipher([]byte(key))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cbc := cipher.NewCBCDecrypter(block, []byte(iv))
|
||||
dst := make([]byte, len(ct))
|
||||
cbc.CryptBlocks(dst, ct)
|
||||
|
||||
return string(PKCS7Trimming(dst))
|
||||
}
|
||||
|
||||
// PKCS7Padding PKCS7Padding
|
||||
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
// PKCS7Trimming PKCS7Trimming
|
||||
func PKCS7Trimming(encrypt []byte) []byte {
|
||||
padding := encrypt[len(encrypt)-1]
|
||||
return encrypt[:len(encrypt)-int(padding)]
|
||||
}
|
||||
|
||||
// DeriveKeyAndIv DeriveKeyAndIv
|
||||
func DeriveKeyAndIv(passphrase string, salt string) (string, string) {
|
||||
salted := ""
|
||||
dI := ""
|
||||
|
||||
for len(salted) < 48 {
|
||||
md := md5.New()
|
||||
md.Write([]byte(dI + passphrase + salt))
|
||||
dM := md.Sum(nil)
|
||||
dI = string(dM[:16])
|
||||
salted = salted + dI
|
||||
}
|
||||
|
||||
key := salted[0:32]
|
||||
iv := salted[32:48]
|
||||
|
||||
return key, iv
|
||||
}
|
||||
108
utils/aeshelper/aeshelper.go
Normal file
108
utils/aeshelper/aeshelper.go
Normal file
@ -0,0 +1,108 @@
|
||||
package aeshelper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/forgoer/openssl"
|
||||
)
|
||||
|
||||
const (
|
||||
sKey = "ptQJqRKyICCTeo6w" // "dde4b1f8a9e6b814"
|
||||
ivParameter = "O3vZvOJSxQDP9hKT" // "dde4b1f8a9e6b814"
|
||||
|
||||
)
|
||||
|
||||
// PswEncrypt 加密
|
||||
func PswEncrypt(src string) (string, error) {
|
||||
key := []byte(sKey)
|
||||
iv := []byte(ivParameter)
|
||||
result, err := Aes128Encrypt([]byte(src), key, iv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawStdEncoding.EncodeToString(result), nil
|
||||
}
|
||||
|
||||
// PswDecrypt 解密
|
||||
func PswDecrypt(src string) (string, error) {
|
||||
key := []byte(sKey)
|
||||
iv := []byte(ivParameter)
|
||||
var result []byte
|
||||
var err error
|
||||
result, err = base64.StdEncoding.DecodeString(src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
origData, err := Aes128Decrypt(result, key, iv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(origData), nil
|
||||
}
|
||||
|
||||
func Aes128Encrypt(origData, key []byte, IV []byte) ([]byte, error) {
|
||||
if key == nil || len(key) != 16 {
|
||||
return nil, nil
|
||||
}
|
||||
if IV != nil && len(IV) != 16 {
|
||||
return nil, nil
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
origData = PKCS5Padding(origData, blockSize)
|
||||
blockMode := cipher.NewCBCEncrypter(block, IV[:blockSize])
|
||||
crypted := make([]byte, len(origData))
|
||||
// 根据CryptBlocks方法的说明,如下方式初始化crypted也可以
|
||||
blockMode.CryptBlocks(crypted, origData)
|
||||
return crypted, nil
|
||||
}
|
||||
func Aes128Decrypt(crypted, key []byte, IV []byte) ([]byte, error) {
|
||||
if key == nil || len(key) != 16 {
|
||||
return nil, nil
|
||||
}
|
||||
if IV != nil && len(IV) != 16 {
|
||||
return nil, nil
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, IV[:blockSize])
|
||||
origData := make([]byte, len(crypted))
|
||||
blockMode.CryptBlocks(origData, crypted)
|
||||
origData = PKCS5UnPadding(origData)
|
||||
return origData, nil
|
||||
}
|
||||
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
func PKCS5UnPadding(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
// 去掉最后一个字节 unpadding 次
|
||||
unpadding := int(origData[length-1])
|
||||
return origData[:(length - unpadding)]
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
func AesEcbEncrypt(origData string) string {
|
||||
//加密
|
||||
dst, _ := openssl.AesECBEncrypt([]byte(origData), []byte(sKey), openssl.PKCS7_PADDING)
|
||||
return hex.EncodeToString(dst)
|
||||
}
|
||||
|
||||
// 密码解密
|
||||
func AesEcbDecrypt(origData string) string {
|
||||
value, _ := hex.DecodeString(origData)
|
||||
dst, _ := openssl.AesECBDecrypt(value, []byte(sKey), openssl.PKCS7_PADDING)
|
||||
return string(dst)
|
||||
}
|
||||
65
utils/aeshelper/aeshelper_test.go
Normal file
65
utils/aeshelper/aeshelper_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
package aeshelper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 测试加密解密
|
||||
func Test_EnDe(t *testing.T) {
|
||||
a := `asg`
|
||||
b := Encrypt(a, `code_verify_success`)
|
||||
c := Decrypt(b, `code_verify_success`)
|
||||
fmt.Println(`原始为`, a)
|
||||
fmt.Println(`加密后`, b)
|
||||
fmt.Println(`解密后`, c)
|
||||
}
|
||||
func TestAesEcbEncrypt(t *testing.T) {
|
||||
//aes := AesEcbEncrypt("123456")
|
||||
aes := AesEcbEncrypt(fmt.Sprintf("%v_%v", time.Now().Unix(), 1332355333))
|
||||
dst := AesEcbDecrypt(aes)
|
||||
fmt.Println(aes)
|
||||
fmt.Println(dst)
|
||||
}
|
||||
|
||||
// TODO:需要加密的接口
|
||||
/**
|
||||
1.合约下单接口 /api/futures/trade/order
|
||||
2.合约撤单 /api/futures/trade/cancelorder
|
||||
3.调整保证金 /api/futures/trade/adjustmargin
|
||||
4.变换逐全仓模式 /api/futures/trade/marginType
|
||||
5.更改持仓模式(方向) /api/futures/trade/positionSide/dual
|
||||
6.资产划转 /api/futures/transfer
|
||||
*/
|
||||
func TestAesEcbEncryptOrder(t *testing.T) {
|
||||
data := addFutOrderReq{
|
||||
OrderType: 1,
|
||||
BuyType: 3,
|
||||
TriggerDecide: 3,
|
||||
IsReduce: 2,
|
||||
Coin: "asdf",
|
||||
Price: "333.23",
|
||||
Num: "23.20",
|
||||
TriggerPrice: "1.023",
|
||||
PositionSide: "long",
|
||||
}
|
||||
b, _ := json.Marshal(data)
|
||||
aes := AesEcbEncrypt(string(b))
|
||||
dst := AesEcbDecrypt(aes)
|
||||
fmt.Println(aes)
|
||||
fmt.Println(dst)
|
||||
}
|
||||
|
||||
type addFutOrderReq struct {
|
||||
OrderType int `json:"order_type"` // 订单类型:1限价,2限价止盈止损,3市价,4市价止盈止损,5强平委托(就是限价委托)
|
||||
BuyType int `json:"buy_type"` // 买卖类型:1买,2卖
|
||||
TriggerDecide int `json:"trigger_decide"` // 触发条件 1按最新成交价格算,2按标记价格算
|
||||
IsReduce int `json:"is_reduce"` // 1是只减仓位(点击仓位列表中的平仓按钮),0正常
|
||||
Coin string `json:"coin"` // 交易币
|
||||
Price string `json:"price"` // 下单价格(限价+止盈止损时,该字段必填)
|
||||
Num string `json:"num"` // 下单数量(市价时该字段必填)
|
||||
TriggerPrice string `json:"trigger_price"` // 触发价格
|
||||
PositionSide string `json:"position_side"` // 持仓方向,单向持仓模式下可填both;在双向持仓模式下必填,且仅可选择 long 或 short
|
||||
}
|
||||
94
utils/aeshelper/aesoldhelper/aesoldhelper.go
Normal file
94
utils/aeshelper/aesoldhelper/aesoldhelper.go
Normal file
@ -0,0 +1,94 @@
|
||||
package aesoldhelper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func addBase64Padding(value string) string {
|
||||
m := len(value) % 4
|
||||
if m != 0 {
|
||||
value += strings.Repeat("=", 4-m)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func removeBase64Padding(value string) string {
|
||||
return strings.Replace(value, "=", "", -1)
|
||||
}
|
||||
|
||||
// Pad Pad
|
||||
func Pad(src []byte) []byte {
|
||||
padding := aes.BlockSize - len(src)%aes.BlockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padtext...)
|
||||
}
|
||||
|
||||
// Unpad Unpad
|
||||
func Unpad(src []byte) ([]byte, error) {
|
||||
length := len(src)
|
||||
unpadding := int(src[length-1])
|
||||
|
||||
if unpadding > length {
|
||||
return nil, errors.New("unpad error. This could happen when incorrect encryption key is used")
|
||||
}
|
||||
|
||||
return src[:(length - unpadding)], nil
|
||||
}
|
||||
|
||||
// AesEncrypt AesEncrypt
|
||||
func AesEncrypt(key []byte, text string) (string, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
msg := Pad([]byte(text))
|
||||
ciphertext := make([]byte, aes.BlockSize+len(msg))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cfb := cipher.NewCFBEncrypter(block, iv)
|
||||
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(msg))
|
||||
finalMsg := removeBase64Padding(base64.URLEncoding.EncodeToString(ciphertext))
|
||||
return finalMsg, nil
|
||||
}
|
||||
|
||||
// AesDecrypt AesDecrypt
|
||||
func AesDecrypt(key []byte, text string) (string, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
decodedMsg, err := base64.URLEncoding.DecodeString(addBase64Padding(text))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if (len(decodedMsg) % aes.BlockSize) != 0 {
|
||||
return "", errors.New("blocksize must be multipe of decoded message length")
|
||||
}
|
||||
|
||||
iv := decodedMsg[:aes.BlockSize]
|
||||
msg := decodedMsg[aes.BlockSize:]
|
||||
|
||||
cfb := cipher.NewCFBDecrypter(block, iv)
|
||||
cfb.XORKeyStream(msg, msg)
|
||||
|
||||
unpadMsg, err := Unpad(msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(unpadMsg), nil
|
||||
}
|
||||
19
utils/aeshelper/apikey.go
Normal file
19
utils/aeshelper/apikey.go
Normal file
@ -0,0 +1,19 @@
|
||||
package aeshelper
|
||||
|
||||
var (
|
||||
apiaeskey = "9jxFTkydwCJsmIA1TUrv"
|
||||
)
|
||||
|
||||
// EncryptApi 加密apikey
|
||||
func EncryptApi(apikey, secretkey string) (apikeyen, secrekeyen string) {
|
||||
apikeyen = Encrypt(apikey, apiaeskey)
|
||||
secrekeyen = Encrypt(secretkey, apiaeskey)
|
||||
return apikeyen, secrekeyen
|
||||
}
|
||||
|
||||
// DecryptApi 解密apikey
|
||||
func DecryptApi(apikeyen, secrekeyen string) (apikey, secrekey string) {
|
||||
apikey = Decrypt(apikeyen, apiaeskey)
|
||||
secrekey = Decrypt(secrekeyen, apiaeskey)
|
||||
return apikey, secrekey
|
||||
}
|
||||
113
utils/ethbalanceofhelper/baalanceof_helper.go
Normal file
113
utils/ethbalanceofhelper/baalanceof_helper.go
Normal file
@ -0,0 +1,113 @@
|
||||
package ethbalanceofhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-admin/abis"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// ERC-20 代币合约 ABI (部分,只包含 balanceOf)
|
||||
// ERC-20 代币合约 ABI (部分,只包含 balanceOf 和 decimals)
|
||||
const erc20ABI = `[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [ { "name": "_owner", "type": "address" } ],
|
||||
"name": "balanceOf",
|
||||
"outputs": [ { "name": "balance", "type": "uint256" } ],
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [ { "name": "", "type": "uint8" } ],
|
||||
"type": "function"
|
||||
}
|
||||
]`
|
||||
|
||||
// GetERC20Balance 查询 ERC-20 代币余额并转换为正常单位 (使用 decimal)
|
||||
func GetERC20Balance(client *ethclient.Client, tokenAbi abis.TokenABI, accountAddress string) (decimal.Decimal, error) {
|
||||
// 1. 解析 ABI
|
||||
contractABI, err := abi.JSON(strings.NewReader(erc20ABI))
|
||||
if err != nil {
|
||||
return decimal.Zero, fmt.Errorf("解析 ABI 失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 构造 balanceOf 函数调用数据
|
||||
balanceOfCallData, err := contractABI.Pack("balanceOf", common.HexToAddress(accountAddress))
|
||||
if err != nil {
|
||||
return decimal.Zero, fmt.Errorf("构造 balanceOf 调用数据失败: %w", err)
|
||||
}
|
||||
|
||||
address := common.HexToAddress(tokenAbi.TestAddress)
|
||||
// 3. 执行 balanceOf 调用
|
||||
balanceResult, err := client.CallContract(context.Background(), ethereum.CallMsg{
|
||||
To: &address,
|
||||
Data: balanceOfCallData,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return decimal.Zero, fmt.Errorf("调用 balanceOf 失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 解析 balanceOf 结果
|
||||
unpackedBalance, err := contractABI.Unpack("balanceOf", balanceResult)
|
||||
if err != nil {
|
||||
return decimal.Zero, fmt.Errorf("解析 balanceOf 结果失败: %w", err)
|
||||
}
|
||||
|
||||
balance, ok := unpackedBalance[0].(*big.Int)
|
||||
if !ok {
|
||||
return decimal.Zero, errors.New("解析 balanceOf 结果为 *big.Int 失败")
|
||||
}
|
||||
|
||||
// 8. 转换为正常单位 (使用 decimal)
|
||||
balanceDecimal := decimal.NewFromBigInt(balance, 0) // Create decimal from big.Int
|
||||
decimalFactor := decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(tokenAbi.Decimals)))
|
||||
readableBalance := balanceDecimal.Div(decimalFactor)
|
||||
|
||||
return readableBalance, nil
|
||||
}
|
||||
|
||||
// 初始化以太坊客户端连接代理
|
||||
func EthClientWithProxy(rawURL string, proxyURL string) (*ethclient.Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid URL: %v", err)
|
||||
}
|
||||
|
||||
if proxyURL == "" {
|
||||
return ethclient.Dial(rawURL)
|
||||
}
|
||||
|
||||
proxy, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid proxy URL: %v", err)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
dialOptions := []rpc.ClientOption{
|
||||
rpc.WithHTTPClient(httpClient),
|
||||
}
|
||||
rpcClient, err := rpc.DialOptions(context.Background(), u.String(), dialOptions...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial RPC client with options: %v", err)
|
||||
}
|
||||
|
||||
return ethclient.NewClient(rpcClient), nil
|
||||
}
|
||||
175
utils/ethtransferhelper/transfer_helper.go
Normal file
175
utils/ethtransferhelper/transfer_helper.go
Normal file
@ -0,0 +1,175 @@
|
||||
package ethtransferhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"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"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// 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 float64,
|
||||
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 := []byte("transfer(address,uint256)")
|
||||
hash := sha3.New256()
|
||||
hash.Write(transferFnSignature)
|
||||
methodID := hash.Sum(nil)[:4] // 取前 4 个字节作为方法 ID
|
||||
|
||||
// 7.2 填充接收者地址和转账金额
|
||||
paddedAddress := common.LeftPadBytes(toAddressCommon.Bytes(), 32)
|
||||
// 7.3 将代币数量转换为最小单位
|
||||
amountBigInt, err := convertTokenAmountToBigInt(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: &tokenAddressCommon,
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("估算 Gas 消耗失败: %w", err)
|
||||
}
|
||||
|
||||
// 9. 增加 Gas 限制的安全边际
|
||||
gasLimit = gasLimit * 11 / 10 // 增加 10%
|
||||
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 convertTokenAmountToBigInt(tokenAmount float64, tokenDecimals uint8) (*big.Int, error) {
|
||||
// 1. 使用最大精度格式化浮点数
|
||||
amountStr := fmt.Sprintf("%.18f", tokenAmount) // 使用最大精度
|
||||
|
||||
// 2. 找到小数点的位置
|
||||
decimalPointIndex := strings.Index(amountStr, ".")
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
return amountBigInt, nil
|
||||
}
|
||||
9
utils/stringhelper/address_helper.go
Normal file
9
utils/stringhelper/address_helper.go
Normal file
@ -0,0 +1,9 @@
|
||||
package stringhelper
|
||||
|
||||
func DesensitizeWalletAddress(address string) string {
|
||||
if len(address) < 10 { // 更严格的长度检查
|
||||
return "0x...invalid"
|
||||
}
|
||||
|
||||
return address[:6] + "..." + address[len(address)-4:]
|
||||
}
|
||||
Reference in New Issue
Block a user