Avalanche Ethers.js Integration Guide
Ethers.js is a modern, lightweight, and TypeScript-first library for interacting with the Avalanche blockchain. This guide covers Ethers.js v6+ patterns and best practices using BlockEden.xyz's Avalanche 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 Avalanche 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://api.blockeden.xyz/avalanche/${accessKey}'
);
// 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);