Wallet seperti MetaMask adalah aplikasi yang menyimpan private key dan menandatangani transaksi atas persetujuanmu. Wallet gak "menyimpan koin": saldo POIN Dinda tercatat di blockchain, dan wallet cuma memegang kunci yang membuktikan saldo itu miliknya. Bayangin stempel dan tanda tangan Dinda yang selalu ada di tasnya sendiri. Halaman web Toko Kaos Raka boleh menyodorkan dokumen dan bilang "tolong distempel", Dinda membacanya lalu memutuskan, tapi stempelnya gak pernah berpindah ke tangan toko. Di lesson ini kita bikin halaman toko yang bisa terhubung ke wallet Dinda.
🧪 Tebakan cepat: halaman toko memanggil
eth_requestAccounts, lalu Dinda menekan Tolak di wallet. Apa yang diterima kode halaman: array kosong,null, atau error dengancode4001? Tebak dulu, jawabannya ada di bagian "Connect: eth_requestAccounts".
EIP-1193: satu pintu bernama request
Wallet browser memasang sebuah provider di halaman, biasanya di window.ethereum. Bentuknya diatur standar EIP-1193: satu fungsi request({ method, params }) yang mengembalikan Promise, plus on(event, handler) untuk mendengar perubahan. Semua obrolan dengan wallet lewat pintu itu, memakai nama method JSON-RPC Ethereum.
const ethereum = window.ethereum
if (!ethereum) show('Pasang wallet dulu, misalnya MetaMask, lalu muat ulang halaman.')
else console.log(await ethereum.request({ method: 'eth_chainId' })) // '0xaa36a7'
window.ethereumgak ada kalau gak ada wallet yang terpasang. Cek dulu, jangan langsung dipakai.- Hasil
eth_chainIdberupa hex string:'0x7a69'itu 31337 (chain lokal),'0xaa36a7'itu 11155111 (Sepolia),'0x1'itu mainnet.
MetaMask gak bisa dipasang di Lab halaman ini, jadi Lab-nya memakai wallet tiruan yang menjalankan antarmuka EIP-1193 yang sama: request, on, kode error yang sama. Tanda tangan di dalamnya dibuat sebelumnya oleh node Hardhat asli dengan akun test.
Connect: eth_requestAccounts
async function connect() {
try {
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
return accounts[0] // '0x70997970c51812dc3a010c7d01b50e0d17dc79c8'
} catch (error) {
if (error.code === 4001) show('Koneksi ditolak. Coba lagi kalau sudah siap.')
else throw error
}
}
eth_requestAccountsmembuka popup wallet. Kalau disetujui, hasilnya array alamat yang diizinkan, dan alamat yang aktif ada di index 0.- Kalau Dinda menekan Tolak, Promise-nya gagal dengan error ber-
code4001 ("User Rejected Request" di EIP-1193), bukan array kosong. Itu jawaban tebakannya. Tangkap dengantry/catchdan tampilkan pesan yang ramah. eth_accountsmengembalikan alamat yang sudah diizinkan tanpa popup (array kosong kalau belum). Pakai waktu halaman dimuat.eth_requestAccountscuma dipanggil saat pengguna menekan tombol "Connect".ethereum.enable()itu cara lama yang sudah usang. Jangan dipakai.
Jaringan: chainId dan wallet_switchEthereumChain
Wallet Dinda bisa sedang di mainnet, padahal toko kita cuma jalan di chain lokal dan Sepolia. Minta pindah dengan wallet_switchEthereumChain. Kalau wallet belum kenal jaringannya, ia menolak dengan code 4902, dan kamu menawarkan untuk menambahkannya:
async function switchToLocalChain() {
try {
await ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x7a69' }] })
} catch (error) {
if (error.code !== 4902) throw error // 4902: wallet belum kenal chain ini
await ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: '0x7a69',
chainName: 'Hardhat lokal',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: ['http://127.0.0.1:8545'],
}],
})
}
}
chainIdselalu ditulis hex: 31337 jadi'0x7a69'.- Sepolia sudah ada bawaan di MetaMask (di daftar test network), jadi cukup
wallet_switchEthereumChaindengan'0xaa36a7'. Chain lokal belum, jadi perlu ditambahkan dulu. - Tiap pindah dan tiap tambah jaringan tetap butuh persetujuan pengguna di wallet.
Event: accountsChanged dan chainChanged
Dinda bisa mengganti akun atau jaringan langsung dari wallet kapan saja. Halaman diberi tahu lewat event:
ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) signOut() // wallet diputus
else switchUser(accounts[0])
})
ethereum.on('chainChanged', (chainId) => {
window.location.reload() // saran MetaMask: muat ulang, mulai dari state bersih
})
accountsChangedmembawa array baru. Array kosong artinya halaman gak boleh lagi melihat akun mana pun.- Jangan menyimpan alamat sekali lalu menganggapnya tetap. Sesi login juga harus ikut berakhir kalau akunnya berganti.
Banyak wallet sekaligus: EIP-6963
Kalau Dinda memasang dua extension wallet, keduanya berebut window.ethereum, dan cuma satu yang menang. EIP-6963 (status Final) memperbaikinya: tiap wallet mengumumkan dirinya lewat event, dan halaman menampilkan pilihan.
const wallets = []
window.addEventListener('eip6963:announceProvider', (event) => {
wallets.push(event.detail) // { info: { uuid, name, icon, rdns }, provider }
})
window.dispatchEvent(new Event('eip6963:requestProvider'))
info.rdns adalah nama unik wallet dalam bentuk domain terbalik (misalnya io.metamask), dan provider adalah provider EIP-1193 yang sama seperti tadi. Library seperti wagmi (nanti di track ini) sudah mengurus ini.
Login dengan wallet: personal_sign dan SIWE
Untuk login, toko gak butuh transaksi. Server memberi pesan berisi nonce acak, Dinda menandatanganinya dengan personal_sign, dan server mengecek tanda tangannya. Tanda tangan itu gratis (gak ada gas) dan gak mengubah apa pun di blockchain. Format pesannya distandarkan Sign-In with Ethereum (SIWE, ERC-4361):
toko-kaos-raka.test wants you to sign in with your Ethereum account:
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Masuk ke Toko Kaos Raka untuk melihat POIN kamu.
URI: https://toko-kaos-raka.test/login
Version: 1
Chain ID: 11155111
Nonce: k8Rq2vX7mPz4
Issued At: 2026-10-11T09:00:00Z
const utf8ToHex = (text) => '0x' + Array.from(new TextEncoder().encode(text), (b) => b.toString(16).padStart(2, '0')).join('')
const signature = await ethereum.request({
method: 'personal_sign',
params: [utf8ToHex(message), address], // pesan dulu, baru alamat
})
// 0xd6ccf93f9c8c...410b665c1c (65 byte)
Tanda tangan di atas asli, dibuat node Hardhat untuk akun Dinda. Di server, verifyMessage({ address, message, signature }) dari viem mengembalikan true, dan false untuk alamat lain. Server juga harus mengecek domain, chain ID, waktunya, dan bahwa nonce itu baru sekali dipakai, supaya tanda tangan lama gak bisa dipakai ulang.
Seed phrase gak pernah diminta
Seed phrase (12 atau 24 kata) adalah cadangan semua private key di wallet. dApp yang jujur gak pernah butuh seed phrase atau private key: semua kerjanya lewat request, dan wallet yang menandatangani. Situs, formulir, atau "admin" di chat yang meminta seed phrase pasti penipu. Hal yang sama berlaku untuk permintaan tanda tangan yang isinya gak bisa kamu pahami.
⚠️ Jebakan yang sering ketemu
- Membandingkan alamat dengan
===begitu saja. Ada wallet yang mengembalikan alamat huruf kecil semua (0x7099...79c8), ada yang versi checksum (0x7099...79C8). Samakan dulu dengangetAddress()dari viem atau ethers, atautoLowerCase(). - Gak menangkap error 4001. Penolakan itu normal. Tanpa
catch, halamanmu crash cuma karena pengguna bilang "nanti dulu". - Memanggil
eth_requestAccountssaat halaman dimuat. Popup muncul tanpa diminta. Pakaieth_accountsuntuk mengecek, dan minta koneksi saat tombol ditekan. - Menganggap jaringannya sudah benar. Cek
eth_chainIddan dengarkanchainChangedsebelum mengirim apa pun.
TL;DR: Wallet menyimpan private key dan menandatangani; dApp cuma bicara lewat provider EIP-1193: request({ method, params }) dan on(event, handler). eth_requestAccounts meminta koneksi lewat popup dan gagal dengan error 4001 kalau ditolak; eth_accounts mengecek tanpa popup. eth_chainId dalam hex (31337 = 0x7a69, Sepolia = 0xaa36a7); pindah dengan wallet_switchEthereumChain, dan kalau error 4902, tambahkan dulu dengan wallet_addEthereumChain. Dengarkan accountsChanged (array kosong = terputus) dan chainChanged. EIP-6963 menemukan semua wallet yang terpasang. Login dengan personal_sign atas pesan SIWE berisi nonce, lalu server memverifikasi tanda tangannya. Gak ada dApp jujur yang meminta seed phrase.