create-izi-noir 0.1.10 → 0.2.1

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.
Files changed (2) hide show
  1. package/dist/index.js +1245 -235
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // src/commands/init.ts
7
7
  import path2 from "path";
8
8
  import { execSync } from "child_process";
9
- import pc3 from "picocolors";
9
+ import pc4 from "picocolors";
10
10
 
11
11
  // src/prompts/project.ts
12
12
  import prompts from "prompts";
@@ -153,41 +153,187 @@ function createSpinner(message) {
153
153
  return new Spinner(message);
154
154
  }
155
155
 
156
+ // src/utils/progress.ts
157
+ import pc3 from "picocolors";
158
+ var THINKING_PHRASES = [
159
+ "Initializing ZK environment",
160
+ "Configuring proof system",
161
+ "Setting up circuit compiler",
162
+ "Preparing cryptographic primitives",
163
+ "Generating project scaffold",
164
+ "Configuring Noir integration",
165
+ "Setting up React components",
166
+ "Preparing WASM bindings"
167
+ ];
168
+ var FILE_ICONS = {
169
+ ".json": "\u{1F4E6}",
170
+ ".ts": "\u{1F4DC}",
171
+ ".tsx": "\u269B\uFE0F",
172
+ ".css": "\u{1F3A8}",
173
+ ".html": "\u{1F310}",
174
+ ".md": "\u{1F4DD}",
175
+ ".svg": "\u{1F5BC}\uFE0F",
176
+ default: "\u{1F4C4}"
177
+ };
178
+ function getFileIcon(filename) {
179
+ const ext = filename.slice(filename.lastIndexOf("."));
180
+ return FILE_ICONS[ext] || FILE_ICONS.default;
181
+ }
182
+ function sleep(ms) {
183
+ return new Promise((resolve) => setTimeout(resolve, ms));
184
+ }
185
+ var ProgressReporter = class {
186
+ currentLine = "";
187
+ thinkingIndex = 0;
188
+ thinkingInterval = null;
189
+ async startThinking() {
190
+ process.stdout.write("\x1B[?25l");
191
+ this.showThinking();
192
+ }
193
+ showThinking() {
194
+ const frames2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
195
+ let frameIndex = 0;
196
+ let dotCount = 0;
197
+ this.thinkingInterval = setInterval(() => {
198
+ const frame = pc3.cyan(frames2[frameIndex]);
199
+ const phrase = THINKING_PHRASES[this.thinkingIndex % THINKING_PHRASES.length];
200
+ const dots = ".".repeat(dotCount % 4);
201
+ process.stdout.write(`\r${frame} ${pc3.dim(phrase)}${dots} `);
202
+ frameIndex = (frameIndex + 1) % frames2.length;
203
+ dotCount++;
204
+ if (dotCount % 12 === 0) {
205
+ this.thinkingIndex++;
206
+ }
207
+ }, 80);
208
+ }
209
+ stopThinking() {
210
+ if (this.thinkingInterval) {
211
+ clearInterval(this.thinkingInterval);
212
+ this.thinkingInterval = null;
213
+ }
214
+ process.stdout.write("\r\x1B[K");
215
+ }
216
+ async reportFile(filename, isLast = false) {
217
+ const icon = getFileIcon(filename);
218
+ const line = ` ${icon} ${pc3.dim("created")} ${pc3.white(filename)}`;
219
+ process.stdout.write(" " + icon + " " + pc3.dim("created") + " ");
220
+ for (const char of filename) {
221
+ process.stdout.write(pc3.white(char));
222
+ await sleep(8 + Math.random() * 12);
223
+ }
224
+ process.stdout.write("\n");
225
+ this.currentLine = line;
226
+ }
227
+ async reportDirectory(dirname) {
228
+ process.stdout.write(` \u{1F4C1} ${pc3.dim("mkdir")} ${pc3.blue(dirname)}/
229
+ `);
230
+ await sleep(30);
231
+ }
232
+ showSuccess(message) {
233
+ process.stdout.write("\x1B[?25h");
234
+ console.log();
235
+ console.log(pc3.green("\u2713") + " " + message);
236
+ }
237
+ showError(message) {
238
+ process.stdout.write("\x1B[?25h");
239
+ console.log();
240
+ console.log(pc3.red("\u2717") + " " + message);
241
+ }
242
+ };
243
+ function createProgressReporter() {
244
+ return new ProgressReporter();
245
+ }
246
+ var InstallProgress = class {
247
+ interval = null;
248
+ progress = 0;
249
+ packages = [
250
+ "react",
251
+ "react-dom",
252
+ "vite",
253
+ "@izi-noir/sdk",
254
+ "@noir-lang/acvm_js",
255
+ "@noir-lang/noirc_abi",
256
+ "prism-react-renderer",
257
+ "typescript"
258
+ ];
259
+ currentPackage = 0;
260
+ start() {
261
+ process.stdout.write("\x1B[?25l");
262
+ const barWidth = 30;
263
+ const frames2 = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
264
+ let frameIndex = 0;
265
+ this.interval = setInterval(() => {
266
+ const frame = pc3.cyan(frames2[frameIndex]);
267
+ const filled = Math.floor(this.progress / 100 * barWidth);
268
+ const empty = barWidth - filled;
269
+ const bar = pc3.green("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(empty));
270
+ const pkg = this.packages[this.currentPackage % this.packages.length];
271
+ process.stdout.write(
272
+ `\r${frame} Installing dependencies ${bar} ${pc3.dim(pkg)} `
273
+ );
274
+ frameIndex = (frameIndex + 1) % frames2.length;
275
+ if (this.progress < 95) {
276
+ this.progress += Math.random() * 3;
277
+ if (this.progress > (this.currentPackage + 1) * 12) {
278
+ this.currentPackage++;
279
+ }
280
+ }
281
+ }, 100);
282
+ }
283
+ stop(success = true) {
284
+ if (this.interval) {
285
+ clearInterval(this.interval);
286
+ this.interval = null;
287
+ }
288
+ process.stdout.write("\x1B[?25h");
289
+ process.stdout.write("\r\x1B[K");
290
+ const icon = success ? pc3.green("\u2713") : pc3.red("\u2717");
291
+ const message = success ? "Dependencies installed" : "Failed to install dependencies";
292
+ console.log(`${icon} ${message}`);
293
+ }
294
+ };
295
+ function createInstallProgress() {
296
+ return new InstallProgress();
297
+ }
298
+
156
299
  // src/generators/package-json.ts
157
300
  function generatePackageJson(options) {
301
+ const isSolana = options.provider === "arkworks";
302
+ const dependencies = {
303
+ "@izi-noir/sdk": "^0.1.4",
304
+ "@noir-lang/acvm_js": "1.0.0-beta.13-1d260df.nightly",
305
+ "@noir-lang/noirc_abi": "1.0.0-beta.13-1d260df.nightly",
306
+ "react": "^18.3.1",
307
+ "react-dom": "^18.3.1",
308
+ "prism-react-renderer": "^2.4.1"
309
+ };
310
+ if (isSolana) {
311
+ dependencies["@solana/wallet-adapter-react"] = "^0.15.0";
312
+ dependencies["@solana/wallet-adapter-react-ui"] = "^0.9.0";
313
+ dependencies["@solana/wallet-adapter-wallets"] = "^0.19.0";
314
+ dependencies["@solana/web3.js"] = "^1.95.0";
315
+ }
158
316
  const pkg = {
159
317
  name: options.projectName,
160
318
  version: "0.1.0",
319
+ private: true,
161
320
  description: "ZK circuits built with IZI-NOIR",
162
321
  type: "module",
163
- main: "./dist/index.js",
164
- types: "./dist/index.d.ts",
165
- exports: {
166
- ".": {
167
- types: "./dist/index.d.ts",
168
- import: "./dist/index.js"
169
- },
170
- "./circuits": {
171
- types: "./circuits/index.d.ts",
172
- import: "./circuits/index.js"
173
- }
174
- },
175
- files: ["dist", "circuits", "generated"],
176
322
  scripts: {
177
- build: "tsc",
178
- test: "tsx scripts/test-proof.ts",
179
- "test:watch": "tsx watch scripts/test-proof.ts",
180
- prepublishOnly: "npm run build"
181
- },
182
- dependencies: {
183
- "@izi-noir/sdk": "^0.1.4"
323
+ dev: "vite",
324
+ build: "vite build",
325
+ preview: "vite preview",
326
+ typecheck: "tsc --noEmit"
184
327
  },
328
+ dependencies,
185
329
  devDependencies: {
186
- "@types/node": "^22.0.0",
187
- tsx: "^4.0.0",
188
- typescript: "^5.4.0"
330
+ "@types/react": "^18.3.0",
331
+ "@types/react-dom": "^18.3.0",
332
+ "@vitejs/plugin-react": "^4.3.0",
333
+ "typescript": "^5.4.0",
334
+ "vite": "^5.4.0"
189
335
  },
190
- keywords: ["zk", "noir", "zero-knowledge", "privacy", "solana"],
336
+ keywords: ["zk", "noir", "zero-knowledge", "privacy", isSolana ? "solana" : "evm"],
191
337
  license: "MIT"
192
338
  };
193
339
  return JSON.stringify(pkg, null, 2);
@@ -197,50 +343,35 @@ function generatePackageJson(options) {
197
343
  function generateTsconfig() {
198
344
  const config = {
199
345
  compilerOptions: {
200
- target: "ES2022",
346
+ target: "ES2020",
347
+ useDefineForClassFields: true,
348
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
201
349
  module: "ESNext",
350
+ skipLibCheck: true,
351
+ // Bundler mode
202
352
  moduleResolution: "bundler",
203
- lib: ["ES2022"],
204
- outDir: "./dist",
205
- rootDir: ".",
353
+ allowImportingTsExtensions: true,
354
+ isolatedModules: true,
355
+ moduleDetection: "force",
356
+ noEmit: true,
357
+ jsx: "react-jsx",
358
+ // Linting
206
359
  strict: true,
207
- esModuleInterop: true,
208
- skipLibCheck: true,
209
- forceConsistentCasingInFileNames: true,
210
- declaration: true,
211
- declarationMap: true,
212
- sourceMap: true,
213
- resolveJsonModule: true
360
+ noUnusedLocals: true,
361
+ noUnusedParameters: true,
362
+ noFallthroughCasesInSwitch: true,
363
+ // Paths
364
+ baseUrl: ".",
365
+ paths: {
366
+ "@/*": ["./src/*"]
367
+ }
214
368
  },
215
- include: ["circuits/**/*", "generated/**/*", "scripts/**/*"],
216
- exclude: ["node_modules", "dist"]
369
+ include: ["src", "circuits"],
370
+ references: [{ path: "./tsconfig.node.json" }]
217
371
  };
218
372
  return JSON.stringify(config, null, 2);
219
373
  }
220
374
 
221
- // src/generators/config.ts
222
- function generateConfig(options) {
223
- return `import { defineConfig } from '@izi-noir/sdk';
224
-
225
- export default defineConfig({
226
- // Directory containing your circuit files
227
- circuitsDir: './circuits',
228
-
229
- // Output directory for compiled circuits
230
- outDir: './generated',
231
-
232
- // Proving provider to use
233
- provider: '${options.provider}',
234
-
235
- // Enable watch mode optimizations
236
- watch: {
237
- // Debounce file changes (ms)
238
- debounce: 100,
239
- },
240
- });
241
- `;
242
- }
243
-
244
375
  // src/generators/circuits.ts
245
376
  function generateBalanceProof() {
246
377
  return `/**
@@ -328,133 +459,49 @@ declare function assert(condition: boolean): void;
328
459
  `;
329
460
  }
330
461
 
331
- // src/generators/scripts.ts
332
- function generateTestScript(options) {
333
- const imports = getImports(options.template);
334
- const tests = getTests(options.template);
335
- return `/**
336
- * Test script for ZK proofs
337
- *
338
- * Run with: npm test
339
- */
340
- import { IziNoir, Provider, AcornParser, generateNoir, type CircuitFunction } from '@izi-noir/sdk';
341
- ${imports}
342
-
343
- // Helper to convert JS circuit function to Noir code
344
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
345
- function toNoir(circuitFn: any): string {
346
- const parser = new AcornParser();
347
- const parsed = parser.parse(circuitFn as CircuitFunction, [], []);
348
- return generateNoir(parsed);
349
- }
350
-
351
- async function main() {
352
- console.log('Initializing IZI-NOIR...');
353
- const izi = await IziNoir.init({
354
- provider: Provider.${capitalizeFirst(options.provider)},
355
- });
356
-
357
- ${tests}
358
-
359
- console.log('\\n\u2713 All proofs verified successfully!');
360
- }
361
-
362
- main().catch((error) => {
363
- console.error('Error:', error);
364
- process.exit(1);
365
- });
366
- `;
367
- }
368
- function getImports(template) {
369
- switch (template) {
370
- case "minimal":
371
- return `import { myCircuit } from '../circuits/index.js';`;
372
- case "balance-proof":
373
- return `import { balanceProof } from '../circuits/index.js';`;
374
- default:
375
- return `import { balanceProof, ageProof } from '../circuits/index.js';`;
376
- }
377
- }
378
- function getTests(template) {
379
- switch (template) {
380
- case "minimal":
381
- return ` // Test: myCircuit
382
- console.log('\\nTesting myCircuit...');
383
- const noirCode1 = toNoir(myCircuit);
384
- const result1 = await izi.createProof(noirCode1, {
385
- publicInput: '42', // public: expected value
386
- privateInput: '42', // private: actual value
387
- });
388
- console.log(' Proof verified:', result1.verified);`;
389
- case "balance-proof":
390
- return ` // Test: Balance Proof
391
- console.log('\\nTesting balanceProof...');
392
- const noirCode1 = toNoir(balanceProof);
393
- const result1 = await izi.createProof(noirCode1, {
394
- threshold: '100', // public: minimum required
395
- balance: '1500', // private: actual balance
396
- });
397
- console.log(' Proof verified:', result1.verified);
398
- console.log(' The prover has >= 100 balance (actual: hidden)');`;
399
- default:
400
- return ` // Test 1: Balance Proof
401
- console.log('\\nTesting balanceProof...');
402
- const noirCode1 = toNoir(balanceProof);
403
- const result1 = await izi.createProof(noirCode1, {
404
- threshold: '100', // public: minimum required
405
- balance: '1500', // private: actual balance
406
- });
407
- console.log(' Proof verified:', result1.verified);
408
- console.log(' The prover has >= 100 balance (actual: hidden)');
409
-
410
- // Test 2: Age Proof
411
- console.log('\\nTesting ageProof...');
412
- const noirCode2 = toNoir(ageProof);
413
- const result2 = await izi.createProof(noirCode2, {
414
- currentYear: '2024', // public: current year
415
- minAge: '18', // public: minimum age
416
- birthYear: '1990', // private: birth year
417
- });
418
- console.log(' Proof verified:', result2.verified);
419
- console.log(' The prover is >= 18 years old (birth year: hidden)');`;
420
- }
421
- }
422
- function capitalizeFirst(str) {
423
- return str.charAt(0).toUpperCase() + str.slice(1);
424
- }
425
-
426
462
  // src/generators/readme.ts
427
463
  function generateReadme(options) {
464
+ const isSolana = options.provider === "arkworks";
465
+ const networkInfo = isSolana ? "- Deploy VK to Solana devnet\n- Verify proofs on-chain" : "- Local proof verification";
428
466
  return `# ${options.projectName}
429
467
 
430
- ZK circuits built with [IZI-NOIR](https://github.com/izi-noir/izi-noir).
468
+ ZK proof demo built with [IZI-NOIR](https://github.com/izi-noir/izi-noir) and React.
431
469
 
432
470
  ## Getting Started
433
471
 
434
472
  \`\`\`bash
435
- # Build circuits
436
- npm run build
437
-
438
- # Run tests
439
- npm test
473
+ # Install dependencies
474
+ npm install
440
475
 
441
- # Watch mode (rebuild on changes)
476
+ # Start development server
442
477
  npm run dev
478
+
479
+ # Build for production
480
+ npm run build
443
481
  \`\`\`
444
482
 
483
+ Then open http://localhost:5173 in your browser.
484
+
485
+ ## Features
486
+
487
+ - Interactive circuit selection
488
+ - Real-time proof generation
489
+ - Syntax-highlighted code display
490
+ ${networkInfo}
491
+
445
492
  ## Project Structure
446
493
 
447
494
  \`\`\`
448
495
  ${options.projectName}/
449
- \u251C\u2500\u2500 circuits/ # Your ZK circuit definitions
450
- \u2502 \u251C\u2500\u2500 *.ts # Circuits as JS functions with assert()
451
- \u2502 \u2514\u2500\u2500 index.ts # Re-exports
452
- \u251C\u2500\u2500 generated/ # Compiled circuits (auto-generated)
453
- \u2502 \u251C\u2500\u2500 *.json # Compiled circuit artifacts
454
- \u2502 \u2514\u2500\u2500 index.ts # Typed re-exports
455
- \u251C\u2500\u2500 scripts/
456
- \u2502 \u2514\u2500\u2500 test-proof.ts # Local test script
457
- \u251C\u2500\u2500 izi-noir.config.ts # Build configuration
496
+ \u251C\u2500\u2500 src/
497
+ \u2502 \u251C\u2500\u2500 App.tsx # Main demo component
498
+ \u2502 \u251C\u2500\u2500 main.tsx # React entry point
499
+ \u2502 \u251C\u2500\u2500 components/ # Reusable components
500
+ \u2502 \u2514\u2500\u2500 lib/ # Utility functions
501
+ \u251C\u2500\u2500 circuits/ # Your ZK circuit definitions
502
+ \u2502 \u251C\u2500\u2500 *.ts # Circuits as JS functions with assert()
503
+ \u2502 \u2514\u2500\u2500 index.ts # Re-exports
504
+ \u251C\u2500\u2500 vite.config.ts # Vite configuration
458
505
  \u2514\u2500\u2500 package.json
459
506
  \`\`\`
460
507
 
@@ -472,35 +519,16 @@ export function myCircuit(
472
519
  }
473
520
  \`\`\`
474
521
 
475
- ## Usage in Frontend
476
-
477
- \`\`\`typescript
478
- import { IziNoir, Provider } from '@izi-noir/sdk';
479
- import { myCircuit } from '${options.projectName}/circuits';
480
-
481
- const izi = await IziNoir.init({ provider: Provider.Arkworks });
482
-
483
- const proof = await izi.createProof(
484
- myCircuit,
485
- [100], // public inputs
486
- [1500] // private inputs (hidden)
487
- );
488
-
489
- console.log(proof.verified); // true
490
- \`\`\`
491
-
492
- ## Publishing
493
-
494
- \`\`\`bash
495
- npm publish
496
- \`\`\`
497
-
498
- Your circuits can then be installed as a dependency in other projects.
522
+ After adding a new circuit:
523
+ 1. Export it from \`circuits/index.ts\`
524
+ 2. Add it to the CIRCUITS array in \`src/App.tsx\`
499
525
 
500
526
  ## Learn More
501
527
 
502
528
  - [IZI-NOIR Documentation](https://github.com/izi-noir/izi-noir)
503
529
  - [Noir Language](https://noir-lang.org)
530
+ - [Vite](https://vitejs.dev)
531
+ - [React](https://react.dev)
504
532
  `;
505
533
  }
506
534
 
@@ -537,6 +565,973 @@ coverage/
537
565
  `;
538
566
  }
539
567
 
568
+ // src/generators/vite.ts
569
+ function generateViteConfig() {
570
+ return `import { defineConfig, type PluginOption } from "vite";
571
+ import react from "@vitejs/plugin-react";
572
+ import path from "path";
573
+
574
+ // Plugin to set correct MIME type for WASM files in dev server
575
+ function wasmMimePlugin(): PluginOption {
576
+ return {
577
+ name: "wasm-mime-type",
578
+ configureServer(server) {
579
+ server.middlewares.use((req, res, next) => {
580
+ if (req.url?.endsWith(".wasm")) {
581
+ res.setHeader("Content-Type", "application/wasm");
582
+ }
583
+ next();
584
+ });
585
+ },
586
+ };
587
+ }
588
+
589
+ export default defineConfig({
590
+ plugins: [react(), wasmMimePlugin()],
591
+ resolve: {
592
+ alias: {
593
+ "@": path.resolve(__dirname, "./src"),
594
+ buffer: "buffer/",
595
+ },
596
+ },
597
+ define: {
598
+ "global": "globalThis",
599
+ },
600
+ optimizeDeps: {
601
+ exclude: ["@noir-lang/noir_wasm", "@aztec/bb.js", "@izi-noir/sdk"],
602
+ include: ["buffer"],
603
+ },
604
+ build: {
605
+ rollupOptions: {
606
+ external: [
607
+ /arkworks_groth16_wasm/,
608
+ ],
609
+ },
610
+ },
611
+ });
612
+ `;
613
+ }
614
+ function generateTsconfigNode() {
615
+ return `{
616
+ "compilerOptions": {
617
+ "composite": true,
618
+ "skipLibCheck": true,
619
+ "module": "ESNext",
620
+ "moduleResolution": "bundler",
621
+ "allowSyntheticDefaultImports": true,
622
+ "strict": true
623
+ },
624
+ "include": ["vite.config.ts"]
625
+ }
626
+ `;
627
+ }
628
+ function generateIndexHtml(options) {
629
+ return `<!doctype html>
630
+ <html lang="en">
631
+ <head>
632
+ <meta charset="UTF-8" />
633
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
634
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
635
+ <title>${options.projectName} - ZK Proof Demo</title>
636
+ </head>
637
+ <body>
638
+ <div id="root"></div>
639
+ <script type="module" src="/src/main.tsx"></script>
640
+ </body>
641
+ </html>
642
+ `;
643
+ }
644
+ function generateMainTsx(options) {
645
+ const isSolana = options.provider === "arkworks";
646
+ if (isSolana) {
647
+ return `import { StrictMode } from 'react';
648
+ import { createRoot } from 'react-dom/client';
649
+ import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
650
+ import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
651
+ import { clusterApiUrl } from '@solana/web3.js';
652
+ import App from './App';
653
+ import './index.css';
654
+ import '@solana/wallet-adapter-react-ui/styles.css';
655
+
656
+ const endpoint = clusterApiUrl('devnet');
657
+
658
+ createRoot(document.getElementById('root')!).render(
659
+ <StrictMode>
660
+ <ConnectionProvider endpoint={endpoint}>
661
+ <WalletProvider wallets={[]} autoConnect>
662
+ <WalletModalProvider>
663
+ <App />
664
+ </WalletModalProvider>
665
+ </WalletProvider>
666
+ </ConnectionProvider>
667
+ </StrictMode>,
668
+ );
669
+ `;
670
+ }
671
+ return `import { StrictMode } from 'react';
672
+ import { createRoot } from 'react-dom/client';
673
+ import App from './App';
674
+ import './index.css';
675
+
676
+ createRoot(document.getElementById('root')!).render(
677
+ <StrictMode>
678
+ <App />
679
+ </StrictMode>,
680
+ );
681
+ `;
682
+ }
683
+ function generateAppTsx(options) {
684
+ const isSolana = options.provider === "arkworks";
685
+ const circuitImports = getCircuitImports(options.template);
686
+ const circuitOptions = getCircuitOptions(options.template);
687
+ const solanaImports = isSolana ? `
688
+ import { useWallet } from '@solana/wallet-adapter-react';
689
+ import { useWalletModal } from '@solana/wallet-adapter-react-ui';
690
+ import { Chain, Network } from '@izi-noir/sdk';` : "";
691
+ const solanaHooks = isSolana ? `
692
+ const { publicKey, connected, sendTransaction } = useWallet();
693
+ const { setVisible } = useWalletModal();` : "";
694
+ const solanaProviderConfig = isSolana ? `
695
+ chain: Chain.Solana,
696
+ network: Network.Devnet,` : "";
697
+ const solanaState = isSolana ? `
698
+ // Deploy state
699
+ const [isDeploying, setIsDeploying] = useState(false);
700
+ const [vkAccount, setVkAccount] = useState<string | null>(null);
701
+ const [deployError, setDeployError] = useState<string | null>(null);
702
+
703
+ // Verify state
704
+ const [isVerifying, setIsVerifying] = useState(false);
705
+ const [verified, setVerified] = useState<boolean | null>(null);
706
+ const [verifyError, setVerifyError] = useState<string | null>(null);` : "";
707
+ const solanaHandlers = isSolana ? `
708
+ // Deploy VK to Solana
709
+ const handleDeploy = async () => {
710
+ if (!iziInstance || !connected || !publicKey || !sendTransaction) return;
711
+
712
+ setIsDeploying(true);
713
+ setDeployError(null);
714
+
715
+ try {
716
+ const result = await iziInstance.deploy({ publicKey, sendTransaction });
717
+ setVkAccount(result.vkAccount);
718
+ } catch (error) {
719
+ console.error('Deploy error:', error);
720
+ setDeployError((error as Error).message);
721
+ } finally {
722
+ setIsDeploying(false);
723
+ }
724
+ };
725
+
726
+ // Verify proof on-chain
727
+ const handleVerify = async () => {
728
+ if (!iziInstance || !vkAccount || !publicKey || !sendTransaction) return;
729
+
730
+ setIsVerifying(true);
731
+ setVerifyError(null);
732
+
733
+ try {
734
+ const result = await iziInstance.verifyOnChain({ publicKey, sendTransaction }, vkAccount);
735
+ setVerified(result.verified);
736
+ } catch (error) {
737
+ console.error('Verify error:', error);
738
+ setVerifyError((error as Error).message);
739
+ } finally {
740
+ setIsVerifying(false);
741
+ }
742
+ };` : "";
743
+ const solanaDeploySection = isSolana ? `
744
+ {/* Deploy & Verify Section */}
745
+ {proof && (
746
+ <div className="section">
747
+ <h2>Deploy & Verify on Solana</h2>
748
+
749
+ {!connected ? (
750
+ <button onClick={() => setVisible(true)} className="btn btn-secondary">
751
+ Connect Wallet
752
+ </button>
753
+ ) : (
754
+ <div className="deploy-verify-row">
755
+ <div className="deploy-box">
756
+ <button
757
+ onClick={handleDeploy}
758
+ disabled={isDeploying || !!vkAccount}
759
+ className="btn btn-primary"
760
+ >
761
+ {isDeploying ? 'Deploying...' : vkAccount ? 'Deployed' : 'Deploy VK'}
762
+ </button>
763
+ {deployError && <p className="error">{deployError}</p>}
764
+ {vkAccount && <p className="success">VK: {vkAccount.slice(0, 8)}...</p>}
765
+ </div>
766
+
767
+ <div className="verify-box">
768
+ <button
769
+ onClick={handleVerify}
770
+ disabled={!vkAccount || isVerifying}
771
+ className="btn btn-primary"
772
+ >
773
+ {isVerifying ? 'Verifying...' : verified ? 'Verified!' : 'Verify On-Chain'}
774
+ </button>
775
+ {verifyError && <p className="error">{verifyError}</p>}
776
+ {verified && <p className="success">Proof verified on Solana!</p>}
777
+ </div>
778
+ </div>
779
+ )}
780
+ </div>
781
+ )}` : "";
782
+ return `import { useState, useEffect, useCallback } from 'react';
783
+ import initNoirC from '@noir-lang/noirc_abi';
784
+ import initACVM from '@noir-lang/acvm_js';
785
+ import acvm from '@noir-lang/acvm_js/web/acvm_js_bg.wasm?url';
786
+ import noirc from '@noir-lang/noirc_abi/web/noirc_abi_wasm_bg.wasm?url';
787
+ import {
788
+ IziNoir,
789
+ Provider,
790
+ markWasmInitialized,
791
+ AcornParser,
792
+ generateNoir,
793
+ } from '@izi-noir/sdk';${solanaImports}
794
+ import { CodeBlock } from './components/CodeBlock';
795
+ ${circuitImports}
796
+ import './App.css';
797
+
798
+ // Circuit definition type
799
+ interface CircuitDef {
800
+ name: string;
801
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
802
+ fn: any;
803
+ publicInputKeys: string[];
804
+ privateInputKeys: string[];
805
+ defaultInputs: Record<string, string>;
806
+ }
807
+
808
+ // Circuit options
809
+ const CIRCUITS: CircuitDef[] = ${circuitOptions};
810
+
811
+ // WASM initialization
812
+ let wasmInitialized = false;
813
+ async function initBrowserWasm() {
814
+ if (wasmInitialized) return;
815
+ await Promise.all([
816
+ initACVM({ module_or_path: acvm }),
817
+ initNoirC({ module_or_path: noirc }),
818
+ ]);
819
+ markWasmInitialized();
820
+ wasmInitialized = true;
821
+ }
822
+
823
+ function App() {
824
+ // Circuit state
825
+ const [selectedCircuit, setSelectedCircuit] = useState(CIRCUITS[0].name);
826
+ const [circuitCode, setCircuitCode] = useState(CIRCUITS[0].fn.toString());
827
+ const [noirCode, setNoirCode] = useState<string | null>(null);
828
+ const [transpileError, setTranspileError] = useState<string | null>(null);
829
+
830
+ // Input state
831
+ const [inputs, setInputs] = useState<Record<string, string>>(CIRCUITS[0].defaultInputs);
832
+
833
+ // Proof state
834
+ const [isGenerating, setIsGenerating] = useState(false);
835
+ const [proof, setProof] = useState<Uint8Array | null>(null);
836
+ const [proofTime, setProofTime] = useState<number | null>(null);
837
+ const [proofError, setProofError] = useState<string | null>(null);
838
+ const [localVerified, setLocalVerified] = useState<boolean | null>(null);
839
+
840
+ // IziNoir instance
841
+ const [iziInstance, setIziInstance] = useState<IziNoir | null>(null);
842
+ ${solanaHooks}
843
+ ${solanaState}
844
+
845
+ // Update circuit when selection changes
846
+ useEffect(() => {
847
+ const circuit = CIRCUITS.find(c => c.name === selectedCircuit);
848
+ if (circuit) {
849
+ setCircuitCode(circuit.fn.toString());
850
+ setInputs(circuit.defaultInputs);
851
+ // Reset proof state
852
+ setProof(null);
853
+ setProofTime(null);
854
+ setLocalVerified(null);
855
+ setNoirCode(null);
856
+ }
857
+ }, [selectedCircuit]);
858
+
859
+ // Transpile circuit to Noir
860
+ useEffect(() => {
861
+ const transpileCode = () => {
862
+ try {
863
+ setTranspileError(null);
864
+ const circuit = CIRCUITS.find(c => c.name === selectedCircuit);
865
+ if (!circuit) return;
866
+
867
+ const parser = new AcornParser();
868
+ const publicInputs = Object.entries(inputs)
869
+ .filter(([key]) => circuit.publicInputKeys.includes(key))
870
+ .map(([, val]) => Number(val));
871
+ const privateInputs = Object.entries(inputs)
872
+ .filter(([key]) => circuit.privateInputKeys.includes(key))
873
+ .map(([, val]) => Number(val));
874
+
875
+ const parsedCircuit = parser.parse(circuit.fn, publicInputs, privateInputs);
876
+ const result = generateNoir(parsedCircuit);
877
+ setNoirCode(result);
878
+ } catch (error) {
879
+ setTranspileError((error as Error).message);
880
+ setNoirCode(null);
881
+ }
882
+ };
883
+
884
+ const debounce = setTimeout(transpileCode, 300);
885
+ return () => clearTimeout(debounce);
886
+ }, [selectedCircuit, inputs]);
887
+
888
+ // Generate proof
889
+ const handleGenerateProof = useCallback(async () => {
890
+ if (!noirCode) return;
891
+
892
+ setIsGenerating(true);
893
+ setProofError(null);
894
+ setProof(null);
895
+ setProofTime(null);
896
+ setLocalVerified(null);
897
+
898
+ try {
899
+ await initBrowserWasm();
900
+
901
+ const startTime = performance.now();
902
+
903
+ const izi = await IziNoir.init({
904
+ provider: Provider.${capitalizeFirst(options.provider)},${solanaProviderConfig}
905
+ });
906
+
907
+ await izi.compile(noirCode);
908
+
909
+ const proofResult = await izi.prove(inputs);
910
+ setIziInstance(izi);
911
+
912
+ // Get proof bytes
913
+ const proofBytes = 'bytes' in proofResult.proof
914
+ ? proofResult.proof.bytes
915
+ : proofResult.proof;
916
+ const publicInputsHex = 'hex' in proofResult.publicInputs
917
+ ? proofResult.publicInputs.hex
918
+ : proofResult.publicInputs;
919
+
920
+ // Local verification
921
+ const verified = await izi.verify(proofBytes, publicInputsHex);
922
+ setLocalVerified(verified);
923
+
924
+ const endTime = performance.now();
925
+ setProof(proofBytes);
926
+ setProofTime(Math.round(endTime - startTime));
927
+ } catch (error) {
928
+ console.error('Proof generation error:', error);
929
+ setProofError((error as Error).message);
930
+ } finally {
931
+ setIsGenerating(false);
932
+ }
933
+ }, [noirCode, inputs]);
934
+ ${solanaHandlers}
935
+
936
+ return (
937
+ <div className="app">
938
+ <header>
939
+ <h1>${options.projectName}</h1>
940
+ <p>Zero-Knowledge Proof Demo</p>
941
+ </header>
942
+
943
+ <main>
944
+ {/* Circuit Selection */}
945
+ <div className="section">
946
+ <h2>1. Select Circuit</h2>
947
+ <select
948
+ value={selectedCircuit}
949
+ onChange={(e) => setSelectedCircuit(e.target.value)}
950
+ className="select"
951
+ >
952
+ {CIRCUITS.map((c) => (
953
+ <option key={c.name} value={c.name}>
954
+ {c.name}
955
+ </option>
956
+ ))}
957
+ </select>
958
+ </div>
959
+
960
+ {/* Circuit Code */}
961
+ <div className="section">
962
+ <h2>2. Circuit Code</h2>
963
+ <CodeBlock code={circuitCode} language="typescript" />
964
+
965
+ {noirCode && (
966
+ <details className="noir-details">
967
+ <summary>View Generated Noir</summary>
968
+ <CodeBlock code={noirCode} language="rust" />
969
+ </details>
970
+ )}
971
+
972
+ {transpileError && (
973
+ <p className="error">{transpileError}</p>
974
+ )}
975
+ </div>
976
+
977
+ {/* Inputs */}
978
+ <div className="section">
979
+ <h2>3. Inputs</h2>
980
+ <div className="inputs-grid">
981
+ {Object.entries(inputs).map(([key, value]) => {
982
+ const circuit = CIRCUITS.find(c => c.name === selectedCircuit);
983
+ const isPublic = circuit?.publicInputKeys.includes(key);
984
+ return (
985
+ <div key={key} className="input-group">
986
+ <label>
987
+ <span className={\`input-badge \${isPublic ? 'public' : 'private'}\`}>
988
+ {isPublic ? 'public' : 'private'}
989
+ </span>
990
+ {key}
991
+ </label>
992
+ <input
993
+ type="number"
994
+ value={value}
995
+ onChange={(e) => setInputs({ ...inputs, [key]: e.target.value })}
996
+ />
997
+ </div>
998
+ );
999
+ })}
1000
+ </div>
1001
+ </div>
1002
+
1003
+ {/* Generate Proof */}
1004
+ <div className="section">
1005
+ <h2>4. Generate Proof</h2>
1006
+ <button
1007
+ onClick={handleGenerateProof}
1008
+ disabled={isGenerating || !noirCode || !!transpileError}
1009
+ className="btn btn-primary"
1010
+ >
1011
+ {isGenerating ? 'Generating...' : 'Generate Proof'}
1012
+ </button>
1013
+
1014
+ {proofError && <p className="error">{proofError}</p>}
1015
+
1016
+ {proof && (
1017
+ <div className="results">
1018
+ <div className="result-card">
1019
+ <span className="result-value">{proof.length} bytes</span>
1020
+ <span className="result-label">Proof Size</span>
1021
+ </div>
1022
+ <div className="result-card">
1023
+ <span className="result-value">{proofTime} ms</span>
1024
+ <span className="result-label">Generation Time</span>
1025
+ </div>
1026
+ <div className="result-card">
1027
+ <span className={\`result-value \${localVerified ? 'success' : 'error'}\`}>
1028
+ {localVerified ? 'Yes' : 'No'}
1029
+ </span>
1030
+ <span className="result-label">Locally Verified</span>
1031
+ </div>
1032
+ </div>
1033
+ )}
1034
+ </div>
1035
+ ${solanaDeploySection}
1036
+ </main>
1037
+
1038
+ <footer>
1039
+ <p>Built with <a href="https://github.com/izi-noir/izi-noir" target="_blank">IZI-NOIR</a></p>
1040
+ </footer>
1041
+ </div>
1042
+ );
1043
+ }
1044
+
1045
+ export default App;
1046
+ `;
1047
+ }
1048
+ function getCircuitImports(template) {
1049
+ switch (template) {
1050
+ case "minimal":
1051
+ return `import { myCircuit } from '../circuits';`;
1052
+ case "balance-proof":
1053
+ return `import { balanceProof } from '../circuits';`;
1054
+ default:
1055
+ return `import { balanceProof, ageProof } from '../circuits';`;
1056
+ }
1057
+ }
1058
+ function getCircuitOptions(template) {
1059
+ switch (template) {
1060
+ case "minimal":
1061
+ return `[
1062
+ {
1063
+ name: 'myCircuit',
1064
+ fn: myCircuit,
1065
+ publicInputKeys: ['publicInput'],
1066
+ privateInputKeys: ['privateInput'],
1067
+ defaultInputs: { publicInput: '42', privateInput: '42' },
1068
+ },
1069
+ ]`;
1070
+ case "balance-proof":
1071
+ return `[
1072
+ {
1073
+ name: 'balanceProof',
1074
+ fn: balanceProof,
1075
+ publicInputKeys: ['threshold'],
1076
+ privateInputKeys: ['balance'],
1077
+ defaultInputs: { threshold: '100', balance: '1500' },
1078
+ },
1079
+ ]`;
1080
+ default:
1081
+ return `[
1082
+ {
1083
+ name: 'balanceProof',
1084
+ fn: balanceProof,
1085
+ publicInputKeys: ['threshold'],
1086
+ privateInputKeys: ['balance'],
1087
+ defaultInputs: { threshold: '100', balance: '1500' },
1088
+ },
1089
+ {
1090
+ name: 'ageProof',
1091
+ fn: ageProof,
1092
+ publicInputKeys: ['currentYear', 'minAge'],
1093
+ privateInputKeys: ['birthYear'],
1094
+ defaultInputs: { currentYear: '2024', minAge: '18', birthYear: '1990' },
1095
+ },
1096
+ ]`;
1097
+ }
1098
+ }
1099
+ function capitalizeFirst(str) {
1100
+ return str.charAt(0).toUpperCase() + str.slice(1);
1101
+ }
1102
+ function generateAppCss() {
1103
+ return `* {
1104
+ box-sizing: border-box;
1105
+ margin: 0;
1106
+ padding: 0;
1107
+ }
1108
+
1109
+ :root {
1110
+ --purple: #9945FF;
1111
+ --green: #14F195;
1112
+ --bg: #0a0a0a;
1113
+ --bg-elevated: #111111;
1114
+ --border: #222222;
1115
+ --text: #ffffff;
1116
+ --text-muted: #888888;
1117
+ }
1118
+
1119
+ body {
1120
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1121
+ background: var(--bg);
1122
+ color: var(--text);
1123
+ line-height: 1.6;
1124
+ }
1125
+
1126
+ .app {
1127
+ min-height: 100vh;
1128
+ display: flex;
1129
+ flex-direction: column;
1130
+ }
1131
+
1132
+ header {
1133
+ padding: 2rem;
1134
+ text-align: center;
1135
+ border-bottom: 1px solid var(--border);
1136
+ }
1137
+
1138
+ header h1 {
1139
+ font-size: 2rem;
1140
+ background: linear-gradient(90deg, var(--purple), var(--green));
1141
+ -webkit-background-clip: text;
1142
+ -webkit-text-fill-color: transparent;
1143
+ background-clip: text;
1144
+ }
1145
+
1146
+ header p {
1147
+ color: var(--text-muted);
1148
+ margin-top: 0.5rem;
1149
+ }
1150
+
1151
+ main {
1152
+ flex: 1;
1153
+ max-width: 800px;
1154
+ margin: 0 auto;
1155
+ padding: 2rem;
1156
+ width: 100%;
1157
+ }
1158
+
1159
+ .section {
1160
+ margin-bottom: 2rem;
1161
+ padding: 1.5rem;
1162
+ background: var(--bg-elevated);
1163
+ border: 1px solid var(--border);
1164
+ border-radius: 12px;
1165
+ }
1166
+
1167
+ .section h2 {
1168
+ font-size: 1.25rem;
1169
+ margin-bottom: 1rem;
1170
+ color: var(--text);
1171
+ }
1172
+
1173
+ .select {
1174
+ width: 100%;
1175
+ padding: 0.75rem;
1176
+ background: var(--bg);
1177
+ border: 1px solid var(--border);
1178
+ border-radius: 8px;
1179
+ color: var(--text);
1180
+ font-size: 1rem;
1181
+ cursor: pointer;
1182
+ }
1183
+
1184
+ .select:focus {
1185
+ outline: none;
1186
+ border-color: var(--purple);
1187
+ }
1188
+
1189
+ .inputs-grid {
1190
+ display: grid;
1191
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1192
+ gap: 1rem;
1193
+ }
1194
+
1195
+ .input-group {
1196
+ display: flex;
1197
+ flex-direction: column;
1198
+ gap: 0.5rem;
1199
+ }
1200
+
1201
+ .input-group label {
1202
+ display: flex;
1203
+ align-items: center;
1204
+ gap: 0.5rem;
1205
+ font-size: 0.875rem;
1206
+ color: var(--text-muted);
1207
+ }
1208
+
1209
+ .input-badge {
1210
+ font-size: 0.625rem;
1211
+ padding: 0.125rem 0.375rem;
1212
+ border-radius: 4px;
1213
+ text-transform: uppercase;
1214
+ font-weight: 600;
1215
+ }
1216
+
1217
+ .input-badge.public {
1218
+ background: var(--green);
1219
+ color: #000;
1220
+ }
1221
+
1222
+ .input-badge.private {
1223
+ background: var(--purple);
1224
+ color: #fff;
1225
+ }
1226
+
1227
+ .input-group input {
1228
+ padding: 0.75rem;
1229
+ background: var(--bg);
1230
+ border: 1px solid var(--border);
1231
+ border-radius: 8px;
1232
+ color: var(--text);
1233
+ font-size: 1rem;
1234
+ }
1235
+
1236
+ .input-group input:focus {
1237
+ outline: none;
1238
+ border-color: var(--purple);
1239
+ }
1240
+
1241
+ .btn {
1242
+ padding: 0.75rem 1.5rem;
1243
+ font-size: 1rem;
1244
+ font-weight: 600;
1245
+ border: none;
1246
+ border-radius: 8px;
1247
+ cursor: pointer;
1248
+ transition: all 0.2s;
1249
+ }
1250
+
1251
+ .btn:disabled {
1252
+ opacity: 0.5;
1253
+ cursor: not-allowed;
1254
+ }
1255
+
1256
+ .btn-primary {
1257
+ background: linear-gradient(90deg, var(--purple), var(--green));
1258
+ color: white;
1259
+ }
1260
+
1261
+ .btn-primary:hover:not(:disabled) {
1262
+ transform: translateY(-2px);
1263
+ box-shadow: 0 4px 20px rgba(153, 69, 255, 0.3);
1264
+ }
1265
+
1266
+ .btn-secondary {
1267
+ background: var(--bg);
1268
+ border: 1px solid var(--border);
1269
+ color: var(--text);
1270
+ }
1271
+
1272
+ .btn-secondary:hover:not(:disabled) {
1273
+ border-color: var(--purple);
1274
+ }
1275
+
1276
+ .results {
1277
+ display: grid;
1278
+ grid-template-columns: repeat(3, 1fr);
1279
+ gap: 1rem;
1280
+ margin-top: 1rem;
1281
+ }
1282
+
1283
+ .result-card {
1284
+ padding: 1rem;
1285
+ background: var(--bg);
1286
+ border: 1px solid var(--border);
1287
+ border-radius: 8px;
1288
+ text-align: center;
1289
+ }
1290
+
1291
+ .result-value {
1292
+ font-size: 1.25rem;
1293
+ font-weight: 600;
1294
+ display: block;
1295
+ }
1296
+
1297
+ .result-value.success {
1298
+ color: var(--green);
1299
+ }
1300
+
1301
+ .result-value.error {
1302
+ color: #ff4444;
1303
+ }
1304
+
1305
+ .result-label {
1306
+ font-size: 0.75rem;
1307
+ color: var(--text-muted);
1308
+ text-transform: uppercase;
1309
+ margin-top: 0.25rem;
1310
+ display: block;
1311
+ }
1312
+
1313
+ .error {
1314
+ color: #ff4444;
1315
+ font-size: 0.875rem;
1316
+ margin-top: 0.5rem;
1317
+ }
1318
+
1319
+ .success {
1320
+ color: var(--green);
1321
+ font-size: 0.875rem;
1322
+ margin-top: 0.5rem;
1323
+ }
1324
+
1325
+ .noir-details {
1326
+ margin-top: 1rem;
1327
+ }
1328
+
1329
+ .noir-details summary {
1330
+ cursor: pointer;
1331
+ color: var(--text-muted);
1332
+ font-size: 0.875rem;
1333
+ }
1334
+
1335
+ .noir-details summary:hover {
1336
+ color: var(--text);
1337
+ }
1338
+
1339
+ .deploy-verify-row {
1340
+ display: grid;
1341
+ grid-template-columns: 1fr 1fr;
1342
+ gap: 1rem;
1343
+ margin-top: 1rem;
1344
+ }
1345
+
1346
+ .deploy-box,
1347
+ .verify-box {
1348
+ padding: 1rem;
1349
+ background: var(--bg);
1350
+ border: 1px solid var(--border);
1351
+ border-radius: 8px;
1352
+ }
1353
+
1354
+ footer {
1355
+ padding: 1.5rem;
1356
+ text-align: center;
1357
+ border-top: 1px solid var(--border);
1358
+ color: var(--text-muted);
1359
+ font-size: 0.875rem;
1360
+ }
1361
+
1362
+ footer a {
1363
+ color: var(--purple);
1364
+ text-decoration: none;
1365
+ }
1366
+
1367
+ footer a:hover {
1368
+ text-decoration: underline;
1369
+ }
1370
+ `;
1371
+ }
1372
+ function generateIndexCss() {
1373
+ return `body {
1374
+ margin: 0;
1375
+ -webkit-font-smoothing: antialiased;
1376
+ -moz-osx-font-smoothing: grayscale;
1377
+ }
1378
+ `;
1379
+ }
1380
+ function generateViteEnvDts() {
1381
+ return `/// <reference types="vite/client" />
1382
+ `;
1383
+ }
1384
+ function generateViteSvg() {
1385
+ return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFBD4F"></stop><stop offset="100%" stop-color="#FF980E"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
1386
+ `;
1387
+ }
1388
+
1389
+ // src/generators/components.ts
1390
+ function generateCodeBlock() {
1391
+ return `import { Highlight, themes } from 'prism-react-renderer';
1392
+
1393
+ interface CodeBlockProps {
1394
+ code: string;
1395
+ language?: 'typescript' | 'javascript' | 'rust';
1396
+ }
1397
+
1398
+ export function CodeBlock({ code, language = 'typescript' }: CodeBlockProps) {
1399
+ return (
1400
+ <Highlight theme={themes.nightOwl} code={code.trim()} language={language}>
1401
+ {({ className, style, tokens, getLineProps, getTokenProps }) => (
1402
+ <pre
1403
+ className={className}
1404
+ style={{
1405
+ ...style,
1406
+ padding: '1rem',
1407
+ borderRadius: '8px',
1408
+ overflow: 'auto',
1409
+ fontSize: '0.875rem',
1410
+ lineHeight: '1.5',
1411
+ }}
1412
+ >
1413
+ {tokens.map((line, i) => (
1414
+ <div key={i} {...getLineProps({ line })}>
1415
+ <span style={{ color: '#666', marginRight: '1rem', userSelect: 'none' }}>
1416
+ {String(i + 1).padStart(2, ' ')}
1417
+ </span>
1418
+ {line.map((token, key) => (
1419
+ <span key={key} {...getTokenProps({ token })} />
1420
+ ))}
1421
+ </div>
1422
+ ))}
1423
+ </pre>
1424
+ )}
1425
+ </Highlight>
1426
+ );
1427
+ }
1428
+ `;
1429
+ }
1430
+ function generateEditableCodeBlock() {
1431
+ return `import { useRef, useEffect, useState } from 'react';
1432
+ import { Highlight, themes } from 'prism-react-renderer';
1433
+
1434
+ interface EditableCodeBlockProps {
1435
+ code: string;
1436
+ onChange: (code: string) => void;
1437
+ language?: 'typescript' | 'javascript';
1438
+ rows?: number;
1439
+ }
1440
+
1441
+ export function EditableCodeBlock({
1442
+ code,
1443
+ onChange,
1444
+ language = 'typescript',
1445
+ rows = 8,
1446
+ }: EditableCodeBlockProps) {
1447
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
1448
+ const preRef = useRef<HTMLPreElement>(null);
1449
+ const [scrollTop, setScrollTop] = useState(0);
1450
+
1451
+ useEffect(() => {
1452
+ if (preRef.current) {
1453
+ preRef.current.scrollTop = scrollTop;
1454
+ }
1455
+ }, [scrollTop]);
1456
+
1457
+ const handleScroll = () => {
1458
+ if (textareaRef.current) {
1459
+ setScrollTop(textareaRef.current.scrollTop);
1460
+ }
1461
+ };
1462
+
1463
+ const lineHeight = 1.5;
1464
+ const padding = 16;
1465
+ const minHeight = rows * 14 * lineHeight + padding * 2;
1466
+
1467
+ return (
1468
+ <div style={{ position: 'relative', minHeight }}>
1469
+ <Highlight theme={themes.nightOwl} code={code} language={language}>
1470
+ {({ className, style, tokens, getLineProps, getTokenProps }) => (
1471
+ <pre
1472
+ ref={preRef}
1473
+ className={className}
1474
+ style={{
1475
+ ...style,
1476
+ position: 'absolute',
1477
+ top: 0,
1478
+ left: 0,
1479
+ right: 0,
1480
+ bottom: 0,
1481
+ padding: '1rem',
1482
+ borderRadius: '8px',
1483
+ overflow: 'auto',
1484
+ fontSize: '0.875rem',
1485
+ lineHeight: '1.5',
1486
+ margin: 0,
1487
+ pointerEvents: 'none',
1488
+ }}
1489
+ >
1490
+ {tokens.map((line, i) => (
1491
+ <div key={i} {...getLineProps({ line })}>
1492
+ {line.map((token, key) => (
1493
+ <span key={key} {...getTokenProps({ token })} />
1494
+ ))}
1495
+ </div>
1496
+ ))}
1497
+ </pre>
1498
+ )}
1499
+ </Highlight>
1500
+
1501
+ <textarea
1502
+ ref={textareaRef}
1503
+ value={code}
1504
+ onChange={(e) => onChange(e.target.value)}
1505
+ onScroll={handleScroll}
1506
+ spellCheck={false}
1507
+ style={{
1508
+ position: 'absolute',
1509
+ top: 0,
1510
+ left: 0,
1511
+ right: 0,
1512
+ bottom: 0,
1513
+ width: '100%',
1514
+ height: '100%',
1515
+ minHeight,
1516
+ padding: '1rem',
1517
+ background: 'transparent',
1518
+ border: 'none',
1519
+ outline: 'none',
1520
+ resize: 'none',
1521
+ fontFamily: 'monospace',
1522
+ fontSize: '0.875rem',
1523
+ lineHeight: '1.5',
1524
+ color: 'transparent',
1525
+ caretColor: 'white',
1526
+ WebkitTextFillColor: 'transparent',
1527
+ }}
1528
+ />
1529
+ </div>
1530
+ );
1531
+ }
1532
+ `;
1533
+ }
1534
+
540
1535
  // src/commands/init.ts
541
1536
  async function initCommand(projectName, options) {
542
1537
  let projectOptions;
@@ -558,28 +1553,31 @@ async function initCommand(projectName, options) {
558
1553
  });
559
1554
  }
560
1555
  if (!projectOptions) {
561
- console.log(pc3.yellow("\nOperation cancelled."));
1556
+ console.log(pc4.yellow("\nOperation cancelled."));
562
1557
  process.exit(0);
563
1558
  }
564
1559
  const projectDir = path2.resolve(process.cwd(), projectOptions.projectName);
565
1560
  if (await directoryExists(projectDir)) {
566
1561
  if (!await isDirectoryEmpty(projectDir)) {
567
1562
  console.log(
568
- pc3.red(`
1563
+ pc4.red(`
569
1564
  Error: Directory "${projectOptions.projectName}" already exists and is not empty.`)
570
1565
  );
571
1566
  process.exit(1);
572
1567
  }
573
1568
  }
574
1569
  console.log();
575
- const spinner = createSpinner("Creating project structure...");
576
- spinner.start();
1570
+ const progress = createProgressReporter();
577
1571
  try {
578
- await createProjectStructure(projectDir, projectOptions);
579
- spinner.stop(true);
1572
+ await progress.startThinking();
1573
+ await new Promise((r) => setTimeout(r, 800));
1574
+ progress.stopThinking();
1575
+ console.log(pc4.bold("\n Scaffolding your ZK project...\n"));
1576
+ await createProjectStructure(projectDir, projectOptions, progress);
1577
+ progress.showSuccess("Project structure created");
580
1578
  } catch (error) {
581
- spinner.stop(false);
582
- console.error(pc3.red("\nFailed to create project structure:"), error);
1579
+ progress.showError("Failed to create project structure");
1580
+ console.error(pc4.red("\n"), error);
583
1581
  process.exit(1);
584
1582
  }
585
1583
  if (!projectOptions.skipGit) {
@@ -590,33 +1588,48 @@ Error: Directory "${projectOptions.projectName}" already exists and is not empty
590
1588
  gitSpinner.stop(true);
591
1589
  } catch {
592
1590
  gitSpinner.stop(false);
593
- console.log(pc3.yellow(" Warning: Failed to initialize git repository"));
1591
+ console.log(pc4.yellow(" Warning: Failed to initialize git repository"));
594
1592
  }
595
1593
  }
596
1594
  if (!projectOptions.skipInstall) {
597
- const installSpinner = createSpinner("Installing dependencies...");
598
- installSpinner.start();
1595
+ const installProgress = createInstallProgress();
1596
+ installProgress.start();
599
1597
  try {
600
1598
  execSync("npm install", { cwd: projectDir, stdio: "ignore" });
601
- installSpinner.stop(true);
1599
+ installProgress.stop(true);
602
1600
  } catch {
603
- installSpinner.stop(false);
604
- console.log(pc3.yellow(' Warning: Failed to install dependencies. Run "npm install" manually.'));
1601
+ installProgress.stop(false);
1602
+ console.log(pc4.yellow(' Run "npm install" manually.'));
605
1603
  }
606
1604
  }
607
1605
  printSuccessMessage(projectOptions);
608
1606
  }
609
- async function createProjectStructure(projectDir, options) {
610
- await ensureDir(path2.join(projectDir, "circuits"));
611
- await ensureDir(path2.join(projectDir, "generated"));
612
- await ensureDir(path2.join(projectDir, "scripts"));
1607
+ async function createProjectStructure(projectDir, options, progress) {
1608
+ const dirs = ["circuits", "src", "src/components", "src/lib", "public"];
1609
+ for (const dir of dirs) {
1610
+ await ensureDir(path2.join(projectDir, dir));
1611
+ await progress.reportDirectory(dir);
1612
+ }
613
1613
  const files = [
1614
+ // Root config files
614
1615
  ["package.json", generatePackageJson(options)],
615
1616
  ["tsconfig.json", generateTsconfig()],
616
- ["izi-noir.config.ts", generateConfig(options)],
1617
+ ["tsconfig.node.json", generateTsconfigNode()],
1618
+ ["vite.config.ts", generateViteConfig()],
1619
+ ["index.html", generateIndexHtml(options)],
617
1620
  ["README.md", generateReadme(options)],
618
1621
  [".gitignore", generateGitignore()],
619
- ["scripts/test-proof.ts", generateTestScript(options)]
1622
+ // Source files
1623
+ ["src/main.tsx", generateMainTsx(options)],
1624
+ ["src/App.tsx", generateAppTsx(options)],
1625
+ ["src/App.css", generateAppCss()],
1626
+ ["src/index.css", generateIndexCss()],
1627
+ ["src/vite-env.d.ts", generateViteEnvDts()],
1628
+ // Components
1629
+ ["src/components/CodeBlock.tsx", generateCodeBlock()],
1630
+ ["src/components/EditableCodeBlock.tsx", generateEditableCodeBlock()],
1631
+ // Public assets
1632
+ ["public/vite.svg", generateViteSvg()]
620
1633
  ];
621
1634
  switch (options.template) {
622
1635
  case "minimal":
@@ -632,37 +1645,34 @@ async function createProjectStructure(projectDir, options) {
632
1645
  }
633
1646
  files.push(["circuits/index.ts", generateCircuitsIndex(options.template)]);
634
1647
  files.push(["circuits/types.d.ts", generateCircuitTypes()]);
635
- files.push([
636
- "generated/.gitkeep",
637
- "# This directory contains compiled circuit artifacts\n# Generated by: npm run build\n"
638
- ]);
639
- await Promise.all(
640
- files.map(
641
- ([relativePath, content]) => writeFile(path2.join(projectDir, relativePath), content)
642
- )
643
- );
1648
+ for (let i = 0; i < files.length; i++) {
1649
+ const [relativePath, content] = files[i];
1650
+ await writeFile(path2.join(projectDir, relativePath), content);
1651
+ await progress.reportFile(relativePath, i === files.length - 1);
1652
+ }
644
1653
  }
645
1654
  function printSuccessMessage(options) {
646
1655
  console.log();
647
- console.log(pc3.green("\u2713") + " Project created successfully!");
1656
+ console.log(pc4.green("\u2713") + " Project created successfully!");
648
1657
  console.log();
649
1658
  console.log("Next steps:");
650
1659
  console.log();
651
- console.log(pc3.cyan(` cd ${options.projectName}`));
1660
+ console.log(pc4.cyan(` cd ${options.projectName}`));
652
1661
  if (options.skipInstall) {
653
- console.log(pc3.cyan(" npm install"));
1662
+ console.log(pc4.cyan(" npm install"));
654
1663
  }
655
- console.log(pc3.cyan(" npm run build"));
656
- console.log(pc3.cyan(" npm test"));
1664
+ console.log(pc4.cyan(" npm run dev"));
1665
+ console.log();
1666
+ console.log("Then open " + pc4.blue("http://localhost:5173") + " in your browser.");
657
1667
  console.log();
658
- console.log("To start developing:");
1668
+ console.log("To add circuits:");
659
1669
  console.log();
660
- console.log(pc3.dim(" 1. Edit circuits in circuits/*.ts"));
661
- console.log(pc3.dim(' 2. Run "npm run dev" for watch mode'));
662
- console.log(pc3.dim(' 3. Test with "npm test"'));
1670
+ console.log(pc4.dim(" 1. Create a new circuit in circuits/*.ts"));
1671
+ console.log(pc4.dim(" 2. Export it from circuits/index.ts"));
1672
+ console.log(pc4.dim(" 3. Add it to CIRCUITS array in src/App.tsx"));
663
1673
  console.log();
664
1674
  console.log(
665
- pc3.dim("Learn more: ") + pc3.blue("https://github.com/izi-noir/izi-noir")
1675
+ pc4.dim("Learn more: ") + pc4.blue("https://github.com/izi-noir/izi-noir")
666
1676
  );
667
1677
  console.log();
668
1678
  }