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)denganblockNumber: 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"}
0x1f4046ffadalah selectorpoinFor(uint256), dan0x17318adalah 95000 (lesson ethers.js / viem).- Node menjalankan kode contract di salinan state-nya sendiri, mengembalikan hasilnya, lalu membuang semua perubahan. Gak ada yang masuk block.
- Parameter terakhir,
"latest", menentukan state block mana yang dipakai.
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):
- topic0 = keccak256 dari tanda tangan event:
keccak256("Earned(address,uint256)")=0x053fa1fc52294a40b4ff1a988765bd298c00caa24d685cc3f767dcfde254ef9a. - topic1 =
customer, karena ditandaiindexed: alamatnya diisi nol di kiri sampai 32 byte. - data =
amount, karena gakindexed.
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
- Menyaring parameter yang gak
indexed. Cuma parameterindexedyang jadi topic.amountada didata, jadi harus disaring di kode kamu sendiri. getLogsdari block 0 di jaringan publik. Mulai dari block deploy contract, minta per potongan, dan siap kalau penyedia menolak rentang yang terlalu besar.- Menganggap
latestsudah final. Block terbaru bisa diganti. Untuk keputusan penting, pakaifinalized. - Mencari event dari transaksi yang gagal. Transaksi yang revert gak meninggalkan log sama sekali, dan contract juga gak bisa membaca event. Event cuma untuk dunia luar.
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.