处理以太坊交易的最佳实践与代码实现

·

在去中心化应用(dapp)开发中,处理以太坊虚拟机(EVM)交易是核心功能之一。无论是使用 Wagmi 框架还是原生 JavaScript,开发者都需要掌握发送交易、实时跟踪状态、估算燃气成本以及处理错误等关键技能。本文将详细介绍这些功能的实现方法,并提供代码示例。

使用 Wagmi 处理交易

Wagmi 是一个流行的 React Hooks 库,专门用于简化以太坊交互。它提供了丰富的钩子函数,帮助开发者轻松发送交易并跟踪其状态。

基础交易示例

以下示例展示了如何使用 Wagmi 发送一笔简单的 ETH 转账交易:

import { parseEther } from "viem"
import { useSendTransaction, useWaitForTransactionReceipt } from "wagmi"

function SendTransaction() {
  const {
    data: hash,
    error,
    isPending,
    sendTransaction
  } = useSendTransaction()

  const {
    isLoading: isConfirming,
    isSuccess: isConfirmed
  } = useWaitForTransactionReceipt({
    hash
  })

  async function handleSend() {
    sendTransaction({
      to: "0x...",
      value: parseEther("0.1") // 0.1 ETH
    })
  }

  return (
    <div>
      <button onClick={handleSend} disabled={isPending}>
        {isPending ? "Confirming..." : "Send 0.1 ETH"}
      </button>

      {hash && (
        <div>
          <div>Transaction Hash: {hash}</div>
          {isConfirming && <div>Waiting for confirmation...</div>}
          {isConfirmed && <div>Transaction confirmed!</div>}
        </div>
      )}

      {error && <div>Error: {error.message}</div>}
    </div>
  )
}

带燃气估算的高级交易

对于更复杂的交易,预先估算燃气可以提升用户体验和交易成功率:

import { parseEther } from "viem"
import {
  useSendTransaction,
  useWaitForTransactionReceipt,
  useEstimateGas
} from "wagmi"

function AdvancedTransaction() {
  const transaction = {
    to: "0x...",
    value: parseEther("0.1"),
    data: "0x..." // 可选合约交互数据
  }

  // 估算燃气
  const { data: gasEstimate } = useEstimateGas(transaction)

  const { sendTransaction } = useSendTransaction({
    ...transaction,
    gas: gasEstimate,
    onSuccess: (hash) => {
      console.log("Transaction sent:", hash)
    }
  })

  return <button onClick={() => sendTransaction()}>Send with Gas Estimate</button>
}

使用原生 JavaScript 处理交易

对于不使用前端框架的项目,可以直接通过以太坊提供商的 JSON-RPC 方法处理交易。

基础交易实现

基础交易涉及三个核心 RPC 方法:eth_requestAccountseth_sendTransactioneth_getTransactionReceipt

async function sendTransaction(recipientAddress, amount) {
  try {
    // 获取当前账户
    const accounts = await ethereum.request({
      method: "eth_requestAccounts"
    });
    const from = accounts[0];

    // 将 ETH 金额转换为 wei(十六进制)
    const value = `0x${(amount * 1e18).toString(16)}`;

    // 准备交易对象
    const transaction = {
      from,
      to: recipientAddress,
      value,
      // 燃气字段可选 - MetaMask 会自动估算
    };

    // 发送交易
    const txHash = await ethereum.request({
      method: "eth_sendTransaction",
      params: [transaction],
    });

    return txHash;
  } catch (error) {
    if (error.code === 4001) {
      throw new Error("Transaction rejected by user");
    }
    throw error;
  }
}

// 跟踪交易状态
function watchTransaction(txHash) {
  return new Promise((resolve, reject) => {
    const checkTransaction = async () => {
      try {
        const tx = await ethereum.request({
          method: "eth_getTransactionReceipt",
          params: [txHash],
        });

        if (tx) {
          if (tx.status === "0x1") {
            resolve(tx);
          } else {
            reject(new Error("Transaction failed"));
          }
        } else {
          setTimeout(checkTransaction, 2000); // 每2秒检查一次
        }
      } catch (error) {
        reject(error);
      }
    };

    checkTransaction();
  });
}

前端界面集成示例

<div>
  <input type="text" id="recipient" placeholder="Recipient Address">
  <input type="number" id="amount" placeholder="Amount (ETH)">
  <button onclick="handleSend()">Send ETH</button>
  <div id="status"></div>
</div>

<script>
async function handleSend() {
  const recipient = document.getElementById("recipient").value;
  const amount = document.getElementById("amount").value;
  const status = document.getElementById("status");

  try {
    status.textContent = "Sending transaction...";
    const txHash = await sendTransaction(recipient, amount);
    status.textContent = `Transaction sent: ${txHash}`;

    // 等待确认
    status.textContent = "Waiting for confirmation...";
    await watchTransaction(txHash);
    status.textContent = "Transaction confirmed!";
  } catch (error) {
    status.textContent = `Error: ${error.message}`;
  }
}
</script>

带燃气估算的高级实现

通过 eth_estimateGas 方法可以预先估算交易所需燃气,并添加安全缓冲:

async function estimateGas(transaction) {
  try {
    const gasEstimate = await ethereum.request({
      method: "eth_estimateGas",
      params: [transaction]
    });

    // 添加20%的安全缓冲
    return BigInt(gasEstimate) * 120n / 100n;
  } catch (error) {
    console.error("Gas estimation failed:", error);
    throw error;
  }
}

交易处理最佳实践

交易安全性

错误处理策略

用户体验优化

常见交易错误代码及处理

错误代码描述解决方案
4001用户拒绝交易显示重试选项和清晰的错误信息
-32603资金不足在发送交易前检查余额
-32000燃气设置过低增加燃气限制或为估算添加缓冲
-32002请求已在进行中防止多个并发交易

👉 获取更多交易处理技巧

常见问题

如何处理用户拒绝交易的情况?

当用户拒绝交易时(错误代码4001),应该友好地提示用户交易已被取消,并提供重新发起交易的选项。避免使用技术性过强的术语,保持提示信息简洁明了。

为什么需要为燃气估算添加缓冲?

网络条件和区块空间需求不断变化,添加20%左右的缓冲可以确保交易有足够燃气顺利执行,避免因燃气不足导致的交易失败。这在网络拥堵时期尤为重要。

如何准确跟踪交易状态?

使用 eth_getTransactionReceipt 方法定期检查交易收据。当收据可用时,检查 status 字段是否为 "0x1"(成功)或 "0x0"(失败)。建议设置合理的轮询间隔,如每2秒检查一次。

什么时候应该使用燃气估算?

对于简单转账,MetaMask 可以自动处理燃气估算。但对于合约交互或复杂交易,预先估算燃气可以提供更准确的成本预测和更好的用户体验,特别是在显示交易确认界面之前。

如何处理资金不足的错误?

在发送交易前检查用户余额,确保其有足够的主链币(如ETH)覆盖交易金额和燃气费用。如果余额不足,应该提前提示用户并阻止交易发送,而不是等待交易失败。

如何优化交易的用户体验?

提供清晰的交易状态反馈、实时进度更新和详细的交易信息。使用加载动画、状态提示和错误恢复选项来增强用户体验,让用户在整个过程中感到 informed 和掌控。

通过遵循这些最佳实践和代码示例,开发者可以构建出既安全又用户友好的交易处理功能,提升去中心化应用的整体质量。