create-izi-noir 0.1.10 → 0.2.0
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 +1073 -209
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -155,39 +155,42 @@ function createSpinner(message) {
|
|
|
155
155
|
|
|
156
156
|
// src/generators/package-json.ts
|
|
157
157
|
function generatePackageJson(options) {
|
|
158
|
+
const isSolana = options.provider === "arkworks";
|
|
159
|
+
const dependencies = {
|
|
160
|
+
"@izi-noir/sdk": "^0.1.4",
|
|
161
|
+
"@noir-lang/acvm_js": "1.0.0-beta.13-1d260df.nightly",
|
|
162
|
+
"@noir-lang/noirc_abi": "1.0.0-beta.13-1d260df.nightly",
|
|
163
|
+
"react": "^18.3.1",
|
|
164
|
+
"react-dom": "^18.3.1",
|
|
165
|
+
"prism-react-renderer": "^2.4.1"
|
|
166
|
+
};
|
|
167
|
+
if (isSolana) {
|
|
168
|
+
dependencies["@solana/wallet-adapter-react"] = "^0.15.0";
|
|
169
|
+
dependencies["@solana/wallet-adapter-react-ui"] = "^0.9.0";
|
|
170
|
+
dependencies["@solana/wallet-adapter-wallets"] = "^0.19.0";
|
|
171
|
+
dependencies["@solana/web3.js"] = "^1.95.0";
|
|
172
|
+
}
|
|
158
173
|
const pkg = {
|
|
159
174
|
name: options.projectName,
|
|
160
175
|
version: "0.1.0",
|
|
176
|
+
private: true,
|
|
161
177
|
description: "ZK circuits built with IZI-NOIR",
|
|
162
178
|
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
179
|
scripts: {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
},
|
|
182
|
-
dependencies: {
|
|
183
|
-
"@izi-noir/sdk": "^0.1.4"
|
|
180
|
+
dev: "vite",
|
|
181
|
+
build: "vite build",
|
|
182
|
+
preview: "vite preview",
|
|
183
|
+
typecheck: "tsc --noEmit"
|
|
184
184
|
},
|
|
185
|
+
dependencies,
|
|
185
186
|
devDependencies: {
|
|
186
|
-
"@types/
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
"@types/react": "^18.3.0",
|
|
188
|
+
"@types/react-dom": "^18.3.0",
|
|
189
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
190
|
+
"typescript": "^5.4.0",
|
|
191
|
+
"vite": "^5.4.0"
|
|
189
192
|
},
|
|
190
|
-
keywords: ["zk", "noir", "zero-knowledge", "privacy", "solana"],
|
|
193
|
+
keywords: ["zk", "noir", "zero-knowledge", "privacy", isSolana ? "solana" : "evm"],
|
|
191
194
|
license: "MIT"
|
|
192
195
|
};
|
|
193
196
|
return JSON.stringify(pkg, null, 2);
|
|
@@ -197,50 +200,35 @@ function generatePackageJson(options) {
|
|
|
197
200
|
function generateTsconfig() {
|
|
198
201
|
const config = {
|
|
199
202
|
compilerOptions: {
|
|
200
|
-
target: "
|
|
203
|
+
target: "ES2020",
|
|
204
|
+
useDefineForClassFields: true,
|
|
205
|
+
lib: ["ES2020", "DOM", "DOM.Iterable"],
|
|
201
206
|
module: "ESNext",
|
|
207
|
+
skipLibCheck: true,
|
|
208
|
+
// Bundler mode
|
|
202
209
|
moduleResolution: "bundler",
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
210
|
+
allowImportingTsExtensions: true,
|
|
211
|
+
isolatedModules: true,
|
|
212
|
+
moduleDetection: "force",
|
|
213
|
+
noEmit: true,
|
|
214
|
+
jsx: "react-jsx",
|
|
215
|
+
// Linting
|
|
206
216
|
strict: true,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
noUnusedLocals: true,
|
|
218
|
+
noUnusedParameters: true,
|
|
219
|
+
noFallthroughCasesInSwitch: true,
|
|
220
|
+
// Paths
|
|
221
|
+
baseUrl: ".",
|
|
222
|
+
paths: {
|
|
223
|
+
"@/*": ["./src/*"]
|
|
224
|
+
}
|
|
214
225
|
},
|
|
215
|
-
include: ["
|
|
216
|
-
|
|
226
|
+
include: ["src", "circuits"],
|
|
227
|
+
references: [{ path: "./tsconfig.node.json" }]
|
|
217
228
|
};
|
|
218
229
|
return JSON.stringify(config, null, 2);
|
|
219
230
|
}
|
|
220
231
|
|
|
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
232
|
// src/generators/circuits.ts
|
|
245
233
|
function generateBalanceProof() {
|
|
246
234
|
return `/**
|
|
@@ -328,133 +316,49 @@ declare function assert(condition: boolean): void;
|
|
|
328
316
|
`;
|
|
329
317
|
}
|
|
330
318
|
|
|
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
319
|
// src/generators/readme.ts
|
|
427
320
|
function generateReadme(options) {
|
|
321
|
+
const isSolana = options.provider === "arkworks";
|
|
322
|
+
const networkInfo = isSolana ? "- Deploy VK to Solana devnet\n- Verify proofs on-chain" : "- Local proof verification";
|
|
428
323
|
return `# ${options.projectName}
|
|
429
324
|
|
|
430
|
-
ZK
|
|
325
|
+
ZK proof demo built with [IZI-NOIR](https://github.com/izi-noir/izi-noir) and React.
|
|
431
326
|
|
|
432
327
|
## Getting Started
|
|
433
328
|
|
|
434
329
|
\`\`\`bash
|
|
435
|
-
#
|
|
436
|
-
npm
|
|
437
|
-
|
|
438
|
-
# Run tests
|
|
439
|
-
npm test
|
|
330
|
+
# Install dependencies
|
|
331
|
+
npm install
|
|
440
332
|
|
|
441
|
-
#
|
|
333
|
+
# Start development server
|
|
442
334
|
npm run dev
|
|
335
|
+
|
|
336
|
+
# Build for production
|
|
337
|
+
npm run build
|
|
443
338
|
\`\`\`
|
|
444
339
|
|
|
340
|
+
Then open http://localhost:5173 in your browser.
|
|
341
|
+
|
|
342
|
+
## Features
|
|
343
|
+
|
|
344
|
+
- Interactive circuit selection
|
|
345
|
+
- Real-time proof generation
|
|
346
|
+
- Syntax-highlighted code display
|
|
347
|
+
${networkInfo}
|
|
348
|
+
|
|
445
349
|
## Project Structure
|
|
446
350
|
|
|
447
351
|
\`\`\`
|
|
448
352
|
${options.projectName}/
|
|
449
|
-
\u251C\u2500\u2500
|
|
450
|
-
\u2502 \u251C\u2500\u2500
|
|
451
|
-
\u2502 \
|
|
452
|
-
\u251C\u2500\u2500
|
|
453
|
-
\u2502 \
|
|
454
|
-
\
|
|
455
|
-
\u251C\u2500\u2500
|
|
456
|
-
\u2502 \u2514\u2500\u2500
|
|
457
|
-
\u251C\u2500\u2500
|
|
353
|
+
\u251C\u2500\u2500 src/
|
|
354
|
+
\u2502 \u251C\u2500\u2500 App.tsx # Main demo component
|
|
355
|
+
\u2502 \u251C\u2500\u2500 main.tsx # React entry point
|
|
356
|
+
\u2502 \u251C\u2500\u2500 components/ # Reusable components
|
|
357
|
+
\u2502 \u2514\u2500\u2500 lib/ # Utility functions
|
|
358
|
+
\u251C\u2500\u2500 circuits/ # Your ZK circuit definitions
|
|
359
|
+
\u2502 \u251C\u2500\u2500 *.ts # Circuits as JS functions with assert()
|
|
360
|
+
\u2502 \u2514\u2500\u2500 index.ts # Re-exports
|
|
361
|
+
\u251C\u2500\u2500 vite.config.ts # Vite configuration
|
|
458
362
|
\u2514\u2500\u2500 package.json
|
|
459
363
|
\`\`\`
|
|
460
364
|
|
|
@@ -472,35 +376,16 @@ export function myCircuit(
|
|
|
472
376
|
}
|
|
473
377
|
\`\`\`
|
|
474
378
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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.
|
|
379
|
+
After adding a new circuit:
|
|
380
|
+
1. Export it from \`circuits/index.ts\`
|
|
381
|
+
2. Add it to the CIRCUITS array in \`src/App.tsx\`
|
|
499
382
|
|
|
500
383
|
## Learn More
|
|
501
384
|
|
|
502
385
|
- [IZI-NOIR Documentation](https://github.com/izi-noir/izi-noir)
|
|
503
386
|
- [Noir Language](https://noir-lang.org)
|
|
387
|
+
- [Vite](https://vitejs.dev)
|
|
388
|
+
- [React](https://react.dev)
|
|
504
389
|
`;
|
|
505
390
|
}
|
|
506
391
|
|
|
@@ -537,6 +422,973 @@ coverage/
|
|
|
537
422
|
`;
|
|
538
423
|
}
|
|
539
424
|
|
|
425
|
+
// src/generators/vite.ts
|
|
426
|
+
function generateViteConfig() {
|
|
427
|
+
return `import { defineConfig, type PluginOption } from "vite";
|
|
428
|
+
import react from "@vitejs/plugin-react";
|
|
429
|
+
import path from "path";
|
|
430
|
+
|
|
431
|
+
// Plugin to set correct MIME type for WASM files in dev server
|
|
432
|
+
function wasmMimePlugin(): PluginOption {
|
|
433
|
+
return {
|
|
434
|
+
name: "wasm-mime-type",
|
|
435
|
+
configureServer(server) {
|
|
436
|
+
server.middlewares.use((req, res, next) => {
|
|
437
|
+
if (req.url?.endsWith(".wasm")) {
|
|
438
|
+
res.setHeader("Content-Type", "application/wasm");
|
|
439
|
+
}
|
|
440
|
+
next();
|
|
441
|
+
});
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export default defineConfig({
|
|
447
|
+
plugins: [react(), wasmMimePlugin()],
|
|
448
|
+
resolve: {
|
|
449
|
+
alias: {
|
|
450
|
+
"@": path.resolve(__dirname, "./src"),
|
|
451
|
+
buffer: "buffer/",
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
define: {
|
|
455
|
+
"global": "globalThis",
|
|
456
|
+
},
|
|
457
|
+
optimizeDeps: {
|
|
458
|
+
exclude: ["@noir-lang/noir_wasm", "@aztec/bb.js", "@izi-noir/sdk"],
|
|
459
|
+
include: ["buffer"],
|
|
460
|
+
},
|
|
461
|
+
build: {
|
|
462
|
+
rollupOptions: {
|
|
463
|
+
external: [
|
|
464
|
+
/arkworks_groth16_wasm/,
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
function generateTsconfigNode() {
|
|
472
|
+
return `{
|
|
473
|
+
"compilerOptions": {
|
|
474
|
+
"composite": true,
|
|
475
|
+
"skipLibCheck": true,
|
|
476
|
+
"module": "ESNext",
|
|
477
|
+
"moduleResolution": "bundler",
|
|
478
|
+
"allowSyntheticDefaultImports": true,
|
|
479
|
+
"strict": true
|
|
480
|
+
},
|
|
481
|
+
"include": ["vite.config.ts"]
|
|
482
|
+
}
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
function generateIndexHtml(options) {
|
|
486
|
+
return `<!doctype html>
|
|
487
|
+
<html lang="en">
|
|
488
|
+
<head>
|
|
489
|
+
<meta charset="UTF-8" />
|
|
490
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
491
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
492
|
+
<title>${options.projectName} - ZK Proof Demo</title>
|
|
493
|
+
</head>
|
|
494
|
+
<body>
|
|
495
|
+
<div id="root"></div>
|
|
496
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
497
|
+
</body>
|
|
498
|
+
</html>
|
|
499
|
+
`;
|
|
500
|
+
}
|
|
501
|
+
function generateMainTsx(options) {
|
|
502
|
+
const isSolana = options.provider === "arkworks";
|
|
503
|
+
if (isSolana) {
|
|
504
|
+
return `import { StrictMode } from 'react';
|
|
505
|
+
import { createRoot } from 'react-dom/client';
|
|
506
|
+
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
|
|
507
|
+
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
|
|
508
|
+
import { clusterApiUrl } from '@solana/web3.js';
|
|
509
|
+
import App from './App';
|
|
510
|
+
import './index.css';
|
|
511
|
+
import '@solana/wallet-adapter-react-ui/styles.css';
|
|
512
|
+
|
|
513
|
+
const endpoint = clusterApiUrl('devnet');
|
|
514
|
+
|
|
515
|
+
createRoot(document.getElementById('root')!).render(
|
|
516
|
+
<StrictMode>
|
|
517
|
+
<ConnectionProvider endpoint={endpoint}>
|
|
518
|
+
<WalletProvider wallets={[]} autoConnect>
|
|
519
|
+
<WalletModalProvider>
|
|
520
|
+
<App />
|
|
521
|
+
</WalletModalProvider>
|
|
522
|
+
</WalletProvider>
|
|
523
|
+
</ConnectionProvider>
|
|
524
|
+
</StrictMode>,
|
|
525
|
+
);
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
return `import { StrictMode } from 'react';
|
|
529
|
+
import { createRoot } from 'react-dom/client';
|
|
530
|
+
import App from './App';
|
|
531
|
+
import './index.css';
|
|
532
|
+
|
|
533
|
+
createRoot(document.getElementById('root')!).render(
|
|
534
|
+
<StrictMode>
|
|
535
|
+
<App />
|
|
536
|
+
</StrictMode>,
|
|
537
|
+
);
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
function generateAppTsx(options) {
|
|
541
|
+
const isSolana = options.provider === "arkworks";
|
|
542
|
+
const circuitImports = getCircuitImports(options.template);
|
|
543
|
+
const circuitOptions = getCircuitOptions(options.template);
|
|
544
|
+
const solanaImports = isSolana ? `
|
|
545
|
+
import { useWallet } from '@solana/wallet-adapter-react';
|
|
546
|
+
import { useWalletModal } from '@solana/wallet-adapter-react-ui';
|
|
547
|
+
import { Chain, Network } from '@izi-noir/sdk';` : "";
|
|
548
|
+
const solanaHooks = isSolana ? `
|
|
549
|
+
const { publicKey, connected, sendTransaction } = useWallet();
|
|
550
|
+
const { setVisible } = useWalletModal();` : "";
|
|
551
|
+
const solanaProviderConfig = isSolana ? `
|
|
552
|
+
chain: Chain.Solana,
|
|
553
|
+
network: Network.Devnet,` : "";
|
|
554
|
+
const solanaState = isSolana ? `
|
|
555
|
+
// Deploy state
|
|
556
|
+
const [isDeploying, setIsDeploying] = useState(false);
|
|
557
|
+
const [vkAccount, setVkAccount] = useState<string | null>(null);
|
|
558
|
+
const [deployError, setDeployError] = useState<string | null>(null);
|
|
559
|
+
|
|
560
|
+
// Verify state
|
|
561
|
+
const [isVerifying, setIsVerifying] = useState(false);
|
|
562
|
+
const [verified, setVerified] = useState<boolean | null>(null);
|
|
563
|
+
const [verifyError, setVerifyError] = useState<string | null>(null);` : "";
|
|
564
|
+
const solanaHandlers = isSolana ? `
|
|
565
|
+
// Deploy VK to Solana
|
|
566
|
+
const handleDeploy = async () => {
|
|
567
|
+
if (!iziInstance || !connected || !publicKey || !sendTransaction) return;
|
|
568
|
+
|
|
569
|
+
setIsDeploying(true);
|
|
570
|
+
setDeployError(null);
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
const result = await iziInstance.deploy({ publicKey, sendTransaction });
|
|
574
|
+
setVkAccount(result.vkAccount);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.error('Deploy error:', error);
|
|
577
|
+
setDeployError((error as Error).message);
|
|
578
|
+
} finally {
|
|
579
|
+
setIsDeploying(false);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Verify proof on-chain
|
|
584
|
+
const handleVerify = async () => {
|
|
585
|
+
if (!iziInstance || !vkAccount || !publicKey || !sendTransaction) return;
|
|
586
|
+
|
|
587
|
+
setIsVerifying(true);
|
|
588
|
+
setVerifyError(null);
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const result = await iziInstance.verifyOnChain({ publicKey, sendTransaction }, vkAccount);
|
|
592
|
+
setVerified(result.verified);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
console.error('Verify error:', error);
|
|
595
|
+
setVerifyError((error as Error).message);
|
|
596
|
+
} finally {
|
|
597
|
+
setIsVerifying(false);
|
|
598
|
+
}
|
|
599
|
+
};` : "";
|
|
600
|
+
const solanaDeploySection = isSolana ? `
|
|
601
|
+
{/* Deploy & Verify Section */}
|
|
602
|
+
{proof && (
|
|
603
|
+
<div className="section">
|
|
604
|
+
<h2>Deploy & Verify on Solana</h2>
|
|
605
|
+
|
|
606
|
+
{!connected ? (
|
|
607
|
+
<button onClick={() => setVisible(true)} className="btn btn-secondary">
|
|
608
|
+
Connect Wallet
|
|
609
|
+
</button>
|
|
610
|
+
) : (
|
|
611
|
+
<div className="deploy-verify-row">
|
|
612
|
+
<div className="deploy-box">
|
|
613
|
+
<button
|
|
614
|
+
onClick={handleDeploy}
|
|
615
|
+
disabled={isDeploying || !!vkAccount}
|
|
616
|
+
className="btn btn-primary"
|
|
617
|
+
>
|
|
618
|
+
{isDeploying ? 'Deploying...' : vkAccount ? 'Deployed' : 'Deploy VK'}
|
|
619
|
+
</button>
|
|
620
|
+
{deployError && <p className="error">{deployError}</p>}
|
|
621
|
+
{vkAccount && <p className="success">VK: {vkAccount.slice(0, 8)}...</p>}
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<div className="verify-box">
|
|
625
|
+
<button
|
|
626
|
+
onClick={handleVerify}
|
|
627
|
+
disabled={!vkAccount || isVerifying}
|
|
628
|
+
className="btn btn-primary"
|
|
629
|
+
>
|
|
630
|
+
{isVerifying ? 'Verifying...' : verified ? 'Verified!' : 'Verify On-Chain'}
|
|
631
|
+
</button>
|
|
632
|
+
{verifyError && <p className="error">{verifyError}</p>}
|
|
633
|
+
{verified && <p className="success">Proof verified on Solana!</p>}
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
)}
|
|
637
|
+
</div>
|
|
638
|
+
)}` : "";
|
|
639
|
+
return `import { useState, useEffect, useCallback } from 'react';
|
|
640
|
+
import initNoirC from '@noir-lang/noirc_abi';
|
|
641
|
+
import initACVM from '@noir-lang/acvm_js';
|
|
642
|
+
import acvm from '@noir-lang/acvm_js/web/acvm_js_bg.wasm?url';
|
|
643
|
+
import noirc from '@noir-lang/noirc_abi/web/noirc_abi_wasm_bg.wasm?url';
|
|
644
|
+
import {
|
|
645
|
+
IziNoir,
|
|
646
|
+
Provider,
|
|
647
|
+
markWasmInitialized,
|
|
648
|
+
AcornParser,
|
|
649
|
+
generateNoir,
|
|
650
|
+
} from '@izi-noir/sdk';${solanaImports}
|
|
651
|
+
import { CodeBlock } from './components/CodeBlock';
|
|
652
|
+
${circuitImports}
|
|
653
|
+
import './App.css';
|
|
654
|
+
|
|
655
|
+
// Circuit definition type
|
|
656
|
+
interface CircuitDef {
|
|
657
|
+
name: string;
|
|
658
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
659
|
+
fn: any;
|
|
660
|
+
publicInputKeys: string[];
|
|
661
|
+
privateInputKeys: string[];
|
|
662
|
+
defaultInputs: Record<string, string>;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Circuit options
|
|
666
|
+
const CIRCUITS: CircuitDef[] = ${circuitOptions};
|
|
667
|
+
|
|
668
|
+
// WASM initialization
|
|
669
|
+
let wasmInitialized = false;
|
|
670
|
+
async function initBrowserWasm() {
|
|
671
|
+
if (wasmInitialized) return;
|
|
672
|
+
await Promise.all([
|
|
673
|
+
initACVM({ module_or_path: acvm }),
|
|
674
|
+
initNoirC({ module_or_path: noirc }),
|
|
675
|
+
]);
|
|
676
|
+
markWasmInitialized();
|
|
677
|
+
wasmInitialized = true;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function App() {
|
|
681
|
+
// Circuit state
|
|
682
|
+
const [selectedCircuit, setSelectedCircuit] = useState(CIRCUITS[0].name);
|
|
683
|
+
const [circuitCode, setCircuitCode] = useState(CIRCUITS[0].fn.toString());
|
|
684
|
+
const [noirCode, setNoirCode] = useState<string | null>(null);
|
|
685
|
+
const [transpileError, setTranspileError] = useState<string | null>(null);
|
|
686
|
+
|
|
687
|
+
// Input state
|
|
688
|
+
const [inputs, setInputs] = useState<Record<string, string>>(CIRCUITS[0].defaultInputs);
|
|
689
|
+
|
|
690
|
+
// Proof state
|
|
691
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
692
|
+
const [proof, setProof] = useState<Uint8Array | null>(null);
|
|
693
|
+
const [proofTime, setProofTime] = useState<number | null>(null);
|
|
694
|
+
const [proofError, setProofError] = useState<string | null>(null);
|
|
695
|
+
const [localVerified, setLocalVerified] = useState<boolean | null>(null);
|
|
696
|
+
|
|
697
|
+
// IziNoir instance
|
|
698
|
+
const [iziInstance, setIziInstance] = useState<IziNoir | null>(null);
|
|
699
|
+
${solanaHooks}
|
|
700
|
+
${solanaState}
|
|
701
|
+
|
|
702
|
+
// Update circuit when selection changes
|
|
703
|
+
useEffect(() => {
|
|
704
|
+
const circuit = CIRCUITS.find(c => c.name === selectedCircuit);
|
|
705
|
+
if (circuit) {
|
|
706
|
+
setCircuitCode(circuit.fn.toString());
|
|
707
|
+
setInputs(circuit.defaultInputs);
|
|
708
|
+
// Reset proof state
|
|
709
|
+
setProof(null);
|
|
710
|
+
setProofTime(null);
|
|
711
|
+
setLocalVerified(null);
|
|
712
|
+
setNoirCode(null);
|
|
713
|
+
}
|
|
714
|
+
}, [selectedCircuit]);
|
|
715
|
+
|
|
716
|
+
// Transpile circuit to Noir
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
const transpileCode = () => {
|
|
719
|
+
try {
|
|
720
|
+
setTranspileError(null);
|
|
721
|
+
const circuit = CIRCUITS.find(c => c.name === selectedCircuit);
|
|
722
|
+
if (!circuit) return;
|
|
723
|
+
|
|
724
|
+
const parser = new AcornParser();
|
|
725
|
+
const publicInputs = Object.entries(inputs)
|
|
726
|
+
.filter(([key]) => circuit.publicInputKeys.includes(key))
|
|
727
|
+
.map(([, val]) => Number(val));
|
|
728
|
+
const privateInputs = Object.entries(inputs)
|
|
729
|
+
.filter(([key]) => circuit.privateInputKeys.includes(key))
|
|
730
|
+
.map(([, val]) => Number(val));
|
|
731
|
+
|
|
732
|
+
const parsedCircuit = parser.parse(circuit.fn, publicInputs, privateInputs);
|
|
733
|
+
const result = generateNoir(parsedCircuit);
|
|
734
|
+
setNoirCode(result);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
setTranspileError((error as Error).message);
|
|
737
|
+
setNoirCode(null);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const debounce = setTimeout(transpileCode, 300);
|
|
742
|
+
return () => clearTimeout(debounce);
|
|
743
|
+
}, [selectedCircuit, inputs]);
|
|
744
|
+
|
|
745
|
+
// Generate proof
|
|
746
|
+
const handleGenerateProof = useCallback(async () => {
|
|
747
|
+
if (!noirCode) return;
|
|
748
|
+
|
|
749
|
+
setIsGenerating(true);
|
|
750
|
+
setProofError(null);
|
|
751
|
+
setProof(null);
|
|
752
|
+
setProofTime(null);
|
|
753
|
+
setLocalVerified(null);
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
await initBrowserWasm();
|
|
757
|
+
|
|
758
|
+
const startTime = performance.now();
|
|
759
|
+
|
|
760
|
+
const izi = await IziNoir.init({
|
|
761
|
+
provider: Provider.${capitalizeFirst(options.provider)},${solanaProviderConfig}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
await izi.compile(noirCode);
|
|
765
|
+
|
|
766
|
+
const proofResult = await izi.prove(inputs);
|
|
767
|
+
setIziInstance(izi);
|
|
768
|
+
|
|
769
|
+
// Get proof bytes
|
|
770
|
+
const proofBytes = 'bytes' in proofResult.proof
|
|
771
|
+
? proofResult.proof.bytes
|
|
772
|
+
: proofResult.proof;
|
|
773
|
+
const publicInputsHex = 'hex' in proofResult.publicInputs
|
|
774
|
+
? proofResult.publicInputs.hex
|
|
775
|
+
: proofResult.publicInputs;
|
|
776
|
+
|
|
777
|
+
// Local verification
|
|
778
|
+
const verified = await izi.verify(proofBytes, publicInputsHex);
|
|
779
|
+
setLocalVerified(verified);
|
|
780
|
+
|
|
781
|
+
const endTime = performance.now();
|
|
782
|
+
setProof(proofBytes);
|
|
783
|
+
setProofTime(Math.round(endTime - startTime));
|
|
784
|
+
} catch (error) {
|
|
785
|
+
console.error('Proof generation error:', error);
|
|
786
|
+
setProofError((error as Error).message);
|
|
787
|
+
} finally {
|
|
788
|
+
setIsGenerating(false);
|
|
789
|
+
}
|
|
790
|
+
}, [noirCode, inputs]);
|
|
791
|
+
${solanaHandlers}
|
|
792
|
+
|
|
793
|
+
return (
|
|
794
|
+
<div className="app">
|
|
795
|
+
<header>
|
|
796
|
+
<h1>${options.projectName}</h1>
|
|
797
|
+
<p>Zero-Knowledge Proof Demo</p>
|
|
798
|
+
</header>
|
|
799
|
+
|
|
800
|
+
<main>
|
|
801
|
+
{/* Circuit Selection */}
|
|
802
|
+
<div className="section">
|
|
803
|
+
<h2>1. Select Circuit</h2>
|
|
804
|
+
<select
|
|
805
|
+
value={selectedCircuit}
|
|
806
|
+
onChange={(e) => setSelectedCircuit(e.target.value)}
|
|
807
|
+
className="select"
|
|
808
|
+
>
|
|
809
|
+
{CIRCUITS.map((c) => (
|
|
810
|
+
<option key={c.name} value={c.name}>
|
|
811
|
+
{c.name}
|
|
812
|
+
</option>
|
|
813
|
+
))}
|
|
814
|
+
</select>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
817
|
+
{/* Circuit Code */}
|
|
818
|
+
<div className="section">
|
|
819
|
+
<h2>2. Circuit Code</h2>
|
|
820
|
+
<CodeBlock code={circuitCode} language="typescript" />
|
|
821
|
+
|
|
822
|
+
{noirCode && (
|
|
823
|
+
<details className="noir-details">
|
|
824
|
+
<summary>View Generated Noir</summary>
|
|
825
|
+
<CodeBlock code={noirCode} language="rust" />
|
|
826
|
+
</details>
|
|
827
|
+
)}
|
|
828
|
+
|
|
829
|
+
{transpileError && (
|
|
830
|
+
<p className="error">{transpileError}</p>
|
|
831
|
+
)}
|
|
832
|
+
</div>
|
|
833
|
+
|
|
834
|
+
{/* Inputs */}
|
|
835
|
+
<div className="section">
|
|
836
|
+
<h2>3. Inputs</h2>
|
|
837
|
+
<div className="inputs-grid">
|
|
838
|
+
{Object.entries(inputs).map(([key, value]) => {
|
|
839
|
+
const circuit = CIRCUITS.find(c => c.name === selectedCircuit);
|
|
840
|
+
const isPublic = circuit?.publicInputKeys.includes(key);
|
|
841
|
+
return (
|
|
842
|
+
<div key={key} className="input-group">
|
|
843
|
+
<label>
|
|
844
|
+
<span className={\`input-badge \${isPublic ? 'public' : 'private'}\`}>
|
|
845
|
+
{isPublic ? 'public' : 'private'}
|
|
846
|
+
</span>
|
|
847
|
+
{key}
|
|
848
|
+
</label>
|
|
849
|
+
<input
|
|
850
|
+
type="number"
|
|
851
|
+
value={value}
|
|
852
|
+
onChange={(e) => setInputs({ ...inputs, [key]: e.target.value })}
|
|
853
|
+
/>
|
|
854
|
+
</div>
|
|
855
|
+
);
|
|
856
|
+
})}
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
|
|
860
|
+
{/* Generate Proof */}
|
|
861
|
+
<div className="section">
|
|
862
|
+
<h2>4. Generate Proof</h2>
|
|
863
|
+
<button
|
|
864
|
+
onClick={handleGenerateProof}
|
|
865
|
+
disabled={isGenerating || !noirCode || !!transpileError}
|
|
866
|
+
className="btn btn-primary"
|
|
867
|
+
>
|
|
868
|
+
{isGenerating ? 'Generating...' : 'Generate Proof'}
|
|
869
|
+
</button>
|
|
870
|
+
|
|
871
|
+
{proofError && <p className="error">{proofError}</p>}
|
|
872
|
+
|
|
873
|
+
{proof && (
|
|
874
|
+
<div className="results">
|
|
875
|
+
<div className="result-card">
|
|
876
|
+
<span className="result-value">{proof.length} bytes</span>
|
|
877
|
+
<span className="result-label">Proof Size</span>
|
|
878
|
+
</div>
|
|
879
|
+
<div className="result-card">
|
|
880
|
+
<span className="result-value">{proofTime} ms</span>
|
|
881
|
+
<span className="result-label">Generation Time</span>
|
|
882
|
+
</div>
|
|
883
|
+
<div className="result-card">
|
|
884
|
+
<span className={\`result-value \${localVerified ? 'success' : 'error'}\`}>
|
|
885
|
+
{localVerified ? 'Yes' : 'No'}
|
|
886
|
+
</span>
|
|
887
|
+
<span className="result-label">Locally Verified</span>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
)}
|
|
891
|
+
</div>
|
|
892
|
+
${solanaDeploySection}
|
|
893
|
+
</main>
|
|
894
|
+
|
|
895
|
+
<footer>
|
|
896
|
+
<p>Built with <a href="https://github.com/izi-noir/izi-noir" target="_blank">IZI-NOIR</a></p>
|
|
897
|
+
</footer>
|
|
898
|
+
</div>
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export default App;
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
function getCircuitImports(template) {
|
|
906
|
+
switch (template) {
|
|
907
|
+
case "minimal":
|
|
908
|
+
return `import { myCircuit } from '../circuits';`;
|
|
909
|
+
case "balance-proof":
|
|
910
|
+
return `import { balanceProof } from '../circuits';`;
|
|
911
|
+
default:
|
|
912
|
+
return `import { balanceProof, ageProof } from '../circuits';`;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function getCircuitOptions(template) {
|
|
916
|
+
switch (template) {
|
|
917
|
+
case "minimal":
|
|
918
|
+
return `[
|
|
919
|
+
{
|
|
920
|
+
name: 'myCircuit',
|
|
921
|
+
fn: myCircuit,
|
|
922
|
+
publicInputKeys: ['publicInput'],
|
|
923
|
+
privateInputKeys: ['privateInput'],
|
|
924
|
+
defaultInputs: { publicInput: '42', privateInput: '42' },
|
|
925
|
+
},
|
|
926
|
+
]`;
|
|
927
|
+
case "balance-proof":
|
|
928
|
+
return `[
|
|
929
|
+
{
|
|
930
|
+
name: 'balanceProof',
|
|
931
|
+
fn: balanceProof,
|
|
932
|
+
publicInputKeys: ['threshold'],
|
|
933
|
+
privateInputKeys: ['balance'],
|
|
934
|
+
defaultInputs: { threshold: '100', balance: '1500' },
|
|
935
|
+
},
|
|
936
|
+
]`;
|
|
937
|
+
default:
|
|
938
|
+
return `[
|
|
939
|
+
{
|
|
940
|
+
name: 'balanceProof',
|
|
941
|
+
fn: balanceProof,
|
|
942
|
+
publicInputKeys: ['threshold'],
|
|
943
|
+
privateInputKeys: ['balance'],
|
|
944
|
+
defaultInputs: { threshold: '100', balance: '1500' },
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: 'ageProof',
|
|
948
|
+
fn: ageProof,
|
|
949
|
+
publicInputKeys: ['currentYear', 'minAge'],
|
|
950
|
+
privateInputKeys: ['birthYear'],
|
|
951
|
+
defaultInputs: { currentYear: '2024', minAge: '18', birthYear: '1990' },
|
|
952
|
+
},
|
|
953
|
+
]`;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function capitalizeFirst(str) {
|
|
957
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
958
|
+
}
|
|
959
|
+
function generateAppCss() {
|
|
960
|
+
return `* {
|
|
961
|
+
box-sizing: border-box;
|
|
962
|
+
margin: 0;
|
|
963
|
+
padding: 0;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
:root {
|
|
967
|
+
--purple: #9945FF;
|
|
968
|
+
--green: #14F195;
|
|
969
|
+
--bg: #0a0a0a;
|
|
970
|
+
--bg-elevated: #111111;
|
|
971
|
+
--border: #222222;
|
|
972
|
+
--text: #ffffff;
|
|
973
|
+
--text-muted: #888888;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
body {
|
|
977
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
978
|
+
background: var(--bg);
|
|
979
|
+
color: var(--text);
|
|
980
|
+
line-height: 1.6;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.app {
|
|
984
|
+
min-height: 100vh;
|
|
985
|
+
display: flex;
|
|
986
|
+
flex-direction: column;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
header {
|
|
990
|
+
padding: 2rem;
|
|
991
|
+
text-align: center;
|
|
992
|
+
border-bottom: 1px solid var(--border);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
header h1 {
|
|
996
|
+
font-size: 2rem;
|
|
997
|
+
background: linear-gradient(90deg, var(--purple), var(--green));
|
|
998
|
+
-webkit-background-clip: text;
|
|
999
|
+
-webkit-text-fill-color: transparent;
|
|
1000
|
+
background-clip: text;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
header p {
|
|
1004
|
+
color: var(--text-muted);
|
|
1005
|
+
margin-top: 0.5rem;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
main {
|
|
1009
|
+
flex: 1;
|
|
1010
|
+
max-width: 800px;
|
|
1011
|
+
margin: 0 auto;
|
|
1012
|
+
padding: 2rem;
|
|
1013
|
+
width: 100%;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.section {
|
|
1017
|
+
margin-bottom: 2rem;
|
|
1018
|
+
padding: 1.5rem;
|
|
1019
|
+
background: var(--bg-elevated);
|
|
1020
|
+
border: 1px solid var(--border);
|
|
1021
|
+
border-radius: 12px;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.section h2 {
|
|
1025
|
+
font-size: 1.25rem;
|
|
1026
|
+
margin-bottom: 1rem;
|
|
1027
|
+
color: var(--text);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.select {
|
|
1031
|
+
width: 100%;
|
|
1032
|
+
padding: 0.75rem;
|
|
1033
|
+
background: var(--bg);
|
|
1034
|
+
border: 1px solid var(--border);
|
|
1035
|
+
border-radius: 8px;
|
|
1036
|
+
color: var(--text);
|
|
1037
|
+
font-size: 1rem;
|
|
1038
|
+
cursor: pointer;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.select:focus {
|
|
1042
|
+
outline: none;
|
|
1043
|
+
border-color: var(--purple);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
.inputs-grid {
|
|
1047
|
+
display: grid;
|
|
1048
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1049
|
+
gap: 1rem;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
.input-group {
|
|
1053
|
+
display: flex;
|
|
1054
|
+
flex-direction: column;
|
|
1055
|
+
gap: 0.5rem;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.input-group label {
|
|
1059
|
+
display: flex;
|
|
1060
|
+
align-items: center;
|
|
1061
|
+
gap: 0.5rem;
|
|
1062
|
+
font-size: 0.875rem;
|
|
1063
|
+
color: var(--text-muted);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.input-badge {
|
|
1067
|
+
font-size: 0.625rem;
|
|
1068
|
+
padding: 0.125rem 0.375rem;
|
|
1069
|
+
border-radius: 4px;
|
|
1070
|
+
text-transform: uppercase;
|
|
1071
|
+
font-weight: 600;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.input-badge.public {
|
|
1075
|
+
background: var(--green);
|
|
1076
|
+
color: #000;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.input-badge.private {
|
|
1080
|
+
background: var(--purple);
|
|
1081
|
+
color: #fff;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.input-group input {
|
|
1085
|
+
padding: 0.75rem;
|
|
1086
|
+
background: var(--bg);
|
|
1087
|
+
border: 1px solid var(--border);
|
|
1088
|
+
border-radius: 8px;
|
|
1089
|
+
color: var(--text);
|
|
1090
|
+
font-size: 1rem;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.input-group input:focus {
|
|
1094
|
+
outline: none;
|
|
1095
|
+
border-color: var(--purple);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.btn {
|
|
1099
|
+
padding: 0.75rem 1.5rem;
|
|
1100
|
+
font-size: 1rem;
|
|
1101
|
+
font-weight: 600;
|
|
1102
|
+
border: none;
|
|
1103
|
+
border-radius: 8px;
|
|
1104
|
+
cursor: pointer;
|
|
1105
|
+
transition: all 0.2s;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.btn:disabled {
|
|
1109
|
+
opacity: 0.5;
|
|
1110
|
+
cursor: not-allowed;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.btn-primary {
|
|
1114
|
+
background: linear-gradient(90deg, var(--purple), var(--green));
|
|
1115
|
+
color: white;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
.btn-primary:hover:not(:disabled) {
|
|
1119
|
+
transform: translateY(-2px);
|
|
1120
|
+
box-shadow: 0 4px 20px rgba(153, 69, 255, 0.3);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.btn-secondary {
|
|
1124
|
+
background: var(--bg);
|
|
1125
|
+
border: 1px solid var(--border);
|
|
1126
|
+
color: var(--text);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.btn-secondary:hover:not(:disabled) {
|
|
1130
|
+
border-color: var(--purple);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.results {
|
|
1134
|
+
display: grid;
|
|
1135
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1136
|
+
gap: 1rem;
|
|
1137
|
+
margin-top: 1rem;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.result-card {
|
|
1141
|
+
padding: 1rem;
|
|
1142
|
+
background: var(--bg);
|
|
1143
|
+
border: 1px solid var(--border);
|
|
1144
|
+
border-radius: 8px;
|
|
1145
|
+
text-align: center;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
.result-value {
|
|
1149
|
+
font-size: 1.25rem;
|
|
1150
|
+
font-weight: 600;
|
|
1151
|
+
display: block;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
.result-value.success {
|
|
1155
|
+
color: var(--green);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.result-value.error {
|
|
1159
|
+
color: #ff4444;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.result-label {
|
|
1163
|
+
font-size: 0.75rem;
|
|
1164
|
+
color: var(--text-muted);
|
|
1165
|
+
text-transform: uppercase;
|
|
1166
|
+
margin-top: 0.25rem;
|
|
1167
|
+
display: block;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.error {
|
|
1171
|
+
color: #ff4444;
|
|
1172
|
+
font-size: 0.875rem;
|
|
1173
|
+
margin-top: 0.5rem;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.success {
|
|
1177
|
+
color: var(--green);
|
|
1178
|
+
font-size: 0.875rem;
|
|
1179
|
+
margin-top: 0.5rem;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.noir-details {
|
|
1183
|
+
margin-top: 1rem;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
.noir-details summary {
|
|
1187
|
+
cursor: pointer;
|
|
1188
|
+
color: var(--text-muted);
|
|
1189
|
+
font-size: 0.875rem;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.noir-details summary:hover {
|
|
1193
|
+
color: var(--text);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.deploy-verify-row {
|
|
1197
|
+
display: grid;
|
|
1198
|
+
grid-template-columns: 1fr 1fr;
|
|
1199
|
+
gap: 1rem;
|
|
1200
|
+
margin-top: 1rem;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
.deploy-box,
|
|
1204
|
+
.verify-box {
|
|
1205
|
+
padding: 1rem;
|
|
1206
|
+
background: var(--bg);
|
|
1207
|
+
border: 1px solid var(--border);
|
|
1208
|
+
border-radius: 8px;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
footer {
|
|
1212
|
+
padding: 1.5rem;
|
|
1213
|
+
text-align: center;
|
|
1214
|
+
border-top: 1px solid var(--border);
|
|
1215
|
+
color: var(--text-muted);
|
|
1216
|
+
font-size: 0.875rem;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
footer a {
|
|
1220
|
+
color: var(--purple);
|
|
1221
|
+
text-decoration: none;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
footer a:hover {
|
|
1225
|
+
text-decoration: underline;
|
|
1226
|
+
}
|
|
1227
|
+
`;
|
|
1228
|
+
}
|
|
1229
|
+
function generateIndexCss() {
|
|
1230
|
+
return `body {
|
|
1231
|
+
margin: 0;
|
|
1232
|
+
-webkit-font-smoothing: antialiased;
|
|
1233
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1234
|
+
}
|
|
1235
|
+
`;
|
|
1236
|
+
}
|
|
1237
|
+
function generateViteEnvDts() {
|
|
1238
|
+
return `/// <reference types="vite/client" />
|
|
1239
|
+
`;
|
|
1240
|
+
}
|
|
1241
|
+
function generateViteSvg() {
|
|
1242
|
+
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>
|
|
1243
|
+
`;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// src/generators/components.ts
|
|
1247
|
+
function generateCodeBlock() {
|
|
1248
|
+
return `import { Highlight, themes } from 'prism-react-renderer';
|
|
1249
|
+
|
|
1250
|
+
interface CodeBlockProps {
|
|
1251
|
+
code: string;
|
|
1252
|
+
language?: 'typescript' | 'javascript' | 'rust';
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
export function CodeBlock({ code, language = 'typescript' }: CodeBlockProps) {
|
|
1256
|
+
return (
|
|
1257
|
+
<Highlight theme={themes.nightOwl} code={code.trim()} language={language}>
|
|
1258
|
+
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
|
1259
|
+
<pre
|
|
1260
|
+
className={className}
|
|
1261
|
+
style={{
|
|
1262
|
+
...style,
|
|
1263
|
+
padding: '1rem',
|
|
1264
|
+
borderRadius: '8px',
|
|
1265
|
+
overflow: 'auto',
|
|
1266
|
+
fontSize: '0.875rem',
|
|
1267
|
+
lineHeight: '1.5',
|
|
1268
|
+
}}
|
|
1269
|
+
>
|
|
1270
|
+
{tokens.map((line, i) => (
|
|
1271
|
+
<div key={i} {...getLineProps({ line })}>
|
|
1272
|
+
<span style={{ color: '#666', marginRight: '1rem', userSelect: 'none' }}>
|
|
1273
|
+
{String(i + 1).padStart(2, ' ')}
|
|
1274
|
+
</span>
|
|
1275
|
+
{line.map((token, key) => (
|
|
1276
|
+
<span key={key} {...getTokenProps({ token })} />
|
|
1277
|
+
))}
|
|
1278
|
+
</div>
|
|
1279
|
+
))}
|
|
1280
|
+
</pre>
|
|
1281
|
+
)}
|
|
1282
|
+
</Highlight>
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
`;
|
|
1286
|
+
}
|
|
1287
|
+
function generateEditableCodeBlock() {
|
|
1288
|
+
return `import { useRef, useEffect, useState } from 'react';
|
|
1289
|
+
import { Highlight, themes } from 'prism-react-renderer';
|
|
1290
|
+
|
|
1291
|
+
interface EditableCodeBlockProps {
|
|
1292
|
+
code: string;
|
|
1293
|
+
onChange: (code: string) => void;
|
|
1294
|
+
language?: 'typescript' | 'javascript';
|
|
1295
|
+
rows?: number;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
export function EditableCodeBlock({
|
|
1299
|
+
code,
|
|
1300
|
+
onChange,
|
|
1301
|
+
language = 'typescript',
|
|
1302
|
+
rows = 8,
|
|
1303
|
+
}: EditableCodeBlockProps) {
|
|
1304
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
1305
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
1306
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
1307
|
+
|
|
1308
|
+
useEffect(() => {
|
|
1309
|
+
if (preRef.current) {
|
|
1310
|
+
preRef.current.scrollTop = scrollTop;
|
|
1311
|
+
}
|
|
1312
|
+
}, [scrollTop]);
|
|
1313
|
+
|
|
1314
|
+
const handleScroll = () => {
|
|
1315
|
+
if (textareaRef.current) {
|
|
1316
|
+
setScrollTop(textareaRef.current.scrollTop);
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
const lineHeight = 1.5;
|
|
1321
|
+
const padding = 16;
|
|
1322
|
+
const minHeight = rows * 14 * lineHeight + padding * 2;
|
|
1323
|
+
|
|
1324
|
+
return (
|
|
1325
|
+
<div style={{ position: 'relative', minHeight }}>
|
|
1326
|
+
<Highlight theme={themes.nightOwl} code={code} language={language}>
|
|
1327
|
+
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
|
1328
|
+
<pre
|
|
1329
|
+
ref={preRef}
|
|
1330
|
+
className={className}
|
|
1331
|
+
style={{
|
|
1332
|
+
...style,
|
|
1333
|
+
position: 'absolute',
|
|
1334
|
+
top: 0,
|
|
1335
|
+
left: 0,
|
|
1336
|
+
right: 0,
|
|
1337
|
+
bottom: 0,
|
|
1338
|
+
padding: '1rem',
|
|
1339
|
+
borderRadius: '8px',
|
|
1340
|
+
overflow: 'auto',
|
|
1341
|
+
fontSize: '0.875rem',
|
|
1342
|
+
lineHeight: '1.5',
|
|
1343
|
+
margin: 0,
|
|
1344
|
+
pointerEvents: 'none',
|
|
1345
|
+
}}
|
|
1346
|
+
>
|
|
1347
|
+
{tokens.map((line, i) => (
|
|
1348
|
+
<div key={i} {...getLineProps({ line })}>
|
|
1349
|
+
{line.map((token, key) => (
|
|
1350
|
+
<span key={key} {...getTokenProps({ token })} />
|
|
1351
|
+
))}
|
|
1352
|
+
</div>
|
|
1353
|
+
))}
|
|
1354
|
+
</pre>
|
|
1355
|
+
)}
|
|
1356
|
+
</Highlight>
|
|
1357
|
+
|
|
1358
|
+
<textarea
|
|
1359
|
+
ref={textareaRef}
|
|
1360
|
+
value={code}
|
|
1361
|
+
onChange={(e) => onChange(e.target.value)}
|
|
1362
|
+
onScroll={handleScroll}
|
|
1363
|
+
spellCheck={false}
|
|
1364
|
+
style={{
|
|
1365
|
+
position: 'absolute',
|
|
1366
|
+
top: 0,
|
|
1367
|
+
left: 0,
|
|
1368
|
+
right: 0,
|
|
1369
|
+
bottom: 0,
|
|
1370
|
+
width: '100%',
|
|
1371
|
+
height: '100%',
|
|
1372
|
+
minHeight,
|
|
1373
|
+
padding: '1rem',
|
|
1374
|
+
background: 'transparent',
|
|
1375
|
+
border: 'none',
|
|
1376
|
+
outline: 'none',
|
|
1377
|
+
resize: 'none',
|
|
1378
|
+
fontFamily: 'monospace',
|
|
1379
|
+
fontSize: '0.875rem',
|
|
1380
|
+
lineHeight: '1.5',
|
|
1381
|
+
color: 'transparent',
|
|
1382
|
+
caretColor: 'white',
|
|
1383
|
+
WebkitTextFillColor: 'transparent',
|
|
1384
|
+
}}
|
|
1385
|
+
/>
|
|
1386
|
+
</div>
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
`;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
540
1392
|
// src/commands/init.ts
|
|
541
1393
|
async function initCommand(projectName, options) {
|
|
542
1394
|
let projectOptions;
|
|
@@ -608,15 +1460,30 @@ Error: Directory "${projectOptions.projectName}" already exists and is not empty
|
|
|
608
1460
|
}
|
|
609
1461
|
async function createProjectStructure(projectDir, options) {
|
|
610
1462
|
await ensureDir(path2.join(projectDir, "circuits"));
|
|
611
|
-
await ensureDir(path2.join(projectDir, "
|
|
612
|
-
await ensureDir(path2.join(projectDir, "
|
|
1463
|
+
await ensureDir(path2.join(projectDir, "src"));
|
|
1464
|
+
await ensureDir(path2.join(projectDir, "src", "components"));
|
|
1465
|
+
await ensureDir(path2.join(projectDir, "src", "lib"));
|
|
1466
|
+
await ensureDir(path2.join(projectDir, "public"));
|
|
613
1467
|
const files = [
|
|
1468
|
+
// Root config files
|
|
614
1469
|
["package.json", generatePackageJson(options)],
|
|
615
1470
|
["tsconfig.json", generateTsconfig()],
|
|
616
|
-
["
|
|
1471
|
+
["tsconfig.node.json", generateTsconfigNode()],
|
|
1472
|
+
["vite.config.ts", generateViteConfig()],
|
|
1473
|
+
["index.html", generateIndexHtml(options)],
|
|
617
1474
|
["README.md", generateReadme(options)],
|
|
618
1475
|
[".gitignore", generateGitignore()],
|
|
619
|
-
|
|
1476
|
+
// Source files
|
|
1477
|
+
["src/main.tsx", generateMainTsx(options)],
|
|
1478
|
+
["src/App.tsx", generateAppTsx(options)],
|
|
1479
|
+
["src/App.css", generateAppCss()],
|
|
1480
|
+
["src/index.css", generateIndexCss()],
|
|
1481
|
+
["src/vite-env.d.ts", generateViteEnvDts()],
|
|
1482
|
+
// Components
|
|
1483
|
+
["src/components/CodeBlock.tsx", generateCodeBlock()],
|
|
1484
|
+
["src/components/EditableCodeBlock.tsx", generateEditableCodeBlock()],
|
|
1485
|
+
// Public assets
|
|
1486
|
+
["public/vite.svg", generateViteSvg()]
|
|
620
1487
|
];
|
|
621
1488
|
switch (options.template) {
|
|
622
1489
|
case "minimal":
|
|
@@ -632,10 +1499,6 @@ async function createProjectStructure(projectDir, options) {
|
|
|
632
1499
|
}
|
|
633
1500
|
files.push(["circuits/index.ts", generateCircuitsIndex(options.template)]);
|
|
634
1501
|
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
1502
|
await Promise.all(
|
|
640
1503
|
files.map(
|
|
641
1504
|
([relativePath, content]) => writeFile(path2.join(projectDir, relativePath), content)
|
|
@@ -652,14 +1515,15 @@ function printSuccessMessage(options) {
|
|
|
652
1515
|
if (options.skipInstall) {
|
|
653
1516
|
console.log(pc3.cyan(" npm install"));
|
|
654
1517
|
}
|
|
655
|
-
console.log(pc3.cyan(" npm run
|
|
656
|
-
console.log(
|
|
1518
|
+
console.log(pc3.cyan(" npm run dev"));
|
|
1519
|
+
console.log();
|
|
1520
|
+
console.log("Then open " + pc3.blue("http://localhost:5173") + " in your browser.");
|
|
657
1521
|
console.log();
|
|
658
|
-
console.log("To
|
|
1522
|
+
console.log("To add circuits:");
|
|
659
1523
|
console.log();
|
|
660
|
-
console.log(pc3.dim(" 1.
|
|
661
|
-
console.log(pc3.dim(
|
|
662
|
-
console.log(pc3.dim(
|
|
1524
|
+
console.log(pc3.dim(" 1. Create a new circuit in circuits/*.ts"));
|
|
1525
|
+
console.log(pc3.dim(" 2. Export it from circuits/index.ts"));
|
|
1526
|
+
console.log(pc3.dim(" 3. Add it to CIRCUITS array in src/App.tsx"));
|
|
663
1527
|
console.log();
|
|
664
1528
|
console.log(
|
|
665
1529
|
pc3.dim("Learn more: ") + pc3.blue("https://github.com/izi-noir/izi-noir")
|