Sending a transaction on the Ethereum blockchain is just the first step. The real challenge is confirming whether it was successfully included in a block or if it failed. Manually checking a block explorer every time isn’t efficient—especially when building automated systems.
This guide walks you through programmatically confirming Ethereum transactions using the Go programming language (Golang) and the go-ethereum library. You’ll learn how to check transaction status, handle pending states, and interpret receipt data to determine success or failure.
Understanding Ethereum Transaction Confirmation
When you submit a transaction to the Ethereum network, it enters a mempool—a waiting area where validators pick it up for inclusion in a block. This process can take anywhere from a few seconds to several minutes, depending on network congestion and the gas fees you’ve set.
A transaction is considered confirmed once it’s included in a block. However, confirmations aren’t instantaneous. Ethereum blocks are produced approximately every 15 seconds, but delays can occur.
Key Concepts:
- Pending State: The transaction is in the mempool but hasn’t been included in a block.
- Confirmation: The transaction is successfully included in a block.
- Failure: The transaction was included but reverted due to an error (e.g., out of gas, invalid operation).
Checking Transaction Status in Go
The go-ethereum library provides methods to check transaction status programmatically. Here’s how it works:
Using TransactionByHash
This function returns transaction details, including whether it’s still pending:
_, isPending, err := ec.TransactionByHash(ctx, txHash)If isPending is false, the transaction is no longer in the mempool—it has either been confirmed or failed.
Using TransactionReceipt
Once a transaction is no longer pending, you can retrieve its receipt to determine the final status:
receipt, err := ec.TransactionReceipt(ctx, txHash)The receipt.Status field indicates success (1) or failure (0).
Implementing a Transaction Confirmation Waiter
To automate confirmation waiting, we’ll create a function that periodically checks the transaction status until it’s no longer pending or a timeout is reached.
Code Breakdown:
func waitConfirm(ctx context.Context, ec *ethclient.Client, txHash common.Hash, timeout time.Duration) error {
pending := true
for pending {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(timeout):
return errors.New("timeout")
case <-time.After(time.Second):
_, isPending, err := ec.TransactionByHash(ctx, txHash)
if err != nil {
return err
}
if !isPending {
pending = false // break the loop
}
}
}
// Check receipt after pending state ends
receipt, err := ec.TransactionReceipt(ctx, txHash)
if err != nil {
return err
}
if receipt.Status == 0 {
msg := fmt.Sprintf("transaction reverted, hash %s", receipt.TxHash.String())
return errors.New(msg)
}
return nil
}How It Works:
- Loop Until Resolved: The function enters a loop that continues until the transaction is no longer pending.
- Timeout Handling: If the transaction isn’t confirmed within the specified timeout (e.g., 10 minutes), it returns an error.
- Periodic Checks: The code checks the transaction status every second to avoid overwhelming the node with requests.
- Receipt Verification: Once the transaction is no longer pending, it retrieves the receipt to check the final status.
Full Example Code
Below is a complete Go program that sends a transaction and waits for confirmation:
package main
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"log"
"math/big"
"os"
"time"
)
func main() {
apiKey := os.Getenv("INFURA_API_KEY")
url := "https://sepolia.infura.io/v3/" + apiKey
ec, err := ethclient.Dial(url)
if err != nil {
log.Fatalf("could not connect to Infura with ethclient: %s", err)
}
ctx := context.Background()
pk, err := crypto.HexToECDSA(os.Getenv("PRIVATE_KEY"))
if err != nil {
log.Fatalf("load private key error: %s", err)
}
to := common.HexToAddress("0x26a1DDA0E911Ea245Fc3Fb7C5C10d18490942a60")
amount := big.NewInt(100_000_000_000_000_000) // 0.1 ether
hash, err := sendEth(ctx, ec, pk, to, amount)
if err != nil {
log.Fatalf("send Ether error: %s", err)
}
log.Printf("tx sent, hash: %s", hash)
if err = waitConfirm(ctx, ec, *hash, time.Minute*10); err != nil {
log.Fatalf("wait confirmation error, please check the tx by yourself: %s", err)
}
log.Printf("tx %s confirmed", hash)
}
func sendEth(ctx context.Context, ec *ethclient.Client, pk *ecdsa.PrivateKey, to common.Address, amount *big.Int) (*common.Hash, error) {
chainId, err := ec.ChainID(ctx)
if err != nil {
return nil, err
}
address := crypto.PubkeyToAddress(pk.PublicKey)
log.Printf("account: %s", address)
nonce, err := ec.NonceAt(ctx, address, nil)
if err != nil {
return nil, err
}
log.Printf("nonce: %d", nonce)
header, err := ec.HeaderByNumber(ctx, nil)
if err != nil {
return nil, err
}
log.Printf("base fee: %s", header.BaseFee)
gasTipCap, err := ec.SuggestGasTipCap(ctx)
if err != nil {
return nil, err
}
log.Printf("Suggested GasTipCap(maxPriorityFeePerGas): %s", gasTipCap)
txData := &types.DynamicFeeTx{
ChainID: chainId,
Nonce: nonce,
To: &to,
Value: amount,
Gas: 21000,
GasFeeCap: header.BaseFee,
GasTipCap: gasTipCap,
}
signedTx, err := types.SignNewTx(pk, types.LatestSignerForChainID(chainId), txData)
if err != nil {
return nil, err
}
err = ec.SendTransaction(ctx, signedTx)
if err != nil {
return nil, err
}
hash := signedTx.Hash()
return &hash, nil
}
func waitConfirm(ctx context.Context, ec *ethclient.Client, txHash common.Hash, timeout time.Duration) error {
pending := true
for pending {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(timeout):
return errors.New("timeout")
case <-time.After(time.Second):
_, isPending, err := ec.TransactionByHash(ctx, txHash)
if err != nil {
return err
}
if !isPending {
pending = false
}
}
}
receipt, err := ec.TransactionReceipt(ctx, txHash)
if err != nil {
return err
}
if receipt.Status == 0 {
msg := fmt.Sprintf("transaction reverted, hash %s", receipt.TxHash.String())
return errors.New(msg)
}
return nil
}Best Practices for Transaction Confirmation
- Set Realistic Timeouts: Network conditions vary. Set timeouts that accommodate peak congestion (e.g., 10–30 minutes).
- Handle Errors Gracefully: Not all failures are permanent. Implement retry logic for transient errors.
- Use Efficient Polling Intervals: Avoid checking too frequently to reduce node load. One check per second is reasonable.
- Monitor Gas Prices: High gas fees can delay transactions. Use gas estimation tools to set appropriate fees.
👉 Explore more strategies for efficient transaction handling
Frequently Asked Questions
How long does an Ethereum transaction take to confirm?
Confirmation times vary based on network activity and gas fees. Typically, transactions confirm within 15 seconds to a few minutes. During high congestion, wait times can extend beyond 10 minutes.
What does transaction status ‘0’ mean?
A status of ‘0’ indicates failure. The transaction was included in a block but reverted during execution. Common causes include insufficient gas, invalid parameters, or smart contract errors.
Can a pending transaction fail?
Yes. Transactions can fail after being pending if validators cannot execute them successfully. Always check the receipt status after the pending phase ends.
How do I avoid timeout errors?
Increase the timeout duration (e.g., 30 minutes) during high network congestion. Alternatively, use gas estimation APIs to set higher gas fees for faster inclusion.
Is it necessary to check receipts if ‘isPending’ is false?
Yes. ‘isPending’ becoming false only means the transaction left the mempool. You must check the receipt to confirm whether it succeeded or failed.
What’s the difference between ‘TransactionByHash’ and ‘TransactionReceipt’?
‘TransactionByHash’ returns basic transaction data and pending status. ‘TransactionReceipt’ provides detailed execution results, including status, gas used, and logs.
Conclusion
Confirming Ethereum transactions programmatically is essential for building robust decentralized applications. By using Go and the go-ethereum library, you can automate status checks, handle pending states, and interpret transaction outcomes accurately.
Remember to implement timeouts, handle errors, and optimize polling intervals to ensure your application remains efficient and user-friendly. With these techniques, you’ll reduce reliance on manual checks and build more autonomous systems.