4. go语言从零实现以太坊客户端 - 区块链转账

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节点

交易哈希

代码链接

欢迎点赞、收藏、评论

相关推荐
Web3李李14 小时前
链游想要获得市场认可,核心从来不是极致去中心化
web3·去中心化·区块链·软件开发·链游开发
圣殿骑士-Khtangc15 小时前
Go错误处理最佳实践进阶从error到panic的完整指南
golang
运维开发笔记16 小时前
3.6 Go defer 语句学习笔记
golang
mykj155117 小时前
稳定的DOO算力质押系统DAPP合约底层逻辑
web3·区块链·dapp·rwa·质押挖矿
FfHUCisI17 小时前
Golang HTTP 路由设计与请求处理
开发语言·http·golang
存在morning17 小时前
【Python 开发实践 一】Python vs Go vs Java 三门语言对比
java·python·golang
大鱼>1 天前
以太坊Layer2扩容:Optimistic vs ZK-Rollup完整对比
区块链
圣殿骑士-Khtangc1 天前
Go字符串高效拼接性能对比与底层原理分析
服务器·前端·golang
汇策研习社1 天前
斐波那契均线交易体系:21/55/89三重均线趋势战法详解
大数据·经验分享·金融·区块链·fastbull
同花顺期货通1 天前
期货新手入门品种筛选:2026年低保证金高流动性品种深度解析
区块链