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

Production में On-Chain Data: जो कोई नहीं बताता

Blockchain data न तो साफ है, न भरोसेमंद, न आसान। RPC rate limits, chain reorgs, BigInt bugs, और indexing tradeoffs — असली DeFi प्रोडक्ट्स ship करने से सीखे कठिन सबक।

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

एक कल्पना है कि on-chain data स्वाभाविक रूप से भरोसेमंद है। Immutable ledger। Transparent state। बस पढ़ो और हो गया।

मैं भी यही मानता था। फिर मैंने एक DeFi dashboard production में ship किया और तीन हफ्ते यह समझने में बिताए कि हमारे token balances गलत क्यों थे, हमारी event history में gaps क्यों थे, और हमारे database में ऐसे blocks के transactions क्यों थे जो अब exist ही नहीं करते।

On-chain data raw, hostile, और ऐसे edge cases से भरा है जो आपकी application को ऐसे तरीकों से तोड़ेंगे जो आपको तब तक पता नहीं चलेंगे जब तक कोई user bug report file नहीं करता। यह पोस्ट वह सब cover करती है जो मैंने कठिन तरीके से सीखा।

भरोसेमंद Data का भ्रम#

यह रही पहली बात जो कोई नहीं बताता: blockchain आपको data नहीं देता। यह आपको state transitions देता है। कोई SELECT * FROM transfers WHERE user = '0x...' नहीं है। Logs हैं, receipts हैं, storage slots हैं, और call traces हैं — सब ऐसे formats में encoded जिन्हें decode करने के लिए context चाहिए।

एक Transfer event log आपको from, to, और value देता है। यह आपको token symbol नहीं बताता। यह आपको decimals नहीं बताता। यह आपको नहीं बताता कि यह एक legitimate transfer है या एक fee-on-transfer token जो ऊपर से 3% काट रहा है। यह आपको नहीं बताता कि यह block 30 seconds बाद भी exist करेगा या नहीं।

"Immutable" वाला हिस्सा सच है — एक बार finalize होने के बाद। लेकिन finalization instant नहीं है। और जो data आपको एक RPC node से वापस मिलता है वह ज़रूरी नहीं कि finalized block से हो। ज़्यादातर developers latest query करते हैं और इसे truth मानते हैं। यह एक bug है, feature नहीं।

फिर encoding है। सब कुछ hex है। Addresses mixed-case checksummed हैं (या नहीं)। Token amounts integers हैं जो 10^decimals से गुणा किए गए हैं। $100 का USDC transfer on-chain 100000000 दिखता है क्योंकि USDC में 6 decimals हैं, 18 नहीं। मैंने production code देखा है जो हर ERC-20 token के लिए 18 decimals assume करता था। resulting balances 10^12 के factor से गलत थे।

RPC Rate Limits आपका Weekend बर्बाद करेंगे#

हर production Web3 app एक RPC endpoint से बात करता है। और हर RPC endpoint की rate limits आपकी उम्मीद से कहीं ज़्यादा aggressive हैं।

यह रहे वो numbers जो मायने रखते हैं:

  • Alchemy Free: ~30M compute units/month, 40 requests/minute। यह तब तक generous लगता है जब तक आपको पता नहीं चलता कि एक single eth_getLogs call wide block range पर सैकड़ों CUs खा सकती है। आप एक दिन की indexing में अपना monthly quota जला देंगे।
  • Infura Free: 100K requests/day, लगभग 1.15 req/sec। इस rate पर 500K blocks of event logs paginate करके देखें।
  • QuickNode Free: Infura जैसा ही — 100K requests/day।

Paid tiers मदद करते हैं, लेकिन समस्या खत्म नहीं करते। Alchemy के Growth plan पर $200/month पर भी, एक heavy indexing job throughput limits hit करेगी। और जब आप उन्हें hit करते हैं, तो graceful degradation नहीं मिलता। आपको 429 errors मिलते हैं, कभी unhelpful messages के साथ, कभी बिना retry-after header के।

Solution fallback providers, retry logic, और बहुत deliberate होने का combination है कि कौन सी calls करें। viem के साथ एक robust RPC setup ऐसा दिखता है:

typescript
import { createPublicClient, fallback, http } from "viem";
import { mainnet } from "viem/chains";
 
const client = createPublicClient({
  chain: mainnet,
  transport: fallback(
    [
      http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY", {
        retryCount: 3,
        retryDelay: 1500,
        timeout: 15_000,
      }),
      http("https://mainnet.infura.io/v3/YOUR_KEY", {
        retryCount: 3,
        retryDelay: 1500,
        timeout: 15_000,
      }),
      http("https://rpc.ankr.com/eth", {
        retryCount: 2,
        retryDelay: 2000,
        timeout: 20_000,
      }),
    ],
    { rank: true }
  ),
});

rank: true option critical है। यह viem को बताता है कि हर transport की latency और success rate measure करे और automatically सबसे तेज़, सबसे reliable को prefer करे। अगर Alchemy rate-limit करना शुरू करता है, viem traffic Infura पर shift करता है। अगर Infura down होता है, Ankr पर fall back करता है।

लेकिन एक सूक्ष्मता है: viem का default retry logic exponential backoff उपयोग करता है, जो आमतौर पर आप चाहते हैं। हालांकि, 2025 की शुरुआत तक, एक known issue है जहां retryCount batch mode enabled होने पर RPC-level errors (जैसे 429s) को properly retry नहीं करता। अगर आप requests batch कर रहे हैं, तो अपने retry behavior को explicitly test करें। यह trust मत करें कि यह काम करता है।

Reorgs: वह Bug जो आप आते नहीं देखेंगे#

Chain reorganization तब होता है जब network अस्थायी रूप से इस पर असहमत होता है कि कौन सा block canonical है। Node A block 1000 transactions [A, B, C] के साथ देखता है। Node B एक अलग block 1000 transactions [A, D] के साथ देखता है। आखिरकार network converge होता है, और एक version जीतता है।

Proof-of-work chains पर, यह आम था — 1-3 block reorgs दिन में कई बार होते थे। Post-merge Ethereum बेहतर है। एक successful reorg attack के लिए अब लगभग 50% validators के coordination की ज़रूरत है। लेकिन "बेहतर" "असंभव" नहीं है। May 2022 में Beacon Chain पर एक उल्लेखनीय 7-block reorg हुआ था, जो proposer boost fork के inconsistent client implementations की वजह से हुआ।

और इससे फ़र्क़ नहीं पड़ता कि Ethereum mainnet पर reorgs कितने rare हैं। अगर आप L2s या sidechains पर build कर रहे हैं — Polygon, Arbitrum, Optimism — reorgs ज़्यादा frequent हैं। Polygon पर historically 10+ blocks के reorgs होते रहे हैं।

यह रही practical समस्या: आपने block 18,000,000 index किया। आपने events अपने database में लिखे। फिर block 18,000,000 reorg हो गया। अब आपके database में ऐसे block के events हैं जो canonical chain पर exist नहीं करता। वे events ऐसे transactions reference कर सकते हैं जो कभी हुए ही नहीं। आपके users phantom transfers देखते हैं।

Fix आपके architecture पर निर्भर करता है:

Option 1: Confirmation delay। Data तब तक index मत करें जब तक N blocks of confirmations pass न हो जाएं। Ethereum mainnet के लिए, 64 blocks (दो epochs) finality guarantees देते हैं। L2s के लिए, specific chain का finality model check करें। यह simple है लेकिन latency जोड़ता है — Ethereum पर लगभग 13 minutes।

Option 2: Reorg detection और rollback। Aggressively index करें लेकिन block hashes track करें। हर नए block पर, verify करें कि parent hash पिछले indexed block से match करता है। अगर नहीं करता, तो आपने reorg detect कर लिया: orphaned blocks से सब कुछ delete करें और canonical chain को re-index करें।

typescript
interface IndexedBlock {
  number: bigint;
  hash: `0x${string}`;
  parentHash: `0x${string}`;
}
 
async function detectReorg(
  client: PublicClient,
  lastIndexed: IndexedBlock
): Promise<{ reorged: boolean; depth: number }> {
  const currentBlock = await client.getBlock({
    blockNumber: lastIndexed.number,
  });
 
  if (currentBlock.hash === lastIndexed.hash) {
    return { reorged: false, depth: 0 };
  }
 
  // पीछे चलकर पता करें कि chain कहां diverge हुई
  let depth = 1;
  let checkNumber = lastIndexed.number - 1n;
 
  while (checkNumber > 0n && depth < 128) {
    const onChain = await client.getBlock({ blockNumber: checkNumber });
    const inDb = await getIndexedBlock(checkNumber); // आपका DB lookup
 
    if (onChain.hash === inDb?.hash) {
      return { reorged: true, depth };
    }
 
    depth++;
    checkNumber--;
  }
 
  return { reorged: true, depth };
}

यह hypothetical नहीं है। मेरे पास एक production system था जहां हमने chain tip पर बिना reorg detection के events index किए। तीन हफ्ते तक ठीक काम किया। फिर Polygon पर एक 2-block reorg ने हमारे database में एक duplicate NFT mint event पैदा कर दिया। Frontend एक user को ऐसा token own करते दिखा रहा था जो उसका नहीं था। उसे debug करने में दो दिन लगे क्योंकि कोई reorgs को root cause के रूप में नहीं देख रहा था।

Indexing समस्या: अपना दर्द चुनें#

On-chain data को structured रूप में अपनी application में लाने के तीन असली options हैं।

Direct RPC Calls#

सीधे getLogs, getBlock, getTransaction call करें। यह small-scale reads के लिए काम करता है — user का balance check करना, एक single contract के recent events fetch करना। यह historical indexing या contracts के across complex queries के लिए काम नहीं करता।

समस्या combinatorial है। पिछले 30 दिनों में सभी Uniswap V3 swaps चाहिए? वह ~200K blocks हैं। Alchemy की 2K block range limit प्रति getLogs call पर, वह minimum 100 paginated requests हैं। हर एक आपकी rate limit के against count होती है। और अगर कोई call fail होती है, आपको retry logic, cursor tracking, और वहां से resume करने का तरीका चाहिए जहां आप रुके थे।

The Graph (Subgraphs)#

The Graph OG solution था। एक schema define करो, AssemblyScript में mappings लिखो, deploy करो, और GraphQL से query करो। Hosted Service deprecated हो गई — अब सब कुछ decentralized Graph Network पर है, जिसका मतलब है queries के लिए GRT tokens से pay करना।

अच्छा: standardized, well-documented, existing subgraphs का बड़ा ecosystem जिन्हें आप fork कर सकते हैं।

बुरा: AssemblyScript painful है। Debugging limited है। Deployment minutes से hours लेती है। अगर आपके subgraph में bug है, आप redeploy करते हैं और scratch से re-sync होने का इंतज़ार करते हैं। Decentralized network latency जोड़ता है और कभी-कभी indexers chain tip से पीछे रह जाते हैं।

मैंने The Graph read-heavy dashboards के लिए उपयोग किया है जहां 30-60 seconds की data freshness acceptable है। वहां यह अच्छा काम करता है। मैं इसे किसी ऐसी चीज़ के लिए उपयोग नहीं करूंगा जिसे real-time data या mappings में complex business logic चाहिए।

Custom Indexers (Ponder, Envio)#

यहां ecosystem काफी mature हो गया है। Ponder और Envio आपको TypeScript में indexing logic लिखने देते हैं (AssemblyScript नहीं), development के दौरान locally run करने देते हैं, और standalone services के रूप में deploy करने देते हैं।

Ponder आपको maximum control देता है। आप TypeScript में event handlers define करते हैं, यह indexing pipeline manage करता है, और आपको output के रूप में SQL database मिलता है। Tradeoff: infrastructure आपकी ज़िम्मेदारी है। Scaling, monitoring, reorg handling — सब आप पर है।

Envio sync speed के लिए optimize करता है। उनके benchmarks The Graph की तुलना में significantly तेज़ initial sync times दिखाते हैं। वे reorgs को natively handle करते हैं और HyperSync support करते हैं, तेज़ data fetching के लिए एक specialized protocol। Tradeoff: आप उनकी infrastructure और API में buy-in कर रहे हैं।

मेरी recommendation: अगर आप production DeFi app बना रहे हैं और आपके पास engineering capacity है, Ponder उपयोग करें। अगर आपको सबसे तेज़ possible sync चाहिए और infrastructure manage नहीं करना चाहते, Envio evaluate करें। अगर आपको quick prototype चाहिए या community-maintained subgraphs चाहिए, The Graph अभी भी ठीक है।

getLogs जितना दिखता है उससे ज़्यादा खतरनाक है#

eth_getLogs RPC method भ्रामक रूप से simple है। इसे एक block range और कुछ filters दें, matching event logs वापस पाएं। Production में वास्तव में यह होता है:

Block range limits provider के अनुसार vary करती हैं। Alchemy 2K blocks पर cap करता है (unlimited logs) या unlimited blocks (max 10K logs)। Infura की अलग limits हैं। QuickNode की अलग limits हैं। एक public RPC 1K blocks पर cap कर सकता है। आपके code को इन सबको handle करना होगा।

Response size limits exist करती हैं। Block range के भीतर भी, अगर एक popular contract हज़ारों events per block emit करता है, आपकी response provider की payload limit (Alchemy पर 150MB) exceed कर सकती है। Call partial results return नहीं करती। यह fail होती है।

Empty ranges free नहीं हैं। भले ही zero matching logs हों, provider फिर भी block range scan करता है। यह आपके compute units के against count होता है।

यह रही एक pagination utility जो इन constraints को handle करती है:

typescript
import type { PublicClient, Log, AbiEvent } from "viem";
 
async function fetchLogsInChunks<T extends AbiEvent>(
  client: PublicClient,
  params: {
    address: `0x${string}`;
    event: T;
    fromBlock: bigint;
    toBlock: bigint;
    maxBlockRange?: bigint;
  }
): Promise<Log<bigint, number, false, T, true>[]> {
  const { address, event, fromBlock, toBlock, maxBlockRange = 2000n } = params;
  const allLogs: Log<bigint, number, false, T, true>[] = [];
 
  let currentFrom = fromBlock;
 
  while (currentFrom <= toBlock) {
    const currentTo =
      currentFrom + maxBlockRange - 1n > toBlock
        ? toBlock
        : currentFrom + maxBlockRange - 1n;
 
    try {
      const logs = await client.getLogs({
        address,
        event,
        fromBlock: currentFrom,
        toBlock: currentTo,
      });
 
      allLogs.push(...logs);
      currentFrom = currentTo + 1n;
    } catch (error) {
      // अगर range बहुत बड़ी है (बहुत ज़्यादा results), इसे आधे में split करें
      if (isRangeTooLargeError(error) && currentTo > currentFrom) {
        const mid = currentFrom + (currentTo - currentFrom) / 2n;
        const firstHalf = await fetchLogsInChunks(client, {
          address,
          event,
          fromBlock: currentFrom,
          toBlock: mid,
          maxBlockRange,
        });
        const secondHalf = await fetchLogsInChunks(client, {
          address,
          event,
          fromBlock: mid + 1n,
          toBlock: currentTo,
          maxBlockRange,
        });
        allLogs.push(...firstHalf, ...secondHalf);
        currentFrom = currentTo + 1n;
      } else {
        throw error;
      }
    }
  }
 
  return allLogs;
}
 
function isRangeTooLargeError(error: unknown): boolean {
  const message = error instanceof Error ? error.message : String(error);
  return (
    message.includes("Log response size exceeded") ||
    message.includes("query returned more than") ||
    message.includes("exceed maximum block range")
  );
}

Key insight failure पर binary split है। अगर 2K block range बहुत ज़्यादा logs return करती है, इसे दो 1K ranges में split करें। अगर 1K अभी भी बहुत ज़्यादा है, फिर split करें। यह high-activity contracts के लिए automatically adapt हो जाता है बिना आपको पहले से event density जानने की ज़रूरत के।

BigInt आपको विनम्र बना देगा#

JavaScript का Number type एक 64-bit float है। यह 2^53 - 1 तक integers represent कर सकता है — लगभग 9 quadrillion। यह तब तक बहुत लगता है जब तक आपको पता नहीं चलता कि wei में 1 ETH की token amount 1000000000000000000 है — 18 zeros वाली संख्या। यह 10^18 है, Number.MAX_SAFE_INTEGER से बहुत आगे।

अगर आप गलती से अपनी pipeline में कहीं भी BigInt को Number में coerce कर देते हैं — JSON.parse, एक database driver, एक logging library — आपको silent precision loss मिलता है। Number लगभग सही दिखता है लेकिन आखिरी कुछ digits गलत हैं। आप testing में यह नहीं पकड़ेंगे क्योंकि आपकी test amounts छोटी हैं।

यह रहा वह bug जो मैंने production में ship किया:

typescript
// BUG: हानिरहित लगता है, नहीं है
function formatTokenAmount(amount: bigint, decimals: number): string {
  return (Number(amount) / Math.pow(10, decimals)).toFixed(4);
}
 
// छोटी amounts के लिए यह ठीक काम करता है:
formatTokenAmount(1000000n, 6); // "1.0000" -- सही
 
// बड़ी amounts के लिए यह चुपचाप टूट जाता है:
formatTokenAmount(123456789012345678n, 18);
// "0.1235" return करता है -- गलत, actual precision lost है
// Number(123456789012345678n) === 123456789012345680
// आखिरी दो digits IEEE 754 द्वारा round हो गए

Fix: divide करने से पहले कभी Number में convert मत करें। viem की built-in utilities उपयोग करें, जो strings और BigInts पर operate करती हैं:

typescript
import { formatUnits, parseUnits } from "viem";
 
// सही: BigInt पर operate करता है, string return करता है
function formatTokenAmount(
  amount: bigint,
  decimals: number,
  displayDecimals: number = 4
): string {
  const formatted = formatUnits(amount, decimals);
 
  // formatUnits पूरी precision string return करता है जैसे "0.123456789012345678"
  // desired display precision तक truncate (round नहीं) करें
  const [whole, fraction = ""] = formatted.split(".");
  const truncated = fraction.slice(0, displayDecimals).padEnd(displayDecimals, "0");
 
  return `${whole}.${truncated}`;
}
 
// यह भी critical है: user input के लिए parseUnits उपयोग करें, parseFloat कभी नहीं
function parseTokenInput(input: string, decimals: number): bigint {
  // parseUnits string-to-BigInt conversion सही तरीके से handle करता है
  return parseUnits(input, decimals);
}

ध्यान दें कि मैं rounding की बजाय truncate करता हूं। यह जानबूझकर है। Financial contexts में, "1.0001 ETH" दिखाना जब real value "1.00009999..." है, "1.0001" दिखाने से बेहतर है जब real value "1.00005001..." है और round up हुई थी। Users displayed amounts के आधार पर decisions लेते हैं। Truncation conservative choice है।

एक और trap: JSON.stringify BigInt serialize करना नहीं जानता। यह throw करता है। आपकी API से हर single response जिसमें token amounts हैं, को एक serialization strategy चाहिए। मैं API boundary पर string conversion उपयोग करता हूं:

typescript
// API response serializer
function serializeForApi(data: Record<string, unknown>): string {
  return JSON.stringify(data, (_, value) =>
    typeof value === "bigint" ? value.toString() : value
  );
}

Caching Strategy: क्या, कितने समय, और कब Invalidate करें#

सभी on-chain data की freshness requirements एक जैसी नहीं हैं। यह रही वह hierarchy जो मैं उपयोग करता हूं:

हमेशा cache करें (immutable):

  • Transaction receipts (एक बार mine होने के बाद, वे बदलती नहीं)
  • Finalized block data (block hash, timestamp, transaction list)
  • Contract bytecode
  • Finalized blocks से historical event logs

Minutes से hours तक cache करें:

  • Token metadata (name, symbol, decimals) — technically ज़्यादातर tokens के लिए immutable, लेकिन proxy upgrades implementation बदल सकते हैं
  • ENS resolutions — 5 minute TTL अच्छा काम करता है
  • Token prices — accuracy requirements पर निर्भर, 30 seconds से 5 minutes

Seconds तक cache करें या बिल्कुल नहीं:

  • Current block number
  • Account balances और nonce
  • Pending transaction status
  • Unfinalized event logs (फिर वही reorg समस्या)

Implementation को complex होने की ज़रूरत नहीं। In-memory LRU और Redis के साथ two-tier cache ज़्यादातर cases cover करता है:

typescript
import { LRUCache } from "lru-cache";
 
const memoryCache = new LRUCache<string, unknown>({
  max: 10_000,
  ttl: 1000 * 60, // 1 minute default
});
 
type CacheTier = "immutable" | "short" | "volatile";
 
const TTL_MAP: Record<CacheTier, number> = {
  immutable: 1000 * 60 * 60 * 24, // memory में 24 hours, Redis में permanent
  short: 1000 * 60 * 5,            // 5 minutes
  volatile: 1000 * 15,             // 15 seconds
};
 
async function cachedRpcCall<T>(
  key: string,
  tier: CacheTier,
  fetcher: () => Promise<T>
): Promise<T> {
  // पहले memory check करें
  const cached = memoryCache.get(key) as T | undefined;
  if (cached !== undefined) return cached;
 
  // फिर Redis (अगर आपके पास है)
  // const redisCached = await redis.get(key);
  // if (redisCached) { ... }
 
  const result = await fetcher();
  memoryCache.set(key, result, { ttl: TTL_MAP[tier] });
 
  return result;
}
 
// उपयोग:
const receipt = await cachedRpcCall(
  `receipt:${txHash}`,
  "immutable",
  () => client.getTransactionReceipt({ hash: txHash })
);

प्रतिकूल सबक: सबसे बड़ी performance win RPC responses cache करना नहीं है। यह RPC calls से पूरी तरह बचना है। हर बार जब आप getBlock call करने वाले हों, खुद से पूछें: क्या मुझे वास्तव में अभी chain से data चाहिए, या मैं इसे पहले से मौजूद data से derive कर सकता हूं? क्या मैं polling की बजाय WebSocket से events सुन सकता हूं? क्या मैं multiple reads को एक single multicall में batch कर सकता हूं?

TypeScript और Contract ABIs: सही तरीका#

Viem का type system, ABIType द्वारा powered, आपके contract ABI से आपके TypeScript code तक पूर्ण end-to-end type inference प्रदान करता है। लेकिन तभी जब आप इसे सही से set up करें।

गलत तरीका:

typescript
// कोई type inference नहीं — args unknown[] है, return unknown है
const result = await client.readContract({
  address: "0x...",
  abi: JSON.parse(abiString), // runtime पर parse = कोई type info नहीं
  functionName: "balanceOf",
  args: ["0x..."],
});

सही तरीका:

typescript
// पूर्ण type inference के लिए ABI को const के रूप में define करें
const erc20Abi = [
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "balance", type: "uint256" }],
  },
  {
    name: "transfer",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "success", type: "bool" }],
  },
] as const;
 
// अब TypeScript जानता है:
// - functionName "balanceOf" | "transfer" में autocomplete होता है
// - balanceOf के args [address: `0x${string}`] है
// - balanceOf का return type bigint है
const balance = await client.readContract({
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  abi: erc20Abi,
  functionName: "balanceOf",
  args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
});
// typeof balance = bigint -- पूरी तरह typed

as const assertion वह है जो इसे काम कराता है। इसके बिना, TypeScript ABI type को { name: string, type: string, ... }[] में widen कर देता है और सारी inference machinery collapse हो जाती है। यह Web3 TypeScript codebases में सबसे आम गलती है जो मैं देखता हूं।

बड़े projects के लिए, @wagmi/cli उपयोग करें अपने Foundry या Hardhat project से directly typed contract bindings generate करने के लिए। यह आपके compiled ABIs पढ़ता है और as const assertions पहले से applied TypeScript files produce करता है। कोई manual ABI copying नहीं, कोई type drift नहीं।

असुविधाजनक सच्चाई#

Blockchain data एक distributed system problem है जो database problem का भेस धारण किए हुए है। जिस पल आप इसे "बस एक और API" मानते हैं, आप ऐसे bugs accumulate करना शुरू करते हैं जो development में invisible और production में intermittent हैं।

Tooling dramatically बेहतर हो गई है। Viem type safety और developer experience के लिए ethers.js पर एक massive improvement है। Ponder और Envio ने custom indexing को accessible बना दिया है। लेकिन fundamental challenges — reorgs, rate limits, encoding, finality — protocol-level हैं। कोई library उन्हें abstract नहीं करती।

इस assumption के साथ build करें कि आपका RPC आपसे झूठ बोलेगा, आपके blocks reorganize होंगे, आपके numbers overflow होंगे, और आपका cache stale data serve करेगा। फिर हर case को explicitly handle करें।

Production-grade on-chain data ऐसा दिखता है।

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