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:
eth_requestAccounts: Requests user permission to access their account.eth_sendTransaction: Sends the transaction.eth_getTransactionReceipt: Polls for transaction confirmation.
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:
- Validate all inputs — ensure addresses are properly formatted and values are within expected ranges.
- Verify wallet balances before allowing transaction submission to avoid failed transactions.
- Use checksummed addresses or validation libraries like
ethers.utils.isAddress().
Error Handling
Common errors include user rejection, insufficient funds, and gas issues. Handle them gracefully:
- Error Code
4001: User rejected the transaction — prompt with a retry option. - Error
-32603: Insufficient funds — display balance and suggest lowering amount. - Error
-32000: Gas too low — increase limit or apply a buffer (e.g., +20%). - Error
-32002: Request already pending — disable buttons during processing.
Provide clear, actionable feedback so users understand what went wrong and how to fix it.
User Experience Optimization
A smooth UX keeps users engaged:
- Show loading indicators during signing and confirmation.
- Display real-time transaction status updates.
- Present transaction details like gas fee, network, and estimated time.
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.