Handle Transactions in EVM-Based DApps: A Developer’s Guide

·

Interacting with Ethereum Virtual Machine (EVM) networks is a core functionality for any decentralized application (dApp). Whether you're building with modern frameworks like Wagmi or opting for direct integration using Vanilla JavaScript, handling transactions securely and efficiently is essential. This guide walks you through best practices, code examples, and key strategies for sending, tracking, and optimizing EVM transactions in your dApp.

Core keywords: EVM transactions, Wagmi, Vanilla JavaScript, transaction handling, gas estimation, MetaMask integration, error handling, user experience


Sending Transactions with Wagmi

Wagmi is a popular React Hooks library that simplifies interaction with EVM-compatible blockchains. It abstracts complex Web3 logic into reusable hooks, making transaction handling intuitive and reliable.

Basic Transaction Example

A basic transaction typically involves sending ETH from one address to another. The following example uses useSendTransaction and useWaitForTransactionReceipt to manage the lifecycle of a simple transfer.

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...", // Recipient address
      value: parseEther("0.1"), // Send 0.1 ETH
    });
  }

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

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

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

👉 Learn how to integrate secure wallet connections in your dApp today.

This pattern ensures users are informed at every stage — from initiation to confirmation — enhancing transparency and trust.

Advanced Transaction with Gas Estimation

For more control over gas costs, you can estimate gas before submission. This helps prevent failed transactions due to insufficient gas limits.

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

function AdvancedTransaction() {
  const transaction = {
    to: "0x...",
    value: parseEther("0.1"),
    data: "0x...", // Optional contract interaction data
  };

  const { data: gasEstimate } = useEstimateGas(transaction);

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

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

By pre-estimating gas, your dApp avoids unnecessary rejections and improves user experience during network congestion.


Implementing Transactions in Vanilla JavaScript

If you're not using a framework, you can still interact directly with MetaMask via Ethereum's JSON-RPC API.

Basic Transaction Using RPC Methods

The following example uses three core methods:

async function sendTransaction(recipientAddress, amount) {
  try {
    const accounts = await ethereum.request({ method: "eth_requestAccounts" });
    const from = accounts[0];
    
    const value = `0x${(amount * 1e18).toString(16)}`; // Convert ETH to wei (hex)

    const transaction = {
      from,
      to: recipientAddress,
      value,
      // Gas fields optional — MetaMask auto-estimates
    };

    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;
  }
}

// Track transaction status
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); // Check every 2 seconds
        }
      } catch (error) {
        reject(error);
      }
    };

    checkTransaction();
  });
}

Here’s how you might use it in a frontend form:

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

<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>

👉 Discover seamless Web3 integration tools to boost your development speed.


Best Practices for Secure and User-Friendly Transactions

Transaction Security

Security should be your top priority when handling transactions:

Error Handling

Common errors include user rejection, insufficient funds, and gas issues. Handle them gracefully:

Provide clear, actionable feedback so users understand what went wrong and how to fix it.

User Experience Optimization

A smooth UX keeps users engaged:


Frequently Asked Questions

Q: What is the difference between eth_sendTransaction and eth_estimateGas?
A: eth_sendTransaction submits a transaction to the network, while eth_estimateGas calculates the approximate gas needed before sending.

Q: How do I handle user rejection in MetaMask?
A: Catch error code 4001. Display a user-friendly message and allow retry without restarting the flow.

Q: Why should I estimate gas instead of relying on MetaMask’s default?
A: Manual estimation gives you better control, especially for complex contract interactions where defaults may be inaccurate.

Q: Can I send transactions without user approval?
A: No. For security reasons, every transaction must be manually approved by the user through their wallet.

Q: How often should I poll for transaction receipts?
A: Every 2–5 seconds is reasonable. Too frequent polling increases load; too infrequent delays feedback.

Q: Is Wagmi compatible with all EVM chains?
A: Yes. Wagmi supports Ethereum, Polygon, Binance Smart Chain, Arbitrum, Optimism, and any EVM-compatible network.


👉 Start building high-performance dApps with advanced wallet connectivity solutions.