create-avalanche-app 0.1.5 → 0.1.7

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,352 @@
1
+ "use client";
2
+
3
+ import { ensureChain, getPublicClient, getWalletClient, readContract } from "@avakit/core";
4
+ import {
5
+ Button,
6
+ ConnectAvalanche,
7
+ useAvaAccount,
8
+ useAvaChain,
9
+ useAvaKit,
10
+ } from "@avakit/react";
11
+ import { ArrowRight, Check, Copy, Moon, Sun } from "lucide-react";
12
+ import { useTheme } from "next-themes";
13
+ import { useCallback, useEffect, useState } from "react";
14
+ import { type Address, formatEther, parseEther } from "viem";
15
+ import {
16
+ blockchainIdOf,
17
+ bridge,
18
+ erc20Abi,
19
+ homeAbi,
20
+ homeChain,
21
+ isConfigured,
22
+ remoteAbi,
23
+ remoteChain,
24
+ } from "@/lib/ictt";
25
+
26
+ const ZERO = "0x0000000000000000000000000000000000000000" as const;
27
+ const GAS_LIMIT = 250000n;
28
+
29
+ export function Demo() {
30
+ if (!isConfigured || !bridge.bridge) {
31
+ return (
32
+ <Shell>
33
+ <SetupPanel />
34
+ </Shell>
35
+ );
36
+ }
37
+ return (
38
+ <Shell>
39
+ <Bridge addrs={bridge.bridge} />
40
+ </Shell>
41
+ );
42
+ }
43
+
44
+ function Bridge({ addrs }: { addrs: NonNullable<typeof bridge.bridge> }) {
45
+ const { address, isConnected } = useAvaAccount();
46
+ const { provider } = useAvaKit();
47
+ const { setChain } = useAvaChain();
48
+
49
+ const [homeBal, setHomeBal] = useState<bigint | null>(null);
50
+ const [remoteBal, setRemoteBal] = useState<bigint | null>(null);
51
+ const [amount, setAmount] = useState("10");
52
+ const [toRemote, setToRemote] = useState(true);
53
+ const [busy, setBusy] = useState<null | "mint" | "bridge">(null);
54
+ const [inFlight, setInFlight] = useState(false);
55
+ const [error, setError] = useState<string | null>(null);
56
+
57
+ // Live balances: the demo token on the home chain, the bridged token on the
58
+ // remote chain. Read-only — no wallet needed.
59
+ useEffect(() => {
60
+ if (!address) return;
61
+ let active = true;
62
+ async function poll() {
63
+ try {
64
+ const [h, r] = await Promise.all([
65
+ readContract(homeChain, { address: addrs.demoToken, abi: erc20Abi, functionName: "balanceOf", args: [address as Address] }),
66
+ readContract(remoteChain, { address: addrs.remote, abi: remoteAbi, functionName: "balanceOf", args: [address as Address] }),
67
+ ]);
68
+ if (!active) return;
69
+ setHomeBal(h as bigint);
70
+ setRemoteBal(r as bigint);
71
+ } catch {
72
+ // chains warming up
73
+ }
74
+ }
75
+ void poll();
76
+ const t = setInterval(poll, 3000);
77
+ return () => {
78
+ active = false;
79
+ clearInterval(t);
80
+ };
81
+ }, [address, addrs]);
82
+
83
+ const run = useCallback(
84
+ async (kind: "mint" | "bridge", fn: () => Promise<void>) => {
85
+ if (!provider || !address) return;
86
+ setBusy(kind);
87
+ setError(null);
88
+ try {
89
+ await fn();
90
+ } catch (e) {
91
+ setError(e instanceof Error ? e.message : String(e));
92
+ } finally {
93
+ setBusy(null);
94
+ }
95
+ },
96
+ [provider, address],
97
+ );
98
+
99
+ const mint = () =>
100
+ run("mint", async () => {
101
+ await ensureChain(provider!, homeChain);
102
+ setChain(homeChain);
103
+ const wallet = getWalletClient(homeChain, provider!);
104
+ const hash = await wallet.writeContract({ address: addrs.demoToken, abi: erc20Abi, functionName: "mint", account: address as Address } as never);
105
+ await getPublicClient(homeChain).waitForTransactionReceipt({ hash });
106
+ });
107
+
108
+ const doBridge = () =>
109
+ run("bridge", async () => {
110
+ const value = parseEther(amount || "0");
111
+ if (value <= 0n) throw new Error("Enter an amount greater than 0.");
112
+ const source = toRemote ? homeChain : remoteChain;
113
+ const destination = toRemote ? remoteChain : homeChain;
114
+ const transferrer = toRemote ? addrs.home : addrs.remote;
115
+ const destTransferrer = toRemote ? addrs.remote : addrs.home;
116
+
117
+ await ensureChain(provider!, source);
118
+ setChain(source);
119
+ const wallet = getWalletClient(source, provider!);
120
+ const pub = getPublicClient(source);
121
+
122
+ // Home locks the underlying ERC-20, so bridging OUT of home needs an
123
+ // approval first. The remote token is burned on send back — no approval.
124
+ if (toRemote) {
125
+ const approveHash = await wallet.writeContract({ address: addrs.demoToken, abi: erc20Abi, functionName: "approve", args: [addrs.home, value], account: address as Address } as never);
126
+ await pub.waitForTransactionReceipt({ hash: approveHash });
127
+ }
128
+
129
+ const input = {
130
+ destinationBlockchainID: blockchainIdOf(destination),
131
+ destinationTokenTransferrerAddress: destTransferrer,
132
+ recipient: address as Address,
133
+ primaryFeeTokenAddress: ZERO,
134
+ primaryFee: 0n,
135
+ secondaryFee: 0n,
136
+ requiredGasLimit: GAS_LIMIT,
137
+ multiHopFallback: ZERO,
138
+ };
139
+ const sendHash = await wallet.writeContract({
140
+ address: transferrer,
141
+ abi: toRemote ? homeAbi : remoteAbi,
142
+ functionName: "send",
143
+ args: [input, value],
144
+ account: address as Address,
145
+ } as never);
146
+ await pub.waitForTransactionReceipt({ hash: sendHash });
147
+ setInFlight(true);
148
+ setTimeout(() => setInFlight(false), 12000);
149
+ });
150
+
151
+ const canBridge =
152
+ isConnected &&
153
+ !!address &&
154
+ busy === null &&
155
+ (toRemote ? (homeBal ?? 0n) >= parseSafe(amount) : (remoteBal ?? 0n) >= parseSafe(amount));
156
+
157
+ return (
158
+ <>
159
+ <div className="flex flex-col gap-2">
160
+ <h1 className="text-3xl font-semibold tracking-tight">Cross-chain token bridge</h1>
161
+ <p className="text-muted-foreground text-sm">
162
+ Move an ERC-20 between two Avalanche L1s with Interchain Token Transfer. The home chain
163
+ locks your token; the remote chain mints a bridged version — and back again.
164
+ </p>
165
+ </div>
166
+
167
+ <section className="grid grid-cols-1 gap-3 sm:grid-cols-2">
168
+ <ChainCard
169
+ title={homeChain.name}
170
+ role="Home"
171
+ symbol={bridge.chain1.token}
172
+ balance={homeBal}
173
+ note="The token you bridge (locked here when sent)."
174
+ />
175
+ <ChainCard
176
+ title={remoteChain.name}
177
+ role="Remote"
178
+ symbol={`${bridge.chain1.token}.b`}
179
+ balance={remoteBal}
180
+ note="The bridged token (minted here on arrival)."
181
+ highlight={inFlight}
182
+ />
183
+ </section>
184
+
185
+ <section className="flex flex-col gap-3 rounded-xl border p-6">
186
+ <div className="flex items-center justify-between">
187
+ <div className="text-muted-foreground flex items-center gap-2 text-sm">
188
+ <span className="font-mono">{toRemote ? homeChain.name : remoteChain.name}</span>
189
+ <ArrowRight className="size-4" />
190
+ <span className="font-mono">{toRemote ? remoteChain.name : homeChain.name}</span>
191
+ </div>
192
+ {!isConnected || !address ? <ConnectAvalanche /> : null}
193
+ </div>
194
+
195
+ <div className="flex gap-2">
196
+ <input
197
+ value={amount}
198
+ onChange={(e) => setAmount(e.target.value)}
199
+ inputMode="decimal"
200
+ placeholder="Amount"
201
+ className="border-input bg-background flex-1 rounded-lg border px-3 py-2 text-sm outline-none"
202
+ />
203
+ <Button variant="outline" size="sm" onClick={() => setToRemote((v) => !v)} disabled={busy !== null}>
204
+ Swap direction
205
+ </Button>
206
+ </div>
207
+
208
+ <div className="flex flex-col gap-2 sm:flex-row">
209
+ {toRemote ? (
210
+ <Button variant="outline" className="flex-1" onClick={mint} disabled={busy !== null || !isConnected}>
211
+ {busy === "mint" ? "Minting…" : `Mint 100 ${bridge.chain1.token}`}
212
+ </Button>
213
+ ) : null}
214
+ <Button className="flex-1" onClick={doBridge} disabled={!canBridge}>
215
+ {busy === "bridge"
216
+ ? "Bridging…"
217
+ : `Bridge to ${toRemote ? remoteChain.name : homeChain.name}`}
218
+ </Button>
219
+ </div>
220
+
221
+ {inFlight ? (
222
+ <p className="flex items-center gap-2 text-sm">
223
+ <span className="bg-foreground size-2 animate-ping rounded-full" />
224
+ In flight — the relayer is carrying your tokens across…
225
+ </p>
226
+ ) : null}
227
+ {error ? (
228
+ <p className="border-destructive text-destructive rounded-md border px-3 py-2 text-sm font-medium break-all">
229
+ {error}
230
+ </p>
231
+ ) : null}
232
+ </section>
233
+ </>
234
+ );
235
+ }
236
+
237
+ function parseSafe(v: string): bigint {
238
+ try {
239
+ return parseEther(v || "0");
240
+ } catch {
241
+ return 0n;
242
+ }
243
+ }
244
+
245
+ function ChainCard({
246
+ title,
247
+ role,
248
+ symbol,
249
+ balance,
250
+ note,
251
+ highlight,
252
+ }: {
253
+ title: string;
254
+ role: string;
255
+ symbol: string;
256
+ balance: bigint | null;
257
+ note: string;
258
+ highlight?: boolean;
259
+ }) {
260
+ return (
261
+ <div className={`flex flex-col gap-2 rounded-xl border p-5 transition-colors ${highlight ? "border-foreground" : ""}`}>
262
+ <div className="flex items-center justify-between">
263
+ <span className="font-mono text-sm font-semibold">{title}</span>
264
+ <span className="text-muted-foreground text-xs">{role}</span>
265
+ </div>
266
+ <div className="flex items-baseline gap-2">
267
+ <span className="font-mono text-2xl">{balance === null ? "…" : formatEther(balance)}</span>
268
+ <span className="text-muted-foreground text-sm">{symbol}</span>
269
+ </div>
270
+ <p className="text-muted-foreground text-xs">{note}</p>
271
+ </div>
272
+ );
273
+ }
274
+
275
+ function SetupPanel() {
276
+ return (
277
+ <div className="flex flex-col gap-4 rounded-xl border p-8">
278
+ <div className="flex flex-col gap-2">
279
+ <h1 className="text-2xl font-semibold tracking-tight">Spin up your cross-chain bridge</h1>
280
+ <p className="text-muted-foreground text-sm">
281
+ This app bridges an ERC-20 between two Avalanche L1s using Interchain Token Transfer. Bring
282
+ up the two chains, a relayer, and the full bridge — one command in your terminal:
283
+ </p>
284
+ </div>
285
+ <CopyCommand command="pnpm bridge" />
286
+ <p className="text-muted-foreground text-xs">
287
+ Needs{" "}
288
+ <a
289
+ href="https://build.avax.network/docs/tooling/avalanche-cli/get-avalanche-cli"
290
+ target="_blank"
291
+ rel="noreferrer"
292
+ className="underline underline-offset-4"
293
+ >
294
+ avalanche-cli
295
+ </a>
296
+ . When it finishes it writes <span className="font-mono">bridge.config.json</span> and this
297
+ page becomes the bridge automatically.
298
+ </p>
299
+ </div>
300
+ );
301
+ }
302
+
303
+ function CopyCommand({ command }: { command: string }) {
304
+ const [copied, setCopied] = useState(false);
305
+ return (
306
+ <button
307
+ type="button"
308
+ onClick={() => {
309
+ void navigator.clipboard?.writeText(command);
310
+ setCopied(true);
311
+ setTimeout(() => setCopied(false), 1500);
312
+ }}
313
+ 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"
314
+ >
315
+ <span>
316
+ <span className="text-muted-foreground select-none">$ </span>
317
+ {command}
318
+ </span>
319
+ {copied ? <Check className="size-4 shrink-0" /> : <Copy className="text-muted-foreground size-4 shrink-0" />}
320
+ </button>
321
+ );
322
+ }
323
+
324
+ function ThemeToggle() {
325
+ const { resolvedTheme, setTheme } = useTheme();
326
+ return (
327
+ <Button
328
+ variant="outline"
329
+ size="icon"
330
+ aria-label="Toggle theme"
331
+ onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
332
+ >
333
+ <Sun className="hidden size-4 dark:block" />
334
+ <Moon className="block size-4 dark:hidden" />
335
+ </Button>
336
+ );
337
+ }
338
+
339
+ function Shell({ children }: { children: React.ReactNode }) {
340
+ return (
341
+ <div className="mx-auto flex min-h-dvh max-w-2xl flex-col gap-8 px-6 py-16">
342
+ <header className="flex items-center justify-between">
343
+ <span className="font-mono text-sm font-semibold">__PROJECT_NAME__</span>
344
+ <div className="flex items-center gap-2">
345
+ <ConnectAvalanche />
346
+ <ThemeToggle />
347
+ </div>
348
+ </header>
349
+ {children}
350
+ </div>
351
+ );
352
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ description: AvaKit ICTT token-bridge conventions for this project
3
+ globs: ["**/*.ts", "**/*.tsx", "**/*.mjs", "**/*.sh", "**/*.css"]
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # AvaKit ICTT token-bridge project rules
8
+
9
+ Bridge an ERC-20 between two Avalanche L1s with Interchain Token Transfer.
10
+
11
+ ## Flow
12
+
13
+ - `pnpm bridge` (scripts/bridge.sh) spins up 2 L1s + relayer, then deploy-bridge.mjs deploys a demo
14
+ ERC-20 + TeleporterRegistry + Home (chain1) + Registry + Remote (chain2) and registers them.
15
+ - `lib/ictt.ts` exposes `homeChain`, `remoteChain`, the addresses, ABIs, `blockchainIdOf`, `isConfigured`.
16
+ - `components/demo.tsx` reads balances and bridges via `home.send` / `remote.send`.
17
+
18
+ ## Bridging
19
+
20
+ - home→remote: `approve(home, amount)` on the token, then `home.send(SendTokensInput, amount)` (locks + ICM).
21
+ - remote→home: `remote.send(SendTokensInput, amount)` (burns the bridged token; no approval).
22
+ - SendTokensInput fields: destinationBlockchainID (bytes32 via `blockchainIdOf`), destinationTokenTransferrerAddress,
23
+ recipient, primaryFeeTokenAddress (0x0 on local), primaryFee (0), secondaryFee (0), requiredGasLimit (250000),
24
+ multiHopFallback (0x0).
25
+
26
+ ## Contracts
27
+
28
+ - Bundled as bytecode in `lib/ictt-artifacts.json`, compiled from ava-labs/icm-contracts with the
29
+ optimizer (keeps Home/Remote under the 24 KB code-size limit). No Solidity toolchain needed to deploy.
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
+ - Amounts are 18-decimal — always parseEther/formatEther.
36
+ - EWOQ (`0x56289e…8027`) is a PUBLIC local-only dev key — never use it or a real key on Fuji/mainnet.
@@ -0,0 +1,7 @@
1
+ # This template needs no env vars for the local flow — the two chains' RPCs and
2
+ # the deployed bridge addresses live in bridge.config.json (written by
3
+ # `pnpm bridge`), and you connect with a browser wallet.
4
+ #
5
+ # Import avalanche-cli's public EWOQ dev key (printed by `pnpm bridge`) into your
6
+ # wallet — it's pre-funded on both local chains. LOCAL ONLY, never a real key:
7
+ # 0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027
@@ -0,0 +1,15 @@
1
+ node_modules/
2
+ .next/
3
+ out/
4
+ *.tsbuildinfo
5
+ next-env.d.ts
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ .DS_Store
10
+ *.log
11
+
12
+ # Foundry
13
+ contracts/out/
14
+ contracts/cache/
15
+ contracts/broadcast/