chore: init repo

This commit is contained in:
ryanycw
2023-06-02 12:40:50 +08:00
commit 7bdc59a483
87 changed files with 13500 additions and 0 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
keys/
**/keys/

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
.DS_Store
config.ts

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
**/build
**/artifacts
**/cache
**/typechain-types

5
.prettierrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"tabWidth": 4,
"singleQuote": true,
"semi": false
}

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM node:16-buster
COPY . /src
WORKDIR /src
RUN yarn && rm -rf packages/frontend
RUN sh scripts/loadKeys.sh
RUN rm -r packages/relay/keys/buildOrdered*
FROM node:16-buster
COPY --from=0 /src /src
WORKDIR /src/packages/relay
CMD ["npm", "start"]

51
README.md Normal file
View File

@@ -0,0 +1,51 @@
# create-unirep-app
This is a demo app of a [unirep](https://github.com/Unirep/Unirep) attester. In this demo app, users can request data from the example attester. After transition, user can prove how much data he has.
> See: [Users and Attesters](https://developer.unirep.io/docs/protocol/users-and-attesters)
## 1. Installation
```shell
npx create-unirep-app
```
Then `cd` into the directory that was created.
## 2 Start with each daemon
### 2.1 Build the files
```shell
yarn build
```
### 2.2 Start a node
```shell
yarn contracts hardhat node
```
### 2.3 Deploy smart contracts
in new terminal window, from root:
```shell
yarn contracts deploy
```
### 2.4 Start a relayer (backend)
```shell
yarn relay start
```
### 2.5 Start a frontend
in new terminal window, from root:
```shell
yarn frontend start
```
It will be running at: http://localhost:3000/

7
lerna.json Normal file
View File

@@ -0,0 +1,7 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "independent",
"npmClient": "yarn",
"packages": ["packages/*"]
}

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "social-tw-website",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"install": "lerna bootstrap",
"build": "lerna run build",
"circuits": "yarn workspace @unirep-app/circuits run",
"contracts": "yarn workspace @unirep-app/contracts run",
"frontend": "yarn workspace @unirep-app/frontend run",
"relay": "yarn workspace @unirep-app/relay run",
"start": "node scripts/start.mjs",
"linkUnirep": "sh ./scripts/linkUnirep.sh",
"copyUnirep": "sh ./scripts/copyUnirep.sh",
"lint": "prettier ."
},
"devDependencies": {
"lerna": "^6.0.1",
"node-fetch": "^3.3.0"
},
"dependencies": {
"prettier": "^2.8.4"
}
}

7
packages/circuits/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
dist
*.ptau
zksnarkBuild/*
!*/dataProof.vkey.json
!*/dataProof.wasm
!*/dataProof.zkey
!*/dataProof_main.circom

View File

@@ -0,0 +1,11 @@
# `circuits`
> TODO: description
## Usage
```
const circuits = require('circuits');
// TODO: DEMONSTRATE API
```

View File

@@ -0,0 +1,54 @@
pragma circom 2.0.0;
include "../../../node_modules/@unirep/circuits/circuits/proveReputation.circom";
include "../../../node_modules/@unirep/circuits/circuits/circomlib/circuits/poseidon.circom";
include "../../../node_modules/@unirep/circuits/circuits/circomlib/circuits/mux1.circom";
include "../../../node_modules/@unirep/circuits/circuits/circomlib/circuits/gates.circom";
include "../../../node_modules/@unirep/circuits/circuits/circomlib/circuits/comparators.circom";
template DataProof(STATE_TREE_DEPTH, FIELD_COUNT, SUM_FIELD_COUNT) {
// State tree leaf: Identity & user state root
signal input identity_secret;
// State tree
signal input state_tree_indexes[STATE_TREE_DEPTH];
signal input state_tree_elements[STATE_TREE_DEPTH];
signal input attester_id;
signal input data[FIELD_COUNT];
signal output state_tree_root;
signal input epoch;
// Prove values
signal input value[SUM_FIELD_COUNT];
/* 1. Check if user exists in the State Tree */
// Compute state tree root
component leaf_hasher = StateTreeLeaf(FIELD_COUNT);
leaf_hasher.identity_secret <== identity_secret;
leaf_hasher.attester_id <== attester_id;
leaf_hasher.epoch <== epoch;
for (var x = 0; x < FIELD_COUNT; x++) {
leaf_hasher.data[x] <== data[x];
}
component merkletree = MerkleTreeInclusionProof(STATE_TREE_DEPTH);
merkletree.leaf <== leaf_hasher.out;
for (var i = 0; i < STATE_TREE_DEPTH; i++) {
merkletree.path_index[i] <== state_tree_indexes[i];
merkletree.path_elements[i] <== state_tree_elements[i];
}
state_tree_root <== merkletree.root;
/* End of check 1 */
/* 2. Check if user data more than given value */
component get[SUM_FIELD_COUNT];
for (var x = 0; x < SUM_FIELD_COUNT; x++) {
get[x] = GreaterEqThan(252);
get[x].in[0] <== data[x];
get[x].in[1] <== value[x];
get[x].out === 1;
}
/* End of check 2 */
}

View File

@@ -0,0 +1,36 @@
{
"name": "@unirep-app/circuits",
"version": "1.0.0",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"description": "ZK proofs used for the unirep attesters",
"author": "Unirep Team <team@unirep.io>",
"homepage": "https://github.com/Unirep/create-unirep-app#readme",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Unirep/create-unirep-app.git"
},
"scripts": {
"build": "tsc && yarn copyCircom",
"buildsnark": "ts-node ./scripts/buildSnarks.ts && tsc && yarn copyCircom",
"test": "mocha -r ts-node/register test/*.test.ts --exit",
"copyCircom": "sh scripts/copyCircom.sh"
},
"bugs": {
"url": "https://github.com/Unirep/create-unirep-app/issues"
},
"devDependencies": {
"@semaphore-protocol/identity": "^3.6.0",
"@types/mocha": "^10.0.1",
"chai": "^4.3.7",
"mocha": "^10.0.0",
"snarkjs": "^0.4.7",
"ts-node": "^10.9.1",
"typescript": "^4.8.2"
},
"dependencies": {
"@unirep/circuits": "2.0.0-beta-3",
"@unirep/core": "2.0.0-beta-3"
}
}

View File

@@ -0,0 +1,61 @@
import path from 'path'
import { Circuit } from '@unirep/circuits'
import * as snarkjs from 'snarkjs'
import { SnarkProof, SnarkPublicSignals } from '@unirep/utils'
const buildPath = '../zksnarkBuild'
/**
* The default prover that uses the circuits in default built folder `zksnarkBuild/`
*/
export const defaultProver = {
/**
* Generate proof and public signals with `snarkjs.groth16.fullProve`
* @param circuitName Name of the circuit, which can be chosen from `Circuit`
* @param inputs The user inputs of the circuit
* @returns snark proof and public signals
*/
genProofAndPublicSignals: async (
circuitName: string | Circuit,
inputs: any
): Promise<any> => {
const circuitWasmPath = path.join(
__dirname,
buildPath,
`${circuitName}.wasm`
)
const zkeyPath = path.join(__dirname, buildPath, `${circuitName}.zkey`)
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
inputs,
circuitWasmPath,
zkeyPath
)
return { proof, publicSignals }
},
/**
* Verify the snark proof and public signals with `snarkjs.groth16.verify`
* @param circuitName Name of the circuit, which can be chosen from `Circuit`
* @param publicSignals The snark public signals that is generated from `genProofAndPublicSignals`
* @param proof The snark proof that is generated from `genProofAndPublicSignals`
* @returns True if the proof is valid, false otherwise
*/
verifyProof: async (
circuitName: string | Circuit,
publicSignals: SnarkPublicSignals,
proof: SnarkProof
): Promise<boolean> => {
const vkey = require(path.join(buildPath, `${circuitName}.vkey.json`))
return snarkjs.groth16.verify(vkey, publicSignals, proof)
},
/**
* Get vkey from default built folder `zksnarkBuild/`
* @param name Name of the circuit, which can be chosen from `Circuit`
* @returns vkey of the circuit
*/
getVKey: async (name: string | Circuit) => {
return require(path.join(buildPath, `${name}.vkey.json`))
},
}

View File

@@ -0,0 +1,95 @@
import { stringifyBigInts } from '@unirep/utils'
import fs from 'fs'
import path from 'path'
import * as snarkjs from 'snarkjs'
import child_process from 'child_process'
import { circuitContents, ptauName } from './circuits'
import { downloadPtau } from './downloadPtau'
main().catch((err) => {
console.log(`Uncaught error: ${err}`)
process.exit(0)
})
async function main() {
await downloadPtau()
const outDir = path.join(__dirname, '../zksnarkBuild')
await fs.promises.mkdir(outDir, { recursive: true })
// pass a space separated list of circuit names to this executable
const [, , ...circuits] = process.argv
if (circuits.length === 0) {
// if no arguments build all
circuits.push(...Object.keys(circuitContents))
}
for (const name of circuits) {
if (!circuitContents[name])
throw new Error(`Unknown circuit name: "${name}"`)
await fs.promises.writeFile(
path.join(outDir, `${name}_main.circom`),
circuitContents[name]
)
const inputFile = path.join(outDir, `${name}_main.circom`)
const circuitOut = path.join(outDir, `${name}_main.r1cs`)
const wasmOut = path.join(outDir, `${name}_main_js/${name}_main.wasm`)
const wasmOutDir = path.join(outDir, `${name}_main_js`)
const wasmOutFinal = path.join(outDir, `${name}.wasm`)
const ptau = path.join(outDir, ptauName)
const zkey = path.join(outDir, `${name}.zkey`)
const vkOut = path.join(outDir, `${name}.vkey.json`)
// Check if the circuitOut file exists
const circuitOutFileExists = await fs.promises
.stat(circuitOut)
.catch(() => false)
if (circuitOutFileExists) {
console.log(
circuitOut.split('/').pop(),
'exists. Skipping compilation.'
)
} else {
console.log(`Compiling ${inputFile.split('/').pop()}...`)
// Compile the .circom file
await new Promise((rs, rj) =>
child_process.exec(
`$HOME/.cargo/bin/circom --r1cs --wasm -o ${outDir} ${inputFile}`,
(err, stdout, stderr) => {
if (err) rj(err)
else rs('')
}
)
)
console.log(
'Generated',
circuitOut.split('/').pop(),
'and',
wasmOut.split('/').pop()
)
}
const zkeyOutFileExists = await fs.promises
.stat(zkey)
.catch(() => false)
if (zkeyOutFileExists) {
console.log(zkey.split('/').pop(), 'exists. Skipping compilation.')
} else {
console.log('Exporting verification key...')
await snarkjs.zKey.newZKey(circuitOut, ptau, zkey)
const vkeyJson = await snarkjs.zKey.exportVerificationKey(zkey)
const S = JSON.stringify(stringifyBigInts(vkeyJson), null, 1)
await fs.promises.writeFile(vkOut, S)
console.log(
`Generated ${zkey.split('/').pop()} and ${vkOut
.split('/')
.pop()}`
)
await fs.promises.rename(wasmOut, wasmOutFinal)
await fs.promises.rm(wasmOutDir, { recursive: true, force: true })
process.exit(0)
}
}
}

View File

@@ -0,0 +1,8 @@
import { CircuitConfig } from '@unirep/circuits'
const { STATE_TREE_DEPTH, FIELD_COUNT, SUM_FIELD_COUNT } = CircuitConfig.default
export const ptauName = 'powersOfTau28_hez_final_18.ptau'
export const circuitContents = {
dataProof: `pragma circom 2.0.0; include "../circuits/dataProof.circom"; \n\ncomponent main { public [ value ] } = DataProof(${STATE_TREE_DEPTH}, ${FIELD_COUNT}, ${SUM_FIELD_COUNT});`,
}

View File

@@ -0,0 +1,6 @@
#!/bin/sh
rm -rf ./dist/zksnarkBuild
cp -r ../../node_modules/@unirep/circuits/zksnarkBuild/. ./zksnarkBuild
cp -rf ./zksnarkBuild ./dist/zksnarkBuild

View File

@@ -0,0 +1,66 @@
import path from 'path'
import https from 'https'
import readline from 'readline'
import fs from 'fs'
import { ptauName } from './circuits'
export async function downloadPtau() {
const outDir = path.join(__dirname, '../zksnarkBuild')
await fs.promises.mkdir(outDir, { recursive: true })
const ptau = path.join(outDir, ptauName)
const ptauExists = await fs.promises.stat(ptau).catch(() => false)
if (!ptauExists) {
// download to a temporary file and then move it into place
const tmp = path.join(outDir, 'ptau.download.tmp')
await fs.promises.unlink(tmp).catch(() => {})
await new Promise((rs, rj) => {
const logPercent = (p) => {
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0)
process.stdout.write(
`Downloading ptau file, please wait... ${p}%`
)
}
const file = fs.createWriteStream(tmp, { flags: 'w' })
logPercent(0)
https.get(
`https://hermez.s3-eu-west-1.amazonaws.com/${ptauName}`,
(res) => {
const { statusCode } = res
const contentLength = res.headers['content-length']
if (statusCode !== 200) {
return rj(
`Received non-200 status code from ptau url: ${statusCode}`
)
}
let totalReceived = 0
const logTimer = setInterval(() => {
logPercent(
Math.floor(
(100 * totalReceived) / Number(contentLength)
)
)
}, 1000)
res.on('data', (chunk) => {
file.write(chunk)
totalReceived += chunk.length
})
res.on('error', (err) => {
clearInterval(logTimer)
rj(err)
})
res.on('end', () => {
file.end()
clearInterval(logTimer)
logPercent(100)
console.log()
rs('')
})
}
)
})
await fs.promises.rename(tmp, ptau)
}
}

View File

@@ -0,0 +1,34 @@
import { SnarkProof } from '@unirep/utils'
import { BigNumberish } from '@ethersproject/bignumber'
import { BaseProof, Prover } from '@unirep/circuits'
/**
* The data proof structure that helps to query the public signals
*/
export class DataProof extends BaseProof {
readonly idx = {
stateTreeRoot: 0,
value: [1, 5],
}
public stateTreeRoot: BigNumberish
public value: BigNumberish[]
/**
* @param _publicSignals The public signals of the data proof that can be verified by the prover
* @param _proof The proof that can be verified by the prover
* @param prover The prover that can verify the public signals and the proof
*/
constructor(
_publicSignals: BigNumberish[],
_proof: SnarkProof,
prover?: Prover
) {
super(_publicSignals, _proof, prover)
this.stateTreeRoot = _publicSignals[this.idx.stateTreeRoot]
this.value = []
for (let i = this.idx.value[0]; i < this.idx.value[1]; i++) {
this.value.push(_publicSignals[i])
}
;(this as any).circuit = 'dataProof'
}
}

View File

@@ -0,0 +1 @@
export * from './DataProof'

View File

@@ -0,0 +1,139 @@
import { expect } from 'chai'
import * as utils from '@unirep/utils'
import { Identity } from '@semaphore-protocol/identity'
import { Circuit, CircuitConfig } from '@unirep/circuits'
import { defaultProver } from '../provers/defaultProver'
const { FIELD_COUNT, SUM_FIELD_COUNT, STATE_TREE_DEPTH } = CircuitConfig.default
const circuit = 'dataProof'
const genCircuitInput = (config: {
id: Identity
epoch: number
attesterId: number | bigint
sumField?: (bigint | number)[]
replField?: (bigint | number)[]
proveValues?: (bigint | number)[]
}) => {
const { id, epoch, attesterId, sumField, replField, proveValues } =
Object.assign(
{
minRep: 0,
maxRep: 0,
graffitiPreImage: 0,
sumField: [],
replField: [],
proveValues: [],
},
config
)
const startBalance = [
...sumField,
...Array(SUM_FIELD_COUNT - sumField.length).fill(0),
...replField,
...Array(FIELD_COUNT - SUM_FIELD_COUNT - replField.length).fill(0),
]
// Global state tree
const stateTree = new utils.IncrementalMerkleTree(STATE_TREE_DEPTH)
const hashedLeaf = utils.genStateTreeLeaf(
id.secret,
BigInt(attesterId),
epoch,
startBalance as any
)
stateTree.insert(hashedLeaf)
const stateTreeProof = stateTree.createProof(0) // if there is only one GST leaf, the index is 0
const value = [
...proveValues,
Array(SUM_FIELD_COUNT - proveValues.length).fill(0),
]
const circuitInputs = {
identity_secret: id.secret,
state_tree_indexes: stateTreeProof.pathIndices,
state_tree_elements: stateTreeProof.siblings,
data: startBalance,
epoch: epoch,
attester_id: attesterId,
value: value,
}
return utils.stringifyBigInts(circuitInputs)
}
const genProofAndVerify = async (
circuit: Circuit | string,
circuitInputs: any
) => {
const startTime = new Date().getTime()
const { proof, publicSignals } =
await defaultProver.genProofAndPublicSignals(circuit, circuitInputs)
const endTime = new Date().getTime()
console.log(
`Gen Proof time: ${endTime - startTime} ms (${Math.floor(
(endTime - startTime) / 1000
)} s)`
)
const isValid = await defaultProver.verifyProof(
circuit,
publicSignals,
proof
)
return { isValid, proof, publicSignals }
}
describe('Prove data in Unirep App', function () {
this.timeout(300000)
it('should generate a data proof', async () => {
const id = new Identity()
const epoch = 20
const attesterId = BigInt(219090124810)
const circuitInputs = genCircuitInput({
id,
epoch,
attesterId,
})
const { isValid } = await genProofAndVerify(circuit, circuitInputs)
expect(isValid).to.be.true
})
it('should generate a data proof with values', async () => {
const id = new Identity()
const epoch = 20
const attesterId = BigInt(219090124810)
const sumField = Array(SUM_FIELD_COUNT).fill(5)
const proveValues = Array(SUM_FIELD_COUNT).fill(4)
const circuitInputs = genCircuitInput({
id,
epoch,
attesterId,
sumField,
proveValues,
})
const { isValid } = await genProofAndVerify(circuit, circuitInputs)
expect(isValid).to.be.true
})
it('should not generate a data proof with invalid values', async () => {
const id = new Identity()
const epoch = 20
const attesterId = BigInt(219090124810)
const sumField = Array(SUM_FIELD_COUNT).fill(5)
const proveValues = Array(SUM_FIELD_COUNT).fill(6)
const circuitInputs = genCircuitInput({
id,
epoch,
attesterId,
sumField,
proveValues,
})
await new Promise<void>((rs, rj) => {
genProofAndVerify(circuit, circuitInputs)
.then(() => rj())
.catch(() => rs())
})
})
})

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"exclude": ["node_modules/**"],
"include": ["./src", "./config", "./provers"]
}

View File

@@ -0,0 +1,105 @@
{
"protocol": "groth16",
"curve": "bn128",
"nPublic": 5,
"vk_alpha_1": [
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
"1"
],
"vk_beta_2": [
[
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
],
[
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
],
["1", "0"]
],
"vk_gamma_2": [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
],
["1", "0"]
],
"vk_delta_2": [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
],
["1", "0"]
],
"vk_alphabeta_12": [
[
[
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
],
[
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
],
[
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
]
],
[
[
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
],
[
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
],
[
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
]
]
],
"IC": [
[
"14726110358384591134086760454227211266393232809490471160396967997329870336324",
"6301497693133236182284138855830975407731307849664315530158455800051197105107",
"1"
],
[
"13329857957128329839690880117138325412550294258410191683945258391022632331307",
"15701325130861937333320206471915054307038089909991540806157830619886259981632",
"1"
],
[
"17478358363831002902765013020426580443221917792536599839946876386745318685717",
"10830371275639987859500296749997362406047033916109142385832377177340910597784",
"1"
],
[
"15664464782499262950870691161009364854866354078489291401447836163624272644191",
"19332680862751361917789648482767065641444158897516551791437007516298388954870",
"1"
],
[
"15865221843292426519624482668434972003968238162007413637120806559812532466797",
"7153207475716291327489834715960116524511661268987194669718830099813831489228",
"1"
],
[
"16545623641835559434451166127442830578864858667488981821708517210008152079051",
"2131437898838391121269897774987270826175119692047631536172420890124968652800",
"1"
]
]
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
pragma circom 2.0.0; include "../circuits/dataProof.circom";
component main { public [ value ] } = DataProof(17, 6, 4);

25
packages/contracts/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
node_modules
.env
coverage
coverage.json
typechain
typechain-types
#Hardhat files
cache
artifacts
node_modules
.env
coverage
coverage.json
typechain
typechain-types
#Hardhat files
cache
artifacts
build
*Verifier.sol

View File

@@ -0,0 +1,29 @@
# Sample Unirep Project
This project demonstrates a basic Unirep application. It comes with a sample contract, a test for that contract, and a script that deploys that contract.
Try running some of the following tasks:
## Compile
```shell
yarn build
```
## Test
```shell
yarn test
```
## Deploy to local hardhat network
```shell
yarn hardhat node
```
and
```shell
yarn deploy
```

View File

@@ -0,0 +1,116 @@
[
{
"inputs": [
{
"internalType": "contract Unirep",
"name": "_unirep",
"type": "address"
},
{
"internalType": "contract IVerifier",
"name": "_dataVerifier",
"type": "address"
},
{
"internalType": "uint48",
"name": "_epochLength",
"type": "uint48"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "epochKey",
"type": "uint256"
},
{
"internalType": "uint48",
"name": "targetEpoch",
"type": "uint48"
},
{
"internalType": "uint256",
"name": "fieldIndex",
"type": "uint256"
},
{ "internalType": "uint256", "name": "val", "type": "uint256" }
],
"name": "submitAttestation",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "epochKey",
"type": "uint256"
},
{
"internalType": "uint48",
"name": "targetEpoch",
"type": "uint48"
},
{
"internalType": "uint256[]",
"name": "fieldIndices",
"type": "uint256[]"
},
{ "internalType": "uint256[]", "name": "vals", "type": "uint256[]" }
],
"name": "submitManyAttestations",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "unirep",
"outputs": [
{ "internalType": "contract Unirep", "name": "", "type": "address" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256[]",
"name": "publicSignals",
"type": "uint256[]"
},
{
"internalType": "uint256[8]",
"name": "proof",
"type": "uint256[8]"
}
],
"name": "userSignUp",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256[5]",
"name": "publicSignals",
"type": "uint256[5]"
},
{
"internalType": "uint256[8]",
"name": "proof",
"type": "uint256[8]"
}
],
"name": "verifyDataProof",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
}
]

View File

@@ -0,0 +1,74 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import { Unirep } from "@unirep/contracts/Unirep.sol";
// Uncomment this line to use console.log
// import "hardhat/console.sol";
interface IVerifier {
function verifyProof(
uint256[5] calldata publicSignals,
uint256[8] calldata proof
) external view returns (bool);
}
contract UnirepApp {
Unirep public unirep;
IVerifier internal dataVerifier;
constructor(Unirep _unirep, IVerifier _dataVerifier, uint48 _epochLength) {
// set unirep address
unirep = _unirep;
// set verifier address
dataVerifier = _dataVerifier;
// sign up as an attester
unirep.attesterSignUp(_epochLength);
}
// sign up users in this app
function userSignUp(
uint256[] memory publicSignals,
uint256[8] memory proof
) public {
unirep.userSignUp(publicSignals, proof);
}
function submitManyAttestations(
uint256 epochKey,
uint48 targetEpoch,
uint[] calldata fieldIndices,
uint[] calldata vals
) public {
require(fieldIndices.length == vals.length, 'arrmismatch');
for (uint8 x = 0; x < fieldIndices.length; x++) {
unirep.attest(epochKey, targetEpoch, fieldIndices[x], vals[x]);
}
}
function submitAttestation(
uint256 epochKey,
uint48 targetEpoch,
uint256 fieldIndex,
uint256 val
) public {
unirep.attest(
epochKey,
targetEpoch,
fieldIndex,
val
);
}
function verifyDataProof(
uint256[5] calldata publicSignals,
uint256[8] calldata proof
) public view returns(bool) {
return dataVerifier.verifyProof(
publicSignals,
proof
);
}
}

View File

@@ -0,0 +1,34 @@
import '@typechain/hardhat'
import '@nomiclabs/hardhat-ethers'
export default {
defaultNetwork: 'local',
networks: {
hardhat: {
blockGasLimit: 12000000,
},
local: {
url: 'http://127.0.0.1:8545',
blockGasLimit: 12000000,
accounts: [
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
],
},
arb: {
url: 'https://arbitrum.goerli.unirep.io',
accounts: [
'0x0f70e777f814334daa4456ac32b9a1fdca75ae07f70c2e6cef92679bad06c88b',
],
},
},
solidity: {
compilers: [
{
version: '0.8.17',
settings: {
optimizer: { enabled: true, runs: 200 },
},
},
],
},
}

View File

@@ -0,0 +1,39 @@
{
"name": "@unirep-app/contracts",
"version": "1.0.0",
"description": "Smart contracts of the Unirep Application",
"keywords": [],
"author": "Unirep Team <team@unirep.io>",
"license": "ISC",
"main": "build/src/index.js",
"repository": "git+https://github.com/Unirep/create-unirep-app.git",
"scripts": {
"build": "yarn buildVerifier && hardhat compile && yarn abi",
"buildVerifier": "ts-node ./scripts/genVerifier.ts ",
"abi": "ts-node scripts/abi",
"hardhat": "hardhat",
"deploy": "hardhat run scripts/deploy.ts",
"test": "hardhat test --network hardhat"
},
"bugs": {
"url": "https://github.com/Unirep/create-unirep-app/issue"
},
"homepage": "https://github.com/Unirep/create-unirep-app#readme",
"devDependencies": {
"@semaphore-protocol/identity": "^3.6.0",
"@nomiclabs/hardhat-ethers": "^2.2.0",
"@openzeppelin/contracts": "^4.7.3",
"@typechain/ethers-v5": "^10.2.0",
"@typechain/hardhat": "^6.1.5",
"@types/node": "^18.15.11",
"@unirep-app/circuits": "1.0.0",
"@unirep/contracts": "2.0.0-beta-3",
"hardhat": "^2.12.0",
"ts-node": "^10.9.1",
"typechain": "^8.1.1",
"typescript": "^5.0.3"
},
"dependencies": {
"poseidon-solidity": "^0.0.3"
}
}

View File

@@ -0,0 +1,8 @@
import * as fs from 'fs'
import * as path from 'path'
import UNIREP_APP_ABI from '../artifacts/contracts/UnirepApp.sol/UnirepApp.json'
fs.writeFileSync(
path.join(__dirname, '../abi/UnirepApp.json'),
JSON.stringify(UNIREP_APP_ABI.abi)
)

View File

@@ -0,0 +1,48 @@
import { ethers } from 'hardhat'
import * as fs from 'fs'
import * as path from 'path'
import { deployUnirep } from '@unirep/contracts/deploy/index.js'
import * as hardhat from 'hardhat'
const epochLength = 300
deployApp().catch((err) => {
console.log(`Uncaught error: ${err}`)
process.exit(1)
})
export async function deployApp() {
const [signer] = await ethers.getSigners()
const unirep = await deployUnirep(signer)
const verifierF = await ethers.getContractFactory('DataProofVerifier')
const verifier = await verifierF.deploy()
await verifier.deployed()
const App = await ethers.getContractFactory('UnirepApp')
const app = await App.deploy(unirep.address, verifier.address, epochLength)
await app.deployed()
console.log(
`Unirep app with epoch length ${epochLength} is deployed to ${app.address}`
)
const config = `export default {
UNIREP_ADDRESS: '${unirep.address}',
APP_ADDRESS: '${app.address}',
ETH_PROVIDER_URL: '${hardhat.network.config.url ?? ''}',
${
Array.isArray(hardhat.network.config.accounts)
? `PRIVATE_KEY: '${hardhat.network.config.accounts[0]}',`
: `/**
This contract was deployed using a mnemonic. The PRIVATE_KEY variable needs to be set manually
**/`
}
}
`
const configPath = path.join(__dirname, '../../../config.ts')
await fs.promises.writeFile(configPath, config)
console.log(`Config written to ${configPath}`)
}

View File

@@ -0,0 +1,105 @@
import path from 'path'
import fs from 'fs'
import { config } from 'hardhat'
const verifiersPath = path.join(config.paths.sources)
const zkFilesPath = path.join('../../circuits/zksnarkBuild')
const Circuit = {
dataProof: 'dataProof',
}
const main = async (): Promise<number> => {
// create verifier folder
try {
fs.mkdirSync(verifiersPath, { recursive: true })
} catch (e) {
console.log('Cannot create folder ', e)
}
for (const circuit of Object.keys(Circuit)) {
const verifierName = createVerifierName(circuit)
const solOut = path.join(verifiersPath, `${verifierName}.sol`)
const vKey = require(path.join(zkFilesPath, `${circuit}.vkey.json`))
console.log(`Exporting ${circuit} verification contract...`)
const verifier = genVerifier(verifierName, vKey)
fs.writeFileSync(solOut, verifier)
fs.copyFileSync(solOut, path.join(verifiersPath, `${verifierName}.sol`))
}
return 0
}
export const createVerifierName = (circuit: string) => {
return `${circuit.charAt(0).toUpperCase() + circuit.slice(1)}Verifier`
}
export const genVerifier = (contractName: string, vk: any): string => {
const templatePath = path.resolve(
__dirname,
'./template/groth16Verifier.txt'
)
let template = fs.readFileSync(templatePath, 'utf8')
template = template.replace('<%contract_name%>', contractName)
const vkalpha1 =
`uint256(${vk.vk_alpha_1[0].toString()}),` +
`uint256(${vk.vk_alpha_1[1].toString()})`
template = template.replace('<%vk_alpha1%>', vkalpha1)
const vkbeta2 =
`[uint256(${vk.vk_beta_2[0][1].toString()}),` +
`uint256(${vk.vk_beta_2[0][0].toString()})], ` +
`[uint256(${vk.vk_beta_2[1][1].toString()}),` +
`uint256(${vk.vk_beta_2[1][0].toString()})]`
template = template.replace('<%vk_beta2%>', vkbeta2)
const vkgamma2 =
`[uint256(${vk.vk_gamma_2[0][1].toString()}),` +
`uint256(${vk.vk_gamma_2[0][0].toString()})], ` +
`[uint256(${vk.vk_gamma_2[1][1].toString()}),` +
`uint256(${vk.vk_gamma_2[1][0].toString()})]`
template = template.replace('<%vk_gamma2%>', vkgamma2)
const vkdelta2 =
`[uint256(${vk.vk_delta_2[0][1].toString()}),` +
`uint256(${vk.vk_delta_2[0][0].toString()})], ` +
`[uint256(${vk.vk_delta_2[1][1].toString()}),` +
`uint256(${vk.vk_delta_2[1][0].toString()})]`
template = template.replace('<%vk_delta2%>', vkdelta2)
template = template.replaceAll(
'<%vk_input_length%>',
(vk.IC.length - 1).toString()
)
template = template.replace('<%vk_ic_length%>', vk.IC.length.toString())
let vi = ''
for (let i = 0; i < vk.IC.length; i++) {
if (vi.length !== 0) {
vi = vi + ' '
}
vi =
vi +
`vk.IC[${i}] = Pairing.G1Point(uint256(${vk.IC[
i
][0].toString()}),` +
`uint256(${vk.IC[i][1].toString()}));\n`
}
template = template.replace('<%vk_ic_pts%>', vi)
return template
}
void (async () => {
let exitCode
try {
exitCode = await main()
} catch (err) {
console.error(err)
exitCode = 1
}
process.exit(exitCode)
})()

View File

@@ -0,0 +1,225 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library Pairing {
uint256 constant PRIME_Q =
21888242871839275222246405745257275088696311157297823662689037894645226208583;
struct G1Point {
uint256 X;
uint256 Y;
}
// Encoding of field elements is: X[0] * z + X[1]
struct G2Point {
uint256[2] X;
uint256[2] Y;
}
/*
* @return The negation of p, i.e. p.plus(p.negate()) should be zero.
*/
function negate(G1Point memory p) internal pure returns (G1Point memory) {
// The prime q in the base field F_q for G1
if (p.X == 0 && p.Y == 0) {
return G1Point(0, 0);
} else {
return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q));
}
}
/*
* @return The sum of two points of G1
*/
function plus(G1Point memory p1, G1Point memory p2)
internal
view
returns (G1Point memory r)
{
uint256[4] memory input;
input[0] = p1.X;
input[1] = p1.Y;
input[2] = p2.X;
input[3] = p2.Y;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60)
// Use "invalid" to make gas estimation work
switch success
case 0 {
invalid()
}
}
require(success, 'pairing-add-failed');
}
/*
* @return The product of a point on G1 and a scalar, i.e.
* p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all
* points p.
*/
function scalar_mul(G1Point memory p, uint256 s)
internal
view
returns (G1Point memory r)
{
uint256[3] memory input;
input[0] = p.X;
input[1] = p.Y;
input[2] = s;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60)
// Use "invalid" to make gas estimation work
switch success
case 0 {
invalid()
}
}
require(success, 'pairing-mul-failed');
}
/* @return The result of computing the pairing check
* e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1
* For example,
* pairing([P1(), P1().negate()], [P2(), P2()]) should return true.
*/
function pairing(
G1Point memory a1,
G2Point memory a2,
G1Point memory b1,
G2Point memory b2,
G1Point memory c1,
G2Point memory c2,
G1Point memory d1,
G2Point memory d2
) internal view returns (bool) {
G1Point[4] memory p1 = [a1, b1, c1, d1];
G2Point[4] memory p2 = [a2, b2, c2, d2];
uint256 inputSize = 24;
uint256[] memory input = new uint256[](inputSize);
for (uint256 i = 0; i < 4; i++) {
uint256 j = i * 6;
input[j + 0] = p1[i].X;
input[j + 1] = p1[i].Y;
input[j + 2] = p2[i].X[0];
input[j + 3] = p2[i].X[1];
input[j + 4] = p2[i].Y[0];
input[j + 5] = p2[i].Y[1];
}
uint256[1] memory out;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(
sub(gas(), 2000),
8,
add(input, 0x20),
mul(inputSize, 0x20),
out,
0x20
)
// Use "invalid" to make gas estimation work
switch success
case 0 {
invalid()
}
}
require(success, 'pairing-opcode-failed');
return out[0] != 0;
}
}
contract <%contract_name%> {
using Pairing for *;
uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
struct VerifyingKey {
Pairing.G1Point alpha1;
Pairing.G2Point beta2;
Pairing.G2Point gamma2;
Pairing.G2Point delta2;
Pairing.G1Point[<%vk_ic_length%>] IC;
}
struct Proof {
Pairing.G1Point A;
Pairing.G2Point B;
Pairing.G1Point C;
}
function verifyingKey() internal pure returns (VerifyingKey memory vk) {
vk.alpha1 = Pairing.G1Point(<%vk_alpha1%>);
vk.beta2 = Pairing.G2Point(<%vk_beta2%>);
vk.gamma2 = Pairing.G2Point(<%vk_gamma2%>);
vk.delta2 = Pairing.G2Point(<%vk_delta2%>);
<%vk_ic_pts%>
}
/*
* @returns Whether the proof is valid given the hardcoded verifying key
* above and the public inputs
*/
function verifyProof(
uint256[<%vk_input_length%>] calldata input,
uint256[8] calldata _proof
) public view returns (bool) {
Proof memory proof;
proof.A = Pairing.G1Point(_proof[0], _proof[1]);
proof.B = Pairing.G2Point([_proof[2], _proof[3]], [_proof[4], _proof[5]]);
proof.C = Pairing.G1Point(_proof[6], _proof[7]);
VerifyingKey memory vk = verifyingKey();
// Compute the linear combination vk_x
Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
// Make sure that proof.A, B, and C are each less than the prime q
require(proof.A.X < PRIME_Q, "verifier-aX-gte-prime-q");
require(proof.A.Y < PRIME_Q, "verifier-aY-gte-prime-q");
require(proof.B.X[0] < PRIME_Q, "verifier-bX0-gte-prime-q");
require(proof.B.Y[0] < PRIME_Q, "verifier-bY0-gte-prime-q");
require(proof.B.X[1] < PRIME_Q, "verifier-bX1-gte-prime-q");
require(proof.B.Y[1] < PRIME_Q, "verifier-bY1-gte-prime-q");
require(proof.C.X < PRIME_Q, "verifier-cX-gte-prime-q");
require(proof.C.Y < PRIME_Q, "verifier-cY-gte-prime-q");
// Make sure that every input is less than the snark scalar field
//for (uint256 i = 0; i < input.length; i++) {
for (uint256 i = 0; i < <%vk_input_length%>; i++) {
require(input[i] < SNARK_SCALAR_FIELD,"verifier-gte-snark-scalar-field");
vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i]));
}
vk_x = Pairing.plus(vk_x, vk.IC[0]);
return Pairing.pairing(
Pairing.negate(proof.A),
proof.B,
vk.alpha1,
vk.beta2,
vk_x,
vk.gamma2,
proof.C,
vk.delta2
);
}
}

View File

@@ -0,0 +1,127 @@
//@ts-ignore
import { ethers } from 'hardhat'
import { expect } from 'chai'
import { deployUnirep } from '@unirep/contracts/deploy'
import { stringifyBigInts } from '@unirep/utils'
import { schema, UserState } from '@unirep/core'
import { SQLiteConnector } from 'anondb/node'
import { DataProof } from '@unirep-app/circuits'
import defaultConfig from '@unirep/circuits/config'
import { Identity } from '@semaphore-protocol/identity'
const { SUM_FIELD_COUNT } = defaultConfig
import { defaultProver as prover } from '@unirep-app/circuits/provers/defaultProver'
async function genUserState(id, app) {
// generate a user state
const db = await SQLiteConnector.create(schema, ':memory:')
const unirepAddress = await app.unirep()
const attesterId = BigInt(app.address)
const userState = new UserState(
{
db,
prover,
unirepAddress,
provider: ethers.provider,
attesterId,
},
id
)
await userState.sync.start()
await userState.waitForSync()
return userState
}
describe('Unirep App', function () {
let unirep
let app
// epoch length
const epochLength = 300
// generate random user id
const id = new Identity()
it('deployment', async function () {
const [deployer] = await ethers.getSigners()
unirep = await deployUnirep(deployer)
const verifierF = await ethers.getContractFactory('DataProofVerifier')
const verifier = await verifierF.deploy()
await verifier.deployed()
const App = await ethers.getContractFactory('UnirepApp')
app = await App.deploy(unirep.address, verifier.address, epochLength)
await app.deployed()
})
it('user sign up', async () => {
const userState = await genUserState(id, app)
// generate
const { publicSignals, proof } = await userState.genUserSignUpProof()
await app.userSignUp(publicSignals, proof).then((t) => t.wait())
userState.sync.stop()
})
it('submit attestations', async () => {
const userState = await genUserState(id, app)
const nonce = 0
const { publicSignals, proof, epochKey, epoch } =
await userState.genEpochKeyProof({ nonce })
await unirep
.verifyEpochKeyProof(publicSignals, proof)
.then((t) => t.wait())
const field = 0
const val = 10
await app
.submitAttestation(epochKey, epoch, field, val)
.then((t) => t.wait())
userState.sync.stop()
})
it('user state transition', async () => {
await ethers.provider.send('evm_increaseTime', [epochLength])
await ethers.provider.send('evm_mine', [])
const newEpoch = await unirep.attesterCurrentEpoch(app.address)
const userState = await genUserState(id, app)
const { publicSignals, proof } =
await userState.genUserStateTransitionProof({
toEpoch: newEpoch,
})
await unirep
.userStateTransition(publicSignals, proof)
.then((t) => t.wait())
userState.sync.stop()
})
it('data proof', async () => {
const userState = await genUserState(id, app)
const epoch = await userState.sync.loadCurrentEpoch()
const stateTree = await userState.sync.genStateTree(epoch)
const index = await userState.latestStateTreeLeafIndex(epoch)
const stateTreeProof = stateTree.createProof(index)
const attesterId = app.address
const data = await userState.getProvableData()
const value = Array(SUM_FIELD_COUNT).fill(0)
const circuitInputs = stringifyBigInts({
identity_secret: id.secret,
state_tree_indexes: stateTreeProof.pathIndices,
state_tree_elements: stateTreeProof.siblings,
data: data,
epoch: epoch,
attester_id: attesterId,
value: value,
})
const p = await prover.genProofAndPublicSignals(
'dataProof',
circuitInputs
)
const dataProof = new DataProof(p.publicSignals, p.proof, prover)
const isValid = await app.verifyDataProof(
dataProof.publicSignals,
dataProof.proof
)
expect(isValid).to.be.true
userState.sync.stop()
})
})

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./build",
"lib": ["ES2021.String"]
},
"include": ["./src", "./deploy", "./typechain"],
"files": ["./hardhat.config.ts"]
}

1
packages/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

View File

@@ -0,0 +1,11 @@
# `frontend`
> TODO: description
## Usage
```
const frontend = require('frontend');
// TODO: DEMONSTRATE API
```

1
packages/frontend/externals/buffer.js vendored Normal file
View File

@@ -0,0 +1 @@
module.exports = require('buffer/').Buffer

View File

@@ -0,0 +1,54 @@
{
"name": "@unirep-app/frontend",
"version": "1.0.0",
"description": "> TODO: description",
"author": "Unirep team <team@unirep.io>",
"homepage": "https://github.com/Unirep/create-unirep-app#readme",
"license": "ISC",
"repository": {
"type": "git",
"url": "git+https://github.com/Unirep/create-unirep-app.git"
},
"scripts": {
"start": "webpack-dev-server",
"build": "webpack"
},
"dependencies": {
"@cloudflare/kv-asset-handler": "^0.2.0",
"@unirep/core": "2.0.0-beta-3",
"@unirep-app/circuits": "1.0.0",
"ethers": "^5.7.2",
"file-loader": "^6.2.0",
"mobx-react-lite": "^3.4.0"
},
"devDependencies": {
"@semaphore-protocol/identity": "^3.6.0",
"@babel/core": "^7.19.6",
"@babel/preset-react": "^7.18.6",
"@types/react": "^18.0.32",
"@types/react-dom": "^18.0.11",
"@types/react-router-dom": "^5.3.3",
"anondb": "^0.0.16",
"assert": "^2.0.0",
"babel-loader": "^8.2.5",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.1",
"events": "^3.3.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.6.1",
"mobx": "^6.6.2",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.2",
"snarkjs": "^0.5.0",
"stream-browserify": "^3.0.0",
"ts-loader": "^9.4.2",
"typescript": "^5.0.3",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}

View File

@@ -0,0 +1,3 @@
<svg width="26" height="10" viewBox="0 0 26 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.4714 5.47141C25.7318 5.21106 25.7318 4.78895 25.4714 4.5286L21.2288 0.28596C20.9684 0.0256108 20.5463 0.0256108 20.286 0.28596C20.0256 0.54631 20.0256 0.96842 20.286 1.22877L23.3905 4.33334L1.00002 4.33334C0.631832 4.33334 0.333355 4.63182 0.333355 5.00001C0.333355 5.3682 0.631832 5.66667 1.00002 5.66667L23.3905 5.66667L20.286 8.77124C20.0256 9.03159 20.0256 9.4537 20.286 9.71405C20.5463 9.9744 20.9684 9.9744 21.2288 9.71405L25.4714 5.47141Z" fill="#151616"/>
</svg>

After

Width:  |  Height:  |  Size: 619 B

View File

@@ -0,0 +1,80 @@
<svg width="612" height="911" viewBox="0 0 612 911" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_512_1372)">
<path opacity="0.5" d="M15 361C15 179.851 161.851 33 343 33C524.149 33 671 179.851 671 361V911H15V361Z" fill="url(#paint0_linear_512_1372)"/>
<path d="M487.255 678.906C487.255 678.906 482.829 608.209 436.532 567.775C390.235 527.341 332.702 517.474 319.6 532.483C306.497 547.492 323.291 604.023 369.588 644.457C415.885 684.891 487.255 678.906 487.255 678.906Z" fill="url(#paint1_linear_512_1372)"/>
<path d="M554.291 493.051C554.291 493.051 487.408 452.92 421.827 469.676C356.246 486.433 311.37 532.47 316.8 553.731C322.23 574.991 383.988 595.055 449.57 578.299C515.151 561.543 554.291 493.051 554.291 493.051Z" fill="url(#paint2_linear_512_1372)"/>
<path d="M160.908 575.309C160.908 575.309 159.243 628.508 191.03 662.015C222.817 695.522 265.161 706.955 276.019 696.65C286.878 686.345 278.294 642.873 246.507 609.366C214.72 575.858 160.908 575.309 160.908 575.309Z" fill="url(#paint3_linear_512_1372)"/>
<path d="M401.024 443.996C395.24 447.378 386.987 453.434 377.397 462.247M283.505 918.144C281.29 906.964 276.434 847.375 276.421 778.27M377.397 462.247C355.105 482.732 325.589 518.106 303.06 569.376C282.098 617.078 276.407 703.277 276.421 778.27M377.397 462.247L409.5 457.787M276.421 778.27L264.757 757.062" stroke="url(#paint4_linear_512_1372)" stroke-width="8" stroke-linecap="round"/>
<path d="M283.087 582.39C283.087 582.39 262.198 620.979 273.24 657.425C284.281 693.871 311.261 717.952 323.072 714.373C334.882 710.793 344.623 675.583 333.582 639.137C322.54 602.69 283.087 582.39 283.087 582.39Z" fill="url(#paint5_linear_512_1372)"/>
<path d="M521.793 584.19C521.793 584.19 480.42 559.229 439.769 569.51C399.117 579.792 371.248 608.244 374.58 621.423C377.912 634.602 416.14 647.124 456.792 636.842C497.443 626.561 521.793 584.19 521.793 584.19Z" fill="url(#paint6_linear_512_1372)"/>
<path d="M199.699 661.967C199.699 661.967 217.098 702.484 252.706 716.488C288.313 730.492 324.106 724.224 328.644 712.68C333.182 701.137 311.499 671.515 275.892 657.511C240.285 643.507 199.699 661.967 199.699 661.967Z" fill="url(#paint7_linear_512_1372)"/>
<path d="M277.371 931.889C275.369 921.652 299.578 850.093 299.72 786.848M373.596 517.635C374.695 550.525 383.495 617.869 362.762 664.741C343.472 708.352 299.873 718.214 299.72 786.848M299.72 786.848L290.148 799.037" stroke="url(#paint8_linear_512_1372)" stroke-width="8" stroke-linecap="round"/>
<path d="M542.467 265.311C544.119 264.194 545.641 263.177 547.007 262.276C545.629 262.613 544.104 263.664 542.467 265.311C525.767 276.607 495.82 298.163 480.633 313.844C459.771 335.385 456.009 355.777 466.418 359.244C474.745 362.018 491.502 360.091 498.84 358.781C508.473 331.279 529.173 278.683 542.467 265.311Z" fill="url(#paint9_linear_512_1372)"/>
<path d="M402.369 329.626C394.541 331.226 386.27 341.055 383.113 345.769C401.418 336.063 421.708 336.112 430.906 341.654C440.103 347.195 444.793 370.756 455.794 385.501C466.795 400.246 576.267 421.117 546.06 411.477C515.852 401.837 507.077 384.521 495.381 360.208C483.685 335.894 473.024 327.773 458.317 316.699C443.611 305.625 429.574 309.638 422.171 315.14C414.768 320.641 412.154 327.626 402.369 329.626Z" fill="url(#paint10_linear_512_1372)"/>
<path d="M564.04 289.929C565.916 289.254 567.643 288.645 569.188 288.109C567.77 288.095 566.033 288.738 564.04 289.929C545.068 296.751 510.727 310.245 492.139 321.69C466.607 337.412 457.927 356.244 467.158 362.174C474.542 366.918 491.257 369.189 498.691 369.732C514.814 345.46 547.857 299.604 564.04 289.929Z" fill="url(#paint11_linear_512_1372)"/>
<path d="M504.954 406.902C494.462 396.781 488.59 377.717 486.966 369.451C492.468 374.239 508.216 386.212 527.198 395.8C546.179 405.387 578.188 404.772 591.82 403.266C586.44 404.567 572.313 407.808 558.838 410.363C545.364 412.918 529.251 417.756 522.879 419.856C521.276 419.756 515.447 417.024 504.954 406.902Z" fill="url(#paint12_linear_512_1372)"/>
</g>
<defs>
<linearGradient id="paint0_linear_512_1372" x1="343" y1="33" x2="343" y2="911" gradientUnits="userSpaceOnUse">
<stop stop-color="#95D5D9"/>
<stop offset="1" stop-color="#A1D9DC" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_512_1372" x1="257" y1="443" x2="484.454" y2="676.545" gradientUnits="userSpaceOnUse">
<stop offset="0.503801" stop-color="#99D7DA"/>
<stop offset="0.961746" stop-color="#99D7DA" stop-opacity="0"/>
<stop offset="1" stop-color="#99D7DA" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_512_1372" x1="240.5" y1="554" x2="553.494" y2="493.97" gradientUnits="userSpaceOnUse">
<stop offset="0.503801" stop-color="#99D7DA"/>
<stop offset="0.961746" stop-color="#99D7DA" stop-opacity="0"/>
<stop offset="1" stop-color="#99D7DA" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_512_1372" x1="414" y1="824" x2="158.548" y2="578.95" gradientUnits="userSpaceOnUse">
<stop offset="0.503801" stop-color="#99D7DA"/>
<stop offset="0.961746" stop-color="#99D7DA" stop-opacity="0"/>
<stop offset="1" stop-color="#99D7DA" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint4_linear_512_1372" x1="399.078" y1="442.476" x2="269.254" y2="919.119" gradientUnits="userSpaceOnUse">
<stop stop-color="#9BD8DB"/>
<stop offset="1" stop-color="#9BD8DB" stop-opacity="0.78"/>
</linearGradient>
<linearGradient id="paint5_linear_512_1372" x1="354" y1="758.5" x2="284.52" y2="577.992" gradientUnits="userSpaceOnUse">
<stop offset="0.503801" stop-color="#99D7DA"/>
<stop offset="0.961746" stop-color="#99D7DA" stop-opacity="0"/>
<stop offset="1" stop-color="#99D7DA" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint6_linear_512_1372" x1="241.5" y1="636.5" x2="520.995" y2="578.974" gradientUnits="userSpaceOnUse">
<stop offset="0.503801" stop-color="#99D7DA"/>
<stop offset="0.961746" stop-color="#99D7DA" stop-opacity="0"/>
<stop offset="1" stop-color="#99D7DA" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint7_linear_512_1372" x1="325" y1="722" x2="188.009" y2="651.482" gradientUnits="userSpaceOnUse">
<stop offset="0.503801" stop-color="#99D7DA"/>
<stop offset="0.961746" stop-color="#99D7DA" stop-opacity="0"/>
<stop offset="1" stop-color="#99D7DA" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint8_linear_512_1372" x1="413.778" y1="511.425" x2="293.903" y2="947.362" gradientUnits="userSpaceOnUse">
<stop stop-color="#9BD8DB"/>
<stop offset="1" stop-color="#9BD8DB" stop-opacity="0.78"/>
</linearGradient>
<linearGradient id="paint9_linear_512_1372" x1="503.296" y1="262.9" x2="504.688" y2="360.294" gradientUnits="userSpaceOnUse">
<stop stop-color="#98D9DC"/>
<stop offset="1" stop-color="#98D9DC" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint10_linear_512_1372" x1="409.048" y1="320.746" x2="512.298" y2="410.683" gradientUnits="userSpaceOnUse">
<stop stop-color="#98D9DC"/>
<stop offset="1" stop-color="#98D9DC" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint11_linear_512_1372" x1="526.677" y1="277.92" x2="503.975" y2="372.64" gradientUnits="userSpaceOnUse">
<stop stop-color="#98D9DC"/>
<stop offset="1" stop-color="#98D9DC" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint12_linear_512_1372" x1="486.408" y1="369.635" x2="549.282" y2="417.276" gradientUnits="userSpaceOnUse">
<stop stop-color="#98D9DC"/>
<stop offset="1" stop-color="#98D9DC" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_512_1372">
<rect width="678" height="911" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="unirep app" content="" />
<link rel="apple-touch-icon" href="logo192.png" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Azeret+Mono:wght@200..700&display=swap"
/>
<title>unirep app</title>
<script>
window.__DEV_CONFIG__ = '__DEV_CONFIG__'
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
--></body>
</html>

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20C4.477 20 0 15.523 0 10C0 4.477 4.477 0 10 0C15.523 0 20 4.477 20 10C20 15.523 15.523 20 10 20ZM10 18C12.1217 18 14.1566 17.1571 15.6569 15.6569C17.1571 14.1566 18 12.1217 18 10C18 7.87827 17.1571 5.84344 15.6569 4.34315C14.1566 2.84285 12.1217 2 10 2C7.87827 2 5.84344 2.84285 4.34315 4.34315C2.84285 5.84344 2 7.87827 2 10C2 12.1217 2.84285 14.1566 4.34315 15.6569C5.84344 17.1571 7.87827 18 10 18ZM9 5H11V7H9V5ZM9 9H11V15H9V9Z" fill="#151616"/>
</svg>

After

Width:  |  Height:  |  Size: 565 B

View File

@@ -0,0 +1,25 @@
<svg width="185" height="48" viewBox="0 0 185 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_512_1287)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.2 0C0.537258 0 0 0.537257 0 1.2V29.9294C0 39.9095 8.09048 48 18.0706 48C18.6943 48 19.2 47.4943 19.2 46.8706V1.2C19.2 0.537258 18.6627 0 18 0H1.2ZM9.6 2.4C9.6 6.37645 6.37645 9.6 2.4 9.6C6.37645 9.6 9.6 12.8236 9.6 16.8C9.6 12.8236 12.8235 9.6 16.8 9.6C12.8235 9.6 9.6 6.37645 9.6 2.4Z" fill="#A0D8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.8 48C22.1372 48 21.6 47.4627 21.6 46.8L21.6 18.0706C21.6 8.09048 29.6905 8.72489e-07 39.6706 0C40.2943 -5.45306e-08 40.8 0.505655 40.8 1.12941L40.8 46.8C40.8 47.4627 40.2627 48 39.6 48H22.8ZM31.2 45.6C31.2 41.6236 27.9764 38.4 24 38.4C27.9764 38.4 31.2 35.1765 31.2 31.2C31.2 35.1765 34.4235 38.4 38.4 38.4C34.4235 38.4 31.2 41.6236 31.2 45.6Z" fill="#A0D8DB"/>
</g>
<path d="M57.984 26.72C56.0427 26.72 54.3893 26.3787 53.024 25.696C51.6587 24.992 50.6133 24.0107 49.888 22.752C49.1627 21.4933 48.8 20.0213 48.8 18.336V4H54.08V18.336C54.08 19.168 54.24 19.8827 54.56 20.48C54.88 21.0773 55.328 21.5467 55.904 21.888C56.5013 22.208 57.1947 22.368 57.984 22.368C58.7947 22.368 59.488 22.208 60.064 21.888C60.64 21.5467 61.088 21.0773 61.408 20.48C61.728 19.8827 61.888 19.168 61.888 18.336V4H67.168V18.336C67.168 20.0213 66.8053 21.4933 66.08 22.752C65.3547 24.0107 64.3093 24.992 62.944 25.696C61.5787 26.3787 59.9253 26.72 57.984 26.72Z" fill="#151616"/>
<path d="M72.5885 26.336V4H79.3085L85.5485 19.808L85.1965 11.68V4H90.1565V26.336H83.4365L77.1325 10.816L77.5485 18.656V26.336H72.5885Z" fill="#151616"/>
<path d="M102.073 26.336V4H107.353V26.336H102.073ZM96.953 4H112.473V8.32H96.953V4ZM96.953 22.016H112.473V26.336H96.953V22.016Z" fill="#151616"/>
<path d="M123.493 17.984V13.664H128.517C129.157 13.664 129.712 13.5467 130.181 13.312C130.672 13.0773 131.045 12.7467 131.301 12.32C131.579 11.8933 131.717 11.4133 131.717 10.88C131.717 10.368 131.589 9.92 131.333 9.536C131.099 9.152 130.768 8.85333 130.341 8.64C129.915 8.42667 129.424 8.32 128.869 8.32H123.493V4H128.869C130.491 4 131.92 4.27733 133.157 4.832C134.416 5.36533 135.397 6.144 136.101 7.168C136.805 8.192 137.157 9.42933 137.157 10.88C137.157 11.7547 136.944 12.6187 136.517 13.472C136.112 14.304 135.515 15.0613 134.725 15.744C133.957 16.4267 133.019 16.9707 131.909 17.376C130.821 17.7813 129.584 17.984 128.197 17.984H123.493ZM119.685 26.336V4H124.965V26.336H119.685ZM131.845 26.336C131.845 25.3973 131.675 24.448 131.333 23.488C130.992 22.5067 130.491 21.5787 129.829 20.704C129.189 19.808 128.389 18.9973 127.429 18.272C126.469 17.5467 125.36 16.96 124.101 16.512L129.957 15.328C131.195 15.9467 132.272 16.6827 133.189 17.536C134.107 18.3893 134.864 19.3067 135.461 20.288C136.08 21.2693 136.539 22.2827 136.837 23.328C137.136 24.352 137.285 25.3547 137.285 26.336H131.845Z" fill="#151616"/>
<path d="M143.41 26.336V4H159.954V8.32H148.69V12.64H155.954V16.96H148.69V22.016H160.082V26.336H143.41Z" fill="#151616"/>
<path d="M170.142 19.328V15.008H175.23C176.254 15.008 177.076 14.6987 177.694 14.08C178.313 13.44 178.622 12.608 178.622 11.584C178.622 10.6027 178.313 9.81333 177.694 9.216C177.076 8.61867 176.254 8.32 175.23 8.32H170.142V4H175.262C177.033 4 178.58 4.30933 179.902 4.928C181.225 5.54667 182.249 6.432 182.974 7.584C183.7 8.71467 184.062 10.048 184.062 11.584C184.062 13.1413 183.7 14.5067 182.974 15.68C182.249 16.832 181.225 17.728 179.902 18.368C178.601 19.008 177.054 19.328 175.262 19.328H170.142ZM166.366 26.336V4H171.646V26.336H166.366Z" fill="#151616"/>
<path d="M50.584 38.996V37.496H54.384C55.3573 37.496 56.144 37.236 56.744 36.716C57.344 36.196 57.644 35.5093 57.644 34.656C57.644 33.816 57.364 33.1427 56.804 32.636C56.2573 32.1293 55.5307 31.876 54.624 31.876H50.584V30.376H54.644C55.6173 30.376 56.464 30.556 57.184 30.916C57.904 31.2627 58.464 31.756 58.864 32.396C59.264 33.0227 59.464 33.776 59.464 34.656C59.464 35.5227 59.2507 36.2827 58.824 36.936C58.4107 37.5893 57.824 38.096 57.064 38.456C56.304 38.816 55.4107 38.996 54.384 38.996H50.584ZM49.344 44.336V30.376H51.064V44.336H49.344Z" fill="#151616"/>
<path d="M68.4718 38.296V36.796H72.1318C72.7851 36.796 73.3585 36.696 73.8518 36.496C74.3585 36.2827 74.7518 35.9827 75.0318 35.596C75.3251 35.196 75.4718 34.736 75.4718 34.216C75.4718 33.736 75.3451 33.3227 75.0918 32.976C74.8385 32.6293 74.4985 32.3627 74.0718 32.176C73.6451 31.976 73.1718 31.876 72.6518 31.876H68.4718V30.376H72.6518C73.5451 30.376 74.3385 30.5293 75.0318 30.836C75.7385 31.1427 76.2918 31.5827 76.6918 32.156C77.0918 32.7293 77.2918 33.416 77.2918 34.216C77.2918 34.7493 77.1718 35.2627 76.9318 35.756C76.7051 36.236 76.3651 36.6693 75.9118 37.056C75.4585 37.4293 74.9118 37.7293 74.2718 37.956C73.6318 38.1827 72.8985 38.296 72.0718 38.296H68.4718ZM67.2318 44.336V30.376H68.9518V44.336H67.2318ZM75.3918 44.336C75.2851 43.696 75.0918 43.0693 74.8118 42.456C74.5451 41.8427 74.1851 41.256 73.7318 40.696C73.2918 40.136 72.7385 39.6093 72.0718 39.116C71.4051 38.6227 70.6318 38.1693 69.7518 37.756L71.6718 37.316C72.6051 37.7693 73.3985 38.2693 74.0518 38.816C74.7051 39.3493 75.2451 39.916 75.6718 40.516C76.1118 41.1027 76.4451 41.716 76.6718 42.356C76.8985 42.996 77.0518 43.656 77.1318 44.336H75.3918Z" fill="#151616"/>
<path d="M89.7196 44.576C88.5463 44.576 87.5263 44.2827 86.6596 43.696C85.793 43.096 85.1196 42.256 84.6396 41.176C84.173 40.0827 83.9396 38.8093 83.9396 37.356C83.9396 35.8893 84.173 34.616 84.6396 33.536C85.1196 32.456 85.793 31.6227 86.6596 31.036C87.5263 30.436 88.5463 30.136 89.7196 30.136C90.893 30.136 91.913 30.436 92.7796 31.036C93.6463 31.6227 94.313 32.456 94.7796 33.536C95.2596 34.616 95.4996 35.8893 95.4996 37.356C95.4996 38.8093 95.2596 40.0827 94.7796 41.176C94.313 42.256 93.6463 43.096 92.7796 43.696C91.913 44.2827 90.893 44.576 89.7196 44.576ZM89.7196 43.076C90.5196 43.076 91.213 42.8427 91.7996 42.376C92.3996 41.9093 92.8596 41.2493 93.1796 40.396C93.513 39.5293 93.6796 38.516 93.6796 37.356C93.6796 36.196 93.513 35.1893 93.1796 34.336C92.8596 33.4827 92.3996 32.8227 91.7996 32.356C91.213 31.876 90.5196 31.636 89.7196 31.636C88.9196 31.636 88.2196 31.876 87.6196 32.356C87.033 32.8227 86.573 33.4827 86.2396 34.336C85.9063 35.1893 85.7396 36.196 85.7396 37.356C85.7396 38.516 85.9063 39.5293 86.2396 40.396C86.573 41.2493 87.033 41.9093 87.6196 42.376C88.2196 42.8427 88.9196 43.076 89.7196 43.076Z" fill="#151616"/>
<path d="M106.627 44.336V31.876H101.987V30.376H113.007V31.876H108.367V44.336H106.627Z" fill="#151616"/>
<path d="M125.335 44.576C124.162 44.576 123.142 44.2827 122.275 43.696C121.409 43.096 120.735 42.256 120.255 41.176C119.789 40.0827 119.555 38.8093 119.555 37.356C119.555 35.8893 119.789 34.616 120.255 33.536C120.735 32.456 121.409 31.6227 122.275 31.036C123.142 30.436 124.162 30.136 125.335 30.136C126.509 30.136 127.529 30.436 128.395 31.036C129.262 31.6227 129.929 32.456 130.395 33.536C130.875 34.616 131.115 35.8893 131.115 37.356C131.115 38.8093 130.875 40.0827 130.395 41.176C129.929 42.256 129.262 43.096 128.395 43.696C127.529 44.2827 126.509 44.576 125.335 44.576ZM125.335 43.076C126.135 43.076 126.829 42.8427 127.415 42.376C128.015 41.9093 128.475 41.2493 128.795 40.396C129.129 39.5293 129.295 38.516 129.295 37.356C129.295 36.196 129.129 35.1893 128.795 34.336C128.475 33.4827 128.015 32.8227 127.415 32.356C126.829 31.876 126.135 31.636 125.335 31.636C124.535 31.636 123.835 31.876 123.235 32.356C122.649 32.8227 122.189 33.4827 121.855 34.336C121.522 35.1893 121.355 36.196 121.355 37.356C121.355 38.516 121.522 39.5293 121.855 40.396C122.189 41.2493 122.649 41.9093 123.235 42.376C123.835 42.8427 124.535 43.076 125.335 43.076Z" fill="#151616"/>
<path d="M143.403 44.576C142.176 44.576 141.11 44.2627 140.203 43.636C139.31 43.0093 138.623 42.156 138.143 41.076C137.663 39.9827 137.423 38.736 137.423 37.336C137.423 35.8693 137.676 34.596 138.183 33.516C138.69 32.436 139.396 31.6027 140.303 31.016C141.21 30.4293 142.256 30.136 143.443 30.136C144.416 30.136 145.29 30.3293 146.063 30.716C146.836 31.0893 147.47 31.6293 147.963 32.336C148.456 33.0427 148.75 33.876 148.843 34.836H146.923C146.816 33.876 146.45 33.1027 145.823 32.516C145.196 31.9293 144.396 31.636 143.423 31.636C142.596 31.636 141.87 31.8693 141.243 32.336C140.616 32.8027 140.123 33.4693 139.763 34.336C139.416 35.1893 139.243 36.196 139.243 37.356C139.243 38.5293 139.423 39.5493 139.783 40.416C140.143 41.2693 140.636 41.9293 141.263 42.396C141.89 42.8493 142.603 43.076 143.403 43.076C144.083 43.076 144.676 42.936 145.183 42.656C145.703 42.376 146.116 41.9827 146.423 41.476C146.743 40.9693 146.936 40.376 147.003 39.696H148.903C148.81 40.6693 148.523 41.5293 148.043 42.276C147.576 43.0093 146.95 43.576 146.163 43.976C145.376 44.376 144.456 44.576 143.403 44.576Z" fill="#151616"/>
<path d="M160.951 44.576C159.778 44.576 158.758 44.2827 157.891 43.696C157.024 43.096 156.351 42.256 155.871 41.176C155.404 40.0827 155.171 38.8093 155.171 37.356C155.171 35.8893 155.404 34.616 155.871 33.536C156.351 32.456 157.024 31.6227 157.891 31.036C158.758 30.436 159.778 30.136 160.951 30.136C162.124 30.136 163.144 30.436 164.011 31.036C164.878 31.6227 165.544 32.456 166.011 33.536C166.491 34.616 166.731 35.8893 166.731 37.356C166.731 38.8093 166.491 40.0827 166.011 41.176C165.544 42.256 164.878 43.096 164.011 43.696C163.144 44.2827 162.124 44.576 160.951 44.576ZM160.951 43.076C161.751 43.076 162.444 42.8427 163.031 42.376C163.631 41.9093 164.091 41.2493 164.411 40.396C164.744 39.5293 164.911 38.516 164.911 37.356C164.911 36.196 164.744 35.1893 164.411 34.336C164.091 33.4827 163.631 32.8227 163.031 32.356C162.444 31.876 161.751 31.636 160.951 31.636C160.151 31.636 159.451 31.876 158.851 32.356C158.264 32.8227 157.804 33.4827 157.471 34.336C157.138 35.1893 156.971 36.196 156.971 37.356C156.971 38.516 157.138 39.5293 157.471 40.396C157.804 41.2493 158.264 41.9093 158.851 42.376C159.451 42.8427 160.151 43.076 160.951 43.076Z" fill="#151616"/>
<path d="M174.619 44.336V30.376H176.339V42.836H183.839V44.336H174.619Z" fill="#151616"/>
<defs>
<clipPath id="clip0_512_1287">
<rect width="40.8" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,41 @@
import React from 'react'
import './button.css'
type Props = {
style?: { [key: string]: string }
loadingText?: string
onClick?: () => void
children: any
}
export default ({ style, loadingText, onClick, children }: Props) => {
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState('')
const handleClick = async () => {
if (loading) return
if (typeof onClick !== 'function') return
try {
setLoading(true)
await onClick()
} catch (err: any) {
console.log(err)
setError(err.toString())
setTimeout(() => setError(''), 2000)
} finally {
setLoading(false)
}
}
return (
<div className="button-outer">
<div
className="button-inner"
style={{ ...(style || {}) }}
onClick={handleClick}
>
{!loading && !error ? children : null}
{loading ? loadingText ?? 'Loading...' : null}
{error ? error : null}
</div>
</div>
)
}

View File

@@ -0,0 +1,86 @@
import React, { useState, useEffect } from 'react'
import measureText from '../utils/measure-text'
import './tooltip.css'
import UIContext from '../contexts/interface'
import { observer } from 'mobx-react-lite'
type Props = {
text: string
maxWidth?: number
}
export default observer(({ text, maxWidth, ...props }: Props) => {
const ui = React.useContext(UIContext)
const containerEl: React.RefObject<any> = React.createRef()
const [timer, setTimer] = useState<any>(null)
const [showingPopup, setShowingPopup] = useState<boolean>(false)
const [leftOffset, setLeftOffset] = useState<number>(0)
const [textWidth, setTextWidth] = useState<number>(0)
useEffect(() => {
const _textWidth = measureText(text, {
fontSize: '12px',
fontWeight: 'normal',
})
const _maxWidth = maxWidth ?? 200
const calcWidth = Math.min(_maxWidth, _textWidth)
setTextWidth(calcWidth)
const { x } = containerEl.current.getBoundingClientRect()
const screenMaxWidth = window.innerWidth - x
const minWidth = _maxWidth + 20
setLeftOffset(screenMaxWidth > minWidth ? 0 : minWidth - screenMaxWidth)
})
const onMouseEnter = () => {
if (!ui.isMobile) {
setShowingPopup(true)
}
}
const onMouseLeave = () => {
if (!ui.isMobile) {
setShowingPopup(false)
}
}
return (
<div
onMouseDown={() => {
if (!ui.isMobile) return
if (timer) clearTimeout(timer)
if (showingPopup) {
setShowingPopup(false)
return
}
setShowingPopup(true)
const _timer: ReturnType<typeof setTimeout> = setTimeout(() => {
setShowingPopup(false)
setTimer(null)
}, 3000)
setTimer(_timer)
}}
className="tooltip-outer"
ref={containerEl}
{...props}
>
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<img
src={require('../../public/info_icon.svg')}
alt="info icon"
/>
</div>
{showingPopup && (
<div
className={`tooltip-popup ${ui.modeCssClass}`}
style={{
width: `${textWidth}px`,
left: `-${leftOffset}px`,
}}
>
<div className={`tooltip-inner ${ui.modeCssClass}`}>
{text}
</div>
</div>
)}
</div>
)
})

View File

@@ -0,0 +1,19 @@
.button-outer {
display: flex;
justify-content: left;
align-items: center;
user-select: none;
}
.button-inner {
border-radius: 32px;
cursor: pointer;
font-weight: 600;
user-select: none;
text-align: center;
font-size: 20px;
background-color: #a0d8db;
padding: 16px 48px;
-webkit-user-select: none;
-ms-user-select: none;
}

View File

@@ -0,0 +1,19 @@
.tooltip-outer {
position: relative;
}
.tooltip-popup {
padding: 12px;
background: black;
z-index: 100000;
position: absolute;
border-radius: 8px;
display: flex;
}
.tooltip-inner {
font-size: 12px;
font-weight: normal;
line-height: 20px;
color: #a0d8db;
}

View File

@@ -0,0 +1,196 @@
import { createContext } from 'react'
import { makeAutoObservable } from 'mobx'
import { stringifyBigInts } from '@unirep/utils'
import { Identity } from '@semaphore-protocol/identity'
import { UserState } from '@unirep/core'
import { DataProof } from '@unirep-app/circuits'
import { SERVER } from '../config'
import prover from './prover'
import { ethers } from 'ethers'
class User {
currentEpoch: number = 0
latestTransitionedEpoch: number = 0
hasSignedUp: boolean = false
data: bigint[] = []
provableData: bigint[] = []
userState?: UserState
provider: any
constructor() {
makeAutoObservable(this)
this.load()
}
async load() {
const id: string = localStorage.getItem('id') ?? ''
const identity = new Identity(id)
if (!id) {
localStorage.setItem('id', identity.toString())
}
const { UNIREP_ADDRESS, APP_ADDRESS, ETH_PROVIDER_URL } = await fetch(
`${SERVER}/api/config`
).then((r) => r.json())
const provider = ETH_PROVIDER_URL.startsWith('http')
? new ethers.providers.JsonRpcProvider(ETH_PROVIDER_URL)
: new ethers.providers.WebSocketProvider(ETH_PROVIDER_URL)
this.provider = provider
const userState = new UserState(
{
provider,
prover,
unirepAddress: UNIREP_ADDRESS,
attesterId: BigInt(APP_ADDRESS),
_id: identity,
},
identity
)
await userState.sync.start()
this.userState = userState
await userState.waitForSync()
this.hasSignedUp = await userState.hasSignedUp()
await this.loadData()
this.latestTransitionedEpoch =
await this.userState.latestTransitionedEpoch()
}
get fieldCount() {
return this.userState?.sync.settings.fieldCount
}
get sumFieldCount() {
return this.userState?.sync.settings.sumFieldCount
}
epochKey(nonce: number) {
if (!this.userState) return '0x'
const epoch = this.userState.sync.calcCurrentEpoch()
const key = this.userState.getEpochKeys(epoch, nonce)
return `0x${key.toString(16)}`
}
async loadData() {
if (!this.userState) throw new Error('user state not initialized')
this.data = await this.userState.getData()
this.provableData = await this.userState.getProvableData()
}
async signup() {
if (!this.userState) throw new Error('user state not initialized')
const signupProof = await this.userState.genUserSignUpProof()
const data = await fetch(`${SERVER}/api/signup`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
publicSignals: signupProof.publicSignals,
proof: signupProof.proof,
}),
}).then((r) => r.json())
await this.provider.waitForTransaction(data.hash)
await this.userState.waitForSync()
this.hasSignedUp = await this.userState.hasSignedUp()
this.latestTransitionedEpoch = this.userState.sync.calcCurrentEpoch()
}
async requestData(
reqData: { [key: number]: string | number },
epkNonce: number
) {
if (!this.userState) throw new Error('user state not initialized')
for (const key of Object.keys(reqData)) {
if (reqData[+key] === '') {
delete reqData[+key]
continue
}
}
if (Object.keys(reqData).length === 0) {
throw new Error('No data in the attestation')
}
const epochKeyProof = await this.userState.genEpochKeyProof({
nonce: epkNonce,
})
const data = await fetch(`${SERVER}/api/request`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(
stringifyBigInts({
reqData,
publicSignals: epochKeyProof.publicSignals,
proof: epochKeyProof.proof,
})
),
}).then((r) => r.json())
await this.provider.waitForTransaction(data.hash)
await this.userState.waitForSync()
await this.loadData()
}
async stateTransition() {
if (!this.userState) throw new Error('user state not initialized')
await this.userState.waitForSync()
const signupProof = await this.userState.genUserStateTransitionProof()
const data = await fetch(`${SERVER}/api/transition`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
publicSignals: signupProof.publicSignals,
proof: signupProof.proof,
}),
}).then((r) => r.json())
await this.provider.waitForTransaction(data.hash)
await this.userState.waitForSync()
await this.loadData()
this.latestTransitionedEpoch =
await this.userState.latestTransitionedEpoch()
}
async proveData(data: { [key: number]: string | number }) {
if (!this.userState) throw new Error('user state not initialized')
const epoch = await this.userState.sync.loadCurrentEpoch()
const stateTree = await this.userState.sync.genStateTree(epoch)
const index = await this.userState.latestStateTreeLeafIndex(epoch)
const stateTreeProof = stateTree.createProof(index)
const provableData = await this.userState.getProvableData()
const sumFieldCount = this.userState.sync.settings.sumFieldCount
const values = Array(sumFieldCount).fill(0)
for (let [key, value] of Object.entries(data)) {
values[Number(key)] = value
}
const attesterId = this.userState.sync.attesterId
const circuitInputs = stringifyBigInts({
identity_secret: this.userState.id.secret,
state_tree_indexes: stateTreeProof.pathIndices,
state_tree_elements: stateTreeProof.siblings,
data: provableData,
epoch: epoch,
attester_id: attesterId,
value: values,
})
const { publicSignals, proof } = await prover.genProofAndPublicSignals(
'dataProof',
circuitInputs
)
const dataProof = new DataProof(publicSignals, proof, prover)
const valid = await dataProof.verify()
return stringifyBigInts({
publicSignals: dataProof.publicSignals,
proof: dataProof.proof,
valid,
})
}
}
export default createContext(new User())

View File

@@ -0,0 +1,55 @@
import { createContext } from 'react'
import { makeAutoObservable } from 'mobx'
const MAX_MOBILE_WIDTH = 780
export class Interface {
// dark/light mode
// interface viewport size
darkmode = false
modeCssClass = ''
screenWidth = -1
screenHeight = -1
isMobile = false
constructor() {
makeAutoObservable(this)
if (typeof window !== 'undefined') {
this.load()
}
}
// must be called in browser, not in SSR
load() {
this.updateWindowSize()
window.addEventListener('resize', this.updateWindowSize.bind(this))
this.setDarkmode(!!localStorage.getItem('darkmode'))
document.cookie = `darkmode=${this.darkmode.toString()}`
}
updateWindowSize() {
// possibly throttle on heavy sites
this.screenWidth = window.innerWidth
this.screenHeight = window.innerHeight
this.isMobile = this.screenWidth <= MAX_MOBILE_WIDTH
}
setDarkmode(enabled: boolean) {
this.darkmode = enabled
if (enabled) {
if (typeof window !== 'undefined') {
localStorage.setItem('darkmode', 'true')
document.cookie = `darkmode=${this.darkmode.toString()}`
}
this.modeCssClass = 'dark'
} else {
if (typeof window !== 'undefined') {
localStorage.removeItem('darkmode')
document.cookie = `darkmode=${this.darkmode.toString()}`
}
this.modeCssClass = ''
}
}
}
export default createContext(new Interface())

View File

@@ -0,0 +1,44 @@
import * as snarkjs from 'snarkjs'
import { SnarkPublicSignals, SnarkProof } from '@unirep/utils'
import { Circuit } from '@unirep/circuits'
import { KEY_SERVER } from '../config'
export default {
verifyProof: async (
circuitName: string | Circuit,
publicSignals: SnarkPublicSignals,
proof: SnarkProof
) => {
const url = new URL(`/build/${circuitName}.vkey.json`, KEY_SERVER)
const vkey = await fetch(url.toString()).then((r) => r.json())
return snarkjs.groth16.verify(vkey, publicSignals, proof)
},
genProofAndPublicSignals: async (
circuitName: string | Circuit,
inputs: any
) => {
const wasmUrl = new URL(`${circuitName}.wasm`, KEY_SERVER)
const wasm = await fetch(wasmUrl.toString()).then((r) =>
r.arrayBuffer()
)
const zkeyUrl = new URL(`${circuitName}.zkey`, KEY_SERVER)
const zkey = await fetch(zkeyUrl.toString()).then((r) =>
r.arrayBuffer()
)
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
inputs,
new Uint8Array(wasm),
new Uint8Array(zkey)
)
return { proof, publicSignals }
},
/**
* Get vkey from default built folder `zksnarkBuild/`
* @param name Name of the circuit, which can be chosen from `Circuit`
* @returns vkey of the circuit
*/
getVKey: async (name: string | Circuit) => {
// return require(path.join(buildPath, `${name}.vkey.json`))
},
}

View File

@@ -0,0 +1,5 @@
body {
/* font-family: sans-serif, helvetica; */
font-family: 'Azeret Mono';
font-weight: 200;
}

View File

@@ -0,0 +1,26 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Header from './pages/Header'
import Start from './pages/Start'
import Dashboard from './pages/Dashboard'
import './index.css'
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Header />}>
<Route index element={<Start />} />
<Route path="dashboard" element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
)
}
const rootElement = document.getElementById('root')
if (rootElement) {
const root = createRoot(rootElement)
root.render(<App />)
}

View File

@@ -0,0 +1,323 @@
import React from 'react'
import { observer } from 'mobx-react-lite'
import './dashboard.css'
import Button from '../components/Button'
import Tooltip from '../components/Tooltip'
import User from '../contexts/User'
type ReqInfo = {
nonce: number
}
type ProofInfo = {
publicSignals: string[]
proof: string[]
valid: boolean
}
export default observer(() => {
const userContext = React.useContext(User)
const [remainingTime, setRemainingTime] = React.useState<number | string>(0)
const [reqData, setReqData] = React.useState<{
[key: number]: number | string
}>({})
const [reqInfo, setReqInfo] = React.useState<ReqInfo>({ nonce: 0 })
const [proveData, setProveData] = React.useState<{
[key: number]: number | string
}>({})
const [repProof, setRepProof] = React.useState<ProofInfo>({
publicSignals: [],
proof: [],
valid: false,
})
const updateTimer = () => {
if (!userContext.userState) {
setRemainingTime('Loading...')
return
}
const time = userContext.userState.sync.calcEpochRemainingTime()
setRemainingTime(time)
}
const fieldType = (i: number) => {
if (i < userContext.sumFieldCount) {
return 'sum'
} else return 'replace'
}
React.useEffect(() => {
setInterval(() => {
updateTimer()
}, 1000)
}, [])
if (!userContext.userState) {
return <div className="container">Loading...</div>
}
return (
<div>
<h1>Dashboard</h1>
<div className="container">
<div className="info-container">
<div className="info-item">
<h3>Epoch</h3>
<Tooltip
text={`An epoch is a unit of time, defined by the attester, with a state tree and epoch tree. User epoch keys are valid for 1 epoch before they change.`}
/>
</div>
<div className="info-item">
<div>Current epoch #</div>
<div className="stat">
{userContext.userState?.sync.calcCurrentEpoch()}
</div>
</div>
<div className="info-item">
<div>Remaining time</div>
<div className="stat">{remainingTime}</div>
</div>
<div className="info-item">
<div>Latest transition epoch</div>
<div className="stat">
{userContext.latestTransitionedEpoch}
</div>
</div>
<hr />
<div className="info-item">
<h3>Latest Data</h3>
<Tooltip text="This is all the data the user has received. The user cannot prove data from the current epoch." />
</div>
{userContext.data.map((data, i) => {
if (i < userContext.sumFieldCount) {
return (
<div key={i} className="info-item">
<div>Data {i}</div>
<div className="stat">
{(data || 0).toString()}
</div>
</div>
)
} else {
return (
<div key={i} className="info-item">
<div>Data {i}</div>
<div className="stat">
{(
data % BigInt(2 ** 206) || 0
).toString()}
</div>
</div>
)
}
})}
<br />
<div className="info-item">
<h3>Provable Data</h3>
<Tooltip text="This is the data the user has received up until their last transitioned epoch. This data can be proven in ZK." />
</div>
{userContext.provableData.map((data, i) => {
if (i < userContext.sumFieldCount) {
return (
<div key={i} className="info-item">
<div>Data {i}</div>
<div className="stat">
{(data || 0).toString()}
</div>
</div>
)
} else {
return (
<div key={i} className="info-item">
<div>Data {i}</div>
<div className="stat">
{(
data % BigInt(2 ** 206) || 0
).toString()}
</div>
</div>
)
}
})}
</div>
<div style={{ display: 'flex' }}>
<div className="action-container">
<div className="icon">
<h2>Change Data</h2>
<Tooltip text="You can request changes to data here. The demo attester will freely change your data." />
</div>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'flex-start',
}}
>
{Array(
userContext.userState.sync.settings.fieldCount
)
.fill(0)
.map((_, i) => {
return (
<div key={i} style={{ margin: '4px' }}>
<p>
Data {i} ({fieldType(i)})
</p>
<input
value={reqData[i] ?? ''}
onChange={(event) => {
if (
!/^\d*$/.test(
event.target.value
)
)
return
setReqData(() => ({
...reqData,
[i]: event.target.value,
}))
}}
/>
</div>
)
})}
</div>
<div className="icon">
<p style={{ marginRight: '8px' }}>
Epoch key nonce
</p>
<Tooltip text="Epoch keys are short lived identifiers for a user. They can be used to receive data and are valid only for 1 epoch." />
</div>
<select
value={reqInfo.nonce ?? 0}
onChange={(event) => {
setReqInfo((v) => ({
...v,
nonce: Number(event.target.value),
}))
}}
>
<option value="0">0</option>
<option value="1">1</option>
{/* TODO: <option value="2">2</option> */}
</select>
<p style={{ fontSize: '12px' }}>
Requesting data with epoch key:
</p>
<p
style={{
maxWidth: '650px',
wordBreak: 'break-all',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{userContext.epochKey(reqInfo.nonce ?? 0)}
</p>
<Button
onClick={async () => {
if (
userContext.userState &&
userContext.userState.sync.calcCurrentEpoch() !==
(await userContext.userState.latestTransitionedEpoch())
) {
throw new Error('Needs transition')
}
await userContext.requestData(
reqData,
reqInfo.nonce ?? 0
)
setReqData({})
}}
>
Attest
</Button>
</div>
<div className="action-container transition">
<div className="icon">
<h2>User State Transition</h2>
<Tooltip
text={`The user state transition allows a user to insert a state tree leaf into the latest epoch. The user sums all the data they've received in the past and proves it in ZK.`}
/>
</div>
<Button onClick={() => userContext.stateTransition()}>
Transition
</Button>
</div>
<div className="action-container">
<div className="icon">
<h2>Prove Data</h2>
<Tooltip text="Users can prove they control some amount of data without revealing exactly how much they control." />
</div>
{Array(
userContext.userState.sync.settings.sumFieldCount
)
.fill(0)
.map((_, i) => {
return (
<div key={i} style={{ margin: '4px' }}>
<p>
Data {i} ({fieldType(i)})
</p>
<input
value={proveData[i] ?? '0'}
onChange={(event) => {
if (
!/^\d*$/.test(
event.target.value
)
)
return
setProveData(() => ({
...proveData,
[i]: event.target.value,
}))
}}
/>
</div>
)
})}
<div style={{ margin: '20px 0 20px' }}>
<Button
onClick={async () => {
const proof = await userContext.proveData(
proveData
)
setRepProof(proof)
}}
>
Generate Proof
</Button>
</div>
{repProof.proof.length ? (
<>
<div>
Is proof valid?{' '}
<span style={{ fontWeight: '600' }}>
{' '}
{repProof.proof.length === 0
? ''
: repProof.valid.toString()}
</span>
</div>
<textarea
readOnly
value={JSON.stringify(repProof, null, 2)}
/>
</>
) : null}
</div>
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,29 @@
import React from 'react'
import { Outlet, Link } from 'react-router-dom'
import './header.css'
export default () => {
return (
<>
<div className="header">
<img src={require('../../public/logo.svg')} alt="UniRep logo" />
<div className="links">
<a href="https://developer.unirep.io/" target="blank">
Docs
</a>
<a href="https://github.com/Unirep" target="blank">
GitHub
</a>
<a
href="https://discord.com/invite/VzMMDJmYc5"
target="blank"
>
Discord
</a>
</div>
</div>
<Outlet />
</>
)
}

View File

@@ -0,0 +1,94 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { observer } from 'mobx-react-lite'
import './start.css'
import Tooltip from '../components/Tooltip'
import Button from '../components/Button'
import User from '../contexts/User'
export default observer(() => {
const userContext = React.useContext(User)
// if (!userContext.userState) {
// return (
// <div className="container">
// Loading...
// </div>
// )
// }
return (
<>
<div className="bg">
<img
src={require('../../public/hummingbird.svg')}
alt="hummingbird at a flower"
/>
</div>
<div className="content">
<div style={{ fontSize: '70px', fontWeight: '600' }}>
Congratulations
</div>
<div className="attester">
<div style={{ marginRight: '12px' }}>
You have created a new UniRep attester{' '}
</div>
<Tooltip text="Attesters define their own data systems and are able to attest to users, giving them data." />
</div>
<p>
Clicking 'Join' adds a user to this attester's membership
group.
</p>
<div className="join">
{!userContext.hasSignedUp ? (
<Button
onClick={() => {
if (!userContext.userState) return
return userContext.signup()
}}
>
{userContext.userState ? 'Join' : 'Initializing...'}
<span style={{ marginLeft: '12px' }}>
<img
src={require('../../public/arrow.svg')}
alt="right arrow"
/>
</span>
</Button>
) : (
<div>
<p
style={{
fontWeight: '400',
lineHeight: '.5em',
}}
>
USER ADDED!
</p>
<Link to="/dashboard">
<Button>
Dashboard
<span style={{ marginLeft: '12px' }}>
<img
src={require('../../public/arrow.svg')}
alt="right arrow"
/>
</span>
</Button>
</Link>
</div>
)}
</div>
<p>
After joining, the member can interact with data in the
attester's application.{' '}
</p>
<p>
Customize this landing page to onboard new users to your
app.
</p>
</div>
</>
)
})

View File

@@ -0,0 +1,114 @@
h1 {
font-size: 48px;
font-weight: 700;
margin: 40px 80px 20px;
}
h2 {
font-size: 24px;
font-weight: 600;
margin-right: 12px;
}
h3 {
font-size: 20px;
font-weight: 600;
}
hr {
margin: 10px 2px;
}
input {
width: 180px;
height: 40px;
text-align: center;
font-size: 20px;
}
select {
width: 120px;
height: 40px;
text-align: center;
font-size: 20px;
font-weight: 600;
}
textarea {
width: 100%;
height: 147px;
margin-top: 10px;
}
.container {
display: flex;
flex-direction: row;
margin: 0 80px;
}
.info-container {
display: flex;
flex-direction: column;
border: 1px solid black;
border-radius: 8px;
padding: 16px 16px 30px;
min-width: 300px;
height: fit-content;
font-weight: 200;
line-height: 32px;
}
.info-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.stat {
font-weight: 600;
}
.action-container {
height: fit-content;
border: 1px solid black;
border-radius: 8px;
padding: 16px 16px 30px;
margin: 0 0 20px 20px;
font-weight: 300;
}
.transition {
width: auto;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.icon {
display: flex;
align-items: center;
cursor: pointer;
}
/* .story {
margin: 4px 0px;
}
.end-divider {
color: gray;
text-align: center;
margin: 5px 0px;
border: 1px solid gray;
padding: 2px;
}
@keyframes highlight {
from {
background: yellow;
}
to {
background: rgba(0, 0, 0, 0);
}
} */

View File

@@ -0,0 +1,21 @@
a:link {
margin-left: 80px;
font-weight: 600;
color: black;
text-decoration: none;
}
a:visited {
color: black;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 50px 80px 0 80px;
}
.links {
display: flex;
}

View File

@@ -0,0 +1,33 @@
p {
font-size: 14px;
font-weight: 200;
line-height: 0.8em;
}
.content {
position: absolute;
bottom: 0px;
padding: 0 0 40px 140px;
z-index: 10;
}
.attester {
display: flex;
align-items: center;
cursor: pointer;
font-size: 20px;
font-weight: 300;
margin-top: 10px;
}
.join {
width: 260px;
margin: 20px 0;
text-align: center;
}
.bg {
position: absolute;
right: 0px;
z-index: 0;
}

View File

@@ -0,0 +1 @@
declare module 'snarkjs'

View File

@@ -0,0 +1,13 @@
export default (text: string, style = {}) => {
const el = document.createElement('div')
el.style.whiteSpace = 'nowrap'
el.style.width = 'auto'
el.style.height = 'auto'
el.style.position = 'absolute'
Object.assign(el.style, style)
el.innerHTML = text
document.body.appendChild(el)
const width = Math.ceil(el.clientWidth)
document.body.removeChild(el)
return width + 1 // add 1 because reasons
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"jsx": "react-jsx",
"typeRoots": ["./types", "./node_modules/@types"],
"useDefineForClassFields": true,
"outDir": "build"
},
"include": ["src"],
"exclude": ["node_modules", "typings", "build/**/*"]
}

View File

@@ -0,0 +1,116 @@
const HtmlWebpackPlugin = require('html-webpack-plugin')
// const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin')
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const webpack = require('webpack')
module.exports = (env) => ({
entry: ['./src/index.tsx'],
mode: 'development',
devServer: {
port: 3000,
historyApiFallback: true,
},
optimization: {
splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
},
},
output: {
path: path.resolve(__dirname, 'build'),
publicPath: '/',
},
resolve: {
extensions: ['*', '.js', '.jsx', '.json', '.scss', '.ts', '.tsx'],
fallback: {
path: require.resolve('path-browserify'),
crypto: require.resolve('crypto-browserify'),
assert: require.resolve('assert/'),
stream: require.resolve('stream-browserify'),
os: require.resolve('os-browserify/browser'),
events: require.resolve('events/'),
fs: false,
readline: false,
constants: false,
},
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
{
loader: 'ts-loader',
},
],
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
{
test: /\.(png|jpg|gif|svg|ico)$/i,
use: [
{
loader: 'file-loader',
options: {
esModule: false,
limit: 8192,
},
},
],
},
{
test: /\.(css)$/,
// exclude: /node_modules/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
filename: 'index.html',
inlineSource: '.(js|css)',
}),
new MiniCssExtractPlugin(),
// new HtmlWebpackInlineSourcePlugin(),
new webpack.DefinePlugin({
'process.env': {},
'process.argv': [],
'process.versions': {},
'process.versions.node': '"12"',
process: {
exit: '(() => {})',
browser: true,
versions: {},
cwd: '(() => "")',
},
...(env.CYPRESS
? {
['process.env.CYPRESS']: 'true',
}
: {}),
}),
new webpack.ProvidePlugin({
Buffer: path.resolve(__dirname, 'externals', 'buffer.js'),
}),
new webpack.ContextReplacementPlugin(/\/maci\-crypto\//, (data) => {
delete data.dependencies[0].critical
return data
}),
],
})

View File

@@ -0,0 +1,21 @@
import {
getAssetFromKV,
serveSinglePageApp,
} from '@cloudflare/kv-asset-handler'
addEventListener('fetch', (event) => {
event.respondWith(handleEvent(event))
})
async function handleEvent(event) {
try {
// Add logic to decide whether to serve an asset or run your original Worker code
return getAssetFromKV(event, { mapRequestToAsset: serveSinglePageApp })
} catch (e) {
const pathname = new URL(event.request.url).pathname
return new Response(`"${pathname}" not found`, {
status: 404,
statusText: 'not found',
})
}
}

View File

@@ -0,0 +1,6 @@
name = "unirep-demo"
main = "worker.js"
compatibility_date = "2022-10-18"
[site]
bucket = "./build"

2
packages/relay/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
build/
keys

11
packages/relay/README.md Normal file
View File

@@ -0,0 +1,11 @@
# `relay`
> TODO: description
## Usage
```
const relay = require('relay');
// TODO: DEMONSTRATE API
```

View File

@@ -0,0 +1,27 @@
{
"name": "@unirep-app/relay",
"version": "1.0.0",
"description": "> TODO: description",
"author": "Unirep team <team@unirep.io>",
"homepage": "https://github.com/Unirep/create-unirep-app#readme",
"license": "ISC",
"repository": "git+https://github.com/Unirep/create-unirep-app.git",
"scripts": {
"build": "yarn keys",
"start": "ts-node ./src/index.ts",
"keys": "sh scripts/loadKeys.sh"
},
"dependencies": {
"@unirep-app/contracts": "1.0.0",
"@unirep/core": "2.0.0-beta-3",
"anondb": "^0.0.13",
"dotenv": "^16.0.3",
"ethers": "^5.7.2",
"express": "^4.18.2"
},
"main": "index.js",
"devDependencies": {
"ts-node": "^10.9.1",
"typescript": "^5.0.3"
}
}

View File

@@ -0,0 +1,5 @@
#!/bin/sh
rm -rf ./keys
cp -r ../circuits/zksnarkBuild/. ./keys

View File

@@ -0,0 +1,57 @@
import path from 'path'
import fs from 'fs'
import express from 'express'
import { Synchronizer } from '@unirep/core'
import { SQLiteConnector } from 'anondb/node.js'
import prover from './singletons/prover'
import schema from './singletons/schema'
import {
provider,
PRIVATE_KEY,
UNIREP_ADDRESS,
DB_PATH,
APP_ADDRESS,
} from './config'
import TransactionManager from './singletons/TransactionManager'
main().catch((err) => {
console.log(`Uncaught error: ${err}`)
process.exit(1)
})
async function main() {
const db = await SQLiteConnector.create(schema, DB_PATH ?? ':memory:')
const synchronizer = new Synchronizer({
db,
provider,
unirepAddress: UNIREP_ADDRESS,
attesterId: BigInt(APP_ADDRESS),
prover,
})
await synchronizer.start()
TransactionManager.configure(PRIVATE_KEY, provider, synchronizer._db)
await TransactionManager.start()
const app = express()
const port = process.env.PORT ?? 8000
app.listen(port, () => console.log(`Listening on port ${port}`))
app.use('*', (req, res, next) => {
res.set('access-control-allow-origin', '*')
res.set('access-control-allow-headers', '*')
next()
})
app.use(express.json())
app.use('/build', express.static(path.join(__dirname, '../keys')))
// import all non-index files from this folder
const routeDir = path.join(__dirname, 'routes')
const routes = await fs.promises.readdir(routeDir)
for (const routeFile of routes) {
const { default: route } = await import(path.join(routeDir, routeFile))
route(app, synchronizer._db, synchronizer)
}
}

View File

@@ -0,0 +1,12 @@
import { Express } from 'express'
import { UNIREP_ADDRESS, APP_ADDRESS, ETH_PROVIDER_URL } from '../config'
export default (app: Express) => {
app.get('/api/config', (_, res) =>
res.json({
UNIREP_ADDRESS,
APP_ADDRESS,
ETH_PROVIDER_URL,
})
)
}

View File

@@ -0,0 +1,56 @@
import { ethers } from 'ethers'
import { Express } from 'express'
import { DB } from 'anondb/node'
import { Synchronizer } from '@unirep/core'
import { EpochKeyProof } from '@unirep/circuits'
import { APP_ADDRESS } from '../config'
import TransactionManager from '../singletons/TransactionManager'
import UNIREP_APP from '@unirep-app/contracts/artifacts/contracts/UnirepApp.sol/UnirepApp.json'
export default (app: Express, db: DB, synchronizer: Synchronizer) => {
app.post('/api/request', async (req, res) => {
try {
const { reqData, publicSignals, proof } = req.body
const epochKeyProof = new EpochKeyProof(
publicSignals,
proof,
synchronizer.prover
)
const valid = await epochKeyProof.verify()
if (!valid) {
res.status(400).json({ error: 'Invalid proof' })
return
}
const epoch = await synchronizer.loadCurrentEpoch()
const appContract = new ethers.Contract(APP_ADDRESS, UNIREP_APP.abi)
const keys = Object.keys(reqData)
let calldata: any
if (keys.length === 1) {
calldata = appContract.interface.encodeFunctionData(
'submitAttestation',
[epochKeyProof.epochKey, epoch, keys[0], reqData[keys[0]]]
)
} else if (keys.length > 1) {
calldata = appContract.interface.encodeFunctionData(
'submitManyAttestations',
[
epochKeyProof.epochKey,
epoch,
keys,
keys.map((k) => reqData[k]),
]
)
}
const hash = await TransactionManager.queueTransaction(
APP_ADDRESS,
calldata
)
res.json({ hash })
} catch (error: any) {
res.status(500).json({ error })
}
})
}

View File

@@ -0,0 +1,46 @@
import { SignupProof } from '@unirep/circuits'
import { ethers } from 'ethers'
import { Express } from 'express'
import { DB } from 'anondb/node'
import { Synchronizer } from '@unirep/core'
import { APP_ADDRESS } from '../config'
import TransactionManager from '../singletons/TransactionManager'
import UNIREP_APP from '@unirep-app/contracts/artifacts/contracts/UnirepApp.sol/UnirepApp.json'
export default (app: Express, db: DB, synchronizer: Synchronizer) => {
app.post('/api/signup', async (req, res) => {
try {
const { publicSignals, proof } = req.body
const signupProof = new SignupProof(
publicSignals,
proof,
synchronizer.prover
)
const valid = await signupProof.verify()
if (!valid) {
res.status(400).json({ error: 'Invalid proof' })
return
}
const currentEpoch = synchronizer.calcCurrentEpoch()
if (currentEpoch !== Number(signupProof.epoch)) {
res.status(400).json({ error: 'Wrong epoch' })
return
}
// make a transaction lil bish
const appContract = new ethers.Contract(APP_ADDRESS, UNIREP_APP.abi)
// const contract =
const calldata = appContract.interface.encodeFunctionData(
'userSignUp',
[signupProof.publicSignals, signupProof.proof]
)
const hash = await TransactionManager.queueTransaction(
APP_ADDRESS,
calldata
)
res.json({ hash })
} catch (error) {
res.status(500).json({ error })
}
})
}

View File

@@ -0,0 +1,36 @@
import { Express } from 'express'
import { DB } from 'anondb/node'
import { Synchronizer } from '@unirep/core'
import { UserStateTransitionProof } from '@unirep/circuits'
import TransactionManager from '../singletons/TransactionManager'
export default (app: Express, db: DB, synchronizer: Synchronizer) => {
app.post('/api/transition', async (req, res) => {
try {
const { publicSignals, proof } = req.body
const transitionProof = new UserStateTransitionProof(
publicSignals,
proof,
synchronizer.prover
)
const valid = await transitionProof.verify()
if (!valid) {
res.status(400).json({ error: 'Invalid proof' })
return
}
const calldata =
synchronizer.unirepContract.interface.encodeFunctionData(
'userStateTransition',
[transitionProof.publicSignals, transitionProof.proof]
)
const hash = await TransactionManager.queueTransaction(
synchronizer.unirepContract.address,
calldata
)
res.json({ hash })
} catch (error: any) {
res.status(500).json({ error })
}
})
}

View File

@@ -0,0 +1,150 @@
import { ethers } from 'ethers'
import { DB } from 'anondb/node'
export class TransactionManager {
wallet?: ethers.Wallet
_db?: DB
configure(key: string, provider: any, db: DB) {
this.wallet = new ethers.Wallet(key, provider)
this._db = db
}
async start() {
if (!this.wallet || !this._db) throw new Error('Not initialized')
const latestNonce = await this.wallet.getTransactionCount()
await this._db.upsert('AccountNonce', {
where: {
address: this.wallet.address,
},
create: {
address: this.wallet.address,
nonce: latestNonce,
},
update: {},
})
this.startDaemon()
}
async startDaemon() {
if (!this._db) throw new Error('No db connected')
for (;;) {
const nextTx = await this._db.findOne('AccountTransaction', {
where: {},
orderBy: {
nonce: 'asc',
},
})
if (!nextTx) {
await new Promise((r) => setTimeout(r, 5000))
continue
}
const sent = await this.tryBroadcastTransaction(nextTx.signedData)
if (sent) {
await this._db.delete('AccountTransaction', {
where: {
signedData: nextTx.signedData,
},
})
} else {
const randWait = Math.random() * 2000
await new Promise((r) => setTimeout(r, 1000 + randWait))
}
}
}
async tryBroadcastTransaction(signedData: string) {
if (!this.wallet) throw new Error('Not initialized')
const hash = ethers.utils.keccak256(signedData)
try {
console.log(`Sending tx ${hash}`)
await this.wallet.provider.sendTransaction(signedData)
return true
} catch (err: any) {
const tx = await this.wallet.provider.getTransaction(hash)
if (tx) {
// if the transaction is reverted the nonce is still used, so we return true
return true
}
if (
err
.toString()
.indexOf(
'Your app has exceeded its compute units per second capacity'
) !== -1
) {
await new Promise((r) => setTimeout(r, 1000))
return this.tryBroadcastTransaction(signedData)
} else {
console.log(err)
return false
}
}
}
async getNonce(address: string) {
const latest = await this._db?.findOne('AccountNonce', {
where: {
address,
},
})
const updated = await this._db?.update('AccountNonce', {
where: {
address,
nonce: latest.nonce,
},
update: {
nonce: latest.nonce + 1,
},
})
if (updated === 0) {
await new Promise((r) => setTimeout(r, Math.random() * 500))
return this.getNonce(address)
}
return latest.nonce
}
async wait(hash: string) {
return this.wallet?.provider.waitForTransaction(hash)
}
async queueTransaction(to: string, data: string | any = {}) {
const args = {} as any
if (typeof data === 'string') {
// assume it's input data
args.data = data
} else {
Object.assign(args, data)
}
if (!this.wallet) throw new Error('Not initialized')
if (!args.gasLimit) {
// don't estimate, use this for unpredictable gas limit tx's
// transactions may revert with this
const gasLimit = await this.wallet.provider.estimateGas({
to,
from: this.wallet.address,
...args,
})
Object.assign(args, {
gasLimit: gasLimit.add(50000),
})
}
const nonce = await this.getNonce(this.wallet.address)
const signedData = await this.wallet.signTransaction({
nonce,
to,
// gasPrice: 2 * 10 ** 9, // 2 gwei
// gasPrice: 10000,
gasPrice: 299365979,
...args,
})
await this._db?.create('AccountTransaction', {
address: this.wallet.address,
signedData,
nonce,
})
return ethers.utils.keccak256(signedData)
}
}
export default new TransactionManager()

View File

@@ -0,0 +1,60 @@
import path from 'path'
import fs from 'fs/promises'
import { Circuit } from '@unirep/circuits'
import * as snarkjs from 'snarkjs'
const buildPath = path.join(__dirname, `../../keys`)
/**
* The default prover that uses the circuits in default built folder `zksnarkBuild/`
*/
export default {
/**
* Generate proof and public signals with `snarkjs.groth16.fullProve`
* @param circuitName Name of the circuit, which can be chosen from `Circuit`
* @param inputs The user inputs of the circuit
* @returns snark proof and public signals
*/
genProofAndPublicSignals: async (
circuitName: string | Circuit,
inputs: any
) => {
const circuitWasmPath = path.join(buildPath, `${circuitName}.wasm`)
const zkeyPath = path.join(buildPath, `${circuitName}.zkey`)
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
inputs,
circuitWasmPath,
zkeyPath
)
return { proof, publicSignals }
},
/**
* Verify the snark proof and public signals with `snarkjs.groth16.verify`
* @param circuitName Name of the circuit, which can be chosen from `Circuit`
* @param publicSignals The snark public signals that is generated from `genProofAndPublicSignals`
* @param proof The snark proof that is generated from `genProofAndPublicSignals`
* @returns True if the proof is valid, false otherwise
*/
verifyProof: async (
circuitName: string | Circuit,
publicSignals: string[],
proof: string[]
) => {
const vkeyData = await fs.readFile(
path.join(buildPath, `${circuitName}.vkey.json`)
)
const vkey = JSON.parse(vkeyData.toString())
return snarkjs.groth16.verify(vkey, publicSignals, proof)
},
/**
* Get vkey from default built folder `zksnarkBuild/`
* @param name Name of the circuit, which can be chosen from `Circuit`
* @returns vkey of the circuit
*/
getVKey: async (name: string | Circuit) => {
return require(path.join(buildPath, `${name}.vkey.json`))
},
}

View File

@@ -0,0 +1,41 @@
import { schema } from '@unirep/core'
import { TableData } from 'anondb/node'
import { nanoid } from 'nanoid'
const _schema = [
{
name: 'AccountTransaction',
primaryKey: 'signedData',
rows: [
['signedData', 'String'],
['address', 'String'],
['nonce', 'Int'],
],
},
{
name: 'AccountNonce',
primaryKey: 'address',
rows: [
['address', 'String'],
['nonce', 'Int'],
],
},
]
export default _schema
.map(
(obj) =>
({
...obj,
primaryKey: obj.primaryKey || '_id',
rows: [
...obj.rows,
{
name: '_id',
type: 'String',
default: () => nanoid(),
},
],
} as TableData)
)
.concat(schema)

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./build"
},
"include": ["./src", ".env"]
}

16
scripts/copyUnirep.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
set -e
# Pass an argument to this script that is the unirep monorepo directory
# this argument must be absolute
# e.g. yarn linkUnirep $(pwd)/../unirep
rm -rf node_modules/@unirep/core
rm -rf node_modules/@unirep/contracts
rm -rf node_modules/@unirep/utils
rm -rf node_modules/@unirep/circuits
cp -r $1/packages/core/build $(pwd)/node_modules/@unirep/core
cp -r $1/packages/contracts/build $(pwd)/node_modules/@unirep/contracts
cp -r $1/packages/utils/build $(pwd)/node_modules/@unirep/utils
cp -r $1/packages/circuits/dist $(pwd)/node_modules/@unirep/circuits

16
scripts/linkUnirep.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
set -e
# Pass an argument to this script that is the unirep monorepo directory
# this argument must be absolute
# e.g. yarn linkUnirep $(pwd)/../unirep
rm -rf node_modules/@unirep/core
rm -rf node_modules/@unirep/contracts
rm -rf node_modules/@unirep/utils
rm -rf node_modules/@unirep/circuits
ln -s $1/packages/core/build $(pwd)/node_modules/@unirep/core
ln -s $1/packages/contracts/build $(pwd)/node_modules/@unirep/contracts
ln -s $1/packages/utils/build $(pwd)/node_modules/@unirep/utils
ln -s $1/packages/circuits/dist $(pwd)/node_modules/@unirep/circuits

14
scripts/loadBeta.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -e
WORKDIR=$(mktemp)
TARFILE=$WORKDIR/unirep-beta-1-3.tar.gz
wget https://pub-0a2a0097caa84eb18d3e5c165665bffb.r2.dev/unirep-beta-1-3.tar.gz -P $WORKDIR --progress=bar:force:noscroll
shasum -a 256 $TARFILE | grep '3bf9a8ed793c4351be4d673c46b203bf341a32a31a41d2b54d16815264a50e12'
rm -rf node_modules/@unirep
tar -xzf $TARFILE -C node_modules
mv node_modules/unirep-beta-1-3 node_modules/@unirep

13
scripts/loadKeys.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
WORKDIR=$(mktemp)
TARFILE=$WORKDIR/keys.tar.gz
wget https://keys.unirep.io/2-beta-1/keys.tar.gz -P $WORKDIR --progress=bar:force:noscroll
shasum -a 256 $TARFILE | grep '99891da0525f617ba382e18e2cc6c9b55d45b55711dd789403d31b3945e44e34'
tar -xzf $TARFILE -C packages/relay
mv packages/relay/zksnarkBuild packages/relay/keys

74
scripts/start.mjs Normal file
View File

@@ -0,0 +1,74 @@
import { spawn, exec } from 'child_process'
import fetch from 'node-fetch'
console.log('Starting a hardhat node...')
const providerURL = `http://127.0.0.1:8545`
const serverURL = `http://127.0.0.1:8000`
const frontendURL = `http://127.0.0.1:3000`
const hardhat = spawn('yarn contracts hardhat node', { shell: true })
hardhat.stderr.on('data', (data) => {
console.error(`hardhat stderr: ${data}`)
})
hardhat.on('close', (code) => {
console.error(`hardhat exit with code: ${code}`)
process.exit(code)
})
for (;;) {
await new Promise((r) => setTimeout(r, 1000))
try {
await fetch(providerURL, { method: 'POST' }).catch(() => {})
break
} catch (_) {}
}
// deploy a Unirep contract and the attester contract
await new Promise((rs, rj) =>
exec('yarn contracts deploy', (err) =>
err ? rj(err) : rs(console.log('Contract has been deployed'))
)
)
// start relay
const relay = spawn('yarn relay start', { shell: true })
relay.stderr.on('data', (data) => {
console.error(`relay stderr: ${data}`)
})
relay.stdout.on('data', (data) => {
console.log(`${data}`)
})
relay.on('close', (code) => {
console.error(`relay exit with code: ${code}`)
hardhat.kill()
process.exit(code)
})
for (;;) {
await new Promise((r) => setTimeout(r, 1000))
try {
await fetch(serverURL, { method: 'POST' }).catch(() => {})
break
} catch (_) {}
}
console.log('Relay started')
// start frontend
const frontend = spawn('yarn frontend start', { shell: true })
frontend.stderr.on('data', (data) => {
console.error(`frontend stderr: ${data}`)
})
frontend.stdout.on('data', (data) => {
console.log(`${data}`)
})
frontend.on('close', (code) => {
console.error(`frontend exit with code: ${code}`)
hardhat.kill()
relay.kill()
process.exit(code)
})
for (;;) {
await new Promise((r) => setTimeout(r, 1000))
try {
await fetch(frontend, { method: 'POST' }).catch(() => {})
break
} catch (_) {}
}
console.log(`Start a frontend at ${frontendURL}`)

14
tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"noImplicitThis": false,
"resolveJsonModule": true,
"noImplicitAny": false,
"declaration": true,
"skipLibCheck": true
},
"exclude": ["**/node_modules"]
}

9751
yarn.lock Normal file

File diff suppressed because it is too large Load Diff