DApp lengkap adalah aplikasi yang logika dan data pentingnya ada di smart contract, dipakai lewat wallet pengguna, ditampilkan oleh frontend, dengan file besar di IPFS dan riwayat yang dirangkum indexer. Di lesson ini semua bagian dari lesson 1 sampai 17 dirakit jadi satu proyek: Toko Kaos Raka. Bayangin hari pembukaan toko. Ada kasir yang aturannya gak bisa diubah (contract), kartu member di dompet pelanggan (wallet), etalase (frontend), gudang foto katalog (IPFS), buku rekap bulanan (indexer), dan ruang kantor untuk hal yang gak boleh dipajang di buku publik, seperti alamat pengiriman (server sendiri).
🧪 Tebakan cepat: Raka men-deploy KaosEdisiKopi ke Sepolia, baru setelah itu meng-upload metadata KOPI ke IPFS dan dapat CID-nya. Bisakah CID itu dipasang ke contract yang sudah jalan? Tebak dulu, jawabannya ada di bagian "Urutan membangun".
Arsitektur Toko Kaos Raka
| Bagian | Isinya | Alat |
|---|---|---|
| Smart contract | PoinRaka (ERC-20, 1 POIN per Rp 1.000, 100 POIN = potongan Rp 5.000) dan KaosEdisiKopi (ERC-721, maksimal 100) | Solidity, OpenZeppelin 5.6.1, Hardhat 3.19.0 |
| Wallet | kunci pelanggan; menandatangani redeem |
MetaMask atau wallet EIP-6963 lain |
| Frontend | halaman "POIN kamu" dan tombol tukar | React, wagmi 3.7.7, viem 2.57.4 |
| IPFS | metadata dan gambar KOPI, ditunjuk lewat ipfs:// |
kubo, layanan pinning |
| Indexer | ranking pemegang POIN, riwayat Transfer, pemilik KOPI | subgraph (The Graph) atau Ponder |
| Server sendiri (off-chain) | alamat pengiriman, notifikasi, cache | backend biasa, tanpa private key pengguna |
Alurnya: frontend membaca lewat RPC atau indexer, wallet menandatangani transaksi, contract mengubah state dan memancarkan event, indexer membaca event itu, dan frontend menampilkan hasilnya.
Checklist lesson 1 sampai 17
| # | Slug lesson | Yang dipakai di proyek ini |
|---|---|---|
| 1 | blockchain-fundamentals |
data on-chain publik dan permanen, jadi data pribadi tetap off-chain |
| 2 | ethereum-smart-contracts |
akun, gas, chain lokal 31337 dan Sepolia 11155111 |
| 3 | wallet-metamask-integration |
menghubungkan wallet, pindah chain, login SIWE |
| 4 | solidity-basics |
state, event, custom error di PoinRaka |
| 5 | ethers-js-viem |
client, calldata, parseUnits('100', 18) |
| 6 | reading-blockchain-data |
eth_call, getLogs, latest vs finalized |
| 7 | sending-transactions |
receipt, revert tetap bayar gas, simulasi dulu |
| 8 | erc20-token |
PoinRaka versi ERC-20, approve secukupnya |
| 9 | erc721-nft |
KaosEdisiKopi, _safeMint, SoldOut() di #101 |
| 10 | dapp-frontend-wagmi-react |
komponen "POIN kamu" dengan wagmi |
| 11 | smart-contract-testing-hardhat |
11 test, fixture, coverage |
| 12 | ipfs-decentralized-storage |
folder metadata kopi/ dan CID-nya di _baseURI |
| 13 | defi-basics-untuk-devs |
belum dipakai toko ini; perlu kalau POIN masuk kolam AMM (slippage, oracle) |
| 14 | gas-optimization |
ukur gas, optimizer untuk deploy |
| 15 | smart-contract-security |
access control, Slither, audit |
| 16 | the-graph-indexing |
subgraph untuk Account, Transfer dan Kaos |
| 17 | l2-scaling-solutions |
pilihan deploy ke L2 yang lebih murah |
Urutan membangun
- Contract + test di chain lokal (lesson 4, 8, 9, 11):
npx hardhat testharus hijau, termasuk jalur gagal. - Metadata ke IPFS (lesson 12): upload folder
kopi/, catat CID-nya, tulis di_baseURI. - Deploy lokal + script alur toko (lesson 7): bagian berikut.
- Frontend ke chain lokal (lesson 10), alamat contract dibaca dari file hasil deploy.
- Indexer (lesson 16) mulai dari block deploy.
- Testnet: Sepolia, atau L2 testnet seperti OP Sepolia (lesson 17), lalu verifikasi source code.
- Persiapan mainnet: bagian "Sebelum mainnet".
Kenapa IPFS sebelum deploy? _baseURI di KaosEdisiKopi adalah fungsi pure yang mengembalikan string tetap, dan contract yang sudah di-deploy gak bisa diedit. CID yang baru didapat setelah deploy gak bisa dipasang; Raka harus deploy ulang dengan alamat baru. Itu jawaban tebakannya.
Jalankan semuanya di chain lokal
// scripts/deploy.ts: deploy PoinRaka dan KaosEdisiKopi, lalu simpan alamatnya untuk script, frontend dan subgraph
import { writeFileSync } from "node:fs";
import { network } from "hardhat";
import { getAddress } from "viem";
const { viem, networkName } = await network.getOrCreate();
const publicClient = await viem.getPublicClient();
const [raka] = await viem.getWalletClients();
const poin = await viem.sendDeploymentTransaction("PoinRaka");
const poinReceipt = await publicClient.waitForTransactionReceipt({ hash: poin.deploymentTransaction.hash });
const kopi = await viem.sendDeploymentTransaction("KaosEdisiKopi");
const kopiReceipt = await publicClient.waitForTransactionReceipt({ hash: kopi.deploymentTransaction.hash });
console.log(`network ${networkName}, chain ID ${await publicClient.getChainId()}, deployer ${getAddress(raka.account.address)}`);
console.log(`PoinRaka ${poin.contract.address} block ${poinReceipt.blockNumber} gas ${poinReceipt.gasUsed}`);
console.log(`KaosEdisiKopi ${kopi.contract.address} block ${kopiReceipt.blockNumber} gas ${kopiReceipt.gasUsed}`);
writeFileSync(
`deployments/${networkName}.json`,
JSON.stringify({ PoinRaka: poin.contract.address, KaosEdisiKopi: kopi.contract.address, startBlock: Number(poinReceipt.blockNumber) }, null, 2),
);
Script kedua, scripts/toko.ts, membaca deployments/localhost.json, lalu menjalankan satu hari di toko: empat belanja (earn), mint KOPI #1 untuk Dinda dan #2 untuk Yoga, Dinda menukar 100 POIN, dan Yoga mencoba menukar 200 lewat simulate (jadi transaksi yang pasti gagal gak dikirim). Ini output sungguhan dari Hardhat 3.19.0, chain lokal yang baru (akun test publik, gak ada uang asli):
$ npx hardhat test
11 passing (2 solidity, 9 nodejs)
$ npx hardhat node # terminal lain: chain lokal 31337
$ npx hardhat run scripts/deploy.ts --network localhost
network localhost, chain ID 31337, deployer 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
PoinRaka 0x5FbDB2315678afecb367f032d93F642f64180aa3 block 1 gas 1229456
KaosEdisiKopi 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 block 2 gas 1999101
$ npx hardhat run scripts/toko.ts --network localhost
block 3 earn(Dinda, 95000) Kaos Sablon Kopi gas 73216 success
block 4 earn(Yoga, 75000) Kaos Polos Hitam gas 56104 success
block 5 earn(Yoga, 40000) Topi Raka gas 38992 success
block 6 earn(Dinda, 65000) Mug Sablon Kopi gas 39004 success
block 7 mint KOPI #1 untuk Dinda gas 96559 success
block 8 mint KOPI #2 untuk Yoga gas 79447 success
block 9 Dinda redeem 100 POIN gas 35679 success
Error: ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)
(0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC, 115000000000000000000, 200000000000000000000)
POIN Dinda 60, Yoga 115, totalSupply 175
KOPI #1 milik 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (Dinda)
tokenURI(1) = ipfs://bafybeifuwwkzdzittkxf5ghlmsq2k6jqasyheos6mb3vgz4l73qcmsgfku/1
Transfer PoinRaka: 5 log, head block 9
- Alamatnya sama dengan lesson sebelumnya karena alamat contract cuma bergantung pada deployer dan nonce: transaksi ke-0 dan ke-1 Raka.
redeemmembakar 100 POIN, jaditotalSupplyturun dari 275 ke 175. Gas-nya 35.679, beda dengan 33.946 di PoinRaka versi pertama, karena kodenya beda (OpenZeppelin).- Yoga gagal di simulasi, sebelum ada gas yang terbuang. Di frontend, tombolnya cukup menampilkan pesan itu.
- 5 log Transfer (4 mint + 1 burn) adalah bahan indexer.
Frontend, indexer, dan L2
Frontend memakai komponen "POIN kamu" dari lesson dapp-frontend-wagmi-react apa adanya; yang berubah cuma daftar chain dan alamat:
import { createConfig, http } from 'wagmi'
import { hardhat, sepolia, optimismSepolia } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'
export const config = createConfig({
chains: [hardhat, sepolia, optimismSepolia],
connectors: [injected()],
transports: { [hardhat.id]: http(), [sepolia.id]: http(), [optimismSepolia.id]: http() },
})
Subgraph dari lesson the-graph-indexing ditambah satu data source untuk KOPI (alamat 0xe7f1...0512, startBlock: 2, event Transfer(indexed address,indexed address,indexed uint256)) dengan entity Kaos { id, owner, mintedAtBlock }; graph build dan test Matchstick-nya lulus. Untuk L2, tambahkan network opSepolia di hardhat.config.ts (lesson l2-scaling-solutions), jalankan npx hardhat run scripts/deploy.ts --network opSepolia, dan deployments/opSepolia.json terisi alamatnya. Simpan alamat per chain ID, jangan satu konstanta.
Kapan butuh server sendiri
Contract cocok untuk aturan yang harus dipercaya semua orang: siapa punya berapa POIN, siapa pemilik KOPI #1. Server biasa (off-chain) tetap dibutuhkan untuk:
- Data yang gak perlu on-chain atau gak boleh publik. Alamat pengiriman dan nomor HP Dinda tersimpan di database toko, bukan di contract.
- Notifikasi. Server mendengarkan event
Earnedlalu mengirim pesan "POIN kamu bertambah". - Cache dan query cepat. Hasil indexer atau RPC disimpan sebentar supaya halaman cepat.
Yang gak boleh: menyimpan private key pengguna atau menandatangani atas nama mereka. Itu tugas wallet.
Sebelum mainnet
Tutorial ini berhenti di testnet. Kalau suatu hari contract ini memegang nilai sungguhan:
- Test dan review: jalur gagal teruji, coverage dicek, Slither bersih, lalu audit oleh pihak luar (lesson
smart-contract-security). - Verifikasi source code:
npx hardhat verify --network sepolia <alamat>(API key Etherscan dari keystore), supaya siapa pun bisa membaca kode yang benar-benar di-deploy. - Kunci owner: kunci yang memanggil
earndanmintgak boleh ada di file.envlaptop. Simpan di hardware wallet, atau pindahkan kepemilikan ke multisig (misalnya Safe) dengantransferOwnership. - Kunci terpisah: kunci testnet gak pernah dipakai di mainnet, dan sebaliknya.
- Pantau dan siapkan respons: alert untuk event yang aneh, kontak darurat, dan bug bounty.
- Kalau pakai L2: cek stage-nya di L2BEAT dan risiko bridge-nya (lesson
l2-scaling-solutions).
⚠️ Jebakan yang sering ketemu
- Deploy KOPI sebelum metadata siap.
_baseURIgak bisa diganti; CID harus ada sebelum deploy. - Alamat contract di-hardcode untuk satu chain. Frontend yang pindah ke Sepolia masih memanggil alamat lokal. Simpan alamat per chain ID dari file hasil deploy.
- Kunci owner di
.envyang ikut ter-commit. Pakainpx hardhat keystore setuntuk testnet, hardware wallet atau multisig untuk mainnet. - Mengirim transaksi tanpa simulasi. Yoga yang menukar 200 POIN akan membayar gas untuk revert; simulasi dulu dan tampilkan pesannya.
TL;DR: DApp lengkap Toko Kaos Raka terdiri dari smart contract (PoinRaka ERC-20 dan KaosEdisiKopi ERC-721) sebagai backend yang dipercaya, wallet untuk menandatangani, frontend React + wagmi, metadata di IPFS, indexer untuk ranking dan riwayat, dan server biasa untuk data yang gak perlu on-chain, notifikasi dan cache (tanpa pernah memegang private key pengguna). Urutannya: contract + test, metadata IPFS (CID harus ada sebelum deploy karena _baseURI gak bisa diganti), deploy lokal + script, frontend, indexer, testnet (Sepolia atau L2 testnet) + verifikasi. Di chain lokal: 11 test lulus, PoinRaka di 0x5FbD...0aa3, KaosEdisiKopi di 0xe7f1...0512, Dinda tinggal 60 POIN setelah redeem, Yoga ditolak ERC20InsufficientBalance di simulasi. Sebelum mainnet: audit, verifikasi source, kunci owner di hardware wallet atau multisig.