Wallet & MetaMask Integration — Web3

Wallet & MetaMask Integration Wallet adalah gerbang user ke dunia Web3. MetaMask adalah browser extension wallet paling populer. Sebagai developer, kamu perlu…

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 dengan code 4001? 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'

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
  }
}

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'],
      }],
    })
  }
}

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
})

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

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.

Yang akan kamu pelajari