Skip to main content

7. Integrasi Frontend dengan Token Factory

Pada bagian ini, kita akan membuat frontend sederhana menggunakan React, Vite, dan TailwindCSS untuk berinteraksi dengan smart contract Token Factory yang telah kita deploy. Frontend ini akan memungkinkan pengguna untuk membuat token baru dan melihat token yang telah dibuat.

Tampilan Frontend Monad Token Factory

1. Setup Proyek Frontend

Mari mulai dengan membuat proyek React + Vite baru dengan TypeScript.

1.1. Membuat Proyek Vite

npm create vite@latest erc20-factory-ui -- --template react-ts
cd erc20-factory-ui

1.2. Menginstal Dependensi

Install paket yang diperlukan untuk mengintegrasikan frontend dengan blockchain:

npm install @rainbow-me/rainbowkit wagmi viem @tanstack/react-query react-hot-toast
npm install -D tailwindcss @tailwindcss/vite

Penjelasan dependensi:

  • @rainbow-me/rainbowkit: Komponen UI untuk koneksi wallet
  • wagmi: Library untuk berinteraksi dengan kontrak Ethereum
  • viem: Library Ethereum untuk TypeScript
  • @tanstack/react-query: Untuk manajemen state dan caching data
  • react-hot-toast: Untuk menampilkan notifikasi
  • tailwindcss: Framework CSS utility-first

2. Konfigurasi TailwindCSS

2.1. Konfigurasi Vite

Update file vite.config.ts:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
plugins: [react(), tailwindcss()],
})

2.2. Konfigurasi CSS

Buat file src/index.css dengan konten:

@import "tailwindcss";

body {
font-family: "Inter", sans-serif;
}

3. Struktur Proyek

Berikut adalah struktur proyek yang akan kita buat:

src/
├── components/
│ ├── Container.tsx # Komponen utama yang berisi logika interaksi dengan kontrak
│ ├── Header.tsx # Header dengan tombol connect wallet
│ ├── TokenForm.tsx # Form untuk membuat token baru
│ └── TokenList.tsx # Tabel untuk menampilkan token yang dibuat
├── constants/
│ ├── ERC20_FACTORY_ABI.json # ABI untuk kontrak factory
│ ├── ERC20_TOKEN_ABI.json # ABI untuk kontrak token ERC20
│ └── index.tsx # Export konstanta
├── App.tsx # Komponen root dengan provider
├── main.tsx # Entry point
└── index.css # Stylesheet utama

4. Menyiapkan Konstanta dan ABI

Pertama, kita perlu menyiapkan ABI (Application Binary Interface) untuk kontrak kita dan alamat kontrak.

4.1. ABI Factory Contract

Buat folder src/constants dan file ERC20_FACTORY_ABI.json dengan ABI dari kontrak TokenFactory:

[
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "tokenAddress",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "initialSupply",
"type": "uint256"
}
],
"name": "TokenCreated",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
},
{
"internalType": "uint256",
"name": "initialSupply",
"type": "uint256"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
}
],
"name": "createToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getAllTokens",
"outputs": [
{
"internalType": "address[]",
"name": "",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getTokenCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]

4.2. ABI Token ERC20

Buat file ERC20_TOKEN_ABI.json dengan ABI dari kontrak Token ERC20.

4.3. Export Konstanta

Buat file src/constants/index.tsx:

import ERC20_JSON from "./ERC20_FACTORY_ABI.json"
import TOKEN_JSON from "./ERC20_TOKEN_ABI.json"

export const ERC20_FACTORY_ABI_JSON = ERC20_JSON;
export const ERC20_TOKEN_ABI_JSON = TOKEN_JSON;
export const ERC20_FACTORY_CONTRACT = "0xYOUR_DEPLOYED_FACTORY_ADDRESS";

⚠️ Penting: Ganti 0xYOUR_DEPLOYED_FACTORY_ADDRESS dengan alamat kontrak TokenFactory yang sudah Anda deploy ke Monad Testnet.

5. Setup RainbowKit dan Wagmi

5.1. Konfigurasi App.tsx

Perbarui src/App.tsx untuk mengatur provider dan konfigurasi:

import Header from './components/Header';
import '@rainbow-me/rainbowkit/styles.css';

import {
getDefaultConfig,
RainbowKitProvider,
} from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import {
Chain
} from 'wagmi/chains';
import {
QueryClientProvider,
QueryClient,
} from "@tanstack/react-query";
import { Toaster } from "react-hot-toast";
import Container from "./components/Container";

// Konfigurasi Chain Monad Testnet
const monadTestnet: Chain = {
id: 10143,
name: 'Monad Testnet',
nativeCurrency: {
decimals: 18,
name: 'MON',
symbol: 'MON',
},
rpcUrls: {
default: {
http: ['https://testnet-rpc.monad.xyz/'],
},
public: {
http: ['https://testnet-rpc.monad.xyz/'],
},
},
blockExplorers: {
default: {
name: 'MonadScan',
url: 'https://testnet.monadexplorer.com',
},
},
testnet: true,
};

// eslint-disable-next-line react-refresh/only-export-components
export const config = getDefaultConfig({
appName: 'ERC20 Factory',
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', // Dapatkan dari https://cloud.walletconnect.com
chains: [monadTestnet],
ssr: true,
});

function App() {
const queryClient = new QueryClient();
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<div className="h-screen bg-gray-900 text-white">
<Header />
<Container />
</div>
<Toaster position="top-center" />
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
)
}

export default App

⚠️ Penting: Daftar di WalletConnect Cloud untuk mendapatkan projectId.

6. Membuat Komponen Header

Buat file src/components/Header.tsx untuk menampilkan header dan tombol connect wallet:

import { ConnectButton } from "@rainbow-me/rainbowkit"

const Header = () => {
return (
<header className="bg-gray-800 py-4">
<div className="container mx-auto px-4 flex justify-between items-center">
<div className="text-xl font-bold">Monad Token Factory</div>
<ConnectButton />
</div>
</header>
)
}

export default Header

7. Membuat Form Pembuatan Token

Buat file src/components/TokenForm.tsx:

import { useState } from "react"

const TokenForm = ({handleAddToken}: {
handleAddToken: (e: React.FormEvent, tokenName: string, tokenSymbol: string, tokenSupply: string) => void
}) => {
const [tokenName, setTokenName] = useState("")
const [tokenSymbol, setTokenSymbol] = useState("")
const [tokenSupply, setTokenSupply] = useState("")

return (
<div className="bg-gray-800 p-6 rounded-lg shadow-lg mb-8">
<h2 className="text-2xl font-bold mb-4">Create New Token</h2>
<form onSubmit={(e) => {
handleAddToken(e, tokenName, tokenSymbol, tokenSupply)
setTokenName("");
setTokenSupply("");
setTokenSymbol("");
}}>
<div className="mb-4">
<label htmlFor="tokenName" className="block text-sm font-medium mb-1">
Token Name
</label>
<input
type="text"
id="tokenName"
value={tokenName}
onChange={(e) => setTokenName(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="mb-4">
<label htmlFor="tokenSymbol" className="block text-sm font-medium mb-1">
Token Symbol
</label>
<input
type="text"
id="tokenSymbol"
value={tokenSymbol}
onChange={(e) => setTokenSymbol(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="mb-4">
<label htmlFor="tokenSupply" className="block text-sm font-medium mb-1">
Initial Supply
</label>
<input
type="number"
id="tokenSupply"
value={tokenSupply}
onChange={(e) => setTokenSupply(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button type="submit" className="w-full cursor-pointer bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
Create Token
</button>
</form>
</div>
)
}

export default TokenForm

8. Membuat Komponen untuk Menampilkan Token

Buat file src/components/TokenList.tsx:

type Token = {
name: string;
symbol: string;
supply: number;
address: `0x${string}`;
}

const TokenList = ({tokens} : {tokens: Token[]}) => {
return (
<div className="bg-gray-800 p-6 rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-4">Tokens History</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left bg-gray-700">
<th className="p-2">Name</th>
<th className="p-2">Symbol</th>
<th className="p-2">Supply</th>
<th className="p-2">Address</th>
</tr>
</thead>
<tbody>
{tokens.length === 0 ? (
<tr>
<td colSpan={4} className="p-2 text-center">No tokens created yet</td>
</tr>
) : (
tokens.map((token, index) => (
<tr key={index} className="border-b border-gray-700">
<td className="p-2">{token.name}</td>
<td className="p-2">{token.symbol}</td>
<td className="p-2">{token.supply.toFixed(0)}</td>
<td className="p-2 text-xs md:text-sm">{token.address}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)
}

export default TokenList

9. Implementasi Komponen Container

Buat file src/components/Container.tsx yang akan berisi logika interaksi dengan kontrak:

import { useMemo } from "react"
import toast from "react-hot-toast"
import { useAccount, useReadContract, useReadContracts, useWriteContract } from "wagmi";
import { ERC20_FACTORY_ABI_JSON, ERC20_FACTORY_CONTRACT, ERC20_TOKEN_ABI_JSON } from "../constants";
import { waitForTransactionReceipt } from "@wagmi/core";
import { config } from "../App";
import TokenForm from "./TokenForm";
import TokenList from "./TokenList";
import { Address } from "viem";

export type Token = {
name: string;
symbol: string;
supply: number;
address: `0x${string}`;
}

const erc20FactoryContract = {
address: ERC20_FACTORY_CONTRACT as `0x${string}`,
abi: ERC20_FACTORY_ABI_JSON,
};

const Container = () => {
const { address, isConnected } = useAccount();
const { writeContractAsync } = useWriteContract();

// Membaca data token dari kontrak
const { data: tokenAddresses, refetch } = useReadContract({
...erc20FactoryContract,
functionName: "getAllTokens",
account: address,
query: {
enabled: isConnected,
},
}) as { data: Address[] | undefined, refetch: () => void };

// Menyiapkan konfigurasi untuk batch reading token details
const tokenDetailsConfig = useMemo(() => {
if (!tokenAddresses) return [];

return tokenAddresses.flatMap((tokenAddress) => [
{
abi: ERC20_TOKEN_ABI_JSON,
address: tokenAddress,
functionName: 'name',
},
{
abi: ERC20_TOKEN_ABI_JSON,
address: tokenAddress,
functionName: 'symbol',
},
{
abi: ERC20_TOKEN_ABI_JSON,
address: tokenAddress,
functionName: 'totalSupply',
},
]);
}, [tokenAddresses]);

// Batch read semua detail token
const { data: tokenDetails } = useReadContracts({
contracts: tokenDetailsConfig,
query: {
enabled: Boolean(tokenAddresses?.length),
},
});

// Memproses data detail token
const tokens: Token[] = useMemo(() => {
if (!tokenAddresses || !tokenDetails) return [];

const result: Token[] = [];
for (let i = 0; i < tokenAddresses.length; i++) {
result.push({
address: tokenAddresses[i],
name: String(tokenDetails[i * 3].result),
symbol: String(tokenDetails[i * 3 + 1].result),
supply: Number(tokenDetails[i * 3 + 2].result) / (10 ** 18),
});
}

return result;
}, [tokenAddresses, tokenDetails]);

// Fungsi untuk membuat token baru
async function handleAddToken(e: React.FormEvent, tokenName: string, tokenSymbol: string, tokenSupply: string) {
e.preventDefault()

// Validasi input
if (tokenName.length === 0 || tokenSymbol.length === 0 || tokenSupply.length === 0) {
toast.dismiss();
toast.error("Please fill the form", {
style: {
background: "#2B2F36",
color: "#fff",
},
});
return
}

toast.loading("Submitting Form...", {
style: {
background: "#2B2F36",
color: "#fff",
},
});

try {
// Menulis ke kontrak
const result = await writeContractAsync({
...erc20FactoryContract,
functionName: "createToken",
args: [address, Number(tokenSupply), tokenName, tokenSymbol],
account: address as `0x${string}`,
});

toast.dismiss();
toast.loading("Creating your token...", {
style: {
background: "#2B2F36",
color: "#fff",
},
});

// Menunggu transaksi dikonfirmasi
await waitForTransactionReceipt(config, {
hash: result as `0x${string}`,
});

toast.dismiss();
toast.success("Token Successfully Created!", {
style: {
background: "#2B2F36",
color: "#fff",
},
});

// Refresh data
refetch();
} catch (error) {
toast.dismiss();
toast.error("Failed to create token", {
style: {
background: "#2B2F36",
color: "#fff",
},
});
console.error(error);
}
}

return (
<main className="container mx-auto px-4 md:px-8 lg:px-20 xl:px-60 py-8">
<h1 className="text-4xl font-bold mb-8 text-center">ERC20 Token Factory</h1>
{isConnected ? (
<>
<TokenForm handleAddToken={handleAddToken} />
<TokenList tokens={tokens} />
</>
) : (
<div className="text-center py-10">
<p className="text-xl mb-4">Please connect your wallet to continue</p>
</div>
)}
</main>
)
}

export default Container

10. Menjalankan Aplikasi

Jalankan aplikasi dengan perintah:

npm run dev

Akses aplikasi di http://localhost:5173/.

11. Penjelasan Kode Container

Mari kita pahami bagian-bagian penting dari kode Container.tsx:

11.1. Hooks Wagmi

const { address, isConnected } = useAccount();
const { writeContractAsync } = useWriteContract();
const { data: tokenAddresses, refetch } = useReadContract({...});
const { data: tokenDetails } = useReadContracts({...});
  • useAccount: Mendapatkan informasi alamat wallet yang terhubung
  • useWriteContract: Untuk menulis ke smart contract (membuat token)
  • useReadContract: Untuk membaca data dari contract (daftar token)
  • useReadContracts: Untuk batch reading data dari kontrak

11.2. Batch Reading Token Details

const tokenDetailsConfig = useMemo(() => {
if (!tokenAddresses) return [];

return tokenAddresses.flatMap((tokenAddress) => [
// ... konfigurasi untuk nama, simbol, dan total supply
]);
}, [tokenAddresses]);
  • useMemo: Membuat cache konfigurasi untuk batch reading
  • flatMap: Untuk setiap alamat token, buat 3 konfigurasi (nama, simbol, supply)

11.3. Memproses Data Token

const tokens: Token[] = useMemo(() => {
if (!tokenAddresses || !tokenDetails) return [];

const result: Token[] = [];
for (let i = 0; i < tokenAddresses.length; i++) {
result.push({
address: tokenAddresses[i],
name: String(tokenDetails[i * 3].result),
symbol: String(tokenDetails[i * 3 + 1].result),
supply: Number(tokenDetails[i * 3 + 2].result) / (10 ** 18),
});
}

return result;
}, [tokenAddresses, tokenDetails]);
  • Mengorganisasi Data: Mengkonversi hasil batch reading menjadi array token
  • Konversi Unit: Mengkonversi supply dari wei (10^18) ke unit token yang dapat dibaca

11.4. Membuat Token Baru

async function handleAddToken(e: React.FormEvent, tokenName: string, tokenSymbol: string, tokenSupply: string) {
// ... validasi dan toast loading

try {
// Menulis ke kontrak
const result = await writeContractAsync({
...erc20FactoryContract,
functionName: "createToken",
args: [address, Number(tokenSupply), tokenName, tokenSymbol],
account: address as `0x${string}`,
});

// Menunggu transaksi dikonfirmasi
await waitForTransactionReceipt(config, {
hash: result as `0x${string}`,
});

// ... tampilkan sukses dan refresh data
} catch (error) {
// ... handle error
}
}
  • writeContractAsync: Untuk menulis ke kontrak (membuat token)
  • waitForTransactionReceipt: Menunggu transaksi dikonfirmasi oleh blockchain
  • refetch: Memuat ulang data token setelah transaksi berhasil

12. Optimasi dan Tips Frontend

12.1. Loading States

Tambahkan indikator loading untuk meningkatkan user experience:

// Tambahkan di Container.tsx
const { data: tokenAddresses, refetch, isLoading: isLoadingTokens } = useReadContract({
// ... konfigurasi yang sudah ada
});

// Tampilkan loading state di render
{isLoadingTokens ? (
<div className="text-center py-4">Loading tokens...</div>
) : (
<TokenList tokens={tokens} />
)}

12.2. Error Handling

Tangani error dengan lebih baik:

// Tambahkan di Container.tsx
const { data: tokenAddresses, refetch, isLoading: isLoadingTokens, error } = useReadContract({
// ... konfigurasi yang sudah ada
});

// Tampilkan pesan error jika ada
{error && (
<div className="text-red-500 mb-4">Error loading tokens: {error.message}</div>
)}

12.3. Responsif Design

Pastikan UI responsif untuk berbagai ukuran layar:

// Contoh di Container.tsx
<main className="container mx-auto px-4 md:px-8 lg:px-20 xl:px-60 py-8">
{/* ... konten yang ada */}
</main>

13. Build dan Deploy

13.1. Build untuk Produksi

npm run build

Ini akan menghasilkan folder dist/ dengan file static yang dapat di-deploy.

Kesimpulan

Selamat! Anda telah berhasil membuat frontend interaktif untuk Token Factory menggunakan React, Vite, dan TailwindCSS. Aplikasi ini memungkinkan pengguna untuk:

  1. Terhubung dengan wallet mereka menggunakan RainbowKit
  2. Berinteraksi dengan kontrak TokenFactory yang di-deploy di Monad Testnet
  3. Membuat token ERC20 baru dengan nama, simbol, dan jumlah awal yang kustom
  4. Melihat daftar token yang telah dibuat

Frontend ini memberikan antarmuka yang user-friendly untuk smart contract TokenFactory, membuatnya lebih mudah digunakan oleh pengguna akhir yang mungkin tidak terbiasa dengan berinteraksi langsung dengan blockchain.

Langkah Selanjutnya

Untuk pengembangan lebih lanjut, Anda dapat:

  1. Tambahkan Fitur Transfer: Memungkinkan pengguna mentransfer token yang mereka miliki
  2. Implementasikan Burning UI: Tambahkan UI untuk membakar token
  3. Tambahkan Detail Token: Halaman terpisah untuk melihat detail lengkap token
  4. Tambahkan Fitur Pencarian: Untuk mencari token berdasarkan nama atau alamat
  5. Integrasi dengan Dex: Memungkinkan pengguna menukar token mereka