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.
- package/.env.example +23 -0
- package/README.md +164 -0
- package/cli-package.json +31 -0
- package/next.config.mjs +8 -0
- package/package.json +33 -0
- package/postcss.config.js +6 -0
- package/scripts/create-starkzap-app.mjs +1749 -0
- package/src/app/globals.css +46 -0
- package/src/app/layout.tsx +21 -0
- package/src/app/page.tsx +159 -0
- package/src/components/payment/PaymentForm.tsx +158 -0
- package/src/components/wallet/TokenBalanceCard.tsx +79 -0
- package/src/components/wallet/WalletButton.tsx +59 -0
- package/src/hooks/useGaslessTransfer.ts +88 -0
- package/src/hooks/useTokenBalance.ts +66 -0
- package/src/hooks/useWallet.ts +88 -0
- package/src/lib/starkzap.ts +22 -0
- package/src/lib/utils.ts +54 -0
- package/tailwind.config.js +43 -0
- package/tsconfig.json +22 -0
|
@@ -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
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -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
|
+
}
|