सामग्री पर जाएं
·34 मिनट पढ़ने का समय

Web Developers के लिए Ethereum: बिना Hype के Smart Contracts

हर web developer को जो Ethereum concepts चाहिए: accounts, transactions, smart contracts, ABI encoding, ethers.js, WAGMI, और बिना अपना node चलाए on-chain data पढ़ना।

साझा करें:X / TwitterLinkedIn

ज़्यादातर "Ethereum for developers" content दो categories में आता है: oversimplified analogies जो कुछ बनाने में मदद नहीं करतीं, या deep protocol specifications जो मानकर चलती हैं कि आपको पहले से Merkle Patricia Trie क्या है पता है। दोनों में से कोई काम का नहीं है अगर आप एक web developer हैं जो token balance पढ़ना चाहते हैं, user को transaction sign करवाना चाहते हैं, या React app में NFT metadata दिखाना चाहते हैं।

यह post practical middle ground है। मैं exactly बताऊंगा कि जब आपका frontend Ethereum से बात करता है तो क्या होता है, moving parts क्या हैं, और modern tooling (ethers.js, viem, WAGMI) कैसे उन concepts से map होती है जो आप web applications बनाते हुए पहले से जानते हैं।

Vending machines के बारे में कोई metaphors नहीं। कोई "imagine a world where..." नहीं। बस technical model और code।

Mental Model#

Ethereum एक replicated state machine है। Network का हर node state की identical copy maintain करता है — एक massive key-value store जो addresses को account data से map करता है। जब आप "transaction send" करते हैं, तो आप एक state transition propose कर रहे हैं। अगर enough validators agree करते हैं कि यह valid है, तो state update हो जाता है। बस इतना ही।

State अपने आप में straightforward है। यह 20-byte addresses से account objects तक की mapping है। हर account के चार fields हैं:

  • nonce: इस account ने कितने transactions भेजे हैं (EOAs के लिए) या कितने contracts create किए हैं (contract accounts के लिए)। यह replay attacks रोकता है।
  • balance: ETH की amount, wei में denominated (1 ETH = 10^18 wei)। हमेशा big integer।
  • codeHash: EVM bytecode का hash। Regular wallets (EOAs) के लिए, यह empty bytes का hash है। Contracts के लिए, यह deployed code का hash है।
  • storageRoot: Account के storage trie का root hash। सिर्फ contracts के पास meaningful storage होता है।

दो तरह के accounts हैं, और यह distinction आगे आने वाली हर चीज़ के लिए important है:

Externally Owned Accounts (EOAs) private key से control होते हैं। MetaMask इन्हीं को manage करता है। ये transactions initiate कर सकते हैं। इनके पास कोई code नहीं है। जब कोई "wallet" बोलता है, तो उनका मतलब EOA है।

Contract Accounts अपने code से control होते हैं। ये transactions initiate नहीं कर सकते — ये सिर्फ call होने पर respond में execute होते हैं। इनके पास code और storage है। जब कोई "smart contract" बोलता है, तो उनका मतलब यह है। Code deploy होने के बाद immutable है (proxy patterns के कुछ exceptions के साथ, जो अलग discussion है)।

Critical insight: Ethereum पर हर state change एक EOA के transaction sign करने से शुरू होता है। Contracts दूसरे contracts को call कर सकते हैं, लेकिन execution की chain हमेशा एक human (या bot) से शुरू होती है जिसके पास private key है।

Gas: Compute की कीमत#

EVM में हर operation gas cost करता है। दो numbers add करना 3 gas cost करता है। 32-byte word store करना 20,000 gas cost करता है (पहली बार) या 5,000 gas (update)। Storage read करना 2,100 gas cost करता है (cold) या 100 gas (warm, इस transaction में पहले access हो चुका)।

आप gas "gas units" में pay नहीं करते। आप ETH में pay करते हैं। Total cost है:

totalCost = gasUsed * gasPrice

EIP-1559 (London upgrade) के बाद, gas pricing two-part system बन गई:

totalCost = gasUsed * (baseFee + priorityFee)
  • baseFee: Network congestion के basis पर protocol set करता है। Burn (destroy) होता है।
  • priorityFee (tip): Validator को जाता है। ज़्यादा tip = तेज़ inclusion।
  • maxFeePerGas: Maximum जो आप per gas unit pay करने को तैयार हैं।
  • maxPriorityFeePerGas: Per gas unit maximum tip।

अगर baseFee + priorityFee > maxFeePerGas, तो आपका transaction baseFee कम होने तक wait करता है। इसीलिए high congestion में transactions "stuck" हो जाते हैं।

Web developers के लिए practical implication: data पढ़ना free है। Data लिखना पैसे लगता है। Web2 और Web3 के बीच यह सबसे important architectural difference है। हर SELECT free है। हर INSERT, UPDATE, DELETE की असली कीमत है। अपने dApps accordingly design करें।

Transactions#

Transaction एक signed data structure है। यहां वो fields हैं जो matter करते हैं:

typescript
interface Transaction {
  // Who receives this transaction — an EOA address or a contract address
  to: string;      // 20-byte hex address, or null for contract deployment
  // How much ETH to send (in wei)
  value: bigint;   // Can be 0n for pure contract calls
  // Encoded function call data, or empty for plain ETH transfers
  data: string;    // Hex-encoded bytes, "0x" for simple transfers
  // Sequential counter, prevents replay attacks
  nonce: number;   // Must exactly equal sender's current nonce
  // Gas limit — maximum gas this tx can consume
  gasLimit: bigint;
  // EIP-1559 fee parameters
  maxFeePerGas: bigint;
  maxPriorityFeePerGas: bigint;
  // Chain identifier (1 = mainnet, 11155111 = Sepolia, 137 = Polygon)
  chainId: number;
}

Transaction की Lifecycle#

  1. Construction: आपका app transaction object build करता है। अगर आप contract function call कर रहे हैं, तो data field में ABI-encoded function call होता है (इसके बारे में नीचे और बताऊंगा)।

  2. Signing: Private key RLP-encoded transaction sign करता है, v, r, s signature components produce करता है। यह prove करता है कि sender ने इस specific transaction को authorize किया। Sender address signature से derive होता है — यह explicitly transaction में नहीं है।

  3. Broadcasting: Signed transaction eth_sendRawTransaction के through RPC node को भेजा जाता है। Node इसे validate करता है (correct nonce, sufficient balance, valid signature) और अपने mempool में add करता है।

  4. Mempool: Transaction pending transactions के pool में बैठता है। Validators next block में include करने के लिए transactions select करते हैं, generally higher tips prefer करते हुए। यहीं front-running होती है — दूसरे actors आपका pending transaction देख सकते हैं और अपना transaction higher tip के साथ submit कर सकते हैं ताकि आपसे पहले execute हो।

  5. Inclusion: एक validator आपका transaction block में include करता है। EVM इसे execute करता है। अगर succeed होता है, तो state changes apply होते हैं। अगर revert होता है, तो state changes rollback हो जाते हैं — लेकिन revert point तक consume हुआ gas फिर भी pay करना पड़ता है।

  6. Finality: Proof-of-stake Ethereum पर, block two epochs (~12.8 minutes) बाद "finalized" हो जाता है। Finality से पहले, chain reorganizations theoretically possible हैं (हालांकि rare)। ज़्यादातर apps non-critical operations के लिए 1-2 block confirmations को "good enough" मानते हैं।

ethers.js v6 के साथ simple ETH transfer भेजना कुछ ऐसा दिखता है:

typescript
import { ethers } from "ethers";
 
const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
const wallet = new ethers.Wallet("0xYOUR_PRIVATE_KEY", provider);
 
const tx = await wallet.sendTransaction({
  to: "0xRecipientAddress...",
  value: ethers.parseEther("0.1"), // Converts "0.1" to wei (100000000000000000n)
});
 
console.log("Tx hash:", tx.hash);
 
// Wait for inclusion in a block
const receipt = await tx.wait();
console.log("Block number:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Status:", receipt.status); // 1 = success, 0 = revert

और viem के साथ:

typescript
import { createWalletClient, http, parseEther } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
 
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
 
const client = createWalletClient({
  account,
  chain: mainnet,
  transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});
 
const hash = await client.sendTransaction({
  to: "0xRecipientAddress...",
  value: parseEther("0.1"),
});
 
console.log("Tx hash:", hash);

Difference notice करें: ethers एक TransactionResponse object return करता है जिसमें .wait() method है। Viem सिर्फ hash return करता है — confirmation wait करने के लिए अलग से publicClient.waitForTransactionReceipt({ hash }) call करना पड़ता है। यह separation of concerns viem के design में intentional है।

Smart Contracts#

Smart contract एक specific address पर deployed bytecode plus persistent storage है। जब आप contract "call" करते हैं, तो आप transaction भेज रहे हैं (या read-only call कर रहे हैं) जिसमें data field encoded function invocation पर set है।

Bytecode और ABI#

Bytecode compiled EVM code है। आप इससे directly interact नहीं करते। यह वो है जो EVM execute करता है।

ABI (Application Binary Interface) contract के interface का JSON description है। यह आपकी client library को बताता है कि function calls कैसे encode करें और return values कैसे decode करें। इसे contract के लिए OpenAPI spec की तरह समझें।

यहां ERC-20 token ABI का एक fragment है:

typescript
const ERC20_ABI = [
  // Read-only functions (view/pure — no gas cost when called externally)
  "function name() view returns (string)",
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function totalSupply() view returns (uint256)",
  "function balanceOf(address owner) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
 
  // State-changing functions (require a transaction, cost gas)
  "function transfer(address to, uint256 amount) returns (bool)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "function transferFrom(address from, address to, uint256 amount) returns (bool)",
 
  // Events (emitted during execution, stored in transaction logs)
  "event Transfer(address indexed from, address indexed to, uint256 value)",
  "event Approval(address indexed owner, address indexed spender, uint256 value)",
] as const;

Ethers.js यह "human-readable ABI" format accept करता है। Viem भी इसे use कर सकता है, लेकिन अक्सर आप Solidity compiler द्वारा generate की गई full JSON ABI के साथ काम करेंगे। दोनों equivalent हैं — human-readable format common interfaces के लिए बस ज़्यादा convenient है।

Function Calls कैसे Encode होते हैं#

यह वो part है जो ज़्यादातर tutorials skip करते हैं, और यही वो part है जो आपके debugging के घंटों बचाएगा।

जब आप transfer("0xBob...", 1000000) call करते हैं, तो transaction का data field कुछ ऐसे set होता है:

0xa9059cbb                                                         // Function selector
0000000000000000000000000xBob...000000000000000000000000             // address, padded to 32 bytes
00000000000000000000000000000000000000000000000000000000000f4240     // uint256 amount (1000000 in hex)

Function selector function signature के Keccak-256 hash के पहले 4 bytes हैं:

keccak256("transfer(address,uint256)") = 0xa9059cbb...
selector = first 4 bytes = 0xa9059cbb

बाकी bytes ABI-encoded arguments हैं, हर एक 32 bytes तक padded। यह encoding scheme deterministic है — same function call हमेशा same calldata produce करता है।

यह matter क्यों करता है? क्योंकि जब आप Etherscan पर raw transaction data देखते हैं और यह 0xa9059cbb से start होता है, तो आप जानते हैं कि यह transfer call है। जब आपका transaction revert होता है और error message सिर्फ hex blob है, तो आप ABI use करके decode कर सकते हैं। और जब आप transaction batches build कर रहे हैं या multicall contracts के साथ interact कर रहे हैं, तो आप manually calldata encode कर रहे होंगे।

ethers.js के साथ manually encode और decode करने का तरीका:

typescript
import { ethers } from "ethers";
 
const iface = new ethers.Interface(ERC20_ABI);
 
// Encode a function call
const calldata = iface.encodeFunctionData("transfer", [
  "0xBobAddress...",
  1000000n,
]);
console.log(calldata);
// 0xa9059cbb000000000000000000000000bob...000000000000000000000000000f4240
 
// Decode calldata back to function name and args
const decoded = iface.parseTransaction({ data: calldata });
console.log(decoded.name);       // "transfer"
console.log(decoded.args[0]);    // "0xBobAddress..."
console.log(decoded.args[1]);    // 1000000n (BigInt)
 
// Decode a function's return data
const returnData = "0x0000000000000000000000000000000000000000000000000000000000000001";
const result = iface.decodeFunctionResult("transfer", returnData);
console.log(result[0]); // true

Storage Slots#

Contract storage एक key-value store है जहां keys और values दोनों 32 bytes हैं। Solidity storage slots को 0 से sequentially assign करता है। पहला declared state variable slot 0 में जाता है, अगला slot 1 में, और इसी तरह आगे। Mappings और dynamic arrays hash-based scheme use करते हैं।

आप किसी भी contract का storage directly पढ़ सकते हैं, भले ही variable Solidity में private mark हो। "Private" का सिर्फ मतलब है कि दूसरे contracts इसे नहीं पढ़ सकते — कोई भी eth_getStorageAt के ज़रिए पढ़ सकता है:

typescript
// Reading storage slot 0 of a contract
const slot0 = await provider.getStorage(
  "0xContractAddress...",
  0
);
console.log(slot0); // Raw 32-byte hex value

Block explorers ऐसे ही "internal" contract state दिखाते हैं। Storage reads पर कोई access control नहीं है। Public blockchain पर privacy fundamentally limited है।

Events और Logs#

Events contract का तरीका है structured data emit करने का जो transaction logs में store होता है लेकिन contract storage में नहीं। ये storage writes से सस्ते हैं (375 gas first topic के लिए + 8 gas per byte of data, बनाम storage write के लिए 20,000 gas) और इन्हें efficiently query करने के लिए design किया गया है।

एक event में 3 indexed parameters तक हो सकते हैं (topics के रूप में store होते हैं) और कितने भी non-indexed parameters (data के रूप में store होते हैं)। Indexed parameters पर filter कर सकते हैं — आप पूछ सकते हैं "मुझे वो सारे Transfer events दो जहां to यह address है।" Non-indexed parameters पर filter नहीं कर सकते; आपको सारे matching events fetch करके client-side filter करना होगा।

typescript
// Listening for Transfer events in real-time with ethers.js
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
 
contract.on("Transfer", (from, to, value, event) => {
  console.log(`${from} -> ${to}: ${ethers.formatUnits(value, 18)} tokens`);
  console.log("Block:", event.log.blockNumber);
  console.log("Tx hash:", event.log.transactionHash);
});
 
// Querying historical events
const filter = contract.filters.Transfer(null, "0xMyAddress..."); // from=any, to=specific
const events = await contract.queryFilter(filter, 19000000, 19100000); // block range
 
for (const event of events) {
  console.log("From:", event.args.from);
  console.log("Value:", event.args.value.toString());
}

Viem के साथ:

typescript
import { createPublicClient, http, parseAbiItem } from "viem";
import { mainnet } from "viem/chains";
 
const client = createPublicClient({
  chain: mainnet,
  transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});
 
// Historical logs
const logs = await client.getLogs({
  address: "0xTokenAddress...",
  event: parseAbiItem(
    "event Transfer(address indexed from, address indexed to, uint256 value)"
  ),
  args: {
    to: "0xMyAddress...",
  },
  fromBlock: 19000000n,
  toBlock: 19100000n,
});
 
for (const log of logs) {
  console.log("From:", log.args.from);
  console.log("To:", log.args.to);
  console.log("Value:", log.args.value);
}
 
// Real-time watching
const unwatch = client.watchEvent({
  address: "0xTokenAddress...",
  event: parseAbiItem(
    "event Transfer(address indexed from, address indexed to, uint256 value)"
  ),
  onLogs: (logs) => {
    for (const log of logs) {
      console.log(`Transfer: ${log.args.from} -> ${log.args.to}`);
    }
  },
});
 
// Call unwatch() to stop listening

On-Chain Data पढ़ना#

यहीं Ethereum web developers के लिए practical बनता है। आपको node run करने की ज़रूरत नहीं। Mine करने की ज़रूरत नहीं। Wallet की भी ज़रूरत नहीं। Ethereum से data पढ़ना free है, permissionless है, और simple JSON-RPC API के through काम करता है।

JSON-RPC: Ethereum की HTTP API#

हर Ethereum node JSON-RPC API expose करता है। यह literally JSON bodies के साथ HTTP POST है। Transport layer में blockchain-specific कुछ नहीं है।

typescript
// This is what your library does under the hood
const response = await fetch("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "eth_call",
    params: [
      {
        to: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
        data: "0x70a08231000000000000000000000000d8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
        // balanceOf(vitalik.eth)
      },
      "latest",
    ],
  }),
});
 
const result = await response.json();
console.log(result);
// { jsonrpc: "2.0", id: 1, result: "0x000000000000000000000000000000000000000000000000000000174876e800" }

यह raw eth_call है। यह transaction execution simulate करता है बिना actually submit किए। कोई gas cost नहीं। कोई state change नहीं। बस return value पढ़ता है। view और pure functions बाहर से ऐसे ही काम करते हैं — ये eth_sendRawTransaction की जगह eth_call use करते हैं।

दो Critical RPC Methods#

eth_call: Execution simulate करता है। Free। कोई state change नहीं। सभी read operations के लिए use होता है — balances check करना, prices पढ़ना, view functions call करना। "latest" की जगह block number specify करके किसी भी historical block पर call किया जा सकता है।

eth_sendRawTransaction: Block में inclusion के लिए signed transaction submit करता है। Gas cost होता है। State change होता है (अगर successful हो)। सभी write operations के लिए — transfers, approvals, swaps, mints।

JSON-RPC API में बाकी सब कुछ या तो इन दोनों का variant है या utility method है (eth_blockNumber, eth_getTransactionReceipt, eth_getLogs, आदि)।

Providers: Chain तक आपका Gateway#

आप अपना node नहीं चलाते। Application development के लिए लगभग कोई नहीं चलाता। इसकी जगह, आप provider service use करते हैं:

  • Alchemy: सबसे popular। बढ़िया dashboard, webhook support, NFTs और token metadata के लिए enhanced APIs। Free tier: ~300M compute units/month।
  • Infura: Original। ConsenSys के owner हैं। Reliable। Free tier: 100K requests/day।
  • QuickNode: Multi-chain के लिए अच्छा। थोड़ा अलग pricing model।
  • Public RPC endpoints: https://rpc.ankr.com/eth, https://cloudflare-eth.com। Free लेकिन rate-limited और कभी-कभी unreliable। Development के लिए ठीक, production के लिए खतरनाक।
  • Tenderly: Simulation और debugging के लिए excellent। इनके RPC में built-in transaction simulator है।

Production के लिए, हमेशा fallbacks के तौर पर कम से कम दो providers configure करें। RPC downtime real है और सबसे खराब time पर होगा।

typescript
import { ethers } from "ethers";
 
// ethers.js v6 fallback provider
const provider = new ethers.FallbackProvider([
  {
    provider: new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/KEY1"),
    priority: 1,
    stallTimeout: 2000,
    weight: 2,
  },
  {
    provider: new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/KEY2"),
    priority: 2,
    stallTimeout: 2000,
    weight: 1,
  },
]);

Free में Contract State पढ़ना#

यह वो power move है जो ज़्यादातर Web2 developers को पता नहीं: आप Ethereum पर किसी भी contract का कोई भी public data बिना कुछ pay किए, बिना wallet के, और बिना किसी authentication के (सिर्फ RPC provider के लिए API key) पढ़ सकते हैं।

typescript
import { ethers } from "ethers";
 
const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
 
// ERC-20 interface — just the read functions
const erc20 = new ethers.Contract(
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
  [
    "function name() view returns (string)",
    "function symbol() view returns (string)",
    "function decimals() view returns (uint8)",
    "function totalSupply() view returns (uint256)",
    "function balanceOf(address) view returns (uint256)",
  ],
  provider // Note: provider, not signer. Read-only.
);
 
const [name, symbol, decimals, totalSupply] = await Promise.all([
  erc20.name(),
  erc20.symbol(),
  erc20.decimals(),
  erc20.totalSupply(),
]);
 
console.log(`${name} (${symbol})`);           // "USD Coin (USDC)"
console.log(`Decimals: ${decimals}`);          // 6 (NOT 18!)
console.log(`Total supply: ${ethers.formatUnits(totalSupply, decimals)}`);
 
// Check a specific address's balance
const balance = await erc20.balanceOf("0xSomeAddress...");
console.log(`Balance: ${ethers.formatUnits(balance, decimals)} USDC`);

कोई wallet नहीं। कोई gas नहीं। कोई transaction नहीं। Hood के नीचे बस JSON-RPC eth_call। यह concept में REST API को GET request करने जैसा ही है। Blockchain database है, contract API है, और eth_call आपकी SELECT query है।

ethers.js v6#

ethers.js Web3 की jQuery है — यह पहली library थी जो ज़्यादातर developers ने सीखी, और अभी भी सबसे widely used है। Version 6 v5 से significant improvement है, native BigInt support (finally), ESM modules, और cleaner API के साथ।

तीन Core Abstractions#

Provider: Blockchain से read-only connection। View functions call कर सकता है, blocks fetch कर सकता है, logs पढ़ सकता है। Sign या transactions send नहीं कर सकता।

typescript
import { ethers } from "ethers";
 
// Connect to a node
const provider = new ethers.JsonRpcProvider("https://...");
 
// Basic queries
const blockNumber = await provider.getBlockNumber();
const balance = await provider.getBalance("0xAddress...");
const block = await provider.getBlock(blockNumber);
const txCount = await provider.getTransactionCount("0xAddress...");

Signer: Private key पर एक abstraction। Transactions और messages sign कर सकता है। Signer हमेशा Provider से connected होता है।

typescript
// From a private key (server-side, scripts)
const wallet = new ethers.Wallet("0xPrivateKey...", provider);
 
// From a browser wallet (client-side)
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const signer = await browserProvider.getSigner();
 
// Get the address
const address = await signer.getAddress();

Contract: Deployed contract के लिए JavaScript proxy। Contract object पर methods ABI में functions से correspond करते हैं। View functions values return करती हैं। State-changing functions TransactionResponse return करती हैं।

typescript
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider);
 
// Read (free, returns value directly)
const balance = await usdc.balanceOf("0xSomeAddress...");
// balance is a bigint: 1000000000n (1000 USDC with 6 decimals)
 
// To write, connect with a signer
const usdcWithSigner = usdc.connect(signer);
 
// Write (costs gas, returns TransactionResponse)
const tx = await usdcWithSigner.transfer("0xRecipient...", 1000000n);
const receipt = await tx.wait(); // Wait for block inclusion
 
if (receipt.status === 0) {
  throw new Error("Transaction reverted");
}

Type Safety के लिए TypeChain#

Raw ABI interactions stringly-typed हैं। आप function name गलत spell कर सकते हैं, wrong argument types pass कर सकते हैं, या return values गलत interpret कर सकते हैं। TypeChain आपकी ABI files से TypeScript types generate करता है:

typescript
// Without TypeChain — no type checking
const balance = await contract.balanceOf("0x...");
// balance is 'any'. No autocomplete. Easy to misuse.
 
// With TypeChain — full type safety
import { USDC__factory } from "./typechain";
 
const usdc = USDC__factory.connect(USDC_ADDRESS, provider);
const balance = await usdc.balanceOf("0x...");
// balance is BigNumber. Autocomplete works. Type errors caught at compile time.

New projects के लिए, ABIs से viem की built-in type inference use करने पर विचार करें। यह same result achieve करता है बिना separate code generation step के।

Events सुनना#

Real-time event streaming responsive dApps के लिए critical है। ethers.js इसके लिए WebSocket providers use करता है:

typescript
// WebSocket for real-time events
const wsProvider = new ethers.WebSocketProvider("wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
 
const contract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, wsProvider);
 
// Listen for all Transfer events
contract.on("Transfer", (from, to, value, event) => {
  console.log(`Transfer: ${from} -> ${to}`);
  console.log(`Amount: ${ethers.formatUnits(value, 6)} USDC`);
});
 
// Listen for transfers TO a specific address
const filter = contract.filters.Transfer(null, "0xMyAddress...");
contract.on(filter, (from, to, value) => {
  console.log(`Incoming transfer: ${ethers.formatUnits(value, 6)} USDC from ${from}`);
});
 
// Clean up when done
contract.removeAllListeners();

WAGMI + Viem: Modern Stack#

WAGMI (We're All Gonna Make It) Ethereum के लिए React hooks library है। Viem underlying TypeScript client है जिसे यह use करता है। दोनों मिलकर ethers.js + web3-react को frontend dApp development के standard stack के रूप में largely replace कर चुके हैं।

यह shift क्यों हुआ? तीन कारण: ABIs से full TypeScript inference (कोई codegen ज़रूरी नहीं), छोटा bundle size, और React hooks जो wallet interactions की messy async state management handle करते हैं।

Setup करना#

typescript
// wagmi.config.ts
import { createConfig, http } from "wagmi";
import { mainnet, sepolia } from "wagmi/chains";
import { injected, walletConnect } from "wagmi/connectors";
 
export const config = createConfig({
  chains: [mainnet, sepolia],
  connectors: [
    injected(),
    walletConnect({ projectId: "YOUR_WALLETCONNECT_PROJECT_ID" }),
  ],
  transports: {
    [mainnet.id]: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
    [sepolia.id]: http("https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY"),
  },
});
typescript
// app/providers.tsx
"use client";
 
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { config } from "./wagmi.config";
 
const queryClient = new QueryClient();
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </WagmiProvider>
  );
}

Contract Data पढ़ना#

useReadContract वो hook है जो आप सबसे ज़्यादा use करेंगे। यह eth_call को React Query caching, refetching, और loading/error states के साथ wrap करता है:

typescript
"use client";
 
import { useReadContract } from "wagmi";
import { formatUnits } from "viem";
 
const ERC20_ABI = [
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "owner", type: "address" }],
    outputs: [{ name: "balance", type: "uint256" }],
  },
] as const;
 
function TokenBalance({ address }: { address: `0x${string}` }) {
  const { data: balance, isLoading, error } = useReadContract({
    address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
    abi: ERC20_ABI,
    functionName: "balanceOf",
    args: [address],
  });
 
  if (isLoading) return <span>Loading...</span>;
  if (error) return <span>Error: {error.message}</span>;
 
  // balance is typed as bigint because the ABI says uint256
  return <span>{formatUnits(balance ?? 0n, 6)} USDC</span>;
}

ABI पर as const notice करें। यह critical है। इसके बिना, TypeScript literal types lose कर देता है और balance bigint की जगह unknown बन जाता है। पूरा type inference system const assertions पर depend करता है।

Contracts में लिखना#

useWriteContract पूरी lifecycle handle करता है: wallet prompt, signing, broadcasting, और confirmation tracking।

typescript
"use client";
 
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { parseUnits } from "viem";
 
function SendTokens() {
  const { writeContract, data: hash, isPending, error } = useWriteContract();
 
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash,
  });
 
  function handleSend() {
    writeContract({
      address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      abi: ERC20_ABI,
      functionName: "transfer",
      args: [
        "0xRecipientAddress...",
        parseUnits("100", 6), // 100 USDC
      ],
    });
  }
 
  return (
    <div>
      <button onClick={handleSend} disabled={isPending}>
        {isPending ? "Confirm in wallet..." : "Send 100 USDC"}
      </button>
 
      {hash && <p>Transaction: {hash}</p>}
      {isConfirming && <p>Waiting for confirmation...</p>}
      {isSuccess && <p>Transfer confirmed!</p>}
      {error && <p>Error: {error.message}</p>}
    </div>
  );
}

Events देखना#

useWatchContractEvent real-time event monitoring के लिए WebSocket subscription setup करता है:

typescript
"use client";
 
import { useWatchContractEvent } from "wagmi";
import { useState } from "react";
import { formatUnits } from "viem";
 
interface TransferEvent {
  from: string;
  to: string;
  value: string;
}
 
function LiveTransfers() {
  const [transfers, setTransfers] = useState<TransferEvent[]>([]);
 
  useWatchContractEvent({
    address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    abi: ERC20_ABI,
    eventName: "Transfer",
    onLogs(logs) {
      const newTransfers = logs.map((log) => ({
        from: log.args.from as string,
        to: log.args.to as string,
        value: formatUnits(log.args.value as bigint, 6),
      }));
      setTransfers((prev) => [...newTransfers, ...prev].slice(0, 50));
    },
  });
 
  return (
    <ul>
      {transfers.map((t, i) => (
        <li key={i}>
          {t.from.slice(0, 8)}... → {t.to.slice(0, 8)}...: {t.value} USDC
        </li>
      ))}
    </ul>
  );
}

Wallet Connect करने के Patterns#

User का wallet connect करना Web3 का "login" है। बस यह login नहीं है। कोई session नहीं, cookie नहीं, server-side state नहीं। Wallet connection आपके app को user का address पढ़ने और transaction signatures request करने की permission देता है। बस इतना।

EIP-1193 Provider Interface#

हर wallet EIP-1193 द्वारा defined standard interface expose करता है। यह request method वाला object है:

typescript
interface EIP1193Provider {
  request(args: { method: string; params?: unknown[] }): Promise<unknown>;
  on(event: string, handler: (...args: unknown[]) => void): void;
  removeListener(event: string, handler: (...args: unknown[]) => void): void;
}

MetaMask इसे window.ethereum के रूप में inject करता है। दूसरे wallets या तो अपनी property inject करते हैं या window.ethereum भी use करते हैं (जो conflicts पैदा करता है — "wallet wars" problem, EIP-6963 से partially solved)।

typescript
// Low-level wallet interaction (you shouldn't do this directly, but it's useful to understand)
 
// Request account access
const accounts = await window.ethereum.request({
  method: "eth_requestAccounts",
});
console.log("Connected address:", accounts[0]);
 
// Get the current chain
const chainId = await window.ethereum.request({
  method: "eth_chainId",
});
console.log("Chain ID:", parseInt(chainId, 16)); // "0x1" -> 1 (mainnet)
 
// Listen for account changes (user switches accounts in MetaMask)
window.ethereum.on("accountsChanged", (accounts: string[]) => {
  if (accounts.length === 0) {
    console.log("Wallet disconnected");
  } else {
    console.log("Switched to:", accounts[0]);
  }
});
 
// Listen for chain changes (user switches networks)
window.ethereum.on("chainChanged", (chainId: string) => {
  // The recommended approach is to reload the page
  window.location.reload();
});

EIP-6963: Multi-Wallet Discovery#

पुराना window.ethereum approach टूट जाता है जब users के पास multiple wallets installed हैं। कौन सा window.ethereum पाता है? आखिरी inject करने वाला? पहला? यह race condition है।

EIP-6963 इसे browser events पर based discovery protocol से fix करता है:

typescript
// Discovering all available wallets
interface EIP6963ProviderDetail {
  info: {
    uuid: string;
    name: string;
    icon: string;
    rdns: string;  // Reverse domain name, e.g., "io.metamask"
  };
  provider: EIP1193Provider;
}
 
const wallets: EIP6963ProviderDetail[] = [];
 
window.addEventListener("eip6963:announceProvider", (event: CustomEvent) => {
  wallets.push(event.detail);
});
 
// Request all wallets to announce themselves
window.dispatchEvent(new Event("eip6963:requestProvider"));
 
// Now 'wallets' contains all installed wallets with their names and icons
// You can show a wallet selection UI

WAGMI यह सब आपके लिए handle करता है। जब आप injected() connector use करते हैं, तो यह automatically EIP-6963 use करता है अगर available हो और window.ethereum पर fall back करता है।

WalletConnect#

WalletConnect एक protocol है जो mobile wallets को desktop dApps से relay server के ज़रिए connect करता है। User अपने mobile wallet से QR code scan करता है, encrypted connection establish होती है। Transaction requests आपके dApp से उनके phone तक relay होते हैं।

WAGMI के साथ, यह बस एक और connector है:

typescript
import { walletConnect } from "wagmi/connectors";
 
const connector = walletConnect({
  projectId: "YOUR_PROJECT_ID", // Get from cloud.walletconnect.com
  showQrModal: true,
});

Chain Switching Handle करना#

Users अक्सर गलत network पर होते हैं। आपका dApp Mainnet पर है, वो Sepolia से connected हैं। या वो Polygon पर हैं और आपको Mainnet चाहिए। WAGMI useSwitchChain provide करता है:

typescript
"use client";
 
import { useAccount, useSwitchChain } from "wagmi";
import { mainnet } from "wagmi/chains";
 
function NetworkGuard({ children }: { children: React.ReactNode }) {
  const { chain } = useAccount();
  const { switchChain, isPending } = useSwitchChain();
 
  if (!chain) return <p>Please connect your wallet</p>;
 
  if (chain.id !== mainnet.id) {
    return (
      <div>
        <p>Please switch to Ethereum Mainnet</p>
        <button
          onClick={() => switchChain({ chainId: mainnet.id })}
          disabled={isPending}
        >
          {isPending ? "Switching..." : "Switch Network"}
        </button>
      </div>
    );
  }
 
  return <>{children}</>;
}

IPFS और Metadata#

NFTs images on-chain store नहीं करते। Blockchain एक URI store करता है जो JSON metadata file को point करता है, जिसमें बदले में image का URL होता है। Standard pattern, ERC-721 के tokenURI function द्वारा defined:

Contract.tokenURI(42) → "ipfs://QmXyz.../42.json"

वो JSON file standard schema follow करती है:

json
{
  "name": "Cool NFT #42",
  "description": "A very cool NFT",
  "image": "ipfs://QmImageHash...",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Rarity", "value": "Legendary" }
  ]
}

IPFS CID vs URL#

IPFS addresses Content Identifiers (CIDs) use करते हैं — content के ही hashes। ipfs://QmXyz... का मतलब है "वो content जिसका hash QmXyz... है"। यह content-addressed storage है: URI content से derive होती है, इसलिए content URI बदले बिना कभी बदल नहीं सकता। NFTs जिस immutability guarantee पर निर्भर हैं वो यही है (जब वो actually IPFS use करते हैं — बहुत सारे centralized URLs use करते हैं, जो red flag है)।

Browser में IPFS content दिखाने के लिए, आपको gateway चाहिए जो IPFS URIs को HTTP में translate करे:

typescript
function ipfsToHttp(uri: string): string {
  if (uri.startsWith("ipfs://")) {
    const cid = uri.replace("ipfs://", "");
    return `https://ipfs.io/ipfs/${cid}`;
    // Or use a dedicated gateway:
    // return `https://YOUR_PROJECT.mypinata.cloud/ipfs/${cid}`;
  }
  return uri;
}
 
// Fetching NFT metadata
async function getNftMetadata(
  contractAddress: string,
  tokenId: bigint,
  provider: ethers.Provider
) {
  const contract = new ethers.Contract(
    contractAddress,
    ["function tokenURI(uint256 tokenId) view returns (string)"],
    provider
  );
 
  const tokenUri = await contract.tokenURI(tokenId);
  const httpUri = ipfsToHttp(tokenUri);
 
  const response = await fetch(httpUri);
  const metadata = await response.json();
 
  return {
    name: metadata.name,
    description: metadata.description,
    image: ipfsToHttp(metadata.image),
    attributes: metadata.attributes,
  };
}

Pinning Services#

IPFS peer-to-peer network है। Content तब तक available रहता है जब तक कोई उसे host ("pin") कर रहा हो। अगर आप IPFS पर NFT image upload करते हैं और फिर अपना node बंद कर देते हैं, तो content गायब हो जाता है।

Pinning services आपका content available रखती हैं:

  • Pinata: सबसे popular। Simple API। Generous free tier (1GB)। तेज़ loading के लिए dedicated gateways।
  • NFT.Storage: Free, Protocol Labs (IPFS के creators) द्वारा backed। NFT metadata के लिए specifically designed। Long-term persistence के लिए Filecoin use करता है।
  • Web3.Storage: NFT.Storage जैसा, ज़्यादा general-purpose।
typescript
// Uploading to Pinata
async function pinToIpfs(file: File): Promise<string> {
  const formData = new FormData();
  formData.append("file", file);
 
  const response = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${PINATA_JWT}`,
    },
    body: formData,
  });
 
  const result = await response.json();
  return `ipfs://${result.IpfsHash}`; // Returns CID
}

Indexing Problem#

यहां blockchain development का dirty secret है: आप RPC node से historical data efficiently query नहीं कर सकते।

पिछले साल के किसी token के सारे Transfer events चाहिए? आपको millions of blocks eth_getLogs के साथ scan करने होंगे, 2,000-10,000 blocks के chunks में paginate करते हुए (maximum provider पर निर्भर करता है)। यह हज़ारों RPC calls हैं। इसमें minutes से hours लगेंगे और आपका API quota burn होगा।

किसी specific address के सारे tokens चाहिए? इसके लिए कोई single RPC call नहीं है। आपको हर ERC-20 contract का हर Transfer event scan करना होगा, balances track करते हुए। यह feasible नहीं है।

Wallet में सारे NFTs चाहिए? Same problem। आपको हर NFT contract के हर ERC-721 Transfer event scan करना होगा।

Blockchain write-optimized data structure है। यह new transactions process करने में excellent है। Historical queries answer करने में terrible है। dApp UIs को जो चाहिए और chain natively जो provide करती है — यह fundamental mismatch है।

The Graph Protocol#

The Graph एक decentralized indexing protocol है। आप "subgraph" लिखते हैं — एक schema और event handlers का set — और The Graph chain index करके GraphQL API के ज़रिए data serve करता है।

graphql
# Subgraph schema (schema.graphql)
type Transfer @entity {
  id: Bytes!
  from: Bytes!
  to: Bytes!
  value: BigInt!
  blockNumber: BigInt!
  timestamp: BigInt!
}
 
type Account @entity {
  id: Bytes!
  balance: BigInt!
  transfersFrom: [Transfer!]! @derivedFrom(field: "from")
  transfersTo: [Transfer!]! @derivedFrom(field: "to")
}
typescript
// Querying a subgraph from your frontend
const SUBGRAPH_URL =
  "https://api.studio.thegraph.com/query/YOUR_ID/YOUR_SUBGRAPH/v0.0.1";
 
async function getRecentTransfers(address: string) {
  const query = `
    query GetTransfers($address: Bytes!) {
      transfers(
        where: { from: $address }
        orderBy: blockNumber
        orderDirection: desc
        first: 100
      ) {
        id
        from
        to
        value
        blockNumber
        timestamp
      }
    }
  `;
 
  const response = await fetch(SUBGRAPH_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables: { address } }),
  });
 
  const { data } = await response.json();
  return data.transfers;
}

Tradeoff: The Graph latency add करता है (typically chain head से 1-2 blocks पीछे) और एक और dependency। Decentralized network पर indexing costs भी हैं (GRT tokens में pay करते हैं)। छोटे projects के लिए, hosted service (Subgraph Studio) free है।

Alchemy और Moralis Enhanced APIs#

अगर आप subgraph maintain नहीं करना चाहते, तो Alchemy और Moralis दोनों pre-indexed APIs offer करते हैं जो common queries directly answer करती हैं:

typescript
// Alchemy: Get all ERC-20 token balances for an address
const response = await fetch(
  `https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "alchemy_getTokenBalances",
      params: ["0xAddress...", "erc20"],
    }),
  }
);
 
// Returns ALL ERC-20 token balances in one call
// vs. scanning every possible ERC-20 contract's balanceOf()
typescript
// Alchemy: Get all NFTs owned by an address
const response = await fetch(
  `https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY/getNFTs?owner=0xAddress...`
);
 
const { ownedNfts } = await response.json();
for (const nft of ownedNfts) {
  console.log(`${nft.title} - ${nft.contract.address}#${nft.tokenId}`);
}

ये APIs proprietary और centralized हैं। आप decentralization को developer experience के लिए trade कर रहे हैं। ज़्यादातर dApps के लिए, यह worthwhile tradeoff है। आपके users को इससे फर्क नहीं पड़ता कि उनका portfolio view subgraph से आता है या Alchemy के database से। उन्हें इससे फर्क पड़ता है कि यह 30 seconds की जगह 200ms में load हो।

Common Pitfalls#

कई production dApps ship करने और दूसरी teams का code debug करने के बाद, ये वो mistakes हैं जो मैं बार-बार देखता हूं। हर एक ने मुझे personally काटा है।

हर जगह BigInt#

Ethereum बहुत बड़े numbers में deal करता है। ETH balances wei में हैं (10^18)। Token supplies 10^27 या उससे ज़्यादा हो सकते हैं। JavaScript Number safely सिर्फ 2^53 - 1 (करीब 9 * 10^15) तक integers represent कर सकता है। Wei amounts के लिए यह काफी नहीं है।

typescript
// WRONG — silent precision loss
const balance = 1000000000000000000; // 1 ETH in wei
const double = balance * 2;
console.log(double); // 2000000000000000000 — looks right, but...
 
const largeBalance = 99999999999999999999; // ~100 ETH
console.log(largeBalance);          // 100000000000000000000 — WRONG! Rounded up.
console.log(largeBalance === 100000000000000000000); // true — data corruption
 
// RIGHT — use BigInt
const balance = 1000000000000000000n;
const double = balance * 2n;
console.log(double.toString()); // "2000000000000000000" — correct
 
const largeBalance = 99999999999999999999n;
console.log(largeBalance.toString()); // "99999999999999999999" — correct

dApp code में BigInt के नियम:

  1. Wei amounts को कभी Number में convert मत करेंBigInt हर जगह use करें, display के लिए ही human-readable strings में convert करें।
  2. BigInts पर Math.floor, Math.round आदि कभी use मत करें। ये काम नहीं करते। Integer division use करें: amount / 10n ** 6n
  3. JSON BigInt support नहीं करता। अगर आप ऐसे state serialize कर रहे हैं जिसमें BigInts हैं, तो custom serializer चाहिए: JSON.stringify(data, (_, v) => typeof v === "bigint" ? v.toString() : v)
  4. Library formatting functions use करेंethers.formatEther(), ethers.formatUnits(), viem की formatEther(), formatUnits()। ये conversion correctly handle करती हैं।
typescript
import { formatUnits, parseUnits } from "viem";
 
// Display: BigInt → human-readable string
const weiAmount = 1500000000000000000n; // 1.5 ETH
const display = formatUnits(weiAmount, 18); // "1.5"
 
// Input: human-readable string → BigInt
const userInput = "1.5";
const wei = parseUnits(userInput, 18); // 1500000000000000000n
 
// USDC has 6 decimals, not 18
const usdcAmount = 100000000n; // 100 USDC
const usdcDisplay = formatUnits(usdcAmount, 6); // "100.0"

Async Wallet Operations#

हर wallet interaction async है और ऐसे तरीकों से fail हो सकती है जिन्हें आपके app को gracefully handle करना चाहिए:

typescript
// The user can reject any wallet prompt
try {
  const tx = await writeContract({
    address: contractAddress,
    abi: ERC20_ABI,
    functionName: "approve",
    args: [spenderAddress, amount],
  });
} catch (error) {
  if (error.code === 4001) {
    // User rejected the transaction in their wallet
    // This is normal — not an error to report
    showToast("Transaction cancelled");
  } else if (error.code === -32603) {
    // Internal JSON-RPC error — often means the transaction would revert
    showToast("Transaction would fail. Check your balance.");
  } else {
    // Unexpected error
    console.error("Transaction error:", error);
    showToast("Something went wrong. Please try again.");
  }
}

मुख्य async pitfalls:

  • Wallet prompts user की तरफ से blocking हैं। आपके code में await 30 seconds ले सकता है जब user MetaMask में transaction details पढ़ रहा है। Loading spinner मत दिखाओ जो उन्हें लगे कि कुछ टूट गया।
  • User interaction के बीच accounts switch कर सकता है। आप Account A से approval request करते हैं, user Account B पर switch करता है, फिर approve करता है। अब Account B ने approve किया लेकिन आप Account A से transaction भेजने वाले हैं। Critical operations से पहले हमेशा connected account re-check करें।
  • Two-step write patterns common हैं। बहुत सारे DeFi operations approve + execute require करते हैं। User को दो transactions sign करने होते हैं। अगर वो approve करता है लेकिन execute नहीं, तो आपको allowance state check करना होगा और अगली बार approval step skip करना होगा।

Network Mismatch Errors#

यह किसी भी दूसरी issue से ज़्यादा debugging time waste करता है। आपका contract Mainnet पर है। आपका wallet Sepolia पर है। आपका RPC provider Polygon को point करता है। तीन अलग networks, तीन अलग states, तीन completely unrelated blockchains। और error message usually unhelpful होता है — "execution reverted" या "contract not found।"

typescript
// Defensive chain checking
import { useAccount, useChainId } from "wagmi";
 
function useRequireChain(requiredChainId: number) {
  const chainId = useChainId();
  const { isConnected } = useAccount();
 
  if (!isConnected) {
    return { ready: false, error: "Please connect your wallet" };
  }
 
  if (chainId !== requiredChainId) {
    return {
      ready: false,
      error: `Please switch to ${getChainName(requiredChainId)}. You're on ${getChainName(chainId)}.`,
    };
  }
 
  return { ready: true, error: null };
}

DeFi में Front-Running#

जब आप DEX पर swap submit करते हैं, तो आपका pending transaction mempool में visible होता है। एक bot आपका trade देख सकता है, price ऊपर push करके front-run कर सकता है, आपका trade worse price पर execute होने देता है, और फिर profit के लिए तुरंत बाद sell करता है। इसे "sandwich attack" कहते हैं।

Frontend developer के रूप में, आप इसे पूरी तरह रोक नहीं सकते, लेकिन mitigate कर सकते हैं:

typescript
// Setting slippage tolerance on a Uniswap-style swap
const amountOutMin = expectedOutput * 995n / 1000n; // 0.5% slippage tolerance
 
// Using a deadline to prevent long-lived pending transactions
const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20); // 20 minutes
 
await router.swapExactTokensForTokens(
  amountIn,
  amountOutMin,  // Minimum acceptable output — revert if we'd get less
  [tokenA, tokenB],
  userAddress,
  deadline,       // Revert if not executed within 20 minutes
);

High-value transactions के लिए, Flashbots Protect RPC use करने पर विचार करें, जो transactions public mempool की जगह directly block builders को भेजता है। यह sandwich attacks completely prevent करता है क्योंकि bots कभी आपका pending transaction देख ही नहीं पाते:

typescript
// Using Flashbots Protect as your RPC endpoint
const provider = new ethers.JsonRpcProvider("https://rpc.flashbots.net");

Decimal Confusion#

सभी tokens 18 decimals नहीं रखते। USDC और USDT 6 रखते हैं। WBTC 8 रखता है। कुछ tokens 0, 2, या arbitrary decimals रखते हैं। Amounts format करने से पहले हमेशा contract से decimals() पढ़ें:

typescript
async function formatTokenAmount(
  tokenAddress: string,
  rawAmount: bigint,
  provider: ethers.Provider
): Promise<string> {
  const contract = new ethers.Contract(
    tokenAddress,
    ["function decimals() view returns (uint8)", "function symbol() view returns (string)"],
    provider
  );
 
  const [decimals, symbol] = await Promise.all([
    contract.decimals(),
    contract.symbol(),
  ]);
 
  return `${ethers.formatUnits(rawAmount, decimals)} ${symbol}`;
}
 
// formatTokenAmount(USDC, 1000000n, provider) → "1.0 USDC"
// formatTokenAmount(WETH, 1000000000000000000n, provider) → "1.0 WETH"
// formatTokenAmount(WBTC, 100000000n, provider) → "1.0 WBTC"

Gas Estimation Failures#

जब estimateGas fail होता है, तो इसका आमतौर पर मतलब है कि transaction revert होगा। लेकिन error message अक्सर सिर्फ "cannot estimate gas" होता है बिना बताए क्यों। eth_call use करके transaction simulate करें और actual revert reason पाएं:

typescript
import { createPublicClient, http, decodeFunctionResult } from "viem";
 
async function simulateAndGetError(client: ReturnType<typeof createPublicClient>, tx: object) {
  try {
    await client.call({
      account: tx.from,
      to: tx.to,
      data: tx.data,
      value: tx.value,
    });
    return null; // No error — transaction would succeed
  } catch (error) {
    // Decode the revert reason
    if (error.data) {
      // Common revert strings
      if (error.data.startsWith("0x08c379a0")) {
        // Error(string) — standard revert with message
        const reason = decodeAbiParameters(
          [{ type: "string" }],
          `0x${error.data.slice(10)}`
        );
        return `Revert: ${reason[0]}`;
      }
    }
    return error.message;
  }
}

सब कुछ एक साथ#

यहां एक complete, minimal React component है जो wallet connect करता है, token balance पढ़ता है, और transfer भेजता है। यह हर dApp का skeleton है:

typescript
"use client";
 
import { useAccount, useConnect, useDisconnect, useReadContract, useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { injected } from "wagmi/connectors";
import { formatUnits, parseUnits } from "viem";
import { useState } from "react";
 
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
 
const USDC_ABI = [
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "transfer",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "", type: "bool" }],
  },
] as const;
 
export function TokenDashboard() {
  const { address, isConnected } = useAccount();
  const { connect } = useConnect();
  const { disconnect } = useDisconnect();
 
  const [recipient, setRecipient] = useState("");
  const [amount, setAmount] = useState("");
 
  // Read balance — only runs when address is defined
  const { data: balance, refetch: refetchBalance } = useReadContract({
    address: USDC_ADDRESS,
    abi: USDC_ABI,
    functionName: "balanceOf",
    args: address ? [address] : undefined,
    query: { enabled: !!address },
  });
 
  // Write: transfer tokens
  const {
    writeContract,
    data: txHash,
    isPending: isSigning,
    error: writeError,
  } = useWriteContract();
 
  // Wait for confirmation
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash: txHash,
  });
 
  // Refetch balance after confirmation
  if (isSuccess) {
    refetchBalance();
  }
 
  if (!isConnected) {
    return (
      <button onClick={() => connect({ connector: injected() })}>
        Connect Wallet
      </button>
    );
  }
 
  return (
    <div>
      <p>Connected: {address}</p>
      <p>
        USDC Balance:{" "}
        {balance !== undefined ? formatUnits(balance, 6) : "Loading..."}
      </p>
 
      <div>
        <input
          placeholder="Recipient address (0x...)"
          value={recipient}
          onChange={(e) => setRecipient(e.target.value)}
        />
        <input
          placeholder="Amount (e.g., 100)"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
        />
        <button
          onClick={() => {
            writeContract({
              address: USDC_ADDRESS,
              abi: USDC_ABI,
              functionName: "transfer",
              args: [recipient as `0x${string}`, parseUnits(amount, 6)],
            });
          }}
          disabled={isSigning || isConfirming}
        >
          {isSigning
            ? "Confirm in wallet..."
            : isConfirming
              ? "Confirming..."
              : "Send USDC"}
        </button>
      </div>
 
      {writeError && <p style={{ color: "red" }}>{writeError.message}</p>}
      {isSuccess && <p style={{ color: "green" }}>Transfer confirmed!</p>}
      {txHash && (
        <a
          href={`https://etherscan.io/tx/${txHash}`}
          target="_blank"
          rel="noopener noreferrer"
        >
          View on Etherscan
        </a>
      )}
 
      <button onClick={() => disconnect()}>Disconnect</button>
    </div>
  );
}

आगे कहां जाएं#

इस post ने web developers के लिए Ethereum में आने के essential concepts और tooling cover किए। हर area में और गहराई है:

  • Solidity: अगर आप contracts लिखना चाहते हैं, सिर्फ interact नहीं करना। Official docs और Patrick Collins के courses सबसे अच्छे starting points हैं।
  • ERC standards: ERC-20 (fungible tokens), ERC-721 (NFTs), ERC-1155 (multi-token), ERC-4626 (tokenized vaults)। हर एक standard interface define करता है जो उस category के सारे contracts implement करते हैं।
  • Layer 2s: Arbitrum, Optimism, Base, zkSync। Same developer experience, कम gas costs, थोड़े अलग trust assumptions। आपका ethers.js और viem code identically काम करता है — बस chain ID और RPC URL बदलें।
  • Account Abstraction (ERC-4337): Wallet UX का अगला evolution। Smart contract wallets जो gas sponsorship, social recovery, और batched transactions support करते हैं। "Connect wallet" pattern इसी दिशा में जा रहा है।
  • MEV और transaction ordering: अगर आप DeFi build कर रहे हैं, तो Maximal Extractable Value समझना optional नहीं है। Flashbots docs canonical resource हैं।

Blockchain ecosystem तेज़ी से आगे बढ़ता है, लेकिन इस post में fundamentals — accounts, transactions, ABI encoding, RPC calls, event indexing — 2015 से नहीं बदले हैं और जल्दी बदलेंगे भी नहीं। इन्हें अच्छे से सीखिए और बाकी सब बस API surface है।

संबंधित पोस्ट