使用Go语言实现ERC-20代币转账的完整指南

·

在以太坊区块链开发中,除了ETH转账外,ERC-20代币的转移也是常见需求。本文将详细介绍如何使用Go语言和go-ethereum库实现ERC-20代币的安全转账。

前置准备

在开始代币转账前,请确保已完成以下准备:

若对这些基础操作不熟悉,建议先掌握以太币转账的基本流程。

代币转账核心概念

与ETH转账不同,ERC-20代币转账不需要实际发送ETH,因此交易中的"value"字段应设置为0:

value := big.NewInt(0)

代币转账的本质是通过调用代币合约的transfer函数来实现的,这需要在交易的data字段中编码方法调用信息。

构建交易数据

设置接收方地址

首先定义代币接收方地址:

toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")

确定代币合约地址

每种ERC-20代币都有唯一的合约地址:

tokenAddress := common.HexToAddress("0x28b149020d2152179873ec60bed6bf7cd705775d")

生成方法ID

方法ID是函数签名的Keccak-256哈希的前4个字节:

transferFnSignature := []byte("transfer(address,uint256)")
hash := sha3.NewKeccak256()
hash.Write(transferFnSignature)
methodID := hash.Sum(nil)[:4]

格式化参数

接收方地址和代币数量都需要左填充到32字节:

paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)

amount := new(big.Int)
amount.SetString("1000000000000000000000", 10) // 1000个代币
paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)

组装数据字段

将方法ID和参数组合成完整的数据字段:

var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)

估算和设置燃气限制

使用客户端的EstimateGas方法估算所需燃气:

gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
    To:   &toAddress,
    Data: data,
})

构建和发送交易

创建交易对象时,关键点是to字段应设置为代币合约地址,而不是接收方地址:

tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)

使用EIP155签名器对交易进行签名:

chainID, err := client.NetworkID(context.Background())
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)

最后广播交易到网络:

err = client.SendTransaction(context.Background(), signedTx)

👉 查看实时交易状态工具 可以帮助您监控交易确认进度。

完整代码示例

package main

import (
    "context"
    "crypto/ecdsa"
    "fmt"
    "log"
    "math/big"
    
    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/crypto/sha3"
    "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
    client, err := ethclient.Dial("https://rinkeby.infura.io")
    if err != nil {
        log.Fatal(err)
    }
    
    privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
    if err != nil {
        log.Fatal(err)
    }
    
    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)
    }
    
    value := big.NewInt(0) // 0 ETH
    gasPrice, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    
    toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
    tokenAddress := common.HexToAddress("0x28b149020d2152179873ec60bed6bf7cd705775d")
    
    transferFnSignature := []byte("transfer(address,uint256)")
    hash := sha3.NewKeccak256()
    hash.Write(transferFnSignature)
    methodID := hash.Sum(nil)[:4]
    
    paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
    
    amount := new(big.Int)
    amount.SetString("1000000000000000000000", 10) // 1000个代币
    paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
    
    var data []byte
    data = append(data, methodID...)
    data = append(data, paddedAddress...)
    data = append(data, paddedAmount...)
    
    gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
        To:   &toAddress,
        Data: data,
    })
    if err != nil {
        log.Fatal(err)
    }
    
    tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
    
    chainID, err := client.NetworkID(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 {
        log.Fatal(err)
    }
    
    fmt.Printf("tx sent: %s", signedTx.Hash().Hex())
}

常见问题

ERC-20代币转账与ETH转账有何不同?

ETH转账直接通过交易的价值字段完成,而ERC-20代币转账是通过调用代币合约的transfer函数实现的,交易的价值字段设为0,实际转账信息编码在数据字段中。

为什么需要估算燃气限制?

因为代币转账涉及智能合约执行,不同合约的复杂程度不同,需要的计算资源也不同。准确估算燃气可以避免交易因燃气不足而失败,同时避免支付过多燃气费用。

如何处理代币小数位数?

ERC-20代币通常有18位小数,但在处理时需要查询具体代币的小数位数。转账数量应以最小单位(类似wei)表示,例如1000个代币需要转换为1000 * 10^18。

交易发送后如何确认?

交易发送后会返回交易哈希,可以使用区块链浏览器通过该哈希查询交易状态。交易需要经过网络确认后才算完成,确认时间取决于网络拥堵程度和设置的燃气价格。

👉 获取进阶开发技巧 可以深入了解更多以太坊开发高级话题。

如果交易失败怎么办?

交易可能因多种原因失败:燃气不足、余额不足、合约错误等。失败的交易仍然会被记录在区块链上,且燃气费用不会被退还。建议在发送前仔细检查所有参数。

如何保证私钥安全?

私钥安全至关重要。在开发环境中,应使用测试网络和测试私钥。生产环境中必须使用安全的密钥管理方案,如硬件钱包或专业的密钥管理服务,避免将私钥硬编码在代码中。