⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

Web3.js Introduction: Building Decentralized Applications

November 15, 2022
web3ethereumblockchainjavascript
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!

Share:

💬 Comments