commit
b3d0a02c09
@ -1,26 +1,96 @@ |
||||
import { RenderIf } from "@remix-ui/helper"; |
||||
import { AppState } from "../types"; |
||||
import { AppState } from "../types" |
||||
import { Dropdown } from "react-bootstrap" |
||||
import React, { Ref } from "react" |
||||
import isElectron from 'is-electron' |
||||
|
||||
export function VersionList ({ currentVersion, versionList, setVersion }: { versionList: AppState['versionList'], currentVersion: string, setVersion: (version: string) => void }) { |
||||
export function VersionList ({ currentVersion, versionList, downloadList, setVersion }: { versionList: AppState['versionList'], currentVersion: string, setVersion: (version: string) => void , downloadList: string[]}) { |
||||
const versionListKeys = Object.keys(versionList) |
||||
|
||||
return ( |
||||
<select |
||||
value={currentVersion} |
||||
onChange={(e) => setVersion(e.target.value)} |
||||
className="custom-select" |
||||
> |
||||
<RenderIf condition={versionListKeys.length > 0}> |
||||
<> |
||||
<Dropdown> |
||||
<Dropdown.Toggle as={CircomVersionMenuToggle} id="circomVersionList" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control"> |
||||
<div style={{ flexGrow: 1, overflow: 'hidden', display:'flex', justifyContent:'left' }}> |
||||
{ versionList[currentVersion].name } |
||||
</div> |
||||
</Dropdown.Toggle> |
||||
|
||||
<Dropdown.Menu as={CircomVersionMenu} className="w-100 custom-dropdown-items overflow-hidden"> |
||||
{ |
||||
versionListKeys.map((version, index) => ( |
||||
<option value={version} key={index}> |
||||
{ versionList[version].name } |
||||
</option> |
||||
versionListKeys.reverse().map((version, index) => ( |
||||
<Dropdown.Item key={index} onClick={() => { |
||||
setVersion(version) |
||||
}}> |
||||
<div className='d-flex w-100 justify-content-between'> |
||||
<div> |
||||
<span className={`fas fa-check text-success mr-2 ${currentVersion === version ? 'visible' : 'invisible'}`}></span> |
||||
<span> |
||||
{ isElectron() ? versionList[version].name.replace('wasm', '') : versionList[version].name } |
||||
</span> |
||||
</div> |
||||
{ isElectron() ? downloadList.includes(version) ? <div className='far fa-arrow-circle-down'></div> : <div className='fas fa-arrow-circle-down text-success ml-auto'></div> : null } |
||||
</div> |
||||
</Dropdown.Item> |
||||
)) |
||||
} |
||||
</> |
||||
</RenderIf> |
||||
</select> |
||||
</Dropdown.Menu> |
||||
</Dropdown> |
||||
) |
||||
} |
||||
|
||||
const CircomVersionMenuToggle = React.forwardRef( |
||||
( |
||||
{ |
||||
children, |
||||
onClick, |
||||
className = '' |
||||
}: { |
||||
children: React.ReactNode |
||||
onClick: (e) => void |
||||
className: string |
||||
}, |
||||
ref: Ref<HTMLButtonElement> |
||||
) => ( |
||||
<button |
||||
ref={ref} |
||||
onClick={(e) => { |
||||
e.preventDefault() |
||||
onClick(e) |
||||
}} |
||||
className={className.replace('dropdown-toggle', '')} |
||||
> |
||||
<div className="d-flex"> |
||||
{children} |
||||
<div> |
||||
<i className="fad fa-sort-circle"></i> |
||||
</div> |
||||
</div> |
||||
</button> |
||||
) |
||||
) |
||||
|
||||
const CircomVersionMenu = React.forwardRef( |
||||
( |
||||
{ |
||||
children, |
||||
style, |
||||
'data-id': dataId, |
||||
className, |
||||
'aria-labelledby': labeledBy |
||||
}: { |
||||
'children': React.ReactNode |
||||
'style'?: React.CSSProperties |
||||
'data-id'?: string |
||||
'className': string |
||||
'aria-labelledby'?: string |
||||
}, |
||||
ref: Ref<HTMLDivElement> |
||||
) => { |
||||
const height = window.innerHeight * 0.6 |
||||
return ( |
||||
<div ref={ref} style={style} className={className} aria-labelledby={labeledBy} data-id={dataId}> |
||||
<ul className="list-unstyled mb-0" style={{ maxHeight: height + 'px',overflowY:'auto' }}> |
||||
{children} |
||||
</ul> |
||||
</div> |
||||
) |
||||
} |
||||
) |
||||
|
@ -0,0 +1,12 @@ |
||||
import { ElectronPlugin } from '@remixproject/engine-electron' |
||||
|
||||
export class circomPlugin extends ElectronPlugin { |
||||
constructor() { |
||||
super({ |
||||
displayName: 'circom', |
||||
name: 'circom', |
||||
description: 'circom language compiler', |
||||
}) |
||||
this.methods = [] |
||||
} |
||||
} |
@ -0,0 +1,96 @@ |
||||
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" |
||||
import { Profile } from "@remixproject/plugin-utils" |
||||
import { getInstallationPath, circomCli, extractParentFromKey, getInstallationUrl, getLogInputSignalsPath, extractNameFromKey } from "../tools/circom" |
||||
import path from "path" |
||||
import { existsSync, readFileSync } from "fs" |
||||
|
||||
const profile: Profile = { |
||||
displayName: 'circom', |
||||
name: 'circom', |
||||
description: 'Circom language compiler' |
||||
} |
||||
|
||||
export class CircomElectronPlugin extends ElectronBasePlugin { |
||||
clients: CircomElectronPluginClient[] = [] |
||||
|
||||
constructor() { |
||||
super(profile, clientProfile, CircomElectronPluginClient) |
||||
this.methods = [...super.methods] |
||||
} |
||||
} |
||||
|
||||
const clientProfile: Profile = { |
||||
name: 'circom', |
||||
displayName: 'circom', |
||||
description: 'Circom Language Compiler', |
||||
methods: ['install', 'run', 'getInputs', 'isVersionInstalled'] |
||||
} |
||||
|
||||
class CircomElectronPluginClient extends ElectronBasePluginClient { |
||||
isCircomInstalled: boolean = false |
||||
|
||||
constructor(webContentsId: number, profile: Profile) { |
||||
super(webContentsId, profile) |
||||
this.onload() |
||||
} |
||||
|
||||
async install(version = 'latest') { |
||||
this.call('terminal' as any, 'logHtml', `Checking if circom compiler (${version}) is installed in ${getInstallationPath(version)}`) |
||||
|
||||
this.isCircomInstalled = await circomCli.isCircomInstalled(version) |
||||
if (!this.isCircomInstalled) { |
||||
this.call('terminal' as any, 'logHtml', 'Downloading circom compiler from ' + getInstallationUrl(version)) |
||||
await circomCli.installCircom(version) |
||||
this.isCircomInstalled = true |
||||
this.call('terminal' as any, 'logHtml', `Circom compiler (${version}) downloaded from ${getInstallationUrl(version)} to ${getInstallationPath(version)}`) |
||||
} |
||||
} |
||||
|
||||
async run(filePath: string, version = 'latest', options: Record<string, string>) { |
||||
if (!this.isCircomInstalled) await this.install(version) |
||||
// @ts-ignore
|
||||
const wd = await this.call('fs', 'getWorkingDir') |
||||
const binDir = path.join(wd, path.join(extractParentFromKey(filePath), '.bin')) |
||||
// @ts-ignore
|
||||
const outputDirExists = await this.call('fs', 'exists', path.join(extractParentFromKey(filePath), '.bin')) |
||||
// @ts-ignore
|
||||
if (!outputDirExists) await this.call('fs', 'mkdir', path.join(extractParentFromKey(filePath), '.bin')) |
||||
else { |
||||
// @ts-ignore
|
||||
if (process.platform === 'win32' && 'wasm' in options) { |
||||
try{ |
||||
// @ts-ignore
|
||||
await this.call('fs', 'rmdir', path.join(extractParentFromKey(filePath), '.bin', extractNameFromKey(filePath).replace('.circom', '_js'))) |
||||
} catch (e) { |
||||
} |
||||
} |
||||
} |
||||
filePath = path.join(wd, filePath) |
||||
const depPath = path.join(wd, '.deps/https/raw.githubusercontent.com/iden3/') |
||||
const outputDir = process.platform !== 'win32' ? path.normalize(extractParentFromKey(filePath) + '/' + '.bin') : binDir |
||||
|
||||
this.call('terminal' as any, 'logHtml', `Compiling ${filePath} with circom compiler (${version})`) |
||||
return await circomCli.run(`${filePath} -l ${depPath} -o ${outputDir}`, version, options) |
||||
} |
||||
|
||||
getInputs() { |
||||
const inputsFile = getLogInputSignalsPath() |
||||
const inputsFileExists = existsSync(inputsFile) |
||||
const signals: string[] = [] |
||||
|
||||
if (inputsFileExists) { |
||||
const inputsContent = readFileSync(inputsFile, 'utf-8') |
||||
const regexPattern = /main\.(\w+)/g |
||||
|
||||
let match |
||||
while ((match = regexPattern.exec(inputsContent)) !== null) { |
||||
signals.push(match[1]) |
||||
} |
||||
return signals |
||||
} |
||||
} |
||||
|
||||
async isVersionInstalled(version: string) { |
||||
return await circomCli.isCircomInstalled(version) |
||||
} |
||||
} |
@ -0,0 +1,136 @@ |
||||
import { app } from 'electron' |
||||
import os from 'os' |
||||
import { exec } from 'child_process' |
||||
import path from 'path' |
||||
import fs, { existsSync } from 'fs' |
||||
import axios from 'axios' |
||||
|
||||
async function downloadFile(url: string, dest: string) { |
||||
const writer = fs.createWriteStream(dest) |
||||
const response = await axios({ |
||||
url, |
||||
method: 'GET', |
||||
responseType: 'stream' |
||||
}) |
||||
|
||||
response.data.pipe(writer) |
||||
|
||||
return new Promise((resolve, reject) => { |
||||
writer.on('finish', () => { |
||||
if (process.platform !== 'win32') { |
||||
// Sets permission to make the file executable
|
||||
fs.chmod(dest, 0o775, (err) => { |
||||
if (err) { |
||||
reject(`Error making file executable: ${err}`) |
||||
} else { |
||||
resolve(dest) |
||||
} |
||||
}) |
||||
} |
||||
resolve(true) |
||||
}) |
||||
writer.on('error', reject) |
||||
}) |
||||
} |
||||
|
||||
export function getInstallationPath(version) { |
||||
const fileNames = { |
||||
win32: 'circom-windows-amd64.exe', |
||||
darwin: 'circom-macos-amd64', |
||||
linux: 'circom-linux-amd64' |
||||
}; |
||||
|
||||
return path.join(os.tmpdir(), 'circom-download', version, fileNames[process.platform]); |
||||
} |
||||
|
||||
export function getInstallationUrl(version) { |
||||
switch (process.platform) { |
||||
case 'win32': |
||||
return version === 'latest' ? 'https://github.com/iden3/circom/releases/latest/download/circom-windows-amd64.exe' : `https://github.com/iden3/circom/releases/download/${version}/circom-windows-amd64.exe` |
||||
|
||||
case 'darwin': |
||||
return version === 'latest' ? 'https://github.com/iden3/circom/releases/latest/download/circom-macos-amd64' : `https://github.com/iden3/circom/releases/download/${version}/circom-macos-amd64` |
||||
|
||||
case 'linux': |
||||
return version === 'latest' ? 'https://github.com/iden3/circom/releases/latest/download/circom-linux-amd64' : `https://github.com/iden3/circom/releases/download/${version}/circom-linux-amd64` |
||||
} |
||||
} |
||||
|
||||
export function getLogInputSignalsPath() { |
||||
|
||||
const tempFilePath = path.join(os.tmpdir(), 'log_input_signals.txt'); |
||||
return tempFilePath; |
||||
} |
||||
|
||||
export const circomCli = { |
||||
async installCircom (version) { |
||||
const installationPath = getInstallationPath(version) |
||||
const installationUrl = getInstallationUrl(version) |
||||
|
||||
if (!existsSync(path.dirname(installationPath))) fs.mkdirSync(path.dirname(installationPath), { recursive: true }) |
||||
try { |
||||
await downloadFile(installationUrl, installationPath) |
||||
} catch (e) { |
||||
fs.rmSync(installationPath) |
||||
throw new Error(e.message) |
||||
} |
||||
}, |
||||
|
||||
async isCircomInstalled (version) { |
||||
try { |
||||
const installationPath = getInstallationPath(version) |
||||
return existsSync(installationPath) |
||||
} catch (e) { |
||||
return false |
||||
} |
||||
}, |
||||
|
||||
async run (filePath: string, version: string, options?: Record<string, string>) { |
||||
const installationPath = getInstallationPath(version) |
||||
const cmd = `${installationPath} ${filePath} ${Object.keys(options || {}).map((key) => options[key] ? `--${key} ${options[key]}` : `--${key}`).join(' ')}` |
||||
console.log(cmd) |
||||
if(process.platform === 'darwin') { |
||||
const rosettaInstalled = await checkRosettaInstalled(); |
||||
if(rosettaInstalled === 'Rosetta is not installed') { |
||||
throw new Error('Rosetta is not installed. Please install Rosetta to run this command.'); |
||||
} |
||||
} |
||||
return new Promise((resolve, reject) => { |
||||
exec(cmd, { cwd: os.tmpdir() }, (error, stdout, stderr) => { |
||||
if (error) { |
||||
reject(`${error.message} with error code ${error.code}`) |
||||
} else { |
||||
resolve({ stdout, stderr }) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
export const extractParentFromKey = (key: string):string => { |
||||
if (!key) return |
||||
const keyPath = key.split('/') |
||||
|
||||
keyPath.pop() |
||||
|
||||
return keyPath.join('/') |
||||
} |
||||
|
||||
export const extractNameFromKey = (key: string): string => { |
||||
if (!key) return |
||||
const keyPath = key.split('/') |
||||
|
||||
return keyPath[keyPath.length - 1] |
||||
} |
||||
|
||||
async function checkRosettaInstalled(): Promise<string> { |
||||
return new Promise((resolve, reject) => { |
||||
exec("/usr/bin/pgrep oahd > /dev/null && echo 'Rosetta is installed' || echo 'Rosetta is not installed'", (error, stdout, stderr) => { |
||||
if (error) { |
||||
reject(`Error: ${stderr}`); |
||||
} else { |
||||
resolve(stdout.trim()); |
||||
} |
||||
}); |
||||
}); |
||||
} |
@ -0,0 +1,86 @@ |
||||
import { NightwatchBrowser } from "nightwatch" |
||||
|
||||
|
||||
const tests = { |
||||
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||
browser.hideToolTips() |
||||
done() |
||||
}, |
||||
'Should create semaphore workspace': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.waitForElementVisible('*[data-id="homeTabGetStartedsemaphore"]', 20000) |
||||
.click('*[data-id="homeTabGetStartedsemaphore"]') |
||||
.pause(3000) |
||||
.windowHandles(function (result) { |
||||
console.log(result.value) |
||||
browser.switchWindow(result.value[1]) |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcircuits"]') |
||||
.click('*[data-id="treeViewLitreeViewItemcircuits"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcircuits/semaphore.circom"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts"]') |
||||
.click('*[data-id="treeViewLitreeViewItemscripts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/groth16"]') |
||||
.click('*[data-id="treeViewLitreeViewItemscripts/groth16"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/groth16/groth16_trusted_setup.ts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/groth16/groth16_zkproof.ts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk"]') |
||||
.click('*[data-id="treeViewLitreeViewItemscripts/plonk"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk/plonk_trusted_setup.ts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk/plonk_zkproof.ts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtemplates"]') |
||||
.click('*[data-id="treeViewLitreeViewItemtemplates"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtemplates/groth16_verifier.sol.ejs"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtemplates/plonk_verifier.sol.ejs"]') |
||||
}) |
||||
}, |
||||
'Should compile a simple circuit using editor play button': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.click('[data-id="treeViewLitreeViewItemcircuits/simple.circom"]') |
||||
.waitForElementVisible('[data-id="play-editor"]') |
||||
.click('[data-id="play-editor"]') |
||||
.pause(3000) |
||||
.click('[data-id="play-editor"]') |
||||
.waitForElementVisible('[data-id="verticalIconsKindcircuit-compiler"]') |
||||
.waitForElementVisible('[data-id="treeViewLitreeViewItemcircuits/.bin"]') |
||||
.click('[data-id="treeViewLitreeViewItemcircuits/.bin"]') |
||||
.waitForElementVisible('[data-id="treeViewLitreeViewItemcircuits/.bin/simple_js"]') |
||||
.click('[data-id="treeViewLitreeViewItemcircuits/.bin/simple_js"]') |
||||
.waitForElementVisible('[data-id="treeViewLitreeViewItemcircuits/.bin/simple_js/simple.wasm"]') |
||||
}, |
||||
'Should run setup script for simple circuit': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.clickLaunchIcon('circuit-compiler') |
||||
.frame(0) |
||||
.waitForElementVisible('[data-id="runSetupBtn"]') |
||||
.click('[data-id="runSetupBtn"]') |
||||
}, |
||||
'Should compute a witness for a simple circuit': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.waitForElementVisible('[data-id="compute_witness_btn"]', 60000) |
||||
.waitForElementVisible('[data-id="circuit_input_a"]') |
||||
.waitForElementVisible('[data-id="circuit_input_b"]') |
||||
.setValue('[data-id="circuit_input_a"]', '1') |
||||
.setValue('[data-id="circuit_input_b"]', '2') |
||||
.click('[data-id="compute_witness_btn"]') |
||||
.frameParent() |
||||
.clickLaunchIcon('filePanel') |
||||
.waitForElementPresent('[data-id="treeViewLitreeViewItemcircuits/.bin/simple_js/simple.wtn"]') |
||||
.waitForElementVisible('[data-id="treeViewLitreeViewItemcircuits/.bin/simple_js/simple.wtn"]') |
||||
}, |
||||
'Should generate proof for a simple circuit': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.clickLaunchIcon('circuit-compiler') |
||||
.frame(0) |
||||
.waitForElementVisible('[data-id="generateProofBtn"]') |
||||
.click('[data-id="generateProofBtn"]') |
||||
.frameParent() |
||||
.waitForElementVisible({ |
||||
locateStrategy: 'xpath', |
||||
selector: "//span[@class='text-log' and contains(., 'zk proof validity true')]", |
||||
timeout: 60000 |
||||
}) |
||||
|
||||
} |
||||
} |
||||
|
||||
module.exports = tests |
@ -0,0 +1,67 @@ |
||||
import { NightwatchBrowser } from "nightwatch" |
||||
|
||||
|
||||
const tests = { |
||||
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||
browser.hideToolTips() |
||||
done() |
||||
}, |
||||
'Should create semaphore workspace': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.waitForElementVisible('*[data-id="homeTabGetStartedsemaphore"]', 20000) |
||||
.click('*[data-id="homeTabGetStartedsemaphore"]') |
||||
.pause(3000) |
||||
.windowHandles(function (result) { |
||||
console.log(result.value) |
||||
browser.switchWindow(result.value[1]) |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcircuits"]') |
||||
.click('*[data-id="treeViewLitreeViewItemcircuits"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcircuits/semaphore.circom"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts"]') |
||||
.click('*[data-id="treeViewLitreeViewItemscripts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/groth16"]') |
||||
.click('*[data-id="treeViewLitreeViewItemscripts/groth16"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/groth16/groth16_trusted_setup.ts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/groth16/groth16_zkproof.ts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk"]') |
||||
.click('*[data-id="treeViewLitreeViewItemscripts/plonk"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk/plonk_trusted_setup.ts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk/plonk_zkproof.ts"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtemplates"]') |
||||
.click('*[data-id="treeViewLitreeViewItemtemplates"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtemplates/groth16_verifier.sol.ejs"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtemplates/plonk_verifier.sol.ejs"]') |
||||
}) |
||||
}, |
||||
'Should run plonk trusted setup script for hash checker #group6': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.click('[data-id="treeViewLitreeViewItemscripts/plonk/plonk_trusted_setup.ts"]') |
||||
.pause(2000) |
||||
.click('[data-id="play-editor"]') |
||||
.waitForElementVisible('[data-id="verticalIconsKindcircuit-compiler"]') |
||||
.waitForElementVisible({ |
||||
locateStrategy: 'xpath', |
||||
selector: "//span[@class='text-log' and contains(., 'setup done.')]", |
||||
timeout: 60000 |
||||
}) |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk/zk"]') |
||||
.click('*[data-id="treeViewLitreeViewItemscripts/plonk/zk"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk/zk/keys"]') |
||||
.click('*[data-id="treeViewLitreeViewItemscripts/plonk/zk/keys"]') |
||||
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/plonk/zk/keys/verification_key.json"]') |
||||
}, |
||||
'Should run plonk zkproof script for hash checker #group6': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.click('[data-id="treeViewLitreeViewItemscripts/plonk/plonk_zkproof.ts"]') |
||||
.pause(2000) |
||||
.click('[data-id="play-editor"]') |
||||
.waitForElementVisible({ |
||||
locateStrategy: 'xpath', |
||||
selector: "//span[@class='text-log' and contains(., 'proof done')]", |
||||
timeout: 60000 |
||||
}) |
||||
} |
||||
|
||||
} |
||||
|
||||
module.exports = tests |
Loading…
Reference in new issue