Frontend - Connect dApp dengan Wallet dan Contract
FE Integration
Welcome ges! Sebelum kita benar-benar nyemplung ke bagian integrasi frontend ↔ smart contract, kita wajib beresin dulu pondasinya. Ibarat bangun rumah, jangan langsung pasang atap kalau fondasi masih amburadul. Jadi di section ini kita akan bikin project FE yang clean, siap dipakai, dan gampang di-scale. Tenang, semua langkah di sini ringan—tinggal ikutin urutannya. Ambil kopi / teh dulu kalau perlu, terus gas. 👇
Initialize Project
npx create-react-router@latest crowdfund
Setelah command itu jalan, CLI bakal nanya beberapa hal standar. Ini semacam onboarding biar struktur project kamu langsung rapih dari awal. Jawabannya tinggal pilih opsi yang aman dan umum dipakai di kebanyakan setup.
Rekomendasi jawaban (boleh copas mental):
Initialize a new git repository? Yes
Install dependencies with npm? Yes
Kalau proses bootstrap udah selesai (biasanya keliatan dari dependency install selesai tanpa error), langsung pindah ke folder project supaya semua perintah selanjutnya context-nya tepat:
cd crowdfund
Next step: kita setup design system. Kenapa? Supaya nggak tiap kali bikin tombol harus mikir styling dari nol. Dengan design system kamu dapet komponen yang konsisten, gampang di-custom, dan hemat waktu. Ini saving mental energy banget.
npx shadcn@latest init
CLI bakal minta pilih tema. Pilih aja yang paling cocok sama vibe project kamu sekarang—semua bisa diubah belakangan kok. Setelah pilih, pencet enter dan beres.
Sekarang tes apakah integrasi design system jalan dengan nambah satu komponen paling basic: button. Kalau ini sukses tampil di UI nanti, artinya environment kamu udah ready untuk lanjut.
npx shadcn@latest add button
Adding Component
Daripada nanti bolak-balik nambah satu-satu di tengah development, kita siapin dulu beberapa komponen inti yang bakal sering dipakai. Ini bikin experience ngoding selanjutnya jauh lebih halus.
npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/text-rotate.json"
npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/card.json"
npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/input.json"
Komponen dasar sudah mendarat. Sekarang waktunya ngatur layout. Tujuannya biar semua halaman punya struktur konsisten (header, container content, dll) dan kamu nggak copy-paste markup terus-menerus.
Layouting
Pertama kita bikin komponen Header. Ini bakal jadi identitas branding kecil di atas halaman. Simpan di app/components/header.tsx.
import { NavLink } from "react-router";
export function Header() {
return (
<div className="flex flex-row items-center justify-between mb-20 mt-8 px-[50px]">
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight flex items-center">
<NavLink to="/" className="hover:underline">
Crowdfund
</NavLink>
.
</h2>
</div>
);
}
Lanjut bikin file _layouts.tsx di app/routes. File ini acting sebagai wrapper global—tempat semua route kamu dirender di dalam struktur yang sama.
import { Outlet } from "react-router";
import { Header } from "~/components/header";
import { cn } from "~/lib/utils";
export default function Layout() {
return (
<div className="overflow-x-hidden">
<Header />
<div className={cn("mt-36", "overflow-hidden px-[50px]")}>
<Outlet />
</div>
</div>
);
}
Setelah komponen layout siap, kita perlu daftarin dia di konfigurasi routing supaya React Router tau hierarki mana yang harus dipakai. File target: app/routes.ts.
Final expected content-nya kurang lebih begini (disesuaikan sama kebutuhan kalau kamu mau extend di masa depan):
import { type RouteConfig, index, layout } from "@react-router/dev/routes";
export default [
layout("routes/_layouts.tsx", [
index("routes/home.tsx")
])
] satisfies RouteConfig;
Saatnya dry run—jalankan dev server untuk verifikasi semua konfigurasi dasar aman.
npm run dev
Kalau server udah nyala tanpa error, buka di browser: http://localhost
. Harusnya tampil halaman kosong dengan layout header yang tadi kamu bikin.Anti Light (optional)
Kalau kamu tim dark mode sejati dan nggak mau lihat putih silau, bisa force dark dari root.tsx. Caranya tinggal pasang class dark di HTML root. Ini bikin semua variant dark Tailwind kamu langsung aktif.
...
<html ... className="dark">
...
Untuk memastikan styling dark bisa nge-cover semua nested element, tambahin variant di app/app.css kayak di bawah. Ini pattern yang sering dipakai supaya kompatibel sama komponen-komponen eksternal juga.
/* @custom-variant dark (&:is(.dark *)); */
@variant dark (&:where(.dark, .dark *));
Wallet Integration
Masuk ke fase yang lebih seru: wallet integration. Bagian ini bikin aplikasi kamu bisa aware sama user yang connect, address mereka, dan balance. Kita tinggal tarik beberapa komponen helper biar nggak implement semuanya manual.
npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/use-wallet.json"
npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/use-native-balance.json"
npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/connect-wallet.json"
npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/wallet.client.json"
Sekarang modifikasi header.tsx buat nampilin tombol connect wallet. Ini entry point interaksi user sama ekosistem Stellar di aplikasi kamu.
...
</h2>
<ConnectWallet />
</div>
);
}
Kalau ada import yang merah atau belum terselesaikan, gunakan shortcut auto-fix: Ctrl + . atau Cmd + .. Hemat waktu, no drama.
Edit file home.tsx untuk mulai baca balance native (XLM) dan nantinya bisa kirim donasi. Ini semacam halaman showcase tempat semua fitur awal digabung.
import { Card } from "~/components/card";
import type { Route } from "./+types/home";
import { TextRotate } from "~/components/text-rotate";
import { Donut } from "lucide-react";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { useWallet } from "~/hooks/use-wallet";
import { useNativeBalance } from "~/hooks/use-native-balance";
import { useEffect } from "react";
export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}
export default function Home() {
const { address, isConnected } = useWallet();
const { balance } = useNativeBalance(address);
useEffect(() => {
console.log("home update");
}, [balance]);
return (
<div className="flex flex-col items-center gap-y-16">
<div className="flex flex-row items-center gap-x-6">
<p className="text-4xl">Learning</p>
<TextRotate
texts={["Stellar", "Rust", "Contract", "Frontend"]}
mainClassName="bg-white text-black rounded-lg text-4xl px-6 py-3"
transition={{ type: "spring", damping: 30, stiffness: 400 }}
rotationInterval={2000}
/>
</div>
<Card className="flex flex-col gap-y-6 py-4 px-8 w-1/3">
<p className="flex flex-row items-center gap-x-2 text-lg mb-6 font-medium">
<Donut className="size-5" />
Donate {balance}
</p>
<div className="flex flex-row justify-between items-center">
<div className="flex flex-row items-center gap-4">
<img src="https://placehold.co/10" className="size-10 rounded-full" />
<p>XLM</p>
</div>
<p className="tabular-nums flex gap-1">
{!isConnected && <span>Connect wallet</span>}
{isConnected && balance === "-" && <span>-</span>}
{isConnected && balance !== "-" && (
<>
<span>{balance}</span>
<span>XLM</span>
</>
)}
</p>
</div>
<Input
type="number"
step="any"
placeholder="0.00"
/>
<Button className="w-max">Submit</Button>
</Card>
</div>
);
}
modify vite.config.ts
export default defineConfig({
...
define: {
global: "window",
},
});
Setelah ini kamu bisa coba connect wallet di UI, liat balance real-time, dan siapin flow donasi. Begitu tombol submit sudah hidup, kita tinggal lanjut ke integrasi contract buat proses benerannya. 💸
Contract Integration
Waktunya ngobrol langsung sama smart contract. Sebelum bisa dipakai dari FE, kita perlu generate binding TypeScript supaya interaksi ke method contract jadi strongly-typed dan nyaman dipakai.
Masuk ke folder tempat kamu menyimpan source contract (biasanya di repo terpisah atau monorepo section khusus).
stellar contract bindings typescript --network testnet --contract-id <contract_id> --output-dir packages/<contract_id>
Penting: Pastikan nilai
--contract-id(saat generate binding) identik dengan--aliasyang kamu pakai ketika deploy. Kalau beda, FE kamu bakal nge-call ID yang salah dan request-nya mental.
Masuk ke folder packages/<contract_id> hasil generate tadi, lalu install dependencies dan build supaya output siap dipakai.
npm i && npm run build
Folder hasil generate (berisi client + jaringan testnet config) tinggal kamu copy ke project FE di path: <fe_project>/packages/<contract_id>. Ini bikin FE bisa langsung instantiate client tanpa tambahan konfigurasi berat.
kita butuh hooks lagi nih, add dulu yach
npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/use-submit-transaction.json"
Lanjut ubah routes/home.tsx untuk mulai bikin instance contract client dan nyiapin fungsi-fungsi kayak donate dan initialize. Di sini mulai kerasa integrasi real-nya.
import { Card } from "~/components/card";
import type { Route } from "./+types/home";
import { TextRotate } from "~/components/text-rotate";
import { Donut } from "lucide-react";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { useWallet } from "~/hooks/use-wallet";
import { useNativeBalance } from "~/hooks/use-native-balance";
import { useSubmitTransaction } from "~/hooks/use-submit-transaction";
import * as Crowdfund from "../../packages/<contract_id>";
import { signTransaction } from "~/config/wallet.client";
import { useState, useMemo, useEffect } from "react";
export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}
export default function Home() {
const RPC_URL = "https://soroban-testnet.stellar.org:443";
const { address, isConnected } = useWallet();
const { balance, refetch: refetchBalance } = useNativeBalance(address);
const [amount, setAmount] = useState<string>("");
const [total, setTotal] = useState(0);
const [previousTotal, setPreviousTotal] = useState(0);
const contract = useMemo(() => {
if (!isConnected || address === "-") return null;
return new Crowdfund.Client({
...Crowdfund.networks.testnet,
rpcUrl: RPC_URL,
signTransaction,
publicKey: address,
});
}, [isConnected, address]);
const { submit, isSubmitting } = useSubmitTransaction({
rpcUrl: RPC_URL,
networkPassphrase: Crowdfund.networks.testnet.networkPassphrase,
onSuccess: handleOnSuccess,
onError: (error) => {
console.error("Donation failed", error);
},
});
async function handleOnSuccess() {
// Fetch updated total
if (contract) {
setPreviousTotal(total);
const totalTx = await contract.get_total_raised();
const updated = BigInt(totalTx.result as any);
setTotal(Number(updated));
}
await refetchBalance();
setAmount("");
}
async function handleSubmit() {
if (!isConnected || !contract) return;
if (!amount.trim()) return;
try {
// Convert XLM to stroops (multiply by 10^7)
const xlmAmount = parseFloat(amount.trim());
const stroopsAmount = Math.floor(xlmAmount * 10_000_000);
const tx = await contract.donate({
donor: address,
amount: BigInt(stroopsAmount),
}) as any;
await submit(tx);
} catch (e) {
console.error("Failed to create donation transaction", e);
}
}
useEffect(() => {
if (!contract) return;
(async () => {
try {
const tx = await contract.get_total_raised();
const total = Number(BigInt(tx.result));
setTotal(total);
} catch (err) {
setTotal(0);
}
})();
}, [contract]);
return (
<div className="flex flex-col items-center gap-y-16">
<div className="flex flex-row items-center gap-x-6">
<p className="text-4xl">Learning</p>
<TextRotate
texts={["Stellar", "Rust", "Contract", "Frontend"]}
mainClassName="bg-white text-black rounded-lg text-4xl px-6 py-3"
transition={{ type: "spring", damping: 30, stiffness: 400 }}
rotationInterval={2000}
/>
</div>
<Card className="flex flex-col gap-y-6 py-4 px-8 w-1/3">
<p className="flex flex-row items-center gap-x-2 text-lg mb-6 font-medium">
<Donut className="size-5" />
Donate {balance}
</p>
<div className="flex flex-row justify-between items-center">
<div className="flex flex-row items-center gap-4">
<img src="https://placehold.co/10" className="size-10 rounded-full" />
<p>XLM</p>
</div>
<p className="tabular-nums flex gap-1">
{!isConnected && <span>Connect wallet</span>}
{isConnected && balance === "-" && <span>-</span>}
{isConnected && balance !== "-" && (
<>
<span>{balance}</span>
<span>XLM</span>
</>
)}
</p>
</div>
<Input
type="text"
inputMode="decimal"
placeholder="0.001"
onChange={(e) => setAmount(e.target.value)}
value={amount}
disabled={isSubmitting}
/>
<Button
className="w-max"
onClick={handleSubmit}
disabled={!isConnected || isSubmitting || !amount.trim()}
>
{isSubmitting ? "Donating..." : "Submit"}
</Button>
</Card>
<div className="flex flex-col items-center gap-2">
<p>Total Donations</p>
<p>{(total / 10_000_000).toFixed(2)} XLM</p>
{previousTotal > 0 && previousTotal !== total && (
<p className="text-sm text-green-600">
+{((total - previousTotal) / 10_000_000).toFixed(7)} XLM added
</p>
)}
</div>
</div>
);
}
Sekarang flow-nya lengkap: connect wallet → (optional) initialize contract → isi amount → submit donasi → lihat total update. Kalau ini sudah jalan mulus, berarti pondasi FE + contract integration kamu sudah solid. Next tinggal styling lanjutan, validasi form, dan mungkin notifikasi UX. Gas lanjut! 🚀