Skip to content

Commit 28ee5f6

Browse files
authored
Frontend Circuit Cache (#730)
1 parent c1f010b commit 28ee5f6

File tree

10 files changed

+192
-13
lines changed

10 files changed

+192
-13
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1717

1818
## Unreleased
1919

20+
## [0.22.8](https://github.com/o1-labs/zkapp-cli/compare/v0.22.7...v0.22.8) - 2025-05-8
21+
22+
### Added
23+
24+
- Adds a frontend circuit cache to the default NextJS UI project generated with the zkapp-CLI. [#730](https://github.com/o1-labs/zkapp-cli/pull/730)
25+
2026
## [0.22.7](https://github.com/o1-labs/zkapp-cli/compare/v0.22.6...v0.22.7) - 2025-04-13
2127

2228
### Changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zkapp-cli",
3-
"version": "0.22.7",
3+
"version": "0.22.8",
44
"description": "CLI to create zkApps (zero-knowledge apps) for Mina Protocol",
55
"homepage": "https://github.com/o1-labs/zkapp-cli/",
66
"repository": {

src/lib/project.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,19 @@ async function project({ name, ui }) {
155155
await shellExec('npm run build --silent');
156156
});
157157

158-
if (ui) shell.cd('..'); // back to project root
158+
// Generates a circuit cache in contracts folder and copies a chachelist to UI
159+
if (ui === 'next') {
160+
await step('Generate circuit cache for UI', async () => {
161+
await shellExec('npx tsx scripts/generate-cache.ts');
162+
});
163+
164+
// Copy the circuit cache to ui/public/cache and the cachelist to ui/app
165+
await step('Copy circuit cachelist to UI', async () => {
166+
await shellExec('npx tsx scripts/copy-cache-to-ui.ts');
167+
});
168+
} else if (ui) {
169+
shell.cd('..'); // Move back to project root for other UI types
170+
}
159171

160172
await step('Git init commit', async () => {
161173
await shellExec(
@@ -488,6 +500,8 @@ const __dirname = path.dirname(__filename);
488500
let x = fs.readJsonSync(path.join('ui', 'package.json'));
489501
x.scripts['ts-watch'] = 'tsc --noEmit --incremental --watch';
490502
x.scripts['build'] = 'next build --no-lint';
503+
x.scripts['clear-cache'] =
504+
'npx rimraf public/cache && npx rimraf app/cache.json && echo "UI Cache cleared successfully!"';
491505
x.type = 'module';
492506
fs.writeJSONSync(path.join('ui', 'package.json'), x, { spaces: 2 });
493507

src/lib/project.test.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ describe('project.js', () => {
226226
message: expect.any(Function),
227227
prefix: expect.any(Function),
228228
});
229-
checkUiProjectSetup(shell.exec.mock.calls);
229+
checkUiProjectSetup(shell.exec.mock.calls, false, true);
230230
checkIfProjectSetupSuccessful();
231231
});
232232

@@ -505,7 +505,7 @@ describe('project.js', () => {
505505

506506
await project({ name: 'test-project', ui: 'next' });
507507

508-
checkUiProjectSetup(shell.exec.mock.calls, true);
508+
checkUiProjectSetup(shell.exec.mock.calls, true, true);
509509
} finally {
510510
Object.defineProperty(process, 'platform', {
511511
value: originalPlatform,
@@ -784,7 +784,11 @@ function checkProjectSetupNoUi(shellExecCalls) {
784784
);
785785
}
786786

787-
function checkUiProjectSetup(shellExecCalls, isWindows = false) {
787+
function checkUiProjectSetup(
788+
shellExecCalls,
789+
isWindows = false,
790+
isNext = false
791+
) {
788792
expect(shellExecCalls[0][0]).toBe(
789793
'npm install --silent > ' + (isWindows ? 'NUL' : '"/dev/null" 2>&1')
790794
);
@@ -793,9 +797,18 @@ function checkUiProjectSetup(shellExecCalls, isWindows = false) {
793797
'npm install --silent > ' + (isWindows ? 'NUL' : '"/dev/null" 2>&1')
794798
);
795799
expect(shellExecCalls[3][0]).toBe('npm run build --silent');
796-
expect(shellExecCalls[4][0]).toBe(
797-
'git add . && git commit -m "Init commit" -q -n && git branch -m main'
798-
);
800+
801+
if (isNext) {
802+
expect(shellExecCalls[4][0]).toBe('npx tsx scripts/generate-cache.ts');
803+
expect(shellExecCalls[5][0]).toBe('npx tsx scripts/copy-cache-to-ui.ts');
804+
expect(shellExecCalls[6][0]).toBe(
805+
'git add . && git commit -m "Init commit" -q -n && git branch -m main'
806+
);
807+
} else {
808+
expect(shellExecCalls[4][0]).toBe(
809+
'git add . && git commit -m "Init commit" -q -n && git branch -m main'
810+
);
811+
}
799812
}
800813

801814
function checkIfProjectSetupSuccessful() {

src/lib/ui/next/customNextPage.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import GradientBG from '../components/GradientBG.js';
66
import styles from '../styles/Home.module.css';
77
import heroMinaLogo from '../public/assets/hero-mina-logo.svg';
88
import arrowRightSmall from '../public/assets/arrow-right-small.svg';
9-
import {fetchAccount, Mina, PublicKey, Field, Proof} from "o1js";
9+
import {fetchAccount, Mina, PublicKey, Field, Proof, Cache} from "o1js";
1010
import { Add, AddZkProgram } from "../../contracts";
11+
import cacheJSONList from "./cache.json";
1112
1213
// We've already deployed the Add contract on testnet at this address
1314
// https://minascan.io/devnet/account/B62qnfpb1Wz7DrW7279B8nR8m4yY6wGJz4dnbAdkzfeUkpyp8aB9VCp
@@ -33,7 +34,9 @@ export default function Home() {
3334
setZkProgramState(num.toString());
3435
3536
// Compile the AddZkProgram
37+
const cacheFiles = await fetchFiles();
3638
console.log("Compiling AddZkProgram");
39+
// ZkProgram cache in the browser is currently not fully supported.
3740
await AddZkProgram.compile();
3841
3942
// Initialize the AddZkProgram with the initial state of the zkapp
@@ -44,7 +47,7 @@ export default function Home() {
4447
4548
// Compile the contract so that o1js has the proving key required to execute contract calls
4649
console.log("Compiling Add contract to generate proving and verification keys");
47-
await Add.compile();
50+
await Add.compile({ cache: FileSystem(cacheFiles) });
4851
4952
setLoading(false);
5053
})();
@@ -119,6 +122,51 @@ export default function Home() {
119122
setLoading(false);
120123
}, [proof]);
121124
125+
const fetchFiles = async () => {
126+
const cacheJson = cacheJSONList;
127+
const cacheListPromises = cacheJson.files.map(async (file) => {
128+
const [header, data] = await Promise.all([
129+
fetch(\`/cache/\${file}.header\`).then((res) => res.text()),
130+
fetch(\`/cache/\${file}\`).then((res) => res.text()),
131+
]);
132+
return { file, header, data };
133+
});
134+
135+
const cacheList = await Promise.all(cacheListPromises);
136+
137+
return cacheList.reduce((acc: any, { file, header, data }) => {
138+
acc[file] = { file, header, data };
139+
return acc;
140+
}, {});
141+
};
142+
143+
const FileSystem = (files: any): Cache => ({
144+
read({ persistentId, uniqueId, dataType }: any) {
145+
if (!files[persistentId]) {
146+
return undefined;
147+
}
148+
149+
const currentId = files[persistentId].header;
150+
151+
if (currentId !== uniqueId) {
152+
return undefined;
153+
}
154+
155+
if (dataType === "string") {
156+
console.log("found in cache:", { persistentId, uniqueId, dataType });
157+
158+
return new TextEncoder().encode(files[persistentId].data);
159+
}
160+
return undefined;
161+
},
162+
163+
write({ persistentId, uniqueId, dataType }: any, data: any) {
164+
console.log({ persistentId, uniqueId, dataType });
165+
},
166+
167+
canWrite: true
168+
});
169+
122170
return (
123171
<>
124172
<Head>

templates/project-ts/cache/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The generated cache files will be added to this directory

templates/project-ts/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
"format": "prettier --write --ignore-unknown **/*",
2121
"test": "npm run build && find build/src -name '*.test.js' -exec node --test {} \\;",
2222
"testw": "npm run build && find build/src -name '*.test.js' -exec node --test --watch {} \\;",
23-
"lint": "npx eslint src/* --fix"
23+
"lint": "npx eslint src/* --fix",
24+
"clear-cache": "npx rimraf cache/* !cache/README.md && npx rimraf cache.json && echo \"Cache cleared successfully!\""
2425
},
2526
"devDependencies": {
2627
"@babel/preset-env": "^7.16.4",
2728
"@babel/preset-typescript": "^7.16.0",
29+
"@types/node": "^22.14.1",
2830
"@typescript-eslint/eslint-plugin": "^5.5.0",
2931
"@typescript-eslint/parser": "^5.5.0",
3032
"eslint": "^8.7.0",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import fs from 'fs/promises';
2+
import path from 'path';
3+
4+
async function copyCacheToUI() {
5+
try {
6+
// Define paths
7+
const cacheDir = path.join('.', 'cache');
8+
const uiPublicCacheDir = path.join('..', 'ui', 'public', 'cache');
9+
const cacheJsonSource = path.join('cache.json');
10+
const cacheJsonDest = path.join('..', 'ui', 'app', 'cache.json');
11+
12+
// Create UI cache directory if it doesn't exist
13+
try {
14+
await fs.mkdir(uiPublicCacheDir, { recursive: true });
15+
} catch (err: any) {
16+
if (err.code !== 'EEXIST') {
17+
throw err;
18+
}
19+
}
20+
21+
// Read files from cache directory
22+
const files = await fs.readdir(cacheDir);
23+
24+
// Copy each file except README.md
25+
for (const file of files) {
26+
if (file !== 'README.md') {
27+
const sourceFile = path.join(cacheDir, file);
28+
const destFile = path.join(uiPublicCacheDir, file);
29+
30+
const data = await fs.readFile(sourceFile);
31+
await fs.writeFile(destFile, data);
32+
}
33+
}
34+
35+
// Copy cache.json to UI app directory
36+
try {
37+
const cacheJsonData = await fs.readFile(cacheJsonSource);
38+
await fs.writeFile(cacheJsonDest, cacheJsonData);
39+
} catch (err: any) {
40+
if (err.code === 'ENOENT') {
41+
console.log('cache.json not found, skipping');
42+
} else {
43+
throw err;
44+
}
45+
}
46+
47+
console.log('Cache files copied to UI successfully');
48+
} catch (error) {
49+
console.error('Error copying cache files:', error);
50+
process.exit(1);
51+
}
52+
}
53+
54+
await copyCacheToUI();
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Cache } from 'o1js';
2+
import fs from 'fs/promises';
3+
4+
// @ts-expect-error - These imports will resolve in the generated project after the contract is built. Remove these comments in your project.
5+
const { Add } = await import('../build/src/Add.js');
6+
// @ts-expect-error - These imports will resolve in the generated project after the ZkProgram is built. Remove these comments in your project.
7+
const { AddZkProgram } = await import('../build/src/AddZkProgram.js');
8+
9+
const cache_directory = 'cache';
10+
11+
// Create a file system cache instance pointing to our cache directory
12+
// This allows o1js to store and retrieve compiled circuit artifacts
13+
const cache: Cache = Cache.FileSystem(cache_directory);
14+
15+
// ZkProgram cache in the browser is currently not fully supported.
16+
await AddZkProgram.compile();
17+
// Compile the smart contract with the cache enabled
18+
await Add.compile({ cache });
19+
20+
type CacheList = {
21+
files: string[];
22+
};
23+
const cacheObj: CacheList = {
24+
files: [],
25+
};
26+
27+
const files = await fs.readdir(cache_directory);
28+
for (const fileName of files) {
29+
if (!fileName.endsWith('.header')) {
30+
cacheObj['files'].push(fileName);
31+
}
32+
}
33+
34+
const jsonCacheFile = `cache.json`;
35+
36+
try {
37+
await fs.writeFile(jsonCacheFile, JSON.stringify(cacheObj, null, 2));
38+
console.log('JSON cached object successfully saved ');
39+
} catch (error) {
40+
console.error('Error writing JSON file:', error);
41+
}

0 commit comments

Comments
 (0)