ethers.js / viem — Web3

ethers.js / viem Untuk berinteraksi dengan blockchain dari JavaScript, kamu butuh library. Dua pilihan utama: ethers.js (mature, populer) dan viem (modern…

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 angka 20 begitu saja, tanpa parseUnits. 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

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

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.

Yang akan kamu pelajari