Introduction to EVM Transaction Handling
Building decentralized applications (dApps) requires robust transaction handling capabilities. Whether you're using Wagmi or Vanilla JavaScript, this guide will walk you through all aspects of EVM transaction management including:
๐ Best practices for secure transaction handling
- Transaction sending and status tracking
- Accurate gas cost estimation
- Graceful error handling
- Complex transaction pattern management
Using Wagmi for Transaction Management
Wagmi provides excellent React hooks for handling Ethereum transactions. Let's explore two common use cases.
Basic ETH 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...",
value: parseEther("0.1")
})
}
return (
<div>
<button onClick={handleSend} disabled={isPending}>
{isPending ? "Confirming..." : "Send 0.1 ETH"}
</button>
{hash && (
<div>
Transaction Hash: {hash}
{isConfirming && <p>Waiting for confirmation...</p>}
{isConfirmed && <p>Transaction confirmed!</p>}
</div>
)}
{error && <p>Error: {error.message}</p>}
</div>
)
}Advanced Transaction With Gas Estimation
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>
)
}Vanilla JavaScript Implementation
For those not using React frameworks, here's how to implement transaction handling directly.
Basic ETH Transfer
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)}`;
const transaction = {
from,
to: recipientAddress,
value
};
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);
}
} catch (error) {
reject(error);
}
};
checkTransaction();
});
}UI Implementation Example
<div>
<h2>Send ETH</h2>
<form id="sendForm">
<input type="text" id="recipient" placeholder="Recipient address">
<input type="number" id="amount" placeholder="ETH amount">
<button type="button" onclick="handleSend()">Send</button>
</form>
<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>Advanced Transaction With Gas Estimation
async function estimateGas(transaction) {
try {
const gasEstimate = await ethereum.request({
method: "eth_estimateGas",
params: [transaction]
});
return BigInt(gasEstimate) * 120n / 100n;
} catch (error) {
console.error("Gas estimation failed:", error);
throw error;
}
}Best Practices for Transaction Handling
Transaction Security
- Validate all user inputs thoroughly
- Verify recipient addresses before sending
- Check wallet balances for sufficient funds
- Implement multi-factor authentication for sensitive operations
๐ Essential security measures for dApp developers
Error Handling Strategies
| Error Code | Description | Recommended Solution |
|---|---|---|
4001 | User rejected transaction | Show friendly message with retry option |
-32603 | Insufficient funds | Check balance before transaction |
-32000 | Gas too low | Add 20% buffer to estimate |
-32002 | Concurrent request | Prevent duplicate transactions |
User Experience Considerations
- Provide clear transaction status updates
- Show estimated completion times
- Offer transaction cancellation options
- Display detailed error explanations
FAQ Section
How do I estimate gas costs accurately?
Use the eth_estimateGas RPC method and add a 20% buffer to account for network fluctuations.
What should I do if a user rejects a transaction?
Provide a clear message explaining what the transaction was for and offer an easy way to retry.
How can I track transaction status?
Use eth_getTransactionReceipt to poll for status updates or WebSocket subscriptions for real-time updates.
What's the best way to handle network congestion?
Implement dynamic gas pricing that adjusts based on current network conditions.
How do I validate recipient addresses?
Use checksum validation and consider implementing address confirmation screens.
Expanding Dapp Functionality
To enhance your dapp's capabilities, consider adding:
- User authentication flows
- Multi-network support
- Smart contract interaction layers
- Transaction history tracking