create-avalanche-app 0.1.2 → 0.1.4
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 +5 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/eerc-token/CLAUDE.md +101 -0
- package/templates/eerc-token/README.md +37 -0
- package/templates/eerc-token/app/globals.css +95 -0
- package/templates/eerc-token/app/layout.tsx +23 -0
- package/templates/eerc-token/app/page.tsx +5 -0
- package/templates/eerc-token/app/providers.tsx +53 -0
- package/templates/eerc-token/components/demo.tsx +250 -0
- package/templates/eerc-token/cursor/rules/avakit.mdc +41 -0
- package/templates/eerc-token/env.example +4 -0
- package/templates/eerc-token/gitignore +10 -0
- package/templates/eerc-token/lib/eerc-config.ts +41 -0
- package/templates/eerc-token/llms.txt +25 -0
- package/templates/eerc-token/manifest.json +6 -0
- package/templates/eerc-token/next.config.ts +7 -0
- package/templates/eerc-token/package.json +35 -0
- package/templates/eerc-token/pnpm-workspace.yaml +16 -0
- package/templates/eerc-token/postcss.config.mjs +7 -0
- package/templates/eerc-token/tsconfig.json +23 -0
- package/templates/erc20-token/pnpm-workspace.yaml +11 -0
- package/templates/icm-messenger/pnpm-workspace.yaml +11 -0
- package/templates/minimal/pnpm-workspace.yaml +11 -0
- package/templates/nft-mint/pnpm-workspace.yaml +11 -0
- package/templates/token-gated-app/pnpm-workspace.yaml +11 -0
package/dist/index.js
CHANGED
|
@@ -10,7 +10,8 @@ import { existsSync, readdirSync } from "fs";
|
|
|
10
10
|
import path from "path";
|
|
11
11
|
import * as p from "@clack/prompts";
|
|
12
12
|
import pc from "picocolors";
|
|
13
|
-
var VERSION = "0.1.
|
|
13
|
+
var VERSION = "0.1.4";
|
|
14
|
+
var AVAKIT_DEP_VERSION = "0.1.2";
|
|
14
15
|
function parseArgs(argv) {
|
|
15
16
|
const opts = { yes: false, local: false, install: true };
|
|
16
17
|
const rest = argv.slice(2);
|
|
@@ -70,7 +71,8 @@ function printHelp() {
|
|
|
70
71
|
"Usage: npm create avalanche-app@latest [name] [options]",
|
|
71
72
|
"",
|
|
72
73
|
"Options:",
|
|
73
|
-
" -t, --template <id> minimal | nft-mint | token-gated-app | erc20-token",
|
|
74
|
+
" -t, --template <id> minimal | nft-mint | token-gated-app | erc20-token |",
|
|
75
|
+
" icm-messenger | eerc-token",
|
|
74
76
|
" -w, --wallet <id> web3auth | injected (default: web3auth)",
|
|
75
77
|
" -c, --chain <id> fuji | c-chain (default: fuji)",
|
|
76
78
|
" --pm <manager> pnpm | npm | yarn | bun",
|
|
@@ -170,7 +172,7 @@ Directory "${resolved.projectName}" already exists and is not empty.
|
|
|
170
172
|
wallet: resolved.wallet,
|
|
171
173
|
chain: resolved.chain,
|
|
172
174
|
local: opts.local,
|
|
173
|
-
avakitVersion:
|
|
175
|
+
avakitVersion: AVAKIT_DEP_VERSION
|
|
174
176
|
});
|
|
175
177
|
spin?.stop(`Created ${files.length} files`);
|
|
176
178
|
if (opts.install) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync, readdirSync } from \"node:fs\";\nimport path from \"node:path\";\nimport * as p from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { type ChainId, listTemplates, scaffoldApp, type WalletId } from \"./api.js\";\n\nconst VERSION = \"0.1.2\";\n\ntype PackageManager = \"pnpm\" | \"npm\" | \"yarn\" | \"bun\";\n\ninterface Options {\n projectName?: string;\n template?: string;\n wallet?: WalletId;\n chain?: ChainId;\n pm?: PackageManager;\n yes: boolean;\n local: boolean;\n install: boolean;\n}\n\nfunction parseArgs(argv: string[]): Options {\n const opts: Options = { yes: false, local: false, install: true };\n const rest = argv.slice(2);\n for (let i = 0; i < rest.length; i++) {\n const arg = rest[i];\n const next = () => rest[++i];\n switch (arg) {\n case \"--yes\":\n case \"-y\":\n opts.yes = true;\n break;\n case \"--local\":\n opts.local = true;\n break;\n case \"--no-install\":\n opts.install = false;\n break;\n case \"--template\":\n case \"-t\":\n opts.template = next();\n break;\n case \"--wallet\":\n case \"-w\":\n opts.wallet = next() as WalletId;\n break;\n case \"--chain\":\n case \"-c\":\n opts.chain = next() as ChainId;\n break;\n case \"--pm\":\n opts.pm = next() as PackageManager;\n break;\n case \"--version\":\n case \"-v\":\n process.stdout.write(`${VERSION}\\n`);\n process.exit(0);\n break;\n case \"--help\":\n case \"-h\":\n printHelp();\n process.exit(0);\n break;\n default:\n if (arg && !arg.startsWith(\"-\") && !opts.projectName) {\n opts.projectName = arg;\n }\n }\n }\n return opts;\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n \"create-avalanche-app — scaffold a batteries-included Avalanche dapp\",\n \"\",\n \"Usage: npm create avalanche-app@latest [name] [options]\",\n \"\",\n \"Options:\",\n \" -t, --template <id> minimal | nft-mint | token-gated-app | erc20-token\",\n \" -w, --wallet <id> web3auth | injected (default: web3auth)\",\n \" -c, --chain <id> fuji | c-chain (default: fuji)\",\n \" --pm <manager> pnpm | npm | yarn | bun\",\n \" -y, --yes skip prompts (non-interactive)\",\n \" --no-install do not install dependencies\",\n \" --local link @avakit/* via workspace (repo dev only)\",\n \" -v, --version print version\",\n \" -h, --help print this help\",\n \"\",\n ].join(\"\\n\"),\n );\n}\n\nfunction isValidName(name: string): boolean {\n return /^[a-z0-9][a-z0-9._-]*$/.test(name);\n}\n\nasync function resolveOptions(\n opts: Options,\n): Promise<Required<Omit<Options, \"yes\" | \"local\" | \"install\">>> {\n const templates = listTemplates();\n const templateIds = templates.map((t) => t.id);\n\n if (opts.yes) {\n const projectName = opts.projectName ?? \"my-avax-app\";\n return {\n projectName,\n template: opts.template && templateIds.includes(opts.template) ? opts.template : \"minimal\",\n wallet: opts.wallet ?? \"web3auth\",\n chain: opts.chain ?? \"fuji\",\n pm: opts.pm ?? \"pnpm\",\n };\n }\n\n p.intro(pc.bgCyan(pc.black(\" create-avalanche-app \")));\n\n const projectName =\n opts.projectName ??\n (await p.text({\n message: \"Project name?\",\n placeholder: \"my-avax-app\",\n defaultValue: \"my-avax-app\",\n validate: (v) => (!v || isValidName(v) ? undefined : \"Use lowercase letters, digits, - . _\"),\n }));\n if (p.isCancel(projectName)) cancel();\n\n const template =\n opts.template ??\n (await p.select({\n message: \"Template?\",\n options: templates.map((t) => ({ value: t.id, label: t.title, hint: t.description })),\n initialValue: \"minimal\",\n }));\n if (p.isCancel(template)) cancel();\n\n const wallet =\n opts.wallet ??\n (await p.select({\n message: \"Wallet provider?\",\n options: [\n { value: \"web3auth\", label: \"Social login (Web3Auth)\", hint: \"free, recommended\" },\n { value: \"injected\", label: \"Browser wallet (Core / MetaMask)\" },\n ],\n initialValue: \"web3auth\",\n }));\n if (p.isCancel(wallet)) cancel();\n\n const chain =\n opts.chain ??\n (await p.select({\n message: \"Target chain?\",\n options: [\n { value: \"fuji\", label: \"Avalanche Fuji (testnet)\", hint: \"recommended\" },\n { value: \"c-chain\", label: \"Avalanche C-Chain (mainnet)\" },\n ],\n initialValue: \"fuji\",\n }));\n if (p.isCancel(chain)) cancel();\n\n const pm =\n opts.pm ??\n (await p.select({\n message: \"Package manager?\",\n options: ([\"pnpm\", \"npm\", \"yarn\", \"bun\"] as const).map((m) => ({ value: m, label: m })),\n initialValue: \"pnpm\",\n }));\n if (p.isCancel(pm)) cancel();\n\n return {\n projectName: projectName as string,\n template: template as string,\n wallet: wallet as WalletId,\n chain: chain as ChainId,\n pm: pm as PackageManager,\n };\n}\n\nfunction cancel(): never {\n p.cancel(\"Cancelled.\");\n process.exit(0);\n}\n\nasync function main(): Promise<void> {\n const opts = parseArgs(process.argv);\n const resolved = await resolveOptions(opts);\n\n const targetDir = path.resolve(process.cwd(), resolved.projectName);\n if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {\n process.stderr.write(\n pc.red(`\\nDirectory \"${resolved.projectName}\" already exists and is not empty.\\n`),\n );\n process.exit(1);\n }\n\n const spin = opts.yes ? null : p.spinner();\n spin?.start(\"Scaffolding project\");\n const { files } = await scaffoldApp({\n projectName: resolved.projectName,\n targetDir,\n template: resolved.template,\n wallet: resolved.wallet,\n chain: resolved.chain,\n local: opts.local,\n avakitVersion: VERSION,\n });\n spin?.stop(`Created ${files.length} files`);\n\n if (opts.install) {\n const installSpin = opts.yes ? null : p.spinner();\n installSpin?.start(`Installing dependencies with ${resolved.pm}`);\n const result = spawnSync(resolved.pm, [\"install\"], {\n cwd: targetDir,\n stdio: opts.yes ? \"inherit\" : \"ignore\",\n });\n if (result.status === 0) {\n installSpin?.stop(\"Dependencies installed\");\n } else {\n installSpin?.stop(pc.yellow(\"Install skipped/failed — run it manually\"));\n }\n }\n\n const next = [\n `cd ${resolved.projectName}`,\n ...(opts.install ? [] : [`${resolved.pm} install`]),\n ...(resolved.wallet === \"web3auth\"\n ? [\"cp .env.example .env.local # add your Web3Auth client ID\"]\n : []),\n `${resolved.pm} run dev`,\n ];\n\n if (opts.yes) {\n process.stdout.write(`\\nDone. Next steps:\\n ${next.join(\"\\n \")}\\n`);\n } else {\n p.note(next.join(\"\\n\"), \"Next steps\");\n p.outro(pc.green(\"Your Avalanche dapp is ready.\"));\n }\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `\\n${pc.red(\"Error:\")} ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n process.exit(1);\n});\n"],"mappings":"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","names":["projectName","cancel"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync, readdirSync } from \"node:fs\";\nimport path from \"node:path\";\nimport * as p from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { type ChainId, listTemplates, scaffoldApp, type WalletId } from \"./api.js\";\n\nconst VERSION = \"0.1.4\";\n\n// The @avakit/* dependency version stamped into scaffolded apps' package.json\n// (as `^AVAKIT_DEP_VERSION`). Kept separate from the CLI's own VERSION: it must\n// resolve every published @avakit package, so it tracks the LOWEST current\n// @avakit version (core 0.1.2 · react 0.1.3 → ^0.1.2 satisfies both). Bump only\n// when the minimum @avakit version a fresh app needs goes up.\nconst AVAKIT_DEP_VERSION = \"0.1.2\";\n\ntype PackageManager = \"pnpm\" | \"npm\" | \"yarn\" | \"bun\";\n\ninterface Options {\n projectName?: string;\n template?: string;\n wallet?: WalletId;\n chain?: ChainId;\n pm?: PackageManager;\n yes: boolean;\n local: boolean;\n install: boolean;\n}\n\nfunction parseArgs(argv: string[]): Options {\n const opts: Options = { yes: false, local: false, install: true };\n const rest = argv.slice(2);\n for (let i = 0; i < rest.length; i++) {\n const arg = rest[i];\n const next = () => rest[++i];\n switch (arg) {\n case \"--yes\":\n case \"-y\":\n opts.yes = true;\n break;\n case \"--local\":\n opts.local = true;\n break;\n case \"--no-install\":\n opts.install = false;\n break;\n case \"--template\":\n case \"-t\":\n opts.template = next();\n break;\n case \"--wallet\":\n case \"-w\":\n opts.wallet = next() as WalletId;\n break;\n case \"--chain\":\n case \"-c\":\n opts.chain = next() as ChainId;\n break;\n case \"--pm\":\n opts.pm = next() as PackageManager;\n break;\n case \"--version\":\n case \"-v\":\n process.stdout.write(`${VERSION}\\n`);\n process.exit(0);\n break;\n case \"--help\":\n case \"-h\":\n printHelp();\n process.exit(0);\n break;\n default:\n if (arg && !arg.startsWith(\"-\") && !opts.projectName) {\n opts.projectName = arg;\n }\n }\n }\n return opts;\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n \"create-avalanche-app — scaffold a batteries-included Avalanche dapp\",\n \"\",\n \"Usage: npm create avalanche-app@latest [name] [options]\",\n \"\",\n \"Options:\",\n \" -t, --template <id> minimal | nft-mint | token-gated-app | erc20-token |\",\n \" icm-messenger | eerc-token\",\n \" -w, --wallet <id> web3auth | injected (default: web3auth)\",\n \" -c, --chain <id> fuji | c-chain (default: fuji)\",\n \" --pm <manager> pnpm | npm | yarn | bun\",\n \" -y, --yes skip prompts (non-interactive)\",\n \" --no-install do not install dependencies\",\n \" --local link @avakit/* via workspace (repo dev only)\",\n \" -v, --version print version\",\n \" -h, --help print this help\",\n \"\",\n ].join(\"\\n\"),\n );\n}\n\nfunction isValidName(name: string): boolean {\n return /^[a-z0-9][a-z0-9._-]*$/.test(name);\n}\n\nasync function resolveOptions(\n opts: Options,\n): Promise<Required<Omit<Options, \"yes\" | \"local\" | \"install\">>> {\n const templates = listTemplates();\n const templateIds = templates.map((t) => t.id);\n\n if (opts.yes) {\n const projectName = opts.projectName ?? \"my-avax-app\";\n return {\n projectName,\n template: opts.template && templateIds.includes(opts.template) ? opts.template : \"minimal\",\n wallet: opts.wallet ?? \"web3auth\",\n chain: opts.chain ?? \"fuji\",\n pm: opts.pm ?? \"pnpm\",\n };\n }\n\n p.intro(pc.bgCyan(pc.black(\" create-avalanche-app \")));\n\n const projectName =\n opts.projectName ??\n (await p.text({\n message: \"Project name?\",\n placeholder: \"my-avax-app\",\n defaultValue: \"my-avax-app\",\n validate: (v) => (!v || isValidName(v) ? undefined : \"Use lowercase letters, digits, - . _\"),\n }));\n if (p.isCancel(projectName)) cancel();\n\n const template =\n opts.template ??\n (await p.select({\n message: \"Template?\",\n options: templates.map((t) => ({ value: t.id, label: t.title, hint: t.description })),\n initialValue: \"minimal\",\n }));\n if (p.isCancel(template)) cancel();\n\n const wallet =\n opts.wallet ??\n (await p.select({\n message: \"Wallet provider?\",\n options: [\n { value: \"web3auth\", label: \"Social login (Web3Auth)\", hint: \"free, recommended\" },\n { value: \"injected\", label: \"Browser wallet (Core / MetaMask)\" },\n ],\n initialValue: \"web3auth\",\n }));\n if (p.isCancel(wallet)) cancel();\n\n const chain =\n opts.chain ??\n (await p.select({\n message: \"Target chain?\",\n options: [\n { value: \"fuji\", label: \"Avalanche Fuji (testnet)\", hint: \"recommended\" },\n { value: \"c-chain\", label: \"Avalanche C-Chain (mainnet)\" },\n ],\n initialValue: \"fuji\",\n }));\n if (p.isCancel(chain)) cancel();\n\n const pm =\n opts.pm ??\n (await p.select({\n message: \"Package manager?\",\n options: ([\"pnpm\", \"npm\", \"yarn\", \"bun\"] as const).map((m) => ({ value: m, label: m })),\n initialValue: \"pnpm\",\n }));\n if (p.isCancel(pm)) cancel();\n\n return {\n projectName: projectName as string,\n template: template as string,\n wallet: wallet as WalletId,\n chain: chain as ChainId,\n pm: pm as PackageManager,\n };\n}\n\nfunction cancel(): never {\n p.cancel(\"Cancelled.\");\n process.exit(0);\n}\n\nasync function main(): Promise<void> {\n const opts = parseArgs(process.argv);\n const resolved = await resolveOptions(opts);\n\n const targetDir = path.resolve(process.cwd(), resolved.projectName);\n if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {\n process.stderr.write(\n pc.red(`\\nDirectory \"${resolved.projectName}\" already exists and is not empty.\\n`),\n );\n process.exit(1);\n }\n\n const spin = opts.yes ? null : p.spinner();\n spin?.start(\"Scaffolding project\");\n const { files } = await scaffoldApp({\n projectName: resolved.projectName,\n targetDir,\n template: resolved.template,\n wallet: resolved.wallet,\n chain: resolved.chain,\n local: opts.local,\n avakitVersion: AVAKIT_DEP_VERSION,\n });\n spin?.stop(`Created ${files.length} files`);\n\n if (opts.install) {\n const installSpin = opts.yes ? null : p.spinner();\n installSpin?.start(`Installing dependencies with ${resolved.pm}`);\n const result = spawnSync(resolved.pm, [\"install\"], {\n cwd: targetDir,\n stdio: opts.yes ? \"inherit\" : \"ignore\",\n });\n if (result.status === 0) {\n installSpin?.stop(\"Dependencies installed\");\n } else {\n installSpin?.stop(pc.yellow(\"Install skipped/failed — run it manually\"));\n }\n }\n\n const next = [\n `cd ${resolved.projectName}`,\n ...(opts.install ? [] : [`${resolved.pm} install`]),\n ...(resolved.wallet === \"web3auth\"\n ? [\"cp .env.example .env.local # add your Web3Auth client ID\"]\n : []),\n `${resolved.pm} run dev`,\n ];\n\n if (opts.yes) {\n process.stdout.write(`\\nDone. Next steps:\\n ${next.join(\"\\n \")}\\n`);\n } else {\n p.note(next.join(\"\\n\"), \"Next steps\");\n p.outro(pc.green(\"Your Avalanche dapp is ready.\"));\n }\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `\\n${pc.red(\"Error:\")} ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n process.exit(1);\n});\n"],"mappings":"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","names":["projectName","cancel"]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# __PROJECT_NAME__ — Confidential token (eERC) dapp (scaffolded with AvaKit)
|
|
2
|
+
|
|
3
|
+
Operational guide for AI agents (Claude Code / Cursor) working in this project.
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
Next.js 16 (App Router) · React 19 · `@avakit/react` · `@avakit/core` · `@avalabs/eerc-sdk` · wagmi · viem · shadcn/ui · next-themes
|
|
8
|
+
|
|
9
|
+
## What this is
|
|
10
|
+
|
|
11
|
+
A demo of Avalanche's **Encrypted ERC (eERC)** standard: token balances and transfer amounts are
|
|
12
|
+
hidden using zero-knowledge proofs (Groth16) and ElGamal encryption over BabyJubJub, while the
|
|
13
|
+
recipient/sender addresses stay public on-chain. This template points at a **shared, pre-deployed
|
|
14
|
+
standalone eERC instance on Fuji** (`lib/eerc-config.ts`) — there is no `contracts/` directory and
|
|
15
|
+
no browser-deploy step, unlike AvaKit's other templates.
|
|
16
|
+
|
|
17
|
+
## Why this template deviates from the others
|
|
18
|
+
|
|
19
|
+
- **No bundled contracts.** eERC's contracts depend on real cryptographic primitives (Poseidon
|
|
20
|
+
hashing, a BabyJubJub library, four separate Groth16 verifiers) generated from circuits — not the
|
|
21
|
+
"self-contained, zero-dependency Solidity" pattern other AvaKit templates use. Vendoring the
|
|
22
|
+
circuits/contracts would also pull in Ava Labs' Ecosystem License (not MIT) into an MIT repo, so
|
|
23
|
+
this template consumes the official `@avalabs/eerc-sdk` (ISC-licensed) as an npm dependency
|
|
24
|
+
instead of vendoring source.
|
|
25
|
+
- **Circuits load from a CDN, not the bundle.** Proof generation (register/mint/transfer/withdraw/burn)
|
|
26
|
+
runs client-side via snarkjs against `.wasm`/`.zkey` circuit files (~2–14 MB each). `lib/eerc-config.ts`
|
|
27
|
+
points at `ava-labs/EncryptedERC`'s committed `circom/build/` directory via jsDelivr's GitHub CDN,
|
|
28
|
+
pinned to a commit hash, so nothing is vendored in this package and nobody has to compile circuits.
|
|
29
|
+
- **wagmi is present alongside `@avakit/react`.** `@avalabs/eerc-sdk`'s hooks use `wagmi`'s
|
|
30
|
+
`useReadContract`/`useBlockNumber` internally for balance/state reads. `app/providers.tsx` wraps
|
|
31
|
+
the app in a `WagmiProvider` (config pointed at Fuji) purely to satisfy that — wallet connect and
|
|
32
|
+
signing still go through `AvaKitProvider`'s adapters (Web3Auth / injected); wagmi never owns the
|
|
33
|
+
connected account.
|
|
34
|
+
- **Pinned wagmi 2.x, not the newest major.** `@avalabs/eerc-sdk`'s peer dependency is `wagmi: ^2.0.0`;
|
|
35
|
+
a newer wagmi major would violate that contract, so this template intentionally does not follow
|
|
36
|
+
AvaKit's usual "latest stable of everything" rule for this one package.
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
- `lib/eerc-config.ts` — the deployed `EncryptedERC` contract address (standalone, name "Test",
|
|
41
|
+
symbol "TEST", 2 decimals) and the circuit URLs.
|
|
42
|
+
- `app/providers.tsx` — `<AvaKitProvider>` (wallet) + `<WagmiProvider>` (eERC SDK internals) + `ThemeProvider`.
|
|
43
|
+
- `components/demo.tsx` — register → unlock → mint → confidential transfer → burn.
|
|
44
|
+
|
|
45
|
+
## The flow
|
|
46
|
+
|
|
47
|
+
1. Connect a wallet (`<ConnectAvalanche />`).
|
|
48
|
+
2. Build viem clients: `getPublicClient(chain)` from `@avakit/core`, and a **wallet client with an
|
|
49
|
+
account attached** — `createWalletClient({ chain, transport: custom(provider), account: address })`.
|
|
50
|
+
(`@avakit/core`'s `getWalletClient` does *not* attach an account; the eERC SDK requires
|
|
51
|
+
`wallet.account.address` to be set, so build the client directly with viem instead.)
|
|
52
|
+
3. `useEERC(publicClient, walletClient, EERC_CONTRACT_ADDRESS, circuitURLs)` from `@avalabs/eerc-sdk`.
|
|
53
|
+
4. **Register** (one-time, on-chain): `eerc.register()`. Generates a BabyJubJub keypair from a wallet
|
|
54
|
+
signature and calls `Registrar.register()` with a ZK proof. Idempotent — safe to call again.
|
|
55
|
+
5. **Unlock** (per session, off-chain): `eerc.generateDecryptionKey()`. Re-derives the same
|
|
56
|
+
deterministic decryption key from a signature so the browser can decrypt your balance. No gas.
|
|
57
|
+
6. `const balance = eerc.useEncryptedBalance()` → `privateMint`, `privateTransfer`, `privateBurn`,
|
|
58
|
+
`decryptedBalance`, `parsedDecryptedBalance`, `refetchBalance`.
|
|
59
|
+
7. **Mint** — `balance.privateMint(recipient, amountInBaseUnits)`. **Owner-only on-chain**
|
|
60
|
+
(`EncryptedERC.privateMint` is `onlyOwner` by design, for compliance) — only the wallet that
|
|
61
|
+
deployed the contract can mint. Works out of the box only if you deploy your own instance and
|
|
62
|
+
connect with the deployer wallet.
|
|
63
|
+
8. **Transfer** — `balance.privateTransfer(to, amountInBaseUnits)`. Permissionless for any two
|
|
64
|
+
*registered* accounts; amounts and balances stay encrypted on-chain.
|
|
65
|
+
9. **Burn** — `balance.privateBurn(amountInBaseUnits)`. Permissionless, standalone-mode only.
|
|
66
|
+
|
|
67
|
+
There is no `withdraw`/`deposit` step in this demo — those convert to/from an underlying public
|
|
68
|
+
ERC-20 in eERC's **converter mode**; this template deploys **standalone mode** (the token only
|
|
69
|
+
exists in encrypted form).
|
|
70
|
+
|
|
71
|
+
## Deploying your own instance
|
|
72
|
+
|
|
73
|
+
The shared demo contract has no auditor key requirements you control and mint is owner-gated. To
|
|
74
|
+
mint from your own wallet:
|
|
75
|
+
|
|
76
|
+
1. Clone `ava-labs/EncryptedERC`, `npm install --ignore-scripts` (circuits are already committed
|
|
77
|
+
under `circom/build/`, no need to run `hardhat zkit make`), `npx hardhat compile`.
|
|
78
|
+
2. Add a `fuji` network to `hardhat.config.ts` (RPC `https://api.avax-test.network/ext/bc/C/rpc`,
|
|
79
|
+
chainId `43113`) with your deployer's private key.
|
|
80
|
+
3. `npx hardhat run scripts/deploy-standalone.ts --network fuji` — deploys verifiers + Registrar +
|
|
81
|
+
`EncryptedERC` (standalone) and prints the addresses.
|
|
82
|
+
4. Register your deployer address via the SDK (or this app), then call
|
|
83
|
+
`setAuditorPublicKey(yourAddress)` on the `EncryptedERC` contract as the owner — an auditor must
|
|
84
|
+
be registered *and* set before any mint/transfer/burn will succeed.
|
|
85
|
+
5. Update `EERC_CONTRACT_ADDRESS` in `lib/eerc-config.ts`.
|
|
86
|
+
|
|
87
|
+
## Rules
|
|
88
|
+
|
|
89
|
+
- shadcn/ui only; `@avakit/react` components are shadcn-styled; plain Tailwind-styled `<input>` for
|
|
90
|
+
amount/address fields (no separate shadcn Input component is bundled by `@avakit/react`).
|
|
91
|
+
- Black & white only for now; dark/light via next-themes; both must work.
|
|
92
|
+
- Animations: Framer Motion or GSAP only.
|
|
93
|
+
- Amounts are in the token's base units (2 decimals here) — always convert with `parseUnits` /
|
|
94
|
+
`formatUnits`, read `decimals` from `balance.decimals` rather than hardcoding it.
|
|
95
|
+
- Never hardcode private keys; wallet signing always goes through the connected adapter.
|
|
96
|
+
- Default chain is Fuji testnet; mainnet requires explicit opt-in.
|
|
97
|
+
|
|
98
|
+
## Commands
|
|
99
|
+
|
|
100
|
+
- `pnpm dev` — dev server (http://localhost:3000)
|
|
101
|
+
- `pnpm build` — production build
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
An Avalanche **confidential token** dapp scaffolded with [AvaKit](https://github.com/mericcintosun/AvaKit), built on Avalanche's [Encrypted ERC (eERC)](https://github.com/ava-labs/EncryptedERC) standard. Register, mint, and transfer tokens with hidden balances and amounts — right from your browser.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. (social login, optional) add a free Web3Auth client ID
|
|
9
|
+
cp .env.example .env.local
|
|
10
|
+
# → https://dashboard.web3auth.io (Sapphire Devnet, EVM)
|
|
11
|
+
|
|
12
|
+
# 2. run it
|
|
13
|
+
pnpm dev # http://localhost:3000
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then: connect a wallet → **Register** (one-time, on-chain) → **Unlock private balance** (free,
|
|
17
|
+
off-chain) → mint / confidential transfer / burn. These cost gas (except unlock), so fund your
|
|
18
|
+
wallet on Fuji first (in-app faucet link).
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
- Points at a shared, pre-deployed standalone eERC instance on Fuji (`lib/eerc-config.ts`) — no
|
|
23
|
+
contract deployment needed to try the demo.
|
|
24
|
+
- Proofs (Groth16, via snarkjs) are generated entirely in the browser. Circuit `.wasm`/`.zkey`
|
|
25
|
+
files load from a CDN (pinned to a commit of `ava-labs/EncryptedERC`), not bundled in this repo.
|
|
26
|
+
- Minting is **owner-only** on the shared instance (eERC design, for compliance) — deploy your own
|
|
27
|
+
instance to mint from your own wallet. See `CLAUDE.md` for the deploy steps. Confidential
|
|
28
|
+
transfer and burn work for any registered wallet.
|
|
29
|
+
|
|
30
|
+
## Stack
|
|
31
|
+
|
|
32
|
+
Next.js 16 · `@avakit/react` · `@avakit/core` · `@avalabs/eerc-sdk` · wagmi · viem · shadcn/ui
|
|
33
|
+
|
|
34
|
+
## AI-native
|
|
35
|
+
|
|
36
|
+
Ships with `CLAUDE.md`, `llms.txt`, and `.cursor/rules` so Claude Code / Cursor understand the
|
|
37
|
+
confidential-token flow and the (deliberate) ways this template differs from AvaKit's others.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
/* Scan @avakit/react's shadcn-style classes from node_modules. */
|
|
5
|
+
@source "../node_modules/@avakit/react/dist";
|
|
6
|
+
|
|
7
|
+
@custom-variant dark (&:is(.dark *));
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* Black & white only (oklch chroma 0 = pure grayscale). Dark/light wired from
|
|
11
|
+
* day one via next-themes. Add brand colors later by editing these tokens —
|
|
12
|
+
* components never hardcode colors.
|
|
13
|
+
*/
|
|
14
|
+
:root {
|
|
15
|
+
--radius: 0.625rem;
|
|
16
|
+
--background: oklch(1 0 0);
|
|
17
|
+
--foreground: oklch(0.145 0 0);
|
|
18
|
+
--card: oklch(1 0 0);
|
|
19
|
+
--card-foreground: oklch(0.145 0 0);
|
|
20
|
+
--popover: oklch(1 0 0);
|
|
21
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
22
|
+
--primary: oklch(0.205 0 0);
|
|
23
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
24
|
+
--secondary: oklch(0.97 0 0);
|
|
25
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
26
|
+
--muted: oklch(0.97 0 0);
|
|
27
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
28
|
+
--accent: oklch(0.97 0 0);
|
|
29
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
30
|
+
--destructive: oklch(0.3 0 0);
|
|
31
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
32
|
+
--border: oklch(0.922 0 0);
|
|
33
|
+
--input: oklch(0.922 0 0);
|
|
34
|
+
--ring: oklch(0.708 0 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.dark {
|
|
38
|
+
--background: oklch(0.145 0 0);
|
|
39
|
+
--foreground: oklch(0.985 0 0);
|
|
40
|
+
--card: oklch(0.205 0 0);
|
|
41
|
+
--card-foreground: oklch(0.985 0 0);
|
|
42
|
+
--popover: oklch(0.205 0 0);
|
|
43
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
44
|
+
--primary: oklch(0.985 0 0);
|
|
45
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
46
|
+
--secondary: oklch(0.269 0 0);
|
|
47
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
48
|
+
--muted: oklch(0.269 0 0);
|
|
49
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
50
|
+
--accent: oklch(0.269 0 0);
|
|
51
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
52
|
+
--destructive: oklch(0.7 0 0);
|
|
53
|
+
--destructive-foreground: oklch(0.205 0 0);
|
|
54
|
+
--border: oklch(1 0 0 / 10%);
|
|
55
|
+
--input: oklch(1 0 0 / 15%);
|
|
56
|
+
--ring: oklch(0.556 0 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@theme inline {
|
|
60
|
+
--color-background: var(--background);
|
|
61
|
+
--color-foreground: var(--foreground);
|
|
62
|
+
--color-card: var(--card);
|
|
63
|
+
--color-card-foreground: var(--card-foreground);
|
|
64
|
+
--color-popover: var(--popover);
|
|
65
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
66
|
+
--color-primary: var(--primary);
|
|
67
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
68
|
+
--color-secondary: var(--secondary);
|
|
69
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
70
|
+
--color-muted: var(--muted);
|
|
71
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
72
|
+
--color-accent: var(--accent);
|
|
73
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
74
|
+
--color-destructive: var(--destructive);
|
|
75
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
76
|
+
--color-border: var(--border);
|
|
77
|
+
--color-input: var(--input);
|
|
78
|
+
--color-ring: var(--ring);
|
|
79
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
80
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
81
|
+
--radius-lg: var(--radius);
|
|
82
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
83
|
+
--font-sans: var(--font-geist-sans);
|
|
84
|
+
--font-mono: var(--font-geist-mono);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@layer base {
|
|
88
|
+
* {
|
|
89
|
+
border-color: var(--border);
|
|
90
|
+
}
|
|
91
|
+
body {
|
|
92
|
+
background-color: var(--background);
|
|
93
|
+
color: var(--foreground);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { Providers } from "./providers";
|
|
5
|
+
import "./globals.css";
|
|
6
|
+
|
|
7
|
+
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
|
|
8
|
+
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = {
|
|
11
|
+
title: "__PROJECT_NAME__",
|
|
12
|
+
description: "A confidential-token Avalanche dapp scaffolded with AvaKit.",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
|
16
|
+
return (
|
|
17
|
+
<html lang="en" suppressHydrationWarning>
|
|
18
|
+
<body className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}>
|
|
19
|
+
<Providers>{children}</Providers>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type WalletAdapter, injectedAdapter, toViemChain } from "@avakit/core";
|
|
4
|
+
import { __CHAIN_CONST__ } from "@avakit/core/chains";
|
|
5
|
+
import { web3authAdapter } from "@avakit/core/web3auth";
|
|
6
|
+
import { AvaKitProvider } from "@avakit/react";
|
|
7
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
8
|
+
import { ThemeProvider } from "next-themes";
|
|
9
|
+
import { type ReactNode, useMemo, useState } from "react";
|
|
10
|
+
import { WagmiProvider, http, createConfig } from "wagmi";
|
|
11
|
+
|
|
12
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
13
|
+
const adapters = useMemo(() => {
|
|
14
|
+
const list: WalletAdapter[] = [];
|
|
15
|
+
// Social login appears only when a Web3Auth client ID is configured.
|
|
16
|
+
const clientId = process.env.NEXT_PUBLIC_WEB3AUTH_CLIENT_ID;
|
|
17
|
+
if (clientId) {
|
|
18
|
+
list.push(web3authAdapter({ clientId }));
|
|
19
|
+
}
|
|
20
|
+
// Injected (Core / MetaMask) is always available.
|
|
21
|
+
list.push(injectedAdapter());
|
|
22
|
+
return list;
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
// The eERC SDK's read hooks (balance/state) are built on wagmi + react-query
|
|
26
|
+
// internally, so this template runs a wagmi config alongside AvaKitProvider
|
|
27
|
+
// purely to satisfy those reads. Wallet connect/sign still goes through
|
|
28
|
+
// AvaKitProvider's adapters (Web3Auth / injected) — wagmi never owns the
|
|
29
|
+
// account here, it only provides an RPC-backed read client. Derive the wagmi
|
|
30
|
+
// chain from the same __CHAIN_CONST__ AvaKit uses, so the two never diverge.
|
|
31
|
+
// (The shared eERC contract in lib/eerc-config.ts only exists on Fuji — deploy
|
|
32
|
+
// your own instance to use another chain; see CLAUDE.md.)
|
|
33
|
+
const [wagmiConfig] = useState(() => {
|
|
34
|
+
const viemChain = toViemChain(__CHAIN_CONST__);
|
|
35
|
+
return createConfig({
|
|
36
|
+
chains: [viemChain],
|
|
37
|
+
transports: { [viemChain.id]: http(__CHAIN_CONST__.rpcUrl) },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const [queryClient] = useState(() => new QueryClient());
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
|
44
|
+
<QueryClientProvider client={queryClient}>
|
|
45
|
+
<WagmiProvider config={wagmiConfig}>
|
|
46
|
+
<AvaKitProvider chains={[__CHAIN_CONST__]} adapters={adapters}>
|
|
47
|
+
{children}
|
|
48
|
+
</AvaKitProvider>
|
|
49
|
+
</WagmiProvider>
|
|
50
|
+
</QueryClientProvider>
|
|
51
|
+
</ThemeProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getPublicClient, toViemChain } from "@avakit/core";
|
|
4
|
+
import { Button, ConnectAvalanche, shortenAddress, useAvaKit } from "@avakit/react";
|
|
5
|
+
import { useEERC } from "@avalabs/eerc-sdk";
|
|
6
|
+
import { Moon, Sun } from "lucide-react";
|
|
7
|
+
import { useTheme } from "next-themes";
|
|
8
|
+
import { useMemo, useState } from "react";
|
|
9
|
+
import { type Address, createWalletClient, custom, formatUnits, parseUnits } from "viem";
|
|
10
|
+
import { circuitURLs, EERC_CONTRACT_ADDRESS } from "@/lib/eerc-config";
|
|
11
|
+
|
|
12
|
+
function ThemeToggle() {
|
|
13
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
14
|
+
return (
|
|
15
|
+
<Button
|
|
16
|
+
variant="outline"
|
|
17
|
+
size="icon"
|
|
18
|
+
aria-label="Toggle theme"
|
|
19
|
+
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
|
|
20
|
+
>
|
|
21
|
+
<Sun className="hidden size-4 dark:block" />
|
|
22
|
+
<Moon className="block size-4 dark:hidden" />
|
|
23
|
+
</Button>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Demo() {
|
|
28
|
+
const { address, provider, chain, status } = useAvaKit();
|
|
29
|
+
const isConnected = status === "connected";
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="mx-auto flex min-h-dvh max-w-xl flex-col gap-8 px-6 py-16">
|
|
33
|
+
<header className="flex items-center justify-between">
|
|
34
|
+
<span className="font-mono text-sm font-semibold">__PROJECT_NAME__</span>
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<ConnectAvalanche />
|
|
37
|
+
<ThemeToggle />
|
|
38
|
+
</div>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<div className="flex flex-col gap-2">
|
|
42
|
+
<h1 className="text-3xl font-semibold tracking-tight">Confidential token transfers</h1>
|
|
43
|
+
<p className="text-muted-foreground text-sm">
|
|
44
|
+
Register, mint, and transfer tokens with hidden balances and amounts on {chain.name},
|
|
45
|
+
using Avalanche's Encrypted ERC (eERC) standard. Proofs are generated fully client-side.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{!isConnected || !address || !provider ? (
|
|
50
|
+
<div className="text-muted-foreground rounded-xl border border-dashed p-10 text-center text-sm">
|
|
51
|
+
Connect a wallet to begin.
|
|
52
|
+
</div>
|
|
53
|
+
) : (
|
|
54
|
+
<EercPanel address={address} provider={provider} explorerUrl={chain.explorerUrl} />
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function EercPanel({
|
|
61
|
+
address,
|
|
62
|
+
provider,
|
|
63
|
+
explorerUrl,
|
|
64
|
+
}: {
|
|
65
|
+
address: Address;
|
|
66
|
+
provider: NonNullable<ReturnType<typeof useAvaKit>["provider"]>;
|
|
67
|
+
explorerUrl: string;
|
|
68
|
+
}) {
|
|
69
|
+
const { chain } = useAvaKit();
|
|
70
|
+
|
|
71
|
+
// eERC needs a wallet client with an account already attached (viem's
|
|
72
|
+
// "account hoisting"), unlike @avakit/core's getWalletClient, which passes
|
|
73
|
+
// account per-call. Build one directly from the connected provider.
|
|
74
|
+
const publicClient = useMemo(() => getPublicClient(chain), [chain]);
|
|
75
|
+
const walletClient = useMemo(
|
|
76
|
+
() => createWalletClient({ chain: toViemChain(chain), transport: custom(provider), account: address }),
|
|
77
|
+
[chain, provider, address],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const eerc = useEERC(publicClient, walletClient, EERC_CONTRACT_ADDRESS, circuitURLs);
|
|
81
|
+
const balance = eerc.useEncryptedBalance();
|
|
82
|
+
|
|
83
|
+
const [unlocked, setUnlocked] = useState(false);
|
|
84
|
+
const [mintAmount, setMintAmount] = useState("10");
|
|
85
|
+
const [transferTo, setTransferTo] = useState("");
|
|
86
|
+
const [transferAmount, setTransferAmount] = useState("5");
|
|
87
|
+
const [burnAmount, setBurnAmount] = useState("1");
|
|
88
|
+
const [busy, setBusy] = useState<null | "register" | "unlock" | "mint" | "transfer" | "burn">(null);
|
|
89
|
+
const [error, setError] = useState<string | null>(null);
|
|
90
|
+
const [lastTx, setLastTx] = useState<string | null>(null);
|
|
91
|
+
|
|
92
|
+
async function run(kind: NonNullable<typeof busy>, fn: () => Promise<{ transactionHash?: string } | void>) {
|
|
93
|
+
setBusy(kind);
|
|
94
|
+
setError(null);
|
|
95
|
+
try {
|
|
96
|
+
const result = await fn();
|
|
97
|
+
if (result && "transactionHash" in result && result.transactionHash) {
|
|
98
|
+
setLastTx(result.transactionHash);
|
|
99
|
+
}
|
|
100
|
+
balance.refetchBalance();
|
|
101
|
+
} catch (e) {
|
|
102
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
103
|
+
} finally {
|
|
104
|
+
setBusy(null);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!eerc.isAllDataFetched) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="text-muted-foreground rounded-xl border border-dashed p-10 text-center text-sm">
|
|
111
|
+
Loading eERC state…
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!eerc.isRegistered) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex flex-col gap-3 rounded-xl border p-6">
|
|
119
|
+
<p className="text-sm">Step 1 — register a confidential account (signature, no gas cost beyond the tx).</p>
|
|
120
|
+
<Button
|
|
121
|
+
disabled={busy !== null}
|
|
122
|
+
onClick={() => run("register", async () => { await eerc.register(); setUnlocked(true); })}
|
|
123
|
+
>
|
|
124
|
+
{busy === "register" ? "Registering…" : "Register"}
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!unlocked) {
|
|
131
|
+
return (
|
|
132
|
+
<div className="flex flex-col gap-3 rounded-xl border p-6">
|
|
133
|
+
<p className="text-sm">
|
|
134
|
+
Step 2 — unlock your private balance for this session (a free signature, derives your
|
|
135
|
+
decryption key locally; nothing is sent on-chain).
|
|
136
|
+
</p>
|
|
137
|
+
<Button
|
|
138
|
+
disabled={busy !== null}
|
|
139
|
+
onClick={() => run("unlock", async () => { await eerc.generateDecryptionKey(); setUnlocked(true); })}
|
|
140
|
+
>
|
|
141
|
+
{busy === "unlock" ? "Unlocking…" : "Unlock private balance"}
|
|
142
|
+
</Button>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const decimals = Number(balance.decimals ?? 2n);
|
|
148
|
+
const fmt = (v: bigint) => `${formatUnits(v, decimals)} ${eerc.symbol || "TEST"}`;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="flex flex-col gap-4 rounded-xl border p-6">
|
|
152
|
+
<Row label="Token" value={`${eerc.name || "eERC"} (${eerc.symbol || "TEST"})`} mono />
|
|
153
|
+
<Row label="Your private balance" value={fmt(balance.decryptedBalance)} mono />
|
|
154
|
+
<Row label="Your account" value={shortenAddress(address, 6)} mono />
|
|
155
|
+
|
|
156
|
+
<div className="flex flex-col gap-2 border-t pt-4">
|
|
157
|
+
<p className="text-sm">Mint privately to yourself</p>
|
|
158
|
+
<p className="text-muted-foreground text-xs">
|
|
159
|
+
Minting is owner-only on this shared instance — deploy your own eERC contract to mint
|
|
160
|
+
from your own wallet (see CLAUDE.md).
|
|
161
|
+
</p>
|
|
162
|
+
<div className="flex gap-2">
|
|
163
|
+
<AmountInput value={mintAmount} onChange={setMintAmount} />
|
|
164
|
+
<Button
|
|
165
|
+
disabled={busy !== null}
|
|
166
|
+
onClick={() =>
|
|
167
|
+
run("mint", () => balance.privateMint(address, parseUnits(mintAmount || "0", decimals)))
|
|
168
|
+
}
|
|
169
|
+
>
|
|
170
|
+
{busy === "mint" ? "Minting…" : "Mint"}
|
|
171
|
+
</Button>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div className="flex flex-col gap-2 border-t pt-4">
|
|
176
|
+
<p className="text-sm">Confidential transfer</p>
|
|
177
|
+
<input
|
|
178
|
+
className="border-input bg-transparent rounded-md border px-3 py-2 text-sm"
|
|
179
|
+
placeholder="Recipient address (0x…, must be registered)"
|
|
180
|
+
value={transferTo}
|
|
181
|
+
onChange={(e) => setTransferTo(e.target.value)}
|
|
182
|
+
/>
|
|
183
|
+
<div className="flex gap-2">
|
|
184
|
+
<AmountInput value={transferAmount} onChange={setTransferAmount} />
|
|
185
|
+
<Button
|
|
186
|
+
disabled={busy !== null || !transferTo}
|
|
187
|
+
onClick={() =>
|
|
188
|
+
run("transfer", () =>
|
|
189
|
+
balance.privateTransfer(transferTo, parseUnits(transferAmount || "0", decimals)),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
>
|
|
193
|
+
{busy === "transfer" ? "Sending…" : "Send"}
|
|
194
|
+
</Button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div className="flex flex-col gap-2 border-t pt-4">
|
|
199
|
+
<p className="text-sm">Burn privately</p>
|
|
200
|
+
<div className="flex gap-2">
|
|
201
|
+
<AmountInput value={burnAmount} onChange={setBurnAmount} />
|
|
202
|
+
<Button
|
|
203
|
+
variant="outline"
|
|
204
|
+
disabled={busy !== null}
|
|
205
|
+
onClick={() => run("burn", () => balance.privateBurn(parseUnits(burnAmount || "0", decimals)))}
|
|
206
|
+
>
|
|
207
|
+
{busy === "burn" ? "Burning…" : "Burn"}
|
|
208
|
+
</Button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{lastTx ? (
|
|
213
|
+
<a
|
|
214
|
+
href={`${explorerUrl}/tx/${lastTx}`}
|
|
215
|
+
target="_blank"
|
|
216
|
+
rel="noreferrer"
|
|
217
|
+
className="text-muted-foreground text-center text-xs underline underline-offset-4"
|
|
218
|
+
>
|
|
219
|
+
View last transaction on explorer
|
|
220
|
+
</a>
|
|
221
|
+
) : null}
|
|
222
|
+
|
|
223
|
+
{error ? (
|
|
224
|
+
<p className="border-destructive text-destructive rounded-md border px-3 py-2 text-sm font-medium">
|
|
225
|
+
{error}
|
|
226
|
+
</p>
|
|
227
|
+
) : null}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function AmountInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
|
233
|
+
return (
|
|
234
|
+
<input
|
|
235
|
+
className="border-input bg-transparent w-24 rounded-md border px-3 py-2 text-sm"
|
|
236
|
+
inputMode="decimal"
|
|
237
|
+
value={value}
|
|
238
|
+
onChange={(e) => onChange(e.target.value)}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
|
244
|
+
return (
|
|
245
|
+
<div className="flex items-center justify-between gap-4">
|
|
246
|
+
<span className="text-muted-foreground text-sm">{label}</span>
|
|
247
|
+
<span className={mono ? "font-mono text-sm" : "text-sm"}>{value}</span>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: AvaKit eERC (confidential token) conventions for this project
|
|
3
|
+
globs: ["**/*.ts", "**/*.tsx", "**/*.css"]
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# AvaKit eERC project rules
|
|
8
|
+
|
|
9
|
+
Register, mint, and privately transfer tokens with hidden balances using Avalanche's Encrypted ERC standard.
|
|
10
|
+
|
|
11
|
+
## Flow
|
|
12
|
+
|
|
13
|
+
- Init: `useEERC(publicClient, walletClient, EERC_CONTRACT_ADDRESS, circuitURLs)` (config in `lib/eerc-config.ts`).
|
|
14
|
+
- Register (one-time, on-chain, idempotent): `eerc.register()`.
|
|
15
|
+
- Unlock (per session, off-chain, free): `eerc.generateDecryptionKey()`.
|
|
16
|
+
- Balance/actions: `eerc.useEncryptedBalance()` → `privateMint` (owner-only), `privateTransfer`, `privateBurn`.
|
|
17
|
+
|
|
18
|
+
## Wallet client
|
|
19
|
+
|
|
20
|
+
`@avakit/core`'s `getWalletClient` does not attach an `account`, but the eERC SDK requires
|
|
21
|
+
`wallet.account.address` to be set. Build the wallet client directly:
|
|
22
|
+
`createWalletClient({ chain: toViemChain(chain), transport: custom(provider), account: address })`.
|
|
23
|
+
|
|
24
|
+
## Amounts
|
|
25
|
+
|
|
26
|
+
- The token has 2 decimals (read `balance.decimals`, don't hardcode). Always convert with
|
|
27
|
+
`parseUnits` / `formatUnits`.
|
|
28
|
+
|
|
29
|
+
## Contract
|
|
30
|
+
|
|
31
|
+
- No bundled `contracts/` — this template points at a shared, pre-deployed standalone eERC
|
|
32
|
+
instance on Fuji. To mint from your own wallet you must deploy your own instance (see
|
|
33
|
+
`CLAUDE.md`) since `privateMint` is `onlyOwner`.
|
|
34
|
+
|
|
35
|
+
## UI & safety
|
|
36
|
+
|
|
37
|
+
- shadcn/ui only; black & white for now; dark/light via next-themes; both must work.
|
|
38
|
+
- Animations: Framer Motion or GSAP only.
|
|
39
|
+
- Registering/minting/transferring/burning cost gas (except unlocking); fund the wallet on Fuji first.
|
|
40
|
+
- Default chain is Fuji testnet; mainnet requires explicit opt-in.
|
|
41
|
+
- wagmi is pinned to 2.x (not the latest major) because `@avalabs/eerc-sdk` peer-depends on `^2.0.0`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Hex } from "viem";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone eERC deployment on Avalanche Fuji, shared by every app scaffolded
|
|
5
|
+
* from this template. Deployed with `ava-labs/EncryptedERC`'s
|
|
6
|
+
* `scripts/deploy-standalone.ts` (name "Test", symbol "TEST", 2 decimals).
|
|
7
|
+
* Redeploy your own instance if you need a fresh registrar / auditor.
|
|
8
|
+
*/
|
|
9
|
+
export const EERC_CONTRACT_ADDRESS: Hex = "0xfB27bcdb845ECF231a36f3d14466e9ce9CF98d58";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Circuit wasm + zkey files, served from the `ava-labs/EncryptedERC` repo via
|
|
13
|
+
* jsDelivr's GitHub CDN, pinned to a commit so they never change under us.
|
|
14
|
+
* Proof generation (registration/mint/transfer/withdraw/burn) runs fully
|
|
15
|
+
* client-side in the browser via snarkjs — nothing is vendored in this repo.
|
|
16
|
+
*/
|
|
17
|
+
const CIRCUIT_COMMIT = "c7eb0e09bc9315e68c35d3c09f5dce4b794d0485";
|
|
18
|
+
const CIRCUIT_BASE = `https://cdn.jsdelivr.net/gh/ava-labs/EncryptedERC@${CIRCUIT_COMMIT}/circom/build`;
|
|
19
|
+
|
|
20
|
+
export const circuitURLs = {
|
|
21
|
+
register: {
|
|
22
|
+
wasm: `${CIRCUIT_BASE}/registration/registration.wasm`,
|
|
23
|
+
zkey: `${CIRCUIT_BASE}/registration/circuit_final.zkey`,
|
|
24
|
+
},
|
|
25
|
+
mint: {
|
|
26
|
+
wasm: `${CIRCUIT_BASE}/mint/mint.wasm`,
|
|
27
|
+
zkey: `${CIRCUIT_BASE}/mint/mint.zkey`,
|
|
28
|
+
},
|
|
29
|
+
transfer: {
|
|
30
|
+
wasm: `${CIRCUIT_BASE}/transfer/transfer.wasm`,
|
|
31
|
+
zkey: `${CIRCUIT_BASE}/transfer/transfer.zkey`,
|
|
32
|
+
},
|
|
33
|
+
withdraw: {
|
|
34
|
+
wasm: `${CIRCUIT_BASE}/withdraw/withdraw.wasm`,
|
|
35
|
+
zkey: `${CIRCUIT_BASE}/withdraw/circuit_final.zkey`,
|
|
36
|
+
},
|
|
37
|
+
burn: {
|
|
38
|
+
wasm: `${CIRCUIT_BASE}/burn/burn.wasm`,
|
|
39
|
+
zkey: `${CIRCUIT_BASE}/burn/burn.zkey`,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
> An Avalanche confidential-token dapp scaffolded with AvaKit, built on Avalanche's Encrypted ERC (eERC) standard. Register, mint, and privately transfer tokens with hidden balances. Next.js + @avakit/react + @avalabs/eerc-sdk + shadcn/ui.
|
|
4
|
+
|
|
5
|
+
## Project map
|
|
6
|
+
|
|
7
|
+
- [lib/eerc-config.ts](lib/eerc-config.ts): deployed eERC contract address + CDN circuit URLs.
|
|
8
|
+
- [app/providers.tsx](app/providers.tsx): `<AvaKitProvider>` (wallet) + `<WagmiProvider>` (required by the eERC SDK's internal reads).
|
|
9
|
+
- [components/demo.tsx](components/demo.tsx): register → unlock → mint → confidential transfer → burn.
|
|
10
|
+
- [CLAUDE.md](CLAUDE.md): agent guide — flow, why this template deviates from AvaKit's usual pattern, how to deploy your own instance.
|
|
11
|
+
|
|
12
|
+
## Key APIs
|
|
13
|
+
|
|
14
|
+
- Init: `useEERC(publicClient, walletClient, EERC_CONTRACT_ADDRESS, circuitURLs)` from `@avalabs/eerc-sdk`.
|
|
15
|
+
- Register: `eerc.register()` (one-time, on-chain, idempotent).
|
|
16
|
+
- Unlock: `eerc.generateDecryptionKey()` (per session, off-chain, no gas).
|
|
17
|
+
- Balance + actions: `eerc.useEncryptedBalance()` → `privateMint(to, amount)` (owner-only), `privateTransfer(to, amount)`, `privateBurn(amount)`, `decryptedBalance`, `parsedDecryptedBalance`.
|
|
18
|
+
- Amounts are base units (token has 2 decimals) — convert with `parseUnits`/`formatUnits`.
|
|
19
|
+
|
|
20
|
+
## External docs
|
|
21
|
+
|
|
22
|
+
- eERC contracts + deploy scripts: https://github.com/ava-labs/EncryptedERC
|
|
23
|
+
- eERC SDK: https://avacloud.gitbook.io/encrypted-erc/d87pGPcNAEEw9ISVd17M/usage/sdk-overview
|
|
24
|
+
- Avalanche Builder Hub: https://build.avax.network/llms.txt
|
|
25
|
+
- Fuji faucet: https://core.app/tools/testnet-faucet
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@avakit/core": "__AVAKIT_DEP__",
|
|
14
|
+
"@avakit/react": "__AVAKIT_DEP__",
|
|
15
|
+
"@avalabs/eerc-sdk": "1.0.2",
|
|
16
|
+
"@tanstack/react-query": "5.101.2",
|
|
17
|
+
"lucide-react": "1.22.0",
|
|
18
|
+
"next": "16.2.9",
|
|
19
|
+
"next-themes": "0.4.6",
|
|
20
|
+
"react": "19.2.7",
|
|
21
|
+
"react-dom": "19.2.7",
|
|
22
|
+
"viem": "2.54.1",
|
|
23
|
+
"wagmi": "2.19.5"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@tailwindcss/postcss": "4.3.2",
|
|
27
|
+
"@types/node": "26.0.1",
|
|
28
|
+
"@types/react": "19.2.17",
|
|
29
|
+
"@types/react-dom": "19.2.3",
|
|
30
|
+
"postcss": "8.5.16",
|
|
31
|
+
"tailwindcss": "4.3.2",
|
|
32
|
+
"tw-animate-css": "1.4.0",
|
|
33
|
+
"typescript": "6.0.3"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# pnpm settings for this scaffolded app.
|
|
2
|
+
# allowBuilds: pre-approve native postinstall scripts so `pnpm install` / `pnpm dev`
|
|
3
|
+
# don't fail with ERR_PNPM_IGNORED_BUILDS on pnpm 10+. The eERC crypto/wallet stack
|
|
4
|
+
# pulls in blake-hash, keccak, bufferutil and utf-8-validate alongside sharp.
|
|
5
|
+
# minimumReleaseAgeExclude: pnpm's supply-chain age gate blocks very fresh releases;
|
|
6
|
+
# exempt AvaKit's own packages so a new @avakit publish never breaks a fresh install
|
|
7
|
+
# (third-party deps keep the protection). Delete this file to opt back into defaults.
|
|
8
|
+
allowBuilds:
|
|
9
|
+
sharp: true
|
|
10
|
+
blake-hash: true
|
|
11
|
+
keccak: true
|
|
12
|
+
bufferutil: true
|
|
13
|
+
utf-8-validate: true
|
|
14
|
+
minimumReleaseAgeExclude:
|
|
15
|
+
- '@avakit/core'
|
|
16
|
+
- '@avakit/react'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# pnpm settings for this scaffolded app.
|
|
2
|
+
# allowBuilds: pre-approve native postinstall scripts so `pnpm install` / `pnpm dev`
|
|
3
|
+
# don't fail with ERR_PNPM_IGNORED_BUILDS on pnpm 10+.
|
|
4
|
+
# minimumReleaseAgeExclude: pnpm's supply-chain age gate blocks very fresh releases;
|
|
5
|
+
# exempt AvaKit's own packages so a new @avakit publish never breaks a fresh install
|
|
6
|
+
# (third-party deps keep the protection). Delete this file to opt back into defaults.
|
|
7
|
+
allowBuilds:
|
|
8
|
+
sharp: true
|
|
9
|
+
minimumReleaseAgeExclude:
|
|
10
|
+
- '@avakit/core'
|
|
11
|
+
- '@avakit/react'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# pnpm settings for this scaffolded app.
|
|
2
|
+
# allowBuilds: pre-approve native postinstall scripts so `pnpm install` / `pnpm dev`
|
|
3
|
+
# don't fail with ERR_PNPM_IGNORED_BUILDS on pnpm 10+.
|
|
4
|
+
# minimumReleaseAgeExclude: pnpm's supply-chain age gate blocks very fresh releases;
|
|
5
|
+
# exempt AvaKit's own packages so a new @avakit publish never breaks a fresh install
|
|
6
|
+
# (third-party deps keep the protection). Delete this file to opt back into defaults.
|
|
7
|
+
allowBuilds:
|
|
8
|
+
sharp: true
|
|
9
|
+
minimumReleaseAgeExclude:
|
|
10
|
+
- '@avakit/core'
|
|
11
|
+
- '@avakit/react'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# pnpm settings for this scaffolded app.
|
|
2
|
+
# allowBuilds: pre-approve native postinstall scripts so `pnpm install` / `pnpm dev`
|
|
3
|
+
# don't fail with ERR_PNPM_IGNORED_BUILDS on pnpm 10+.
|
|
4
|
+
# minimumReleaseAgeExclude: pnpm's supply-chain age gate blocks very fresh releases;
|
|
5
|
+
# exempt AvaKit's own packages so a new @avakit publish never breaks a fresh install
|
|
6
|
+
# (third-party deps keep the protection). Delete this file to opt back into defaults.
|
|
7
|
+
allowBuilds:
|
|
8
|
+
sharp: true
|
|
9
|
+
minimumReleaseAgeExclude:
|
|
10
|
+
- '@avakit/core'
|
|
11
|
+
- '@avakit/react'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# pnpm settings for this scaffolded app.
|
|
2
|
+
# allowBuilds: pre-approve native postinstall scripts so `pnpm install` / `pnpm dev`
|
|
3
|
+
# don't fail with ERR_PNPM_IGNORED_BUILDS on pnpm 10+.
|
|
4
|
+
# minimumReleaseAgeExclude: pnpm's supply-chain age gate blocks very fresh releases;
|
|
5
|
+
# exempt AvaKit's own packages so a new @avakit publish never breaks a fresh install
|
|
6
|
+
# (third-party deps keep the protection). Delete this file to opt back into defaults.
|
|
7
|
+
allowBuilds:
|
|
8
|
+
sharp: true
|
|
9
|
+
minimumReleaseAgeExclude:
|
|
10
|
+
- '@avakit/core'
|
|
11
|
+
- '@avakit/react'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# pnpm settings for this scaffolded app.
|
|
2
|
+
# allowBuilds: pre-approve native postinstall scripts so `pnpm install` / `pnpm dev`
|
|
3
|
+
# don't fail with ERR_PNPM_IGNORED_BUILDS on pnpm 10+.
|
|
4
|
+
# minimumReleaseAgeExclude: pnpm's supply-chain age gate blocks very fresh releases;
|
|
5
|
+
# exempt AvaKit's own packages so a new @avakit publish never breaks a fresh install
|
|
6
|
+
# (third-party deps keep the protection). Delete this file to opt back into defaults.
|
|
7
|
+
allowBuilds:
|
|
8
|
+
sharp: true
|
|
9
|
+
minimumReleaseAgeExclude:
|
|
10
|
+
- '@avakit/core'
|
|
11
|
+
- '@avakit/react'
|