区块链钱包开发(十三)—— 构建BlockTracker

概述

我们这一节介绍下钱包开发中必不可少的工具 BlockTracker。在钱包开发中,实时感知区块链状态变化(如新块产生)对于账户余额、交易状态等功能至关重要,BlockTracker 正是为此而生。

源码:github.com/MetaMask/et...


BlockTracker 是什么

BlockTracker 是一个区块链区块追踪器,主要负责:

  • 实时监控区块链状态:持续追踪最新的区块信息
  • 提供区块数据:获取当前区块号和最新区块号
  • 事件通知:当新区块产生时发出事件通知
  • 状态管理:维护区块状态和轮询机制

如何使用

适合 provider 不支持订阅的环境

以太坊节点的 provider 有两种主流类型:

  • 支持订阅(Subscription) :如 WebSocket provider,可以直接订阅 newHeads,一有新区块就推送通知。
  • 不支持订阅 :如 HTTP provider,只能通过轮询(polling)不断请求 eth_blockNumber 来获取新区块。

PollingBlockTracker 的设计初衷:

  • 适合 HTTP provider,因为 HTTP 不能主动推送新区块,只能靠定时请求。
  • 如果用 WebSocket provider,推荐用事件订阅(如 eth_subscribe),这样更高效、实时。

举例说明:

  • 你用 Infura 的 HTTP RPC 地址(如 https://mainnet.infura.io/v3/xxx),只能用 PollingBlockTracker。
  • 你用 WebSocket 地址(如 wss://mainnet.infura.io/ws/v3/xxx),可以用 SubscriptionBlockTracker(订阅型)。

网络控制器中的使用

  • 为每个网络客户端创建独立的区块追踪器
  • 提供网络状态监控能力
  • 支持多链环境下的区块追踪

交易控制器中的使用

  • 监控交易状态变化
  • 检测交易确认情况
  • 触发交易状态更新

账户追踪器中的使用

  • 监听新区块事件
  • 更新账户余额和状态
  • 同步账户信息

BlockTracker 实现机制

核心接口

js 复制代码
export type BlockTracker = SafeEventEmitter & {
  destroy(): Promise<void>;           // 销毁追踪器
  isRunning(): boolean;               // 检查是否正在运行
  getCurrentBlock(): string | null;   // 获取当前区块号
  getLatestBlock(): Promise<string>;  // 获取最新区块号
  checkForLatestBlock(): Promise<string>; // 检查最新区块
};

主要属性

  • _isRunning:是否正在轮询
  • _currentBlock:当前已知的最新区块号
  • _provider:用于发起 JSON-RPC 请求的 provider
  • _pollingInterval:轮询间隔(默认20秒)
  • _retryTimeout:出错时的重试间隔
  • _blockResetDuration:区块号失效的超时时间
  • _usePastBlocks:是否允许回退到较旧区块
  • #internalEventListeners:内部事件监听器数组
  • #pendingLatestBlock / #pendingFetch:用于处理并发请求的延迟 Promise

关键方法与流程

伪代码

js 复制代码
class PollingBlockTracker:
    初始化(opts):
        provider = opts.provider
        pollingInterval = opts.pollingInterval or 20秒
        retryTimeout = opts.retryTimeout or pollingInterval/10
        blockResetDuration = opts.blockResetDuration or 20秒
        usePastBlocks = opts.usePastBlocks or false
        currentBlock = null
        isRunning = false
        this.on('newListener', this.onNewListener);
        this.on('removeListener', this.onRemoveListener);

    onNewListener(eventName):
        if eventName in ['sync', 'latest']:
            maybeStart()

    onRemoveListener():
        if 没有任何监听器:
            maybeEnd()

    maybeStart():
        if isRunning: return
        isRunning = true
        取消区块失效定时器
        start()
        触发 '_started' 事件

    maybeEnd():
        if not isRunning: return
        isRunning = false
        设置区块失效定时器
        end()
        触发 '_ended' 事件

    start():
        调用 updateAndQueue()  // 启动轮询主循环

    end():
        清除轮询定时器

    updateAndQueue():
        interval = pollingInterval
        try:
            updateLatestBlock()
        except error:
            触发 'error' 事件
            interval = retryTimeout
        if not isRunning: return
        清除轮询定时器
        设置定时器 interval 后再次调用 updateAndQueue()
        触发 '_waitingForNextIteration' 事件

    updateLatestBlock():
        latestBlock = fetchLatestBlock()
        newPotentialLatest(latestBlock)

    fetchLatestBlock():
        如果有 pendingFetch:
            return pendingFetch.promise
        创建新的 deferredPromise
        try:
            result = provider.request('eth_blockNumber')
            resolve(result)
            return result
        except error:
            reject(error)
            rejectPendingLatestBlock(error)
            抛出 error
        finally:
            pendingFetch = undefined

    newPotentialLatest(newBlock):
        if shouldUseNewBlock(newBlock):
            setCurrentBlock(newBlock)

    shouldUseNewBlock(newBlock):
        if currentBlock == null: return true
        if usePastBlocks and newBlock < currentBlock: return true
        if newBlock > currentBlock: return true
        return false

    setCurrentBlock(newBlock):
        oldBlock = currentBlock
        currentBlock = newBlock
        触发 'latest' 事件(newBlock)
        触发 'sync' 事件({oldBlock, newBlock})

    setupBlockResetTimeout():
        清除区块失效定时器
        设置定时器 blockResetDuration 后调用 resetCurrentBlock()

    resetCurrentBlock():
        currentBlock = null

    getLatestBlock():
        if currentBlock: return currentBlock
        if pendingLatestBlock: return pendingLatestBlock.promise
        创建新的 deferredPromise
        if not isRunning:
            latestBlock = fetchLatestBlock()
            newPotentialLatest(latestBlock)
            resolve(latestBlock)
            return latestBlock
        else:
            添加 internalListener
            once('latest', internalListener)
            return promise

    destroy():
        取消区块失效定时器
        maybeEnd()
        移除所有监听器
        rejectPendingLatestBlock('Block tracker destroyed')

监听器驱动的自动启动/停止

  • _onNewListener:当有监听器监听 synclatest 事件时自动启动轮询
  • _onRemoveListener:当所有监听器移除后自动停止轮询

轮询主循环

flowchart TD A[有监听器] --> B[启动 _maybeStart] B --> C[设置 _isRunning = true] C --> D[调用 _start] D --> E[执行 _updateAndQueue] E --> F[调用 _updateLatestBlock] F --> G[调用 _fetchLatestBlock] G --> H{获取成功?} H -- 是 --> I[更新 _currentBlock, 触发 latest/sync 事件] H -- 否 --> J[触发 error 事件, 设置重试间隔] I & J --> K[setTimeout 下一轮 _updateAndQueue] K --> L{_isRunning?} L -- 是 --> F L -- 否 --> M[停止]

区块获取与事件触发

  • _fetchLatestBlock:通过 provider 发起 eth_blockNumber 请求,支持跳过缓存
  • _newPotentialLatest:判断新获取的区块号是否比当前新,若是则更新并触发事件
  • _setCurrentBlock:设置当前区块号,并触发 latestsync 事件

并发与缓存处理

  • #pendingLatestBlock / #pendingFetch:防止并发请求导致重复 RPC 调用
  • getLatestBlock:如果已有区块号直接返回,否则等待最新区块事件或主动拉取

区块失效与重置

  • _setupBlockResetTimeout / cancelBlockResetTimeout:在停止轮询后,定时清空当前区块号,防止数据过时

学习交流请添加 vx: gh313061

下期预告:构建 NonceTracker

相关推荐
运维开发王义杰5 小时前
Ethereum: Uniswap V3核心”Tick”如何引爆DEX的流动性革命?
web3·区块链·智能合约
牧天白衣.21 小时前
区块链基础之Merkle B+树
学习·区块链
终端域名1 天前
区块链:重构信任的价值互联网革命
重构·区块链
终端域名1 天前
信用机制的发展与货币演进
区块链
选择不变1 天前
反阶持仓筹码副图指标,三红做多持股技术及指标案例
区块链·炒股技巧·短线指标·炒股指标·翻倍密码系统
运维开发王义杰1 天前
Ethereum:智能合约开发者的“瑞士军刀”OpenZeppelin
web3·区块链·智能合约
余_弦1 天前
区块链钱包开发(十二)—— 前后端状态同步机制
区块链
Joker时代1 天前
LOOP Finance:一场 Web3 共和国中的金融制度实验
金融·web3·区块链
ithadoop2 天前
Solidity智能合约开发全攻略
区块链·智能合约