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