Lewati ke konten utama

Viem

1.1 Pendahuluan​

1.1.1 Apa itu Viem?​

Viem adalah library TypeScript untuk berinteraksi dengan Ethereum dan blockchain yang kompatibel dengan EVM. Library ini menyediakan interface modern dan type-safe untuk operasi blockchain umum seperti membaca data, mengirim transaksi, dan berinteraksi dengan smart contract.

Bayangkan Viem sebagai jembatan antara aplikasi TypeScript/JavaScript kamu dengan blockchain. Daripada membuat panggilan JSON-RPC secara langsung, Viem memberikan fungsi-fungsi yang bersih dan well-typed untuk:

  • Membaca data blockchain (saldo, info block, state contract)
  • Mengirim transaksi (transfer ETH, memanggil fungsi contract)
  • Menandatangani pesan dan typed data
  • Mendengarkan event dan log

1.1.2 Kenapa Viem?​

Keunggulan Viem:

  • Type Safety: Dukungan TypeScript penuh dengan autocompletion dan pengecekan error saat compile
  • Tree-shakable: Hanya bundle apa yang kamu pakai, menghasilkan build yang lebih kecil
  • API Modern: Menggunakan native BigInt, bukan class BigNumber custom
  • Performa: Dioptimasi untuk kecepatan dengan overhead minimal
  • Modular: Client terpisah untuk use case berbeda (public, wallet, test)

Perbandingan dengan alternatif lain:

FiturViemethers.jsweb3.js
TypeScriptFirst-classBaikMembaik
Ukuran bundleKecil (tree-shakable)SedangBesar
Dukungan BigIntNativeClass customClass custom
Kurva belajarSedangMudahMudah

Viem adalah pilihan yang direkomendasikan untuk proyek baru, terutama yang membutuhkan type safety kuat dan tooling modern.

1.1.3 Tujuan​

Di akhir panduan ini, kamu akan mampu:

  • Menyiapkan proyek Viem dengan TypeScript
  • Membaca saldo ETH dari blockchain
  • Mengirim transaksi ETH
  • Memahami apa itu ABI dan kenapa diperlukan
  • Membaca data dari smart contract
  • Menulis ke smart contract (mint token, transfer token)

1.2 Setup​

1.2.1 Instalasi​

Prasyarat:

  • Node.js 18+
  • npm atau yarn
  • Code editor (VS Code direkomendasikan untuk dukungan TypeScript)

Buat proyek baru:

git clone git@github.com:DevWeb3Jogja/template-ts.git workshop-viem
cd workshop-viem
npm install

Verifikasi setup:

npx ts-node src/test-001-hello-world.ts

Install Viem:

npm install viem

1.2.2 Konfigurasi Environment​

Buat file .env di root proyek untuk menyimpan data sensitif seperti private key:

.env

MY_PRIVATE_KEY=<YOUR_PRIVATE_KEY>

Buat file config untuk memuat environment variable:

src/config.ts

import dotenv from 'dotenv';

dotenv.config();

const MY_PRIVATE_KEY = process.env.MY_PRIVATE_KEY;

if (!MY_PRIVATE_KEY) {
throw new Error('MY_PRIVATE_KEY is required');
}

export { MY_PRIVATE_KEY };

Catatan Keamanan: Jangan pernah commit file .env ke version control. Tambahkan ke .gitignore.


1.3 Konsep Dasar​

1.3.1 Client (Public vs Wallet)​

Viem menggunakan tipe client berbeda untuk operasi yang berbeda:

Public Client - Untuk operasi read-only:

  • Membaca saldo
  • Mengambil data block
  • Memanggil view function di contract
  • Tidak butuh private key
import { createPublicClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';

const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
});

Wallet Client - Untuk operasi write:

  • Mengirim transaksi
  • Menandatangani pesan
  • Memanggil state-changing function di contract
  • Membutuhkan private key
import { createWalletClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount('<YOUR_PRIVATE_KEY>');
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(),
});

1.3.2 Chain​

Viem memiliki dukungan built-in untuk banyak chain EVM. Kamu import dari viem/chains:

import { mainnet, sepolia, baseSepolia, arbitrum, polygon } from 'viem/chains';

Setiap objek chain berisi:

  • Chain ID
  • Nama
  • Info native currency
  • URL RPC
  • URL block explorer

Catatan: Panduan ini menggunakan testnet Base Sepolia. Kamu bisa mengganti dengan chain EVM-compatible lainnya dengan mengimport chain yang berbeda dan memperbarui kode.

1.3.3 Transport​

Transport mendefinisikan bagaimana Viem berkomunikasi dengan blockchain. Yang paling umum adalah HTTP:

import { http } from 'viem';

// RPC publik default
const transport = http();

// URL RPC custom
const customTransport = http('https://your-rpc-url.com');

Opsi transport lainnya termasuk WebSocket untuk subscription real-time.


1.4 Native Action​

1.4.1 Membaca Saldo​

Untuk membaca saldo ETH, gunakan fungsi getBalance di public client:

src/test-002-balance-eth.ts

import { baseSepolia } from 'viem/chains';
import { createPublicClient, formatUnits, http } from 'viem';

const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
});

async function main() {
// Dapatkan saldo ETH (mengembalikan nilai dalam wei)
const ethBalance = await publicClient.getBalance({
address: '<YOUR_WALLET_ADDRESS>',
});
console.log('Saldo ETH (wei):', ethBalance);

// Format dari wei ke ETH (18 desimal)
const ethBalanceFmt = formatUnits(ethBalance, 18);
console.log('Saldo ETH (ETH):', ethBalanceFmt);
}

main();

Konsep penting:

  • Saldo dikembalikan dalam wei (unit terkecil, seperti sen terhadap rupiah)
  • 1 ETH = 10^18 wei
  • formatUnits(value, decimals) mengkonversi dari wei ke format yang mudah dibaca

1.4.2 Mengirim ETH​

Untuk mengirim ETH, kamu butuh wallet client dengan private key:

src/test-003-send-eth.ts

import { baseSepolia } from 'viem/chains';
import { createWalletClient, http, parseUnits } from 'viem';
import { Address, privateKeyToAccount } from 'viem/accounts';
import { MY_PRIVATE_KEY } from './config';

const walletAccount = privateKeyToAccount(MY_PRIVATE_KEY as Address);
const walletClient = createWalletClient({
account: walletAccount,
chain: baseSepolia,
transport: http(),
});

async function main() {
// Kirim 0.001 ETH
const hash = await walletClient.sendTransaction({
account: walletAccount,
to: '<RECEIVER_ADDRESS>',
value: parseUnits('0.001', 18), // 0.001 ETH dalam wei
});

console.log('Transaction Hash:', hash);
}

main();

Konsep penting:

  • parseUnits(value, decimals) mengkonversi nilai yang mudah dibaca ke wei
  • Fungsi mengembalikan transaction hash yang bisa kamu cek di block explorer
  • Transaksi akan gagal jika kamu tidak punya ETH cukup untuk value + gas fee

1.5 Interaksi dengan Contract​

1.5.1 Apa itu ABI?​

ABI (Application Binary Interface) adalah spesifikasi JSON yang menjelaskan cara berinteraksi dengan smart contract. ABI memberi tahu aplikasimu:

  • Fungsi apa saja yang ada di contract
  • Parameter apa yang dibutuhkan setiap fungsi
  • Tipe data apa yang digunakan
  • Event apa yang di-emit oleh contract

Tanpa ABI, kamu tidak bisa memanggil fungsi contract karena kamu tidak tahu:

  • Signature fungsi
  • Cara encode parameter
  • Cara decode return value

Di mana mendapatkan ABI:

  1. Dari source code contract di block explorer (jika sudah verified)
  2. Dari dokumentasi proyek atau GitHub
  3. Generate dari source code Solidity

Contoh: Mendapatkan ABI dari BaseScan

  1. Buka halaman contract: https://sepolia.basescan.org/address/0x726da6f345ddd8d081f690f02cb9f636929ffa1e
  2. Klik tab "Contract"
  3. Klik "ABI" dan copy JSON-nya

Buat file ABI:

src/abi.ts

export const mockTokenABI = [
{ inputs: [], stateMutability: 'nonpayable', type: 'constructor' },
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'owner', type: 'address' },
{ indexed: true, internalType: 'address', name: 'spender', type: 'address' },
{ indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' },
],
name: 'Approval',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'from', type: 'address' },
{ indexed: true, internalType: 'address', name: 'to', type: 'address' },
{ indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' },
],
name: 'Transfer',
type: 'event',
},
{
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
name: 'balanceOf',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'decimals',
outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'to', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'mint',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'name',
outputs: [{ internalType: 'string', name: '', type: 'string' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'symbol',
outputs: [{ internalType: 'string', name: '', type: 'string' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'totalSupply',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'to', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'transfer',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;

Catatan: Assertion as const mengaktifkan type inference Viem untuk autocompletion yang lebih baik.

1.5.2 Membaca Contract (view function)​

View function adalah read-only dan tidak memerlukan gas. Gunakan readContract di public client:

src/test-004-read-contract.ts

import { baseSepolia } from 'viem/chains';
import { createPublicClient, formatUnits, http } from 'viem';
import { mockTokenABI } from './abi';

const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
});

async function main() {
const tokenAddress = '0x726da6f345ddd8d081f690f02cb9f636929ffa1e';

// Baca nama token
const tokenName = await publicClient.readContract({
address: tokenAddress,
abi: mockTokenABI,
functionName: 'name',
});

// Baca simbol token
const tokenSymbol = await publicClient.readContract({
address: tokenAddress,
abi: mockTokenABI,
functionName: 'symbol',
});

console.log('Nama Token:', tokenName);
console.log('Simbol Token:', tokenSymbol);

// Baca saldo token untuk sebuah address
const tokenBalance = await publicClient.readContract({
address: tokenAddress,
abi: mockTokenABI,
functionName: 'balanceOf',
args: ['<YOUR_WALLET_ADDRESS>'],
});

const tokenBalanceFmt = formatUnits(tokenBalance, 18);

console.log('Saldo Token (raw):', tokenBalance);
console.log('Saldo Token (formatted):', tokenBalanceFmt);
}

main();

Konsep penting:

  • readContract membutuhkan: address, ABI, dan nama fungsi
  • Untuk fungsi dengan parameter, pass di array args
  • Viem menyediakan type checking untuk nama fungsi dan argumen

1.5.3 Menulis ke Contract (state-changing function)​

State-changing function mengubah data blockchain dan membutuhkan:

  • Wallet client dengan private key
  • ETH untuk gas fee

a. Mint Token​

src/test-005-mint-token.ts

import { baseSepolia } from 'viem/chains';
import { createWalletClient, http, parseUnits } from 'viem';
import { mockTokenABI } from './abi';
import { Address, privateKeyToAccount } from 'viem/accounts';
import { MY_PRIVATE_KEY } from './config';

const walletAccount = privateKeyToAccount(MY_PRIVATE_KEY as Address);
const walletClient = createWalletClient({
account: walletAccount,
chain: baseSepolia,
transport: http(),
});

async function main() {
const txHash = await walletClient.writeContract({
address: '<TOKEN_ADDRESS>',
abi: mockTokenABI,
functionName: 'mint',
args: [
'<YOUR_WALLET_ADDRESS>',
parseUnits('1000', 18), // Mint 1000 token
],
});

console.log('Transaction Hash:', txHash);
}

main();

b. Transfer Token​

src/test-006-transfer-token.ts

import { baseSepolia } from 'viem/chains';
import { createWalletClient, http, parseUnits } from 'viem';
import { mockTokenABI } from './abi';
import { Address, privateKeyToAccount } from 'viem/accounts';
import { MY_PRIVATE_KEY } from './config';

const walletAccount = privateKeyToAccount(MY_PRIVATE_KEY as Address);
const walletClient = createWalletClient({
account: walletAccount,
chain: baseSepolia,
transport: http(),
});

async function main() {
const txHash = await walletClient.writeContract({
address: '<TOKEN_ADDRESS>',
abi: mockTokenABI,
functionName: 'transfer',
args: [
'<RECEIVER_ADDRESS>',
parseUnits('100', 18), // Transfer 100 token
],
});

console.log('Transaction Hash:', txHash);
}

main();

Konsep penting:

  • writeContract mengembalikan transaction hash langsung (tidak menunggu konfirmasi)
  • Gunakan hash untuk mengecek status transaksi di block explorer
  • Jika perlu menunggu konfirmasi, gunakan waitForTransactionReceipt

1.6 Kesimpulan​

Di panduan ini, kamu belajar:

  1. Apa itu Viem - Library TypeScript modern untuk interaksi blockchain
  2. Kenapa Viem - Type safety, tree-shaking, dukungan native BigInt
  3. Konsep dasar - Public vs Wallet client, chain, transport
  4. Native action - Membaca saldo, mengirim ETH
  5. Interaksi contract - Apa itu ABI, membaca contract, menulis ke contract

Skill yang didapat:

  • Menyiapkan proyek Viem
  • Membaca data on-chain tanpa wallet
  • Mengirim transaksi dengan wallet
  • Berinteraksi dengan contract token ERC20

Langkah selanjutnya:

  • Eksplorasi kemampuan event listening Viem
  • Pelajari multicall untuk batching request
  • Cek dokumentasi Viem untuk fitur lanjutan