Web3.js Introduction: Building Decentralized Applications
Web3.js Introduction: Building Decentralized Applications
Web3.js is a collection of libraries that allow you to interact with a local or remote Ethereum node using HTTP, IPC, or WebSocket. This guide will help you understand the fundamentals of building decentralized applications (dApps).
What is Web3.js?
Web3.js serves as the bridge between traditional web applications and the Ethereum blockchain. It enables:
- Reading blockchain data
- Sending transactions
- Interacting with smart contracts
- Managing user accounts
Installation
# Using npm
npm install web3
# Using yarn
yarn add web3
# Using pnpm
pnpm add web3
Setting Up Web3.js
Connecting to an Ethereum Node
import Web3 from 'web3'
// Connect to a local node
const web3 = new Web3('http://localhost:8545')
// Connect to a remote node (Infura)
const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_PROJECT_ID')
// Connect to a WebSocket
const web3 = new Web3('wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID')
// Using MetaMask provider
if (window.ethereum) {
const web3 = new Web3(window.ethereum)
await window.ethereum.request({ method: 'eth_requestAccounts' })
}
Checking Connection
async function checkConnection() {
try {
const blockNumber = await web3.eth.getBlockNumber()
console.log('Current block:', blockNumber)
const networkId = await web3.eth.net.getId()
console.log('Network ID:', networkId)
return true
} catch (error) {
console.error('Connection failed:', error)
return false
}
}
Working with Accounts
Getting Account Balance
async function getBalance(address) {
const balanceWei = await web3.eth.getBalance(address)
// Convert from Wei to Ether
const balanceEth = web3.utils.fromWei(balanceWei, 'ether')
console.log(`Balance: ${balanceEth} ETH`)
return balanceEth
}
// Usage
getBalance('0x742d35Cc6634C0532925a3b844Bc454e4438f44e')
Getting Account Information
async function getAccountInfo(address) {
const balance = await web3.eth.getBalance(address)
const code = await web3.eth.getCode(address)
const transactionCount = await web3.eth.getTransactionCount(address)
return {
address,
balance: web3.utils.fromWei(balance, 'ether'),
isContract: code !== '0x',
transactionCount,
}
}
Sending Transactions
Basic Transaction
async function sendTransaction(from, to, amount, privateKey) {
const nonce = await web3.eth.getTransactionCount(from)
const txObject = {
from,
to,
value: web3.utils.toWei(amount, 'ether'),
gas: 21000,
gasPrice: await web3.eth.getGasPrice(),
nonce,
}
// Sign the transaction
const signedTx = await web3.eth.accounts.signTransaction(
txObject,
privateKey
)
// Send the transaction
const receipt = await web3.eth.sendSignedTransaction(
signedTx.rawTransaction
)
console.log('Transaction receipt:', receipt)
return receipt
}
Sending with MetaMask
async function sendWithMetaMask(toAddress, amountEth) {
if (!window.ethereum) {
throw new Error('MetaMask not installed')
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
})
const from = accounts[0]
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from,
to: toAddress,
value: web3.utils.toHex(web3.utils.toWei(amountEth, 'ether')),
gas: web3.utils.toHex(21000),
}],
})
console.log('Transaction hash:', txHash)
return txHash
}
Interacting with Smart Contracts
Contract ABI and Address
const contractABI = [
{
"inputs": [],
"name": "getCount",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "increment",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "decrement",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
const contractAddress = '0x123...'
const contract = new web3.eth.Contract(contractABI, contractAddress)
Reading Contract Data (View Functions)
async function readContract() {
// Call a view function (no gas cost)
const count = await contract.methods.getCount().call()
console.log('Current count:', count)
return count
}
Writing to Contract (State-Changing Functions)
async function writeToContract(fromAddress) {
// Estimate gas
const gasEstimate = await contract.methods.increment().estimateGas({
from: fromAddress
})
// Send transaction
const receipt = await contract.methods.increment().send({
from: fromAddress,
gas: gasEstimate,
})
console.log('Transaction receipt:', receipt)
return receipt
}
Handling Events
// Listen for events
contract.events.CounterIncremented({
fromBlock: 0
}, (error, event) => {
if (error) {
console.error('Event error:', error)
return
}
console.log('Event:', event.returnValues)
})
// Get past events
async function getPastEvents() {
const events = await contract.getPastEvents('CounterIncremented', {
fromBlock: 0,
toBlock: 'latest'
})
console.log('Past events:', events)
return events
}
Utility Functions
Unit Conversion
// Wei to Ether
const ether = web3.utils.fromWei('1000000000000000000', 'ether')
console.log(ether) // "1"
// Ether to Wei
const wei = web3.utils.toWei('1', 'ether')
console.log(wei) // "1000000000000000000"
// Other units: gwei, finney, szabo, etc.
const gwei = web3.utils.fromWei('1000000000', 'gwei')
console.log(gwei) // "1"
Address Validation
const address = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'
// Check if address is valid
const isValid = web3.utils.isAddress(address)
console.log('Valid:', isValid) // true
// Convert to checksum address
const checksumAddress = web3.utils.toChecksumAddress(address)
console.log('Checksum:', checksumAddress)
// Check if checksum
const isChecksum = web3.utils.checkAddressChecksum(checksumAddress)
console.log('Is checksum:', isChecksum) // true
Hash Functions
// SHA3 (Keccak-256)
const hash = web3.utils.sha3('Hello World')
console.log('Hash:', hash)
// Solidity-style hash
const solidityHash = web3.utils.soliditySha3(
{ type: 'string', value: 'Hello' },
{ type: 'uint256', value: 123 }
)
console.log('Solidity hash:', solidityHash)
Real-World Example: Token Transfer
// ERC-20 Token ABI (simplified)
const erc20ABI = [
{
"inputs": [{"name": "account", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"name": "recipient", "type": "address"},
{"name": "amount", "type": "uint256"}
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function"
}
]
async function transferToken(
tokenAddress,
recipientAddress,
amount,
fromAddress
) {
const tokenContract = new web3.eth.Contract(erc20ABI, tokenAddress)
// Get token decimals
const decimals = await tokenContract.methods.decimals().call()
// Convert amount to token units
const amountInUnits = web3.utils.toWei(amount, 'ether')
// Estimate gas
const gasEstimate = await tokenContract.methods
.transfer(recipientAddress, amountInUnits)
.estimateGas({ from: fromAddress })
// Send transaction
const receipt = await tokenContract.methods
.transfer(recipientAddress, amountInUnits)
.send({
from: fromAddress,
gas: gasEstimate,
})
return receipt
}
React Integration
Web3 Context Provider
// context/Web3Context.jsx
import React, { createContext, useContext, useState, useEffect } from 'react'
import Web3 from 'web3'
const Web3Context = createContext()
export function Web3Provider({ children }) {
const [web3, setWeb3] = useState(null)
const [account, setAccount] = useState(null)
const [chainId, setChainId] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
if (window.ethereum) {
const web3Instance = new Web3(window.ethereum)
setWeb3(web3Instance)
// Handle account changes
window.ethereum.on('accountsChanged', (accounts) => {
setAccount(accounts[0])
})
// Handle chain changes
window.ethereum.on('chainChanged', (chainId) => {
setChainId(parseInt(chainId, 16))
window.location.reload()
})
}
}, [])
const connect = async () => {
try {
if (!window.ethereum) {
throw new Error('Please install MetaMask')
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
})
const chainId = await window.ethereum.request({
method: 'eth_chainId'
})
setAccount(accounts[0])
setChainId(parseInt(chainId, 16))
setError(null)
} catch (err) {
setError(err.message)
}
}
return (
<Web3Context.Provider value={{ web3, account, chainId, connect, error }}>
{children}
</Web3Context.Provider>
)
}
export function useWeb3() {
return useContext(Web3Context)
}
Using the Provider
// components/Dapp.jsx
import { useWeb3 } from '@/context/Web3Context'
export function Dapp() {
const { web3, account, chainId, connect, error } = useWeb3()
if (error) {
return <div>Error: {error}</div>
}
if (!account) {
return <button onClick={connect}>Connect Wallet</button>
}
return (
<div>
<p>Connected: {account}</p>
<p>Chain ID: {chainId}</p>
<p>Network: {chainId === 1 ? 'Mainnet' : 'Testnet'}</p>
</div>
)
}
Error Handling
async function handleTransaction(txPromise) {
try {
const receipt = await txPromise
return { success: true, receipt }
} catch (error) {
// Parse common errors
if (error.code === 4001) {
return { success: false, error: 'User rejected transaction' }
}
if (error.message.includes('insufficient funds')) {
return { success: false, error: 'Insufficient funds' }
}
if (error.message.includes('gas')) {
return { success: false, error: 'Gas estimation failed' }
}
return { success: false, error: error.message }
}
}
Best Practices
1. Always Validate Inputs
function validateAddress(address) {
if (!web3.utils.isAddress(address)) {
throw new Error('Invalid address format')
}
return web3.utils.toChecksumAddress(address)
}
function validateAmount(amount) {
const num = parseFloat(amount)
if (isNaN(num) || num <= 0) {
throw new Error('Invalid amount')
}
return amount
}
2. Handle Network Changes
async function ensureCorrectNetwork(targetChainId) {
const currentChainId = await web3.eth.getChainId()
if (currentChainId !== targetChainId) {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${targetChainId.toString(16)}` }],
})
} catch (switchError) {
// Chain not added to wallet
if (switchError.code === 4902) {
await addNetwork(targetChainId)
}
throw switchError
}
}
}
3. Use Event Listeners Wisely
// Clean up listeners on unmount
useEffect(() => {
const subscription = contract.events.Transfer()
.on('data', handleTransfer)
.on('error', console.error)
return () => {
subscription.unsubscribe()
}
}, [])
Security Considerations
// Never store private keys in code
// BAD
const privateKey = '0x1234567890abcdef...'
// GOOD - Use environment variables
const privateKey = process.env.PRIVATE_KEY
// BETTER - Use wallet providers (MetaMask, WalletConnect)
// Always verify transactions before signing
async function verifyTransaction(tx) {
console.log('To:', tx.to)
console.log('Value:', web3.utils.fromWei(tx.value, 'ether'), 'ETH')
console.log('Data:', tx.data)
// Prompt user confirmation
const confirmed = confirm('Confirm transaction?')
if (!confirmed) {
throw new Error('Transaction cancelled')
}
}
Conclusion
Web3.js is a powerful library for building decentralized applications. By understanding its core concepts and following best practices, you can create secure and user-friendly dApps.
Key Takeaways
- Use MetaMask or other wallet providers for user interactions
- Always validate inputs and handle errors gracefully
- Test thoroughly on testnets before deploying to mainnet
- Follow security best practices for handling sensitive data
Start building your first dApp today and join the decentralized web revolution!