Reading Smart Contract Data with ethers.js v6: A Complete Beginner's Guide
Introduction
Welcome to Web3 development! If you're new to blockchain development, this guide will walk you through the fundamentals of reading data from smart contracts. We'll start with the basics and build up to creating a real-world application.
Prerequisites
Basic JavaScript/TypeScript knowledge
Node.js installed on your system
A code editor (VS Code recommended)
No prior blockchain experience needed!
Table of Contents
Understanding Web3 Infrastructure
Setting Up Your Development Environment
Reading Smart Contract Data
Building a Token Balance API
1. Understanding Web3 Infrastructure
What is a JSON-RPC?
JSON-RPC (Remote Procedure Call) is how we communicate with blockchain nodes. Think of it as an API endpoint for blockchain interactions. When you want to:
Read blockchain data
Send transactions
Deploy contracts You'll need to connect to a JSON-RPC endpoint.
Where to Get JSON-RPC Access?
You have several options:
Node Providers (Recommended for Beginners):
Running Your Own Node:
More advanced
Requires server maintenance
Complete control over your infrastructure
For this tutorial, we'll use Alchemy. Here's how to get started:
Sign up at Alchemy.com
Create a new app (choose Ethereum mainnet or Sepolia testnet)
Get your API key and HTTP URL (looks like:
https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY
)
Why Not Connect Directly to Ethereum?
Running a node is resource-intensive
Node providers offer:
High availability
Fast response times
Multiple network support
Developer tools
2. Setting Up Your Development Environment
First, let's create a new project and install dependencies:
mkdir smart-contract-reader
cd smart-contract-reader
npm init -y
npm install ethers dotenv typescript @types/node ts-node
npx tsc --init
Create a .env
file:
ALCHEMY_URL=your_alchemy_url_here
Create a basic TypeScript configuration in tsconfig.json
:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
3. Reading Smart Contract Data
Understanding Smart Contracts
Smart contracts are like backend APIs deployed on the blockchain. To interact with them, we need:
Contract Address (where the contract lives on the blockchain)
ABI (Application Binary Interface - tells us how to interact with the contract)
Provider (our connection to the blockchain)
Contract ABIs Explained
An ABI is like an API specification. It tells us:
What functions are available
What parameters they take
What data they return
For ERC20 tokens (the most common token type), here's a minimal ABI:
const ERC20_ABI = [
// Read token balance
"function balanceOf(address owner) view returns (uint256)",
// Get token decimals
"function decimals() view returns (uint8)",
// Get token symbol
"function symbol() view returns (string)",
// Get token name
"function name() view returns (string)"
];
Let's Write Some Code!
Create src/index.ts
:
import { ethers } from 'ethers';
import dotenv from 'dotenv';
dotenv.config();
async function main() {
try {
// 1. Connect to the network
const provider = new ethers.JsonRpcProvider(process.env.ALCHEMY_URL);
// 2. Test connection by getting the latest block
const blockNumber = await provider.getBlockNumber();
console.log(`Current block number: ${blockNumber}`);
// 3. Let's read some token data (using USDT as an example)
const USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7';
const ERC20_ABI = [
"function balanceOf(address) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)"
];
// 4. Create contract instance
const contract = new ethers.Contract(USDT_ADDRESS, ERC20_ABI, provider);
// 5. Read some basic token info
const decimals = await contract.decimals();
const symbol = await contract.symbol();
console.log(`Token Symbol: ${symbol}`);
console.log(`Token Decimals: ${decimals}`);
// 6. Read balance of a specific address
const walletAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e';
const balance = await contract.balanceOf(walletAddress);
// 7. Format balance to human-readable format
const formattedBalance = ethers.formatUnits(balance, decimals);
console.log(`Balance: ${formattedBalance} ${symbol}`);
} catch (error) {
console.error('Error:', error);
}
}
main();
Understanding the Code
Let's break down what's happening:
Provider Setup:
const provider = new ethers.JsonRpcProvider(process.env.ALCHEMY_URL);
This creates our connection to Ethereum using our Alchemy URL.
Contract Instance:
const contract = new ethers.Contract(USDT_ADDRESS, ERC20_ABI, provider);
This creates an interface to interact with the smart contract.
Reading Data:
const balance = await contract.balanceOf(walletAddress);
This calls the
balanceOf
function on the smart contract.Formatting Data:
const formattedBalance = ethers.formatUnits(balance, decimals);
Converts the raw balance (in wei) to a human-readable format.
Understanding Big Numbers
Ethereum handles numbers as "BigNumbers" due to precision requirements. Example:
Raw balance might be: 1000000000000000000
With 18 decimals, this is actually: 1.0
That's why we use formatUnits
and always need to know the token's decimals!
4. Building a Token Balance API
Now let's build something practical! We'll create an API that can:
Check multiple token balances
Handle errors gracefully
Return formatted results
Create src/api.ts
:
import express from 'express';
import { ethers } from 'ethers';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
app.use(express.json());
// Interfaces for type safety
interface TokenBalance {
token: string;
symbol: string;
balanceWei: string;
balanceFormatted: string;
decimals: number;
}
interface TokenBalanceResponse {
address: string;
balances: TokenBalance[];
error?: string;
}
// Initialize provider
const provider = new ethers.JsonRpcProvider(process.env.ALCHEMY_URL);
const ERC20_ABI = [
"function balanceOf(address) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)"
];
app.post('/api/token-balances', async (req, res) => {
try {
const { addresses, tokens } = req.body;
// Validate input
if (!addresses || !tokens || !Array.isArray(addresses) || !Array.isArray(tokens)) {
return res.status(400).json({ error: 'Invalid input. Provide addresses and tokens arrays.' });
}
const results: TokenBalanceResponse[] = [];
// Process each address
for (const address of addresses) {
try {
// Validate address format
if (!ethers.isAddress(address)) {
throw new Error('Invalid address format');
}
const balances: TokenBalance[] = [];
// Process each token
for (const tokenAddress of tokens) {
try {
// Validate token address format
if (!ethers.isAddress(tokenAddress)) {
throw new Error('Invalid token address format');
}
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
// Fetch all token data in parallel
const [balance, decimals, symbol] = await Promise.all([
contract.balanceOf(address),
contract.decimals(),
contract.symbol()
]);
balances.push({
token: tokenAddress,
symbol,
balanceWei: balance.toString(),
balanceFormatted: ethers.formatUnits(balance, decimals),
decimals
});
} catch (tokenError) {
console.error(`Error fetching token ${tokenAddress}:`, tokenError);
balances.push({
token: tokenAddress,
symbol: 'ERROR',
balanceWei: '0',
balanceFormatted: '0',
decimals: 0
});
}
}
results.push({
address,
balances
});
} catch (addressError) {
console.error(`Error processing address ${address}:`, addressError);
results.push({
address,
balances: [],
error: 'Failed to fetch balances for this address'
});
}
}
res.json(results);
} catch (error) {
console.error('Server error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Testing the API
Use curl or Postman to test:
curl -X POST http://localhost:3000/api/token-balances \
-H "Content-Type: application/json" \
-d '{
"addresses": ["0x742d35Cc6634C0532925a3b844Bc454e4438f44e"],
"tokens": [
"0xdac17f958d2ee523a2206206994597c13d831ec7", # USDT
"0x6b175474e89094c44da98b954eedeac495271d0f" # DAI
]
}'
Common Challenges and Solutions
- Rate Limiting
// Add delay between requests
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
await delay(100); // Wait 100ms between requests
- Error Handling
try {
const balance = await contract.balanceOf(address);
} catch (error) {
if (error.code === 'CALL_EXCEPTION') {
console.log('Contract call failed - possibly not an ERC20 token');
}
}
- Gas Optimization
Use
callStatic
for simulating transactionsBatch multiple calls using
multicall
(advanced topic)
Best Practices
- Always Validate Addresses
if (!ethers.isAddress(address)) {
throw new Error('Invalid address');
}
Cache Contract Data Common data like decimals and symbols can be cached.
Use Type Safety TypeScript interfaces help catch errors early.
Handle Network Issues Implement retry logic for failed requests.
Conclusion
You've learned:
What JSON-RPC providers are and how to use them
How to read data from smart contracts
How to handle blockchain-specific data types
How to build a production-ready API
Next Steps
Try extending the API with:
Support for more token standards (ERC721, ERC1155)
Historical balance lookups
Price data integration
Multiple network support
Resources
Remember: Always test on testnets (like Sepolia) before deploying to mainnet!
Subscribe to my newsletter
Read articles from Diluk Angelo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Diluk Angelo
Diluk Angelo
Hey there! I'm Diluk Angelo, a Tech Lead and Web3 developer passionate about bridging the gap between traditional web solutions and the decentralized future. With years of leadership experience under my belt, I've guided teams and mentored developers in their technical journey. What really drives me is the art of transformation – taking proven Web2 solutions and reimagining them for the Web3 ecosystem while ensuring they remain scalable and efficient. Through this blog, I share practical insights from my experience in architecting decentralized solutions, leading technical teams, and navigating the exciting challenges of Web3 development. Whether you're a seasoned developer looking to pivot to Web3 or a curious mind exploring the possibilities of decentralized technology, you'll find actionable knowledge and real-world perspectives here. Expect deep dives into Web3 architecture, scalability solutions, team leadership in blockchain projects, and practical guides on transitioning from Web2 to Web3. I believe in making complex concepts accessible and sharing lessons learned from the trenches. Join me as we explore the future of the web, one block at a time!