ethers.js dan viem adalah library JavaScript/TypeScript untuk berbicara dengan blockchain Ethereum: membaca saldo, memanggil contract, dan mengirim transaksi. Keduanya ngobrol dengan node lewat JSON-RPC, protokol yang sama yang dipakai wallet. Bayangin kurir Toko Kaos Raka yang membawa formulir baku ke gudang. Kamu bilang "cek POIN Dinda", kurir mengisi formulir dengan kode yang dimengerti gudang (calldata hasil ABI encoding), mengantarnya (request JSON-RPC lewat HTTP), lalu menerjemahkan jawabannya kembali ke bahasamu (angka BigInt). Versi yang dipakai di lesson ini: ethers 6.17.0 dan viem 2.57.4, versi terbaru per Oktober 2026.
🧪 Tebakan cepat: POIN punya 18 decimals. Kamu memanggil
transfer(yoga, 20)dengan angka20begitu saja, tanpaparseUnits. Berapa POIN yang diterima Yoga: 20, 0,00000000000000002, atau error? Tebak dulu, jawabannya ada di bagian "Jumlah: BigInt dan decimals".
Baca dan tulis: provider, signer, dan client
| ethers v6 | viem v2 | |
|---|---|---|
Baca (gratis, eth_call) |
JsonRpcProvider, BrowserProvider |
createPublicClient |
| Tulis (ditandatangani, bayar gas) | Signer dari provider.getSigner() |
createWalletClient |
| Panggil contract | new ethers.Contract(alamat, abi, provider atau signer) |
readContract, simulateContract, writeContract |
Provider (di viem: public client) cuma membaca: saldo, block, fungsi view. Signer (di viem: wallet client) memegang akses ke akun yang bisa menandatangani, jadi dia yang mengirim transaksi. Di browser, signer-nya adalah wallet pengguna: new ethers.BrowserProvider(window.ethereum) di ethers, custom(window.ethereum) di viem, dan tiap transaksi tetap minta persetujuan lewat popup (lesson Wallet). Di chain lokal Hardhat atau Anvil, node-nya sendiri memegang akun test, jadi getSigner(0) langsung memakai akun #0 tanpa private key di kode.
Membaca dan menulis dengan ethers v6
PoinRaka sudah di-deploy di chain lokal (0x5FbD...0aa3). Raka mencatat Kaos Sablon Kopi Dinda:
import { ethers } from 'ethers'
const POIN = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
const DINDA = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
const abi = [
'function balanceOf(address) view returns (uint256)',
'function earn(address customer, uint256 rupiah)',
]
const provider = new ethers.JsonRpcProvider('http://127.0.0.1:8545')
const raka = await provider.getSigner(0)
const poin = new ethers.Contract(POIN, abi, provider)
console.log(await poin.balanceOf(DINDA))
const tx = await poin.connect(raka).earn(DINDA, 95000n)
const receipt = await tx.wait()
console.log(receipt.status, receipt.gasUsed)
const saldo = await poin.balanceOf(DINDA)
console.log(saldo, ethers.formatUnits(saldo, 18))
$ node poin-ethers.mjs
0n
1 71679n
95000000000000000000n 95.0
abidi ethers boleh ditulis sebagai teks yang mudah dibaca. Cukup fungsi yang kamu pakai.new ethers.Contract(POIN, abi, provider)membuat objek contract untuk membaca.poin.connect(raka)membuat salinannya yang terhubung ke signer, untuk menulis.earn(...)mengembalikan transaksi yang baru dikirim.await tx.wait()menunggu sampai masuk block dan mengembalikan receipt (status1 artinya berhasil).- Angka dari contract selalu
bigint(berakhirann).
Hal yang sama dengan viem v2
Dinda lalu membeli Mug Sablon Kopi (Rp 65.000):
import { createPublicClient, createWalletClient, http, parseAbi, formatUnits } from 'viem'
import { hardhat } from 'viem/chains'
const abi = parseAbi([
'function balanceOf(address) view returns (uint256)',
'function earn(address customer, uint256 rupiah)',
])
const publicClient = createPublicClient({ chain: hardhat, transport: http() })
const walletClient = createWalletClient({
account: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
chain: hardhat,
transport: http(),
})
const { request } = await publicClient.simulateContract({
account: walletClient.account,
address: POIN,
abi,
functionName: 'earn',
args: [DINDA, 65000n],
})
const hash = await walletClient.writeContract(request)
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log(receipt.status, receipt.gasUsed)
const saldo = await publicClient.readContract({ address: POIN, abi, functionName: 'balanceOf', args: [DINDA] })
console.log(saldo, formatUnits(saldo, 18))
$ node poin-viem.mjs
success 37467n
160000000000000000000n 160
(POIN dan DINDA sama seperti di skrip ethers.) Gaya viem berupa fungsi, bukan objek contract. simulateContract menjalankan transaksinya dulu lewat eth_call; kalau bakal revert, kamu dapat error-nya sebelum ada yang ditandatangani. Perhatikan juga formatUnits: ethers menulis 95.0, viem menulis 160.
Di balik layar: selector dan ABI encoding
Node gak mengerti balanceOf(DINDA). Library mengubahnya jadi calldata: 4 byte function selector, lalu tiap argumen dalam slot 32 byte. Selector adalah 4 byte pertama dari keccak256 tanda tangan fungsinya, ditulis tanpa spasi dan tanpa nama parameter:
keccak256("transfer(address,uint256)") = 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
selector = 0xa9059cbb
Calldata untuk transfer(Yoga, 20 POIN), fungsi standar ERC-20 yang nanti dimiliki POIN:
0xa9059cbb selector
0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc alamat Yoga, diisi nol di kiri sampai 32 byte
000000000000000000000000000000000000000000000001158e460913d00000 20 × 10^18 dalam hex
Ini request asli yang dikirim viem untuk readContract balanceOf(DINDA) tadi, dan jawaban node-nya:
request: {"jsonrpc":"2.0","id":0,"method":"eth_call","params":[{"data":"0x70a0823100000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8","to":"0x5FbDB2315678afecb367f032d93F642f64180aa3"},"latest"]}
response: {"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000000000000000000000000008ac7230489e800000"}
0x70a08231 adalah selector balanceOf(address), dan 0x...08ac7230489e800000 dibaca sebagai 160000000000000000000n, 160 POIN. Untuk menulis, isinya sama, cuma method-nya eth_sendTransaction dengan from akun Raka.
Jumlah: BigInt dan decimals
Jumlah token selalu bilangan bulat dalam satuan terkecil. Untuk menulis "20 POIN", ubah dengan parseUnits('20', 18) (atau parseEther untuk ETH):
| Kamu tulis | Yang dikirim | Artinya |
|---|---|---|
parseUnits('20', 18) |
20000000000000000000n |
20 POIN |
20n |
20n |
0,00000000000000002 POIN |
Jadi tanpa parseUnits, Yoga menerima 0,00000000000000002 POIN, tanpa error apa pun. Itu jawaban tebakannya. Ke arah sebaliknya, formatUnits(nilai, 18) mengubah satuan terkecil jadi teks untuk ditampilkan.
Kenapa BigInt, bukan number biasa? number cuma tepat sampai Number.MAX_SAFE_INTEGER (9.007.199.254.740.991). Number(95000000000000000001n) jadi 95000000000000000000: satu satuan hilang diam-diam. Dan mencampur keduanya langsung gagal:
> 95000000000000000000n + 1
TypeError: Cannot mix BigInt and other types, use explicit conversions
ethers atau viem?
Dua-duanya aktif dikembangkan dan sama-sama benar untuk project baru. ethers memakai objek (Contract, Signer) dan sudah lama dipakai, jadi tutorialnya banyak. viem memakai fungsi, tipe TypeScript-nya dibaca langsung dari ABI, dan dipakai oleh wagmi untuk React (nanti di track ini). Calldata yang dihasilkan keduanya sama byte demi byte, karena aturannya ditentukan ABI, bukan library-nya. Pilih satu per project, jangan dicampur.
⚠️ Jebakan yang sering ketemu
- Lupa
parseUnits.transfer(yoga, 20)mengirim 20 satuan terkecil, bukan 20 POIN. Kebalikannya juga: menampilkan95000000000000000000tanpaformatUnits. - Menganggap transaksi selesai setelah dikirim.
earn(...)atauwriteContractcuma mengembalikan hash. Tunggutx.wait()atauwaitForTransactionReceipt, lalu cekstatus. - Signature yang salah tulis. Selector dihitung dari teks persis:
transfer(address,uint)jadi0x6cb927d8,transfer(address, uint256)(dengan spasi) jadi0x9d61d234, dan dua-duanya gak cocok dengan fungsi mana pun. Biarkan library yang menghitung dari ABI. - Private key di kode frontend. Semua yang dikirim ke browser bisa dibaca orang. Di browser, signer-nya wallet pengguna; di chain lokal, akun test node; di server, simpan kunci di secret manager, jangan di repo.
TL;DR: ethers v6 dan viem v2 adalah library JavaScript untuk membaca dan menulis ke Ethereum lewat JSON-RPC. Provider (viem: public client) cuma membaca dan gratis; signer (viem: wallet client) menandatangani transaksi yang bayar gas. ethers memakai new ethers.Contract(...) dan tx.wait(); viem memakai readContract, simulateContract, writeContract dan waitForTransactionReceipt. Di balik layar, panggilan jadi calldata: selector 4 byte (keccak256("transfer(address,uint256)") diawali 0xa9059cbb) lalu argumen 32 byte. Jumlah token adalah BigInt dalam satuan terkecil: parseUnits('20', 18) = 20000000000000000000n, dan 20n saja cuma 0,00000000000000002 POIN. formatUnits untuk menampilkan (ethers 95.0, viem 95). Jangan pakai number untuk jumlah, dan jangan taruh private key di frontend.