一笔交易的旅程:前端构造 → 钱包签名 → RPC 广播 → mempool 等待 → 矿工/PBS 打包 → 区块确认。理解这个流程的每一步,才能写出不丢交易、不错 gas、不卡 UX 的 DApp。
14.3.1 交易对象的基本结构
graph LR
subgraph Tx["EIP-1559 交易对象"]
T1[to: 目标地址]
T2[data: 0x 调用数据]
T3[value: ETH 转账金额]
T4[maxFeePerGas: 每 gas 最高总费]
T5[maxPriorityFeePerGas: 给矿工/validator 的小费]
T6[gasLimit: 最大 gas 数量]
T7[nonce: 发送者交易计数]
T8[chainId: 网络标识]
end
T4 --" - "--> T4a[" = baseFee + maxPriorityFee"]
T4a --"但最多只收"--> T4b["baseFee 由网络动态调整\n小费给 validator 决定顺序"]
style Tx fill:#e3f2fd
Gas 价格计算
typescript
/**
* Gas 价格估算与交易构造
*/
interface TransactionRequest {
to: string;
data: string;
value: bigint;
maxFeePerGas: bigint;
maxPriorityFeePerGas: bigint;
gasLimit: bigint;
nonce: number;
chainId: number;
}
class GasEstimator {
// EIP-1559 的 baseFee 由网络根据区块容量动态调整:
// target = 15M gas/block(上限 30M)
// 如果区块超过 target,baseFee 增加最多 12.5%
// 如果低于,baseFee 减少最多 12.5%
async estimate1559Fees(blockUtilization: number): Promise<{[key: string]: bigint}> {
const currentBaseFee = 20n * 10n**9n; // 20 gwei (示例)
const target = 0.5; // 50% 利用率 = 稳定
// baseFee 变化公式 (简化)
let newBaseFee: bigint;
if (blockUtilization > 0.5) {
// 拥堵:baseFee 增加
const increase = 1n + BigInt(Math.floor((blockUtilization - 0.5) * 1000));
newBaseFee = currentBaseFee * increase / 1000n;
} else {
// 空闲:baseFee 减少
const decrease = 1n - BigInt(Math.floor((0.5 - blockUtilization) * 1000));
newBaseFee = currentBaseFee * decrease / 1000n;
}
const priorityFee = 2n * 10n**9n; // 2 gwei 小费
const maxFee = newBaseFee * 2n + priorityFee; // 余量
return {
baseFee: newBaseFee,
maxPriorityFeePerGas: priorityFee,
maxFeePerGas: maxFee,
};
}
// gas 限制估算(调用节点的 gasEstimate 模拟执行)
async estimateGasLimit(tx: Partial<TransactionRequest>): Promise<bigint> {
// 模拟:简单 ETH 转账 21,000 gas
if (!tx.data || tx.data === '0x') return 21000n;
// 合约调用:通常 50K-500K 不等
return 150000n; // 示例
}
}
// 自动构造交易
async function buildTransaction(args: {
to: string;
value?: bigint;
data?: string;
priority?: 'low' | 'normal' | 'high';
}): Promise<TransactionRequest> {
const estimator = new GasEstimator();
const fees = await estimator.estimate1559Fees(0.7); // 70% 利用率 = 轻度拥堵
const gasLimit = await estimator.estimateGasLimit(args);
// 根据 priority 调整小费
const multiplier = { low: 0.8, normal: 1.0, high: 2.0 }[args.priority || 'normal'];
return {
to: args.to,
data: args.data || '0x',
value: args.value || 0n,
maxFeePerGas: fees.maxFeePerGas * BigInt(Math.floor(multiplier * 100)) / 100n,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas * BigInt(Math.floor(multiplier * 100)) / 100n,
gasLimit,
nonce: 42, // 实际需要 eth_getTransactionCount
chainId: 1,
};
}
console.log("Gas 估算器已定义");14.3.2 交易生命周期
sequenceDiagram
participant User as 用户
participant DApp as 前端 DApp
participant Wallet as 钱包
participant RPC as RPC 节点
participant Mempool as Mempool
participant Builder as Block Builder
participant Chain as 链
User ->> DApp: 点击 "Swap"
DApp ->> DApp: 构造交易对象
DApp ->> Wallet: eth_sendTransaction
Wallet ->> Wallet: 用户确认 + 签名
Wallet -->> DApp: raw signed tx
DApp ->> RPC: eth_sendRawTransaction
RPC ->> Mempool: 加入 mempool
Mempool -->> RPC: tx hash
RPC -->> DApp: pending 回执
Builder ->> Mempool: SEAL 搜索 MEV
Builder ->> Builder: 构建区块
Builder ->> Chain: 打包入块
Chain -->> Builder: 区块收据
DApp ->> RPC: eth_getTransactionReceipt (轮询)
RPC -->> DApp: receipt (status, gas, logs)
DApp ->> User: 显示结果
Note over DApp: 乐观更新:先显示成功,<br/>等 receipt 最终确认
14.3.3 状态追踪与乐观更新
typescript
/**
* 交易状态追踪 + 乐观更新
*/
type TxStatus = "initiated" | "pending" | "success" | "reverted" | "dropped";
interface TransactionState {
hash: string;
status: TxStatus;
confirmations: number;
receipt?: {
gasUsed: bigint;
logs: any[];
status: string; // "0x1" = success, "0x0" = revert
};
}
class TransactionTracker {
private transactions: Map<string, TransactionState> = new Map();
private listeners: Set<(txs: TransactionState[]) => void> = new Set();
// 乐观更新:前端先假设成功
async optimisticallySend(
sendFn: () => Promise<string>, // 返回 txHash
preview: () => void, // 先更新 UI
): Promise<string> {
preview(); // 立即更新UI(乐观)
const hash = await sendFn(); // 但实际还要等待链上确认
this.transactions.set(hash, {
hash,
status: "pending",
confirmations: 0,
});
this.pollForConfirmation(hash);
return hash;
}
private async pollForConfirmation(hash: string) {
for (let i = 0; i < 60; i++) { // 最多等 5 分钟
await new Promise(r => setTimeout(r, 5000));
try {
const receipt = await this.fetchReceipt(hash);
if (receipt) {
const status: TxStatus = receipt.status === "0x1" ? "success" : "reverted";
this.transactions.set(hash, {
hash,
status,
confirmations: 1, // 后续可增加
receipt,
});
this.notifyListeners();
if (status === "reverted") {
// 回滚乐观更新
this.rollbackPreview(hash);
}
return;
}
} catch {
// 继续轮询
}
}
// 超时:标记为 dropped
this.transactions.set(hash, { hash, status: "dropped", confirmations: 0 });
}
private fetchReceipt(hash: string): Promise<any> {
// 实际调用 eth_getTransactionReceipt
return Promise.resolve({ gasUsed: 145000n, logs: [], status: "0x1" });
}
private rollbackPreview(hash: string) {
console.log("Transaction reverted, rolling back UI:", hash);
}
private notifyListeners() {
const txs = Array.from(this.transactions.values());
for (const cb of this.listeners) cb(txs);
}
subscribe(cb: (txs: TransactionState[]) => void) {
this.listeners.add(cb);
return () => this.listeners.delete(cb);
}
}
// React 风格的自定义 hook(概念性展示)
function useSendTransaction() {
const [status, setStatus] = React.useState<TxStatus>("initiated");
async function send(tx: TransactionRequest) {
setStatus("pending");
try {
// 实际签名广播
const hash = "0x...";
setStatus("success");
return hash;
} catch (e) {
setStatus("dropped");
throw e;
}
}
return { send, status };
}14.3.4 常见错误诊断
| 错误 | 原因 | 解决 |
|---|---|---|
insufficient funds | 余额不足以支付 gas + value | 检查 ETH 余额 |
maxFeePerGas < baseFee | gas 价格设定低于网络 baseFee | 使用最新 baseFee 的 2 倍 |
nonce too high/low | nonce 不正确 | 顺序发送,或用 eth_getTransactionCount |
replacement fee too low | 加速旧交易时新交易小费不够高 | 小费至少增加 10% |
intrinsic gas too low | gas 限制 < 21,000 | 至少 21,000 基础费用 |
execution reverted | 合约拒绝(无 gas 问题) | 检查 revert reason 和参数 |
> ← 14.2 钱包连接 | 前往 → 14.4 事件订阅与状态同步 |*
评论
0评论加载中…