Lompat ke konten
·11 menit membaca

Data On-Chain di Production: Apa yang Tidak Diberitahukan Siapa pun

Data blockchain itu tidak bersih, tidak andal, dan tidak mudah. Rate limit RPC, chain reorg, bug BigInt, dan tradeoff indexing — pelajaran berat dari merilis produk DeFi yang sesungguhnya.

Bagikan:X / TwitterLinkedIn

Ada fantasi bahwa data on-chain secara inheren bisa dipercaya. Ledger yang immutable. State yang transparan. Tinggal baca dan selesai.

Saya juga percaya itu. Lalu saya merilis dashboard DeFi ke production dan menghabiskan tiga minggu mencari tahu mengapa saldo token kami salah, riwayat event kami ada celahnya, dan database kami berisi transaksi dari blok yang sudah tidak ada lagi.

Data on-chain itu mentah, kejam, dan penuh dengan edge case yang akan merusak aplikasi Anda dengan cara yang tidak Anda sadari sampai pengguna mengajukan laporan bug. Postingan ini mencakup semua yang saya pelajari dengan cara yang sulit.

Ilusi Data yang Andal#

Ini hal pertama yang tidak diberitahukan siapa pun: blockchain tidak memberi Anda data. Ia memberi Anda transisi state. Tidak ada SELECT * FROM transfers WHERE user = '0x...'. Yang ada adalah log, receipt, storage slot, dan call trace — semuanya dikodekan dalam format yang membutuhkan konteks untuk didekode.

Log event Transfer memberi Anda from, to, dan value. Ia tidak memberi tahu Anda simbol token. Ia tidak memberi tahu Anda desimal. Ia tidak memberi tahu Anda apakah ini transfer yang sah atau token fee-on-transfer yang memotong 3% dari atas. Ia tidak memberi tahu Anda apakah blok ini masih akan ada dalam 30 detik.

Bagian "immutable" itu benar — setelah finalisasi. Tapi finalisasi tidak instan. Dan data yang Anda dapatkan dari node RPC belum tentu dari blok yang sudah difinalisasi. Kebanyakan developer melakukan query ke latest dan memperlakukannya sebagai kebenaran. Itu bug, bukan fitur.

Lalu ada encoding. Semuanya hex. Alamat adalah mixed-case checksummed (atau tidak). Jumlah token adalah integer dikalikan 10^decimals. Transfer USDC sebesar $100 terlihat seperti 100000000 on-chain karena USDC punya 6 desimal, bukan 18. Saya pernah melihat kode production yang mengasumsikan 18 desimal untuk setiap token ERC-20. Saldo yang dihasilkan meleset dengan faktor 10^12.

Rate Limit RPC Akan Menghancurkan Akhir Pekan Anda#

Setiap aplikasi Web3 production berbicara ke endpoint RPC. Dan setiap endpoint RPC punya rate limit yang jauh lebih agresif dari yang Anda kira.

Berikut angka-angka yang penting:

  • Alchemy Free: ~30M compute unit/bulan, 40 request/menit. Terdengar banyak sampai Anda sadar satu panggilan eth_getLogs untuk range blok yang lebar bisa memakan ratusan CU. Anda akan menghabiskan kuota bulanan dalam sehari indexing.
  • Infura Free: 100K request/hari, kira-kira 1,15 req/detik. Coba paginasi melalui 500K blok event log dengan kecepatan itu.
  • QuickNode Free: Mirip Infura — 100K request/hari.

Tier berbayar membantu, tapi tidak menghilangkan masalah. Bahkan dengan $200/bulan di paket Growth Alchemy, pekerjaan indexing berat akan mengenai batas throughput. Dan ketika Anda terkena limit, Anda tidak mendapat degradasi yang mulus. Anda mendapat error 429, kadang dengan pesan yang tidak membantu, kadang tanpa header retry-after.

Solusinya adalah kombinasi fallback provider, logika retry, dan sangat disengaja tentang panggilan mana yang Anda buat. Berikut seperti apa setup RPC yang robust dengan viem:

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 }
  ),
});

Opsi rank: true itu kritis. Ini memberi tahu viem untuk mengukur latensi dan tingkat keberhasilan setiap transport dan otomatis memilih yang tercepat dan paling andal. Jika Alchemy mulai membatasi Anda, viem mengalihkan traffic ke Infura. Jika Infura down, ia fallback ke Ankr.

Tapi ada kehalusan: logika retry default viem menggunakan exponential backoff, yang biasanya memang Anda inginkan. Namun, per awal 2025, ada masalah yang diketahui di mana retryCount tidak benar-benar melakukan retry pada error level RPC (seperti 429) ketika mode batch diaktifkan. Jika Anda melakukan batching request, uji perilaku retry Anda secara eksplisit. Jangan percaya begitu saja bahwa itu berfungsi.

Reorg: Bug yang Tidak Akan Anda Lihat Datang#

Reorganisasi chain terjadi ketika jaringan untuk sementara tidak sepakat tentang blok mana yang kanonik. Node A melihat blok 1000 dengan transaksi [A, B, C]. Node B melihat blok 1000 yang berbeda dengan transaksi [A, D]. Akhirnya jaringan konvergen, dan satu versi menang.

Di chain proof-of-work, ini umum — reorg 1-3 blok terjadi berkali-kali per hari. Ethereum pasca-merge lebih baik. Serangan reorg yang berhasil sekarang membutuhkan koordinasi hampir 50% validator. Tapi "lebih baik" bukan "tidak mungkin." Ada reorg 7-blok yang terkenal di Beacon Chain pada Mei 2022, disebabkan oleh implementasi client yang tidak konsisten terhadap proposer boost fork.

Dan tidak masalah seberapa jarang reorg di mainnet Ethereum. Jika Anda membangun di L2 atau sidechain — Polygon, Arbitrum, Optimism — reorg lebih sering. Polygon secara historis mengalami reorg 10+ blok.

Berikut masalah praktisnya: Anda mengindeks blok 18.000.000. Anda menulis event ke database. Lalu blok 18.000.000 kena reorg. Sekarang database Anda punya event dari blok yang tidak ada di chain kanonik. Event itu mungkin merujuk pada transaksi yang tidak pernah terjadi. Pengguna Anda melihat transfer hantu.

Solusinya tergantung arsitektur Anda:

Opsi 1: Delay konfirmasi. Jangan mengindeks data sampai N blok konfirmasi telah berlalu. Untuk mainnet Ethereum, 64 blok (dua epoch) memberi Anda jaminan finalitas. Untuk L2, periksa model finalitas chain spesifiknya. Ini sederhana tapi menambah latensi — kira-kira 13 menit di Ethereum.

Opsi 2: Deteksi reorg dan rollback. Indeks secara agresif tapi lacak hash blok. Di setiap blok baru, verifikasi bahwa parent hash cocok dengan blok sebelumnya yang Anda indeks. Jika tidak cocok, Anda telah mendeteksi reorg: hapus semua dari blok orphan dan indeks ulang chain kanonik.

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 };
  }
 
  // Telusuri mundur untuk menemukan di mana chain bercabang
  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); // pencarian DB Anda
 
    if (onChain.hash === inDb?.hash) {
      return { reorged: true, depth };
    }
 
    depth++;
    checkNumber--;
  }
 
  return { reorged: true, depth };
}

Ini bukan hipotetis. Saya pernah punya sistem production di mana kami mengindeks event di chain tip tanpa deteksi reorg. Selama tiga minggu berjalan baik. Lalu reorg 2-blok di Polygon menyebabkan event mint NFT duplikat di database kami. Frontend menampilkan pengguna memiliki token yang tidak mereka miliki. Butuh dua hari untuk debug yang satu itu karena tidak ada yang mencari reorg sebagai akar masalahnya.

Masalah Indexing: Pilih Rasa Sakit Anda#

Anda punya tiga opsi nyata untuk memasukkan data on-chain terstruktur ke dalam aplikasi Anda.

Panggilan RPC Langsung#

Cukup panggil getLogs, getBlock, getTransaction secara langsung. Ini berfungsi untuk pembacaan skala kecil — mengecek saldo pengguna, mengambil event terbaru untuk satu kontrak. Ini tidak berfungsi untuk indexing historis atau query kompleks lintas kontrak.

Masalahnya adalah kombinatorial. Ingin semua swap Uniswap V3 dalam 30 hari terakhir? Itu ~200K blok. Dengan batas range blok 2K per panggilan getLogs di Alchemy, itu minimal 100 request terpaginasi. Setiap satu dihitung terhadap rate limit Anda. Dan jika ada panggilan yang gagal, Anda butuh logika retry, pelacakan cursor, dan cara untuk melanjutkan dari tempat Anda berhenti.

The Graph (Subgraph)#

The Graph adalah solusi OG. Definisikan schema, tulis mapping dalam AssemblyScript, deploy, dan query dengan GraphQL. Hosted Service sudah deprecated — semuanya sekarang di Graph Network yang terdesentralisasi, yang berarti Anda membayar dengan token GRT untuk query.

Kelebihannya: terstandarisasi, terdokumentasi dengan baik, ekosistem besar subgraph yang sudah ada yang bisa Anda fork.

Kekurangannya: AssemblyScript itu menyakitkan. Debugging terbatas. Deployment memakan waktu menit hingga jam. Jika subgraph Anda punya bug, Anda deploy ulang dan menunggu ia sync ulang dari awal. Jaringan terdesentralisasi menambah latensi dan kadang indexer tertinggal dari chain tip.

Saya pernah menggunakan The Graph untuk dashboard yang banyak membaca di mana kesegaran data 30-60 detik bisa diterima. Itu berfungsi dengan baik di sana. Saya tidak akan menggunakannya untuk apa pun yang membutuhkan data real-time atau logika bisnis kompleks dalam mapping.

Custom Indexer (Ponder, Envio)#

Di sinilah ekosistem telah matang secara signifikan. Ponder dan Envio memungkinkan Anda menulis logika indexing dalam TypeScript (bukan AssemblyScript), berjalan secara lokal selama development, dan deploy sebagai layanan mandiri.

Ponder memberi Anda kontrol maksimum. Anda mendefinisikan event handler dalam TypeScript, ia mengelola pipeline indexing, dan Anda mendapat database SQL sebagai output. Trade-off-nya: Anda yang memiliki infrastrukturnya. Scaling, monitoring, penanganan reorg — itu tanggung jawab Anda.

Envio mengoptimalkan kecepatan sync. Benchmark mereka menunjukkan waktu initial sync yang jauh lebih cepat dibandingkan The Graph. Mereka menangani reorg secara native dan mendukung HyperSync, protokol khusus untuk pengambilan data yang lebih cepat. Trade-off-nya: Anda membeli infrastruktur dan API mereka.

Rekomendasi saya: jika Anda membangun aplikasi DeFi production dan punya kapasitas engineering, gunakan Ponder. Jika Anda butuh sync tercepat dan tidak ingin mengelola infrastruktur, evaluasi Envio. Jika Anda butuh prototipe cepat atau ingin subgraph yang dipelihara komunitas, The Graph masih baik.

getLogs Lebih Berbahaya dari yang Terlihat#

Metode RPC eth_getLogs tampak sederhana. Beri range blok dan beberapa filter, dapatkan event log yang cocok. Berikut yang sebenarnya terjadi di production:

Batas range blok berbeda per provider. Alchemy membatasi di 2K blok (log unlimited) atau blok unlimited (maks 10K log). Infura punya batas berbeda. QuickNode punya batas berbeda. RPC publik mungkin membatasi di 1K blok. Kode Anda harus menangani semuanya.

Batas ukuran respons ada. Bahkan dalam range blok, jika kontrak populer mengeluarkan ribuan event per blok, respons Anda bisa melampaui batas payload provider (150MB di Alchemy). Panggilan itu tidak mengembalikan hasil parsial. Ia gagal.

Range kosong tidak gratis. Bahkan jika tidak ada log yang cocok, provider tetap memindai range blok. Ini dihitung terhadap compute unit Anda.

Berikut utilitas paginasi yang menangani batasan-batasan ini:

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) {
      // Jika range terlalu besar (terlalu banyak hasil), bagi dua
      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")
  );
}

Insight kuncinya adalah pembagian biner saat gagal. Jika range 2K blok mengembalikan terlalu banyak log, bagi menjadi dua range 1K. Jika 1K masih terlalu banyak, bagi lagi. Ini beradaptasi secara otomatis dengan kontrak yang aktivitasnya tinggi tanpa mengharuskan Anda mengetahui kepadatan event sebelumnya.

BigInt Akan Merendahkan Anda#

Tipe Number JavaScript adalah float 64-bit. Ia bisa merepresentasikan integer hingga 2^53 - 1 — sekitar 9 kuadriliun. Terdengar banyak sampai Anda sadar bahwa jumlah token 1 ETH dalam wei adalah 1000000000000000000 — angka dengan 18 nol. Itu 10^18, jauh melampaui Number.MAX_SAFE_INTEGER.

Jika Anda secara tidak sengaja mengkonversi BigInt ke Number di mana pun dalam pipeline Anda — JSON.parse, driver database, library logging — Anda mendapat kehilangan presisi yang senyap. Angkanya terlihat kira-kira benar tapi beberapa digit terakhir salah. Anda tidak akan menangkap ini saat testing karena jumlah test Anda kecil.

Berikut bug yang saya kirim ke production:

typescript
// BUG-NYA: Terlihat tidak berbahaya, tapi berbahaya
function formatTokenAmount(amount: bigint, decimals: number): string {
  return (Number(amount) / Math.pow(10, decimals)).toFixed(4);
}
 
// Untuk jumlah kecil ini berfungsi:
formatTokenAmount(1000000n, 6); // "1.0000" -- benar
 
// Untuk jumlah besar ini rusak secara senyap:
formatTokenAmount(123456789012345678n, 18);
// Mengembalikan "0.1235" -- SALAH, presisi sebenarnya hilang
// Number(123456789012345678n) === 123456789012345680
// Dua digit terakhir dibulatkan oleh IEEE 754

Solusinya: jangan pernah konversi ke Number sebelum membagi. Gunakan utilitas bawaan viem, yang beroperasi pada string dan BigInt:

typescript
import { formatUnits, parseUnits } from "viem";
 
// Benar: beroperasi pada BigInt, mengembalikan string
function formatTokenAmount(
  amount: bigint,
  decimals: number,
  displayDecimals: number = 4
): string {
  const formatted = formatUnits(amount, decimals);
 
  // formatUnits mengembalikan string presisi penuh seperti "0.123456789012345678"
  // Potong (jangan bulatkan) ke presisi tampilan yang diinginkan
  const [whole, fraction = ""] = formatted.split(".");
  const truncated = fraction.slice(0, displayDecimals).padEnd(displayDecimals, "0");
 
  return `${whole}.${truncated}`;
}
 
// Juga penting: gunakan parseUnits untuk input pengguna, jangan pernah parseFloat
function parseTokenInput(input: string, decimals: number): bigint {
  // parseUnits menangani konversi string-ke-BigInt dengan benar
  return parseUnits(input, decimals);
}

Perhatikan saya memotong alih-alih membulatkan. Ini disengaja. Dalam konteks keuangan, menampilkan "1.0001 ETH" ketika nilai sebenarnya "1.00009999..." lebih baik daripada menampilkan "1.0001" ketika nilai sebenarnya "1.00005001..." dan dibulatkan ke atas. Pengguna membuat keputusan berdasarkan jumlah yang ditampilkan. Pemotongan adalah pilihan yang konservatif.

Jebakan lain: JSON.stringify tidak tahu cara melakukan serialisasi BigInt. Ia melempar error. Setiap respons dari API Anda yang menyertakan jumlah token membutuhkan strategi serialisasi. Saya menggunakan konversi string di batas API:

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

Strategi Caching: Apa, Berapa Lama, dan Kapan Diinvalidasi#

Tidak semua data on-chain punya kebutuhan kesegaran yang sama. Berikut hierarki yang saya gunakan:

Cache selamanya (immutable):

  • Receipt transaksi (setelah dimining, tidak berubah)
  • Data blok yang difinalisasi (hash blok, timestamp, daftar transaksi)
  • Bytecode kontrak
  • Log event historis dari blok yang difinalisasi

Cache selama menit hingga jam:

  • Metadata token (nama, simbol, desimal) — secara teknis immutable untuk kebanyakan token, tapi upgrade proxy bisa mengubah implementasi
  • Resolusi ENS — TTL 5 menit berfungsi baik
  • Harga token — tergantung kebutuhan akurasi Anda, 30 detik hingga 5 menit

Cache selama detik atau tidak sama sekali:

  • Nomor blok saat ini
  • Saldo dan nonce akun
  • Status transaksi pending
  • Log event yang belum difinalisasi (masalah reorg lagi)

Implementasinya tidak harus kompleks. Cache dua tier dengan LRU in-memory dan Redis mencakup sebagian besar kasus:

typescript
import { LRUCache } from "lru-cache";
 
const memoryCache = new LRUCache<string, unknown>({
  max: 10_000,
  ttl: 1000 * 60, // default 1 menit
});
 
type CacheTier = "immutable" | "short" | "volatile";
 
const TTL_MAP: Record<CacheTier, number> = {
  immutable: 1000 * 60 * 60 * 24, // 24 jam di memory, permanen di Redis
  short: 1000 * 60 * 5,            // 5 menit
  volatile: 1000 * 15,             // 15 detik
};
 
async function cachedRpcCall<T>(
  key: string,
  tier: CacheTier,
  fetcher: () => Promise<T>
): Promise<T> {
  // Cek memory dulu
  const cached = memoryCache.get(key) as T | undefined;
  if (cached !== undefined) return cached;
 
  // Lalu Redis (jika punya)
  // const redisCached = await redis.get(key);
  // if (redisCached) { ... }
 
  const result = await fetcher();
  memoryCache.set(key, result, { ttl: TTL_MAP[tier] });
 
  return result;
}
 
// Penggunaan:
const receipt = await cachedRpcCall(
  `receipt:${txHash}`,
  "immutable",
  () => client.getTransactionReceipt({ hash: txHash })
);

Pelajaran kontra-intuitif: kemenangan performa terbesar bukan caching respons RPC. Melainkan menghindari panggilan RPC sepenuhnya. Setiap kali Anda akan memanggil getBlock, tanyakan pada diri sendiri: apakah saya benar-benar butuh data dari chain sekarang, atau bisakah saya menurunkannya dari data yang sudah saya punya? Bisakah saya mendengarkan event via WebSocket alih-alih polling? Bisakah saya menggabungkan beberapa pembacaan ke dalam satu multicall?

TypeScript dan Contract ABI: Cara yang Benar#

Sistem tipe viem, didukung oleh ABIType, menyediakan inferensi tipe end-to-end penuh dari ABI kontrak Anda ke kode TypeScript Anda. Tapi hanya jika Anda mengaturnya dengan benar.

Cara yang salah:

typescript
// Tidak ada inferensi tipe — args adalah unknown[], return adalah unknown
const result = await client.readContract({
  address: "0x...",
  abi: JSON.parse(abiString), // di-parse saat runtime = tidak ada info tipe
  functionName: "balanceOf",
  args: ["0x..."],
});

Cara yang benar:

typescript
// Definisikan ABI sebagai const untuk inferensi tipe penuh
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;
 
// Sekarang TypeScript tahu:
// - functionName autocomplete ke "balanceOf" | "transfer"
// - args untuk balanceOf adalah [address: `0x${string}`]
// - tipe return untuk balanceOf adalah bigint
const balance = await client.readContract({
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  abi: erc20Abi,
  functionName: "balanceOf",
  args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
});
// typeof balance = bigint -- fully typed

Assertion as const adalah yang membuatnya berfungsi. Tanpa itu, TypeScript melebarkan tipe ABI menjadi { name: string, type: string, ... }[] dan semua mesin inferensi runtuh. Ini adalah kesalahan paling umum yang saya lihat di codebase Web3 TypeScript.

Untuk proyek yang lebih besar, gunakan @wagmi/cli untuk menghasilkan binding kontrak yang ter-type langsung dari proyek Foundry atau Hardhat Anda. Ia membaca ABI yang sudah dikompilasi dan menghasilkan file TypeScript dengan assertion as const yang sudah diterapkan. Tanpa penyalinan ABI manual, tanpa perbedaan tipe.

Kenyataan yang Tidak Nyaman#

Data blockchain adalah masalah sistem terdistribusi yang menyamar sebagai masalah database. Begitu Anda memperlakukannya sebagai "cuma API biasa," Anda mulai mengakumulasi bug yang tidak terlihat saat development dan intermiten di production.

Tooling-nya sudah jauh lebih baik. Viem adalah peningkatan besar dibandingkan ethers.js untuk type safety dan developer experience. Ponder dan Envio telah membuat custom indexing bisa diakses. Tapi tantangan fundamental — reorg, rate limit, encoding, finalitas — ada di level protokol. Tidak ada library yang mengabstraksikannya.

Bangun dengan asumsi bahwa RPC Anda akan berbohong, blok Anda akan tereorganisasi, angka Anda akan overflow, dan cache Anda akan menyajikan data basi. Lalu tangani setiap kasus secara eksplisit.

Itulah seperti apa data on-chain yang production-grade.

Artikel Terkait