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 工具类)
- [2.7 主函数](#2.7 主函数)
- [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类
clike
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:decor decorator.LogJsonRPC是装饰器,用来做返回值类型转换。以太坊JSON-RPC返回的数据是十六进制字符串,要转换为对应的类型
clike
package service
import (
_ "client/domain/decorator"
"client/utils/jsonrpc"
_ "github.com/dengsgo/go-decorator/decor"
)
type ContractRead struct {
BaseService
}
func (this *ContractRead) GetName() interface{} {
return jsonrpc.SendJsonRPCContract("eth_call", this.contractAddr, "name()", nil)
}
func (this *ContractRead) GetBalance() interface{} {
return jsonrpc.SendJsonRPCContract("eth_call", this.contractAddr, "balanceOf(address)", this.from)
}
2.3 JSON-RPC库
使用JSON-RPC库github.com/ybbus/jsonrpc/v3,组装
clike
{
jsonrpc: '2.0',
method: "eth_getBalance"
params: [this.from, "latest"]
id: 1
}
clike
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 组装传输对象
clike
package jsonrpc
import (
_ "client/domain/decorator"
"client/utils"
_ "github.com/dengsgo/go-decorator/decor"
)
// SendJsonRPCContract
// eth_call
// 1. Object - The transaction call object
// from: DATA, 20 Bytes - (optional) The address the transaction is sent from.
// to: DATA, 20 Bytes - The address the transaction is directed to.
// gas: QUANTITY - (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions.
// gasPrice: QUANTITY - (optional) Integer of the gasPrice used for each paid gas
// value: QUANTITY - (optional) Integer of the value sent with this transaction
// input: DATA - (optional) Hash of the method signature and encoded parameters.
// 2. QUANTITY|TAG - integer block number, or the string "latest", "earliest", "pending", "safe" or "finalized", see the block parameter
//
//go:decor decorator.LogJsonRPC
func SendJsonRPCContract(chainFuncName string, contractAddr string, contractFuncName string, contractParams ...interface{}) interface{} {
jsonRpcMethod := chainFuncName
rsp := interface{}(nil)
if chainFuncName == "eth_call" {
JsonRpcParams := utils.GetContractParamsRead(contractAddr, contractFuncName, contractParams)
rsp = SendJsonRPC(jsonRpcMethod, JsonRpcParams, "latest")
} else if chainFuncName == "eth_sendTransaction" {
JsonRpcParams := utils.GetContractParamsRead(contractAddr, contractFuncName, contractParams)
rsp = SendJsonRPC(jsonRpcMethod, JsonRpcParams)
} else if chainFuncName == "eth_sendRawTransaction" {
//input := JsonRpcParams["input"]
//blockChainWrite := service.BlockChainWrite{}
//blockChainWrite.Init()
//encodedArray := blockChainWrite.GetRawTransaction(contractAddr, 0, input.(string))
//rsp = SendJsonRPC(jsonRpcMethod, encodedArray)
}
return rsp
}
2.5 装饰器
以太坊的JSON-RPC返回的值是十六进制字符串,需要转换为对应的类型。为了分离非业务代码和业务代码,架构层面分层清晰,这里统一使用装饰器做返回值转换,这里选择了"github.com/dengsgo/go-decorator/decor"开源框架,只需要在被装饰的类上加//go:decor decorator.LogJsonRPC即可。
clike
package decorator
import (
"client/utils"
"github.com/dengsgo/go-decorator/decor"
)
func LogJsonRPC(ctx *decor.Context) {
if ctx.Func != nil {
ctx.Func()
if ctx.TargetOut == nil || len(ctx.TargetOut) == 0 {
return
}
switch ctx.TargetIn[0] {
case "eth_call":
switch ctx.TargetIn[2] {
case "name()":
ctx.TargetOut[0] = utils.HexStrToAscii(ctx.TargetOut[0].(string))
case "balanceOf(address)":
wei := utils.HexStrToInt(ctx.TargetOut[0].(string))
ctx.TargetOut[0] = utils.WeiToETH(wei)
}
case "eth_sendTransaction":
switch ctx.TargetIn[2] {
case "transfer(address,uint256)":
//ctx.TargetOut[0] = ctx.TargetOut[0].(string)
}
}
}
}
2.6 工具类
工具类
clike
package utils
import (
"encoding/hex"
"fmt"
"math/big"
"strconv"
"strings"
"golang.org/x/crypto/sha3"
)
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
}
// 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)
}
func GetContractParamsRead(contactAddr string, funcNameContract string, params []interface{}) map[string]any {
hash := sha3.NewLegacyKeccak256()
hash.Write([]byte(funcNameContract))
hashBytes := hash.Sum(nil)
// 2. 取前4个字节(8个十六进制字符)
selector := hashBytes[:4]
// 3. 转换为十六进制字符串(带 0x 前缀)
funcABI := "0x" + hex.EncodeToString(selector)
if params != nil && params[0] != nil && len(params) > 0 {
for _, param := range params {
switch param.(type) {
case string:
funcABI += fmt.Sprintf("%064s", strings.ToLower(strings.TrimPrefix(param.(string), "0x")))
case uint64:
funcABI += fmt.Sprintf("%064x", param)
}
}
}
return map[string]any{
"to": contactAddr,
"input": funcABI,
}
}
2.7 主函数
主函数
clike
package main
import (
"fmt"
"client/ethereum.client.blockchain/service"
)
func main() {
contractRead()
}
func contractRead() {
contractRead := service.ContractRead{}
contractRead.Init()
fmt.Println(contractRead.GetName())
fmt.Println(contractRead.GetBalance())
}
3. 测试
3.1 hardhat node节点


3.2 sepolia节点

欢迎点赞、收藏、评论