starkzap-starter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ :root {
8
+ --stark-glow: 0 0 40px rgba(26, 71, 251, 0.15);
9
+ }
10
+
11
+ * {
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ html {
16
+ scroll-behavior: smooth;
17
+ }
18
+
19
+ body {
20
+ background-color: #04060f;
21
+ color: #e2e8ff;
22
+ font-family: 'DM Sans', sans-serif;
23
+ -webkit-font-smoothing: antialiased;
24
+ }
25
+
26
+ /* Subtle animated grid background */
27
+ .grid-bg {
28
+ background-image:
29
+ linear-gradient(rgba(26, 71, 251, 0.04) 1px, transparent 1px),
30
+ linear-gradient(90deg, rgba(26, 71, 251, 0.04) 1px, transparent 1px);
31
+ background-size: 40px 40px;
32
+ }
33
+
34
+ /* Glowing border on focus */
35
+ .glow-border:focus-within {
36
+ box-shadow: 0 0 0 2px rgba(26, 71, 251, 0.4);
37
+ }
38
+
39
+ @layer utilities {
40
+ .text-gradient {
41
+ background: linear-gradient(135deg, #86a8ff 0%, #ffffff 50%, #ff6b35 100%);
42
+ -webkit-background-clip: text;
43
+ -webkit-text-fill-color: transparent;
44
+ background-clip: text;
45
+ }
46
+ }
@@ -0,0 +1,21 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Starkzap Starter",
6
+ description:
7
+ "Next.js 14 starter kit for building on Starknet with the Starkzap SDK — wallet connect, gasless transactions, token balances, and payment UI out of the box.",
8
+ keywords: ["starknet", "starkzap", "web3", "blockchain", "defi", "nextjs"],
9
+ };
10
+
11
+ export default function RootLayout({
12
+ children,
13
+ }: {
14
+ children: React.ReactNode;
15
+ }) {
16
+ return (
17
+ <html lang="en">
18
+ <body className="min-h-screen grid-bg antialiased">{children}</body>
19
+ </html>
20
+ );
21
+ }
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ import { useWallet } from "@/hooks/useWallet";
4
+ import { WalletButton } from "@/components/wallet/WalletButton";
5
+ import { TokenBalanceCard } from "@/components/wallet/TokenBalanceCard";
6
+ import { PaymentForm } from "@/components/payment/PaymentForm";
7
+ import { formatAddress } from "@/lib/utils";
8
+ import { network } from "@/lib/starkzap";
9
+
10
+ export default function Home() {
11
+ const walletState = useWallet();
12
+ const { wallet, address, error: walletError } = walletState;
13
+
14
+ return (
15
+ <div className="min-h-screen flex flex-col">
16
+ {/* ── Header ─────────────────────────────────────────── */}
17
+ <header className="border-b border-stark-900/80 bg-stark-950/50 backdrop-blur-md sticky top-0 z-50">
18
+ <div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
19
+ <div className="flex items-center gap-3">
20
+ <span className="text-xl">⚡</span>
21
+ <span className="font-mono font-semibold text-white tracking-tight">
22
+ starkzap<span className="text-stark-400">-starter</span>
23
+ </span>
24
+ <span className="rounded-full bg-stark-900 border border-stark-800 px-2.5 py-0.5 text-xs font-mono text-stark-400">
25
+ {network}
26
+ </span>
27
+ </div>
28
+ <WalletButton walletState={walletState} />
29
+ </div>
30
+ </header>
31
+
32
+ {/* ── Hero ───────────────────────────────────────────── */}
33
+ <section className="mx-auto max-w-5xl px-6 py-16 text-center animate-fade-in">
34
+ <p className="mb-3 text-xs font-mono uppercase tracking-[0.2em] text-stark-500">
35
+ Next.js 14 · App Router · TypeScript
36
+ </p>
37
+ <h1 className="text-5xl font-bold leading-tight text-gradient mb-5">
38
+ Build on Starknet.
39
+ <br />
40
+ Ship in minutes.
41
+ </h1>
42
+ <p className="mx-auto max-w-xl text-stark-400 text-lg leading-relaxed">
43
+ A production-ready starter kit powered by the{" "}
44
+ <a
45
+ href="https://docs.starknet.io/build/starkzap/overview"
46
+ target="_blank"
47
+ rel="noopener noreferrer"
48
+ className="text-stark-300 underline decoration-stark-700 hover:decoration-stark-400 transition-colors"
49
+ >
50
+ Starkzap SDK
51
+ </a>
52
+ . Wallet connect, gasless transactions, token balances, and a payment UI — all wired up.
53
+ </p>
54
+
55
+ {walletError && (
56
+ <div className="mx-auto mt-6 max-w-md rounded-xl border border-red-900 bg-red-950/40 px-4 py-3 text-sm text-red-400">
57
+ {walletError}
58
+ </div>
59
+ )}
60
+ </section>
61
+
62
+ {/* ── Main Demo ──────────────────────────────────────── */}
63
+ <main className="mx-auto w-full max-w-5xl flex-1 px-6 pb-20">
64
+
65
+ {!address ? (
66
+ /* ── Not connected state ── */
67
+ <div className="rounded-2xl border border-dashed border-stark-800 bg-stark-950/40 p-12 text-center">
68
+ <p className="text-4xl mb-4">🔌</p>
69
+ <p className="font-semibold text-stark-300 mb-2">Connect your wallet to get started</p>
70
+ <p className="text-sm text-stark-600 mb-6">
71
+ Supports Argent X and Braavos. Social login via Privy available — see README.
72
+ </p>
73
+ <WalletButton walletState={walletState} className="mx-auto" />
74
+ </div>
75
+ ) : (
76
+ /* ── Connected state ── */
77
+ <div className="animate-fade-in space-y-6">
78
+
79
+ {/* Address banner */}
80
+ <div className="flex items-center gap-3 rounded-xl border border-stark-800 bg-stark-900/40 px-4 py-3">
81
+ <span className="h-2 w-2 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.6)]" />
82
+ <span className="text-sm text-stark-400">Connected as</span>
83
+ <span className="font-mono text-sm text-white">{address}</span>
84
+ <a
85
+ href={`https://${network === "mainnet" ? "" : "sepolia."}starkscan.co/address/${address}`}
86
+ target="_blank"
87
+ rel="noopener noreferrer"
88
+ className="ml-auto text-xs text-stark-600 hover:text-stark-300 transition-colors"
89
+ >
90
+ View on explorer ↗
91
+ </a>
92
+ </div>
93
+
94
+ {/* Balances + Payment side by side */}
95
+ <div className="grid gap-6 md:grid-cols-2">
96
+
97
+ {/* Left: token balances */}
98
+ <div className="space-y-4">
99
+ <h2 className="text-xs font-mono uppercase tracking-widest text-stark-500">
100
+ Token Balances
101
+ </h2>
102
+ <TokenBalanceCard wallet={wallet} symbol="STRK" />
103
+ <TokenBalanceCard wallet={wallet} symbol="ETH" />
104
+ <TokenBalanceCard wallet={wallet} symbol="USDC" />
105
+ </div>
106
+
107
+ {/* Right: gasless payment form */}
108
+ <div className="space-y-4">
109
+ <h2 className="text-xs font-mono uppercase tracking-widest text-stark-500">
110
+ Gasless Payment
111
+ </h2>
112
+ <PaymentForm wallet={wallet} />
113
+ </div>
114
+ </div>
115
+
116
+ {/* Guides / code snippets */}
117
+ <div className="rounded-2xl border border-stark-800 bg-stark-950/60 p-6 space-y-4">
118
+ <h2 className="text-sm font-mono uppercase tracking-widest text-stark-500">
119
+ What's included
120
+ </h2>
121
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
122
+ {[
123
+ { icon: "🔑", title: "Wallet Connect", desc: "Argent X, Braavos, or Privy social login" },
124
+ { icon: "⛽", title: "Gasless Transfers", desc: "AVNU paymaster sponsors user gas fees" },
125
+ { icon: "💰", title: "Token Balances", desc: "STRK, ETH, USDC with live refresh" },
126
+ { icon: "💸", title: "Payment UI", desc: "Ready-to-use send form with status flow" },
127
+ ].map((item) => (
128
+ <div
129
+ key={item.title}
130
+ className="rounded-xl border border-stark-800 bg-stark-900/40 p-4"
131
+ >
132
+ <div className="text-2xl mb-2">{item.icon}</div>
133
+ <p className="font-semibold text-stark-200 text-sm mb-1">{item.title}</p>
134
+ <p className="text-xs text-stark-500 leading-relaxed">{item.desc}</p>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </div>
139
+
140
+ </div>
141
+ )}
142
+ </main>
143
+
144
+ {/* ── Footer ─────────────────────────────────────────── */}
145
+ <footer className="border-t border-stark-900 py-6 text-center text-xs text-stark-700 font-mono">
146
+ Built with{" "}
147
+ <a
148
+ href="https://github.com/keep-starknet-strange/starkzap"
149
+ target="_blank"
150
+ rel="noopener noreferrer"
151
+ className="text-stark-500 hover:text-stark-300 transition-colors"
152
+ >
153
+ Starkzap SDK
154
+ </a>{" "}
155
+ · Deployed on Starknet {network}
156
+ </footer>
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,158 @@
1
+ "use client";
2
+
3
+ /**
4
+ * components/payment/PaymentForm.tsx
5
+ *
6
+ * A complete gasless payment form.
7
+ * Lets users send STRK / ETH / USDC to any Starknet address — no gas required.
8
+ */
9
+
10
+ import { useState } from "react";
11
+ import { useGaslessTransfer } from "@/hooks/useGaslessTransfer";
12
+ import type { WalletState } from "@/hooks/useWallet";
13
+ import { cn, explorerUrl } from "@/lib/utils";
14
+ import { network } from "@/lib/starkzap";
15
+
16
+ const TOKENS = ["STRK", "ETH", "USDC"] as const;
17
+
18
+ type Props = {
19
+ wallet: WalletState["wallet"];
20
+ className?: string;
21
+ };
22
+
23
+ export function PaymentForm({ wallet, className }: Props) {
24
+ const [to, setTo] = useState("");
25
+ const [amount, setAmount] = useState("");
26
+ const [symbol, setSymbol] = useState<"STRK" | "ETH" | "USDC">("STRK");
27
+
28
+ const { send, txHash, isPending, isSuccess, error, reset } =
29
+ useGaslessTransfer(wallet);
30
+
31
+ const isValid =
32
+ to.startsWith("0x") && to.length >= 60 && Number(amount) > 0;
33
+
34
+ const handleSubmit = async (e: React.FormEvent) => {
35
+ e.preventDefault();
36
+ if (!isValid) return;
37
+ await send({ to, amount, symbol });
38
+ };
39
+
40
+ if (isSuccess && txHash) {
41
+ return (
42
+ <div
43
+ className={cn(
44
+ "rounded-2xl border border-emerald-800 bg-emerald-950/40 p-6 text-center animate-fade-in",
45
+ className
46
+ )}
47
+ >
48
+ <div className="text-4xl mb-3">✓</div>
49
+ <p className="font-semibold text-emerald-300 mb-1">Payment sent!</p>
50
+ <p className="text-sm text-stark-400 mb-4">
51
+ {amount} {symbol} sent gaslessly
52
+ </p>
53
+ <a
54
+ href={explorerUrl(txHash, "tx", network)}
55
+ target="_blank"
56
+ rel="noopener noreferrer"
57
+ className="text-xs text-stark-400 underline hover:text-stark-200 font-mono"
58
+ >
59
+ View on Starkscan ↗
60
+ </a>
61
+ <button
62
+ onClick={reset}
63
+ className="mt-4 block w-full rounded-xl bg-stark-800 py-2 text-sm text-stark-300 hover:bg-stark-700 transition-colors"
64
+ >
65
+ Send another
66
+ </button>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <form
73
+ onSubmit={handleSubmit}
74
+ className={cn(
75
+ "rounded-2xl border border-stark-800 bg-stark-950/60 p-5 backdrop-blur-sm space-y-4",
76
+ className
77
+ )}
78
+ >
79
+ <h3 className="text-sm font-mono uppercase tracking-widest text-stark-400">
80
+ Gasless Payment
81
+ </h3>
82
+
83
+ {/* Token selector */}
84
+ <div className="flex gap-2">
85
+ {TOKENS.map((t) => (
86
+ <button
87
+ key={t}
88
+ type="button"
89
+ onClick={() => setSymbol(t)}
90
+ className={cn(
91
+ "flex-1 rounded-xl py-2 text-sm font-semibold transition-all",
92
+ symbol === t
93
+ ? "bg-stark-500 text-white shadow"
94
+ : "bg-stark-900 text-stark-400 hover:bg-stark-800"
95
+ )}
96
+ >
97
+ {t}
98
+ </button>
99
+ ))}
100
+ </div>
101
+
102
+ {/* Amount input */}
103
+ <div>
104
+ <label className="mb-1.5 block text-xs text-stark-500">Amount</label>
105
+ <input
106
+ type="number"
107
+ min="0"
108
+ step="any"
109
+ placeholder="0.00"
110
+ value={amount}
111
+ onChange={(e) => setAmount(e.target.value)}
112
+ className="w-full rounded-xl bg-stark-900 border border-stark-800 px-4 py-2.5 font-mono text-lg text-white placeholder-stark-700 focus:border-stark-500 focus:outline-none transition-colors"
113
+ />
114
+ </div>
115
+
116
+ {/* Recipient address */}
117
+ <div>
118
+ <label className="mb-1.5 block text-xs text-stark-500">
119
+ Recipient Address
120
+ </label>
121
+ <input
122
+ type="text"
123
+ placeholder="0x..."
124
+ value={to}
125
+ onChange={(e) => setTo(e.target.value)}
126
+ className="w-full rounded-xl bg-stark-900 border border-stark-800 px-4 py-2.5 font-mono text-sm text-white placeholder-stark-700 focus:border-stark-500 focus:outline-none transition-colors"
127
+ />
128
+ </div>
129
+
130
+ {/* Error */}
131
+ {error && (
132
+ <p className="rounded-xl bg-red-950/60 border border-red-900 px-4 py-2 text-sm text-red-400">
133
+ {error}
134
+ </p>
135
+ )}
136
+
137
+ {/* Submit */}
138
+ <button
139
+ type="submit"
140
+ disabled={!isValid || isPending || !wallet}
141
+ className="w-full rounded-xl bg-stark-500 py-3 font-semibold text-white shadow-lg transition-all hover:bg-stark-400 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed"
142
+ >
143
+ {isPending ? (
144
+ <span className="flex items-center justify-center gap-2">
145
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
146
+ Sending…
147
+ </span>
148
+ ) : (
149
+ `Send ${symbol} — No Gas Required`
150
+ )}
151
+ </button>
152
+
153
+ <p className="text-center text-xs text-stark-600">
154
+ Powered by AVNU Paymaster · Gas sponsored by this app
155
+ </p>
156
+ </form>
157
+ );
158
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ /**
4
+ * components/wallet/TokenBalanceCard.tsx
5
+ *
6
+ * Displays a single token balance with a refresh button.
7
+ * Supports STRK, ETH, and USDC out of the box.
8
+ */
9
+
10
+ import { useTokenBalance, type TokenSymbol } from "@/hooks/useTokenBalance";
11
+ import type { WalletState } from "@/hooks/useWallet";
12
+ import { cn } from "@/lib/utils";
13
+
14
+ const TOKEN_META: Record<TokenSymbol, { icon: string; color: string }> = {
15
+ STRK: { icon: "⚡", color: "text-stark-300" },
16
+ ETH: { icon: "Ξ", color: "text-indigo-300" },
17
+ USDC: { icon: "$", color: "text-emerald-300" },
18
+ };
19
+
20
+ type Props = {
21
+ wallet: WalletState["wallet"];
22
+ symbol?: TokenSymbol;
23
+ className?: string;
24
+ };
25
+
26
+ export function TokenBalanceCard({ wallet, symbol = "STRK", className }: Props) {
27
+ const { formatted, isLoading, error, refetch } = useTokenBalance(wallet, symbol);
28
+ const meta = TOKEN_META[symbol];
29
+
30
+ return (
31
+ <div
32
+ className={cn(
33
+ "rounded-2xl border border-stark-800 bg-stark-950/60 p-5 backdrop-blur-sm",
34
+ className
35
+ )}
36
+ >
37
+ <div className="flex items-center justify-between mb-3">
38
+ <span className="text-xs font-mono uppercase tracking-widest text-stark-400">
39
+ {symbol} Balance
40
+ </span>
41
+ <button
42
+ onClick={refetch}
43
+ disabled={isLoading || !wallet}
44
+ className="rounded-full p-1.5 text-stark-500 hover:text-stark-200 hover:bg-stark-800 transition-colors disabled:opacity-40"
45
+ title="Refresh balance"
46
+ >
47
+ <svg
48
+ className={cn("h-3.5 w-3.5", isLoading && "animate-spin")}
49
+ fill="none"
50
+ viewBox="0 0 24 24"
51
+ stroke="currentColor"
52
+ strokeWidth={2.5}
53
+ >
54
+ <path
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
58
+ />
59
+ </svg>
60
+ </button>
61
+ </div>
62
+
63
+ {error ? (
64
+ <p className="text-sm text-red-400">{error}</p>
65
+ ) : (
66
+ <p className={cn("text-3xl font-mono font-bold", meta.color)}>
67
+ {isLoading || !wallet ? (
68
+ <span className="animate-pulse text-stark-700">——.————</span>
69
+ ) : (
70
+ <>
71
+ <span className="mr-1 text-xl opacity-70">{meta.icon}</span>
72
+ {formatted ?? "—"}
73
+ </>
74
+ )}
75
+ </p>
76
+ )}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ /**
4
+ * components/wallet/WalletButton.tsx
5
+ *
6
+ * Drop-in connect/disconnect button.
7
+ * Shows a truncated address when connected.
8
+ */
9
+
10
+ import { cn, formatAddress } from "@/lib/utils";
11
+ import type { WalletState } from "@/hooks/useWallet";
12
+
13
+ type Props = {
14
+ walletState: WalletState;
15
+ className?: string;
16
+ };
17
+
18
+ export function WalletButton({ walletState, className }: Props) {
19
+ const { address, isConnecting, connect, disconnect } = walletState;
20
+
21
+ if (address) {
22
+ return (
23
+ <button
24
+ onClick={disconnect}
25
+ className={cn(
26
+ "group flex items-center gap-2 rounded-full border border-stark-700 bg-stark-900 px-4 py-2 text-sm font-mono text-stark-200 transition-all hover:border-red-500 hover:text-red-400",
27
+ className
28
+ )}
29
+ >
30
+ {/* Green online dot */}
31
+ <span className="h-2 w-2 rounded-full bg-emerald-400 group-hover:bg-red-400 transition-colors" />
32
+ {formatAddress(address)}
33
+ <span className="opacity-0 group-hover:opacity-100 transition-opacity text-xs">
34
+ disconnect
35
+ </span>
36
+ </button>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <button
42
+ onClick={connect}
43
+ disabled={isConnecting}
44
+ className={cn(
45
+ "flex items-center gap-2 rounded-full bg-stark-500 px-5 py-2 text-sm font-semibold text-white shadow-lg transition-all hover:bg-stark-400 active:scale-95 disabled:opacity-60 disabled:cursor-not-allowed",
46
+ className
47
+ )}
48
+ >
49
+ {isConnecting ? (
50
+ <>
51
+ <span className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
52
+ Connecting…
53
+ </>
54
+ ) : (
55
+ "Connect Wallet"
56
+ )}
57
+ </button>
58
+ );
59
+ }
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ /**
4
+ * hooks/useGaslessTransfer.ts
5
+ *
6
+ * Sends ERC-20 tokens gaslessly using the AVNU paymaster.
7
+ * The user pays zero gas — your app (or the paymaster policy) covers it.
8
+ *
9
+ * Usage:
10
+ * const { send, txHash, isPending, error } = useGaslessTransfer(wallet);
11
+ */
12
+
13
+ import { useState, useCallback } from "react";
14
+ import { getPresets, Amount } from "starkzap";
15
+ import type { WalletState } from "./useWallet";
16
+
17
+ export type TransferParams = {
18
+ to: string; // Recipient Starknet address
19
+ amount: string; // Human-readable amount, e.g. "1.5"
20
+ symbol: "STRK" | "ETH" | "USDC";
21
+ };
22
+
23
+ export type GaslessTransferState = {
24
+ send: (params: TransferParams) => Promise<void>;
25
+ txHash: string | null;
26
+ isPending: boolean;
27
+ isSuccess: boolean;
28
+ error: string | null;
29
+ reset: () => void;
30
+ };
31
+
32
+ export function useGaslessTransfer(
33
+ wallet: WalletState["wallet"]
34
+ ): GaslessTransferState {
35
+ const [txHash, setTxHash] = useState<string | null>(null);
36
+ const [isPending, setIsPending] = useState(false);
37
+ const [isSuccess, setIsSuccess] = useState(false);
38
+ const [error, setError] = useState<string | null>(null);
39
+
40
+ const send = useCallback(
41
+ async ({ to, amount, symbol }: TransferParams) => {
42
+ if (!wallet) {
43
+ setError("Wallet not connected");
44
+ return;
45
+ }
46
+
47
+ setIsPending(true);
48
+ setError(null);
49
+ setIsSuccess(false);
50
+ setTxHash(null);
51
+
52
+ try {
53
+ const presets = getPresets(wallet.getChainId());
54
+ const token = presets[symbol as keyof typeof presets];
55
+ if (!token) throw new Error(`Token ${symbol} not available`);
56
+
57
+ const parsedAmount = Amount.parse(amount, token);
58
+
59
+ // feeMode: "sponsored" → AVNU paymaster covers gas (gasless for the user)
60
+ // feeMode: "default" → user pays gas normally
61
+ const tx = await wallet.transfer(
62
+ { to, token, amount: parsedAmount },
63
+ { feeMode: "sponsored" }
64
+ );
65
+
66
+ setTxHash(tx.hash);
67
+
68
+ // Wait for onchain confirmation
69
+ await tx.wait();
70
+ setIsSuccess(true);
71
+ } catch (err) {
72
+ setError(err instanceof Error ? err.message : "Transaction failed");
73
+ } finally {
74
+ setIsPending(false);
75
+ }
76
+ },
77
+ [wallet]
78
+ );
79
+
80
+ const reset = useCallback(() => {
81
+ setTxHash(null);
82
+ setIsPending(false);
83
+ setIsSuccess(false);
84
+ setError(null);
85
+ }, []);
86
+
87
+ return { send, txHash, isPending, isSuccess, error, reset };
88
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ /**
4
+ * hooks/useTokenBalance.ts
5
+ *
6
+ * Fetches the balance of a given ERC-20 token for the connected wallet.
7
+ *
8
+ * Usage:
9
+ * const { balance, formatted, isLoading, refetch } = useTokenBalance(wallet, token);
10
+ */
11
+
12
+ import { useState, useEffect, useCallback } from "react";
13
+ import { getPresets, Amount } from "starkzap";
14
+ import type { WalletState } from "./useWallet";
15
+ import { network } from "@/lib/starkzap";
16
+
17
+ export type TokenSymbol = "STRK" | "ETH" | "USDC";
18
+
19
+ export type TokenBalance = {
20
+ raw: bigint | null;
21
+ formatted: string | null;
22
+ symbol: TokenSymbol;
23
+ isLoading: boolean;
24
+ error: string | null;
25
+ refetch: () => Promise<void>;
26
+ };
27
+
28
+ export function useTokenBalance(
29
+ wallet: WalletState["wallet"],
30
+ symbol: TokenSymbol = "STRK"
31
+ ): TokenBalance {
32
+ const [raw, setRaw] = useState<bigint | null>(null);
33
+ const [formatted, setFormatted] = useState<string | null>(null);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ const fetchBalance = useCallback(async () => {
38
+ if (!wallet) return;
39
+
40
+ setIsLoading(true);
41
+ setError(null);
42
+
43
+ try {
44
+ const presets = getPresets(wallet.getChainId());
45
+ const token = presets[symbol as keyof typeof presets];
46
+
47
+ if (!token) throw new Error(`Token ${symbol} not found for this network`);
48
+
49
+ const balanceRaw = await wallet.getBalance(token);
50
+ const amount = Amount.fromBase(balanceRaw, token);
51
+
52
+ setRaw(balanceRaw);
53
+ setFormatted(amount.toFixed(4));
54
+ } catch (err) {
55
+ setError(err instanceof Error ? err.message : "Failed to fetch balance");
56
+ } finally {
57
+ setIsLoading(false);
58
+ }
59
+ }, [wallet, symbol]);
60
+
61
+ useEffect(() => {
62
+ fetchBalance();
63
+ }, [fetchBalance]);
64
+
65
+ return { raw, formatted, symbol, isLoading, error, refetch: fetchBalance };
66
+ }