Back to blog
Jun 03, 2025
4 min read

Web3 Development with ethers.js: A Complete Guide

Master Ethereum blockchain interactions using ethers.js - from basic transactions to complex smart contract interactions

Web3 Development with ethers.js

Ethers.js has become the go-to library for Ethereum blockchain interactions. Let’s explore how to build robust Web3 applications using ethers.js.

Getting Started with ethers.js

1. Basic Setup

import { ethers } from 'ethers';

// Connect to Ethereum network
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();

// Request account access
async function connectWallet() {
  try {
    await window.ethereum.request({ method: 'eth_requestAccounts' });
    const address = await signer.getAddress();
    console.log('Connected wallet:', address);
    return address;
  } catch (error) {
    console.error('Failed to connect wallet:', error);
  }
}

Smart Contract Interaction

1. Contract Instance Creation

const contractAddress = "0x123..."; // Your contract address
const contractABI = [
  "function balanceOf(address) view returns (uint)",
  "function transfer(address to, uint amount) returns (bool)",
];

const contract = new ethers.Contract(
  contractAddress,
  contractABI,
  signer
);

2. Reading Contract Data

async function getTokenBalance(address: string) {
  try {
    const balance = await contract.balanceOf(address);
    // Convert from Wei to Ether
    return ethers.utils.formatEther(balance);
  } catch (error) {
    console.error('Error fetching balance:', error);
    throw error;
  }
}

// Reading events
const filter = contract.filters.Transfer();
contract.on(filter, (from, to, amount) => {
  console.log(`${from} sent ${amount} tokens to ${to}`);
});

Transaction Management

1. Sending Transactions

async function sendTransaction(to: string, amount: string) {
  try {
    // Convert Ether to Wei
    const amountWei = ethers.utils.parseEther(amount);
    
    // Create transaction
    const tx = await signer.sendTransaction({
      to,
      value: amountWei
    });
    
    // Wait for confirmation
    const receipt = await tx.wait();
    console.log('Transaction confirmed:', receipt.transactionHash);
    return receipt;
  } catch (error) {
    console.error('Transaction failed:', error);
    throw error;
  }
}

2. Gas Estimation

async function estimateGas(to: string, amount: string) {
  const amountWei = ethers.utils.parseEther(amount);
  
  // Estimate gas limit
  const gasLimit = await signer.estimateGas({
    to,
    value: amountWei
  });
  
  // Get current gas price
  const gasPrice = await provider.getGasPrice();
  
  // Calculate total gas cost
  const gasCost = gasLimit.mul(gasPrice);
  return ethers.utils.formatEther(gasCost);
}

Advanced Contract Interactions

1. Contract Deployment

async function deployContract(contractBytecode: string, contractABI: any[]) {
  const factory = new ethers.ContractFactory(
    contractABI,
    contractBytecode,
    signer
  );
  
  const contract = await factory.deploy();
  await contract.deployed();
  
  console.log('Contract deployed to:', contract.address);
  return contract;
}

2. Batch Transactions

async function batchTransfer(recipients: string[], amounts: string[]) {
  // Create a multi-call contract instance
  const multicall = new ethers.Contract(
    MULTICALL_ADDRESS,
    MULTICALL_ABI,
    signer
  );
  
  const calls = recipients.map((recipient, index) => ({
    target: TOKEN_ADDRESS,
    callData: contract.interface.encodeFunctionData('transfer', [
      recipient,
      ethers.utils.parseEther(amounts[index])
    ])
  }));
  
  const tx = await multicall.aggregate(calls);
  return await tx.wait();
}

Error Handling and Security

1. Transaction Error Handling

async function safeContractCall(
  method: string,
  params: any[]
): Promise<any> {
  try {
    const tx = await contract[method](...params);
    const receipt = await tx.wait();
    
    if (receipt.status === 0) {
      throw new Error('Transaction failed');
    }
    
    return receipt;
  } catch (error: any) {
    if (error.code === 'INSUFFICIENT_FUNDS') {
      throw new Error('Not enough ETH for transaction');
    }
    if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
      throw new Error('Contract call will fail');
    }
    throw error;
  }
}

2. Signature Verification

async function verifySignature(
  message: string,
  signature: string,
  address: string
): Promise<boolean> {
  try {
    const signerAddr = ethers.utils.verifyMessage(message, signature);
    return signerAddr.toLowerCase() === address.toLowerCase();
  } catch {
    return false;
  }
}

Event Monitoring

1. Real-time Event Tracking

function watchContractEvents() {
  contract.on('Transfer', (from, to, amount, event) => {
    console.log({
      from,
      to,
      amount: ethers.utils.formatEther(amount),
      blockNumber: event.blockNumber,
      transactionHash: event.transactionHash
    });
  });
}

2. Historical Event Query

async function getHistoricalEvents(
  fromBlock: number,
  toBlock: number
) {
  const events = await contract.queryFilter(
    contract.filters.Transfer(),
    fromBlock,
    toBlock
  );
  
  return events.map(event => ({
    from: event.args?.from,
    to: event.args?.to,
    amount: ethers.utils.formatEther(event.args?.amount),
    blockNumber: event.blockNumber,
    transactionHash: event.transactionHash
  }));
}

Best Practices

  1. Provider Management

    • Always handle network changes
    • Implement proper fallback providers
    • Cache provider connections
  2. Transaction Safety

    • Always estimate gas before sending
    • Implement proper nonce management
    • Handle transaction replacements
  3. Error Recovery

    • Implement proper retry mechanisms
    • Handle RPC errors gracefully
    • Monitor transaction status

Resources