go语言从零实现以太坊客户端 - 区块链转账
- [1. 环境配置和合约代码](#1. 环境配置和合约代码)
- [2. 编写调试代码](#2. 编写调试代码)
-
- [2.1 基类,用于初始化常量](#2.1 基类,用于初始化常量)
- [2.2 核心业务类](#2.2 核心业务类)
- [2.3 JSON-RPC库](#2.3 JSON-RPC库)
- [2.4 组装传输对象](#2.4 组装传输对象)
- [2.5 工具类](#2.5 工具类)
- [2.6 主函数](#2.6 主函数)
- [3. 测试](#3. 测试)
-
- [3.1 hardhat node节点](#3.1 hardhat node节点)
- [3.2 sepolia节点](#3.2 sepolia节点)
1. 环境配置和合约代码
参考文章12. Hardhat编写、编译、部署、测试Solidity ERC20合约 - 进阶篇 - Metamask导入代币,转账ETH,转账代币
GoLand 2025.2.4版本
go 1.25.4
2. 编写调试代码
可连接hardhat node本地网络和sepolia测试网络
为了突出业务逻辑,省略了错误处理。
2.1 基类,用于初始化常量
后面文章中的类均继承BaseService类
go
package service
import "client/utils/jsonrpc"
type BaseService struct {
url string
contractAddr string
from string
to string
privateKey string
chainID uint64
}
func (this *BaseService) Init() {
this.url = "http://localhost:8545"
this.contractAddr = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
// hardhat 默认账户地址1
this.from = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
// hardhat 默认账户地址2
this.to = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
this.privateKey = 你自己的私钥
this.chainID = uint64(1337)
//this.url = "https://ethereum-sepolia-rpc.publicnode.com"
//this.contractAddr = "0x451Dc02Cee616361815253C858Df0a3028c42901"
//// sepolia 账户地址1
//this.from = "0xc312c07abEb8246510412a7ce87A295E0ceC5D48"
//// sepolia 账户地址1
//this.to = "0xE78Ff27498c9a6Fd8BC3ff8170Ecf9a13ECBE49e"
//this.privateKey = 你自己的私钥
//this.chainID = uint64(11155111)
//this.url = "https://webhook.site/fc1b1d2d-7dcd-4a2a-b653-16d8f2b9de0e"
jsonrpc.InitJsonRPCClient(this.url)
}
2.2 核心业务类
继承基类。区块链转账
go
package service
import (
"bytes"
"client/domain"
"client/utils"
"client/utils/jsonrpc"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
)
type BlockChainWrite struct {
BaseService
}
// SendTransaction hardhat node eth_sendTransaction十进制数字和16进制字符串都可以。sepolia不支持eth_sendTransaction
// 发送json对象
func (this *BlockChainWrite) SendTransaction(to string, value uint64) interface{} {
tx := domain.Transaction{
Nonce: utils.IntToHexStr(2),
From: this.from,
To: to,
Value: utils.IntToHexStr(value),
Gas: utils.IntToHexStr(21000),
GasPrice: utils.IntToHexStr(875000000),
Input: "0x",
}
toAddr := common.HexToAddress(to)
fromAddr := common.HexToAddress(this.from)
tx1 := domain.Transaction1{
BaseTransaction: domain.BaseTransaction{
Nonce: uint64(3),
GasPrice: big.NewInt(875000000),
Gas: uint64(21000),
To: &toAddr,
Value: big.NewInt(int64(value)),
Input: nil,
},
From: &fromAddr,
}
jsonrpc.SendJsonRPC("eth_sendTransaction", tx)
return jsonrpc.SendJsonRPC("eth_sendTransaction", tx1)
}
// SendRawTransaction 以太坊协议
// 发送json对象的值组成的数组
// {
// nonce, // 位置0 - 必须是非ce
// gasPrice, // 位置1 - 必须是gasPrice
// gas, // 位置2 - 必须是gasLimit
// to, // 位置3 - 必须是to地址
// value, // 位置4 - 必须是value
// data, // 位置5 - 必须是data/input
// v, // 位置6 - 必须是v
// r, // 位置7 - 必须是r
// s, // 位置8 - 必须是s
// }
func (this *BlockChainWrite) SendRawTransaction(value uint64) interface{} {
return jsonrpc.SendJsonRPC("eth_sendRawTransaction", this.GetRawTransaction(this.to, value, nil))
}
func (this *BlockChainWrite) GetRawTransaction(to string, value uint64, input []byte) string {
tx := this.sendRawTransaction(to, value, input)
dataSend := tx.GetRawTransaction()
// 内部将结构体中的字段,按结构体字段定义顺序取值,转换为数组。结构体中字段定义顺序要符合以太坊协议,但使用时结构体字段顺序可以不一致。
// 结构体字段要全部定义,不能继承,rlp.EncodeToBytes不会将基类字段展开到子类。但json.Marshal可以将基类字段展开到子类。
//txBytesStruct, _ := rlp.EncodeToBytes(tx)
txBytesArray, _ := rlp.EncodeToBytes(dataSend)
encodedArray := hexutil.Encode(txBytesArray)
return encodedArray
}
var nonce = uint64(297)
func (this *BlockChainWrite) sendRawTransaction(to string, value uint64, input []byte) domain.RawTransaction {
nonce += 1
toAddr := common.HexToAddress(to)
baseTransaction := domain.BaseTransaction{
Nonce: nonce,
GasPrice: big.NewInt(875000000),
Gas: uint64(210000),
// 结构体中字段定义顺序要符合以太坊协议,但使用时结构体字段顺序可以不一致。
To: &toAddr,
Value: big.NewInt(int64(value)),
Input: input,
}
tx := domain.RawTransaction{
BaseTransaction: baseTransaction,
}
dataSign := append(
baseTransaction.GetBaseTransaction(),
big.NewInt(int64(this.chainID)),
uint(0),
uint(0),
)
var buf bytes.Buffer
_ = rlp.Encode(&buf, dataSign)
rawTxHash := crypto.Keccak256Hash(buf.Bytes())
privateKey, _ := crypto.HexToECDSA(this.privateKey)
sig, _ := crypto.Sign(rawTxHash.Bytes(), privateKey)
tx.V = big.NewInt(int64(sig[64]+35) + int64(this.chainID*2))
tx.R = new(big.Int).SetBytes(sig[:32])
tx.S = new(big.Int).SetBytes(sig[32:64])
return tx
}
2.3 JSON-RPC库
使用JSON-RPC库github.com/ybbus/jsonrpc/v3,组装
go
{
jsonrpc: '2.0',
method: "eth_getBalance"
params: [this.from, "latest"]
id: 1
}
go
package jsonrpc
import (
_ "client/domain/decorator"
"context"
_ "github.com/dengsgo/go-decorator/decor"
"github.com/ybbus/jsonrpc/v3"
)
var JsonRPCClient jsonrpc.RPCClient
func InitJsonRPCClient(url string) {
JsonRPCClient = jsonrpc.NewClient(url)
}
func SendJsonRPC(funcName string, params ...interface{}) interface{} {
var rsp interface{}
err := JsonRPCClient.CallFor(context.Background(), &rsp, funcName, params)
if err != nil {
panic(err)
}
return rsp
}
2.4 组装传输对象
go
package domain
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
)
type BaseTransaction struct {
Nonce uint64 `json:"nonce"`
GasPrice *big.Int `json:"gasPrice"`
Gas uint64 `json:"gas"`
To *common.Address `json:"to"`
Value *big.Int `json:"value"`
Input []byte `json:"input"`
}
func (this *BaseTransaction) GetBaseTransaction() []any {
return []any{this.Nonce, this.GasPrice, this.Gas, this.To, this.Value, this.Input}
}
// RawTransaction 不需要from
// 节点操作:
// 1. 解析交易数据
// 2. 从签名 (v, r, s) 中恢复出 from 地址
// 3. 验证签名有效性
// 4. 广播交易
type RawTransaction struct {
BaseTransaction
V *big.Int `json:"v"`
R *big.Int `json:"r"`
S *big.Int `json:"s"`
}
func (this *RawTransaction) GetRawTransaction() []any {
return []any{this.Nonce, this.GasPrice, this.Gas, this.To, this.Value, this.Input, this.V, this.R, this.S}
}
// Transaction 需要from
// 节点操作:
// 1. 收到请求,看到 from: "0xabc..."
// 2. 查找节点中 "0xabc..." 对应的私钥
// 3. 用这个私钥签名交易数据
// 4. 返回签名后的原始交易:"0xf86c808504a817c8..."
// 5. 广播交易
type Transaction struct {
Nonce string `json:"nonce"`
GasPrice string `json:"gasPrice"`
Gas string `json:"gas"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Input string `json:"input"`
}
type Transaction1 struct {
BaseTransaction
From *common.Address `json:"from"`
}
2.5 工具类
工具类
go
package utils
import (
"encoding/hex"
"math/big"
"strconv"
"strings"
)
func HexStrToInt(hexStr string) *big.Int {
data, _ := new(big.Int).SetString(strings.TrimPrefix(hexStr, "0x"), 16)
return data
}
func WeiToETH(wei *big.Int) float64 {
eth, _ := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e18)).Float64()
return eth
}
func IntToHexStr(num uint64) string {
hexStr := "0x" + strconv.FormatUint(num, 16)
return hexStr
}
// HexStrToAscii 解析 ABI 编码的字符串
// 0x
// 0000000000000000000000000000000000000000000000000000000000000020 // 偏移量:32字节
// 0000000000000000000000000000000000000000000000000000000000000007 // 长度:7字节
// 4d79546f6b656e00000000000000000000000000000000000000000000000000 // 数据:"MyToken"
func HexStrToAscii(hexStr string) string {
hexStr = strings.TrimPrefix(hexStr, "0x")
// 第2个32字节是长度
lengthHex := hexStr[64:128]
length, _ := strconv.ParseUint(lengthHex, 16, 32)
// 提取字符串内容
start := 128
contentHex := hexStr[start : start+int(length)*2]
bytes, _ := hex.DecodeString(contentHex)
return string(bytes)
}
2.6 主函数
主函数
go
package main
import (
"fmt"
"client/ethereum.client.blockchain/service"
)
func main() {
blockchainWrite()
}
func blockchainWrite() {
blockChainWrite := service.BlockChainWrite{}
blockChainWrite.Init()
//_ = blockChainWrite.SendTransaction("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 1e18)
fmt.Println(blockChainWrite.SendRawTransaction(1e12))
}
3. 测试
3.1 hardhat node节点

交易哈希

3.2 sepolia节点
交易哈希

欢迎点赞、收藏、评论