Skip to content

Commit e95092d

Browse files
committed
feat: Enhance UI, add TS verifier script generation & backend refactor
1 parent dbb949c commit e95092d

File tree

3 files changed

+127
-79
lines changed

3 files changed

+127
-79
lines changed

apps/noir-compiler/src/app/components/container.tsx

Lines changed: 82 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { compileNoirCircuit } from '../actions'
88
export function Container () {
99
const noirApp = useContext(NoirAppContext)
1010

11+
const projectRoot = noirApp.appState.filePath.substring(0, noirApp.appState.filePath.lastIndexOf('/src/'))
12+
const buildPath = projectRoot === '' ? 'build' : `${projectRoot}/build`
13+
const contractsPath = projectRoot === '' ? 'contracts' : `${projectRoot}/contracts`
14+
const scriptsPath = projectRoot === '' ? 'scripts' : `${projectRoot}/scripts`
15+
const proverTomlPath = projectRoot === '' ? 'Prover.toml' : `${projectRoot}/Prover.toml`
16+
1117
const showCompilerLicense = async (message = 'License not available') => {
1218
try {
1319
const response = await fetch('https://raw.githubusercontent.com/noir-lang/noir/master/LICENSE-APACHE')
@@ -45,13 +51,12 @@ export function Container () {
4551
noirApp.plugin.generateProof(noirApp.appState.filePath)
4652
}
4753

48-
const handleViewProgramArtefact = (e: React.MouseEvent<HTMLAnchorElement>) => {
54+
const handleViewFile = (e: React.MouseEvent<HTMLButtonElement>, filePath: string) => {
4955
e.preventDefault()
50-
const projectRoot = noirApp.appState.filePath.substring(0, noirApp.appState.filePath.lastIndexOf('/src/'))
51-
const buildPath = projectRoot === '' ? 'build' : `${projectRoot}/build`
52-
noirApp.plugin.call('fileManager', 'open', 'build/program.json')
56+
noirApp.plugin.call('fileManager', 'open', filePath)
5357
}
5458

59+
5560
const formattedPublicInputsString = JSON.stringify(noirApp.appState.formattedPublicInputs, null, 2)
5661

5762
return (
@@ -70,75 +75,94 @@ export function Container () {
7075
>
7176
<span className="far fa-file-certificate border-0 p-0 ms-2" onClick={() => showCompilerLicense()}></span>
7277
</CustomTooltip>
73-
<CompileOptions setCircuitAutoCompile={handleCircuitAutoCompile} setCircuitHideWarnings={handleCircuitHideWarnings} autoCompile={noirApp.appState.autoCompile} hideWarnings={noirApp.appState.hideWarnings} />
74-
<div className="pb-2">
78+
{/* <CompileOptions setCircuitAutoCompile={handleCircuitAutoCompile} setCircuitHideWarnings={handleCircuitHideWarnings} autoCompile={noirApp.appState.autoCompile} hideWarnings={noirApp.appState.hideWarnings} /> */}
79+
<hr></hr>
80+
<div>
7581
<CompileBtn id="noir" plugin={noirApp.plugin} appState={noirApp.appState} compileAction={handleCompileClick} />
7682
</div>
77-
<RenderIf condition={noirApp.appState.status !== 'compiling'}>
78-
<CompilerFeedback feedback={noirApp.appState.compilerFeedback} filePathToId={noirApp.appState.filePathToId} openErrorLocation={handleOpenErrorLocation} hideWarnings={noirApp.appState.hideWarnings} askGPT={askGPT} />
79-
</RenderIf>
8083
<RenderIf condition={noirApp.appState.status === 'succeed'}>
8184
<>
82-
<div className='mt-2'>
83-
<a data-id="view-noir-compilation-result" className="cursor-pointer text-decoration-none" href='#' onClick={handleViewProgramArtefact}>
84-
<i className="text-success mt-1 px-1 fas fa-check"></i> View compiled noir program artefact.
85-
</a>
86-
</div>
87-
88-
<div className="mt-2">
89-
<button
90-
id="noir_generate_proof"
91-
className="btn btn-primary w-100"
92-
onClick={handleGenerateProofClick}
93-
disabled={noirApp.appState.proofingStatus === 'proofing' || noirApp.appState.status === 'compiling'}
94-
>
95-
{noirApp.appState.proofingStatus === 'proofing' ? (
96-
<>
97-
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
98-
<span className="ms-1">Generating Proof...</span>
99-
</>
100-
) : (
101-
<FormattedMessage id="noir.generateProof" defaultMessage="Generate Proof" />
102-
)}
85+
<label className="noir_label form-check-label mt-3">
86+
<FormattedMessage id="noir.compilationArtifacts" defaultMessage="Compilation Artifacts" />
87+
</label>
88+
<button className="btn btn-sm btn-outline-info w-100 text-start mt-2" onClick={(e) => handleViewFile(e, `${buildPath}/program.json`)}>
89+
<div className="d-flex align-items-center">
90+
<i className="fas fa-file-invoice me-2"></i>
91+
<span>View Artifact</span>
92+
</div>
10393
</button>
104-
</div>
94+
<hr></hr>
95+
<div>
96+
<CustomTooltip
97+
placement="bottom-start"
98+
tooltipId="generateProofTooltip"
99+
tooltipClasses="text-nowrap"
100+
tooltipText='If your circuit has public inputs, edit Prover.toml before generating the proof.'
101+
>
102+
<button
103+
id="noir_generate_proof"
104+
className="btn btn-primary w-100"
105+
onClick={handleGenerateProofClick}
106+
disabled={noirApp.appState.proofingStatus === 'proofing' || noirApp.appState.status === 'compiling'}
107+
>
108+
{noirApp.appState.proofingStatus === 'proofing' ? (
109+
<>
110+
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
111+
<span className="ms-1">Generating Proof...</span>
112+
</>
113+
) : (
114+
<FormattedMessage id="noir.generateProof" defaultMessage="Generate Proof" />
115+
)}
116+
</button>
117+
</CustomTooltip>
118+
<button className="btn btn-sm btn-outline-info w-100 text-start mt-2" onClick={(e) => handleViewFile(e, proverTomlPath)}>
119+
<div className="d-flex align-items-center">
120+
<i className="fas fa-file-invoice me-2"></i>
121+
<span>View Prover.toml</span>
122+
</div>
123+
</button>
124+
</div>
105125
</>
106126
</RenderIf>
107127
<RenderIf condition={noirApp.appState.proofingStatus === 'succeed' && !!noirApp.appState.formattedProof}>
108128
<div className="mt-3">
109129
<label className="noir_label form-check-label">
110-
<FormattedMessage id="noir.verifierInputs" defaultMessage="Verifier Inputs" />
130+
<FormattedMessage id="noir.proofArtifacts" defaultMessage="Proof Artifacts" />
111131
</label>
112-
113-
{/* _proof (bytes) */}
114-
<div className="mt-2">
115-
<label className="form-label small mb-0">
116-
<code>_proof (bytes)</code>
117-
</label>
118-
<textarea
119-
className="form-control form-control-sm"
120-
value={noirApp.appState.formattedProof}
121-
readOnly
122-
rows={4}
123-
data-id="noir-verifier-input-proof"
124-
></textarea>
125-
</div>
126132

127-
{/* _publicInputs (bytes32[]) */}
128-
<div className="mt-2">
129-
<label className="form-label small mb-0">
130-
<code>_publicInputs (bytes32[])</code>
131-
</label>
132-
<textarea
133-
className="form-control form-control-sm"
134-
value={formattedPublicInputsString}
135-
readOnly
136-
rows={4}
137-
data-id="noir-verifier-input-public-inputs"
138-
></textarea>
133+
<div className="d-flex flex-wrap justify-content-between mt-2">
134+
<button className="btn btn-sm btn-outline-info mb-1 flex-grow-1 text-start" onClick={(e) => handleViewFile(e, `${buildPath}/proof`)}>
135+
<div className="d-flex align-items-center">
136+
<i className="fas fa-file-code me-2"></i>
137+
<span>View Proof</span>
138+
</div>
139+
</button>
140+
<button className="btn btn-sm btn-outline-info mb-1 flex-grow-1 text-start" onClick={(e) => handleViewFile(e, `${buildPath}/public_inputs`)}>
141+
<div className="d-flex align-items-center">
142+
<i className="fas fa-file-invoice me-2"></i>
143+
<span>View Public Inputs</span>
144+
</div>
145+
</button>
146+
</div>
147+
<div className="d-flex flex-wrap justify-content-between">
148+
<button className="btn btn-sm btn-outline-info mb-1 flex-grow-1 text-start" onClick={(e) => handleViewFile(e, `${contractsPath}/Verifier.sol`)}>
149+
<div className="d-flex align-items-center">
150+
<i className="fab fa-ethereum me-2"></i>
151+
<span>View Verifier.sol</span>
152+
</div>
153+
</button>
154+
<button className="btn btn-sm btn-outline-info mb-1 flex-grow-1 text-start" onClick={(e) => handleViewFile(e, `${scriptsPath}/verify.ts`)}>
155+
<div className="d-flex align-items-center">
156+
<i className="fab fa-js-square me-2"></i>
157+
<span>View verify.ts</span>
158+
</div>
159+
</button>
139160
</div>
140161
</div>
141162
</RenderIf>
163+
<RenderIf condition={noirApp.appState.status !== 'compiling' && noirApp.appState.proofingStatus !== 'succeed'}>
164+
<CompilerFeedback feedback={noirApp.appState.compilerFeedback} filePathToId={noirApp.appState.filePathToId} openErrorLocation={handleOpenErrorLocation} hideWarnings={noirApp.appState.hideWarnings} askGPT={askGPT} />
165+
</RenderIf>
142166
</div>
143167
</div>
144168
</article>

apps/noir-compiler/src/app/reducers/state.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export const appReducer = (state = appInitialState, action: Actions): AppState =
4646
status: action.payload,
4747
proofingStatus: 'idle',
4848
formattedProof: '',
49-
formattedPublicInputs: []
49+
formattedPublicInputs: [],
50+
compilerFeedback: ''
5051
}
5152
}
5253
return {
@@ -60,7 +61,8 @@ export const appReducer = (state = appInitialState, action: Actions): AppState =
6061
...state,
6162
proofingStatus: action.payload,
6263
formattedProof: '',
63-
formattedPublicInputs: []
64+
formattedPublicInputs: [],
65+
compilerFeedback: ''
6466
}
6567
}
6668
return {

apps/noir-compiler/src/app/services/noirPluginClient.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ export class NoirPluginClient extends PluginClient {
186186
const requestID = this.generateRequestID()
187187
console.log(`[${requestID}] New proof generation request for: ${path}`)
188188

189-
// this.internalEvents.emit('noir_proofing_start')
190189
this.internalEvents.emit('noir_proofing_start')
191190
this.emit('statusChanged', { key: 'loading', title: 'Generating Proof...', type: 'info' })
192191
this.call('terminal', 'log', { type: 'log', value: 'Generating proof for ' + path })
@@ -209,7 +208,6 @@ export class NoirPluginClient extends PluginClient {
209208
formData.append('file', zippedProject, `${extractNameFromKey(path)}.zip`)
210209

211210
this.ws.send(JSON.stringify({ requestId: requestID }))
212-
213211
// @ts-ignore
214212
const response = await axios.post(`${BASE_URL}/generate-proof-with-verifier?requestId=${requestID}`, formData, {
215213
responseType: 'blob'
@@ -229,54 +227,79 @@ export class NoirPluginClient extends PluginClient {
229227

230228
const zip = await JSZip.loadAsync(receivedBlob)
231229
const buildPath = projectRoot === '/' ? 'build' : `${projectRoot}/build`
232-
230+
const contractsPath = projectRoot === '/' ? 'contracts' : `${projectRoot}/contracts`
231+
const scriptsPath = projectRoot === '/' ? 'scripts' : `${projectRoot}/scripts`
232+
233+
let formattedProof: string | null = null
234+
let formattedPublicInputsStr: string | null = null
235+
233236
const filesToSave = {
234-
'proof': { path: `${buildPath}/proof`, type: 'hex' },
235-
'vk': { path: `${buildPath}/vk`, type: 'hex' },
236-
'verifier/solidity/Verifier.sol': { path: 'contracts/Verifier.sol', type: 'string' },
237-
'program.json': { path: `${buildPath}/program.json`, type: 'string' },
237+
'proof.bin': { path: `${buildPath}/proof.bin`, type: 'hex' },
238+
'vk.bin': { path: `${buildPath}/vk.bin`, type: 'hex' },
239+
'scripts/verify.ts': { path: `${scriptsPath}/verify.ts`, type: 'string', isScript: true },
240+
'verifier/solidity/Verifier.sol': { path: `${contractsPath}/Verifier.sol`, type: 'string' },
241+
'proof': { path: `${buildPath}/proof`, type: 'string', isProof: true },
242+
'public_inputs': { path: `${buildPath}/public_inputs`, type: 'string', isPublicInputs: true },
238243
}
239244

245+
console.log('[Noir Plugin] Starting file extraction loop...')
246+
240247
for (const [zipPath, info] of Object.entries(filesToSave)) {
241248
const file = zip.file(zipPath)
249+
242250
if (file) {
243251
let content: string;
252+
244253
if (info.type === 'hex') {
245254
const bytes = await file.async('uint8array');
246255
content = this.bytesToHex(bytes);
247256
} else {
248257
content = await file.async('string');
249258
}
259+
260+
// @ts-ignore
261+
if (info.isProof) formattedProof = content
262+
// @ts-ignore
263+
if (info.isPublicInputs) formattedPublicInputsStr = content
264+
// @ts-ignore
265+
if (info.isScript) {
266+
console.log(`[Noir Plugin] Found script file: ${zipPath}. Replacing placeholder with: '${buildPath}'`)
267+
content = content.replace(/%%BUILD_PATH%%/g, buildPath)
268+
}
269+
250270
await this.call('fileManager', 'writeFile', info.path, content)
251271
// @ts-ignore
252272
this.call('terminal', 'log', { type: 'log', value: `Wrote artifact: ${info.path}` })
273+
274+
} else {
275+
// @ts-ignore
276+
this.call('terminal', 'log', { type: 'warn', value: `Warning: File '${zipPath}' not found in zip from backend.` })
277+
console.warn(`[Noir Plugin] File not found in zip: ${zipPath}`)
253278
}
254279
}
280+
console.log('[Noir Plugin] File extraction loop finished.')
255281

256-
282+
console.log('[Noir Plugin] Formatting verifier inputs...')
283+
// @ts-ignore
257284
this.call('terminal', 'log', { type: 'log', value: 'Formatting Verifier.sol inputs...' })
258-
259-
const proofFile = zip.file('formatted_proof.txt');
260-
const inputsFile = zip.file('formatted_public_inputs.json');
261285

262-
if (!proofFile || !inputsFile) {
263-
throw new Error("Formatted proof or public inputs not found in zip response from backend.");
286+
if (!formattedProof || !formattedPublicInputsStr) {
287+
console.error('[Noir Plugin] Error: formattedProof or formattedPublicInputsStr is null or empty after loop.')
288+
throw new Error("Formatted proof or public inputs data could not be read from zip stream.")
264289
}
265-
266-
const formattedProof = await proofFile.async('string');
267-
const formattedPublicInputsStr = await inputsFile.async('string');
268-
const formattedPublicInputs = JSON.parse(formattedPublicInputsStr);
290+
291+
const formattedPublicInputs = JSON.parse(formattedPublicInputsStr)
269292

270293
const verifierInputs: VerifierInputs = {
271294
proof: formattedProof,
272295
publicInputs: formattedPublicInputs
273296
}
274297

298+
console.log('[Noir Plugin] Emitting noir_proofing_done with payload:', verifierInputs)
275299
this.internalEvents.emit('noir_proofing_done', verifierInputs)
276300

277301
this.emit('statusChanged', { key: 'succeed', title: 'Proof generated successfully', type: 'success' })
278302
this.call('terminal', 'log', { type: 'log', value: 'Proof generation and file extraction complete.' })
279-
// this.internalEvents.emit('noir_proofing_done')
280303

281304
} catch (e) {
282305
console.error(`[${requestID}] Proof generation failed:`, e)
@@ -303,7 +326,6 @@ export class NoirPluginClient extends PluginClient {
303326
}
304327
this.internalEvents.emit('noir_proofing_errored', e)
305328
this.call('terminal', 'log', { type: 'error', value: errorMsg })
306-
// this.internalEvents.emit('noir_proofing_errored', new Error(errorMsg))
307329

308330
if (projectRoot !== null) {
309331
try {

0 commit comments

Comments
 (0)