以太坊账户余额查询原理与实现指南

·

账户状态与stateTrie结构

以太坊通过stateTrie数据结构管理所有账户的状态信息。Block.Header.Root即stateRoot,是一棵PMT树,存储了所有账户的最新状态数据,包括账户余额等关键信息。

路径(path)的计算方式为:sha3(ethereumAddress),而值(value)的存储格式为:rlp(ethereumAccount)。Root作为哈希值,通过它可以找到stateTrie的根节点,再结合sha3(ethereumAddress)生成的路径,就能逐步定位到每个账户的rlp(ethereumAccount)数据。

账户结构解析

以太坊账户分为两种类型:普通账户余额和合约代币余额。账户的基本结构如下:

type Account struct {
    Nonce    uint64      // 账户发起交易的次数
    Balance  *big.Int    // 该账户的余额
    Root     common.Hash // 存储树MPT,存储所有合约数据
    CodeHash []byte      // 合约代码的Hash值(仅对合约账户有效)
}

每个用户对应一个StateObject,代表着stateTrie中的位置,反映了账户的动态变化结果:

type stateObject struct {
    address  common.Address
    addrHash common.Hash // 以太坊地址的哈希值
    data     Account
    db       *StateDB
}

余额查询实现原理

代码执行思路

查询余额的基本逻辑包含以下几个步骤:

查询流程详解

完整的余额查询流程包括:

  1. 查询获取当前最新区块,得到lastBlock.header.Root
  2. 先在本地缓存查找stateObject热点数据,若无则根据Root定位数据库中的根节点
  3. 按照Address在MPT树中的排列结构,找到对应的stateObject
  4. 通过stateObject获取对应的Account信息
  5. 从Account中提取余额数据

获取账户余额实战

代码解析与实现

读取账户余额非常简单。调用客户端的BalanceAt方法,传入账户地址和可选的区块号参数。将区块号设置为nil将返回最新余额。

account := common.HexToAddress("0x71c7656ec7ab88b098defb751b7401b5f6d8976f")
balance, err := client.BalanceAt(context.Background(), account, nil)
if err != nil {
    log.Fatal(err)
}
fmt.Println(balance) // 输出:25893180161173005034

传入区块号可以查询历史余额,区块号必须为big.Int类型:

blockNumber := big.NewInt(5532993)
balance, err := client.BalanceAt(context.Background(), account, blockNumber)
if err != nil {
    log.Fatal(err)
}
fmt.Println(balance) // 输出:25729324269165216042

单位转换技巧

以太坊使用最小单位wei处理数字,因为它们是定点精度。要将wei转换为ETH,需要除以10^18。由于处理的是大数,需要导入math和math/big包:

fbalance := new(big.Float)
fbalance.SetString(balance.String())
ethValue := new(big.Float).Quo(fbalance, big.NewFloat(math.Pow10(18)))
fmt.Println(ethValue) // 输出:25.729324269165216041

待处理余额查询

有时需要查询待确认的账户余额,例如在提交交易后等待确认时。客户端提供了PendingBalanceAt方法,用法与BalanceAt类似:

pendingBalance, err := client.PendingBalanceAt(context.Background(), account)
fmt.Println(pendingBalance) // 输出:25729324269165216042

完整代码示例

package main

import (
    "context"
    "fmt"
    "log"
    "math"
    "math/big"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
    client, err := ethclient.Dial("https://mainnet.infura.io")
    if err != nil {
        log.Fatal(err)
    }

    account := common.HexToAddress("0x71c7656ec7ab88b098defb751b7401b5f6d8976f")
    balance, err := client.BalanceAt(context.Background(), account, nil)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(balance) // 25893180161173005034

    blockNumber := big.NewInt(5532993)
    balanceAt, err := client.BalanceAt(context.Background(), account, blockNumber)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(balanceAt) // 25729324269165216042

    fbalance := new(big.Float)
    fbalance.SetString(balanceAt.String())
    ethValue := new(big.Float).Quo(fbalance, big.NewFloat(math.Pow10(18)))
    fmt.Println(ethValue) // 25.729324269165216041

    pendingBalance, err := client.PendingBalanceAt(context.Background(), account)
    fmt.Println(pendingBalance) // 25729324269165216042
}

获取账户代币余额

参数获取方法

获取代币余额需要from、to和data参数:

func (rc *RequestChain) GetCallContractInfo(from, to, data string) (string, error) {
    info, err := rc.Client.CallContract(from, to, data)
    if err != nil {
        logger.Error("GetCallContractInfo", "step", "CallContract", "err", err.Error())
        return "", err
    }
    res, err := common.HexToString(info.(string))
    if err != nil {
        logger.Error("GetCallContractInfo", "step", "HexToString", "err", err.Error())
        return "", err
    }
    resByte, err := hex.DecodeString(res)
    return string(resByte), nil
}

以太坊RPC调用

通过调用以太坊RPC接口查询合约余额:

// CallContract 查询合约
func (eth *Http) CallContract(from, to, data string) (interface{}, error) {
    tag := "latest"
    args = []interface{}{CallMsg{
        From: from,
        To:   to,
        Data: data,
    }, tag}
    params := NewHttpParams("eth_call", args)
    resBody, err := eth.rpc.HttpRequest(params)
    if err != nil {
        return nil, err
    }
    return eth.ParseJsonRPCResponse(resBody)
}

👉 查看实时余额查询工具

常见问题

什么是以太坊stateTrie?

stateTrie是以太坊中存储所有账户状态的数据结构,采用PMT树形式组织。它包含了每个账户的余额、nonce、合约存储根和代码哈希等信息,通过区块头中的stateRoot可以访问到特定区块时的全局状态。

如何查询历史区块的账户余额?

使用BalanceAt方法时,除了传入账户地址外,还需要指定要查询的区块号。区块号需要转换为big.Int类型,这样可以获取到该区块确认时的账户余额状态。

待处理余额与确认余额有何区别?

待处理余额(PendingBalanceAt)反映的是包含尚未被区块确认的交易后的余额,而确认余额(BalanceAt)只计算已经被区块确认的交易。在交易等待期间,查询待处理余额可以预估交易完成后的账户状态。

如何正确转换wei和ETH单位?

以太坊使用wei作为最小单位,1 ETH = 10^18 wei。转换时需要使