跳到主要内容

交易签名与确认

交易签名与确认是Web3应用中最核心的功能之一,直接关系到用户资产的安全和使用体验。一个安全、高效的交易流程能极大地增强用户对DApp的信任度。本章将详细介绍Web3交易的技术原理、创建、签名、发送和确认的完整流程,以及各种优化和安全措施。

Web3交易的基本原理

在区块链网络中,交易是账户之间价值或数据的转移行为。与传统金融交易不同,Web3交易需要用户使用私钥进行数字签名,以证明交易的合法性和不可抵赖性。

交易的核心组成部分

以太坊及EVM兼容链的交易通常包含以下核心字段:

interface Transaction {
from: string; // 发送方地址
to: string; // 接收方地址(合约地址或外部账户地址)
value: string; // 转账金额(以wei为单位的十六进制字符串)
gasLimit: string; // 交易允许使用的最大gas量
gasPrice: string; // 每单位gas的价格(以wei为单位)
data?: string; // 交易数据(用于合约交互,十六进制字符串)
nonce: string; // 交易随机数,用于防止交易重放
chainId: string; // 区块链网络ID,用于防止跨链重放攻击
}

交易生命周期

一个完整的Web3交易生命周期包括以下几个阶段:

  1. 交易创建:前端应用根据用户操作生成交易数据
  2. Gas估算:计算完成交易所需要的Gas量
  3. 交易签名:用户使用钱包对交易进行数字签名
  4. 交易发送:将签名后的交易发送到区块链网络
  5. 交易确认:等待矿工打包交易并获得网络确认
  6. 交易完成:交易被确认并记录在区块链上

交易创建与Gas估算

1. 基础交易创建

创建一个基础的ETH转账交易是最常见的场景。

// 创建基础ETH转账交易
async function createEthTransferTransaction(fromAddress, toAddress, amountInEth) {
try {
// 获取当前网络ID
const chainId = await window.ethereum.request({
method: 'eth_chainId'
});

// 获取发送方的交易随机数
const nonce = await window.ethereum.request({
method: 'eth_getTransactionCount',
params: [fromAddress, 'pending']
});

// 获取当前Gas价格
const gasPrice = await window.ethereum.request({
method: 'eth_gasPrice'
});

// 估算Gas Limit(基础转账通常为21000)
const gasLimit = '0x5208'; // 21000的十六进制表示

// 将ETH转换为wei
const web3 = new Web3();
const value = web3.utils.toHex(web3.utils.toWei(amountInEth, 'ether'));

// 构建交易对象
const transaction = {
from: fromAddress,
to: toAddress,
value: value,
gasLimit: gasLimit,
gasPrice: gasPrice,
nonce: nonce,
chainId: chainId
};

console.log('创建的转账交易:', transaction);
return transaction;
} catch (error) {
console.error('创建交易失败:', error);
throw error;
}
}

// 使用示例
const fromAddress = '0x1234567890123456789012345678901234567890'; // 用户地址
const toAddress = '0x0987654321098765432109876543210987654321'; // 接收地址
const amountInEth = '0.1'; // 转账金额(ETH)

const transaction = await createEthTransferTransaction(fromAddress, toAddress, amountInEth);

2. 合约交互交易创建

与智能合约交互的交易需要设置data字段,包含合约方法调用信息和参数。

// 创建合约交互交易
async function createContractInteractionTransaction(fromAddress, contractAddress, abi, methodName, methodParams) {
try {
// 创建Web3实例
const web3 = new Web3(window.ethereum);

// 创建合约实例
const contract = new web3.eth.Contract(abi, contractAddress);

// 编码合约方法调用数据
const data = contract.methods[methodName](...methodParams).encodeABI();

// 获取当前网络ID
const chainId = await web3.eth.getChainId();

// 获取发送方的交易随机数
const nonce = await web3.eth.getTransactionCount(fromAddress, 'pending');

// 获取当前Gas价格
const gasPrice = await web3.eth.getGasPrice();

// 估算Gas Limit
const gasLimit = await contract.methods[methodName](...methodParams).estimateGas({
from: fromAddress
});

// 构建交易对象
const transaction = {
from: fromAddress,
to: contractAddress,
data: data,
gasLimit: web3.utils.toHex(gasLimit),
gasPrice: web3.utils.toHex(gasPrice),
nonce: web3.utils.toHex(nonce),
chainId: web3.utils.toHex(chainId)
};

console.log('创建的合约交互交易:', transaction);
return transaction;
} catch (error) {
console.error('创建合约交互交易失败:', error);

// 处理Gas估算失败的情况
if (error.message.includes('gas required exceeds allowance') ||
error.message.includes('always failing transaction')) {
console.warn('Gas估算失败,使用默认值');
// 返回带默认Gas Limit的交易
return createTransactionWithDefaultGas(fromAddress, contractAddress, abi, methodName, methodParams);
}

throw error;
}
}

// 创建带默认Gas的合约交互交易
async function createTransactionWithDefaultGas(fromAddress, contractAddress, abi, methodName, methodParams) {
// 简化实现,与上面类似,但使用默认的Gas Limit
// ...
}

// 使用示例(以ERC20代币转账为例)
const erc20Abi = [
// ERC20代币的ABI片段
{
"constant": false,
"inputs": [
{"name": "_to", "type": "address"},
{"name": "_value", "type": "uint256"}
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
}
// ...其他ABI定义
];

const contractAddress = '0xabcdef1234567890abcdef1234567890abcdef12'; // ERC20代币合约地址
const toAddress = '0x0987654321098765432109876543210987654321'; // 接收地址
const amountInTokens = '100'; // 转账代币数量
const decimals = 18; // 代币小数位数

// 将代币数量转换为最小单位
const web3 = new Web3();
const amountInWei = web3.utils.toWei(amountInTokens, 'ether'); // 假设代币使用18位小数

const transaction = await createContractInteractionTransaction(
fromAddress,
contractAddress,
erc20Abi,
'transfer',
[toAddress, amountInWei]
);

3. 高级Gas策略

为了优化交易体验,可以实现更智能的Gas策略,如动态调整Gas价格、设置Gas上限等。

// 高级Gas策略管理器
class GasStrategyManager {
constructor() {
this.web3 = new Web3(window.ethereum);
this.baseGasMultiplier = 1.2; // 基础Gas乘数
this.priorityFeeMultiplier = 1.5; // 优先级Gas乘数
this.maxGasPrice = Web3.utils.toWei('500', 'gwei'); // 最大Gas价格
}

// 获取当前Gas价格建议
async getGasPriceSuggestion(strategy = 'standard') {
try {
// 使用Etherscan API获取Gas价格建议(需要API密钥)
// 实际项目中应使用自己的后端服务或其他可靠来源
const response = await fetch('https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=YourApiKeyToken');
const data = await response.json();

if (data.status === '1') {
const gasPrices = {
safeLow: parseInt(data.result.SafeGasPrice),
standard: parseInt(data.result.ProposeGasPrice),
fast: parseInt(data.result.FastGasPrice)
};

// 根据策略选择Gas价格
let gasPriceGwei = gasPrices[strategy] || gasPrices.standard;

// 应用乘数调整
if (strategy === 'fast') {
gasPriceGwei = Math.floor(gasPriceGwei * this.priorityFeeMultiplier);
} else if (strategy === 'safeLow') {
gasPriceGwei = Math.floor(gasPriceGwei * 0.8); // 安全低Gas价格使用较小乘数
} else {
gasPriceGwei = Math.floor(gasPriceGwei * this.baseGasMultiplier);
}

// 确保不超过最大Gas价格
const gasPriceWei = this.web3.utils.toWei(gasPriceGwei.toString(), 'gwei');
if (parseInt(gasPriceWei, 16) > parseInt(this.maxGasPrice, 16)) {
return this.maxGasPrice;
}

return gasPriceWei;
} else {
console.warn('Etherscan API调用失败,使用默认Gas价格');
// 回退到web3.eth.getGasPrice()
return await this.web3.eth.getGasPrice();
}
} catch (error) {
console.error('获取Gas价格建议失败:', error);
// 回退到web3.eth.getGasPrice()
return await this.web3.eth.getGasPrice();
}
}

// 估算交易Gas Limit并添加安全边际
async estimateGasWithSafetyMargin(transaction) {
try {
// 估算基础Gas Limit
const baseGasLimit = await this.web3.eth.estimateGas(transaction);

// 添加10%-20%的安全边际
const safetyMargin = Math.floor(baseGasLimit * 0.15); // 15%的安全边际
const gasLimitWithMargin = baseGasLimit + safetyMargin;

return this.web3.utils.toHex(gasLimitWithMargin);
} catch (error) {
console.error('Gas估算失败:', error);
// 返回默认Gas Limit(根据交易类型选择合适的值)
if (transaction.data && transaction.data !== '0x') {
// 合约交互交易使用较高的默认值
return this.web3.utils.toHex(200000);
} else {
// 简单转账使用标准值
return this.web3.utils.toHex(21000);
}
}
}

// 获取交易预计完成时间
estimateTransactionTime(gasPriceWei) {
const gasPriceGwei = this.web3.utils.fromWei(gasPriceWei, 'gwei');

// 根据Gas价格估算交易确认时间(经验值)
if (gasPriceGwei >= 100) {
return '~15秒';
} else if (gasPriceGwei >= 50) {
return '~30秒';
} else if (gasPriceGwei >= 20) {
return '~1分钟';
} else if (gasPriceGwei >= 10) {
return '~2分钟';
} else if (gasPriceGwei >= 5) {
return '~5分钟';
} else {
return '>10分钟';
}
}

// 计算交易总成本
calculateTransactionCost(gasPriceWei, gasLimitHex) {
const gasPrice = parseInt(gasPriceWei, 16);
const gasLimit = parseInt(gasLimitHex, 16);
const totalWei = gasPrice * gasLimit;
const totalEth = this.web3.utils.fromWei(totalWei.toString(), 'ether');

return {
wei: totalWei.toString(),
eth: totalEth
};
}
}

// 使用示例
const gasManager = new GasStrategyManager();

// 获取快速交易的Gas价格
const fastGasPrice = await gasManager.getGasPriceSuggestion('fast');
console.log('快速交易Gas价格:', gasManager.web3.utils.fromWei(fastGasPrice, 'gwei'), 'gwei');

// 估算交易Gas Limit
const transaction = {
from: fromAddress,
to: toAddress,
value: '0x16345785d8a0000' // 0.1 ETH
};
const gasLimit = await gasManager.estimateGasWithSafetyMargin(transaction);

// 计算交易总成本
const cost = gasManager.calculateTransactionCost(fastGasPrice, gasLimit);
console.log('交易预计成本:', cost.eth, 'ETH');

// 估算交易确认时间
const estimatedTime = gasManager.estimateTransactionTime(fastGasPrice);
console.log('预计确认时间:', estimatedTime);

交易签名与发送

1. 使用用户钱包签名交易

最常见的方式是让用户通过他们的Web3钱包(如MetaMask)来签名和发送交易。

// 使用钱包签名并发送交易
async function signAndSendTransaction(transaction) {
try {
// 显示交易确认对话框,包含Gas费用等信息
const userConfirmed = await showTransactionConfirmationDialog(transaction);

if (!userConfirmed) {
throw new Error('用户取消了交易');
}

// 请求用户钱包签名并发送交易
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [transaction]
});

console.log('交易已发送,哈希:', txHash);

// 返回交易哈希,用于后续查询交易状态
return txHash;
} catch (error) {
console.error('交易签名或发送失败:', error);

// 处理常见错误
if (error.code === 4001) {
throw new Error('用户取消了交易签名');
} else if (error.message.includes('insufficient funds')) {
throw new Error('账户余额不足,无法完成交易');
} else if (error.message.includes('nonce too low')) {
throw new Error('交易随机数过低,请稍后重试或增加Gas价格');
} else {
throw new Error('交易失败,请稍后重试');
}
}
}

// 交易确认对话框
async function showTransactionConfirmationDialog(transaction) {
// 在实际应用中,这里应该显示一个美观的UI对话框
// 包含交易详情、预估Gas费用、预计确认时间等信息

console.log('显示交易确认对话框:', {
from: transaction.from,
to: transaction.to,
value: transaction.value ? web3.utils.fromWei(transaction.value, 'ether') + ' ETH' : '0',
gasPrice: web3.utils.fromWei(transaction.gasPrice, 'gwei') + ' gwei',
gasLimit: transaction.gasLimit
});

// 简化实现,实际应用中应使用UI组件
return new Promise((resolve) => {
// 模拟用户确认
setTimeout(() => resolve(true), 1000);
});
}

// 使用示例
const transaction = await createEthTransferTransaction(fromAddress, toAddress, '0.1');
const txHash = await signAndSendTransaction(transaction);

2. 离线签名交易

在某些场景下,可能需要先离线签名交易,然后再在合适的时机发送到网络。

// 离线签名交易
async function signTransactionOffline(transaction, privateKey) {
try {
// 创建Web3实例
const web3 = new Web3();

// 使用私钥签名交易
const signedTransaction = await web3.eth.accounts.signTransaction(
transaction,
privateKey
);

console.log('交易已离线签名');

// 返回签名后的交易数据
return signedTransaction.rawTransaction;
} catch (error) {
console.error('离线签名交易失败:', error);
throw new Error('交易签名失败,请检查私钥是否正确');
}
}

// 发送已签名的交易
async function sendSignedTransaction(rawTransaction) {
try {
// 发送已签名的交易到网络
const txHash = await web3.eth.sendSignedTransaction(rawTransaction);

console.log('已签名交易发送成功,哈希:', txHash.transactionHash);

return txHash.transactionHash;
} catch (error) {
console.error('发送已签名交易失败:', error);
throw error;
}
}

// 使用示例(注意:在实际前端应用中不应直接处理私钥)
// 此示例仅用于展示离线签名的技术原理
const transaction = {
from: fromAddress,
to: toAddress,
value: web3.utils.toHex(web3.utils.toWei('0.1', 'ether')),
gasLimit: '0x5208', // 21000
gasPrice: await web3.eth.getGasPrice(),
nonce: await web3.eth.getTransactionCount(fromAddress, 'pending'),
chainId: await web3.eth.getChainId()
};

// 注意:在前端应用中永远不要直接处理用户的私钥!
// 此示例仅用于演示,实际应用中应使用硬件钱包或其他安全方式
const privateKey = '0x...'; // 用户的私钥(不应在前端存储)

const signedTx = await signTransactionOffline(transaction, privateKey);
const txHash = await sendSignedTransaction(signedTx);

3. EIP-1559交易支持

EIP-1559引入了新的交易费用模型,包括基础费用(base fee)和优先费用(priority fee)。支持EIP-1559可以提供更好的用户体验和更可预测的Gas费用。

// 创建EIP-1559交易
async function createEIP1559Transaction(fromAddress, toAddress, amountInEth) {
try {
// 创建Web3实例
const web3 = new Web3(window.ethereum);

// 获取当前网络ID
const chainId = await web3.eth.getChainId();

// 获取发送方的交易随机数
const nonce = await web3.eth.getTransactionCount(fromAddress, 'pending');

// 获取当前区块基础费用
const latestBlock = await web3.eth.getBlock('latest');
const baseFeePerGas = latestBlock.baseFeePerGas;

// 计算建议的最大优先费用(tip)
const priorityFeePerGas = web3.utils.toWei('2', 'gwei'); // 2 gwei的小费

// 计算最大FeePerGas(基础费用 + 优先费用)
const maxFeePerGas = web3.utils.toHex(parseInt(baseFeePerGas, 16) + parseInt(priorityFeePerGas, 16));

// 估算Gas Limit
const gasLimit = await web3.eth.estimateGas({
from: fromAddress,
to: toAddress,
value: web3.utils.toWei(amountInEth, 'ether')
});

// 构建EIP-1559交易对象
const transaction = {
from: fromAddress,
to: toAddress,
value: web3.utils.toHex(web3.utils.toWei(amountInEth, 'ether')),
gasLimit: web3.utils.toHex(gasLimit),
maxPriorityFeePerGas: priorityFeePerGas,
maxFeePerGas: maxFeePerGas,
nonce: web3.utils.toHex(nonce),
chainId: web3.utils.toHex(chainId),
type: '0x2' // EIP-1559交易类型
};

console.log('创建的EIP-1559交易:', transaction);
return transaction;
} catch (error) {
console.error('创建EIP-1559交易失败:', error);
// 回退到传统交易类型
return createEthTransferTransaction(fromAddress, toAddress, amountInEth);
}
}

// 检测钱包是否支持EIP-1559
async function checkEIP1559Support() {
try {
// 检查网络是否支持EIP-1559(通过查看最近区块是否有baseFeePerGas字段)
const web3 = new Web3(window.ethereum);
const latestBlock = await web3.eth.getBlock('latest');

return latestBlock && 'baseFeePerGas' in latestBlock;
} catch (error) {
console.error('检查EIP-1559支持失败:', error);
return false;
}
}

// 使用示例
const supportsEIP1559 = await checkEIP1559Support();
let transaction;

if (supportsEIP1559) {
// 网络支持EIP-1559,使用新交易类型
transaction = await createEIP1559Transaction(fromAddress, toAddress, '0.1');
} else {
// 网络不支持EIP-1559,使用传统交易类型
transaction = await createEthTransferTransaction(fromAddress, toAddress, '0.1');
}

const txHash = await signAndSendTransaction(transaction);

交易状态跟踪与确认

交易发送后,需要跟踪其状态,直到交易被确认或失败。

1. 交易状态查询

// 查询交易状态
async function getTransactionStatus(txHash, retries = 30, interval = 2000) {
try {
const web3 = new Web3(window.ethereum);

// 重试逻辑
let attempts = 0;

while (attempts < retries) {
try {
// 获取交易收据
const receipt = await web3.eth.getTransactionReceipt(txHash);

if (receipt) {
// 交易已被挖出并包含在区块中
console.log('交易收据:', receipt);

return {
status: 'confirmed',
txHash: txHash,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed,
cumulativeGasUsed: receipt.cumulativeGasUsed,
effectiveGasPrice: receipt.effectiveGasPrice,
contractAddress: receipt.contractAddress, // 如果是合约创建交易
logs: receipt.logs, // 交易产生的事件日志
success: receipt.status === true || receipt.status === '0x1' // 交易是否成功
};
}

// 交易还未被挖出,检查是否存在于交易池
try {
const transaction = await web3.eth.getTransaction(txHash);

if (transaction) {
// 交易存在于交易池或正在确认中
console.log('交易在交易池中:', transaction);

return {
status: 'pending',
txHash: txHash,
nonce: transaction.nonce,
gasPrice: transaction.gasPrice,
gasLimit: transaction.gasLimit,
from: transaction.from,
to: transaction.to
};
}
} catch (txError) {
// 忽略交易不存在的错误
}

// 等待一段时间后重试
attempts++;
await new Promise(resolve => setTimeout(resolve, interval));
} catch (error) {
console.error(`查询交易状态失败 (尝试 ${attempts}/${retries}):`, error);
attempts++;
await new Promise(resolve => setTimeout(resolve, interval));
}
}

// 超过重试次数,返回未知状态
return {
status: 'unknown',
txHash: txHash,
message: '交易状态未知,请稍后手动查询'
};
} catch (error) {
console.error('获取交易状态失败:', error);
throw error;
}
}

// 使用示例
const txHash = '0x1234567890123456789012345678901234567890123456789012345678901234';
const status = await getTransactionStatus(txHash);
console.log('交易状态:', status);

2. 交易确认跟踪器

创建一个交易确认跟踪器,持续监控交易状态并提供实时反馈。

// 交易确认跟踪器
class TransactionTracker {
constructor() {
this.web3 = new Web3(window.ethereum);
this.trackedTransactions = new Map();
this.pollingInterval = null;
}

// 开始跟踪交易
trackTransaction(txHash, options = {}) {
const {
confirmationsNeeded = 1,
onStatusChange,
onConfirmation,
onFinalized,
onError
} = options;

// 初始化交易跟踪信息
this.trackedTransactions.set(txHash, {
confirmationsNeeded,
currentConfirmations: 0,
isFinalized: false,
callbacks: {
onStatusChange,
onConfirmation,
onFinalized,
onError
},
startTime: Date.now()
});

console.log(`开始跟踪交易: ${txHash}, 需要 ${confirmationsNeeded} 个确认`);

// 启动轮询(如果还没有启动)
this.startPolling();

return txHash;
}

// 停止跟踪交易
stopTrackingTransaction(txHash) {
if (this.trackedTransactions.has(txHash)) {
this.trackedTransactions.delete(txHash);
console.log(`停止跟踪交易: ${txHash}`);

// 如果没有更多交易要跟踪,停止轮询
if (this.trackedTransactions.size === 0) {
this.stopPolling();
}
}
}

// 停止所有交易跟踪
stopAllTracking() {
this.trackedTransactions.clear();
this.stopPolling();
console.log('停止所有交易跟踪');
}

// 启动轮询
startPolling() {
if (this.pollingInterval) return;

// 每2秒轮询一次交易状态
this.pollingInterval = setInterval(() => {
this.pollTransactionStatuses();
}, 2000);

console.log('交易状态轮询已启动');
}

// 停止轮询
stopPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
console.log('交易状态轮询已停止');
}
}

// 轮询所有交易状态
async pollTransactionStatuses() {
if (this.trackedTransactions.size === 0) {
this.stopPolling();
return;
}

// 并行查询所有交易状态
const promises = Array.from(this.trackedTransactions.keys()).map(async (txHash) => {
try {
await this.updateTransactionStatus(txHash);
} catch (error) {
console.error(`更新交易状态失败 (${txHash}):`, error);

// 调用错误回调
const txInfo = this.trackedTransactions.get(txHash);
if (txInfo && txInfo.callbacks.onError) {
try {
txInfo.callbacks.onError(error, txHash);
} catch (callbackError) {
console.error(`错误回调执行失败 (${txHash}):`, callbackError);
}
}
}
});

// 等待所有查询完成
await Promise.allSettled(promises);
}

// 更新单个交易状态
async updateTransactionStatus(txHash) {
const txInfo = this.trackedTransactions.get(txHash);
if (!txInfo || txInfo.isFinalized) return;

// 获取交易收据
const receipt = await this.web3.eth.getTransactionReceipt(txHash);

if (!receipt) {
// 交易还未被确认,检查是否在交易池中
try {
const transaction = await this.web3.eth.getTransaction(txHash);

if (transaction) {
// 交易在交易池中
if (txInfo.callbacks.onStatusChange) {
try {
txInfo.callbacks.onStatusChange('pending', txHash, {
nonce: transaction.nonce,
gasPrice: transaction.gasPrice,
age: Date.now() - txInfo.startTime
});
} catch (callbackError) {
console.error(`状态回调执行失败 (${txHash}):`, callbackError);
}
}
} else {
// 交易不在交易池中,可能已被丢弃
if (txInfo.callbacks.onStatusChange) {
try {
txInfo.callbacks.onStatusChange('dropped', txHash, {
age: Date.now() - txInfo.startTime
});
} catch (callbackError) {
console.error(`状态回调执行失败 (${txHash}):`, callbackError);
}
}
}
} catch (error) {
console.warn(`检查交易池状态失败 (${txHash}):`, error);
}
return;
}

// 交易已被确认,计算确认数
const latestBlock = await this.web3.eth.getBlockNumber();
const confirmations = latestBlock - receipt.blockNumber + 1;
const previousConfirmations = txInfo.currentConfirmations;

// 更新确认数
txInfo.currentConfirmations = confirmations;

// 交易状态为成功或失败
const success = receipt.status === true || receipt.status === '0x1';
const transactionStatus = success ? 'confirmed' : 'failed';

// 调用状态变化回调
if (txInfo.callbacks.onStatusChange) {
try {
txInfo.callbacks.onStatusChange(transactionStatus, txHash, {
confirmations,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed,
success
});
} catch (callbackError) {
console.error(`状态回调执行失败 (${txHash}):`, callbackError);
}
}

// 调用确认回调(仅当确认数增加时)
if (confirmations > previousConfirmations && txInfo.callbacks.onConfirmation) {
try {
txInfo.callbacks.onConfirmation(confirmations, txHash, receipt);
} catch (callbackError) {
console.error(`确认回调执行失败 (${txHash}):`, callbackError);
}
}

// 检查是否达到所需确认数且未标记为已完成
if (confirmations >= txInfo.confirmationsNeeded && !txInfo.isFinalized) {
txInfo.isFinalized = true;

// 调用完成回调
if (txInfo.callbacks.onFinalized) {
try {
txInfo.callbacks.onFinalized(txHash, receipt, confirmations);
} catch (callbackError) {
console.error(`完成回调执行失败 (${txHash}):`, callbackError);
}
}

console.log(`交易 ${txHash} 已完成确认 (${confirmations}/${txInfo.confirmationsNeeded})`);
}
}
}

// 使用示例
const transactionTracker = new TransactionTracker();

// 跟踪交易,设置回调函数
const txHash = transactionTracker.trackTransaction('0x1234567890123456789012345678901234567890123456789012345678901234', {
confirmationsNeeded: 3, // 需要3个确认
onStatusChange: (status, hash, data) => {
console.log(`交易 ${hash} 状态变更为: ${status}`, data);
// 更新UI显示交易状态
updateTransactionUI(hash, status, data);
},
onConfirmation: (confirmations, hash, receipt) => {
console.log(`交易 ${hash} 已获得 ${confirmations} 个确认`);
// 更新UI显示确认数
updateConfirmationCount(hash, confirmations);
},
onFinalized: (hash, receipt, confirmations) => {
console.log(`交易 ${hash} 已完成所有 ${confirmations} 个确认`);
// 显示交易完成提示
showTransactionCompleteNotification(hash);
// 可以选择停止跟踪
// transactionTracker.stopTrackingTransaction(hash);
},
onError: (error, hash) => {
console.error(`跟踪交易 ${hash} 时发生错误:`, error);
// 显示错误提示
showTransactionErrorNotification(hash, error);
}
});

// 组件卸载时清理资源
function cleanup() {
transactionTracker.stopAllTracking();
}

3. 交易失败处理与重试

处理交易失败的情况,并提供重试机制。

// 交易重试管理器
class TransactionRetryManager {
constructor() {
this.maxRetries = 3;
this.baseRetryDelay = 3000; // 基础重试延迟(毫秒)
this.retryMultiplier = 2; // 指数退避乘数
}

// 检查交易是否可以重试
canRetryTransaction(error, transaction) {
// 可以重试的错误类型
const retryableErrors = [
'nonce too low',
'transaction underpriced',
'insufficient funds for gas * price + value',
'replacement transaction underpriced',
'timeout'
];

// 检查错误消息是否包含可重试的错误
const errorMessage = error.message.toLowerCase();
const isRetryable = retryableErrors.some(err => errorMessage.includes(err));

// 检查交易是否已经重试了最大次数
const retryCount = transaction.retryCount || 0;
const hasRetryAttemptsLeft = retryCount < this.maxRetries;

return isRetryable && hasRetryAttemptsLeft;
}

// 准备重试交易
async prepareRetryTransaction(originalTransaction, error) {
try {
const web3 = new Web3(window.ethereum);

// 创建新的交易对象,复制原始交易的所有属性
const retryTransaction = { ...originalTransaction };

// 增加重试计数
retryTransaction.retryCount = (originalTransaction.retryCount || 0) + 1;

// 获取最新的nonce
const nonce = await web3.eth.getTransactionCount(retryTransaction.from, 'pending');
retryTransaction.nonce = web3.utils.toHex(nonce);

// 根据错误类型调整Gas价格
if (error.message.toLowerCase().includes('nonce too low') ||
error.message.toLowerCase().includes('transaction underpriced') ||
error.message.toLowerCase().includes('replacement transaction underpriced')) {
// 增加Gas价格以替换之前的交易
const currentGasPrice = await web3.eth.getGasPrice();
const increasedGasPrice = web3.utils.toHex(Math.floor(parseInt(currentGasPrice, 16) * 1.2)); // 增加20%
retryTransaction.gasPrice = increasedGasPrice;

console.log(`准备重试交易,Gas价格从 ${currentGasPrice} 增加到 ${increasedGasPrice}`);
}

// 如果是EIP-1559交易,调整maxFeePerGas
if (retryTransaction.type === '0x2' && retryTransaction.maxFeePerGas) {
const currentBaseFee = await this.getCurrentBaseFee();
const newMaxFeePerGas = web3.utils.toHex(Math.floor(parseInt(currentBaseFee, 16) * 1.2));
retryTransaction.maxFeePerGas = newMaxFeePerGas;

console.log(`准备重试EIP-1559交易,maxFeePerGas设置为 ${newMaxFeePerGas}`);
}

return retryTransaction;
} catch (prepareError) {
console.error('准备重试交易失败:', prepareError);
throw prepareError;
}
}

// 获取当前基础费用
async getCurrentBaseFee() {
const web3 = new Web3(window.ethereum);
const latestBlock = await web3.eth.getBlock('latest');
return latestBlock.baseFeePerGas || '0x0';
}

// 执行交易重试
async retryTransaction(originalTransaction, error, signAndSendFunction) {
try {
// 检查是否可以重试
if (!this.canRetryTransaction(error, originalTransaction)) {
throw new Error(`交易无法重试(已达到最大重试次数或错误不可重试): ${error.message}`);
}

// 计算重试延迟(指数退避)
const retryCount = originalTransaction.retryCount || 0;
const delay = this.baseRetryDelay * Math.pow(this.retryMultiplier, retryCount);

console.log(`等待 ${delay}ms 后重试交易...`);
await new Promise(resolve => setTimeout(resolve, delay));

// 准备重试交易
const retryTransaction = await this.prepareRetryTransaction(originalTransaction, error);

// 显示重试确认对话框
const userConfirmed = await showRetryConfirmationDialog(retryTransaction, error, retryCount + 1, this.maxRetries);

if (!userConfirmed) {
throw new Error('用户取消了交易重试');
}

// 执行重试交易
console.log(`执行第 ${retryCount + 1} 次交易重试`);
const txHash = await signAndSendFunction(retryTransaction);

return {
success: true,
txHash: txHash,
retryCount: retryCount + 1,
transaction: retryTransaction
};
} catch (retryError) {
console.error('交易重试失败:', retryError);
throw retryError;
}
}

// 显示重试确认对话框
async showRetryConfirmationDialog(transaction, error, currentRetry, maxRetries) {
// 实际应用中应显示一个美观的UI对话框
console.log(`显示交易重试确认对话框 (${currentRetry}/${maxRetries}):`, {
error: error.message,
newGasPrice: transaction.gasPrice ? web3.utils.fromWei(transaction.gasPrice, 'gwei') + ' gwei' : 'N/A'
});

// 简化实现,实际应用中应使用UI组件
return new Promise((resolve) => {
// 模拟用户确认
setTimeout(() => resolve(true), 1000);
});
}
}

// 使用示例
const retryManager = new TransactionRetryManager();

async function processTransaction(transaction) {
try {
// 尝试发送交易
const txHash = await signAndSendTransaction(transaction);
return { success: true, txHash };
} catch (error) {
console.error('交易处理失败:', error);

// 尝试重试交易
try {
const retryResult = await retryManager.retryTransaction(
transaction,
error,
signAndSendTransaction // 传入签名发送函数
);

console.log('交易重试成功:', retryResult);
return { success: true, txHash: retryResult.txHash, isRetry: true };
} catch (retryError) {
console.error('交易重试也失败了:', retryError);

// 显示最终错误信息
showErrorMessage(`交易失败: ${retryError.message}`);

return { success: false, error: retryError };
}
}
}

批量交易处理

在某些场景下,需要处理多个相关交易,例如批量转账、批量NFT铸造等。

1. 顺序执行批量交易

// 顺序执行批量交易
async function executeBatchedTransactions(transactions, onProgress) {
const results = [];

try {
// 遍历所有交易
for (let i = 0; i < transactions.length; i++) {
const transaction = transactions[i];

try {
// 发送当前交易
console.log(`执行交易 ${i + 1}/${transactions.length}...`);

// 更新进度回调
if (onProgress) {
onProgress({
step: 'sending',
current: i + 1,
total: transactions.length,
transaction: transaction,
status: 'pending'
});
}

// 发送交易
const txHash = await signAndSendTransaction(transaction);

// 更新进度回调 - 交易已发送
if (onProgress) {
onProgress({
step: 'sent',
current: i + 1,
total: transactions.length,
transaction: transaction,
txHash: txHash,
status: 'sent'
});
}

// 等待交易确认(可选,根据需求决定是否等待)
const receipt = await waitForTransactionConfirmation(txHash);

// 检查交易是否成功
const success = receipt.status === true || receipt.status === '0x1';

// 记录结果
results.push({
index: i,
transaction: transaction,
txHash: txHash,
success: success,
receipt: receipt
});

// 更新进度回调 - 交易已确认
if (onProgress) {
onProgress({
step: 'confirmed',
current: i + 1,
total: transactions.length,
transaction: transaction,
txHash: txHash,
success: success,
receipt: receipt
});
}

} catch (txError) {
console.error(`交易 ${i + 1} 执行失败:`, txError);

// 记录失败结果
results.push({
index: i,
transaction: transaction,
success: false,
error: txError.message
});

// 更新进度回调 - 交易失败
if (onProgress) {
onProgress({
step: 'failed',
current: i + 1,
total: transactions.length,
transaction: transaction,
status: 'failed',
error: txError.message
});
}

// 决定是否继续执行后续交易
// 这里可以添加逻辑,例如询问用户是否继续
const shouldContinue = await shouldContinueAfterFailure(i, transactions.length, txError);
if (!shouldContinue) {
// 用户选择停止,中断执行
break;
}
}
}

// 计算成功率和统计信息
const successfulCount = results.filter(r => r.success).length;
const failedCount = results.filter(r => !r.success).length;
const totalCount = results.length;

return {
results: results,
summary: {
total: totalCount,
successful: successfulCount,
failed: failedCount,
successRate: totalCount > 0 ? (successfulCount / totalCount * 100).toFixed(2) + '%' : '0%'
},
isComplete: totalCount === transactions.length
};
} catch (error) {
console.error('批量交易执行失败:', error);
throw error;
}
}

// 等待交易确认
async function waitForTransactionConfirmation(txHash, confirmations = 1) {
const web3 = new Web3(window.ethereum);

return new Promise((resolve, reject) => {
let confirmationCount = 0;

// 设置检查间隔
const checkInterval = setInterval(async () => {
try {
const receipt = await web3.eth.getTransactionReceipt(txHash);

if (receipt) {
const latestBlock = await web3.eth.getBlockNumber();
const currentConfirmations = latestBlock - receipt.blockNumber + 1;

if (currentConfirmations >= confirmations) {
clearInterval(checkInterval);
resolve(receipt);
} else {
console.log(`交易 ${txHash} 已获得 ${currentConfirmations}/${confirmations} 个确认`);
}
}
} catch (error) {
clearInterval(checkInterval);
reject(error);
}
}, 2000);

// 设置超时(例如10分钟)
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error(`交易确认超时: ${txHash}`));
}, 10 * 60 * 1000);
});
}

// 失败后是否继续
async function shouldContinueAfterFailure(currentIndex, totalCount, error) {
// 实际应用中应显示一个确认对话框
console.log(`交易 ${currentIndex + 1} 失败,是否继续执行剩余 ${totalCount - currentIndex - 1} 个交易?`);

// 简化实现,默认继续
return true;
}

// 使用示例
const batchTransactions = [
// 多个交易对象
await createEthTransferTransaction(fromAddress, '0x1111...', '0.01'),
await createEthTransferTransaction(fromAddress, '0x2222...', '0.02'),
await createEthTransferTransaction(fromAddress, '0x3333...', '0.03')
];

const batchResult = await executeBatchedTransactions(batchTransactions, (progress) => {
console.log(`批量交易进度: ${progress.current}/${progress.total} - ${progress.step}`);
// 更新UI显示进度
updateBatchTransactionUI(progress);
});

console.log('批量交易执行完成:', batchResult.summary);

交易安全最佳实践

1. 防止重放攻击

重放攻击是一种常见的区块链安全威胁,攻击者尝试重复使用之前的交易。

// 防止重放攻击的措施
function protectAgainstReplayAttacks(transaction, currentNetwork) {
// 1. 设置正确的chainId
if (!transaction.chainId || transaction.chainId !== currentNetwork.chainId) {
console.warn('交易chainId不正确,可能存在重放攻击风险');
transaction.chainId = currentNetwork.chainId;
}

// 2. 使用递增的nonce
// 确保每次交易使用唯一的nonce

// 3. 显示交易详情确认
showDetailedTransactionConfirmation(transaction);

// 4. 实现交易哈希本地存储与验证
storeAndVerifyTransactionHash(transaction);

return transaction;
}

// 存储并验证交易哈希
function storeAndVerifyTransactionHash(transaction) {
try {
// 生成交易的唯一标识符(不包括nonce和gasPrice)
const txIdentifier = createTransactionIdentifier(transaction);

// 检查本地存储中是否已有相同的交易
const storedTxHashes = JSON.parse(localStorage.getItem('recent_transactions') || '[]');

const existingTx = storedTxHashes.find(tx => tx.identifier === txIdentifier);

if (existingTx) {
const timeDiff = Date.now() - existingTx.timestamp;
// 如果5分钟内有相同的交易,提示用户
if (timeDiff < 5 * 60 * 1000) {
console.warn('检测到近期相似的交易,可能存在重复提交风险');
showWarningMessage('检测到近期提交过相似的交易,请确认是否继续');
}
}

// 生成交易哈希(用于显示给用户验证)
const web3 = new Web3();
const txHash = web3.utils.keccak256(JSON.stringify(transaction));

// 存储交易哈希和标识符
storedTxHashes.push({
identifier: txIdentifier,
hash: txHash,
timestamp: Date.now(),
transaction: sanitizeTransactionForStorage(transaction)
});

// 只保留最近50条交易记录
if (storedTxHashes.length > 50) {
storedTxHashes.splice(0, storedTxHashes.length - 50);
}

localStorage.setItem('recent_transactions', JSON.stringify(storedTxHashes));

return txHash;
} catch (error) {
console.error('存储或验证交易哈希失败:', error);
// 即使存储失败,也应该继续交易流程
return null;
}
}

// 创建交易唯一标识符
function createTransactionIdentifier(transaction) {
// 基于交易的关键属性创建标识符,排除会变化的nonce和gasPrice
const { from, to, value, data, chainId } = transaction;
return Web3.utils.keccak256(
JSON.stringify({ from, to, value, data, chainId })
);
}

// 清理交易数据以便存储
function sanitizeTransactionForStorage(transaction) {
// 移除敏感信息,只保留必要的交易详情
const { from, to, value, chainId, data } = transaction;
return { from, to, value, chainId, data: data ? data.substring(0, 10) + '...' : null };
}

2. 交易费用保护

防止用户支付过高的Gas费用。

// 交易费用保护机制
function enforceTransactionCostLimits(transaction, userSettings) {
const web3 = new Web3();

// 获取用户设置的费用限制
const maxGasPriceGwei = userSettings.maxGasPriceGwei || 100;
const maxTotalFeeEth = userSettings.maxTotalFeeEth || 0.1;

// 计算当前交易的Gas价格和总费用
let gasPriceGwei, totalFeeEth;

if (transaction.maxFeePerGas) {
// EIP-1559交易
gasPriceGwei = web3.utils.fromWei(transaction.maxFeePerGas, 'gwei');
totalFeeEth = (parseInt(transaction.maxFeePerGas, 16) * parseInt(transaction.gasLimit, 16)) / Math.pow(10, 18);
} else {
// 传统交易
gasPriceGwei = web3.utils.fromWei(transaction.gasPrice, 'gwei');
totalFeeEth = (parseInt(transaction.gasPrice, 16) * parseInt(transaction.gasLimit, 16)) / Math.pow(10, 18);
}

// 检查Gas价格是否超过限制
if (parseFloat(gasPriceGwei) > maxGasPriceGwei) {
console.warn(`Gas价格 ${gasPriceGwei} gwei 超过用户设置的限制 ${maxGasPriceGwei} gwei`);

// 提示用户并提供降低Gas价格的选项
const userConfirmed = showHighGasPriceWarning(gasPriceGwei, maxGasPriceGwei);

if (!userConfirmed) {
throw new Error('用户取消了高Gas价格的交易');
}
}

// 检查总交易费用是否超过限制
if (totalFeeEth > maxTotalFeeEth) {
console.warn(`交易总费用 ${totalFeeEth.toFixed(6)} ETH 超过用户设置的限制 ${maxTotalFeeEth} ETH`);

// 提示用户并提供确认选项
const userConfirmed = showHighTransactionFeeWarning(totalFeeEth, maxTotalFeeEth);

if (!userConfirmed) {
throw new Error('用户取消了高费用的交易');
}
}

// 检查Gas Limit是否合理
const gasLimit = parseInt(transaction.gasLimit, 16);
if (gasLimit > 1000000) { // 100万Gas作为示例上限
console.warn(`Gas Limit ${gasLimit} 异常高,可能存在风险`);

// 提示用户并建议审核交易
showAbnormallyHighGasLimitWarning(gasLimit);
}

return transaction;
}

// 高Gas价格警告
function showHighGasPriceWarning(currentPrice, maxAllowedPrice) {
// 实际应用中应显示一个警告对话框
console.log(`警告: 当前Gas价格 (${currentPrice} gwei) 高于您设置的最大限制 (${maxAllowedPrice} gwei)。继续可能会支付更多费用。`);

// 简化实现,默认返回true
return true;
}

3. 安全的交易确认UI

创建一个安全、清晰的交易确认界面,帮助用户理解他们正在签署的交易内容。

// 安全的交易确认组件
function SecureTransactionConfirmation({ transaction, onConfirm, onReject }) {
const [isReviewing, setIsReviewing] = useState(false);
const web3 = new Web3();

// 计算交易费用
const gasPrice = transaction.maxFeePerGas ? transaction.maxFeePerGas : transaction.gasPrice;
const gasLimit = transaction.gasLimit;
const totalFeeWei = parseInt(gasPrice, 16) * parseInt(gasLimit, 16);
const totalFeeEth = web3.utils.fromWei(totalFeeWei.toString(), 'ether');
const gasPriceGwei = web3.utils.fromWei(gasPrice, 'gwei');

// 格式化地址(显示前6位和后4位)
const formatAddress = (address) => {
if (!address) return 'None';
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
};

// 处理交易详情审核
const handleReviewDetails = () => {
setIsReviewing(true);
// 这里可以添加额外的安全检查逻辑
performSecurityChecks(transaction);
};

return (
<div className="secure-transaction-confirmation">
<div className="confirmation-header">
<h3>确认交易</h3>
<div className="security-badge">安全验证中</div>
</div>

<div className="transaction-summary">
<div className="summary-item">
<label>发送方</label>
<div className="address">{formatAddress(transaction.from)} <CopyButton text={transaction.from} /></div>
</div>

<div className="summary-item">
<label>接收方</label>
<div className="address">{formatAddress(transaction.to)} <CopyButton text={transaction.to} /></div>
</div>

{transaction.value && (
<div className="summary-item">
<label>金额</label>
<div className="amount">
{web3.utils.fromWei(transaction.value, 'ether')} ETH
</div>
</div>
)}

<div className="summary-item highlighted">
<label>Gas费用</label>
<div className="gas-details">
<div>Gas价格: {gasPriceGwei} gwei</div>
<div>Gas Limit: {parseInt(gasLimit, 16).toLocaleString()}</div>
<div className="total-fee">总费用: <strong>{totalFeeEth} ETH</strong></div>
</div>
</div>

{transaction.data && transaction.data !== '0x' && (
<div className="summary-item">
<label>交易数据</label>
<div className="transaction-data">
{isReviewing ? (
<div className="data-preview">{transaction.data}</div>
) : (
<button onClick={handleReviewDetails} className="review-button">
查看交易数据
</button>
)}
</div>
</div>
)}
</div>

<div className="security-notice">
<div className="notice-icon">⚠️</div>
<p>请仔细检查交易详情。一旦确认,交易将无法撤销。</p>
</div>

<div className="confirmation-actions">
<button
className="reject-button"
onClick={onReject}
>
取消
</button>
<button
className="confirm-button"
onClick={onConfirm}
disabled={isReviewing && !confirmSecurityChecks()}
>
确认交易
</button>
</div>
</div>
);
}

// 执行安全检查
function performSecurityChecks(transaction) {
// 实现各种安全检查,例如:

// 1. 检查接收地址是否为黑名单地址
checkIfAddressIsBlacklisted(transaction.to);

// 2. 检查交易数据是否包含可疑模式
scanTransactionDataForSuspiciousPatterns(transaction.data);

// 3. 验证合约代码(如果接收方是合约)
if (transaction.to) {
verifyContractCode(transaction.to);
}

// 4. 检查交易是否与之前的交易相似
checkForSimilarRecentTransactions(transaction);
}

总结

交易签名与确认是Web3应用中最核心、最敏感的功能,直接关系到用户资产的安全和使用体验。本章详细介绍了交易的完整生命周期,包括交易创建、Gas估算、签名发送、状态跟踪和确认等关键环节,并提供了丰富的代码示例和最佳实践。

在实际开发中,应特别注意以下几点:

  1. 提供透明的交易信息:清晰地向用户展示交易详情、Gas费用和预计确认时间
  2. 实现智能的Gas策略:根据网络状况动态调整Gas价格,提供不同优先级的交易选项
  3. 确保交易的安全性:实施防重放攻击措施、交易费用限制和安全的确认UI
  4. 提供完善的错误处理和重试机制:优雅地处理交易失败的情况,为用户提供重试选项
  5. 跟踪交易状态并及时反馈:实时更新交易状态,让用户了解交易的进展情况
  6. 支持批量交易处理:针对需要执行多个相关交易的场景,提供高效的批量处理功能

通过遵循这些最佳实践,开发者可以为用户提供安全、高效、便捷的Web3交易体验,增强用户对DApp的信任度和满意度。