1、前端如何处理交易回执(Transaction Receipt)
答:交易回执就是区块链上交易执行完成后返回的一份“证明单据”。
- 你发了一笔交易(比如转账/调用合约方法),链上会先给你一个交易哈希(txHash)。
- 然后你需要等交易被矿工打包、执行完,链才会给你一份Receipt,里面有这笔交易的结果(成功/失败)、
Gas用了多少、事件日志(Logs)等。
// ethers.js 发送交易
const tx = await contract.transfer(to, amount);
console.log('tx', tx);
console.log('交易哈希:', tx.hash); // 这里可以拿到交易hash
// 等待链上确认,得到回执
const receipt = await tx.wait();
console.log('交易回执:', receipt);
console.log('区块号:', receipt.blockNumber);
console.log('Gas 用量:', receipt.gasUsed.toString());
console.log('事件日志:', receipt.logs);
注意:
- 只拿
txHash就当交易完成txHash只是“受理单号”,交易可能还在排队,甚至可能失败。一定要等receipt
- 忘记处理失败交易
receipt里有个status字段:1 = 成功。- 有些链上环境(比如测试网)
Gas不够时,交易会失败,但还是能拿到receipt。
- 事件解析
Receipt里的logs只是原始数据,需要用ABI解码才能拿到可读的事件参数。
2、如何处理链上交易失败或被拒绝?
答:链上交易失败/被拒绝有几种情况:
- 用户拒绝签名(在钱包里点了“拒绝”) → 前端要捕获异常。
- 交易未打包(Gas 设置不合理,或者节点不同步) → 可能卡在
pending状态。 - 链上执行失败(比如合约 require 条件没满足,或者余额不足) → 会有回执,但
status !==1。
解决方式:
- 捕获异常(
try/catch)。 - 检查交易回执的
status。 - 给用户友好的提示(比如 “余额不足”、“
Gas不够”、“合约执行失败”)。
常见坑点:
- 没区分拒绝签名和链上失败:用户点“拒绝”报的是
error.code === 4001,不是交易失败。 - 只看
tx,不看receipt:tx.hash并不能保证交易执行成功,必须等receipt并检查status。 - 忘记处理
pending过久:可以用provider.waitForTransaction(tx.hash, timeout)设置超时,否则用户会一直等。 - 错误提示不明确:不要只提示“交易失败”,最好能告诉用户可能原因:
Gas不够、余额不足、合约条件未满足。
try {
// 1. 发送交易
const tx = await contract.transfer(to, amount);
// 2. 等待回执
const receipt = await tx.wait();
// 3. 检查交易状态
if (receipt.status === 1) {
console.log('✅ 交易成功:', receipt.transactionHash);
} else {
console.error('❌ 交易失败:', receipt);
}
}catch (error) {
// 捕获钱包拒绝签名 或 RPC 报错
if (error.code === 4001) {
console.error('用户拒绝签名');
} else {
console.error('交易异常:', error);
}
}
3、前端如何防止用户签名被恶意使用?
答:区块链里用户签名(signature)= 用户用私钥对一段消息做的确认。
问题:
- 如果你让用户随便签一段字符串(比如空白签名),黑客可能拿去伪造请求。
- 如果签名消息没有“上下文”,别人截获后可能在别处复用(重放攻击)。
方法:
- 明确签名目的(不要让用户签空白消息)。
- 加入唯一性(比如随机数
nonce、时间戳)。 - 限制作用范围(指定链 ID、DApp 域名)。
- 采用标准协议(比如 EIP-712 typed data)。
常见坑点
- 空白签名:千万不要用 "personal_sign" 让用户签空白字符串。
- 缺少
nonce:没有随机数/一次性标识,签名可能被重放。(确保每次签名都是唯一的,可以防止重复签名) - 没绑定
domain/chainId:签名可能被其他DApp或链复用。 - 误导性签名:提示要清楚(比如“确认登录”,而不是“签个字”),否则用户体验差。
- 前端存储签名:不要把签名直接存
localStorage,避免被盗用。
// 普通签名
const signature = await signer.signMessage(''); // 签名空字符串 得到 “0x”
// 验证签名
const recovered = verifyMessage(message, signature);
console.log('签名者:', recovered); // 签名地址
// EIP-712签名
const domain = {
name: 'XXX APP',
version: '2',
chainId: 1,
verifyingContract: '0x833589fCD6eDb...54bdA02913'
};
const types = {
Login: [
{ name: 'user', type: 'string' },
{ name: 'nonce', type: 'uint256' },
{ name: 'timestamp', type: 'uint256' }
]
};
const value = {
user: 'XXX',
nonce: 42,
timestamp: Date.now()
};
const signature = await signer.signTypedData(domain, types, value);
// 验证签名
const recoveredAddress = verifyTypedData(domain, types, value, signature);
console.log('签名者:', recoveredAddress);