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
-
Provider Management
- Always handle network changes
- Implement proper fallback providers
- Cache provider connections
-
Transaction Safety
- Always estimate gas before sending
- Implement proper nonce management
- Handle transaction replacements
-
Error Recovery
- Implement proper retry mechanisms
- Handle RPC errors gracefully
- Monitor transaction status