30 秒回答
把交易建模为有持久化标识的状态机:准备参数并模拟,等待钱包签名,拿到 hash 后记录 chainId、account、nonce 和 hash,再由独立 Public Client 等待回执。回执 success 才代表执行成功,reverted 是链上失败;达到产品要求的确认数后再标记完成。监听相同 nonce 的替换,更新为新 hash 并区分提速、取消和替换。RPC 超时只能标记为未知或继续查询,不能直接宣告失败。页面刷新后根据持久化记录恢复追踪,绝不自动重发写请求。
深入回答
推荐状态模型
可以把一次用户意图和实际链上交易分开:
idle
→ preparing
→ simulation-failed | awaiting-signature
→ rejected | broadcast
→ pending
→ replaced | cancelled | confirmed | reverted | unknown
其中 replaced 不是简单终态:如果属于提速或重新提交,应该保存新 hash 并继续追踪;如果替换交易是给自己转 0 的取消交易,则原业务意图进入 cancelled。
建议持久化:
- 本地生成的 operationId。
- 业务动作类型和用于恢复展示的最小摘要。
- chainId、account、目标合约。
- 原始 hash、当前有效 hash 和历史 hash。
- nonce(能够取得时)。
- 当前阶段、创建时间、最后检查时间。
- receipt 的 blockNumber、status 和确认数。
不要持久化私钥、签名原文中的敏感数据或未经处理的完整业务表单。
发送前
- 校验账户、目标链、合约地址、参数和余额。
- 用整数处理代币数量,避免浮点精度问题。
- 使用
simulateContract 或等价调用预执行,解析 revert 原因。
- 向用户展示即将调用的动作、资产、数量和目标地址。
模拟成功不保证交易最终成功。模拟与实际执行之间,余额、授权、价格、nonce 和合约状态都可能变化。
钱包交互与广播
等待钱包期间是 awaiting-signature,此时还没有链上 hash。用户拒绝属于明确的本地终止,不应该显示为“交易失败”。钱包返回 hash 后,立即把最小追踪信息持久化;即使页面关闭,也可以恢复查询。
不要因为接口超时就再次调用写方法。第一次请求可能已经广播,盲目重试会产生第二笔用户意图或重复钱包弹窗。
Pending、替换和取消
Ethereum 账户交易按 nonce 排序。相同账户、相同 nonce 的另一笔交易可以替换原交易,常见原因包括:
- 提高费用进行加速。
- 发送给自己的取消交易。
- 钱包重新构造了交易参数。
前端要维护原始 hash 与有效 hash 的映射。发现替换后,界面应说明原因并继续追踪替换交易,不能让旧 hash 永久停留在 Pending。
长时间 Pending 也不等于失败。可能是费用不足、前序 nonce 阻塞、节点 mempool 视图不同或交易已经从部分节点的池中丢弃。此时可以提供区块浏览器链接、刷新查询和钱包提速入口,但状态应表达为“仍在等待”或“当前无法确认”。
回执、确认数和重组
回执出现后检查 status:
success:交易已在该区块执行成功。
reverted:交易已上链但执行回滚,Gas 仍可能被消耗。
一份回执不等于业务上的最终确定。高价值操作可以等待多个确认,或者依据链提供的安全/最终区块标签。短链重组可能让回执暂时消失或改变所在区块,因此追踪层应能回到 Pending 并重新确认。
业务数据刷新应以确认后的链上状态为准。交易成功提示和索引服务的数据可见性也要分开:索引器可能尚未同步,不能因此把成功交易改成失败。
RPC 故障与未知状态
以下情况不能直接归类为链上失败:
waitForTransactionReceipt 超时。
- 某个 RPC 返回未找到交易。
- 浏览器离线或请求被限流。
- 不同 RPC 的最新区块高度不一致。
这时使用 unknown 或“查询中断”,保留 hash 并允许换 RPC 继续查询。只有拿到明确 revert 回执,或业务有可靠证据证明交易永远不会生效时,才能给出确定失败结论。
示例代码
const { request } = await publicClient.simulateContract({
account,
address: contractAddress,
abi,
functionName: "deposit",
args: [amount],
});
const originalHash = await walletClient.writeContract(request);
savePendingOperation({
operationId,
account,
chainId: publicClient.chain.id,
currentHash: originalHash,
});
const receipt = await publicClient.waitForTransactionReceipt({
hash: originalHash,
confirmations: 2,
onReplaced(replacement) {
saveReplacement({
operationId,
reason: replacement.reason,
currentHash: replacement.transaction.hash,
});
},
});
if (receipt.status === "reverted") {
markOperationReverted(operationId, receipt);
} else {
markOperationConfirmed(operationId, receipt);
}
示例省略了错误分类、请求取消、重组检测和存储实现。simulateContract 产生的 request 应与实际使用的 account、chain 和合约配置保持一致。
常见错误
- 钱包返回 hash 后立即显示“交易成功”。
- 把用户拒绝签名、RPC 超时和链上 revert 都显示成同一种失败。
- 只保存一个 hash,不处理相同 nonce 的提速或取消交易。
- 页面刷新后丢失 Pending 交易,或者自动重新发送。
- 模拟成功后认为交易一定成功。
- 收到一次回执就永久完成状态,不考虑确认数和短链重组。
- 交易成功后等待索引器数据,索引器延迟时错误地回退交易状态。
面试官追问
- 没有拿到 nonce,只拿到 hash,如何识别替换交易?
- 用户在钱包里点击取消后,为什么取消也可能失败?
- 交易回执成功,但业务查询仍显示旧数据,应该怎样向用户解释?
- 多个标签页同时追踪同一 operationId,如何避免重复通知?
- L2 的“确认”和最终性与以太坊主网有什么不同?
评分标准
初级回答
- 能区分等待签名、Pending、成功和失败。
- 知道检查交易回执,而不是只依赖 hash。
- 能处理模拟失败、用户拒绝、链上 revert、确认数和页面恢复。
- 知道提速与取消是相同 nonce 的替换交易,并继续追踪新 hash。
- 不会把 RPC 超时直接判定为失败。
高级回答
- 能设计用户意图与链上交易分离的可恢复状态机。
- 能处理重组、不同 RPC 视图、索引延迟、跨标签页同步和幂等通知。
- 能根据资产风险和不同链的最终性制定确认策略,并说明可观测性指标。
参考资料