create-avalanche-app 0.1.4 → 0.1.6
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/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/l1-launch/CLAUDE.md +91 -0
- package/templates/l1-launch/README.md +45 -0
- package/templates/l1-launch/app/globals.css +95 -0
- package/templates/l1-launch/app/layout.tsx +23 -0
- package/templates/l1-launch/app/page.tsx +5 -0
- package/templates/l1-launch/app/providers.tsx +22 -0
- package/templates/l1-launch/components/demo.tsx +396 -0
- package/templates/l1-launch/contracts/foundry.toml +9 -0
- package/templates/l1-launch/contracts/src/AvaKitToken.sol +53 -0
- package/templates/l1-launch/cursor/rules/avakit.mdc +36 -0
- package/templates/l1-launch/env.example +9 -0
- package/templates/l1-launch/gitignore +15 -0
- package/templates/l1-launch/l1.config.json +10 -0
- package/templates/l1-launch/lib/l1.ts +40 -0
- package/templates/l1-launch/lib/token-artifact.ts +239 -0
- package/templates/l1-launch/llms.txt +31 -0
- package/templates/l1-launch/manifest.json +6 -0
- package/templates/l1-launch/next.config.ts +7 -0
- package/templates/l1-launch/package.json +34 -0
- package/templates/l1-launch/pnpm-workspace.yaml +11 -0
- package/templates/l1-launch/postcss.config.mjs +7 -0
- package/templates/l1-launch/scripts/l1-fuji.sh +100 -0
- package/templates/l1-launch/scripts/l1.sh +108 -0
- package/templates/l1-launch/tsconfig.json +23 -0
- package/templates/token-bridge/CLAUDE.md +70 -0
- package/templates/token-bridge/README.md +39 -0
- package/templates/token-bridge/app/globals.css +95 -0
- package/templates/token-bridge/app/layout.tsx +23 -0
- package/templates/token-bridge/app/page.tsx +5 -0
- package/templates/token-bridge/app/providers.tsx +22 -0
- package/templates/token-bridge/bridge.config.json +6 -0
- package/templates/token-bridge/components/demo.tsx +352 -0
- package/templates/token-bridge/cursor/rules/avakit.mdc +36 -0
- package/templates/token-bridge/env.example +7 -0
- package/templates/token-bridge/gitignore +15 -0
- package/templates/token-bridge/lib/ictt-artifacts.json +1 -0
- package/templates/token-bridge/lib/ictt.ts +70 -0
- package/templates/token-bridge/llms.txt +25 -0
- package/templates/token-bridge/manifest.json +6 -0
- package/templates/token-bridge/next.config.ts +7 -0
- package/templates/token-bridge/package.json +33 -0
- package/templates/token-bridge/pnpm-workspace.yaml +11 -0
- package/templates/token-bridge/postcss.config.mjs +7 -0
- package/templates/token-bridge/scripts/bridge.sh +95 -0
- package/templates/token-bridge/scripts/deploy-bridge.mjs +107 -0
- package/templates/token-bridge/tsconfig.json +23 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { deployContract, ensureChain, getPublicClient, getWalletClient } from "@avakit/core";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
ConnectAvalanche,
|
|
7
|
+
shortenAddress,
|
|
8
|
+
useAvaAccount,
|
|
9
|
+
useAvaKit,
|
|
10
|
+
} from "@avakit/react";
|
|
11
|
+
import { Blocks, Check, Copy, Moon, Rocket, Sun } from "lucide-react";
|
|
12
|
+
import { useTheme } from "next-themes";
|
|
13
|
+
import { useCallback, useEffect, useState } from "react";
|
|
14
|
+
import { type Address, type Hash, formatEther, formatUnits } from "viem";
|
|
15
|
+
import { chain, isConfigured, l1 } from "@/lib/l1";
|
|
16
|
+
import { abi, bytecode } from "@/lib/token-artifact";
|
|
17
|
+
|
|
18
|
+
const CONTRACT_KEY = "l1-launch:token";
|
|
19
|
+
const MAX_BLOCKS = 8;
|
|
20
|
+
|
|
21
|
+
type BlockRow = { number: bigint; hash: Hash; txCount: number; timestamp: bigint };
|
|
22
|
+
type TxRow = { hash: Hash; from: Address; to: Address | null; value: bigint };
|
|
23
|
+
|
|
24
|
+
export function Demo() {
|
|
25
|
+
if (!isConfigured) {
|
|
26
|
+
return (
|
|
27
|
+
<Shell>
|
|
28
|
+
<SetupPanel />
|
|
29
|
+
</Shell>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<Shell>
|
|
34
|
+
<Dashboard />
|
|
35
|
+
</Shell>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Dashboard() {
|
|
40
|
+
const { address, isConnected } = useAvaAccount();
|
|
41
|
+
const { provider } = useAvaKit();
|
|
42
|
+
|
|
43
|
+
const [height, setHeight] = useState<bigint | null>(null);
|
|
44
|
+
const [gasPrice, setGasPrice] = useState<bigint | null>(null);
|
|
45
|
+
const [balance, setBalance] = useState<bigint | null>(null);
|
|
46
|
+
const [blocks, setBlocks] = useState<BlockRow[]>([]);
|
|
47
|
+
const [txs, setTxs] = useState<TxRow[]>([]);
|
|
48
|
+
const [online, setOnline] = useState(true);
|
|
49
|
+
|
|
50
|
+
const [token, setToken] = useState<Address | null>(null);
|
|
51
|
+
const [busy, setBusy] = useState<null | "deploy" | "mint">(null);
|
|
52
|
+
const [error, setError] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (typeof window === "undefined") return;
|
|
56
|
+
const saved = window.localStorage.getItem(CONTRACT_KEY);
|
|
57
|
+
if (saved) setToken(saved as Address);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
// Live block explorer: poll the L1's RPC for height, gas, the latest blocks,
|
|
61
|
+
// and the transactions inside them. All read-only — no wallet needed.
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const client = getPublicClient(chain);
|
|
64
|
+
let active = true;
|
|
65
|
+
async function poll() {
|
|
66
|
+
try {
|
|
67
|
+
const [tip, gas] = await Promise.all([client.getBlockNumber(), client.getGasPrice()]);
|
|
68
|
+
if (!active) return;
|
|
69
|
+
setOnline(true);
|
|
70
|
+
setHeight(tip);
|
|
71
|
+
setGasPrice(gas);
|
|
72
|
+
|
|
73
|
+
const from = tip > BigInt(MAX_BLOCKS - 1) ? tip - BigInt(MAX_BLOCKS - 1) : 0n;
|
|
74
|
+
const numbers: bigint[] = [];
|
|
75
|
+
for (let n = tip; n >= from; n--) numbers.push(n);
|
|
76
|
+
const fetched = await Promise.all(
|
|
77
|
+
numbers.map((n) => client.getBlock({ blockNumber: n, includeTransactions: true })),
|
|
78
|
+
);
|
|
79
|
+
if (!active) return;
|
|
80
|
+
|
|
81
|
+
const blockRows: BlockRow[] = fetched.map((b) => ({
|
|
82
|
+
number: b.number,
|
|
83
|
+
hash: b.hash,
|
|
84
|
+
txCount: b.transactions.length,
|
|
85
|
+
timestamp: b.timestamp,
|
|
86
|
+
}));
|
|
87
|
+
const txRows: TxRow[] = fetched
|
|
88
|
+
.flatMap((b) => b.transactions)
|
|
89
|
+
.filter((t): t is Exclude<typeof t, Hash> => typeof t !== "string")
|
|
90
|
+
.slice(0, 12)
|
|
91
|
+
.map((t) => ({ hash: t.hash, from: t.from, to: t.to, value: t.value }));
|
|
92
|
+
setBlocks(blockRows);
|
|
93
|
+
setTxs(txRows);
|
|
94
|
+
} catch {
|
|
95
|
+
if (active) setOnline(false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
void poll();
|
|
99
|
+
const timer = setInterval(poll, 2500);
|
|
100
|
+
return () => {
|
|
101
|
+
active = false;
|
|
102
|
+
clearInterval(timer);
|
|
103
|
+
};
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
// Your native-token balance, refreshed with the tip.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!address) {
|
|
109
|
+
setBalance(null);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const client = getPublicClient(chain);
|
|
113
|
+
let active = true;
|
|
114
|
+
client
|
|
115
|
+
.getBalance({ address })
|
|
116
|
+
.then((b) => active && setBalance(b))
|
|
117
|
+
.catch(() => {});
|
|
118
|
+
return () => {
|
|
119
|
+
active = false;
|
|
120
|
+
};
|
|
121
|
+
}, [address, height]);
|
|
122
|
+
|
|
123
|
+
const deployToken = useCallback(async () => {
|
|
124
|
+
if (!provider || !address) return;
|
|
125
|
+
setBusy("deploy");
|
|
126
|
+
setError(null);
|
|
127
|
+
try {
|
|
128
|
+
await ensureChain(provider, chain);
|
|
129
|
+
const { address: deployed } = await deployContract({
|
|
130
|
+
artifact: { abi, bytecode },
|
|
131
|
+
chain,
|
|
132
|
+
provider,
|
|
133
|
+
account: address,
|
|
134
|
+
});
|
|
135
|
+
window.localStorage.setItem(CONTRACT_KEY, deployed);
|
|
136
|
+
setToken(deployed);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
139
|
+
} finally {
|
|
140
|
+
setBusy(null);
|
|
141
|
+
}
|
|
142
|
+
}, [provider, address]);
|
|
143
|
+
|
|
144
|
+
const mint = useCallback(async () => {
|
|
145
|
+
if (!provider || !address || !token) return;
|
|
146
|
+
setBusy("mint");
|
|
147
|
+
setError(null);
|
|
148
|
+
try {
|
|
149
|
+
await ensureChain(provider, chain);
|
|
150
|
+
const wallet = getWalletClient(chain, provider);
|
|
151
|
+
await wallet.writeContract({
|
|
152
|
+
address: token,
|
|
153
|
+
abi,
|
|
154
|
+
functionName: "mint",
|
|
155
|
+
account: address,
|
|
156
|
+
} as never);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
159
|
+
} finally {
|
|
160
|
+
setBusy(null);
|
|
161
|
+
}
|
|
162
|
+
}, [provider, address, token]);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
<div className="flex flex-col gap-2">
|
|
167
|
+
<h1 className="text-3xl font-semibold tracking-tight">{l1.name}</h1>
|
|
168
|
+
<p className="text-muted-foreground text-sm">
|
|
169
|
+
Your own Avalanche L1, live and explorable. Deploy a contract, send transactions, and
|
|
170
|
+
watch blocks land in real time — no third-party explorer needed.
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Chain stat strip */}
|
|
175
|
+
<section className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
176
|
+
<Stat label="Block height" value={height === null ? "…" : height.toString()} live={online} />
|
|
177
|
+
<Stat label="Chain ID" value={String(chain.id)} />
|
|
178
|
+
<Stat label="Token" value={l1.token} />
|
|
179
|
+
<Stat
|
|
180
|
+
label="Gas price"
|
|
181
|
+
value={gasPrice === null ? "…" : `${formatUnits(gasPrice, 9)} gwei`}
|
|
182
|
+
/>
|
|
183
|
+
</section>
|
|
184
|
+
|
|
185
|
+
{/* Account + deploy */}
|
|
186
|
+
<section className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
187
|
+
<div className="flex flex-col gap-3 rounded-xl border p-5">
|
|
188
|
+
<p className="text-muted-foreground text-xs uppercase tracking-wide">Your account</p>
|
|
189
|
+
{isConnected && address ? (
|
|
190
|
+
<>
|
|
191
|
+
<Row label="Address" value={shortenAddress(address, 6)} mono />
|
|
192
|
+
<Row
|
|
193
|
+
label="Balance"
|
|
194
|
+
value={balance === null ? "…" : `${formatEther(balance)} ${l1.token}`}
|
|
195
|
+
mono
|
|
196
|
+
/>
|
|
197
|
+
</>
|
|
198
|
+
) : (
|
|
199
|
+
<div className="flex flex-col items-start gap-2">
|
|
200
|
+
<p className="text-muted-foreground text-sm">Connect a wallet to transact.</p>
|
|
201
|
+
<ConnectAvalanche />
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
{l1.network === "local" && l1.faucetAccount.privateKey ? (
|
|
205
|
+
<p className="text-muted-foreground text-xs">
|
|
206
|
+
Local dev: import the pre-funded EWOQ key (from{" "}
|
|
207
|
+
<span className="font-mono">pnpm l1</span>) to get {l1.token}.
|
|
208
|
+
</p>
|
|
209
|
+
) : null}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="flex flex-col gap-3 rounded-xl border p-5">
|
|
213
|
+
<p className="text-muted-foreground text-xs uppercase tracking-wide">Demo contract</p>
|
|
214
|
+
{token ? (
|
|
215
|
+
<>
|
|
216
|
+
<Row label="ERC-20" value={shortenAddress(token, 6)} mono />
|
|
217
|
+
<Button onClick={mint} disabled={busy !== null || !isConnected} size="sm">
|
|
218
|
+
{busy === "mint" ? "Minting…" : "Mint 100 AKT"}
|
|
219
|
+
</Button>
|
|
220
|
+
<p className="text-muted-foreground text-xs">
|
|
221
|
+
Each action is a real transaction — watch it appear below.
|
|
222
|
+
</p>
|
|
223
|
+
</>
|
|
224
|
+
) : (
|
|
225
|
+
<>
|
|
226
|
+
<p className="text-muted-foreground text-sm">
|
|
227
|
+
Deploy an ERC-20 to your chain from the browser.
|
|
228
|
+
</p>
|
|
229
|
+
<Button onClick={deployToken} disabled={busy !== null || !isConnected} size="sm">
|
|
230
|
+
<Rocket className="size-4" />
|
|
231
|
+
{busy === "deploy" ? "Deploying…" : "Deploy demo token"}
|
|
232
|
+
</Button>
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</section>
|
|
237
|
+
|
|
238
|
+
{/* Explorer: blocks + txs */}
|
|
239
|
+
<section className="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
|
240
|
+
<div className="flex flex-col gap-2 rounded-xl border p-5">
|
|
241
|
+
<div className="mb-1 flex items-center gap-2">
|
|
242
|
+
<Blocks className="size-4" />
|
|
243
|
+
<span className="text-sm font-semibold">Latest blocks</span>
|
|
244
|
+
</div>
|
|
245
|
+
{blocks.length === 0 ? (
|
|
246
|
+
<p className="text-muted-foreground text-sm">Waiting for blocks…</p>
|
|
247
|
+
) : (
|
|
248
|
+
blocks.map((b) => (
|
|
249
|
+
<div key={b.hash} className="flex items-center justify-between gap-3 border-b py-1.5 last:border-0">
|
|
250
|
+
<span className="font-mono text-sm">#{b.number.toString()}</span>
|
|
251
|
+
<span className="text-muted-foreground text-xs">{b.txCount} tx</span>
|
|
252
|
+
<span className="text-muted-foreground font-mono text-xs">{ago(b.timestamp)}</span>
|
|
253
|
+
</div>
|
|
254
|
+
))
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div className="flex flex-col gap-2 rounded-xl border p-5">
|
|
259
|
+
<span className="mb-1 text-sm font-semibold">Latest transactions</span>
|
|
260
|
+
{txs.length === 0 ? (
|
|
261
|
+
<p className="text-muted-foreground text-sm">No transactions yet — deploy the token above.</p>
|
|
262
|
+
) : (
|
|
263
|
+
txs.map((t) => (
|
|
264
|
+
<div key={t.hash} className="flex flex-col gap-0.5 border-b py-1.5 last:border-0">
|
|
265
|
+
<span className="font-mono text-xs">{shortenAddress(t.hash, 8)}</span>
|
|
266
|
+
<span className="text-muted-foreground text-xs">
|
|
267
|
+
{shortenAddress(t.from, 4)} →{" "}
|
|
268
|
+
{t.to ? shortenAddress(t.to, 4) : "contract creation"}
|
|
269
|
+
{t.value > 0n ? ` · ${formatEther(t.value)} ${l1.token}` : ""}
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
))
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</section>
|
|
276
|
+
|
|
277
|
+
{error ? <p className="text-muted-foreground text-sm break-all">{error}</p> : null}
|
|
278
|
+
</>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function ago(timestamp: bigint): string {
|
|
283
|
+
const secs = Math.max(0, Math.floor(Date.now() / 1000) - Number(timestamp));
|
|
284
|
+
if (secs < 60) return `${secs}s ago`;
|
|
285
|
+
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
|
|
286
|
+
return `${Math.floor(secs / 3600)}h ago`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function Stat({ label, value, live }: { label: string; value: string; live?: boolean }) {
|
|
290
|
+
return (
|
|
291
|
+
<div className="flex flex-col gap-1 rounded-xl border p-4">
|
|
292
|
+
<span className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
|
293
|
+
{live !== undefined ? (
|
|
294
|
+
<span
|
|
295
|
+
className={`size-1.5 rounded-full ${live ? "bg-foreground animate-pulse" : "bg-muted-foreground/40"}`}
|
|
296
|
+
/>
|
|
297
|
+
) : null}
|
|
298
|
+
{label}
|
|
299
|
+
</span>
|
|
300
|
+
<span className="font-mono text-lg">{value}</span>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function SetupPanel() {
|
|
306
|
+
return (
|
|
307
|
+
<div className="flex flex-col gap-4 rounded-xl border p-8">
|
|
308
|
+
<div className="flex flex-col gap-2">
|
|
309
|
+
<h1 className="text-2xl font-semibold tracking-tight">Launch your own Avalanche L1</h1>
|
|
310
|
+
<p className="text-muted-foreground text-sm">
|
|
311
|
+
One command spins up your own blockchain — a Subnet-EVM L1 on a local Avalanche network —
|
|
312
|
+
and wires this app to it. No test AVAX, no faucet, no always-on node.
|
|
313
|
+
</p>
|
|
314
|
+
</div>
|
|
315
|
+
<CopyCommand command="pnpm l1" />
|
|
316
|
+
<p className="text-muted-foreground text-xs">
|
|
317
|
+
Configure it: <span className="font-mono">L1_NAME=mychain L1_CHAIN_ID=9999 L1_TOKEN=MYL1 pnpm l1</span>.
|
|
318
|
+
Needs{" "}
|
|
319
|
+
<a
|
|
320
|
+
href="https://build.avax.network/docs/tooling/avalanche-cli/get-avalanche-cli"
|
|
321
|
+
target="_blank"
|
|
322
|
+
rel="noreferrer"
|
|
323
|
+
className="underline underline-offset-4"
|
|
324
|
+
>
|
|
325
|
+
avalanche-cli
|
|
326
|
+
</a>
|
|
327
|
+
. When it finishes it writes <span className="font-mono">l1.config.json</span> and this page
|
|
328
|
+
becomes your chain dashboard automatically.
|
|
329
|
+
</p>
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function CopyCommand({ command }: { command: string }) {
|
|
335
|
+
const [copied, setCopied] = useState(false);
|
|
336
|
+
return (
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
onClick={() => {
|
|
340
|
+
void navigator.clipboard?.writeText(command);
|
|
341
|
+
setCopied(true);
|
|
342
|
+
setTimeout(() => setCopied(false), 1500);
|
|
343
|
+
}}
|
|
344
|
+
className="bg-muted hover:bg-muted/70 flex items-center justify-between gap-4 rounded-lg p-4 text-left font-mono text-sm transition-colors"
|
|
345
|
+
>
|
|
346
|
+
<span>
|
|
347
|
+
<span className="text-muted-foreground select-none">$ </span>
|
|
348
|
+
{command}
|
|
349
|
+
</span>
|
|
350
|
+
{copied ? (
|
|
351
|
+
<Check className="size-4 shrink-0" />
|
|
352
|
+
) : (
|
|
353
|
+
<Copy className="text-muted-foreground size-4 shrink-0" />
|
|
354
|
+
)}
|
|
355
|
+
</button>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ThemeToggle() {
|
|
360
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
361
|
+
return (
|
|
362
|
+
<Button
|
|
363
|
+
variant="outline"
|
|
364
|
+
size="icon"
|
|
365
|
+
aria-label="Toggle theme"
|
|
366
|
+
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
|
|
367
|
+
>
|
|
368
|
+
<Sun className="hidden size-4 dark:block" />
|
|
369
|
+
<Moon className="block size-4 dark:hidden" />
|
|
370
|
+
</Button>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function Shell({ children }: { children: React.ReactNode }) {
|
|
375
|
+
return (
|
|
376
|
+
<div className="mx-auto flex min-h-dvh max-w-3xl flex-col gap-8 px-6 py-16">
|
|
377
|
+
<header className="flex items-center justify-between">
|
|
378
|
+
<span className="font-mono text-sm font-semibold">__PROJECT_NAME__</span>
|
|
379
|
+
<div className="flex items-center gap-2">
|
|
380
|
+
<ConnectAvalanche />
|
|
381
|
+
<ThemeToggle />
|
|
382
|
+
</div>
|
|
383
|
+
</header>
|
|
384
|
+
{children}
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
|
390
|
+
return (
|
|
391
|
+
<div className="flex items-center justify-between gap-4">
|
|
392
|
+
<span className="text-muted-foreground text-sm">{label}</span>
|
|
393
|
+
<span className={`${mono ? "font-mono" : ""} truncate text-sm`}>{value}</span>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
/// @title AvaKitToken
|
|
5
|
+
/// @notice A minimal, self-contained ERC-20 with a public demo faucet (`mint`).
|
|
6
|
+
/// No external deps, so it compiles out of the box with `forge build`.
|
|
7
|
+
contract AvaKitToken {
|
|
8
|
+
string public constant name = "AvaKit Token";
|
|
9
|
+
string public constant symbol = "AKT";
|
|
10
|
+
uint8 public constant decimals = 18;
|
|
11
|
+
|
|
12
|
+
uint256 public totalSupply;
|
|
13
|
+
mapping(address => uint256) public balanceOf;
|
|
14
|
+
mapping(address => mapping(address => uint256)) public allowance;
|
|
15
|
+
|
|
16
|
+
event Transfer(address indexed from, address indexed to, uint256 value);
|
|
17
|
+
event Approval(address indexed owner, address indexed spender, uint256 value);
|
|
18
|
+
|
|
19
|
+
/// @notice Mint 100 AKT to the caller (demo faucet).
|
|
20
|
+
function mint() external {
|
|
21
|
+
uint256 amount = 100 * 10 ** decimals;
|
|
22
|
+
totalSupply += amount;
|
|
23
|
+
unchecked {
|
|
24
|
+
balanceOf[msg.sender] += amount;
|
|
25
|
+
}
|
|
26
|
+
emit Transfer(address(0), msg.sender, amount);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function transfer(address to, uint256 value) external returns (bool) {
|
|
30
|
+
balanceOf[msg.sender] -= value;
|
|
31
|
+
unchecked {
|
|
32
|
+
balanceOf[to] += value;
|
|
33
|
+
}
|
|
34
|
+
emit Transfer(msg.sender, to, value);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function approve(address spender, uint256 value) external returns (bool) {
|
|
39
|
+
allowance[msg.sender][spender] = value;
|
|
40
|
+
emit Approval(msg.sender, spender, value);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function transferFrom(address from, address to, uint256 value) external returns (bool) {
|
|
45
|
+
allowance[from][msg.sender] -= value;
|
|
46
|
+
balanceOf[from] -= value;
|
|
47
|
+
unchecked {
|
|
48
|
+
balanceOf[to] += value;
|
|
49
|
+
}
|
|
50
|
+
emit Transfer(from, to, value);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: AvaKit L1-launch conventions for this project
|
|
3
|
+
globs: ["**/*.ts", "**/*.tsx", "**/*.sol", "**/*.sh", "**/*.css"]
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# AvaKit L1-launch project rules
|
|
8
|
+
|
|
9
|
+
Launch your own Avalanche L1, then explore it in a built-in dashboard.
|
|
10
|
+
|
|
11
|
+
## Flow
|
|
12
|
+
|
|
13
|
+
- `pnpm l1` (scripts/l1.sh) creates + deploys a local Subnet-EVM L1 and writes `l1.config.json`.
|
|
14
|
+
- `lib/l1.ts` turns that into an AvaKit `chain` + `isConfigured`.
|
|
15
|
+
- `components/demo.tsx` is the explorer: poll `getPublicClient(chain)` for `getBlockNumber`,
|
|
16
|
+
`getGasPrice`, `getBlock({ blockNumber, includeTransactions: true })`, `getBalance`.
|
|
17
|
+
- Deploy the demo token: `deployContract({ artifact: { abi, bytecode }, chain, provider, account })`.
|
|
18
|
+
- `pnpm l1:fuji` deploys the same L1 to Fuji (advanced; needs test AVAX + an always-on validator).
|
|
19
|
+
|
|
20
|
+
## L1 config decisions
|
|
21
|
+
|
|
22
|
+
VM = Subnet-EVM (`--evm`); EVM chain id (`--evm-chain-id`, avoid collisions); native token
|
|
23
|
+
(`--evm-token`); `--sovereign=false` for a zero-prompt local chain (a true sovereign L1 picks a
|
|
24
|
+
Validator Manager model — PoA / PoS-native / PoS-erc20); `--test-defaults` pre-funds the EWOQ key.
|
|
25
|
+
See CLAUDE.md for the full explainer — surface it when the user asks "what does this setting do".
|
|
26
|
+
|
|
27
|
+
## Explorer
|
|
28
|
+
|
|
29
|
+
- Read-only viem only. No server, no indexer, no Docker. Poll on an interval, clean up the timer.
|
|
30
|
+
|
|
31
|
+
## UI & safety
|
|
32
|
+
|
|
33
|
+
- shadcn/ui only; black & white for now; dark/light via next-themes; both must work.
|
|
34
|
+
- Animations: Framer Motion or GSAP only.
|
|
35
|
+
- EWOQ (`0x56289e…8027`) is a PUBLIC local-only dev key — never use it or commit a real key on Fuji/mainnet.
|
|
36
|
+
- Deploying/minting cost gas — on local, import the EWOQ key; on Fuji, fund your own wallet.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# This template needs no env vars for the local flow — your chain's RPC lives in
|
|
2
|
+
# l1.config.json (written by `pnpm l1`), and you connect with a browser wallet.
|
|
3
|
+
#
|
|
4
|
+
# On local networks, import avalanche-cli's public EWOQ dev key (printed by
|
|
5
|
+
# `pnpm l1`) into your wallet — it's pre-funded on your chain. LOCAL ONLY:
|
|
6
|
+
# 0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027
|
|
7
|
+
#
|
|
8
|
+
# For the Fuji path (`pnpm l1:fuji`), bring your own funded Fuji wallet — never a
|
|
9
|
+
# real private key in this file.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your Avalanche L1 configuration.
|
|
3
|
+
*
|
|
4
|
+
* `scripts/l1.sh` (`pnpm l1`) creates a Subnet-EVM chain, deploys it to a local
|
|
5
|
+
* Avalanche network, and writes the discovered RPC URL + blockchain ID into
|
|
6
|
+
* `l1.config.json`. This module turns that into an AvaKit chain the app uses.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type AvaChain, defineChain } from "@avakit/core/chains";
|
|
10
|
+
import config from "../l1.config.json";
|
|
11
|
+
|
|
12
|
+
export interface L1Config {
|
|
13
|
+
configured: boolean;
|
|
14
|
+
/** "local" (pnpm l1) or "fuji" (pnpm l1:fuji). */
|
|
15
|
+
network: string;
|
|
16
|
+
name: string;
|
|
17
|
+
token: string;
|
|
18
|
+
/** EVM chain id (for the wallet / RPC). */
|
|
19
|
+
evmChainId: number;
|
|
20
|
+
rpcUrl: string;
|
|
21
|
+
/** The bytes32 (Avalanche) blockchain ID in hex. */
|
|
22
|
+
blockchainIdHex: string;
|
|
23
|
+
/** EWOQ dev key on local networks (empty on Fuji — bring your own wallet). */
|
|
24
|
+
faucetAccount: { address: string; privateKey: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const l1 = config as L1Config;
|
|
28
|
+
|
|
29
|
+
/** True once `pnpm l1` has filled in the chain's RPC URL. */
|
|
30
|
+
export const isConfigured = Boolean(l1.configured && l1.rpcUrl);
|
|
31
|
+
|
|
32
|
+
/** Your L1 as an AvaKit chain. Explorer is built into this app (see components/explorer.tsx). */
|
|
33
|
+
export const chain: AvaChain = defineChain({
|
|
34
|
+
id: l1.evmChainId,
|
|
35
|
+
name: l1.name,
|
|
36
|
+
rpcUrl: l1.rpcUrl || "http://127.0.0.1:9650",
|
|
37
|
+
explorerUrl: "",
|
|
38
|
+
nativeCurrency: { name: l1.token, symbol: l1.token, decimals: 18 },
|
|
39
|
+
testnet: true,
|
|
40
|
+
});
|