Web3 — Explained with Examples
Web3 is the decentralized internet model where users control their own data and digital assets through blockchain technology, enabling peer-to-peer interactions without intermediaries using wallets, smart contracts, and decentralized applications (dApps).
Why Web3 Matters
Web3 shifts power from centralized platforms back to users. Instead of Google owning your email and Facebook owning your social graph, you control your identity and data with a wallet. DodaTech is building Web3 features into Doda Browser — a built-in wallet, dApp browser, and decentralized file storage. The Web3 market is projected to grow to $40+ billion by 2030.
Web3 Architecture — How dApps Work
Think of Web3 like a vending machine network. The smart contract is the machine — it does exactly what it’s programmed to do. Your wallet is your money and ID card. The frontend is the button panel. When you press a button (click an action), the machine dispenses the result (executes the contract).
graph TD
subgraph Web3[<b>Web3 dApp Architecture</b>]
FE[Frontend<br/>HTML, CSS, React/Vue]
Wallet[Wallet<br/>MetaMask, WalletConnect]
Provider[Provider<br/>JSON-RPC to blockchain]
SC[Smart Contract<br/>Solidity, Vyper]
BC[Blockchain<br/>Ethereum, Polygon]
IPFS[IPFS<br/>Decentralized storage]
end
User[User] --> FE
FE --> Wallet
Wallet --> Provider
Provider -->|Read call<br/>Free, no gas| SC
Provider -->|Write call<br/>Costs gas| SC
SC --> BC
FE --> IPFS
style Web3 fill:#3b82f6,color:#fff
Read vs Write Transactions
Web3 has two fundamentally different types of blockchain interactions.
// read_vs_write.js
const { ethers } = require("ethers");
async function demonstrateReadWrite() {
const provider = new ethers.providers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/demo");
// READ — query blockchain state, free, no gas
const blockNumber = await provider.getBlockNumber();
const balance = await provider.getBalance("vitalik.eth");
console.log("=== READ Transactions (Free) ===");
console.log("Current block:", blockNumber);
console.log("Vitalik's ETH balance:", ethers.utils.formatEther(balance));
// READ a contract — using a read-only provider
const daiAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
const abi = ["function balanceOf(address) view returns (uint256)"];
const dai = new ethers.Contract(daiAddress, abi, provider);
const vitalikBalance = await dai.balanceOf("vitalik.eth");
console.log("Vitalik's DAI balance:", ethers.utils.formatEther(vitalikBalance));
// WRITE — needs a signer (private key) and costs gas
console.log("\n=== WRITE Transactions (Costs Gas) ===");
console.log("To write, you need:");
console.log(" 1. A signer (private key or MetaMask)");
console.log(" 2. ETH for gas");
console.log(" 3. A transaction to send");
// Simulated write (not executed — needs real signer)
console.log("\nExample write (not executed):");
console.log("const tx = await contract.transfer('0x...', amount);");
console.log("await tx.wait(); // Wait for confirmation");
console.log("console.log('Transaction mined:', tx.hash);");
}
demonstrateReadWrite();Expected output:
=== READ Transactions (Free) ===
Current block: 20123456
Vitalik's ETH balance: 1234.5678
Vitalik's DAI balance: 5678.9012
=== WRITE Transactions (Costs Gas) ===
To write, you need:
1. A signer (private key or MetaMask)
2. ETH for gas
3. A transaction to send
Example write (not executed):
const tx = await contract.transfer('0x...', amount);
await tx.wait(); // Wait for confirmation
console.log('Transaction mined:', tx.hash);Wallet Integration — MetaMask
MetaMask is the most popular Web3 wallet. Your dApp connects to it through the browser’s window.ethereum object.
<!DOCTYPE html>
<html>
<head>
<title>DodaTech dApp — Wallet Connect</title>
<style>
body { font-family: sans-serif; padding: 2rem; }
button { padding: 12px 24px; font-size: 16px; }
#status { margin-top: 1rem; padding: 1rem; background: #f5f5f5; border-radius: 8px; }
</style>
</head>
<body>
<h1>DodaTech dApp Demo</h1>
<button id="connect-btn">Connect MetaMask</button>
<div id="status">Not connected</div>
<script>
const connectBtn = document.getElementById('connect-btn');
const status = document.getElementById('status');
async function connectWallet() {
if (typeof window.ethereum === 'undefined') {
status.textContent = 'Please install MetaMask!';
status.style.background = '#fee2e2';
return;
}
try {
// Request account access
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const account = accounts[0];
// Get network info
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const networkNames = {
'0x1': 'Ethereum Mainnet',
'0x5': 'Goerli Testnet',
'0xaa36a7': 'Sepolia Testnet',
'0x89': 'Polygon',
'0x13881': 'Mumbai'
};
// Get balance
const balance = await window.ethereum.request({
method: 'eth_getBalance',
params: [account, 'latest']
});
const ethBalance = parseInt(balance, 16) / 1e18;
status.innerHTML = `
<strong>Connected!</strong><br>
Account: ${account.substring(0, 6)}...${account.substring(38)}<br>
Network: ${networkNames[chainId] || 'Unknown (' + chainId + ')'}<br>
Balance: ${ethBalance.toFixed(4)} ETH
`;
status.style.background = '#dcfce7';
// Listen for account changes
window.ethereum.on('accountsChanged', (accounts) => {
console.log('Account changed to:', accounts[0]);
window.location.reload();
});
} catch (error) {
console.error('Connection error:', error);
status.textContent = 'Connection rejected: ' + error.message;
status.style.background = '#fee2e2';
}
}
connectBtn.addEventListener('click', connectWallet);
</script>
</body>
</html>Web3.js — Interacting with Ethereum
Web3.js is the original JavaScript library for Ethereum interaction.
// web3js_demo.js
const Web3 = require("web3");
async function web3jsExample() {
const web3 = new Web3("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
// Get account balances
const accounts = [
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // vitalik.eth
"0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B" // VB2
];
console.log("=== Account Balances ===");
for (const addr of accounts) {
const balance = await web3.eth.getBalance(addr);
console.log(`${addr.substring(0, 10)}... → ${web3.utils.fromWei(balance, "ether")} ETH`);
}
// Get transaction details
const txHash = "0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b";
try {
const tx = await web3.eth.getTransaction(txHash);
console.log("\n=== Transaction Details ===");
console.log("From:", tx.from);
console.log("To:", tx.to);
console.log("Value:", web3.utils.fromWei(tx.value, "ether"), "ETH");
console.log("Gas price:", web3.utils.fromWei(tx.gasPrice, "gwei"), "Gwei");
} catch {
console.log("Transaction not found (demo hash)");
}
// Create contract instance
const abi = [{"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"}];
const contract = new web3.eth.Contract(abi, "0x6B175474E89094C44Da98b954EedeAC495271d0F");
const totalSupply = await contract.methods.totalSupply().call();
console.log("\nDAI Total Supply:", web3.utils.fromWei(totalSupply, "ether"));
}
web3jsExample();Expected output:
=== Account Balances ===
0xd8dA6BF26... → 1234.5678 ETH
0xAb5801a7D... → 890.1234 ETH
=== Transaction Details ===
From: 0x1234...abcd
To: 0x5678...ef01
Value: 10.5 ETH
Gas price: 25 Gwei
DAI Total Supply: 5000000000.0Ethers.js — The Modern Alternative
ethers.js is lighter and more developer-friendly than Web3.js.
// ethersjs_demo.js
const { ethers } = require("ethers");
async function ethersjsExample() {
// Connecting to Ethereum
const provider = new ethers.providers.JsonRpcProvider(
"https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
);
// ENS resolution (name to address)
const vitalikAddress = await provider.resolveName("vitalik.eth");
console.log("vitalik.eth →", vitalikAddress);
// Reverse resolution (address to name)
const name = await provider.lookupAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
console.log("Address →", name);
// Formatting utilities
const weiAmount = ethers.utils.parseEther("1.5");
console.log("\n1.5 ETH =", weiAmount.toString(), "wei");
const ethAmount = ethers.utils.formatEther("1500000000000000000");
console.log("1,500,000,000,000,000,000 wei =", ethAmount, "ETH");
// Contract interaction
const contractABI = [
"function balanceOf(address) view returns (uint256)",
"function symbol() view returns (string)"
];
const usdcContract = new ethers.Contract(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
contractABI,
provider
);
const symbol = await usdcContract.symbol();
const whales = ["0x55FE002aefF02F77364de339a1292923A15844B8", "0x47ac0Fb4F2D84898e04D9e62b9F7C3cbF6A5b3d9"];
console.log(`\n=== ${symbol} Whale Balances ===`);
for (const address of whales) {
const bal = await usdcContract.balanceOf(address);
console.log(`${address.substring(0, 10)}... → ${ethers.utils.formatUnits(bal, 6)} ${symbol}`);
}
}
ethersjsExample();Expected output:
vitalik.eth → 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
Address → vitalik.eth
1.5 ETH = 1500000000000000000 wei
1,500,000,000,000,000,000 wei = 1.5 ETH
=== USDC Whale Balances ===
0x55FE002ae... → 250,000,000 USDC
0x47ac0Fb4F... → 180,500,000 USDCSending a Transaction with Ethers.js
// send_transaction.js
const { ethers } = require("ethers");
async function sendTransaction() {
// Connect to Sepolia testnet
const provider = new ethers.providers.JsonRpcProvider(
"https://sepolia.infura.io/v3/YOUR_PROJECT_ID"
);
// NEVER expose private keys in code! Use environment variables.
const privateKey = process.env.PRIVATE_KEY;
if (!privateKey) {
console.log("Set PRIVATE_KEY environment variable");
console.log("\nSimulating transaction flow...\n");
simulateTransaction();
return;
}
const wallet = new ethers.Wallet(privateKey, provider);
const balance = await wallet.getBalance();
console.log("Wallet balance:", ethers.utils.formatEther(balance), "ETH");
// Send ETH to another address
const tx = await wallet.sendTransaction({
to: "0xRecipientAddressHere",
value: ethers.utils.parseEther("0.01")
});
console.log("Transaction sent:", tx.hash);
console.log("Waiting for confirmation...");
const receipt = await tx.wait();
console.log("Confirmed in block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
}
function simulateTransaction() {
const tx = {
to: "0xRecipientAddress",
value: ethers.utils.parseEther("0.01"),
gasLimit: 21000,
gasPrice: ethers.utils.parseUnits("25", "gwei"),
nonce: 42,
chainId: 11155111 // Sepolia
};
const estimatedCost = tx.gasLimit * tx.gasPrice;
console.log("=== Transaction Simulation ===");
console.log("To:", tx.to);
console.log("Amount: 0.01 ETH");
console.log("Gas limit:", tx.gasLimit);
console.log("Gas price: 25 Gwei");
console.log("Max fee:", ethers.utils.formatEther(estimatedCost), "ETH");
console.log("Total cost: 0.01" + " + " + ethers.utils.formatEther(estimatedCost) + " = " +
ethers.utils.formatEther(ethers.utils.parseEther("0.01").add(estimatedCost)) + " ETH");
}
sendTransaction();Expected output (without PRIVATE_KEY):
Set PRIVATE_KEY environment variable
Simulating transaction flow...
=== Transaction Simulation ===
To: 0xRecipientAddress
Amount: 0.01 ETH
Gas limit: 21000
Gas price: 25 Gwei
Max fee: 0.000525 ETH
Total cost: 0.010525 ETHdApp Project Structure
my-dapp/
├── contracts/
│ └── MyNFT.sol
├── scripts/
│ ├── deploy.js
│ └── mint.js
├── frontend/
│ ├── index.html
│ ├── app.js
│ └── style.css
├── test/
│ └── MyNFT.test.js
├── hardhat.config.js
├── package.json
└── README.mdCommon Mistakes
- Not handling network changes: Users switch networks in MetaMask. Listen for
chainChangedevents and update your dApp accordingly. - Exposing private keys in client-side code: Never put private keys in frontend code. Use MetaMask for user transactions and environment variables for backend.
- Not waiting for transaction confirmations: Calling
contract.methodwithoutawait tx.wait()means you’re checking state before the transaction is mined. - Using Mainnet for testing: Always start on Sepolia or Hardhat local network. Mainnet mistakes cost real money.
- Not handling wallet disconnection: Users can disconnect MetaMask. Your dApp should gracefully handle missing wallets and prompt reconnection.
Practice Questions
- What is the difference between a read and a write transaction?
- What does
window.ethereumprovide in a dApp? - How does an ENS name like “vitalik.eth” resolve to an address?
- What is the purpose of
tx.wait()after sending a transaction? - Why should you never put private keys in frontend JavaScript?
Answers:
- Read transactions query blockchain state without modifying it — they’re free. Write transactions modify state (transfer tokens, mint NFTs) and cost gas.
window.ethereumis the provider injected by MetaMask. It provides access to the user’s accounts, allows signing transactions, and connects to the blockchain.- ENS resolves human-readable names to addresses via a smart contract.
provider.resolveName('vitalik.eth')looks up the ENS contract and returns the associated address. tx.wait()waits for the transaction to be mined and included in a block. Without it, you’d check state before the transaction is confirmed.- Private keys give full control of the wallet. Client-side code is visible to users. Exposure would let anyone steal all funds. Always use MetaMask or a backend signer.
Mini Project: Build a dApp that Reads and Writes
Build a complete dApp that:
- Shows a “Connect Wallet” button (MetaMask integration)
- Detects network and account changes
- Reads the current ETH balance of the connected wallet
- Displays the latest block number
- Reads a value from a deployed smart contract
- Allows the user to write to the contract (e.g., set a message)
- Shows the transaction status (pending, confirmed, failed)
- Works on Sepolia testnet
This is the foundation DodaTech uses for browser-based dApps integrated into Doda Browser’s Web3 features.
Related topics: Ethereum, Smart Contracts, NFTs, DeFi, Layer 2
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro