# 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

1. Understanding Web3 Infrastructure
    
2. Setting Up Your Development Environment
    
3. Reading Smart Contract Data
    
4. 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:

1. **Node Providers (Recommended for Beginners)**:
    
    * [Alchemy](https://alchemy.com) - Generous free tier
        
    * [Infura](https://infura.io) - Popular choice
        
    * [QuickNode](https://quicknode.com) - Good performance
        
2. **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:

1. Sign up at [Alchemy.com](http://Alchemy.com)
    
2. Create a new app (choose Ethereum mainnet or Sepolia testnet)
    
3. Get your API key and HTTP URL (looks like: [`https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY`](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:

```bash
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:

```typescript
ALCHEMY_URL=your_alchemy_url_here
```

Create a basic TypeScript configuration in `tsconfig.json`:

```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:

1. Contract Address (where the contract lives on the blockchain)
    
2. ABI (Application Binary Interface - tells us how to interact with the contract)
    
3. 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:

```typescript
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`:

```typescript
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:

1. **Provider Setup**:
    
    ```typescript
    const provider = new ethers.JsonRpcProvider(process.env.ALCHEMY_URL);
    ```
    
    This creates our connection to Ethereum using our Alchemy URL.
    
2. **Contract Instance**:
    
    ```typescript
    const contract = new ethers.Contract(USDT_ADDRESS, ERC20_ABI, provider);
    ```
    
    This creates an interface to interact with the smart contract.
    
3. **Reading Data**:
    
    ```typescript
    const balance = await contract.balanceOf(walletAddress);
    ```
    
    This calls the `balanceOf` function on the smart contract.
    
4. **Formatting Data**:
    
    ```typescript
    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`:

```typescript
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:

```bash
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

1. **Rate Limiting**
    

```typescript
// Add delay between requests
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
await delay(100); // Wait 100ms between requests
```

2. **Error Handling**
    

```typescript
try {
    const balance = await contract.balanceOf(address);
} catch (error) {
    if (error.code === 'CALL_EXCEPTION') {
        console.log('Contract call failed - possibly not an ERC20 token');
    }
}
```

3. **Gas Optimization**
    

* Use `callStatic` for simulating transactions
    
* Batch multiple calls using `multicall` (advanced topic)
    

## Best Practices

1. **Always Validate Addresses**
    

```typescript
if (!ethers.isAddress(address)) {
    throw new Error('Invalid address');
}
```

2. **Cache Contract Data** Common data like decimals and symbols can be cached.
    
3. **Use Type Safety** TypeScript interfaces help catch errors early.
    
4. **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

* [ethers.js Documentation](https://docs.ethers.org/v6/)
    
* [Ethereum Developer Portal](https://ethereum.org/developers/)
    
* [Alchemy Documentation](https://docs.alchemy.com/)
    

Remember: Always test on testnets (like Sepolia) before deploying to mainnet!
