Reading Blockchain Data — Web3

Reading Blockchain Data Membaca data dari blockchain adalah operasi gratis (tidak butuh gas). Kamu bisa membaca state variables, events/logs, dan melakukan…

Membaca data blockchain artinya bertanya ke sebuah node: berapa saldo sebuah alamat, apa jawaban fungsi view sebuah contract, dan apa saja yang pernah terjadi. Membaca itu gratis: gak ada transaksi, gak ada tanda tangan, gak ada gas. Bayangin buku kas Toko Kaos Raka yang dipajang di balik kaca. Siapa pun boleh melihat halaman terakhir (state sekarang), membuka halaman lama (block lama), atau mencari semua catatan bertanda "Dinda" (event log). Melihat gak mengubah isi buku, jadi gak ada yang perlu dibayar.

🧪 Tebakan cepat: Raka mencatat Kaos Sablon Kopi Dinda (95 POIN) di block 2 dan Mug Sablon Kopi (65 POIN) di block 5. Sekarang chain sudah di block 5. Kamu memanggil balanceOf(Dinda) dengan blockNumber: 3n. Hasilnya 0, 95, atau 160 POIN? Tebak dulu, jawabannya ada di bagian "Membaca state di block tertentu".

Chain lokal yang kita baca

Semua angka di lesson ini dibaca sungguhan dari node Hardhat 3 di laptop (npx hardhat node, alamat http://127.0.0.1:8545, chain ID 31337). Akunnya akun test publik Hardhat/Anvil, private key-nya dicetak oleh node itu sendiri, jadi gak pernah ada uang asli. Isi chain-nya:

Block Transaksi (dari Raka) Event
1 deploy PoinRaka di 0x5FbD...0aa3
2 earn(Dinda, 95000), Kaos Sablon Kopi Earned(Dinda, 95 POIN)
3 earn(Yoga, 75000), Kaos Polos Hitam Earned(Yoga, 75 POIN)
4 earn(Yoga, 40000), Topi Raka Earned(Yoga, 40 POIN)
5 earn(Dinda, 65000), Mug Sablon Kopi Earned(Dinda, 65 POIN)

Library (viem atau ethers) mengirim pertanyaan ke node dalam format JSON-RPC. Tiga metode yang dipakai paling sering, semuanya gratis:

Metode Membaca viem ethers v6
eth_getBalance saldo ETH sebuah alamat getBalance({ address }) provider.getBalance(address)
eth_call jawaban fungsi view/pure readContract(...) contract.balanceOf(...)
eth_getLogs event yang pernah terjadi getLogs(...) contract.queryFilter(...)

Saldo ETH Dinda, lewat ethers: await provider.getBalance(DINDA) mengembalikan 10000000000000000000000n (dalam wei), dan ethers.formatEther(...) menulisnya '10000.0'. Itu ETH test bawaan node.

eth_call: menjalankan fungsi tanpa transaksi

import { createPublicClient, http, parseAbi, formatUnits } from 'viem'
import { hardhat } from 'viem/chains'

const POIN = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
const DINDA = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
const abi = parseAbi([
  'function balanceOf(address) view returns (uint256)',
  'function poinFor(uint256 rupiah) pure returns (uint256)',
  'event Earned(address indexed customer, uint256 amount)',
])
const client = createPublicClient({ chain: hardhat, transport: http() })

const saldo = await client.readContract({ address: POIN, abi, functionName: 'balanceOf', args: [DINDA] })
console.log(formatUnits(saldo, 18)) // '160'

const poin = await client.readContract({ address: POIN, abi, functionName: 'poinFor', args: [95000n] })
console.log(poin) // 95000000000000000000n

Yang benar-benar dikirim untuk poinFor(95000), dan jawaban node-nya:

request:  {"jsonrpc":"2.0","id":5,"method":"eth_call","params":[{"data":"0x1f4046ff0000000000000000000000000000000000000000000000000000000000017318","to":"0x5FbDB2315678afecb367f032d93F642f64180aa3"},"latest"]}
response: {"jsonrpc":"2.0","id":5,"result":"0x0000000000000000000000000000000000000000000000052663ccab1e1c0000"}

Membaca state di block tertentu

Ganti "latest" dengan nomor block, dan node menjawab seperti apa state-nya waktu itu:

await client.readContract({ address: POIN, abi, functionName: 'balanceOf', args: [DINDA], blockNumber: 3n })
// 95000000000000000000n
blockNumber 1 2 3 4 5
POIN Dinda 0 95 95 95 160

Di block 3 Dinda baru punya Kaos Sablon Kopi, jadi jawabannya 95 POIN. Itu jawaban tebakannya. Mug-nya baru tercatat di block 5. Di chain lokal semua block lama tersedia. Node publik biasanya cuma menyimpan state beberapa waktu terakhir; untuk state yang sudah lama dibutuhkan archive node.

Event dan log: jejak yang ditinggalkan transaksi

Setiap emit Earned(customer, amount) menulis satu log ke receipt transaksi. Log punya tiga bagian: address (contract yang menulis), topics (maksimal 4, masing-masing 32 byte, bisa dipakai untuk menyaring) dan data (sisanya). Untuk event Earned(address indexed customer, uint256 amount):

const logs = await client.getLogs({
  address: POIN,
  event: abi[2],
  args: { customer: DINDA },
  fromBlock: 0n,
  toBlock: 'latest',
})
for (const log of logs) console.log(log.blockNumber, formatUnits(log.args.amount, 18))
// 2n '95'
// 5n '65'

Request yang dikirim viem, dan log pertama di jawabannya (beberapa field dipotong):

{"jsonrpc":"2.0","id":11,"method":"eth_getLogs","params":[{"address":"0x5FbDB2315678afecb367f032d93F642f64180aa3","topics":["0x053fa1fc52294a40b4ff1a988765bd298c00caa24d685cc3f767dcfde254ef9a","0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"],"fromBlock":"0x0","toBlock":"latest"}]}

{"blockNumber":"0x2","transactionHash":"0xc32ebe65a4f221668ce0238723c33260ff6b2eb437a0a19eab99ca396aa5f077","logIndex":"0x0","address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0x053fa1fc...ef9a","0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"],"data":"0x0000000000000000000000000000000000000000000000052663ccab1e1c0000"}

data 0x...052663ccab1e1c0000 dibaca sebagai 95000000000000000000n, 95 POIN. Node menyaring berdasarkan topics, jadi yang bisa disaring cuma parameter indexed. Mencari "semua Earned yang amount-nya 95" gak bisa lewat filter; ambil semua log-nya, lalu saring sendiri.

Rentang block dan batas penyedia RPC

fromBlock dan toBlock menentukan block mana yang diperiksa. Tanpa keduanya, node cuma memeriksa block terbaru. Di chain lokal 5 block itu ringan, tapi di Sepolia atau mainnet ada jutaan block, dan penyedia RPC membatasi eth_getLogs: rentang block per request, jumlah log per jawaban, atau keduanya. Angkanya beda tiap penyedia, jadi baca dokumentasinya. Polanya selalu sama: mulai dari block saat contract di-deploy (bukan 0), lalu minta per potongan:

const DEPLOY_BLOCK = 1n // block tempat PoinRaka di-deploy
const STEP = 2n // contoh kecil; di jaringan publik misalnya 2000n
const head = await client.getBlockNumber() // 5n
const logs = []
for (let from = DEPLOY_BLOCK; from <= head; from += STEP) {
  const to = from + STEP - 1n < head ? from + STEP - 1n : head
  logs.push(...(await client.getLogs({ address: POIN, event: abi[2], fromBlock: from, toBlock: to })))
}
console.log(logs.length) // 4, dari 3 request: block 1-2, 3-4, 5

latest, safe, finalized

Block terbaru belum tentu permanen. Selain nomor, node menerima tiga label:

Label Artinya
latest block terbaru yang diterima node. Masih bisa diganti (reorg)
safe sudah didukung mayoritas validator, sangat kecil kemungkinan berubah
finalized final. Di mainnet dan Sepolia biasanya tertinggal sekitar 13 menit (2 epoch, 64 slot × 12 detik) dari latest

Di node Hardhat lokal gak ada validator, jadi getBlock({ blockTag: 'safe' }) dan 'finalized' sama-sama mengembalikan block 5, sama dengan latest. Untuk menampilkan saldo, latest cukup. Untuk keputusan yang mahal kalau salah, misalnya mengirim barang setelah pembayaran on-chain, tunggu sampai finalized.

📌 Membaca storage langsung

eth_call menjalankan getter. State-nya sendiri tersimpan di slot storage 32 byte, dan eth_getStorageAt membacanya mentah-mentah. PoinRaka menyimpan owner di slot 0 dan totalSupply di slot 1:

getStorageAt(POIN, 0x0) = 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266   (Raka)
getStorageAt(POIN, 0x1) = 0x00000000000000000000000000000000000000000000000ee86442fcd06c0000   (275 POIN)

Ini juga bukti bahwa private di Solidity bukan rahasia: siapa pun bisa membaca slot-nya.

⚠️ Jebakan yang sering ketemu

TL;DR: Membaca blockchain itu gratis: saldo ETH (eth_getBalance, di ethers provider.getBalance(address)), fungsi view/pure (eth_call, viem readContract), dan event (eth_getLogs, viem getLogs). eth_call menjalankan kode contract di salinan state node lalu membuang perubahannya; dengan blockNumber kamu membaca state masa lalu (Dinda: 95 POIN di block 3, 160 di block 5). Log berisi address, topics dan data: topic0 = keccak256 tanda tangan event (Earned(address,uint256) = 0x053fa1fc...), parameter indexed jadi topic berikutnya dan bisa disaring, sisanya di data. Batasi rentang fromBlock/toBlock dan minta per potongan karena penyedia RPC punya batas. latest bisa berubah, safe hampir pasti, finalized permanen.

Yang akan kamu pelajari