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.
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/l1-launch/CLAUDE.md +38 -0
- package/templates/l1-launch/components/demo.tsx +99 -1
- 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,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
|