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.
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:
- Terhubung dengan wallet mereka menggunakan RainbowKit
- Berinteraksi dengan kontrak TokenFactory yang di-deploy di Monad Testnet
- Membuat token ERC20 baru dengan nama, simbol, dan jumlah awal yang kustom
- 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:
- Tambahkan Fitur Transfer: Memungkinkan pengguna mentransfer token yang mereka miliki
- Implementasikan Burning UI: Tambahkan UI untuk membakar token
- Tambahkan Detail Token: Halaman terpisah untuk melihat detail lengkap token
- Tambahkan Fitur Pencarian: Untuk mencari token berdasarkan nama atau alamat
- Integrasi dengan Dex: Memungkinkan pengguna menukar token mereka