Ethereum Ethers.js Integration Guide
Ethers.js is a modern, lightweight, and TypeScript-first library for interacting with the Ethereum blockchain. This guide covers Ethers.js v6+ patterns and best practices using BlockEden.xyz's Ethereum infrastructure.
Why Ethers.js?
Advantages over Web3.js
- TypeScript First: Built with TypeScript for better developer experience
- Modular Design: Import only what you need
- Modern Promise-based API: Clean async/await patterns
- Better Error Handling: More informative error messages
- Tree Shakable: Smaller bundle sizes
- ENS Support: Built-in Ethereum Name Service support
- Provider Abstraction: Clean separation between providers and signers
Installation & Setup
Installation
# Using npm
npm install ethers
# Using yarn
yarn add ethers
# Using pnpm
pnpm add ethers
Basic Setup
import { ethers } from 'ethers';
// Create provider with BlockEden.xyz endpoint
const provider = new ethers.JsonRpcProvider(
'https://ethereum-mainnet.blockeden.xyz/<your-api-key>'
);
// Verify connection
async function testConnection() {
try {
const network = await provider.getNetwork();
const blockNumber = await provider.getBlockNumber();
console.log('Connected to:', network.name);
console.log('Chain ID:', network.chainId);
console.log('Current block:', blockNumber);
} catch (error) {
console.error('Connection failed:', error);
}
}
testConnection();
Multi-Network Configuration
// config/providers.ts
import { ethers } from 'ethers';
interface NetworkConfig {
name: string;
chainId: number;
rpcUrl: string;
}
const networks: Record<string, NetworkConfig> = {
mainnet: {
name: 'mainnet',
chainId: 1,
rpcUrl: 'https://ethereum-mainnet.blockeden.xyz'
},
sepolia: {
name: 'sepolia',
chainId: 11155111,
rpcUrl: 'https://ethereum-sepolia.blockeden.xyz'
},
polygon: {
name: 'polygon',
chainId: 137,
rpcUrl: 'https://polygon-mainnet.blockeden.xyz'
},
arbitrum: {
name: 'arbitrum',
chainId: 42161,
rpcUrl: 'https://arbitrum-mainnet.blockeden.xyz'
}
};
export class ProviderManager {
private providers: Map<string, ethers.JsonRpcProvider> = new Map();
constructor(private apiKey: string) {}
getProvider(network: string): ethers.JsonRpcProvider {
if (!this.providers.has(network)) {
const config = networks[network];
if (!config) {
throw new Error(`Unsupported network: ${network}`);
}
const provider = new ethers.JsonRpcProvider(`${config.rpcUrl}/${this.apiKey}`);
this.providers.set(network, provider);
}
return this.providers.get(network)!;
}
getAllProviders(): Record<string, ethers.JsonRpcProvider> {
const result: Record<string, ethers.JsonRpcProvider> = {};
Object.keys(networks).forEach(network => {
result[network] = this.getProvider(network);
});
return result;
}
}
// Usage
const providerManager = new ProviderManager(process.env.BLOCKEDEN_API_KEY!);
const mainnetProvider = providerManager.getProvider('mainnet');
const sepoliaProvider = providerManager.getProvider('sepolia');
Wallets and Signers
Creating Wallets
// Create random wallet
const randomWallet = ethers.Wallet.createRandom();
console.log('Address:', randomWallet.address);
console.log('Private Key:', randomWallet.privateKey);
console.log('Mnemonic:', randomWallet.mnemonic?.phrase);
// Create wallet from private key
const privateKey = '0x1234567890abcdef...';
const wallet = new ethers.Wallet(privateKey);
// Create wallet from mnemonic
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const walletFromMnemonic = ethers.Wallet.fromPhrase(mnemonic);
// Connect wallet to provider
const connectedWallet = wallet.connect(provider);
HD Wallet Management
// HD Wallet utilities
class HDWallet {
private hdNode: ethers.HDNodeWallet;
constructor(mnemonic?: string) {
if (mnemonic) {
this.hdNode = ethers.Wallet.fromPhrase(mnemonic);
} else {
this.hdNode = ethers.Wallet.createRandom();
}
}
// Get wallet at specific derivation path
getWallet(index: number, account: number = 0): ethers.HDNodeWallet {
const path = `m/44'/60'/${account}'/0/${index}`;
return this.hdNode.derivePath(path);
}
// Get multiple wallets
getWallets(count: number, account: number = 0): ethers.HDNodeWallet[] {
const wallets: ethers.HDNodeWallet[] = [];
for (let i = 0; i < count; i++) {
wallets.push(this.getWallet(i, account));
}
return wallets;
}
// Get master wallet
getMasterWallet(): ethers.HDNodeWallet {
return this.hdNode;
}
// Get mnemonic
getMnemonic(): string | null {
return this.hdNode.mnemonic?.phrase || null;
}
// Connect wallets to provider
connectWallets(wallets: ethers.HDNodeWallet[], provider: ethers.JsonRpcProvider): ethers.Wallet[] {
return wallets.map(wallet => wallet.connect(provider));
}
}
// Usage
const hdWallet = new HDWallet();
const wallets = hdWallet.getWallets(5); // Get first 5 wallets
const connectedWallets = hdWallet.connectWallets(wallets, provider);
console.log('Generated addresses:');
connectedWallets.forEach((wallet, index) => {
console.log(`Wallet ${index}:`, wallet.address);
});
Encrypted Wallet Storage
// Encrypt and store wallet
async function encryptWallet(wallet: ethers.Wallet, password: string): Promise<string> {
try {
const encryptedJson = await wallet.encrypt(password);
return encryptedJson;
} catch (error) {
console.error('Error encrypting wallet:', error);
throw error;
}
}
// Decrypt wallet
async function decryptWallet(encryptedJson: string, password: string): Promise<ethers.Wallet> {
try {
const wallet = await ethers.Wallet.fromEncryptedJson(encryptedJson, password);
return wallet;
} catch (error) {
console.error('Error decrypting wallet:', error);
throw error;
}
}
// Wallet manager with encryption
class SecureWalletManager {
private encryptedWallets: Map<string, string> = new Map();
async addWallet(alias: string, privateKey: string, password: string): Promise<string> {
const wallet = new ethers.Wallet(privateKey);
const encrypted = await encryptWallet(wallet, password);
this.encryptedWallets.set(alias, encrypted);
return wallet.address;
}
async getWallet(alias: string, password: string): Promise<ethers.Wallet> {
const encrypted = this.encryptedWallets.get(alias);
if (!encrypted) {
throw new Error(`Wallet not found: ${alias}`);
}
return await decryptWallet(encrypted, password);
}
getAliases(): string[] {
return Array.from(this.encryptedWallets.keys());
}
removeWallet(alias: string): boolean {
return this.encryptedWallets.delete(alias);
}
// Export for storage
export(): Record<string, string> {
return Object.fromEntries(this.encryptedWallets);
}
// Import from storage
import(data: Record<string, string>): void {
this.encryptedWallets = new Map(Object.entries(data));
}
}
Reading Blockchain Data
Account Information
// Get comprehensive account information
async function getAccountInfo(address: string, provider: ethers.JsonRpcProvider) {
try {
const [balance, nonce, code] = await Promise.all([
provider.getBalance(address),
provider.getTransactionCount(address),
provider.getCode(address)
]);
return {
address,
balance: {
wei: balance.toString(),
eth: ethers.formatEther(balance)
},
nonce,
isContract: code !== '0x',
code: code !== '0x' ? code : null
};
} catch (error) {
console.error('Error getting account info:', error);
throw error;
}
}
// Get balance in different denominations
async function getBalanceFormatted(address: string, provider: ethers.JsonRpcProvider) {
const balance = await provider.getBalance(address);
return {
wei: balance.toString(),
gwei: ethers.formatUnits(balance, 'gwei'),
eth: ethers.formatEther(balance),
usd: null // You would need to fetch price data
};
}
Block Information
// Get detailed block information
async function getBlockInfo(blockNumber: number | 'latest', provider: ethers.JsonRpcProvider) {
try {
const block = await provider.getBlock(blockNumber, true); // true = include transactions
if (!block) {
throw new Error('Block not found');
}
return {
number: block.number,
hash: block.hash,
parentHash: block.parentHash,
timestamp: new Date(block.timestamp * 1000),
miner: block.miner,
difficulty: block.difficulty?.toString(),
gasLimit: block.gasLimit.toString(),
gasUsed: block.gasUsed.toString(),
baseFeePerGas: block.baseFeePerGas?.toString(),
transactionCount: block.transactions.length,
transactions: block.transactions.map(tx =>
typeof tx === 'string' ? tx : {
hash: tx.hash,
from: tx.from,
to: tx.to,
value: ethers.formatEther(tx.value),
gasLimit: tx.gasLimit.toString(),
gasPrice: tx.gasPrice?.toString()
}
)
};
} catch (error) {
console.error('Error getting block info:', error);
throw error;
}
}
// Get block range
async function getBlockRange(
startBlock: number,
endBlock: number,
provider: ethers.JsonRpcProvider
) {
const blocks = [];
for (let i = startBlock; i <= endBlock; i++) {
const block = await provider.getBlock(i, false); // false = only tx hashes
if (block) {
blocks.push(block);
}
}
return blocks;
}
Transaction Information
// Get comprehensive transaction information
async function getTransactionInfo(txHash: string, provider: ethers.JsonRpcProvider) {
try {
const [tx, receipt] = await Promise.all([
provider.getTransaction(txHash),
provider.getTransactionReceipt(txHash)
]);
if (!tx) {
throw new Error('Transaction not found');
}
return {
hash: tx.hash,
status: receipt ? (receipt.status === 1 ? 'success' : 'failed') : 'pending',
blockNumber: tx.blockNumber,
blockHash: tx.blockHash,
from: tx.from,
to: tx.to,
value: ethers.formatEther(tx.value),
gasLimit: tx.gasLimit.toString(),
gasPrice: tx.gasPrice ? ethers.formatUnits(tx.gasPrice, 'gwei') : null,
maxFeePerGas: tx.maxFeePerGas ? ethers.formatUnits(tx.maxFeePerGas, 'gwei') : null,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas ? ethers.formatUnits(tx.maxPriorityFeePerGas, 'gwei') : null,
nonce: tx.nonce,
data: tx.data,
type: tx.type,
chainId: tx.chainId,
gasUsed: receipt ? receipt.gasUsed.toString() : null,
effectiveGasPrice: receipt ? ethers.formatUnits(receipt.gasPrice, 'gwei') : null,
logs: receipt ? receipt.logs : null,
confirmations: await tx.confirmations()
};
} catch (error) {
console.error('Error getting transaction info:', error);
throw error;
}
}
// Wait for transaction with timeout
async function waitForTransaction(
txHash: string,
provider: ethers.JsonRpcProvider,
confirmations: number = 1,
timeout: number = 300000 // 5 minutes
) {
try {
const receipt = await provider.waitForTransaction(txHash, confirmations, timeout);
if (!receipt) {
throw new Error('Transaction not found or timed out');
}
return {
success: receipt.status === 1,
receipt,
gasUsed: receipt.gasUsed.toString(),
effectiveGasPrice: ethers.formatUnits(receipt.gasPrice, 'gwei')
};
} catch (error) {
console.error('Error waiting for transaction:', error);
throw error;
}
}
Sending Transactions
Basic Ether Transfer
// Send Ether with modern Ethers.js patterns
async function sendEther(
signer: ethers.Signer,
toAddress: string,
amountEth: string,
options?: {
gasLimit?: string;
gasPrice?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
}
) {
try {
// Prepare transaction
const tx: ethers.TransactionRequest = {
to: toAddress,
value: ethers.parseEther(amountEth)
};
// Add gas options if provided
if (options?.gasLimit) {
tx.gasLimit = options.gasLimit;
}
if (options?.gasPrice) {
tx.gasPrice = ethers.parseUnits(options.gasPrice, 'gwei');
} else if (options?.maxFeePerGas && options?.maxPriorityFeePerGas) {
// EIP-1559 transaction
tx.maxFeePerGas = ethers.parseUnits(options.maxFeePerGas, 'gwei');
tx.maxPriorityFeePerGas = ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei');
tx.type = 2;
}
// Send transaction
const txResponse = await signer.sendTransaction(tx);
console.log('Transaction sent:', txResponse.hash);
// Wait for confirmation
const receipt = await txResponse.wait();
return {
hash: txResponse.hash,
success: receipt?.status === 1,
gasUsed: receipt?.gasUsed.toString(),
effectiveGasPrice: receipt ? ethers.formatUnits(receipt.gasPrice, 'gwei') : null
};
} catch (error) {
console.error('Error sending ether:', error);
throw error;
}
}
// Usage
const wallet = new ethers.Wallet(privateKey, provider);
const result = await sendEther(wallet, '0x742D5Cc6bF2442E8C7c74c7b4Be6AB9d6f10f5B4', '0.1');
EIP-1559 Transaction with Dynamic Fees
// Get optimal gas fees for EIP-1559
async function getOptimalGasFees(provider: ethers.JsonRpcProvider) {
try {
const feeData = await provider.getFeeData();
// If EIP-1559 is supported
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
return {
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
gasPrice: null // Not used in EIP-1559
};
} else {
// Fallback to legacy gas pricing
return {
maxFeePerGas: null,
maxPriorityFeePerGas: null,
gasPrice: feeData.gasPrice
};
}
} catch (error) {
console.error('Error getting gas fees:', error);
throw error;
}
}
// Send EIP-1559 transaction with optimal fees
async function sendEIP1559Transaction(
signer: ethers.Signer,
toAddress: string,
amountEth: string
) {
const provider = signer.provider!;
const gasData = await getOptimalGasFees(provider);
const tx: ethers.TransactionRequest = {
to: toAddress,
value: ethers.parseEther(amountEth),
type: 2 // EIP-1559
};
if (gasData.maxFeePerGas && gasData.maxPriorityFeePerGas) {
tx.maxFeePerGas = gasData.maxFeePerGas;
tx.maxPriorityFeePerGas = gasData.maxPriorityFeePerGas;
} else if (gasData.gasPrice) {
tx.gasPrice = gasData.gasPrice;
tx.type = 0; // Legacy transaction
}
const txResponse = await signer.sendTransaction(tx);
return await txResponse.wait();
}
Gas Estimation and Optimization
// Comprehensive gas estimation
async function estimateTransactionCost(
signer: ethers.Signer,
txRequest: ethers.TransactionRequest
) {
try {
const provider = signer.provider!;
// Estimate gas limit
const gasLimit = await provider.estimateGas(txRequest);
// Get current fee data
const feeData = await provider.getFeeData();
let totalCost: bigint;
let gasPrice: bigint;
if (feeData.maxFeePerGas) {
// EIP-1559 transaction
gasPrice = feeData.maxFeePerGas;
totalCost = gasLimit * gasPrice;
} else if (feeData.gasPrice) {
// Legacy transaction
gasPrice = feeData.gasPrice;
totalCost = gasLimit * gasPrice;
} else {
throw new Error('Unable to determine gas price');
}
return {
gasLimit: gasLimit.toString(),
gasPrice: ethers.formatUnits(gasPrice, 'gwei'),
totalCostWei: totalCost.toString(),
totalCostEth: ethers.formatEther(totalCost),
maxFeePerGas: feeData.maxFeePerGas ? ethers.formatUnits(feeData.maxFeePerGas, 'gwei') : null,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ? ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei') : null
};
} catch (error) {
console.error('Error estimating gas:', error);
throw error;
}
}
Smart Contract Interaction
Contract Instance Creation
// ERC-20 Token interface
const ERC20_ABI = [
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'function allowance(address owner, address spender) view returns (uint256)',
'function approve(address spender, uint256 amount) returns (bool)',
'function transferFrom(address from, address to, uint256 amount) returns (bool)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
'event Approval(address indexed owner, address indexed spender, uint256 value)'
];
// Create contract instance
const tokenAddress = '0xA0b86a33E6c0e4A2a2a5FB1C6A6D6a30BF8b6B3a'; // USDC example
const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
// Connect with signer for write operations
const tokenContractWithSigner = tokenContract.connect(signer);
Reading from Contracts
// Comprehensive token information
async function getTokenInfo(tokenContract: ethers.Contract) {
try {
const [name, symbol, decimals, totalSupply] = await Promise.all([
tokenContract.name(),
tokenContract.symbol(),
tokenContract.decimals(),
tokenContract.totalSupply()
]);
return {
name,
symbol,
decimals: Number(decimals),
totalSupply: ethers.formatUnits(totalSupply, decimals)
};
} catch (error) {
console.error('Error getting token info:', error);
throw error;
}
}
// Get token balance with proper formatting
async function getTokenBalance(tokenContract: ethers.Contract, address: string) {
try {
const [balance, decimals] = await Promise.all([
tokenContract.balanceOf(address),
tokenContract.decimals()
]);
return {
raw: balance.toString(),
formatted: ethers.formatUnits(balance, decimals),
decimals: Number(decimals)
};
} catch (error) {
console.error('Error getting token balance:', error);
throw error;
}
}
// Batch multiple contract calls
async function batchContractCalls(calls: Array<{ contract: ethers.Contract; method: string; params: any[] }>) {
try {
const promises = calls.map(({ contract, method, params }) =>
contract[method](...params)
);
return await Promise.all(promises);
} catch (error) {
console.error('Error in batch calls:', error);
throw error;
}
}
Writing to Contracts
// Transfer tokens with comprehensive error handling
async function transferTokens(
tokenContract: ethers.Contract,
signer: ethers.Signer,
toAddress: string,
amount: string
) {
try {
const contractWithSigner = tokenContract.connect(signer);
// Get token decimals
const decimals = await tokenContract.decimals();
// Parse amount to correct decimals
const amountWei = ethers.parseUnits(amount, decimals);
// Check balance
const senderAddress = await signer.getAddress();
const balance = await tokenContract.balanceOf(senderAddress);
if (balance < amountWei) {
throw new Error(`Insufficient balance. Have: ${ethers.formatUnits(balance, decimals)}, Need: ${amount}`);
}
// Estimate gas
const gasEstimate = await contractWithSigner.transfer.estimateGas(toAddress, amountWei);
// Add 20% buffer to gas estimate
const gasLimit = (gasEstimate * 120n) / 100n;
// Send transaction
const txResponse = await contractWithSigner.transfer(toAddress, amountWei, {
gasLimit
});
console.log('Transfer transaction sent:', txResponse.hash);
// Wait for confirmation
const receipt = await txResponse.wait();
return {
hash: txResponse.hash,
success: receipt?.status === 1,
gasUsed: receipt?.gasUsed.toString(),
blockNumber: receipt?.blockNumber
};
} catch (error) {
console.error('Error transferring tokens:', error);
throw error;
}
}
// Approve tokens for spending
async function approveTokens(
tokenContract: ethers.Contract,
signer: ethers.Signer,
spenderAddress: string,
amount: string
) {
try {
const contractWithSigner = tokenContract.connect(signer);
const decimals = await tokenContract.decimals();
const amountWei = ethers.parseUnits(amount, decimals);
const txResponse = await contractWithSigner.approve(spenderAddress, amountWei);
const receipt = await txResponse.wait();
return {
hash: txResponse.hash,
success: receipt?.status === 1
};
} catch (error) {
console.error('Error approving tokens:', error);
throw error;
}
}
Contract Deployment
// Deploy contract with TypeScript support
interface ContractDeployment {
contractAddress: string;
transactionHash: string;
deploymentCost: string;
gasUsed: string;
}
async function deployContract(
contractABI: ethers.InterfaceAbi,
contractBytecode: string,
constructorArgs: any[],
signer: ethers.Signer
): Promise<ContractDeployment> {
try {
// Create contract factory
const contractFactory = new ethers.ContractFactory(contractABI, contractBytecode, signer);
// Estimate deployment gas
const deploymentData = contractFactory.getDeployTransaction(...constructorArgs);
const gasEstimate = await signer.estimateGas(deploymentData);
// Deploy contract
const contract = await contractFactory.deploy(...constructorArgs, {
gasLimit: (gasEstimate * 120n) / 100n // 20% buffer
});
// Wait for deployment
await contract.waitForDeployment();
const deploymentReceipt = await contract.deploymentTransaction()?.wait();
if (!deploymentReceipt) {
throw new Error('Deployment transaction not found');
}
return {
contractAddress: await contract.getAddress(),
transactionHash: deploymentReceipt.hash,
deploymentCost: ethers.formatEther(deploymentReceipt.gasUsed * deploymentReceipt.gasPrice),
gasUsed: deploymentReceipt.gasUsed.toString()
};
} catch (error) {
console.error('Error deploying contract:', error);
throw error;
}
}
Event Monitoring
Listening to Contract Events
// Event listener with TypeScript support
interface TransferEvent {
from: string;
to: string;
value: string;
blockNumber: number;
transactionHash: string;
logIndex: number;
}
function listenToTransferEvents(
tokenContract: ethers.Contract,
callback: (event: TransferEvent) => void,
fromBlock: number | 'latest' = 'latest'
) {
// Create event filter
const transferFilter = tokenContract.filters.Transfer();
// Listen to events
tokenContract.on(transferFilter, async (from, to, value, event) => {
try {
const decimals = await tokenContract.decimals();
const transferEvent: TransferEvent = {
from,
to,
value: ethers.formatUnits(value, decimals),
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
logIndex: event.logIndex
};
callback(transferEvent);
} catch (error) {
console.error('Error processing transfer event:', error);
}
});
return () => {
tokenContract.removeAllListeners(transferFilter);
};
}
// Advanced event monitoring with filtering
class EventMonitor {
private contract: ethers.Contract;
private listeners: Map<string, () => void> = new Map();
constructor(contract: ethers.Contract) {
this.contract = contract;
}
// Listen to specific events with filters
listenToEvent<T extends Record<string, any>>(
eventName: string,
filters: Record<string, any> = {},
callback: (event: T, eventData: ethers.Log) => void
): string {
const listenerId = `${eventName}_${Date.now()}`;
const eventFilter = this.contract.filters[eventName](...Object.values(filters));
const listener = this.contract.on(eventFilter, (...args) => {
const event = args[args.length - 1]; // Last argument is event data
const eventArgs = args.slice(0, -1); // All other arguments are event values
// Create typed event object
const typedEvent = this.contract.interface.decodeEventLog(eventName, event.data, event.topics);
callback(typedEvent as T, event);
});
this.listeners.set(listenerId, () => {
this.contract.removeListener(eventFilter, listener);
});
return listenerId;
}
// Remove specific listener
removeListener(listenerId: string): boolean {
const remover = this.listeners.get(listenerId);
if (remover) {
remover();
this.listeners.delete(listenerId);
return true;
}
return false;
}
// Remove all listeners
removeAllListeners(): void {
this.listeners.forEach(remover => remover());
this.listeners.clear();
}
}
Historical Event Queries
// Query historical events with pagination
async function getHistoricalEvents(
contract: ethers.Contract,
eventName: string,
fromBlock: number,
toBlock: number | 'latest',
filters: Record<string, any> = {},
pageSize: number = 1000
) {
const events: ethers.EventLog[] = [];
let currentFromBlock = fromBlock;
const finalToBlock = toBlock === 'latest' ? await contract.runner!.provider!.getBlockNumber() : toBlock;
while (currentFromBlock <= finalToBlock) {
const currentToBlock = Math.min(currentFromBlock + pageSize - 1, finalToBlock);
try {
const eventFilter = contract.filters[eventName](...Object.values(filters));
const pageEvents = await contract.queryFilter(eventFilter, currentFromBlock, currentToBlock);
events.push(...pageEvents as ethers.EventLog[]);
console.log(`Fetched ${pageEvents.length} events from blocks ${currentFromBlock} to ${currentToBlock}`);
currentFromBlock = currentToBlock + 1;
// Add small delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.error(`Error fetching events from blocks ${currentFromBlock} to ${currentToBlock}:`, error);
currentFromBlock += pageSize;
}
}
return events;
}
// Get events with block timestamps and detailed parsing
async function getEventsWithDetails(
contract: ethers.Contract,
eventName: string,
fromBlock: number,
toBlock: number | 'latest'
) {
const events = await getHistoricalEvents(contract, eventName, fromBlock, toBlock);
// Get unique block numbers for timestamp lookup
const blockNumbers = [...new Set(events.map(event => event.blockNumber))];
// Fetch block data in batches
const blockData = new Map<number, ethers.Block>();
const batchSize = 100;
for (let i = 0; i < blockNumbers.length; i += batchSize) {
const batch = blockNumbers.slice(i, i + batchSize);
const blocks = await Promise.all(
batch.map(blockNum => contract.runner!.provider!.getBlock(blockNum))
);
blocks.forEach((block, index) => {
if (block) {
blockData.set(batch[index], block);
}
});
}
// Parse and enrich events
return events.map(event => {
const block = blockData.get(event.blockNumber);
const parsedEvent = contract.interface.parseLog({
topics: event.topics,
data: event.data
});
return {
name: parsedEvent?.name,
args: parsedEvent?.args,
blockNumber: event.blockNumber,
blockHash: event.blockHash,
transactionHash: event.transactionHash,
transactionIndex: event.transactionIndex,
logIndex: event.logIndex,
timestamp: block ? new Date(block.timestamp * 1000) : null,
removed: event.removed
};
});
}
Advanced Patterns
Multi-call Pattern
// Efficient multiple contract calls using Multicall
interface MulticallRequest {
target: string;
callData: string;
}
interface MulticallResponse {
success: boolean;
returnData: string;
}
class Multicaller {
private multicallContract: ethers.Contract;
constructor(multicallAddress: string, provider: ethers.Provider) {
// Multicall3 ABI (simplified)
const multicallABI = [
'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) returns (tuple(bool success, bytes returnData)[] returnData)'
];
this.multicallContract = new ethers.Contract(multicallAddress, multicallABI, provider);
}
async call(calls: MulticallRequest[], allowFailure: boolean = true): Promise<MulticallResponse[]> {
const callData = calls.map(call => ({
target: call.target,
allowFailure,
callData: call.callData
}));
const results = await this.multicallContract.aggregate3(callData);
return results;
}
// Helper for ERC-20 token data
async getTokensData(tokenAddresses: string[]): Promise<any[]> {
const erc20Interface = new ethers.Interface([
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)'
]);
const calls: MulticallRequest[] = [];
tokenAddresses.forEach(address => {
calls.push(
{ target: address, callData: erc20Interface.encodeFunctionData('name') },
{ target: address, callData: erc20Interface.encodeFunctionData('symbol') },
{ target: address, callData: erc20Interface.encodeFunctionData('decimals') },
{ target: address, callData: erc20Interface.encodeFunctionData('totalSupply') }
);
});
const results = await this.call(calls);
// Parse results
const tokens = [];
for (let i = 0; i < tokenAddresses.length; i++) {
const baseIndex = i * 4;
try {
tokens.push({
address: tokenAddresses[i],
name: erc20Interface.decodeFunctionResult('name', results[baseIndex].returnData)[0],
symbol: erc20Interface.decodeFunctionResult('symbol', results[baseIndex + 1].returnData)[0],
decimals: erc20Interface.decodeFunctionResult('decimals', results[baseIndex + 2].returnData)[0],
totalSupply: erc20Interface.decodeFunctionResult('totalSupply', results[baseIndex + 3].returnData)[0]
});
} catch (error) {
console.error(`Error parsing token data for ${tokenAddresses[i]}:`, error);
tokens.push({
address: tokenAddresses[i],
error: 'Failed to parse token data'
});
}
}
return tokens;
}
}
Transaction Builder Pattern
// Fluent transaction builder
class TransactionBuilder {
private tx: ethers.TransactionRequest = {};
to(address: string): TransactionBuilder {
this.tx.to = address;
return this;
}
value(amount: string | bigint): TransactionBuilder {
this.tx.value = typeof amount === 'string' ? ethers.parseEther(amount) : amount;
return this;
}
data(data: string): TransactionBuilder {
this.tx.data = data;
return this;
}
gasLimit(limit: string | bigint): TransactionBuilder {
this.tx.gasLimit = limit;
return this;
}
gasPrice(price: string): TransactionBuilder {
this.tx.gasPrice = ethers.parseUnits(price, 'gwei');
return this;
}
eip1559(maxFee: string, priorityFee: string): TransactionBuilder {
this.tx.maxFeePerGas = ethers.parseUnits(maxFee, 'gwei');
this.tx.maxPriorityFeePerGas = ethers.parseUnits(priorityFee, 'gwei');
this.tx.type = 2;
return this;
}
nonce(nonce: number): TransactionBuilder {
this.tx.nonce = nonce;
return this;
}
async estimateGas(signer: ethers.Signer): Promise<TransactionBuilder> {
this.tx.gasLimit = await signer.estimateGas(this.tx);
return this;
}
async send(signer: ethers.Signer): Promise<ethers.TransactionResponse> {
return await signer.sendTransaction(this.tx);
}
build(): ethers.TransactionRequest {
return { ...this.tx };
}
}
// Usage
const txBuilder = new TransactionBuilder()
.to('0x742D5Cc6bF2442E8C7c74c7b4Be6AB9d6f10f5B4')
.value('1.0')
.eip1559('30', '2');
await txBuilder.estimateGas(signer);
const txResponse = await txBuilder.send(signer);
Testing and Development
Mock Provider for Testing
// Mock provider for unit testing
import { MockProvider } from '@ethersproject/providers';
describe('Ethereum Integration Tests', () => {
let mockProvider: MockProvider;
let wallet: ethers.Wallet;
beforeEach(() => {
mockProvider = new MockProvider();
wallet = mockProvider.getWallets()[0];
});
test('should get balance', async () => {
const balance = await wallet.getBalance();
expect(balance).toBeInstanceOf(BigNumber);
});
test('should send transaction', async () => {
const [wallet1, wallet2] = mockProvider.getWallets();
const tx = await wallet1.sendTransaction({
to: wallet2.address,
value: ethers.parseEther('1.0')
});
await tx.wait();
const balance = await wallet2.getBalance();
expect(ethers.formatEther(balance)).toBe('10001.0'); // Initial 10000 + 1
});
});
Development Utilities
// Development helper utilities
class EthersDevUtils {
static async fundAccount(
faucetWallet: ethers.Wallet,
targetAddress: string,
amountEth: string
): Promise<string> {
const tx = await faucetWallet.sendTransaction({
to: targetAddress,
value: ethers.parseEther(amountEth)
});
await tx.wait();
return tx.hash;
}
static async deployTestToken(
deployer: ethers.Signer,
name: string,
symbol: string,
initialSupply: string
): Promise<ethers.Contract> {
// Simplified ERC-20 implementation would go here
// This is just a placeholder showing the pattern
throw new Error('Implementation needed');
}
static formatters = {
wei: (value: bigint) => value.toString(),
gwei: (value: bigint) => ethers.formatUnits(value, 'gwei'),
eth: (value: bigint) => ethers.formatEther(value),
token: (value: bigint, decimals: number) => ethers.formatUnits(value, decimals)
};
static parsers = {
eth: (value: string) => ethers.parseEther(value),
gwei: (value: string) => ethers.parseUnits(value, 'gwei'),
token: (value: string, decimals: number) => ethers.parseUnits(value, decimals)
};
}
Next Steps
- Learn about advanced smart contract interaction patterns
- Explore WebSocket Guide for real-time features
- Follow JSON-RPC API Reference for low-level operations
- Check out Web3.js Integration for alternative patterns