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

交易哈希

代码链接

欢迎点赞、收藏、评论

相关推荐
花酒锄作田41 分钟前
MCP官方Go SDK尝鲜
golang·mcp
老姚---老姚8 小时前
在windows下编译go语言编写的dll库
开发语言·windows·golang
链上罗主任9 小时前
《以太坊十年:从概念验证到模块化架构》
去中心化·区块链·智能合约
henujolly13 小时前
what`s pos
区块链
bing.shao13 小时前
Golang 开发者视角:解读《“人工智能 + 制造” 专项行动》的技术落地机遇
人工智能·golang·制造
ONE_PUNCH_Ge13 小时前
Go 语言泛型
开发语言·后端·golang
傻小胖19 小时前
solana开发者训练营第1课:区块链基础
区块链
老蒋每日coding1 天前
从零构建 IPFS 上传流水线:NFT 元数据去中心化实战指南
区块链
qq_368019661 天前
区块链生态参与方概述
区块链
devmoon1 天前
Polkadot Hub 智能合约中的账户体系
web3·区块链·智能合约·polkadot