区块链钱包开发(二十一)—— 一次交易的全流程分析

本文将详细分析一次转账交易在钱包中是如何处理的,包括"创建交易→展示确认→用户确认→发布上链→确认落链"的端到端全流程分析。

阶段 0:UI 发起"创建未批准交易"

  • 创建交易并跳转到确认页
js 复制代码
// metamask-extension/ui/store/actions.ts
export function addTransactionAndRouteToConfirmationPage(
  txParams, options?
): ThunkAction<Promise<TransactionMeta | null>, ...> {
  return async (dispatch) => {
    const actionId = generateActionId();
    try {
      const transactionMeta = await submitRequestToBackground<TransactionMeta>(
        'addTransaction',
        [txParams, { ...options, actionId, origin: ORIGIN_METAMASK }],
      );
      dispatch(showConfTxPage());
      return transactionMeta;
    } catch (error) {
      dispatch(hideLoadingIndication());
      dispatch(displayWarning(error));
      throw error;
    }
  };
}

要点:

  • UI 生成 actionId,用于去重(后台会用它避免重复创建)。
  • origin: ORIGIN_METAMASK 标记来源。

阶段 1:后台 TransactionController.addTransaction

在后台,addTransaction 完成参数标准化、校验、构建 txMeta、推送"未批准交易事件",并返回一个 result 对象,里面的 result 是一个 promise:等待审批完成后继续处理(参见下面的 #processApproval)。

js 复制代码
// core/packages/transaction-controller/src/TransactionController.ts
async addTransaction(txParams, options): Promise<Result> {
  // 1) 解构 options
  // 2) 标准化 txParams
  txParams = normalizeTransactionParams(txParams);

  // 3) 校验网络、获取 chainId / ethQuery
  const chainId = this.#getChainId(networkClientId);
  const ethQuery = this.#getEthQuery({ networkClientId });

  // 4) 校验 origin 权限、内部账户等
  const permittedAddresses = origin === undefined ? undefined : await this.#getPermittedAccounts?.(origin);
  const internalAccounts = this.#getInternalAccounts();
  await validateTransactionOrigin({...});

  // 5) 获取委托地址(可选)、EIP-1559 兼容性与基本 txParams 校验
  const delegationAddressPromise = getDelegationAddress(txParams.from as Hex, ethQuery).catch(() => undefined);
  const isEIP1559Compatible = await this.#getEIP1559Compatibility(networkClientId);
  validateTxParams(txParams, isEIP1559Compatible, chainId);
  if (!txParams.type) setEnvelopeType(txParams, isEIP1559Compatible);

  // 6) 批次 ID 去重(非 MetaMask origin 不允许重复)
  const isDuplicateBatchId = ...
  if (isDuplicateBatchId && origin && origin !== ORIGIN_METAMASK) throw new JsonRpcError(...)

  // 7) 生成 dApp 建议 gas、识别交易类型
  const dappSuggestedGasFees = this.#generateDappSuggestedGasFees(txParams, origin);
  const transactionType = type ?? (await determineTransactionType(txParams, ethQuery)).type;
  const delegationAddress = await delegationAddressPromise;

  // 8) 通过 actionId 去重:若已存在同 actionId 交易,复用;否则创建新的 txMeta
  const existingTransactionMeta = this.#getTransactionWithActionId(actionId);
  let addedTransactionMeta: TransactionMeta = existingTransactionMeta ? cloneDeep(existingTransactionMeta) : {
    actionId, batchId, chainId, dappSuggestedGasFees, delegationAddress,
    deviceConfirmedOn, disableGasBuffer, id: random(), isFirstTimeInteraction: undefined,
    nestedTransactions, networkClientId, origin, securityAlertResponse,
    status: TransactionStatus.unapproved as const,
    time: Date.now(), txParams, type: transactionType,
    userEditedGasLimit: false, verifiedOnBlockchain: false,
  };

  // 9) afterAdd hook(可能对 tx 进一步修正)
  const { updateTransaction } = await this.#afterAdd({ transactionMeta: addedTransactionMeta });
  if (updateTransaction) {
    addedTransactionMeta.txParamsOriginal = cloneDeep(addedTransactionMeta.txParams);
    updateTransaction(addedTransactionMeta);
  }

  // 10) 估算 gas 属性(trace 包裹)
  await this.#trace({ name: 'Estimate Gas Properties', parentContext: traceContext },
    (context) => this.#updateGasProperties(addedTransactionMeta, { traceContext: context }),
  );

  // 11) 首次创建(非重复)时,写安全扫描、历史快照、swaps 更新、添加元数据、模拟数据/首次交互标记
  if (!existingTransactionMeta) {
    if (method && this.#securityProviderRequest) {
      const resp = await this.#securityProviderRequest(addedTransactionMeta, method);
      addedTransactionMeta.securityProviderResponse = resp;
    }
    if (!this.#isSendFlowHistoryDisabled) {
      addedTransactionMeta.sendFlowHistory = sendFlowHistory ?? [];
    }
    if (!this.#isHistoryDisabled) {
      addedTransactionMeta = addInitialHistorySnapshot(addedTransactionMeta);
    }
    addedTransactionMeta = updateSwapsTransaction(addedTransactionMeta, transactionType, swaps, {...});
    this.#addMetadata(addedTransactionMeta);

    if (requireApproval !== false) {
      this.#updateSimulationData(addedTransactionMeta, { traceContext }).catch(...)
      this.#updateFirstTimeInteraction(addedTransactionMeta, { traceContext }).catch(...)
    } else {
      log('Skipping simulation & first interaction update as approval not required');
    }

    // 12) 发布"未批准交易已添加"事件,UI 看到它后进入确认页面
    this.messagingSystem.publish(
      `${controllerName}:unapprovedTransactionAdded`,
      addedTransactionMeta,
    );
  }

  // 13) 返回结果:内部 result 是 #processApproval(...) 的 promise
  return {
    result: this.#processApproval(addedTransactionMeta, {
      actionId,
      isExisting: Boolean(existingTransactionMeta),
      publishHook,
      requireApproval,
      traceContext,
    }),
    transactionMeta: addedTransactionMeta,
  };
}

要点:

  • 交易初始状态为 unapproved,并触发 unapprovedTransactionAdded,UI 因此可以展示确认页。
  • 如果 requireApproval === false,则跳过模拟与首次交互标记,稍后 #processApproval 会直接走发布,不等待 UI 确认。
  • 返回一个对象:transactionMeta(当前元数据)与 result(等待审批并最终发布的 promise)。

阶段 2:审批与确认页(UI 侧)

  • UI 订阅 unapprovedTransactionAdded(通过 background state 更新),加载确认页,用户修改 gas/nonce 等参数。

阶段 3:点击确认 → 发起批准

  1. 组件绑定确认逻辑,调用 Hook 的 onTransactionConfirm
js 复制代码
// metamask-extension/ui/pages/confirmations/hooks/transactions/useTransactionConfirm.ts
export function useTransactionConfirm() {
  ...
  const onTransactionConfirm = useCallback(async () => {
    newTransactionMeta.customNonceValue = customNonceValue;

    if (isSmartTransaction) {
      handleSmartTransaction();
    } else if (selectedGasFeeToken) {
      handleGasless7702();
    }

    await dispatch(updateAndApproveTx(newTransactionMeta, true, ''));
  }, [...]);

  return { onTransactionConfirm };
}
  • 可能修改 newTransactionMeta(例如智能交易批处理、Gasless 7702 的标志)。
  • 派发 updateAndApproveTx,进入 Redux 异步流程。

阶段 4:更新并批准交易(调用后台 resolve)

  1. updateAndApproveTx:显示加载、调用后台 resolvePendingApproval
js 复制代码
// metamask-extension/ui/store/actions.ts
export function updateAndApproveTx(txMeta, dontShowLoadingIndicator, message) {
  return (dispatch, getState) => {
    !dontShowLoadingIndicator && dispatch(showLoadingIndication(message));
    const getIsSendActive = () => Boolean(getState().send.stage !== SEND_STAGES.INACTIVE);

    return new Promise((resolve, reject) => {
      const actionId = generateActionId();

      callBackgroundMethod(
        'resolvePendingApproval',
        [String(txMeta.id), { txMeta, actionId }, { waitForResult: true }],
        (err) => {
          dispatch(updateTransactionParams(txMeta.id, txMeta.txParams));
          if (!getIsSendActive()) {
            dispatch(resetSendState());
          }
          if (err) {
            dispatch(goHome());
            logErrorWithMessage(err);
            reject(err);
            return;
          }
          resolve(txMeta);
        },
      );
    })
      .then(() => forceUpdateMetamaskState(dispatch))
      .then(() => {
        if (!getIsSendActive()) {
          dispatch(resetSendState());
        }
        dispatch(completedTx(txMeta.id));
        dispatch(hideLoadingIndication());
        dispatch(updateCustomNonce(''));
        dispatch(closeCurrentNotificationWindow());
        return txMeta;
      })
      .catch((err) => {
        dispatch(hideLoadingIndication());
        return Promise.reject(err);
      });
  };
}
  • 通过 callBackgroundMethod('resolvePendingApproval', ...) 通知后台"用户已批准这笔审批请求",并把 txMetaactionId 一并传递。
  • UI 在回调里做参数同步、状态清理;随后触发 forceUpdateMetamaskState 拉取最新后台状态并收尾。

补充:UI 也存在一个 thunk 版本(用于无需回调直接等待):

js 复制代码
// metamask-extension/ui/store/actions.ts
export function resolvePendingApproval(id, value) {
  return async (_dispatch) => {
    await submitRequestToBackground('resolvePendingApproval', [id, value]);
    const { pendingApprovals } = await forceUpdateMetamaskState(_dispatch);
    if (Object.values(pendingApprovals).length === 0) {
      _dispatch(closeCurrentNotificationWindow());
    }
  };
}

阶段 5:MetamaskController → ApprovalController

  1. 后台处理 resolvePendingApproval:交给 ApprovalController.accept
js 复制代码
// metamask-extension/app/scripts/metamask-controller.js
resolvePendingApproval = async (id, value, options) => {
  try {
    await this.approvalController.accept(id, value, options);
  } catch (exp) {
    if (!(exp instanceof ApprovalRequestNotFoundError)) {
      throw exp;
    }
  }
};
  • 该调用解除此前创建的挂起审批(由交易控制器在创建交易时发起)。
  • value 中包含 UI 传回的 txMeta(可能用户确认时调整了 nonce/gas 等)。

阶段 6:处理审批 → 发布交易

审批通过后,TransactionController 对应进行"处理审批"与"发布交易"。关键流程如下:

  1. #processApproval:等待审批→合入变更→调用 #approveTransaction
js 复制代码
// core/packages/transaction-controller/src/TransactionController.ts
async #processApproval(transactionMeta, { actionId, isExisting, publishHook, requireApproval, shouldShowRequest, traceContext }) {
  ...
  if (requireApproval !== false) {
    const acceptResult = await this.#trace(
      { name: 'Await Approval', parentContext: traceContext },
      (context) => this.#requestApproval(transactionMeta, { shouldShowRequest, traceContext: context }),
    );
    resultCallbacks = acceptResult.resultCallbacks;
    const approvalValue = acceptResult.value as { txMeta?: TransactionMeta } | undefined;
    const updatedTransaction = approvalValue?.txMeta;
    if (updatedTransaction) {
      this.updateTransaction(updatedTransaction, '... Updated with approval data');
    }
  }

  const { isCompleted: isTxCompleted } = this.#isTransactionCompleted(transactionId);
  if (!isTxCompleted) {
    const approvalResult = await this.#approveTransaction(transactionId, traceContext, publishHook);
    ...
    const updatedTransactionMeta = this.#getTransaction(transactionId) as TransactionMeta;
    this.messagingSystem.publish(`${controllerName}:transactionApproved`, { transactionMeta: updatedTransactionMeta, actionId });
  }
}
  • 这里的 #requestApproval 对应此前由控制器发起的审批请求;UI 通过 accept 解锁,控制器在这里 await。
  • 若 UI 在审批期提供了新的 txMeta,此处用 updateTransaction 合并。
  1. #approveTransaction:签名、构造 rawTx、发布
js 复制代码
// core/packages/transaction-controller/src/TransactionController.ts
async #approveTransaction(transactionId, traceContext, publishHookOverride) {
  ...
  await this.#trace({ name: 'Publish', parentContext: traceContext }, async () => {
    const publishHook = publishHookOverride ?? this.#publish;

    ({ transactionHash: hash } = await publishHook(
      transactionMeta,
      rawTx ?? '0x',
    ));

    if (hash === undefined) {
      hash = await this.#publishTransaction(ethQuery, {
        ...transactionMeta,
        rawTx,
      });
    }
  });

  transactionMeta = this.#updateTransactionInternal(
    { transactionId, note: '... Transaction submitted' },
    (draft) => {
      draft.hash = hash;
      draft.status = TransactionStatus.submitted;
      draft.submittedTime = new Date().getTime();
      if (shouldUpdatePreTxBalance) { draft.preTxBalance = preTxBalance; }
    },
  );

  this.messagingSystem.publish(`${controllerName}:transactionSubmitted`, { transactionMeta });
  this.messagingSystem.publish(`${controllerName}:transactionFinished`, transactionMeta);
  this.#internalEvents.emit(`${transactionId}:finished`, transactionMeta);
  this.#onTransactionStatusChange(transactionMeta);
  return ApprovalState.Approved;
}
  • 若存在自定义 publishHook(如智能交易、批处理等),优先调用。否则走默认 #publishTransaction
  • 发布成功后写入 hash,状态转为 submitted,并广播事件。
  1. 默认发布:发送 rawTx → eth_sendRawTransaction
3097:3111:core/packages/transaction-controller/src/TransactionController.ts 复制代码
async #publishTransaction(ethQuery, transactionMeta, { skipSubmitHistory } = {}) {
  const transactionHash = await query(ethQuery, 'sendRawTransaction', [
    transactionMeta.rawTx,
  ]);
  if (skipSubmitHistory !== true) {
    this.#updateSubmitHistory(transactionMeta, transactionHash);
  }
  return transactionHash;
}
  • 通过 EthQuerysendRawTransaction 发送签名交易至节点。
  • 记录提交历史。
  1. 批量/顺序发布等路径(可选)
  • 若为批量(例如 EIP-7702 或智能交易场景),在 addTransactionBatch/hooks 中也会注入 publishTransaction 钩子,内部仍可回落到 #publishTransaction
js 复制代码
// core/packages/transaction-controller/src/TransactionController.ts
publishTransaction: (ethQuery, transactionMeta) =>
  this.#publishTransaction(ethQuery, transactionMeta) as Promise<Hex>,
  • 另见 SequentialPublishBatchHookPendingTransactionTracker 在特定条件下调用 #publishTransaction 进行重发。

阶段 7:状态轮询与最终确认

  1. 提交后由 PendingTransactionTracker 轮询节点,确认/失败/丢弃
js 复制代码
// core/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts
await this.#publishTransaction(ethQuery, txMeta); // 重发(需要时)

...
451:486:core/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts
async #onTransactionConfirmed(txMeta, receipt) {
  ...
  updatedTxMeta.status = TransactionStatus.confirmed;
  updatedTxMeta.txReceipt = receipt;
  updatedTxMeta.verifiedOnBlockchain = true;
  this.#updateTransaction(updatedTxMeta, '... Transaction confirmed');
  this.hub.emit('transaction-confirmed', updatedTxMeta);
}
  • 周期性调用 getTransactionReceipt。成功则状态转 confirmed,失败或替换则触发对应处理。
  • 重发机制具备指数退避、已知错误忽略、丢弃检测(nonce 被占用)等逻辑。

全链路时序图(从创建到落链)

sequenceDiagram autonumber participant UI as UI组件/页面 participant Hook as useTransactionConfirm participant Actions as UI Actions (Redux) participant BG as 背景页(MetamaskController) participant AC as ApprovalController participant TC as TransactionController participant Node as 以太坊节点 participant PTT as PendingTransactionTracker rect rgb(245,245,245) Note over UI,TC: 前置阶段:创建"未批准交易" UI->>Actions: addTransactionAndRouteToConfirmationPage(txParams, options) Actions->>BG: submitRequestToBackground('addTransaction', [txParams, {..., actionId, origin}]) BG->>TC: TransactionController.addTransaction(txParams, options) TC->>TC: normalize/validate/类型识别/估算gas/安全扫描/历史快照/Swaps处理/添加元数据 TC-->>UI: publish(`${TC}:unapprovedTransactionAdded`, txMeta) UI-->>UI: 跳转确认页(展示未批准交易) end rect rgb(235,245,255) Note over UI,AC: 用户确认阶段:批准挂起审批 UI->>Hook: 点击"确认交易" Hook->>Actions: dispatch(updateAndApproveTx(newTxMeta, ...)) Actions->>BG: callBackgroundMethod('resolvePendingApproval', [txId, {txMeta, actionId}, {waitForResult:true}]) BG->>AC: ApprovalController.accept(id, value, options) AC-->>TC: 解锁等待中的审批(#processApproval 持续 await) end rect rgb(245,255,245) Note over TC,Node: 发布与提交阶段 TC->>TC: #processApproval: 合并approval返回的txMeta(若存在) TC->>TC: #approveTransaction(transactionId,...) alt 有 publishHook(智能交易/批处理) TC->>TC: 调用 publishHook(transactionMeta, rawTx) TC-->>TC: 返回transactionHash 或 fallback else 默认发布 TC->>Node: eth_sendRawTransaction(rawTx) Node-->>TC: 返回 transactionHash end TC->>TC: 写入 hash、status=submitted、submittedTime TC-->>UI: publish(`${TC}:transactionSubmitted`, {transactionMeta}) TC-->>UI: publish(`${TC}:transactionFinished`, transactionMeta) UI->>Actions: forceUpdateMetamaskState/hideLoading/closeWindow end rect rgb(255,250,235) Note over PTT,Node: 轮询确认阶段 TC->>PTT: 开始/继续轮询pending交易 loop 每新区块 PTT->>Node: getTransactionReceipt(hash) alt 成功(receipt.status=0x1 且有 blockNumber/blockHash) PTT->>TC: #onTransactionConfirmed(txMeta, receipt) TC->>TC: status=confirmed、记录receipt/区块时间/链上已验证 TC-->>UI: hub.emit('transaction-confirmed', updatedTxMeta) UI-->>UI: 交易显示"已确认" else 失败或替换/丢弃 PTT->>TC: #failTransaction 或 #dropTransaction TC-->>UI: hub.emit('transaction-failed'/'transaction-dropped') end opt 需重发 PTT->>TC: #publishTransaction(重发,指数退避/已知错误忽略) end end end

学习交流请添加vx: gh313061

相关推荐
IvorySQL2 天前
PG 日报|优化缓冲区批量扫描,降低多套接字并发竞争
数据库·人工智能·postgresql·开源·区块链
mrbande2 天前
Vatee万腾:把信息透明度做扎实,更谨慎的使用者更容易感受到的清单
区块链
Web3李李2 天前
Web3 创业最大误区:DApp 能跑≠能上线!无数项目死在这个
金融·web3·区块链·智能合约·软件开发·dapp开发·defi开发
ithadoop2 天前
CLOB-FIRST:当永续合约把“订单簿“重新变成主角
区块链·clob·amm·永续合约·hyperliquid·dydx v4·gmx v2
ZDGJ60993 天前
外盘期货与内盘期货交易时间核心区别
区块链
cd_949217213 天前
数据使用控制的工程实现:数字合约、策略引擎与沙箱环境
区块链
Bczheng13 天前
三十六.区块链网络(7)--修复net与测试例子
区块链
LedgerNinja3 天前
WEEX API 稳定性实战:限频、重试与风控的工程化清单
运维·区块链
IvorySQL3 天前
PG 日报|修复高 IO 并发场景,解决预读机制耗尽本地缓冲区缺陷
数据库·人工智能·sql·postgresql·区块链
北冥you鱼5 天前
OpenZeppelin Contracts 完全指南:从入门到精通,构建安全的智能合约
安全·区块链·智能合约