区块链钱包开发(十五)—— 构建交易控制器(TransactionController)

176 阅读9分钟

0. 前言

TransactionController 是 MetaMask 交易管理的核心模块之一,负责从用户发起交易到交易最终确认的完整生命周期管理。本章聚焦 TransactionController 的设计思想、状态机、主要流程、关键组件与工程实践,并通过示例代码与流程图把抽象逻辑落地为可复用实现思路。

源码: link.juejin.cn/?target=htt…

1. 核心概念(统一术语)

在开始以前,先明确几个核心术语与数据结构,便于后文表达一致、清晰。

  • transactionMeta(交易元信息)
    描述一笔交易的完整元数据:idtxParamsstatustimechainIdsubmittedTimehashtxReceipt 等。Controller 的状态即以一组 transactionMeta[] 为主。
  • TransactionController(交易控制器)
    管理交易的创建、模拟、批准、签名、广播与后续跟踪。对外提供 add/spend/speedUp/cancel/batch 等 API。
  • PendingTransactionTracker(待处理交易跟踪器)
    长期或短期轮询/订阅链上交易状态,检测交易是否被确认、reverted、dropped 等,并触发回调事件。
  • NonceTracker(nonce 管理器)
    维护每个账户在各链的当前 nonce,用于并发发送交易时的序号分配与防冲突。
  • MultichainTrackingHelper(多链追踪助手)
    当钱包支持多网络(RPC client)时,为每个网络实例分别维护 nonceTrackerpendingTransactionTracker 等,防止跨链冲突。
  • messagingSystem(消息总线)
    Controller 内部与其它 controller(NetworkController、PreferencesController)交互的事件通道,用于广播 unapprovedTransactionAddedtransactionConfirmed 等事件。

2. 架构总览(模块关系)

下面给出 TransactionController 与关键模块的关系图:

graph LR
  UI -->|addTransaction| TC[TransactionController]
  TC -->|use| NonceTracker
  TC -->|create| PendingTracker[PendingTransactionTracker]
  TC -->|publish| Network[Network / RPC]
  PendingTracker -->|poll| Network
  TC -->|notify| Messaging[MessagingSystem]
  Network -->|events| TC
  subgraph Helpers
    MultichainTrackingHelper
  end
  TC --> Helpers

说明:

  • UI(或 dApp)调用 TransactionController API 创建交易;
  • TransactionController 使用 NonceTracker 分配 nonce,调用本地模拟(或远程模拟服务),等待用户批准;
  • 批准后签名并 publish 到链,交给 PendingTransactionTracker 跟踪;
  • MultichainTrackingHelper 管理多网络下的追踪器实例。

3. 状态模型(TransactionControllerState)

统一展示 Controller 维护的状态 slice(示例 TypeScript 接口):

interface TransactionControllerState {
  transactions: TransactionMeta[];           // 所有交易(包括未批准、待提交、已确认等)
  transactionBatches: TransactionBatchMeta[]; // 批量交易元数据
  methodData: Record<string, MethodData>;    // 合约方法签名缓存
  lastFetchedBlockNumbers: { [networkClientId: string]: number }; // 每个网络上次读取区块号
  submitHistory: SubmitHistoryEntry[];       // 提交/重试历史
}

TransactionMeta(简化版):

interface TransactionMeta {
  id: string;
  txParams: TransactionParams; // to, from, value, data, gas, gasPrice ...
  status: TransactionStatus;
  chainId: number;
  networkClientId: string;
  origin?: string; // 发起来源
  time: number;
  submittedTime?: number;
  hash?: string;
  txReceipt?: any;
  simulation?: SimulationResult;
  batchId?: string;
}

TransactionStatus 常见枚举:

enum TransactionStatus {
  unapproved = 'unapproved',
  approved = 'approved',
  signed = 'signed',
  submitted = 'submitted',
  confirmed = 'confirmed',
  failed = 'failed',
  dropped = 'dropped',
  rejected = 'rejected',
}

4. 交易完整流程详解

4.1 概览(阶段列表)

  1. 创建交易元数据(构建 transactionMeta
  2. 写入状态并广播 unapprovedTransactionAdded
  3. 模拟交易(eth_call / 自定义模拟服务)
  4. 请求用户批准(UI 阶段)
  5. 签名交易(私钥或外部签名器)
  6. 发布交易(eth_sendRawTransaction
  7. 更新状态并启动 PendingTransactionTracker 跟踪
  8. 处理确认 / 失败 / 重试 / 取消

4.2 入口:addTransaction(伪代码)

输入txParams: TransactionParams, options: { requireApproval?: boolean, actionId?, networkClientId?, origin? }
输出:返回 { result: Promise<string>, transactionMeta }result resolve 为 txHash

async addTransaction(txParams: TransactionParams, options = {}) {
  // 1. 构建 transactionMeta
  const transactionMeta: TransactionMeta = {
    id: generateId(),
    txParams,
    status: TransactionStatus.unapproved,
    chainId: options.chainId || await this.#getChainId(options.networkClientId),
    networkClientId: options.networkClientId,
    origin: options.origin,
    time: Date.now(),
  };

  // 2. 写入状态
  this.updateState((s) => s.transactions.push(transactionMeta));

  // 3. 通知 UI 显示未批准交易
  this.messagingSystem.publish('unapprovedTransactionAdded', transactionMeta);

  // 4. 模拟交易
  try {
    transactionMeta.simulation = await this.#updateSimulationData(transactionMeta);
  } catch (err) {
    // 模拟失败 -> 记录但不阻止用户批准
    this.logger.warn('simulation failed', err);
  }

  // 5. 等待批准(如果需要)
  if (options.requireApproval !== false) {
    await this.#requestApproval(transactionMeta); // UI 显示弹窗,用户确认或拒绝
  }
  transactionMeta.status = TransactionStatus.approved;

  // 6. 签名(可能是内部私钥或外部硬件/隔离环境)
  const signedTx = await this.#signTransaction(transactionMeta);
  transactionMeta.status = TransactionStatus.signed;

  // 7. 发布
  const hash = await this.#publishTransaction(signedTx);
  transactionMeta.hash = hash;
  transactionMeta.status = TransactionStatus.submitted;
  transactionMeta.submittedTime = Date.now();

  // 8. 启动 PendingTransactionTracker
  const tracker = this.#createPendingTrackerFor(transactionMeta);
  tracker.startTracking();

  return {
    result: Promise.resolve(hash),
    transactionMeta,
  };
}

注意点

  • #updateSimulationData:可调用 eth_calleth_estimateGas 或 MetaMask 专用模拟服务(可获得更丰富风险信息)。
  • #requestApproval:会阻塞直到用户确认/拒绝;若拒绝,更新 statusrejected 并结束流程。
  • #signTransaction:需要考虑 chainId(EIP-155)、legacy vs EIP-1559;若使用外部 signer(硬件或 Web3 提供者),签名可能异步弹窗。
  • #publishTransaction:真正调用 RPC 的地方;失败需合理回滚/告警并允许重试。

5. 关键机制解析

5.1 Nonce 管理(并发发送的核心)

  • Nonce 是并发发送交易的根本矛盾点。TransactionController 使用 NonceTracker

    • 缓存本地估算 nonce(来自 RPC 的 getTransactionCount);
    • 对 pending 本地交易占位,保证并发发送时不会重复 nonce
    • 支持在交易被确认后释放占位并更新真实 nonce。

关于NonceTracker的详细教程参考:juejin.cn/post/753575…

示例逻辑

const nextNonce = await nonceTracker.getNextNonceFor(address, networkClientId);
txParams.nonce = nextNonce;
nonceTracker.increment(address, networkClientId);

注意:必须在 publishTransaction 之前锁定 nonce;若发送失败并确认为未广播,应 rollback(decrement 或释放占位)。

5.2 PendingTransactionTracker(跟踪器)

  • 负责定期或事件驱动地查询交易回执(getTransactionReceipt)和交易状态;当发现确认或 revert 时触发回调,更新 transactionMeta。

  • 典型行为:

    • 初始轮询:短间隔快速试探(例如 10s);
    • 长时间等待:逐渐延长轮询周期,或监听区块事件来触发检查。
  • 需处理的边界:

    • 交易被 drop(nonce 被别的 tx 替代):将其标记为 dropped
    • 交易已确认但 receipt 里 status = 0:标 failed 并记录 revert reason(若可得)。

5.3 多链支持(MultichainTrackingHelper)

  • 每个 networkClientId(如不同 RPC/链或同链不同节点)维护独立的 nonceTrackerpendingTransactionTracker
  • 当网络切换或某个 RPC 失效时,helper 会创建/销毁对应的跟踪器,保证跟踪隔离。

5.4 模拟与安全检查(Simulation)

  • MetaMask 在 addTransaction 阶段会做模拟以检测:

    • 可能的余额变化(token 转账、swap 预期结果);
    • 是否会 revert(合约调用失败);
    • gas 估算与异常 gas 消耗风险;
    • 安全性告警(如可能存在闪电贷、重入风险等定制逻辑)。
  • 模拟既可以使用标准 eth_call/eth_estimateGas,也可以使用专门的模拟服务(返回更丰富的分析信息与风险分数)。


6. 高级功能(实现与要点)

6.1 交易加速(speedUp)

  • 逻辑:使用相同 nonce、更高的 gasPrice/maxPriorityFeePerGas 发送替换交易(replace-by-fee)。

  • 必要判断:

    • 原交易仍在 pending(未在链上被确认/替换);
    • 新交易 gas 提示要高于原交易一定比例(SPEEDUP_RATE)以提高被矿工优先打包的概率。

伪代码:

async speedUpTransaction(transactionId, gasParams) {
  const tx = this.getTransaction(transactionId);
  if (!tx || tx.status !== TransactionStatus.submitted) throw Error();
  const newTxParams = { ...tx.txParams, gasPrice: gasParams.gasPrice };
  newTxParams.nonce = tx.txParams.nonce;
  return this.addTransaction(newTxParams, { requireApproval: false });
}

6.2 交易取消(cancel)

  • 逻辑:发送一笔同 nonce、to = from, value = 0、足够高 gas 的交易,以覆盖原交易。
  • 注意:cancel 也依赖替换逻辑,用户须承担替换交易的费用。

6.3 批量交易(batch)

  • 支持把一组交易打包成 batch(例如批量授权、批量转账):

    • 每笔交易仍有独立 transactionMeta,但归属同 batchId
    • 可以整体审批或逐笔审批(取决于 UX 设计)。
  • 需要考虑 nonce 分配(连续 nonces)、错误回滚策略(部分失败如何反馈)。


7. 安全机制(检查点与防护)

TransactionController 需要与安全检测模块协作,常见检查点:

  1. 参数校验(入参阶段) :校验 to 格式、value 是否合理、data 长度是否异常。
  2. 模拟风险检测(模拟阶段) :通过模拟查出 revert、异常高 gas、滑点风险、非正常合约调用。
  3. 钓鱼/黑名单保护:对 origin 源进行白名单/黑名单校验,或与 PhishingDetectionController 协作警告用户。
  4. 签名策略:优先 EIP-155 签名保障重放保护;对敏感合约操作弹窗二次确认。
  5. 最小权限原则:批量/高级操作建议分步权限确认或默认仅列核心字段。

8. 性能与工程优化建议

实际系统需要在保证可用性的同时控制资源与状态体积,以下为常用优化策略:

8.1 交易历史裁剪

  • 保持 state.transactions 不无限增长,定期按策略归档或裁剪(例如保留最近 1000 条)。
  • 裁剪策略需兼顾用户可见性(已确认但重要的 tx 不应轻易删除)。

8.2 缓存机制(methodData、gas、blockNumber)

  • methodData(ABI 方法签名)缓存减少重复解析开销;
  • gasestimate 结果短期内可以缓存,避免频繁 RPC 调用;
  • lastFetchedBlockNumbers 用于节流 block-based 刷新,避免对所有网络频繁轮询。

8.3 异步与限流

  • PendingTransactionTracker 的轮询采用分层节流:短期内快速重试 -> 长周期降频;
  • 在多账户/多网络场景,使用队列/并发限制防止 RPC 限流。

8.4 可观测性

  • 在关键流程埋点(add/sim/sign/publish/confirmed)发送事件到日志/监控;
  • 记录失败原因(RPC 错误、签名拒绝、chain mismatch 等),便于后续回溯。

9. 常见陷阱与调试建议

  • Nonce 被占用:并发发送时若未正确占位,会出现 replacement transaction underpricednonce too low。使用本地 NonceTracker 并在失败时同步 RPC nonce。
  • 模拟与真实结果不一致:模拟基于当时链状态;若在签名到上链间链状态变了(nonce、合约状态),结果可能不同。
  • 替换失败:发送替换交易要确保 gas 显著更高;不同节点对替换策略的接受程度不同。
  • 交易被 drop:若交易在 txpool 被踢出(因 gas 太低或池清理),需检测并允许重发。
  • 跨链/多 RPC 一致性:确保 MultichainTrackingHelper 在网络切换时能正确停止/启动 tracker,避免漏掉 events。

10. 示例:用 ethers.js 模拟签名 + 发送(对照实现)

这是 wallet 层如何在签名后发送 raw tx 的简化代码(非 controller 完整实现):

const { ethers } = require('ethers');
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);

async function signAndPublishTx(privateKey, txParams) {
  const wallet = new ethers.Wallet(privateKey, provider);
  // 填 nonce/gas/gasPrice 等
  txParams.nonce = txParams.nonce ?? await provider.getTransactionCount(wallet.address, 'pending');
  txParams.gasPrice = txParams.gasPrice ?? await provider.getGasPrice();
  const signed = await wallet.signTransaction(txParams);
  const txHash = await provider.sendTransaction(signed);
  return txHash.hash;
}

11. 总结与建议

  • TransactionController 的核心价值在于把复杂的交易生命周期(并发、替换、重试、跟踪)工程化为可管理的模块;成功的实现需要在正确的状态机、可靠的 nonce 管理、可控的跟踪器、以及实用的模拟/安全检测之间取得平衡。

  • 建议:

    • 始终使用 EIP-155 签名以防跨链重放;优先支持 EIP-1559(若网络支持);
    • 使用 eth_estimateGas + 模拟服务做双保险,必要时向用户展示模拟结果与风险提示;
    • 维护本地 nonce 占位机制以支持高并发发送;
    • 对频繁操作(如加速/取消)提供 UX 级别的引导与默认费率策略。

学习交流请添加vx: gh313061

下期预告:构建网络控制器(NetworkController)