假设您已连接客户端,,下⼀步就是加载您的私钥。
privateKey, err := crypto.HexToECDSA("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") if err != nil {
log.Fatal(err) }
之后我们需要获得帐户的随机数(nonce)。 每笔交易都需要⼀个nonce。 根据定义,nonce是仅使⽤⼀ 次的数字。 如果是发送交易的新帐户,则该随机数将为“0”。 来⾃帐户的每个新事务都必须具有前⼀个 nonce增加1的nonce。很难对所有nonce进⾏⼿动跟踪,于是ethereum客户端提供⼀个帮助⽅ 法 PendingNonceAt ,它将返回你应该使⽤的下⼀个nonce。 该函数需要我们发送的帐户的公共地址 - 这个我们可以从私钥派⽣。
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
接下来我们可以读取我们应该⽤于帐户交易的随机数。
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
代币传输不需要传输ETH,因此将交易“值”设置为“0”。
value := big.NewInt(0)
先将您要发送代币的地址存储在变量中。
toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
现在轮到有趣的部分。 我们需要弄清楚交易的 data 部分。 这意味着我们需要找出我们将要调⽤的智 能合约函数名,以及函数将接收的输⼊。 然后我们使⽤函数名的keccak-256哈希来检索 ⽅法ID,它是 前8个字符(4个字节)。 然后,我们附加我们发送的地址,并附加我们打算转账的代币数量。 这些输 ⼊需要256位⻓(32字节)并填充左侧。 ⽅法ID不需填充。
下述的代币地址是我私有链发行的代币地址,具体如何发行代币,我其他的帖子中有介绍
tokenAddress := common.HexToAddress(“xxxxxx”)
函数名将是传递函数的名称,即ERC-20规范中的 transfer 和参数类型。 第⼀个参数类型 是 address (令牌的接收者),第⼆个类型是 uint256 (要发送的代币数量)。 不需要没有空格和参 数名称。 我们还需要⽤字节切⽚格式。
transferFnSignature := []byte("transfer(address,uint256)")
我们现在将从go-ethereum导⼊ crypto/sha3 包以⽣成函数签名的Keccak256哈希。 然后我们只使⽤ 前4个字节来获取⽅法ID。
hash := sha3.NewKeccak256()
hash.Write(transferFnSignature)
methodID := hash.Sum(nil)[:4]
fmt.Println(hexutil.Encode(methodID))
接下来,我们需要将给我们发送代币的地址左填充到32字节。
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
fmt.Println(hexutil.Encode(paddedAddress))
接下来我们确定要发送多少个代币,在这个例⼦⾥是1,000个,并且我们需要在 big.Int 中格式化为 wei。
amount := new(big.Int)
amount.SetString("1000000000000000000000", 10)
代币量也需要左填充到32个字节。
paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
fmt.Println(hexutil.Encode(paddedAmount))
接下来我们只需将⽅法ID,填充后的地址和填后的转账量,接到将成为我们数据字段的字节⽚。
var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)
燃⽓上限制将取决于交易数据的⼤⼩和智能合约必须执⾏的计算步骤。 幸运的是,客户端提供 了 EstimateGas ⽅法,它可以为我们估算所需的燃⽓量。 这个函数从 ethereum 包中获取 CallMsg 结 构,我们在其中指定数据和地址。 它将返回我们估算的完成交易所需的估计燃⽓上限
gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
From:fromAddress,
To: &tokenAddress,
GasPrice:gasPrice,
Data: data,
})
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
接下来我们需要做的是构建交易事务类型,这类似于您在ETH转账部分中看到的,除了to字段将是代 币智能合约地址。 这个常让⼈困惑。我们还必须在调⽤中包含0 ETH的值字段和刚刚⽣成的数据字 节
//构建交易
tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
下⼀步是使⽤发件⼈的私钥对事务进⾏签名。 SignTx ⽅法需要EIP155签名器(EIP155 signer),这需 要我们从客户端拿到链ID。
//获取链id
chainID, err :=client.ChainID(context.Background())
if err != nil {
log.Fatal(err)
}
//签名
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
最后⼴播交易。
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
transactionHash := signedTx.Hash().Hex()
全部代码
这个代码是我外部接口调用的需要自行修改
/*
@Time : 2020/8/10 10:51
@Author : xd
@File : main
@Software: GoLand
*/
package main
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"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/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/os/glog"
"github.com/gogf/gf/util/gconv"
"github.com/gogf/gf/util/gvalid"
"github.com/shopspring/decimal"
"golang.org/x/crypto/sha3"
token "hyzz/Token"
"math"
"math/big"
"time"
)
func tryCacth() {
//捕获程序运行异常,防止程序意外退出
if err := recover(); err != nil {
time.Sleep(time.Second) //休眠一秒
main() //重启主方法
}
}
func MiddlewareCORS(r *ghttp.Request) {
r.Response.CORSDefault()
r.Middleware.Next()
}
func main() {
defer tryCacth()
s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(MiddlewareCORS)
group.ALL("/",ContractTransaction)
})
s.SetPort(g.Cfg().GetInt("port",8080))
s.Run()
}
type RegisterReq struct {
PrivateKey string `p:"private_key" v:"required#private_key not found"`
ToAddress string `json:"to_address" p:"to_address" v:"required#to_address not found"`
TokenAddress string `json:"token_address" p:"token_address" v:"required#token_address not found"`
Amount string `json:"amount" p:"amount" v:"required#amount not found"`
}
type RegisterRes struct {
Code int `json:"code"`
Error string `json:"error"`
Data interface{
} `json:"data"`
}
func ContractTransaction(r *ghttp.Request) {
var req *RegisterReq
if err := r.Parse(&req); err != nil {
// Validation error.
if v, ok := err.(*gvalid.Error); ok {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: v.FirstString(),
})
}
// Other error.
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
client, err := ethclient.Dial(g.Cfg().GetString("ethclient","http://127.0.0.1:8545"))
defer client.Close()
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
//加载私钥
privateKey, err := crypto.HexToECDSA(r.GetString("private_key"))
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
//发送的的帐户的公共地址 -从私钥派⽣
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: errors.New("cannot assert type: publicKey is not of type *ecdsa.PublicKey").Error(),
})
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
//获取用于帐户交易的随机数。
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
//因为是合约转账所以设置成0
value := big.NewInt(0) // in wei (0 eth)
//手续费价格
gasPrice := big.NewInt(0)
if gasP := r.GetVar("gas_price"); !gasP.IsEmpty(){
gasPrice = big.NewInt(gasP.Int64())
}else {
gasPrice, err = client.SuggestGasPrice(context.Background())
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
}
//接收地址
toAddress := common.HexToAddress(fmt.Sprintf("0x%s",r.GetString("to_address")[2:]))
//合约地址
tokenAddress := common.HexToAddress(fmt.Sprintf("0x%s",r.GetString("token_address")[2:]))
//address (令牌的接收者) uint256 (要发送的代币数量
transferFnSignature := []byte("transfer(address,uint256)")
hash := sha3.NewLegacyKeccak256()
hash.Write(transferFnSignature)
methodID := hash.Sum(nil)[:4] //获取⽅法ID
//fmt.Println(hexutil.Encode(methodID)) // 0xa9059cbb
//左填充32个字节
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
//要发送多少个代币
amount := new(big.Int)
xsdws := ContractAnalysis(fmt.Sprintf("0x%s",r.GetString("token_address")[2:]))
chengshu := decimal.NewFromFloat(math.Pow10(xsdws))
money,err := decimal.NewFromString(r.GetString("amount"))
if err != nil {
panic(err)
}
money = money.Mul(chengshu) //转换为真实转账数量
amount.SetString(money.String(), 10) // sets the value to 1000 tokens, in the token denomination
paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)
//手续费上限
var gasLimit uint64
if limit := r.GetVar("gas_limit"); limit.IsEmpty() {
gasLimit = limit.Uint64()
}else {
gasLimit, err = client.EstimateGas(context.Background(), ethereum.CallMsg{
From:fromAddress,
To: &tokenAddress,
GasPrice:gasPrice,
Data: data,
})
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
}
//构建交易
tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
//获取链id
chainID, err :=client.ChainID(context.Background())
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
//签名
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
//广播交易
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
transactionHash := signedTx.Hash().Hex()
glog.Info(r.GetString("to_address")," 代币token ",r.GetString("token_address"),"成功到账",money.String(),"交易hash:",transactionHash)
r.Response.WriteJsonExit(RegisterRes{
Code: 0,
Error: "",
Data:transactionHash,
})
return
}
var decimalscache = gcache.New()
//获取小数点位数
func ContractAnalysis(address string) int {
address = fmt.Sprintf("0x%s", address[2:])
if value := decimalscache.GetVar(address); !value.IsEmpty() {
return value.Int()
}
client, err := ethclient.Dial(g.Cfg().GetString("ethclient","http://127.0.0.1:8545"))
//defer client.ETH.Close()
tokenAddress := common.HexToAddress(address)
instance, err := token.NewToken(tokenAddress, client)
if err != nil {
panic(err)
}
callopts := new(bind.CallOpts)
Decimals, err := instance.Decimals(callopts) //小数点位数
if err != nil {
glog.Debug(err)
}
decimalscache.Set(address,gconv.Int(Decimals),0)
return gconv.Int(Decimals)
}