欢迎光临
我们一直在努力

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

概述

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

源码:https://github.com/MetaMask/eth-block-tracker/blob/main/src/BlockTracker.ts


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 实现机制

核心接口

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

关键方法与流程

伪代码

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:当有监听器监听 sync 或 latest 事件时自动启动轮询
  • _onRemoveListener:当所有监听器移除后自动停止轮询

轮询主循环

#mermaid-svg-jd0nPiGHnCdQulpG {font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-jd0nPiGHnCdQulpG .error-icon{fill:#552222;}#mermaid-svg-jd0nPiGHnCdQulpG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jd0nPiGHnCdQulpG .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-jd0nPiGHnCdQulpG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jd0nPiGHnCdQulpG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jd0nPiGHnCdQulpG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jd0nPiGHnCdQulpG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jd0nPiGHnCdQulpG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jd0nPiGHnCdQulpG .marker.cross{stroke:#333333;}#mermaid-svg-jd0nPiGHnCdQulpG svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jd0nPiGHnCdQulpG .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-jd0nPiGHnCdQulpG .cluster-label text{fill:#333;}#mermaid-svg-jd0nPiGHnCdQulpG .cluster-label span{color:#333;}#mermaid-svg-jd0nPiGHnCdQulpG .label text,#mermaid-svg-jd0nPiGHnCdQulpG span{fill:#333;color:#333;}#mermaid-svg-jd0nPiGHnCdQulpG .node rect,#mermaid-svg-jd0nPiGHnCdQulpG .node circle,#mermaid-svg-jd0nPiGHnCdQulpG .node ellipse,#mermaid-svg-jd0nPiGHnCdQulpG .node polygon,#mermaid-svg-jd0nPiGHnCdQulpG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jd0nPiGHnCdQulpG .node .label{text-align:center;}#mermaid-svg-jd0nPiGHnCdQulpG .node.clickable{cursor:pointer;}#mermaid-svg-jd0nPiGHnCdQulpG .arrowheadPath{fill:#333333;}#mermaid-svg-jd0nPiGHnCdQulpG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jd0nPiGHnCdQulpG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jd0nPiGHnCdQulpG .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-jd0nPiGHnCdQulpG .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-jd0nPiGHnCdQulpG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jd0nPiGHnCdQulpG .cluster text{fill:#333;}#mermaid-svg-jd0nPiGHnCdQulpG .cluster span{color:#333;}#mermaid-svg-jd0nPiGHnCdQulpG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jd0nPiGHnCdQulpG :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

有监听器

启动 _maybeStart

设置 _isRunning = true

调用 _start

执行 _updateAndQueue

调用 _updateLatestBlock

调用 _fetchLatestBlock

获取成功?

更新 _currentBlock, 触发 latest/sync 事件

触发 error 事件, 设置重试间隔

setTimeout 下一轮 _updateAndQueue

_isRunning?

停止


区块获取与事件触发

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

并发与缓存处理

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

区块失效与重置

  • _setupBlockResetTimeout / cancelBlockResetTimeout:在停止轮询后,定时清空当前区块号,防止数据过时
赞(0)
未经允许不得转载:171主机测评 » 区块链钱包开发(十三)—— 构建BlockTracker
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址