本文将详细分析一次转账交易在钱包中是如何处理的,包括“创建交易→展示确认→用户确认→发布上链→确认落链”的端到端全流程分析。
阶段 0:UI 发起“创建未批准交易”
- 创建交易并跳转到确认页
// 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)。
// 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(
