mirror of
https://github.com/faa/zk-keeper.git
synced 2026-01-09 14:18:04 -05:00
initial commit
This commit is contained in:
33
README.md
Normal file
33
README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Description (WIP)
|
||||
|
||||
ZK-Keeper is a browser plugin which enables Zero knowledge identity management and proof generation.
|
||||
Currently it supports operations for Semaphore and RLN gadgets.
|
||||
|
||||
This plugin is still in development phase.
|
||||
|
||||
The following features are supported currently:
|
||||
- Identity secret and Identity commitment generation
|
||||
- Semaphore ZK-Proof generation
|
||||
- RLN ZK-Proof generation
|
||||
|
||||
The plugin uses the [zk-kit library](https://github.com/appliedzkp/zk-kit).
|
||||
|
||||
Proof generation is enabled in two ways:
|
||||
- by providing merkle witness directly
|
||||
- by providing a secure service address from which the merkle witness should be obtained
|
||||
|
||||
# Development
|
||||
|
||||
1. `npm install`
|
||||
2. `npm run dev`
|
||||
3. Load the dist directory as an unpacked extension from your browser.
|
||||
|
||||
# Demo
|
||||
|
||||
1. `npm run dev`
|
||||
2. `npm run merkle`
|
||||
3. `npm run serve`
|
||||
4. `cd demo && npm run demo`
|
||||
|
||||
To run the demo and generate proofs, you additionally need the circuit files for Semaphore and RLN. For compatible Semaphore and RLN zk files you can use the following [link](https://drive.google.com/file/d/1Yi14jwly70VwMSuqJrPCc3j15MWeE7mc/view?usp=sharing).
|
||||
Please extract the files into a directory named `zkeyFiles` at the root of this repository.
|
||||
13
demo/index.html
Normal file
13
demo/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>ZK-keeper demo</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
232
demo/index.tsx
Normal file
232
demo/index.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { genExternalNullifier, RLN } from '@zk-kit/protocols'
|
||||
import { bigintToHex } from 'bigint-conversion'
|
||||
import { ZkIdentity } from '@zk-kit/identity'
|
||||
|
||||
import { ToastContainer, toast } from 'react-toastify'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
|
||||
const semaphorePath = {
|
||||
circuitFilePath: 'http://localhost:8095/semaphore/semaphore.wasm',
|
||||
zkeyFilePath: 'http://localhost:8095/semaphore/semaphore_final.zkey'
|
||||
}
|
||||
|
||||
const rlnPath = {
|
||||
circuitFilePath: 'http://localhost:8095/rln/rln.wasm',
|
||||
zkeyFilePath: 'http://localhost:8095/rln/rln_final.zkey'
|
||||
}
|
||||
|
||||
const merkleStorageAddress = 'http://localhost:8090/merkleProof'
|
||||
|
||||
enum MerkleProofType {
|
||||
STORAGE_ADDRESS,
|
||||
ARTIFACTS
|
||||
}
|
||||
|
||||
const genMockIdentityCommitments = (): string[] => {
|
||||
let identityCommitments: string[] = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const mockIdentity = new ZkIdentity()
|
||||
let idCommitment = bigintToHex(mockIdentity.genIdentityCommitment())
|
||||
|
||||
identityCommitments.push(idCommitment)
|
||||
}
|
||||
return identityCommitments
|
||||
}
|
||||
|
||||
function NotConnected() {
|
||||
return <div>Please connect to ZK-Keeper to continue.</div>
|
||||
}
|
||||
|
||||
function NoActiveIDCommitment() {
|
||||
return <div>Please set an active Identity Commitment in the ZK-Keeper plugin to continue.</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [client, setClient] = useState(null)
|
||||
const [isLocked, setIsLocked] = useState(true)
|
||||
const [identityCommitment, setIdentityCommitment] = useState('')
|
||||
const mockIdentityCommitments: string[] = genMockIdentityCommitments()
|
||||
|
||||
const genSemaphoreProof = async (proofType: MerkleProofType = MerkleProofType.STORAGE_ADDRESS) => {
|
||||
const externalNullifier = genExternalNullifier('voting-1')
|
||||
const signal = '0x111'
|
||||
|
||||
let storageAddressOrArtifacts: any = `${merkleStorageAddress}/Semaphore`
|
||||
if (proofType === MerkleProofType.ARTIFACTS) {
|
||||
if (!mockIdentityCommitments.includes(identityCommitment)) {
|
||||
mockIdentityCommitments.push(identityCommitment)
|
||||
}
|
||||
storageAddressOrArtifacts = {
|
||||
leaves: mockIdentityCommitments,
|
||||
depth: 20,
|
||||
leavesPerNode: 2
|
||||
}
|
||||
}
|
||||
|
||||
let toastId
|
||||
try {
|
||||
toastId = toast('Generating semaphore proof...', {
|
||||
type: 'info',
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: false
|
||||
})
|
||||
|
||||
const proof = await client.semaphoreProof(
|
||||
externalNullifier,
|
||||
signal,
|
||||
semaphorePath.circuitFilePath,
|
||||
semaphorePath.zkeyFilePath,
|
||||
storageAddressOrArtifacts
|
||||
)
|
||||
|
||||
toast('Semaphore proof generated successfully!', { type: 'success' })
|
||||
} catch (e) {
|
||||
toast('Error while generating Semaphore proof!', { type: 'error' })
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
toast.dismiss(toastId)
|
||||
}
|
||||
|
||||
const genRLNProof = async (proofType: MerkleProofType = MerkleProofType.STORAGE_ADDRESS) => {
|
||||
const externalNullifier = genExternalNullifier('voting-1')
|
||||
const signal = '0x111'
|
||||
const rlnIdentifier = RLN.genIdentifier()
|
||||
const rlnIdentifierHex = bigintToHex(rlnIdentifier)
|
||||
|
||||
let storageAddressOrArtifacts: any = `${merkleStorageAddress}/RLN`
|
||||
|
||||
if (proofType === MerkleProofType.ARTIFACTS) {
|
||||
if (!mockIdentityCommitments.includes(identityCommitment)) {
|
||||
mockIdentityCommitments.push(identityCommitment)
|
||||
}
|
||||
|
||||
storageAddressOrArtifacts = {
|
||||
leaves: mockIdentityCommitments,
|
||||
depth: 15,
|
||||
leavesPerNode: 2
|
||||
}
|
||||
}
|
||||
|
||||
let circuitPath = rlnPath.circuitFilePath
|
||||
let zkeyFilePath = rlnPath.zkeyFilePath
|
||||
|
||||
let toastId
|
||||
try {
|
||||
toastId = toast('Generating RLN proof...', {
|
||||
type: 'info',
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: false
|
||||
})
|
||||
|
||||
const proof = await client.rlnProof(
|
||||
externalNullifier,
|
||||
signal,
|
||||
circuitPath,
|
||||
zkeyFilePath,
|
||||
storageAddressOrArtifacts,
|
||||
rlnIdentifierHex
|
||||
)
|
||||
|
||||
toast('RLN proof generated successfully!', { type: 'success' })
|
||||
} catch (e) {
|
||||
toast('Error while generating RLN proof!', { type: 'error' })
|
||||
console.error(e)
|
||||
}
|
||||
toast.dismiss(toastId)
|
||||
}
|
||||
|
||||
const getIdentityCommitment = async () => {
|
||||
const idCommitment = await client.getActiveIdentity()
|
||||
setIdentityCommitment(idCommitment)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
;(async function IIFE() {
|
||||
initClient()
|
||||
|
||||
if (client) {
|
||||
await getIdentityCommitment()
|
||||
await client.on('identityChanged', (idCommitment) => {
|
||||
setIdentityCommitment(idCommitment)
|
||||
})
|
||||
|
||||
await client.on('logout', async () => {
|
||||
setIdentityCommitment('')
|
||||
setIsLocked(true)
|
||||
})
|
||||
|
||||
await client.on('login', async () => {
|
||||
setIsLocked(false)
|
||||
await getIdentityCommitment()
|
||||
})
|
||||
}
|
||||
})()
|
||||
}, [client])
|
||||
|
||||
const initClient = async () => {
|
||||
const { zkpr } = window as any
|
||||
const client = await zkpr.connect()
|
||||
setClient(client)
|
||||
setIsLocked(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!client || isLocked ? (
|
||||
<NotConnected />
|
||||
) : identityCommitment === '' || identityCommitment === null ? (
|
||||
<NoActiveIDCommitment />
|
||||
) : (
|
||||
<div>
|
||||
<div>
|
||||
<h2>Semaphore</h2>
|
||||
<button onClick={() => genSemaphoreProof(MerkleProofType.STORAGE_ADDRESS)}>
|
||||
Generate proof from Merkle proof storage address
|
||||
</button>{' '}
|
||||
<br />
|
||||
<br />
|
||||
<button onClick={() => genSemaphoreProof(MerkleProofType.ARTIFACTS)}>
|
||||
Generate proof from Merkle proof artifacts
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<h2>RLN</h2>
|
||||
<button onClick={() => genRLNProof(MerkleProofType.STORAGE_ADDRESS)}>
|
||||
Generate proof from Merkle proof storage address
|
||||
</button>{' '}
|
||||
<br />
|
||||
<br />
|
||||
<button onClick={() => genRLNProof(MerkleProofType.ARTIFACTS)}>
|
||||
Generate proof from Merkle proof artifacts
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div>
|
||||
<h2>Get identity commitment</h2>
|
||||
<button onClick={() => getIdentityCommitment()}>Get</button> <br />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div>
|
||||
<h2>Identity commitment for active identity:</h2>
|
||||
<p>{identityCommitment}</p>
|
||||
</div>
|
||||
|
||||
<ToastContainer newestOnTop={true} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const root = document.getElementById('root')
|
||||
|
||||
ReactDOM.render(<App />, root)
|
||||
14290
demo/package-lock.json
generated
Normal file
14290
demo/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
demo/package.json
Normal file
17
demo/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "zk-keeper-demo",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"start": "parcel serve --open --no-cache index.html"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-toastify": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.18",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"parcel": "^2.0.0-rc.0"
|
||||
}
|
||||
}
|
||||
BIN
dist/icon-128.png
vendored
Normal file
BIN
dist/icon-128.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
dist/icon-16.png
vendored
Normal file
BIN
dist/icon-16.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 423 B |
BIN
dist/icon-48.png
vendored
Normal file
BIN
dist/icon-48.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
134792
dist/index.e15ca01d.js
vendored
Normal file
134792
dist/index.e15ca01d.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/index.e15ca01d.js.map
vendored
Normal file
1
dist/index.e15ca01d.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
dist/index.html
vendored
Normal file
7
dist/index.html
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<script src="/index.e15ca01d.js" defer=""></script>
|
||||
</body>
|
||||
53
dist/manifest.json
vendored
Normal file
53
dist/manifest.json
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "ZK-KEEPER",
|
||||
"description": "Extension that stores credentials and creates semaphore proofs",
|
||||
"version": "1.0.0",
|
||||
"browser_action": {
|
||||
"default_icon": "icon-16.png",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"background": {
|
||||
"scripts": [
|
||||
"js/backgroundPage.js"
|
||||
],
|
||||
"persistent": true
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"file://*/*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"js": [
|
||||
"js/content.js"
|
||||
],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"16": "icon-16.png",
|
||||
"48": "icon-48.png",
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'; worker-src 'self' data:",
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"activeTab",
|
||||
"notifications",
|
||||
"http://*/",
|
||||
"https://*/",
|
||||
"fileSystem",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"proxy",
|
||||
"storage",
|
||||
"unlimitedStorage",
|
||||
"<all_urls>"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"js/injected.js"
|
||||
]
|
||||
}
|
||||
16
dist/popup.html
vendored
Normal file
16
dist/popup.html
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html width="357" height="600">
|
||||
<head>
|
||||
<title>zk keeper</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w==" crossorigin="anonymous" />
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
||||
<script src="js/popup.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="popup"></div>
|
||||
<div id="modal"></div>
|
||||
</body>
|
||||
</html>
|
||||
2
externals/worker_threads.js
vendored
Normal file
2
externals/worker_threads.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = window.worker_threads
|
||||
20
jest.config.json
Normal file
20
jest.config.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"transform": {
|
||||
"\\.(ts|tsx)": "ts-jest"
|
||||
},
|
||||
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$",
|
||||
"moduleFileExtensions": ["ts", "js", "tsx"],
|
||||
"moduleNameMapper": {
|
||||
"@src/(.*)$": "<rootDir>/src/$1"
|
||||
},
|
||||
"coveragePathIgnorePatterns": ["/node_modules/", "/test/"],
|
||||
"collectCoverageFrom": ["src/**/*.{ts,tsx}"],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 90,
|
||||
"functions": 95,
|
||||
"lines": 95,
|
||||
"statements": 95
|
||||
}
|
||||
}
|
||||
}
|
||||
46074
package-lock.json
generated
Normal file
46074
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
130
package.json
Normal file
130
package.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"name": "zk-keeper",
|
||||
"version": "1.0.0",
|
||||
"description": "ZK-Keeper, zero knowledge identity management and proof generation tool",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production webpack --config webpack.prod.js",
|
||||
"dev": "NODE_ENV=development webpack -w --config webpack.dev.js",
|
||||
"serve": "./scripts/serve.sh",
|
||||
"merkle": "node scripts/mock-merkle-proof.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"prettier": "prettier -c .",
|
||||
"prettier:fix": "prettier -w .",
|
||||
"commit": "cz",
|
||||
"precommit": "lint-staged"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"typescript",
|
||||
"chrome",
|
||||
"extension",
|
||||
"boilerplate"
|
||||
],
|
||||
"author": "Privacy and Scaling explorations team",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/appliedzkp/zk-keeper"
|
||||
},
|
||||
"homepage": "https://github.com/appliedzkp/zk-keeper",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.14.5",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@commitlint/cli": "^15.0.0",
|
||||
"@commitlint/config-conventional": "^15.0.0",
|
||||
"@types/chrome": "^0.0.124",
|
||||
"@types/eventemitter2": "^4.1.0",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/node": "^14.11.8",
|
||||
"@types/react": "^16.9.52",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/redux-logger": "^3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.5.0",
|
||||
"@typescript-eslint/parser": "^4.4.1",
|
||||
"assert": "^2.0.0",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-jest": "^26.5.2",
|
||||
"babel-loader": "^8.2.2",
|
||||
"commitizen": "^4.2.4",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"css-loader": "^4.3.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-config-airbnb": "^19.0.1",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jest": "^25.3.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.27.1",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"express": "^4.17.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"http-server": "^14.0.0",
|
||||
"image-webpack-loader": "^7.0.1",
|
||||
"jest": "^27.5.1",
|
||||
"lint-staged": "^12.1.2",
|
||||
"npm-force-resolutions": "0.0.10",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^2.5.0",
|
||||
"react-docgen-typescript-loader": "^3.7.2",
|
||||
"react-docgen-typescript-webpack-plugin": "^1.1.0",
|
||||
"sass-loader": "^10.0.3",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"ts-loader": "^8.0.5",
|
||||
"typescript": "^4.5.0",
|
||||
"webpack": "^5.52.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-merge": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dimensiondev/metamask-extension-provider": "https://github.com/DimensionDev/extension-provider/tarball/master",
|
||||
"@interep/identity": "^0.1.1",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@zk-kit/identity": "^1.4.1",
|
||||
"@zk-kit/protocols": "^1.8.2",
|
||||
"axios": "^0.24.0",
|
||||
"bigint-conversion": "^2.1.12",
|
||||
"browserify": "^17.0.0",
|
||||
"circomlibjs": "^0.0.8",
|
||||
"classnames": "^2.2.6",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"eventemitter2": "^6.4.4",
|
||||
"extensionizer": "^1.0.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ffjavascript": "0.2.39",
|
||||
"link-preview-js": "^2.1.13",
|
||||
"node-sass": "^6.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-jazzicon": "^0.1.3",
|
||||
"react-redux": "^7.2.3",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"redux": "^4.0.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"web3": "^1.5.3",
|
||||
"webextension-polyfill-ts": "^0.20.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
69
scripts/mock-merkle-proof.js
Normal file
69
scripts/mock-merkle-proof.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const express = require('express')
|
||||
const { generateMerkleProof } = require('@zk-kit/protocols')
|
||||
const { ZkIdentity } = require('@zk-kit/identity')
|
||||
const { bigintToHex, hexToBigint } = require('bigint-conversion')
|
||||
|
||||
const DEPTH_RLN = 15
|
||||
const NUMBER_OF_LEAVES_RLN = 2
|
||||
const DEPTH_SEMAPHORE = 20
|
||||
const NUMBER_OF_LEAVES_SEMAPHORE = 2
|
||||
const ZERO_VALUE = BigInt(0)
|
||||
|
||||
const serializeMerkleProof = (merkleProof) => {
|
||||
const serialized = {}
|
||||
serialized.root = bigintToHex(merkleProof.root)
|
||||
serialized.siblings = merkleProof.siblings.map((siblings) =>
|
||||
Array.isArray(siblings) ? siblings.map((element) => bigintToHex(element)) : bigintToHex(siblings)
|
||||
)
|
||||
serialized.pathIndices = merkleProof.pathIndices
|
||||
serialized.leaf = bigintToHex(merkleProof.leaf)
|
||||
return serialized
|
||||
}
|
||||
|
||||
const generateMerkleProofRLN = (identityCommitments, identityCommitment) => {
|
||||
return generateMerkleProof(DEPTH_RLN, ZERO_VALUE, NUMBER_OF_LEAVES_RLN, identityCommitments, identityCommitment)
|
||||
}
|
||||
|
||||
const generateMerkleProofSemaphore = (identityCommitments, identityCommitment) => {
|
||||
return generateMerkleProof(
|
||||
DEPTH_SEMAPHORE,
|
||||
ZERO_VALUE,
|
||||
NUMBER_OF_LEAVES_SEMAPHORE,
|
||||
identityCommitments,
|
||||
identityCommitment
|
||||
)
|
||||
}
|
||||
|
||||
const identityCommitments = []
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const mockIdentity = new ZkIdentity()
|
||||
identityCommitments.push(mockIdentity.genIdentityCommitment())
|
||||
}
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
|
||||
app.post('/merkleProof/:type', (req, res) => {
|
||||
let type = req.params.type
|
||||
let { identityCommitment } = req.body
|
||||
identityCommitment = hexToBigint(identityCommitment)
|
||||
|
||||
if (!identityCommitments.includes(identityCommitment)) {
|
||||
identityCommitments.push(identityCommitment)
|
||||
}
|
||||
const merkleProof =
|
||||
type === 'RLN'
|
||||
? generateMerkleProofRLN(identityCommitments, identityCommitment)
|
||||
: generateMerkleProofSemaphore(identityCommitments, identityCommitment)
|
||||
|
||||
const serializedMerkleProof = serializeMerkleProof(merkleProof)
|
||||
console.log('Sending proof with root: ', serializedMerkleProof.root)
|
||||
res.send({ merkleProof: serializedMerkleProof })
|
||||
})
|
||||
|
||||
app.listen(8090, () => {
|
||||
console.log('Merkle service is listening')
|
||||
})
|
||||
6
scripts/serve.sh
Executable file
6
scripts/serve.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
cd ../zkeyFiles
|
||||
http-server -p 8095 --cors
|
||||
|
||||
30
src/background/backgroundPage.ts
Normal file
30
src/background/backgroundPage.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
import { Request } from '@src/types'
|
||||
import ZkKepperController from './zk-kepeer'
|
||||
|
||||
// TODO consider adding inTest env
|
||||
const app: ZkKepperController = new ZkKepperController()
|
||||
|
||||
app.initialize().then(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
browser.runtime.onMessage.addListener(async (request: Request, _) => {
|
||||
try {
|
||||
const res = await app.handle(request)
|
||||
return [null, res]
|
||||
} catch (e: any) {
|
||||
return [e.message, null]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
||||
if (reason === 'install') {
|
||||
// TODO open html where password will be interested
|
||||
// browser.tabs.create({
|
||||
// url: 'popup.html'
|
||||
// });
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// browser.tabs.create({ url: 'popup.html' });
|
||||
}
|
||||
})
|
||||
50
src/background/controllers/browser-utils.ts
Normal file
50
src/background/controllers/browser-utils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
|
||||
class BrowserUtils {
|
||||
cached: any | null
|
||||
|
||||
constructor() {
|
||||
browser.windows.onRemoved.addListener((windowId) => {
|
||||
if (this.cached?.id === windowId) {
|
||||
this.cached = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
createTab = async (options: any) => browser.tabs.create(options)
|
||||
|
||||
createWindow = async (options: any) => browser.windows.create(options)
|
||||
|
||||
openPopup = async () => {
|
||||
if (this.cached) {
|
||||
this.focusWindow(this.cached.id)
|
||||
return this.cached
|
||||
}
|
||||
|
||||
const tab = await this.createTab({ url: 'popup.html', active: false })
|
||||
|
||||
// TODO add this in config/constants...
|
||||
const popup = await this.createWindow({
|
||||
tabId: tab.id,
|
||||
type: 'popup',
|
||||
focused: true,
|
||||
width: 357,
|
||||
height: 600
|
||||
})
|
||||
|
||||
this.cached = popup
|
||||
return popup
|
||||
}
|
||||
|
||||
closePopup = async () => {
|
||||
if (this.cached) {
|
||||
browser.windows.remove(this.cached.id)
|
||||
}
|
||||
}
|
||||
|
||||
focusWindow = (windowId) => browser.windows.update(windowId, { focused: true })
|
||||
|
||||
getAllWindows = () => browser.windows.getAll()
|
||||
}
|
||||
|
||||
export default new BrowserUtils()
|
||||
35
src/background/controllers/handler.ts
Normal file
35
src/background/controllers/handler.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Request } from '@src/types'
|
||||
|
||||
type Chain = {
|
||||
middlewares: Array<(payload: any, meta?: any) => Promise<any>>
|
||||
handler: (payload: any, meta?: any) => Promise<any>
|
||||
}
|
||||
|
||||
export default class Handler {
|
||||
private handlers: Map<string, Chain>
|
||||
|
||||
constructor() {
|
||||
this.handlers = new Map()
|
||||
}
|
||||
|
||||
add = (method: string, ...args: Array<(payload: any, meta?: any) => any>) => {
|
||||
const handler = args[args.length - 1]
|
||||
const middlewares = args.slice(0, args.length - 1)
|
||||
this.handlers.set(method, { middlewares, handler })
|
||||
}
|
||||
|
||||
handle = async (request: Request): Promise<any> => {
|
||||
const { method } = request
|
||||
const handler: Chain | undefined = this.handlers.get(method)
|
||||
if (!handler) throw new Error(`method: ${method} not detected`)
|
||||
|
||||
let { payload, meta } = request
|
||||
|
||||
for (const middleware of handler.middlewares) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
payload = await middleware(payload, meta)
|
||||
}
|
||||
|
||||
return handler.handler(payload, meta)
|
||||
}
|
||||
}
|
||||
73
src/background/controllers/request-manager.ts
Normal file
73
src/background/controllers/request-manager.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import pushMessage from '@src/util/pushMessage'
|
||||
import { EventEmitter2 } from 'eventemitter2'
|
||||
import { PendingRequest, PendingRequestType, RequestResolutionAction } from '@src/types'
|
||||
import { setPendingRequest } from '@src/ui/ducks/requests'
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
import BrowserUtils from './browser-utils'
|
||||
|
||||
let nonce = 0
|
||||
|
||||
export default class RequestManager extends EventEmitter2 {
|
||||
private pendingRequests: Array<PendingRequest>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.pendingRequests = []
|
||||
}
|
||||
|
||||
getRequests = (): PendingRequest[] => this.pendingRequests
|
||||
|
||||
finalizeRequest = async (action: RequestResolutionAction<any>): Promise<boolean> => {
|
||||
const { id } = action
|
||||
if (!id) throw new Error('id not provided')
|
||||
// TODO add some mutex lock just in case something strange occurs
|
||||
this.pendingRequests = this.pendingRequests.filter((pendingRequest: PendingRequest) => pendingRequest.id !== id)
|
||||
this.emit(`${id}:finalized`, action)
|
||||
await pushMessage(setPendingRequest(this.pendingRequests))
|
||||
return true
|
||||
}
|
||||
|
||||
addToQueue = async (type: PendingRequestType, payload?: any): Promise<string> => {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
const id: string = `${nonce++}`
|
||||
this.pendingRequests.push({ id, type, payload })
|
||||
await pushMessage(setPendingRequest(this.pendingRequests))
|
||||
return id
|
||||
}
|
||||
|
||||
newRequest = async (type: PendingRequestType, payload?: any) => {
|
||||
const id: string = await this.addToQueue(type, payload)
|
||||
const popup = await BrowserUtils.openPopup()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const onPopupClose = (windowId: number) => {
|
||||
if (windowId === popup.id) {
|
||||
reject(new Error('user rejected.'))
|
||||
browser.windows.onRemoved.removeListener(onPopupClose)
|
||||
}
|
||||
}
|
||||
|
||||
browser.windows.onRemoved.addListener(onPopupClose)
|
||||
|
||||
this.once(`${id}:finalized`, (action: RequestResolutionAction<any>) => {
|
||||
browser.windows.onRemoved.removeListener(onPopupClose)
|
||||
switch (action.status) {
|
||||
case 'accept':
|
||||
resolve(action.data)
|
||||
return
|
||||
case 'reject':
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject(new Error('user rejected.'))
|
||||
return
|
||||
default:
|
||||
reject(new Error(`action: ${action.status} not supproted`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handlePopup = async () => {
|
||||
const newPopup = await BrowserUtils.openPopup()
|
||||
if (!newPopup?.id) throw new Error('Something went wrong in opening popup')
|
||||
}
|
||||
}
|
||||
37
src/background/identity-decorater.ts
Normal file
37
src/background/identity-decorater.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ZkIdentity } from '@zk-kit/identity'
|
||||
import { SerializedIdentity, IdentityMetadata } from '@src/types'
|
||||
|
||||
export default class ZkIdentityDecorater {
|
||||
public zkIdentity: ZkIdentity
|
||||
|
||||
public metadata: IdentityMetadata
|
||||
|
||||
constructor(zkIdentity: ZkIdentity, metadata: IdentityMetadata) {
|
||||
this.zkIdentity = zkIdentity
|
||||
this.metadata = metadata
|
||||
}
|
||||
|
||||
genIdentityCommitment = (): bigint => {
|
||||
const idCommitment = this.zkIdentity.genIdentityCommitment()
|
||||
return idCommitment
|
||||
}
|
||||
|
||||
serialize = (): string => {
|
||||
const serialized = {
|
||||
secret: this.zkIdentity.serializeIdentity(),
|
||||
metadata: this.metadata
|
||||
}
|
||||
|
||||
return JSON.stringify(serialized)
|
||||
}
|
||||
|
||||
static genFromSerialized = (serialized: string): ZkIdentityDecorater => {
|
||||
const data: SerializedIdentity = JSON.parse(serialized)
|
||||
if (!data.metadata) throw new Error('Metadata missing')
|
||||
if (!data.secret) throw new Error('Secret missing')
|
||||
|
||||
// TODO overload zkIdentity function to work both with array and string
|
||||
const zkIdentity = new ZkIdentity(2, data.secret)
|
||||
return new ZkIdentityDecorater(zkIdentity, data.metadata)
|
||||
}
|
||||
}
|
||||
25
src/background/identity-factory.test.ts
Normal file
25
src/background/identity-factory.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import ZkIdentityDecorater from './identity-decorater'
|
||||
import identityFactory from './identity-factory'
|
||||
|
||||
describe('# identityFactory', () => {
|
||||
it('Should not create a random identity without the required parameters', async () => {
|
||||
const fun = () => identityFactory('random', undefined as any)
|
||||
|
||||
await expect(fun).rejects.toThrow("Parameter 'config' is not defined")
|
||||
})
|
||||
|
||||
it('Should create a random identity', async () => {
|
||||
const identity1 = await identityFactory('random', { name: 'name' })
|
||||
const identity2 = ZkIdentityDecorater.genFromSerialized(identity1.serialize())
|
||||
|
||||
expect(identity1.zkIdentity.getTrapdoor()).toEqual(identity2.zkIdentity.getTrapdoor())
|
||||
expect(identity1.zkIdentity.getNullifier()).toEqual(identity2.zkIdentity.getNullifier())
|
||||
expect(identity1.zkIdentity.getSecretHash()).toEqual(identity2.zkIdentity.getSecretHash())
|
||||
})
|
||||
|
||||
it('Should not create an InterRep identity without the required parameters', async () => {
|
||||
const fun = () => identityFactory('interrep', undefined as any)
|
||||
|
||||
await expect(fun).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
53
src/background/identity-factory.ts
Normal file
53
src/background/identity-factory.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { IdentityMetadata } from '@src/types'
|
||||
import { ZkIdentity } from '@zk-kit/identity'
|
||||
import createIdentity from '@interep/identity'
|
||||
import checkParameter from '@src/util/checkParameter'
|
||||
import ZkIdentityDecorater from './identity-decorater'
|
||||
|
||||
const createInterrepIdentity = async (config: any): Promise<ZkIdentityDecorater> => {
|
||||
checkParameter(config, 'config', 'object')
|
||||
|
||||
const { web2Provider, nonce = 0, name, web3, walletInfo } = config
|
||||
|
||||
checkParameter(name, 'name', 'string')
|
||||
checkParameter(web2Provider, 'provider', 'string')
|
||||
checkParameter(web3, 'web3', 'object')
|
||||
checkParameter(walletInfo, 'walletInfo', 'object')
|
||||
|
||||
const sign = (message: string) => web3.eth.personal.sign(message, walletInfo?.account)
|
||||
|
||||
const identity: ZkIdentity = await createIdentity(sign, web2Provider, nonce)
|
||||
const metadata: IdentityMetadata = {
|
||||
account: walletInfo.account,
|
||||
name,
|
||||
provider: 'interrep'
|
||||
}
|
||||
|
||||
return new ZkIdentityDecorater(identity, metadata)
|
||||
}
|
||||
|
||||
const createRandomIdentity = (config: any): ZkIdentityDecorater => {
|
||||
checkParameter(config, 'config', 'object')
|
||||
const { name } = config
|
||||
|
||||
checkParameter(name, 'name', 'string')
|
||||
|
||||
const identity: ZkIdentity = new ZkIdentity()
|
||||
const metadata: IdentityMetadata = {
|
||||
account: '',
|
||||
name: config.name,
|
||||
provider: 'random'
|
||||
}
|
||||
|
||||
return new ZkIdentityDecorater(identity, metadata)
|
||||
}
|
||||
|
||||
const strategiesMap = {
|
||||
random: createRandomIdentity,
|
||||
interrep: createInterrepIdentity
|
||||
}
|
||||
|
||||
const identityFactory = async (strategy: keyof typeof strategiesMap, config: any): Promise<ZkIdentityDecorater> =>
|
||||
strategiesMap[strategy](config)
|
||||
|
||||
export default identityFactory
|
||||
120
src/background/services/approval.ts
Normal file
120
src/background/services/approval.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import SimpleStorage from './simple-storage'
|
||||
import LockService from './lock'
|
||||
|
||||
const DB_KEY = '@APPROVED@'
|
||||
|
||||
export default class ApprovalService extends SimpleStorage {
|
||||
private allowedHosts: Array<string>
|
||||
|
||||
permissions: SimpleStorage
|
||||
|
||||
constructor() {
|
||||
super(DB_KEY)
|
||||
this.allowedHosts = []
|
||||
this.permissions = new SimpleStorage('@HOST_PERMISSIONS@')
|
||||
}
|
||||
|
||||
getAllowedHosts = () => this.allowedHosts
|
||||
|
||||
isApproved = (origin: string): boolean => this.allowedHosts.includes(origin)
|
||||
|
||||
unlock = async (): Promise<boolean> => {
|
||||
const encrypedArray: Array<string> = await this.get()
|
||||
if (!encrypedArray) return true
|
||||
|
||||
const promises: Array<Promise<string>> = encrypedArray.map((cipertext: string) =>
|
||||
LockService.decrypt(cipertext)
|
||||
)
|
||||
|
||||
this.allowedHosts = await Promise.all(promises)
|
||||
return true
|
||||
}
|
||||
|
||||
refresh = async () => {
|
||||
const encrypedArray: Array<string> = await this.get()
|
||||
if (!encrypedArray) {
|
||||
this.allowedHosts = []
|
||||
return
|
||||
}
|
||||
|
||||
const promises: Array<Promise<string>> = encrypedArray.map((cipertext: string) =>
|
||||
LockService.decrypt(cipertext)
|
||||
)
|
||||
|
||||
this.allowedHosts = await Promise.all(promises)
|
||||
}
|
||||
|
||||
getPermission = async (host: string) => {
|
||||
const store = await this.permissions.get()
|
||||
const permission = store ? store[host] : false
|
||||
return {
|
||||
noApproval: !!permission?.noApproval
|
||||
}
|
||||
}
|
||||
|
||||
setPermission = async (
|
||||
host: string,
|
||||
permission: {
|
||||
noApproval: boolean
|
||||
}
|
||||
) => {
|
||||
const { noApproval } = permission
|
||||
const existing = await this.getPermission(host)
|
||||
const newPer = {
|
||||
...existing,
|
||||
noApproval
|
||||
}
|
||||
|
||||
const store = await this.permissions.get()
|
||||
await this.permissions.set({
|
||||
...(store || {}),
|
||||
[host]: newPer
|
||||
})
|
||||
return newPer
|
||||
}
|
||||
|
||||
add = async (payload: { host: string; noApproval?: boolean }) => {
|
||||
const { host, noApproval } = payload
|
||||
|
||||
if (!host) throw new Error('No host provided')
|
||||
|
||||
if (this.allowedHosts.includes(host)) return
|
||||
|
||||
this.allowedHosts.push(host)
|
||||
|
||||
const promises: Array<Promise<string>> = this.allowedHosts.map((allowedHost: string) =>
|
||||
LockService.encrypt(allowedHost)
|
||||
)
|
||||
|
||||
const newValue: Array<string> = await Promise.all(promises)
|
||||
|
||||
await this.set(newValue)
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
remove = async (payload: any) => {
|
||||
const { host }: { host: string } = payload
|
||||
console.log(payload)
|
||||
if (!host) throw new Error('No address provided')
|
||||
|
||||
const index: number = this.allowedHosts.indexOf(host)
|
||||
if (index === -1) return
|
||||
|
||||
this.allowedHosts = [...this.allowedHosts.slice(0, index), ...this.allowedHosts.slice(index + 1)]
|
||||
|
||||
const promises: Array<Promise<string>> = this.allowedHosts.map((allowedHost: string) =>
|
||||
LockService.encrypt(allowedHost)
|
||||
)
|
||||
|
||||
const newValue: Array<string> = await Promise.all(promises)
|
||||
await this.set(newValue)
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
/** dev only */
|
||||
empty = async (): Promise<any> => {
|
||||
if (!(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test')) return
|
||||
await this.clear()
|
||||
await this.refresh()
|
||||
}
|
||||
}
|
||||
118
src/background/services/identity.ts
Normal file
118
src/background/services/identity.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { bigintToHex } from 'bigint-conversion'
|
||||
import pushMessage from '@src/util/pushMessage'
|
||||
import { setIdentities, setSelected } from '@src/ui/ducks/identities'
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
import { IdentityMetadata } from '@src/types'
|
||||
import SimpleStorage from './simple-storage'
|
||||
import LockService from './lock'
|
||||
import ZkIdentityDecorater from '../identity-decorater'
|
||||
|
||||
const DB_KEY = '@@IDS-t1@@'
|
||||
|
||||
export default class IdentityService extends SimpleStorage {
|
||||
identities: Map<string, ZkIdentityDecorater>
|
||||
activeIdentity?: ZkIdentityDecorater
|
||||
|
||||
constructor() {
|
||||
super(DB_KEY)
|
||||
this.identities = new Map()
|
||||
this.activeIdentity = undefined
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
unlock = async (_: any) => {
|
||||
const encryptedContent = await this.get()
|
||||
if (!encryptedContent) return true
|
||||
|
||||
const decrypted: any = await LockService.decrypt(encryptedContent)
|
||||
await this.loadInMemory(JSON.parse(decrypted))
|
||||
await this.setDefaultIdentity()
|
||||
|
||||
pushMessage(setIdentities(await this.getIdentities()))
|
||||
return true
|
||||
}
|
||||
|
||||
refresh = async () => {
|
||||
const encryptedContent = await this.get()
|
||||
if (!encryptedContent) return
|
||||
|
||||
const decrypted: any = await LockService.decrypt(encryptedContent)
|
||||
await this.loadInMemory(JSON.parse(decrypted))
|
||||
// if the first identity just added, set it to active
|
||||
if (this.identities.size === 1) {
|
||||
await this.setDefaultIdentity()
|
||||
}
|
||||
|
||||
pushMessage(setIdentities(await this.getIdentities()))
|
||||
}
|
||||
|
||||
loadInMemory = async (decrypted: any) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Object.entries(decrypted || {}).forEach(([_, value]) => {
|
||||
const identity: ZkIdentityDecorater = ZkIdentityDecorater.genFromSerialized(value as string)
|
||||
const identityCommitment: bigint = identity.genIdentityCommitment()
|
||||
this.identities.set(bigintToHex(identityCommitment), identity)
|
||||
})
|
||||
}
|
||||
|
||||
setDefaultIdentity = async () => {
|
||||
if (!this.identities.size) return
|
||||
|
||||
const firstKey: string = this.identities.keys().next().value
|
||||
this.activeIdentity = this.identities.get(firstKey)
|
||||
}
|
||||
|
||||
setActiveIdentity = async (identityCommitment: string) => {
|
||||
if (this.identities.has(identityCommitment)) {
|
||||
this.activeIdentity = this.identities.get(identityCommitment)
|
||||
pushMessage(setSelected(identityCommitment))
|
||||
const tabs = await browser.tabs.query({ active: true })
|
||||
for (const tab of tabs) {
|
||||
await browser.tabs.sendMessage(tab.id as number, setSelected(identityCommitment))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getActiveidentity = async (): Promise<ZkIdentityDecorater | undefined> => this.activeIdentity
|
||||
|
||||
getIdentityCommitments = async () => {
|
||||
const commitments: string[] = []
|
||||
for (const key of this.identities.keys()) {
|
||||
commitments.push(key)
|
||||
}
|
||||
return commitments
|
||||
}
|
||||
|
||||
getIdentities = async (): Promise<{ commitment: string; metadata: IdentityMetadata }[]> => {
|
||||
const commitments = await this.getIdentityCommitments()
|
||||
return commitments.map((commitment) => {
|
||||
const id = this.identities.get(commitment)
|
||||
return {
|
||||
commitment,
|
||||
metadata: id!.metadata
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
insert = async (newIdentity: ZkIdentityDecorater): Promise<boolean> => {
|
||||
const identityCommitment: string = bigintToHex(newIdentity.genIdentityCommitment())
|
||||
const existing: boolean = this.identities.has(identityCommitment)
|
||||
|
||||
if (existing) return false
|
||||
|
||||
const existingIdentites: string[] = []
|
||||
for (const identity of this.identities.values()) {
|
||||
existingIdentites.push(identity.serialize())
|
||||
}
|
||||
|
||||
const newValue: string[] = [...existingIdentites, newIdentity.serialize()]
|
||||
const ciphertext: string = await LockService.encrypt(JSON.stringify(newValue))
|
||||
|
||||
await this.set(ciphertext)
|
||||
await this.refresh()
|
||||
await this.setActiveIdentity(identityCommitment)
|
||||
return true
|
||||
}
|
||||
|
||||
getNumOfIdentites = (): number => this.identities.size
|
||||
}
|
||||
118
src/background/services/lock.ts
Normal file
118
src/background/services/lock.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
import pushMessage from '@src/util/pushMessage'
|
||||
import { setStatus } from '@src/ui/ducks/app'
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
import SimpleStorage from './simple-storage'
|
||||
|
||||
const passwordKey: string = '@password@'
|
||||
|
||||
class LockService extends SimpleStorage {
|
||||
private isUnlocked: boolean
|
||||
private password?: string
|
||||
private passwordChecker: string
|
||||
private unlockCB?: any
|
||||
|
||||
constructor() {
|
||||
super(passwordKey)
|
||||
this.isUnlocked = false
|
||||
this.password = undefined
|
||||
this.passwordChecker = 'Password is correct'
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when install event occurs
|
||||
*/
|
||||
setupPassword = async (password: string) => {
|
||||
const ciphertext: string = CryptoJS.AES.encrypt(this.passwordChecker, password).toString()
|
||||
await this.set(ciphertext)
|
||||
await this.unlock(password)
|
||||
await pushMessage(setStatus(await this.getStatus()))
|
||||
}
|
||||
|
||||
getStatus = async () => {
|
||||
const ciphertext = await this.get()
|
||||
|
||||
return {
|
||||
initialized: !!ciphertext,
|
||||
unlocked: this.isUnlocked
|
||||
}
|
||||
}
|
||||
|
||||
awaitUnlock = async () => {
|
||||
if (this.isUnlocked) return
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.unlockCB = resolve
|
||||
})
|
||||
}
|
||||
|
||||
onUnlocked = () => {
|
||||
if (this.unlockCB) {
|
||||
this.unlockCB()
|
||||
this.unlockCB = undefined
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
unlock = async (password: string): Promise<boolean> => {
|
||||
if (this.isUnlocked) return true
|
||||
|
||||
const ciphertext = await this.get()
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new Error('Something badly gone wrong (reinstallation probably required)')
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
throw new Error('Password is not provided')
|
||||
}
|
||||
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, password)
|
||||
const retrievedPasswordChecker: string = bytes.toString(CryptoJS.enc.Utf8)
|
||||
|
||||
if (retrievedPasswordChecker !== this.passwordChecker) {
|
||||
throw new Error('Incorrect password')
|
||||
}
|
||||
|
||||
this.password = password
|
||||
this.isUnlocked = true
|
||||
|
||||
const status = await this.getStatus()
|
||||
await pushMessage(setStatus(status))
|
||||
const tabs = await browser.tabs.query({ active: true })
|
||||
for (const tab of tabs) {
|
||||
await browser.tabs.sendMessage(tab.id as number, setStatus(status))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
logout = async (): Promise<boolean> => {
|
||||
this.isUnlocked = false
|
||||
this.password = undefined
|
||||
const status = await this.getStatus()
|
||||
await pushMessage(setStatus(status))
|
||||
const tabs = await browser.tabs.query({ active: true })
|
||||
for (const tab of tabs) {
|
||||
await browser.tabs.sendMessage(tab.id as number, setStatus(status))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
ensure = async (payload: any = null) => {
|
||||
if (!this.isUnlocked || !this.password) throw new Error('state is locked!')
|
||||
return payload
|
||||
}
|
||||
|
||||
encrypt = async (payload: string): Promise<string> => {
|
||||
await this.ensure()
|
||||
return CryptoJS.AES.encrypt(payload, this.password).toString()
|
||||
}
|
||||
|
||||
decrypt = async (ciphertext: string): Promise<string> => {
|
||||
await this.ensure()
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.password)
|
||||
return bytes.toString(CryptoJS.enc.Utf8)
|
||||
}
|
||||
}
|
||||
|
||||
export default new LockService()
|
||||
98
src/background/services/metamask.ts
Normal file
98
src/background/services/metamask.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import pushMessage from '@src/util/pushMessage'
|
||||
import createMetaMaskProvider from '@dimensiondev/metamask-extension-provider'
|
||||
import Web3 from 'web3'
|
||||
import { setAccount, setChainId, setNetwork, setWeb3Connecting } from '@src/ui/ducks/web3'
|
||||
import { WalletInfo } from '@src/types'
|
||||
|
||||
export default class MetamaskService {
|
||||
provider?: any
|
||||
web3?: Web3
|
||||
|
||||
constructor() {
|
||||
this.ensure()
|
||||
}
|
||||
|
||||
ensure = async (payload: any = null) => {
|
||||
if (!this.provider) {
|
||||
this.provider = await createMetaMaskProvider()
|
||||
}
|
||||
|
||||
if (this.provider) {
|
||||
if (!this.web3) {
|
||||
this.web3 = new Web3(this.provider)
|
||||
}
|
||||
|
||||
this.provider.on('accountsChanged', ([account]) => {
|
||||
pushMessage(setAccount(account))
|
||||
})
|
||||
|
||||
this.provider.on('chainChanged', async () => {
|
||||
const networkType = await this.web3?.eth.net.getNetworkType()
|
||||
const chainId = await this.web3?.eth.getChainId()
|
||||
|
||||
if (networkType) pushMessage(setNetwork(networkType))
|
||||
if (chainId) pushMessage(setChainId(chainId))
|
||||
})
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
getWeb3 = async (): Promise<Web3> => {
|
||||
if (!this.web3) throw new Error(`web3 is not initialized`)
|
||||
return this.web3
|
||||
}
|
||||
|
||||
getWalletInfo = async (): Promise<WalletInfo | null> => {
|
||||
await this.ensure()
|
||||
|
||||
if (!this.web3) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.provider?.selectedAddress) {
|
||||
const accounts = await this.web3.eth.requestAccounts()
|
||||
const networkType = await this.web3.eth.net.getNetworkType()
|
||||
const chainId = await this.web3.eth.getChainId()
|
||||
|
||||
if (!accounts.length) {
|
||||
throw new Error('No accounts found')
|
||||
}
|
||||
|
||||
return {
|
||||
account: accounts[0],
|
||||
networkType,
|
||||
chainId
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
connectMetamask = async () => {
|
||||
await pushMessage(setWeb3Connecting(true))
|
||||
|
||||
try {
|
||||
await this.ensure()
|
||||
|
||||
if (this.web3) {
|
||||
const accounts = await this.web3.eth.requestAccounts()
|
||||
const networkType = await this.web3.eth.net.getNetworkType()
|
||||
const chainId = await this.web3.eth.getChainId()
|
||||
|
||||
if (!accounts.length) {
|
||||
throw new Error('No accounts found')
|
||||
}
|
||||
|
||||
await pushMessage(setAccount(accounts[0]))
|
||||
await pushMessage(setNetwork(networkType))
|
||||
await pushMessage(setChainId(chainId))
|
||||
}
|
||||
|
||||
await pushMessage(setWeb3Connecting(false))
|
||||
} catch (e) {
|
||||
await pushMessage(setWeb3Connecting(false))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/background/services/protocols/interfaces.ts
Normal file
27
src/background/services/protocols/interfaces.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { MerkleProof, SemaphoreFullProof, SemaphoreSolidityProof } from '@zk-kit/protocols'
|
||||
import { MerkleProofArtifacts } from '@src/types'
|
||||
|
||||
export enum Protocol {
|
||||
SEMAPHORE,
|
||||
RLN,
|
||||
NRLN
|
||||
}
|
||||
|
||||
export interface SemaphoreProofRequest {
|
||||
externalNullifier: string
|
||||
signal: string
|
||||
merkleStorageAddress?: string
|
||||
circuitFilePath: string
|
||||
zkeyFilePath: string
|
||||
merkleProofArtifacts?: MerkleProofArtifacts
|
||||
merkleProof?: MerkleProof
|
||||
}
|
||||
|
||||
export interface RLNProofRequest extends SemaphoreProofRequest {
|
||||
rlnIdentifier: string
|
||||
}
|
||||
|
||||
export interface SemaphoreProof {
|
||||
fullProof: SemaphoreFullProof
|
||||
solidityProof: SemaphoreSolidityProof
|
||||
}
|
||||
59
src/background/services/protocols/rln.ts
Normal file
59
src/background/services/protocols/rln.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { RLN, MerkleProof, RLNFullProof, generateMerkleProof } from '@zk-kit/protocols'
|
||||
import { ZkIdentity } from '@zk-kit/identity'
|
||||
import { bigintToHex, hexToBigint } from 'bigint-conversion'
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
import { MerkleProofArtifacts } from '@src/types'
|
||||
import { RLNProofRequest } from './interfaces'
|
||||
import { deserializeMerkleProof } from './utils'
|
||||
|
||||
export default class RLNService {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async genProof(identity: ZkIdentity, request: RLNProofRequest): Promise<RLNFullProof> {
|
||||
try {
|
||||
const {
|
||||
circuitFilePath,
|
||||
zkeyFilePath,
|
||||
merkleStorageAddress,
|
||||
externalNullifier,
|
||||
signal,
|
||||
merkleProofArtifacts,
|
||||
rlnIdentifier
|
||||
} = request
|
||||
let merkleProof: MerkleProof
|
||||
|
||||
const identitySecretHash: bigint = identity.getSecretHash()
|
||||
const identityCommitment = identity.genIdentityCommitment()
|
||||
const identityCommitmentHex = bigintToHex(identityCommitment)
|
||||
const rlnIdentifierBigInt = hexToBigint(rlnIdentifier)
|
||||
if (merkleStorageAddress) {
|
||||
const response: AxiosResponse = await axios.post(merkleStorageAddress, {
|
||||
identityCommitment: identityCommitmentHex
|
||||
})
|
||||
|
||||
merkleProof = deserializeMerkleProof(response.data.merkleProof)
|
||||
} else {
|
||||
const proofArtifacts = merkleProofArtifacts as MerkleProofArtifacts
|
||||
const leaves = proofArtifacts.leaves.map((leaf) => hexToBigint(leaf))
|
||||
merkleProof = generateMerkleProof(
|
||||
proofArtifacts.depth,
|
||||
BigInt(0),
|
||||
proofArtifacts.leavesPerNode,
|
||||
leaves,
|
||||
identityCommitment
|
||||
)
|
||||
}
|
||||
|
||||
const witness = RLN.genWitness(
|
||||
identitySecretHash,
|
||||
merkleProof,
|
||||
externalNullifier,
|
||||
signal,
|
||||
rlnIdentifierBigInt
|
||||
)
|
||||
const fullProof: RLNFullProof = await RLN.genProof(witness, circuitFilePath, zkeyFilePath)
|
||||
return fullProof
|
||||
} catch (e) {
|
||||
throw new Error(`Error while generating RLN proof: ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/background/services/protocols/semaphore.ts
Normal file
75
src/background/services/protocols/semaphore.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Semaphore,
|
||||
MerkleProof,
|
||||
SemaphoreFullProof,
|
||||
SemaphoreSolidityProof,
|
||||
SemaphorePublicSignals,
|
||||
genSignalHash,
|
||||
generateMerkleProof
|
||||
} from '@zk-kit/protocols'
|
||||
import { ZkIdentity } from '@zk-kit/identity'
|
||||
import { bigintToHex, hexToBigint } from 'bigint-conversion'
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
import { MerkleProofArtifacts } from '@src/types'
|
||||
import { SemaphoreProof, SemaphoreProofRequest } from './interfaces'
|
||||
import { deserializeMerkleProof } from './utils'
|
||||
|
||||
export default class SemaphoreService {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async genProof(identity: ZkIdentity, request: SemaphoreProofRequest): Promise<SemaphoreProof> {
|
||||
try {
|
||||
const {
|
||||
circuitFilePath,
|
||||
zkeyFilePath,
|
||||
merkleStorageAddress,
|
||||
externalNullifier,
|
||||
signal,
|
||||
merkleProofArtifacts,
|
||||
merkleProof: _merkleProof
|
||||
} = request
|
||||
let merkleProof: MerkleProof
|
||||
const identityCommitment = identity.genIdentityCommitment()
|
||||
const identityCommitmentHex = bigintToHex(identityCommitment)
|
||||
|
||||
if (_merkleProof) {
|
||||
merkleProof = _merkleProof
|
||||
} else if (merkleStorageAddress) {
|
||||
const response: AxiosResponse = await axios.post(merkleStorageAddress, {
|
||||
identityCommitment: identityCommitmentHex
|
||||
})
|
||||
|
||||
merkleProof = deserializeMerkleProof(response.data.merkleProof)
|
||||
} else {
|
||||
const proofArtifacts = merkleProofArtifacts as MerkleProofArtifacts
|
||||
|
||||
const leaves = proofArtifacts.leaves.map((leaf) => hexToBigint(leaf))
|
||||
merkleProof = generateMerkleProof(
|
||||
proofArtifacts.depth,
|
||||
BigInt(0),
|
||||
proofArtifacts.leavesPerNode,
|
||||
leaves,
|
||||
identityCommitment
|
||||
)
|
||||
}
|
||||
|
||||
const witness = Semaphore.genWitness(
|
||||
identity.getTrapdoor(),
|
||||
identity.getNullifier(),
|
||||
merkleProof,
|
||||
externalNullifier,
|
||||
signal
|
||||
)
|
||||
|
||||
const fullProof: SemaphoreFullProof = await Semaphore.genProof(witness, circuitFilePath, zkeyFilePath)
|
||||
|
||||
const solidityProof: SemaphoreSolidityProof = Semaphore.packToSolidityProof(fullProof)
|
||||
|
||||
return {
|
||||
fullProof,
|
||||
solidityProof
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Error while generating semaphore proof: ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/background/services/protocols/utils.ts
Normal file
16
src/background/services/protocols/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { hexToBigint } from 'bigint-conversion'
|
||||
import { MerkleProof } from '@zk-kit/protocols'
|
||||
import * as ciromlibjs from 'circomlibjs'
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function deserializeMerkleProof(merkleProof): MerkleProof {
|
||||
const deserialized = {} as MerkleProof
|
||||
deserialized.root = hexToBigint(merkleProof.root)
|
||||
deserialized.siblings = merkleProof.siblings.map((siblings) =>
|
||||
Array.isArray(siblings) ? siblings.map((element) => hexToBigint(element)) : hexToBigint(siblings)
|
||||
)
|
||||
deserialized.pathIndices = merkleProof.pathIndices
|
||||
deserialized.leaf = hexToBigint(merkleProof.leaf)
|
||||
return deserialized
|
||||
}
|
||||
|
||||
export const poseidonHash = (data: Array<bigint>): bigint => ciromlibjs.poseidon(data)
|
||||
18
src/background/services/simple-storage.ts
Normal file
18
src/background/services/simple-storage.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
|
||||
export default class SimpleStorage {
|
||||
private key: string
|
||||
|
||||
constructor(key) {
|
||||
this.key = key
|
||||
}
|
||||
|
||||
get = async (): Promise<any | null> => {
|
||||
const content = await browser.storage.sync.get(this.key)
|
||||
return content ? content[this.key] : null
|
||||
}
|
||||
|
||||
set = async (value) => browser.storage.sync.set({ [this.key]: value })
|
||||
|
||||
clear = async () => browser.storage.sync.remove(this.key)
|
||||
}
|
||||
10
src/background/services/storage.ts
Normal file
10
src/background/services/storage.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
|
||||
export async function get(key): Promise<any | null> {
|
||||
const content = await browser.storage.sync.get(key)
|
||||
return content ? content[key] : null
|
||||
}
|
||||
|
||||
export async function set(key, value) {
|
||||
return browser.storage.sync.set({ [key]: value })
|
||||
}
|
||||
25
src/background/services/zk-validator.ts
Normal file
25
src/background/services/zk-validator.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ZkInputs } from '@src/types'
|
||||
|
||||
export default class ZkValidator {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
validateZkInputs(payload: Required<ZkInputs>) {
|
||||
const { circuitFilePath, zkeyFilePath, merkleProofArtifacts, merkleProof } = payload
|
||||
|
||||
if (!circuitFilePath) throw new Error('circuitFilePath not provided')
|
||||
if (!zkeyFilePath) throw new Error('zkeyFilePath not provided')
|
||||
|
||||
if (merkleProof) {
|
||||
if (!merkleProof.root) throw new Error('invalid merkleProof.root value')
|
||||
if (!merkleProof.siblings.length) throw new Error('invalid merkleProof.siblings value')
|
||||
if (!merkleProof.pathIndices.length) throw new Error('invalid merkleProof.pathIndices value')
|
||||
if (!merkleProof.leaf) throw new Error('invalid merkleProof.leaf value')
|
||||
} else if (merkleProofArtifacts) {
|
||||
if (!merkleProofArtifacts.leaves.length || merkleProofArtifacts.leaves.length === 0)
|
||||
throw new Error('invalid merkleProofArtifacts.leaves value')
|
||||
if (!merkleProofArtifacts.depth) throw new Error('invalid merkleProofArtifacts.depth value')
|
||||
if (!merkleProofArtifacts.leavesPerNode) throw new Error('invalid merkleProofArtifacts.leavesPerNode value')
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
}
|
||||
242
src/background/zk-kepeer.ts
Normal file
242
src/background/zk-kepeer.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import RPCAction from '@src/util/constants'
|
||||
import { PendingRequestType, NewIdentityRequest, WalletInfo } from '@src/types'
|
||||
import Web3 from 'web3'
|
||||
import { bigintToHex } from 'bigint-conversion'
|
||||
import { RLNFullProof } from '@zk-kit/protocols'
|
||||
import Handler from './controllers/handler'
|
||||
import LockService from './services/lock'
|
||||
import IdentityService from './services/identity'
|
||||
import MetamaskService from './services/metamask'
|
||||
import ZkValidator from './services/zk-validator'
|
||||
import RequestManager from './controllers/request-manager'
|
||||
import SemaphoreService from './services/protocols/semaphore'
|
||||
import RLNService from './services/protocols/rln'
|
||||
import { RLNProofRequest, SemaphoreProof, SemaphoreProofRequest } from './services/protocols/interfaces'
|
||||
import ApprovalService from './services/approval'
|
||||
import ZkIdentityWrapper from './identity-decorater'
|
||||
import identityFactory from './identity-factory'
|
||||
import BrowserUtils from './controllers/browser-utils'
|
||||
|
||||
export default class ZkKepperController extends Handler {
|
||||
private identityService: IdentityService
|
||||
private metamaskService: MetamaskService
|
||||
private zkValidator: ZkValidator
|
||||
private requestManager: RequestManager
|
||||
private semaphoreService: SemaphoreService
|
||||
private rlnService: RLNService
|
||||
private approvalService: ApprovalService
|
||||
constructor() {
|
||||
super()
|
||||
this.identityService = new IdentityService()
|
||||
this.metamaskService = new MetamaskService()
|
||||
this.zkValidator = new ZkValidator()
|
||||
this.requestManager = new RequestManager()
|
||||
this.semaphoreService = new SemaphoreService()
|
||||
this.rlnService = new RLNService()
|
||||
this.approvalService = new ApprovalService()
|
||||
}
|
||||
|
||||
initialize = async (): Promise<ZkKepperController> => {
|
||||
// common
|
||||
this.add(
|
||||
RPCAction.UNLOCK,
|
||||
LockService.unlock,
|
||||
this.metamaskService.ensure,
|
||||
this.identityService.unlock,
|
||||
this.approvalService.unlock,
|
||||
LockService.onUnlocked
|
||||
)
|
||||
|
||||
this.add(RPCAction.LOCK, LockService.logout)
|
||||
|
||||
/**
|
||||
* Return status of background process
|
||||
* @returns {Object} status Background process status
|
||||
* @returns {boolean} status.initialized has background process been initialized
|
||||
* @returns {boolean} status.unlocked is background process unlocked
|
||||
*/
|
||||
this.add(RPCAction.GET_STATUS, async () => {
|
||||
const { initialized, unlocked } = await LockService.getStatus()
|
||||
return {
|
||||
initialized,
|
||||
unlocked
|
||||
}
|
||||
})
|
||||
|
||||
// requests
|
||||
this.add(RPCAction.GET_PENDING_REQUESTS, LockService.ensure, this.requestManager.getRequests)
|
||||
this.add(RPCAction.FINALIZE_REQUEST, LockService.ensure, this.requestManager.finalizeRequest)
|
||||
|
||||
// web3
|
||||
this.add(RPCAction.CONNECT_METAMASK, LockService.ensure, this.metamaskService.connectMetamask)
|
||||
this.add(RPCAction.GET_WALLET_INFO, this.metamaskService.getWalletInfo)
|
||||
|
||||
// lock
|
||||
this.add(RPCAction.SETUP_PASSWORD, (payload: string) => LockService.setupPassword(payload))
|
||||
|
||||
// identites
|
||||
this.add(
|
||||
RPCAction.CREATE_IDENTITY,
|
||||
LockService.ensure,
|
||||
this.metamaskService.ensure,
|
||||
async (payload: NewIdentityRequest) => {
|
||||
try {
|
||||
const { strategy, options } = payload
|
||||
if (!strategy) throw new Error('strategy not provided')
|
||||
|
||||
const numOfIdentites = this.identityService.getNumOfIdentites()
|
||||
const config: any = {
|
||||
...options,
|
||||
name: options?.name || `Account # ${numOfIdentites}`
|
||||
}
|
||||
|
||||
if (strategy === 'interrep') {
|
||||
const web3: Web3 = await this.metamaskService.getWeb3()
|
||||
const walletInfo: WalletInfo | null = await this.metamaskService.getWalletInfo()
|
||||
config.web3 = web3
|
||||
config.walletInfo = walletInfo
|
||||
}
|
||||
|
||||
const identity: ZkIdentityWrapper | undefined = await identityFactory(strategy, config)
|
||||
|
||||
if (!identity) {
|
||||
throw new Error('Identity not created, make sure to check strategy')
|
||||
}
|
||||
|
||||
await this.identityService.insert(identity)
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.add(RPCAction.GET_COMMITMENTS, LockService.ensure, this.identityService.getIdentityCommitments)
|
||||
this.add(RPCAction.GET_IDENTITIES, LockService.ensure, this.identityService.getIdentities)
|
||||
this.add(RPCAction.SET_ACTIVE_IDENTITY, LockService.ensure, this.identityService.setActiveIdentity)
|
||||
this.add(RPCAction.GET_ACTIVE_IDENTITY, LockService.ensure, async () => {
|
||||
const identity = await this.identityService.getActiveidentity()
|
||||
if (!identity) {
|
||||
return null
|
||||
}
|
||||
const identityCommitment: bigint = identity.genIdentityCommitment()
|
||||
const identityCommitmentHex = bigintToHex(identityCommitment)
|
||||
return identityCommitmentHex
|
||||
})
|
||||
|
||||
// protocols
|
||||
this.add(
|
||||
RPCAction.SEMAPHORE_PROOF,
|
||||
LockService.ensure,
|
||||
this.zkValidator.validateZkInputs,
|
||||
async (payload: SemaphoreProofRequest, meta: any) => {
|
||||
const { unlocked } = await LockService.getStatus()
|
||||
|
||||
if (!unlocked) {
|
||||
await BrowserUtils.openPopup()
|
||||
await LockService.awaitUnlock()
|
||||
}
|
||||
|
||||
const identity: ZkIdentityWrapper | undefined = await this.identityService.getActiveidentity()
|
||||
const approved: boolean = await this.approvalService.isApproved(meta.origin)
|
||||
const perm: any = await this.approvalService.getPermission(meta.origin)
|
||||
|
||||
if (!identity) throw new Error('active identity not found')
|
||||
if (!approved) throw new Error(`${meta.origin} is not approved`)
|
||||
|
||||
try {
|
||||
if (!perm.noApproval) {
|
||||
await this.requestManager.newRequest(PendingRequestType.SEMAPHORE_PROOF, {
|
||||
...payload,
|
||||
origin: meta.origin
|
||||
})
|
||||
}
|
||||
|
||||
await BrowserUtils.closePopup()
|
||||
|
||||
const proof: SemaphoreProof = await this.semaphoreService.genProof(identity.zkIdentity, payload)
|
||||
|
||||
return proof
|
||||
} catch (err) {
|
||||
await BrowserUtils.closePopup()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.add(
|
||||
RPCAction.RLN_PROOF,
|
||||
LockService.ensure,
|
||||
this.zkValidator.validateZkInputs,
|
||||
async (payload: RLNProofRequest) => {
|
||||
const identity: ZkIdentityWrapper | undefined = await this.identityService.getActiveidentity()
|
||||
if (!identity) throw new Error('active identity not found')
|
||||
|
||||
const proof: RLNFullProof = await this.rlnService.genProof(identity.zkIdentity, payload)
|
||||
return proof
|
||||
}
|
||||
)
|
||||
|
||||
// injecting
|
||||
this.add(RPCAction.TRY_INJECT, async (payload: any) => {
|
||||
const { origin }: { origin: string } = payload
|
||||
if (!origin) throw new Error('Origin not provided')
|
||||
|
||||
const { unlocked } = await LockService.getStatus()
|
||||
|
||||
if (!unlocked) {
|
||||
await BrowserUtils.openPopup()
|
||||
await LockService.awaitUnlock()
|
||||
}
|
||||
|
||||
const includes: boolean = await this.approvalService.isApproved(origin)
|
||||
|
||||
if (includes) return true
|
||||
|
||||
try {
|
||||
await this.requestManager.newRequest(PendingRequestType.INJECT, { origin })
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
})
|
||||
this.add(RPCAction.APPROVE_HOST, LockService.ensure, async (payload: any) => {
|
||||
this.approvalService.add(payload)
|
||||
})
|
||||
this.add(RPCAction.IS_HOST_APPROVED, LockService.ensure, this.approvalService.isApproved)
|
||||
this.add(RPCAction.REMOVE_HOST, LockService.ensure, this.approvalService.remove)
|
||||
|
||||
this.add(RPCAction.GET_HOST_PERMISSIONS, LockService.ensure, async (payload: any) => this.approvalService.getPermission(payload))
|
||||
|
||||
this.add(RPCAction.SET_HOST_PERMISSIONS, LockService.ensure, async (payload: any) => {
|
||||
const { host, ...permissions } = payload
|
||||
return this.approvalService.setPermission(host, permissions)
|
||||
})
|
||||
|
||||
this.add(RPCAction.CLOSE_POPUP, async () => BrowserUtils.closePopup())
|
||||
|
||||
this.add(RPCAction.CREATE_IDENTITY_REQ, LockService.ensure, this.metamaskService.ensure, async () => {
|
||||
const res: any = await this.requestManager.newRequest(PendingRequestType.CREATE_IDENTITY, { origin })
|
||||
|
||||
const { provider, options } = res
|
||||
|
||||
return this.handle({
|
||||
method: RPCAction.CREATE_IDENTITY,
|
||||
payload: {
|
||||
strategy: provider,
|
||||
options
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// dev
|
||||
this.add(RPCAction.CLEAR_APPROVED_HOSTS, this.approvalService.empty)
|
||||
this.add(RPCAction.DUMMY_REQUEST, async () =>
|
||||
this.requestManager.newRequest(PendingRequestType.DUMMY, 'hello from dummy')
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
70
src/contentscripts/index.ts
Normal file
70
src/contentscripts/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
import { ActionType as IdentityActionType } from '@src/ui/ducks/identities'
|
||||
import { ActionType as AppActionType } from '@src/ui/ducks/app'
|
||||
|
||||
;
|
||||
|
||||
(async function () {
|
||||
try {
|
||||
const url = browser.runtime.getURL('js/injected.js')
|
||||
const container = document.head || document.documentElement
|
||||
const scriptTag = document.createElement('script')
|
||||
scriptTag.src = url
|
||||
scriptTag.setAttribute('async', 'false')
|
||||
container.insertBefore(scriptTag, container.children[0])
|
||||
container.removeChild(scriptTag)
|
||||
|
||||
window.addEventListener('message', async (event) => {
|
||||
const { data } = event
|
||||
if (data && data.target === 'injected-contentscript') {
|
||||
const res = await browser.runtime.sendMessage(data.message)
|
||||
window.postMessage(
|
||||
{
|
||||
target: 'injected-injectedscript',
|
||||
payload: res,
|
||||
nonce: data.nonce
|
||||
},
|
||||
'*'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
browser.runtime.onMessage.addListener((action) => {
|
||||
switch (action.type) {
|
||||
case IdentityActionType.SET_SELECTED:
|
||||
window.postMessage(
|
||||
{
|
||||
target: 'injected-injectedscript',
|
||||
payload: [null, action.payload],
|
||||
nonce: 'identityChanged'
|
||||
},
|
||||
'*'
|
||||
)
|
||||
return
|
||||
case AppActionType.SET_STATUS:
|
||||
if (!action.payload.unlocked) {
|
||||
window.postMessage(
|
||||
{
|
||||
target: 'injected-injectedscript',
|
||||
payload: [null],
|
||||
nonce: 'logout'
|
||||
},
|
||||
'*'
|
||||
)
|
||||
} else {
|
||||
window.postMessage(
|
||||
{
|
||||
target: 'injected-injectedscript',
|
||||
payload: [null],
|
||||
nonce: 'login'
|
||||
},
|
||||
'*'
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('error occured', e)
|
||||
}
|
||||
})()
|
||||
307
src/contentscripts/injected.ts
Normal file
307
src/contentscripts/injected.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
|
||||
import { MerkleProofArtifacts } from '@src/types'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import { MerkleProof } from '@zk-kit/protocols'
|
||||
|
||||
export type IRequest = {
|
||||
method: string
|
||||
payload?: any
|
||||
error?: boolean
|
||||
meta?: any
|
||||
}
|
||||
|
||||
const promises: {
|
||||
[k: string]: {
|
||||
resolve: Function
|
||||
reject: Function
|
||||
}
|
||||
} = {}
|
||||
|
||||
let nonce = 0
|
||||
|
||||
async function getIdentityCommitments() {
|
||||
return post({
|
||||
method: RPCAction.GET_COMMITMENTS
|
||||
})
|
||||
}
|
||||
|
||||
async function getActiveIdentity() {
|
||||
return post({
|
||||
method: RPCAction.GET_ACTIVE_IDENTITY
|
||||
})
|
||||
}
|
||||
|
||||
async function getHostPermissions(host: string) {
|
||||
return post({
|
||||
method: RPCAction.GET_HOST_PERMISSIONS,
|
||||
payload: host
|
||||
})
|
||||
}
|
||||
|
||||
async function setHostPermissions(
|
||||
host: string,
|
||||
permissions?: {
|
||||
noApproval?: boolean
|
||||
}
|
||||
) {
|
||||
return post({
|
||||
method: RPCAction.SET_HOST_PERMISSIONS,
|
||||
payload: {
|
||||
host,
|
||||
...permissions
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function createIdentity() {
|
||||
try {
|
||||
const res = await post({
|
||||
method: RPCAction.CREATE_IDENTITY_REQ
|
||||
})
|
||||
|
||||
await post({ method: RPCAction.CLOSE_POPUP })
|
||||
return res
|
||||
} catch (e) {
|
||||
await post({ method: RPCAction.CLOSE_POPUP })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function createDummyRequest() {
|
||||
try {
|
||||
const res = await post({
|
||||
method: RPCAction.DUMMY_REQUEST
|
||||
})
|
||||
|
||||
await post({ method: RPCAction.CLOSE_POPUP })
|
||||
return res
|
||||
} catch (e) {
|
||||
await post({ method: RPCAction.CLOSE_POPUP })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function semaphoreProof(
|
||||
externalNullifier: string,
|
||||
signal: string,
|
||||
circuitFilePath: string,
|
||||
zkeyFilePath: string,
|
||||
merkleProofArtifactsOrStorageAddress: string | MerkleProofArtifacts,
|
||||
merkleProof?: MerkleProof
|
||||
) {
|
||||
const merkleProofArtifacts =
|
||||
typeof merkleProofArtifactsOrStorageAddress === 'string' ? undefined : merkleProofArtifactsOrStorageAddress
|
||||
const merkleStorageAddress =
|
||||
typeof merkleProofArtifactsOrStorageAddress === 'string' ? merkleProofArtifactsOrStorageAddress : undefined
|
||||
|
||||
return post({
|
||||
method: RPCAction.SEMAPHORE_PROOF,
|
||||
payload: {
|
||||
externalNullifier,
|
||||
signal,
|
||||
merkleStorageAddress,
|
||||
circuitFilePath,
|
||||
zkeyFilePath,
|
||||
merkleProofArtifacts,
|
||||
merkleProof
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function rlnProof(
|
||||
externalNullifier: string,
|
||||
signal: string,
|
||||
circuitFilePath: string,
|
||||
zkeyFilePath: string,
|
||||
merkleProofArtifactsOrStorageAddress: string | MerkleProofArtifacts,
|
||||
rlnIdentifier: string
|
||||
) {
|
||||
const merkleProofArtifacts =
|
||||
typeof merkleProofArtifactsOrStorageAddress === 'string' ? undefined : merkleProofArtifactsOrStorageAddress
|
||||
const merkleStorageAddress =
|
||||
typeof merkleProofArtifactsOrStorageAddress === 'string' ? merkleProofArtifactsOrStorageAddress : undefined
|
||||
return post({
|
||||
method: RPCAction.RLN_PROOF,
|
||||
payload: {
|
||||
externalNullifier,
|
||||
signal,
|
||||
merkleStorageAddress,
|
||||
circuitFilePath,
|
||||
zkeyFilePath,
|
||||
merkleProofArtifacts,
|
||||
rlnIdentifier
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// dev-only
|
||||
async function clearApproved() {
|
||||
return post({
|
||||
method: RPCAction.CLEAR_APPROVED_HOSTS
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Popup
|
||||
*/
|
||||
async function openPopup() {
|
||||
return post({
|
||||
method: 'OPEN_POPUP'
|
||||
})
|
||||
}
|
||||
|
||||
async function tryInject(origin: string) {
|
||||
return post({
|
||||
method: RPCAction.TRY_INJECT,
|
||||
payload: { origin }
|
||||
})
|
||||
}
|
||||
|
||||
async function addHost(host: string) {
|
||||
return post({
|
||||
method: RPCAction.APPROVE_HOST,
|
||||
payload: { host }
|
||||
})
|
||||
}
|
||||
|
||||
const EVENTS: {
|
||||
[eventName: string]: ((data: unknown) => void)[]
|
||||
} = {}
|
||||
|
||||
const on = (eventName: string, cb: (data: unknown) => void) => {
|
||||
const bucket = EVENTS[eventName] || []
|
||||
bucket.push(cb)
|
||||
EVENTS[eventName] = bucket
|
||||
}
|
||||
|
||||
const off = (eventName: string, cb: (data: unknown) => void) => {
|
||||
const bucket = EVENTS[eventName] || []
|
||||
EVENTS[eventName] = bucket.filter((callback) => callback === cb)
|
||||
}
|
||||
|
||||
const emit = (eventName: string, payload?: any) => {
|
||||
const bucket = EVENTS[eventName] || []
|
||||
|
||||
for (const cb of bucket) {
|
||||
cb(payload)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injected Client
|
||||
*/
|
||||
const client = {
|
||||
openPopup,
|
||||
getIdentityCommitments,
|
||||
getActiveIdentity,
|
||||
createIdentity,
|
||||
getHostPermissions,
|
||||
setHostPermissions,
|
||||
semaphoreProof,
|
||||
rlnProof,
|
||||
on,
|
||||
off,
|
||||
// dev-only
|
||||
clearApproved,
|
||||
createDummyRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Extension
|
||||
* @returns injected client
|
||||
*/
|
||||
// eslint-disable-next-line consistent-return
|
||||
async function connect() {
|
||||
let result
|
||||
try {
|
||||
const approved = await tryInject(window.location.origin)
|
||||
|
||||
if (approved) {
|
||||
await addHost(window.location.origin)
|
||||
result = client
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Err: ', err)
|
||||
result = null
|
||||
}
|
||||
|
||||
await post({ method: RPCAction.CLOSE_POPUP })
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
zkpr: {
|
||||
connect: () => any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.zkpr = {
|
||||
connect
|
||||
}
|
||||
|
||||
// Connect injected script messages with content script messages
|
||||
async function post(message: IRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
const messageNonce = nonce++
|
||||
window.postMessage(
|
||||
{
|
||||
target: 'injected-contentscript',
|
||||
message: {
|
||||
...message,
|
||||
meta: {
|
||||
...message.meta,
|
||||
origin: window.location.origin
|
||||
},
|
||||
type: message.method
|
||||
},
|
||||
nonce: messageNonce
|
||||
},
|
||||
'*'
|
||||
)
|
||||
|
||||
promises[messageNonce] = { resolve, reject }
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const { data } = event
|
||||
|
||||
if (data && data.target === 'injected-injectedscript') {
|
||||
if (data.nonce === 'identityChanged') {
|
||||
const [err, res] = data.payload
|
||||
emit('identityChanged', res)
|
||||
return
|
||||
}
|
||||
if (data.nonce === 'logout') {
|
||||
const [err, res] = data.payload
|
||||
emit('logout', res)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.nonce === 'login') {
|
||||
const [err, res] = data.payload
|
||||
emit('login', res)
|
||||
return
|
||||
}
|
||||
|
||||
if (!promises[data.nonce]) return
|
||||
|
||||
const [err, res] = data.payload
|
||||
const { resolve, reject } = promises[data.nonce]
|
||||
|
||||
if (err) {
|
||||
// eslint-disable-next-line consistent-return
|
||||
return reject(new Error(err))
|
||||
}
|
||||
|
||||
resolve(res)
|
||||
|
||||
delete promises[data.nonce]
|
||||
}
|
||||
})
|
||||
14
src/custom.d.ts
vendored
Normal file
14
src/custom.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
declare module '*.svg' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.gif' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
2914
src/static/chains.json
Normal file
2914
src/static/chains.json
Normal file
File diff suppressed because it is too large
Load Diff
6
src/static/icons/loader.svg
Normal file
6
src/static/icons/loader.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<circle cx="50" cy="50" fill="none" stroke="#ffffff" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138">
|
||||
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform>
|
||||
</circle>
|
||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
||||
|
After Width: | Height: | Size: 639 B |
6
src/static/icons/logo.svg
Normal file
6
src/static/icons/logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M68.3781 22.0343H24.0643V19H73.0223V76.0773H23V73.043L68.3781 22.0343ZM69.4424 73.043V25.8886L27.4507 73.043H69.4424Z" fill="#94FEBF"/>
|
||||
<path d="M108.941 42.7822L137 76.0773H82.8172V19H137L108.941 42.7822ZM106.425 44.9144L86.3972 61.89V73.043H130.13L106.425 44.9144Z" fill="#94FEBF"/>
|
||||
<path d="M25.6124 141V83.9227H53.7681C58.4768 83.9227 62.5405 84.6608 65.9592 86.1369C69.8939 87.8317 73.0223 90.2646 75.3444 93.4356C77.6665 96.5519 78.8276 99.9962 78.8276 103.769C78.8276 107.541 77.6665 111.013 75.3444 114.184C73.0868 117.354 69.9907 119.787 66.056 121.482C62.7018 122.958 58.6058 123.696 53.7681 123.696H43.2218V141H25.6124Z" fill="#94FEBF"/>
|
||||
<path d="M135.839 141H84.075V83.9227H110.296C115.907 83.9227 120.487 84.7701 124.035 86.4649C127.518 88.1051 130.227 90.3466 132.162 93.1896C134.097 96.0325 135.065 99.1761 135.065 102.62C135.065 106.885 133.517 110.712 130.421 114.101C127.325 117.436 123.422 119.651 118.713 120.744L135.839 141ZM90.1706 86.957L116.488 118.12C120.81 117.354 124.39 115.523 127.228 112.625C130.066 109.728 131.485 106.393 131.485 102.62C131.485 99.6135 130.614 96.8799 128.873 94.4197C127.131 91.9594 124.68 90.0733 121.519 88.7611C118.681 87.5584 114.972 86.957 110.392 86.957H90.1706ZM87.655 88.9252V118.366H112.521L87.655 88.9252ZM87.655 137.966H129.066L114.94 121.318L111.94 121.4H87.655V137.966Z" fill="#94FEBF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
92
src/types/index.ts
Normal file
92
src/types/index.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { MerkleProof } from '@zk-kit/protocols'
|
||||
|
||||
export type Request = {
|
||||
method: string
|
||||
payload?: any
|
||||
error?: boolean
|
||||
meta?: any
|
||||
}
|
||||
|
||||
export type WalletInfo = {
|
||||
account: string
|
||||
networkType: string
|
||||
chainId: number
|
||||
}
|
||||
|
||||
export type CreateInterrepIdentityMetadata = {
|
||||
web2Provider: 'Twitter' | 'Reddit' | 'Github'
|
||||
nonce?: number
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type CreateRandomIdentityMetadata = {
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type CreateIdentityMetadata = CreateInterrepIdentityMetadata | CreateRandomIdentityMetadata
|
||||
export type CreateIdentityStrategy = 'interrep' | 'random'
|
||||
|
||||
export type NewIdentityRequest = {
|
||||
strategy: CreateIdentityStrategy
|
||||
options: any
|
||||
}
|
||||
|
||||
export type MerkleProofArtifacts = {
|
||||
leaves: string[]
|
||||
depth: number
|
||||
leavesPerNode: number
|
||||
}
|
||||
|
||||
export type ZkInputs = {
|
||||
circuitFilePath: string
|
||||
zkeyFilePath: string
|
||||
merkleStorageAddress?: string
|
||||
merkleProofArtifacts?: MerkleProofArtifacts
|
||||
merkleProof?: MerkleProof
|
||||
}
|
||||
|
||||
export enum PendingRequestType {
|
||||
SEMAPHORE_PROOF,
|
||||
DUMMY,
|
||||
APPROVE,
|
||||
INJECT,
|
||||
CREATE_IDENTITY
|
||||
}
|
||||
|
||||
export type PendingRequest = {
|
||||
id: string
|
||||
type: PendingRequestType
|
||||
payload?: any
|
||||
}
|
||||
|
||||
export type RequestResolutionAction<data> = {
|
||||
id: string
|
||||
status: 'accept' | 'reject'
|
||||
data?: data
|
||||
}
|
||||
|
||||
export type FinalizedRequest = {
|
||||
id: string
|
||||
action: boolean
|
||||
}
|
||||
|
||||
export type ApprovalAction = {
|
||||
host: string
|
||||
action: 'add' | 'remove'
|
||||
}
|
||||
|
||||
export type IdentityMetadata = {
|
||||
account: string
|
||||
name: string
|
||||
provider: string
|
||||
}
|
||||
|
||||
export type SerializedIdentity = {
|
||||
metadata: IdentityMetadata
|
||||
secret: string
|
||||
}
|
||||
|
||||
export enum ZkProofType {
|
||||
SEMAPHORE,
|
||||
RLN
|
||||
}
|
||||
97
src/ui/components/Button/button.scss
Normal file
97
src/ui/components/Button/button.scss
Normal file
@@ -0,0 +1,97 @@
|
||||
@import './src/util/variables';
|
||||
|
||||
.button {
|
||||
@extend %regular-font;
|
||||
@extend %bold;
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
height: 2.375rem;
|
||||
font-weight: 600;
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
&__loader {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
&--small {
|
||||
padding: 0.25rem 0.75rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&--tiny {
|
||||
font-size: 0.6875rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
font-weight: 600;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.button--primary {
|
||||
background-color: $primary-green;
|
||||
color: $black;
|
||||
border: 2px solid $primary-green;
|
||||
transition: background-color 150ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($primary-green, 5);
|
||||
border: 2px solid lighten($primary-green, 5);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: darken($primary-green, 5);
|
||||
border: 2px solid darken($primary-green, 5);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: $primary-green;
|
||||
color: $black;
|
||||
border: 2px solid $primary-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
color: $primary-green;
|
||||
border: 2px solid $primary-green;
|
||||
transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($primary-green, 0.5);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba($primary-green, 8);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: transparent;
|
||||
color: $primary-green;
|
||||
border: 2px solid $primary-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
41
src/ui/components/Button/index.tsx
Normal file
41
src/ui/components/Button/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/button-has-type */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React, { ButtonHTMLAttributes, ReactElement } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import './button.scss'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
import LoaderGIF from '../../../static/icons/loader.svg'
|
||||
|
||||
export enum ButtonType {
|
||||
primary,
|
||||
secondary
|
||||
}
|
||||
|
||||
export type ButtonProps = {
|
||||
className?: string
|
||||
loading?: boolean
|
||||
btnType?: ButtonType
|
||||
small?: boolean
|
||||
tiny?: boolean
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>
|
||||
|
||||
export default function Button(props: ButtonProps): ReactElement {
|
||||
const { className, loading, children, btnType = ButtonType.primary, small, tiny, ...btnProps } = props
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames('button', className, {
|
||||
'button--small': small,
|
||||
'button--tiny': tiny,
|
||||
'button--loading': loading,
|
||||
'button--primary': btnType === ButtonType.primary,
|
||||
'button--secondary': btnType === ButtonType.secondary
|
||||
})}
|
||||
{...btnProps}
|
||||
>
|
||||
{loading && <Icon className="button__loader" url={LoaderGIF} size={2} />}
|
||||
{!loading && children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
46
src/ui/components/Checkbox/index.scss
Normal file
46
src/ui/components/Checkbox/index.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
@import './src/util/variables';
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
border-radius: 0.1875rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 200ms ease-in-out, border-color 200ms ease-in-out, color 200ms ease-in-out;
|
||||
opacity: 1;
|
||||
border: 2px solid $gray-800;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 0.75rem;
|
||||
position: absolute;
|
||||
top: 0.125rem;
|
||||
left: 0.125rem;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: $gray-600;
|
||||
}
|
||||
|
||||
&--checked {
|
||||
background-color: $primary-blue;
|
||||
border-color: $primary-blue;
|
||||
|
||||
.icon {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/ui/components/Checkbox/index.tsx
Normal file
28
src/ui/components/Checkbox/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React, { ChangeEventHandler, ReactElement } from 'react'
|
||||
import c from 'classnames'
|
||||
import './index.scss'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
onChange: ChangeEventHandler<HTMLInputElement>
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function Checkbox(props: Props): ReactElement {
|
||||
const { className, checked, onChange, disabled } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={c('checkbox', className, {
|
||||
'checkbox--checked': checked
|
||||
})}
|
||||
>
|
||||
<input type="checkbox" checked={checked} onChange={onChange} disabled={disabled} />
|
||||
<Icon fontAwesome="fa-check" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
src/ui/components/ConfirmRequestModal/confirm-modal.scss
Normal file
16
src/ui/components/ConfirmRequestModal/confirm-modal.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.confirm-modal {
|
||||
@media only screen and (min-width: 358px) {
|
||||
min-height: 36rem;
|
||||
}
|
||||
}
|
||||
|
||||
.semaphore-proof__file {
|
||||
font-size: 0.75rem;
|
||||
margin: 0.5rem;
|
||||
|
||||
.icon {
|
||||
font-size: 1rem !important;
|
||||
cursor: pointer;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
449
src/ui/components/ConfirmRequestModal/index.tsx
Normal file
449
src/ui/components/ConfirmRequestModal/index.tsx
Normal file
@@ -0,0 +1,449 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import FullModal, { FullModalContent, FullModalFooter, FullModalHeader } from '@src/ui/components/FullModal'
|
||||
import Button, { ButtonType } from '@src/ui/components/Button'
|
||||
import { useRequestsPending } from '@src/ui/ducks/requests'
|
||||
import { PendingRequest, PendingRequestType, RequestResolutionAction } from '@src/types'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import './confirm-modal.scss'
|
||||
import Input from '@src/ui/components/Input'
|
||||
import Dropdown from '@src/ui/components/Dropdown'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Checkbox from '@src/ui/components/Checkbox'
|
||||
import { getLinkPreview } from 'link-preview-js'
|
||||
|
||||
export default function ConfirmRequestModal(): ReactElement {
|
||||
const pendingRequests = useRequestsPending()
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const dispatch = useDispatch()
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const pendingRequest = pendingRequests[activeIndex]
|
||||
|
||||
const reject = useCallback(
|
||||
async (err?: any) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const id = pendingRequest?.id
|
||||
const req: RequestResolutionAction<undefined> = {
|
||||
id,
|
||||
status: 'reject',
|
||||
data: err
|
||||
}
|
||||
await postMessage({
|
||||
method: RPCAction.FINALIZE_REQUEST,
|
||||
payload: req
|
||||
})
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[pendingRequest]
|
||||
)
|
||||
|
||||
const approve = useCallback(
|
||||
async (data?: any) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const id = pendingRequest?.id
|
||||
const req: RequestResolutionAction<undefined> = {
|
||||
id,
|
||||
status: 'accept',
|
||||
data
|
||||
}
|
||||
await postMessage({
|
||||
method: RPCAction.FINALIZE_REQUEST,
|
||||
payload: req
|
||||
})
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[pendingRequest]
|
||||
)
|
||||
|
||||
if (!pendingRequest) return <></>
|
||||
|
||||
switch (pendingRequest.type) {
|
||||
case PendingRequestType.INJECT:
|
||||
return (
|
||||
<ConnectionApprovalModal
|
||||
len={pendingRequests.length}
|
||||
pendingRequest={pendingRequest}
|
||||
accept={() => approve()}
|
||||
reject={() => reject()}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
case PendingRequestType.SEMAPHORE_PROOF:
|
||||
return (
|
||||
<ProofModal
|
||||
len={pendingRequests.length}
|
||||
pendingRequest={pendingRequest}
|
||||
accept={() => approve()}
|
||||
reject={() => reject()}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
case PendingRequestType.DUMMY:
|
||||
return (
|
||||
<DummyApprovalModal
|
||||
len={pendingRequests.length}
|
||||
pendingRequest={pendingRequest}
|
||||
accept={() => approve()}
|
||||
reject={() => reject()}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
case PendingRequestType.CREATE_IDENTITY:
|
||||
return (
|
||||
<CreateIdentityApprovalModal
|
||||
len={pendingRequests.length}
|
||||
pendingRequest={pendingRequest}
|
||||
accept={approve}
|
||||
reject={reject}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<DefaultApprovalModal
|
||||
len={pendingRequests.length}
|
||||
pendingRequest={pendingRequest}
|
||||
accept={approve}
|
||||
reject={reject}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var ConnectionApprovalModal = function(props: {
|
||||
len: number
|
||||
reject: () => void
|
||||
accept: () => void
|
||||
loading: boolean
|
||||
error: string
|
||||
pendingRequest: PendingRequest
|
||||
}) {
|
||||
const origin = props.pendingRequest.payload?.origin
|
||||
const [checked, setChecked] = useState(false)
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (origin) {
|
||||
const res = await postMessage({
|
||||
method: RPCAction.GET_HOST_PERMISSIONS,
|
||||
payload: origin
|
||||
})
|
||||
setChecked(res?.noApproval)
|
||||
}
|
||||
})()
|
||||
}, [origin])
|
||||
|
||||
const [faviconUrl, setFaviconUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (origin) {
|
||||
const data = await getLinkPreview(origin)
|
||||
const [favicon] = data?.favicons || []
|
||||
setFaviconUrl(favicon)
|
||||
}
|
||||
})()
|
||||
}, [origin])
|
||||
|
||||
const setApproval = useCallback(
|
||||
async (noApproval: boolean) => {
|
||||
const res = await postMessage({
|
||||
method: RPCAction.SET_HOST_PERMISSIONS,
|
||||
payload: {
|
||||
host: origin,
|
||||
noApproval
|
||||
}
|
||||
})
|
||||
setChecked(res?.noApproval)
|
||||
},
|
||||
[origin]
|
||||
)
|
||||
|
||||
return (
|
||||
<FullModal className="confirm-modal" onClose={() => null}>
|
||||
<FullModalHeader>
|
||||
Connect with ZK Keeper
|
||||
{props.len > 1 && <div className="flex-grow flex flex-row justify-end">{`1 of ${props.len}`}</div>}
|
||||
</FullModalHeader>
|
||||
<FullModalContent className="flex flex-col items-center">
|
||||
<div className="w-16 h-16 rounded-full my-6 border border-gray-800 p-2 flex-shrink-0">
|
||||
<div
|
||||
className="w-16 h-16"
|
||||
style={{
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundImage: `url(${faviconUrl})`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-lg font-semibold mb-2 text-center">
|
||||
{`${origin} would like to connect to your identity`}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 text-center">
|
||||
This site is requesting access to view your current identity. Always make sure you trust the site
|
||||
you interact with.
|
||||
</div>
|
||||
<div className="font-bold mt-4">Permissions</div>
|
||||
<div className="flex flex-row items-start">
|
||||
<Checkbox
|
||||
className="mr-2 mt-2 flex-shrink-0"
|
||||
checked={checked}
|
||||
onChange={(e) => {
|
||||
setApproval(e.target.checked)
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm mt-2">Allow host to create proof without approvals</div>
|
||||
</div>
|
||||
</FullModalContent>
|
||||
{props.error && <div className="text-xs text-red-500 text-center pb-1">{props.error}</div>}
|
||||
<FullModalFooter>
|
||||
<Button btnType={ButtonType.secondary} onClick={props.reject} loading={props.loading}>
|
||||
Reject
|
||||
</Button>
|
||||
<Button className="ml-2" onClick={props.accept} loading={props.loading}>
|
||||
Approve
|
||||
</Button>
|
||||
</FullModalFooter>
|
||||
</FullModal>
|
||||
)
|
||||
}
|
||||
|
||||
var DummyApprovalModal = function(props: {
|
||||
len: number
|
||||
reject: () => void
|
||||
accept: () => void
|
||||
loading: boolean
|
||||
error: string
|
||||
pendingRequest: PendingRequest
|
||||
}) {
|
||||
const {payload} = props.pendingRequest
|
||||
|
||||
return (
|
||||
<FullModal className="confirm-modal" onClose={() => null}>
|
||||
<FullModalHeader>
|
||||
Dummy Request
|
||||
{props.len > 1 && <div className="flex-grow flex flex-row justify-end">{`1 of ${props.len}`}</div>}
|
||||
</FullModalHeader>
|
||||
<FullModalContent className="flex flex-col">
|
||||
<div className="text-sm font-semibold mb-2">{payload}</div>
|
||||
</FullModalContent>
|
||||
{props.error && <div className="text-xs text-red-500 text-center pb-1">{props.error}</div>}
|
||||
<FullModalFooter>
|
||||
<Button btnType={ButtonType.secondary} onClick={props.reject} loading={props.loading}>
|
||||
Reject
|
||||
</Button>
|
||||
<Button className="ml-2" onClick={props.accept} loading={props.loading}>
|
||||
Approve
|
||||
</Button>
|
||||
</FullModalFooter>
|
||||
</FullModal>
|
||||
)
|
||||
}
|
||||
|
||||
var CreateIdentityApprovalModal = function(props: {
|
||||
len: number
|
||||
reject: (error?: any) => void
|
||||
accept: (data?: any) => void
|
||||
loading: boolean
|
||||
error: string
|
||||
pendingRequest: PendingRequest
|
||||
}) {
|
||||
const [nonce, setNonce] = useState(0)
|
||||
const [identityType, setIdentityType] = useState<'InterRep' | 'Random'>('InterRep')
|
||||
const [web2Provider, setWeb2Provider] = useState<'Twitter' | 'Github' | 'Reddit'>('Twitter')
|
||||
|
||||
const create = useCallback(async () => {
|
||||
let options: any = {
|
||||
nonce,
|
||||
web2Provider
|
||||
}
|
||||
let provider = 'interrep'
|
||||
|
||||
if (identityType === 'Random') {
|
||||
provider = 'random'
|
||||
options = {}
|
||||
}
|
||||
|
||||
props.accept({
|
||||
provider,
|
||||
options
|
||||
})
|
||||
}, [nonce, web2Provider, identityType, props.accept])
|
||||
|
||||
return (
|
||||
<FullModal className="confirm-modal" onClose={() => null}>
|
||||
<FullModalHeader>
|
||||
Create Identity
|
||||
{props.len > 1 && <div className="flex-grow flex flex-row justify-end">{`1 of ${props.len}`}</div>}
|
||||
</FullModalHeader>
|
||||
<FullModalContent>
|
||||
<Dropdown
|
||||
className="my-2"
|
||||
label="Identity type"
|
||||
options={[{ value: 'InterRep' }, { value: 'Random' }]}
|
||||
onChange={(e) => {
|
||||
setIdentityType(e.target.value as any)
|
||||
}}
|
||||
value={identityType}
|
||||
/>
|
||||
{identityType === 'InterRep' && (
|
||||
<>
|
||||
<Dropdown
|
||||
className="my-2"
|
||||
label="Web2 Provider"
|
||||
options={[{ value: 'Twitter' }, { value: 'Reddit' }, { value: 'Github' }]}
|
||||
onChange={(e) => {
|
||||
setWeb2Provider(e.target.value as any)
|
||||
}}
|
||||
value={web2Provider}
|
||||
/>
|
||||
<Input
|
||||
className="my-2"
|
||||
type="number"
|
||||
label="Nonce"
|
||||
step={1}
|
||||
defaultValue={nonce}
|
||||
onChange={(e) => setNonce(Number(e.target.value))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FullModalContent>
|
||||
{props.error && <div className="text-xs text-red-500 text-center pb-1">{props.error}</div>}
|
||||
<FullModalFooter>
|
||||
<Button btnType={ButtonType.secondary} onClick={() => props.reject()} loading={props.loading}>
|
||||
Reject
|
||||
</Button>
|
||||
<Button className="ml-2" onClick={create} loading={props.loading}>
|
||||
Approve
|
||||
</Button>
|
||||
</FullModalFooter>
|
||||
</FullModal>
|
||||
)
|
||||
}
|
||||
|
||||
var DefaultApprovalModal = function(props: {
|
||||
len: number
|
||||
reject: () => void
|
||||
accept: () => void
|
||||
loading: boolean
|
||||
error: string
|
||||
pendingRequest: PendingRequest
|
||||
}) {
|
||||
return (
|
||||
<FullModal className="confirm-modal" onClose={() => null}>
|
||||
<FullModalHeader>
|
||||
Unhandled Request
|
||||
{props.len > 1 && <div className="flex-grow flex flex-row justify-end">{`1 of ${props.len}`}</div>}
|
||||
</FullModalHeader>
|
||||
<FullModalContent className="flex flex-col">
|
||||
<div className="text-sm font-semibold mb-2 break-all">{JSON.stringify(props.pendingRequest)}</div>
|
||||
</FullModalContent>
|
||||
{props.error && <div className="text-xs text-red-500 text-center pb-1">{props.error}</div>}
|
||||
<FullModalFooter>
|
||||
<Button btnType={ButtonType.secondary} onClick={props.reject} loading={props.loading}>
|
||||
Reject
|
||||
</Button>
|
||||
<Button className="ml-2" onClick={props.accept} loading={props.loading} disabled>
|
||||
Approve
|
||||
</Button>
|
||||
</FullModalFooter>
|
||||
</FullModal>
|
||||
)
|
||||
}
|
||||
|
||||
var ProofModal = function(props: {
|
||||
len: number
|
||||
reject: () => void
|
||||
accept: () => void
|
||||
loading: boolean
|
||||
error: string
|
||||
pendingRequest: PendingRequest
|
||||
}) {
|
||||
const { circuitFilePath, externalNullifier, merkleProof, signal, zkeyFilePath, origin } =
|
||||
props.pendingRequest?.payload || {}
|
||||
|
||||
const [faviconUrl, setFaviconUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (origin) {
|
||||
const data = await getLinkPreview(origin)
|
||||
const [favicon] = data?.favicons || []
|
||||
setFaviconUrl(favicon)
|
||||
}
|
||||
})()
|
||||
}, [origin])
|
||||
|
||||
return (
|
||||
<FullModal className="confirm-modal" onClose={() => null}>
|
||||
<FullModalHeader>
|
||||
Generate Semaphore Proof
|
||||
{props.len > 1 && <div className="flex-grow flex flex-row justify-end">{`1 of ${props.len}`}</div>}
|
||||
</FullModalHeader>
|
||||
<FullModalContent className="flex flex-col items-center">
|
||||
<div className="w-16 h-16 rounded-full my-6 border border-gray-800 p-2 flex-shrink-0">
|
||||
<div
|
||||
className="w-16 h-16"
|
||||
style={{
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundImage: `url(${faviconUrl}`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-lg font-semibold mb-2 text-center">
|
||||
{`${origin} is requesting a semaphore proof`}
|
||||
</div>
|
||||
<div className="semaphore-proof__files flex flex-row items-center mb-2">
|
||||
<div className="semaphore-proof__file">
|
||||
<div className="semaphore-proof__file__title">Circuit</div>
|
||||
<Icon fontAwesome="fas fa-link" onClick={() => window.open(circuitFilePath, '_blank')} />
|
||||
</div>
|
||||
<div className="semaphore-proof__file">
|
||||
<div className="semaphore-proof__file__title">ZKey</div>
|
||||
<Icon fontAwesome="fas fa-link" onClick={() => window.open(zkeyFilePath, '_blank')} />
|
||||
</div>
|
||||
<div className="semaphore-proof__file">
|
||||
<div className="semaphore-proof__file__title">Merkle</div>
|
||||
{typeof merkleProof === 'string' ? (
|
||||
<Icon fontAwesome="fas fa-link" onClick={() => window.open(merkleProof, '_blank')} />
|
||||
) : (
|
||||
<Icon fontAwesome="fas fa-copy" onClick={() => copy(JSON.stringify(merkleProof))} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Input className="w-full mb-2" label="External Nullifier" value={externalNullifier} />
|
||||
<Input className="w-full mb-2" label="Signal" value={signal} />
|
||||
</FullModalContent>
|
||||
{props.error && <div className="text-xs text-red-500 text-center pb-1">{props.error}</div>}
|
||||
<FullModalFooter>
|
||||
<Button btnType={ButtonType.secondary} onClick={props.reject} loading={props.loading}>
|
||||
Reject
|
||||
</Button>
|
||||
<Button className="ml-2" onClick={props.accept} loading={props.loading}>
|
||||
Approve
|
||||
</Button>
|
||||
</FullModalFooter>
|
||||
</FullModal>
|
||||
)
|
||||
}
|
||||
127
src/ui/components/ConnectionModal/index.tsx
Normal file
127
src/ui/components/ConnectionModal/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import FullModal, { FullModalContent, FullModalFooter, FullModalHeader } from '@src/ui/components/FullModal'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
import Button, { ButtonType } from '@src/ui/components/Button'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import Checkbox from '@src/ui/components/Checkbox'
|
||||
import { getLinkPreview } from 'link-preview-js'
|
||||
|
||||
export default function ConnectionModal(props: { onClose: () => void; refreshConnectionStatus: () => void }) {
|
||||
const { onClose, refreshConnectionStatus } = props
|
||||
|
||||
const [checked, setChecked] = useState(false)
|
||||
const [url, setUrl] = useState<URL>()
|
||||
const [faviconUrl, setFaviconUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
;(async function onConnectionModalMount() {
|
||||
try {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true
|
||||
})
|
||||
|
||||
const [tab] = tabs || []
|
||||
|
||||
if (tab?.url) {
|
||||
setUrl(new URL(tab.url))
|
||||
}
|
||||
} catch (e) {}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (url?.origin) {
|
||||
const res = await postMessage({
|
||||
method: RPCAction.GET_HOST_PERMISSIONS,
|
||||
payload: url?.origin
|
||||
})
|
||||
setChecked(res?.noApproval)
|
||||
}
|
||||
})()
|
||||
}, [url])
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (url?.origin) {
|
||||
const data = await getLinkPreview(url?.origin)
|
||||
const [favicon] = data?.favicons || []
|
||||
setFaviconUrl(favicon)
|
||||
}
|
||||
})()
|
||||
}, [url])
|
||||
|
||||
const onRemoveHost = useCallback(async () => {
|
||||
await postMessage({
|
||||
method: RPCAction.REMOVE_HOST,
|
||||
payload: {
|
||||
host: url?.origin
|
||||
}
|
||||
})
|
||||
await refreshConnectionStatus()
|
||||
props.onClose()
|
||||
}, [url?.origin])
|
||||
|
||||
const setApproval = useCallback(
|
||||
async (noApproval: boolean) => {
|
||||
const res = await postMessage({
|
||||
method: RPCAction.SET_HOST_PERMISSIONS,
|
||||
payload: {
|
||||
host: url?.origin,
|
||||
noApproval
|
||||
}
|
||||
})
|
||||
setChecked(res?.noApproval)
|
||||
},
|
||||
[url?.origin]
|
||||
)
|
||||
|
||||
return (
|
||||
<FullModal onClose={onClose}>
|
||||
<FullModalHeader onClose={onClose}>
|
||||
{url?.protocol === 'chrome-extension:' ? 'Chrome Extension Page' : url?.host}
|
||||
</FullModalHeader>
|
||||
<FullModalContent className="flex flex-col items-center">
|
||||
{url?.protocol === 'chrome-extension:' ? (
|
||||
<div className="w-16 h-16 rounded-full my-6 border border-gray-800 p-2 flex-shrink-0 flex flex-row items-center justify-center">
|
||||
<Icon fontAwesome="fas fa-tools" size={1.5} className="text-gray-700" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-full my-6 border border-gray-800 p-2 flex-shrink-0 flex flex-row items-center justify-center">
|
||||
<div
|
||||
className="w-16 h-16"
|
||||
style={{
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundImage: `url(${faviconUrl})`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="font-bold">Permissions</div>
|
||||
<div className="flex flex-row items-start">
|
||||
<Checkbox
|
||||
className="mr-2 mt-2 flex-shrink-0"
|
||||
checked={checked}
|
||||
onChange={(e) => {
|
||||
setApproval(e.target.checked)
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm mt-2">Allow host to create proof without approvals</div>
|
||||
</div>
|
||||
</FullModalContent>
|
||||
<FullModalFooter className="justify-center">
|
||||
<Button className="ml-2" btnType={ButtonType.secondary} onClick={onRemoveHost}>
|
||||
Disconnect
|
||||
</Button>
|
||||
<Button className="ml-2" onClick={props.onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</FullModalFooter>
|
||||
</FullModal>
|
||||
)
|
||||
}
|
||||
83
src/ui/components/CreateIdentityModal/index.tsx
Normal file
83
src/ui/components/CreateIdentityModal/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { createIdentity } from '@src/ui/ducks/identities'
|
||||
import FullModal, { FullModalContent, FullModalFooter, FullModalHeader } from '@src/ui/components/FullModal'
|
||||
import Dropdown from '@src/ui/components/Dropdown'
|
||||
import Input from '@src/ui/components/Input'
|
||||
import Button from '@src/ui/components/Button'
|
||||
|
||||
export default function CreateIdentityModal(props: { onClose: () => void }): ReactElement {
|
||||
const [nonce, setNonce] = useState(0)
|
||||
const [identityType, setIdentityType] = useState<'InterRep' | 'Random'>('InterRep')
|
||||
const [web2Provider, setWeb2Provider] = useState<'Twitter' | 'Github' | 'Reddit'>('Twitter')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const create = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
let options: any = {
|
||||
nonce,
|
||||
web2Provider
|
||||
}
|
||||
let provider = 'interrep'
|
||||
|
||||
if (identityType === 'Random') {
|
||||
provider = 'random'
|
||||
options = {}
|
||||
}
|
||||
|
||||
await dispatch(createIdentity(provider, options))
|
||||
props.onClose()
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [nonce, web2Provider, identityType])
|
||||
|
||||
return (
|
||||
<FullModal onClose={props.onClose}>
|
||||
<FullModalHeader onClose={props.onClose}>Create Identity</FullModalHeader>
|
||||
<FullModalContent>
|
||||
<Dropdown
|
||||
className="my-2"
|
||||
label="Identity type"
|
||||
options={[{ value: 'InterRep' }, { value: 'Random' }]}
|
||||
onChange={(e) => {
|
||||
setIdentityType(e.target.value as any)
|
||||
}}
|
||||
value={identityType}
|
||||
/>
|
||||
{identityType === 'InterRep' && (
|
||||
<>
|
||||
<Dropdown
|
||||
className="my-2"
|
||||
label="Web2 Provider"
|
||||
options={[{ value: 'Twitter' }, { value: 'Reddit' }, { value: 'Github' }]}
|
||||
onChange={(e) => {
|
||||
setWeb2Provider(e.target.value as any)
|
||||
}}
|
||||
value={web2Provider}
|
||||
/>
|
||||
<Input
|
||||
className="my-2"
|
||||
type="number"
|
||||
label="Nonce"
|
||||
step={1}
|
||||
defaultValue={nonce}
|
||||
onChange={(e) => setNonce(Number(e.target.value))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FullModalContent>
|
||||
{error && <div className="text-xs text-red-500 text-center pb-1">{error}</div>}
|
||||
<FullModalFooter>
|
||||
<Button onClick={create} loading={loading}>
|
||||
Create
|
||||
</Button>
|
||||
</FullModalFooter>
|
||||
</FullModal>
|
||||
)
|
||||
}
|
||||
40
src/ui/components/Dropdown/dropdown.scss
Normal file
40
src/ui/components/Dropdown/dropdown.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
@import '../../../util/variables';
|
||||
|
||||
.dropdown {
|
||||
@extend %regular-font;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
color: $white;
|
||||
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
|
||||
|
||||
&__label {
|
||||
@extend %small-font;
|
||||
color: $label-text;
|
||||
padding: 0 0.5rem 0.25rem;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__group {
|
||||
@extend %row-nowrap;
|
||||
background-color: lighten($black, 15);
|
||||
border-bottom: 1px solid transparent;
|
||||
border-radius: 0.125rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
select {
|
||||
flex: 1 1 auto;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
@extend %small-font;
|
||||
color: $error-red;
|
||||
padding: 0.25rem 0.5rem 0;
|
||||
}
|
||||
}
|
||||
33
src/ui/components/Dropdown/index.tsx
Normal file
33
src/ui/components/Dropdown/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
/* eslint-disable react/require-default-props */
|
||||
import React, { InputHTMLAttributes, ReactElement } from 'react'
|
||||
import './dropdown.scss'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type Props = {
|
||||
label?: string
|
||||
errorMessage?: string
|
||||
options: { value: string; label?: string }[]
|
||||
} & InputHTMLAttributes<HTMLSelectElement>
|
||||
|
||||
export default function Dropdown(props: Props): ReactElement {
|
||||
const { label, errorMessage, className, ...selectProps } = props
|
||||
return (
|
||||
<div className={classNames('dropdown', className)}>
|
||||
{label && <div className="dropdown__label">{label}</div>}
|
||||
<div className="dropdown__group">
|
||||
<select {...selectProps}>
|
||||
{props.options.map(({ value, label }) => (
|
||||
<option key={value} value={value}>
|
||||
{label || value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{errorMessage && <div className="dropdown__error-message">{errorMessage}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
src/ui/components/FullModal/full-modal.scss
Normal file
52
src/ui/components/FullModal/full-modal.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
@import '../../../util/variables';
|
||||
|
||||
.full-modal {
|
||||
@extend %col-nowrap;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@media only screen and (min-width: 358px) {
|
||||
border: 1px solid $gray-700;
|
||||
max-width: 32rem;
|
||||
min-height: 24rem;
|
||||
max-height: 36rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 357px) {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&__header {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background-color: $gray-900;
|
||||
border-bottom: 1px solid $gray-800;
|
||||
|
||||
&__content {
|
||||
@extend %row-nowrap;
|
||||
}
|
||||
|
||||
&__action {
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
@extend %col-nowrap;
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
height: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
border-top: 1px solid $gray-800;
|
||||
flex: 0 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
}
|
||||
41
src/ui/components/FullModal/index.tsx
Normal file
41
src/ui/components/FullModal/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import Modal from '@src/ui/components/Modal'
|
||||
import classNames from 'classnames'
|
||||
import './full-modal.scss'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}
|
||||
export default function FullModal(props: Props): ReactElement {
|
||||
return (
|
||||
<Modal className={classNames('full-modal', props.className)} onClose={props.onClose}>
|
||||
{props.children}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export var FullModalHeader = function(props: {
|
||||
className?: string
|
||||
children: ReactNode
|
||||
onClose?: () => void
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className={classNames('full-modal__header', props.className)}>
|
||||
<div className="text-xl flex-grow flex-shrink full-modal__header__content">{props.children}</div>
|
||||
<div className="flex-grow-0 flex-shrink-0 full-modal__header__action">
|
||||
{props.onClose && <Icon fontAwesome="fas fa-times" size={1.25} onClick={props.onClose} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export var FullModalContent = function(props: { className?: string; children: ReactNode }): ReactElement {
|
||||
return <div className={classNames('full-modal__content', props.className)}>{props.children}</div>
|
||||
}
|
||||
|
||||
export var FullModalFooter = function(props: { className?: string; children: ReactNode }): ReactElement {
|
||||
return <div className={classNames('full-modal__footer', props.className)}>{props.children}</div>
|
||||
}
|
||||
42
src/ui/components/Header/header.scss
Normal file
42
src/ui/components/Header/header.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
@import '../../../util/variables';
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid $gray-800;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&__network-type {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: $gray-800;
|
||||
color: $gray-200;
|
||||
font-weight: 600;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
&__account-icon {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.375rem;
|
||||
width: 2.375rem;
|
||||
border-radius: 50%;
|
||||
border: 3px solid $gray-600;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: $gray-300;
|
||||
}
|
||||
|
||||
.paper {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
.menuable {
|
||||
&__menu {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/ui/components/Header/index.tsx
Normal file
59
src/ui/components/Header/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { ReactElement, useCallback } from 'react'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
import LogoSVG from '@src/static/icons/logo.svg'
|
||||
import LoaderSVG from '@src/static/icons/loader.svg'
|
||||
import { useAccount, useNetwork, useWeb3Connecting } from '@src/ui/ducks/web3'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon'
|
||||
import './header.scss'
|
||||
import classNames from 'classnames'
|
||||
import Menuable from '@src/ui/components/Menuable'
|
||||
|
||||
export default function Header(): ReactElement {
|
||||
const network = useNetwork()
|
||||
const account = useAccount()
|
||||
const web3Connecting = useWeb3Connecting()
|
||||
|
||||
const connectMetamask = useCallback(async () => {
|
||||
await postMessage({ method: RPCAction.CONNECT_METAMASK })
|
||||
}, [])
|
||||
|
||||
const disconnect = useCallback(async () => {
|
||||
await postMessage({ method: RPCAction.LOCK })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="header h-16 flex flex-row items-center px-4">
|
||||
<Icon url={LogoSVG} size={3} />
|
||||
<div className="flex-grow flex flex-row items-center justify-end header__content">
|
||||
{network && <div className="text-sm rounded-full header__network-type">{network?.name}</div>}
|
||||
<div className="header__account-icon">
|
||||
{account ? (
|
||||
<Menuable
|
||||
className="flex user-menu"
|
||||
items={[
|
||||
{
|
||||
label: 'Logout',
|
||||
onClick: disconnect
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Jazzicon diameter={32} seed={jsNumberForAddress(account)} />
|
||||
</Menuable>
|
||||
) : (
|
||||
<div onClick={connectMetamask}>
|
||||
<Icon
|
||||
fontAwesome={classNames({
|
||||
'fas fa-plug': !web3Connecting
|
||||
})}
|
||||
url={web3Connecting ? LoaderSVG : undefined}
|
||||
size={1.25}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
src/ui/components/Icon/icon.scss
Normal file
48
src/ui/components/Icon/icon.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
@import './src/util/variables';
|
||||
|
||||
.icon {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
user-select: none;
|
||||
|
||||
&__text {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
font-weight: 400;
|
||||
height: 80%;
|
||||
width: 80%;
|
||||
background-color: rgba(#000, 0.05);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&--clickable {
|
||||
@extend %clickable;
|
||||
}
|
||||
}
|
||||
|
||||
button.icon {
|
||||
cursor: pointer;
|
||||
padding: 0 !important;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
39
src/ui/components/Icon/index.tsx
Normal file
39
src/ui/components/Icon/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable react/prefer-stateless-function */
|
||||
import React, { Component, MouseEventHandler } from 'react'
|
||||
import c from 'classnames'
|
||||
import './icon.scss'
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
fontAwesome?: string
|
||||
className?: string
|
||||
size?: number
|
||||
onClick?: MouseEventHandler
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default class Icon extends Component<Props> {
|
||||
render() {
|
||||
const { url, size = 0.75, className = '', disabled, fontAwesome, onClick } = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={c('icon', className, {
|
||||
'icon--disabled': disabled,
|
||||
'icon--clickable': onClick
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: url ? `url(${url})` : undefined,
|
||||
width: !fontAwesome ? `${size}rem` : undefined,
|
||||
height: !fontAwesome ? `${size}rem` : undefined,
|
||||
fontSize: fontAwesome && `${size}rem`
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{fontAwesome && <i className={`fas ${fontAwesome}`} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
36
src/ui/components/Input/index.tsx
Normal file
36
src/ui/components/Input/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { InputHTMLAttributes, MouseEventHandler } from 'react'
|
||||
|
||||
import './input.scss'
|
||||
import classNames from 'classnames'
|
||||
import Icon from '../Icon'
|
||||
|
||||
type Props = {
|
||||
label?: string
|
||||
errorMessage?: string
|
||||
fontAwesome?: string
|
||||
url?: string
|
||||
onIconClick?: MouseEventHandler
|
||||
} & InputHTMLAttributes<HTMLInputElement>
|
||||
|
||||
export default function Input(props: Props) {
|
||||
const { fontAwesome, url, size = 1, onIconClick, label, errorMessage, className, ...inputProps } = props
|
||||
|
||||
return (
|
||||
<div className={classNames(`input-group`, className)}>
|
||||
{label && <div className="input-group__label">{label}</div>}
|
||||
<div className="input-group__group">
|
||||
<input
|
||||
className={classNames('input', {
|
||||
'input--full-width': !url && !fontAwesome
|
||||
})}
|
||||
title={label}
|
||||
{...(inputProps as any)}
|
||||
/>
|
||||
{(!!url || !!fontAwesome) && (
|
||||
<Icon fontAwesome={fontAwesome} url={url} size={size} onClick={onIconClick} />
|
||||
)}
|
||||
</div>
|
||||
{errorMessage && <div className="input-group__error-message">{errorMessage}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
src/ui/components/Input/input.scss
Normal file
73
src/ui/components/Input/input.scss
Normal file
@@ -0,0 +1,73 @@
|
||||
@import './src/util/variables';
|
||||
|
||||
.input {
|
||||
@extend %regular-font;
|
||||
padding: 0.5rem 0.5rem;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: calc(100% - 2rem);
|
||||
color: $primary-text;
|
||||
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
|
||||
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: rgba($white, 0.2);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@extend %col-nowrap;
|
||||
|
||||
&__label {
|
||||
@extend %small-font;
|
||||
color: $label-text;
|
||||
padding: 0 0.5rem 0.25rem;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
@extend %small-font;
|
||||
color: $error-red;
|
||||
padding: 0.25rem 0.5rem 0;
|
||||
}
|
||||
|
||||
&__group {
|
||||
@extend %row-nowrap;
|
||||
background-color: lighten($black, 15);
|
||||
border-bottom: 1px solid transparent;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
&:focus-within {
|
||||
background-color: $gray-900;
|
||||
border-bottom: 1px solid $primary-green;
|
||||
}
|
||||
|
||||
.icon {
|
||||
opacity: 0.1;
|
||||
transition: opacity 150ms ease-in-out;
|
||||
margin-right: 0.75rem;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
170
src/ui/components/Menuable/index.tsx
Normal file
170
src/ui/components/Menuable/index.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { MouseEvent, ReactElement, ReactNode, useCallback, useEffect, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import './menuable.scss'
|
||||
import Icon from '../Icon'
|
||||
|
||||
type MenuableProps = {
|
||||
items: ItemProps[]
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
menuClassName?: string
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
opened?: boolean
|
||||
}
|
||||
|
||||
export type ItemProps = {
|
||||
label: string
|
||||
iconUrl?: string
|
||||
iconFA?: string
|
||||
iconClassName?: string
|
||||
className?: string
|
||||
onClick?: (e: MouseEvent, reset: () => void) => void
|
||||
disabled?: boolean
|
||||
children?: ItemProps[]
|
||||
component?: ReactNode
|
||||
}
|
||||
|
||||
export default function Menuable(props: MenuableProps): ReactElement {
|
||||
const { opened } = props
|
||||
|
||||
const [isShowing, setShowing] = useState(!!props.opened)
|
||||
const [path, setPath] = useState<number[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof opened !== 'undefined') {
|
||||
setShowing(opened)
|
||||
if (!opened) {
|
||||
setPath([])
|
||||
}
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.onClose && props.onClose()
|
||||
setShowing(false)
|
||||
}, [])
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
props.onOpen && props.onOpen()
|
||||
setShowing(true)
|
||||
|
||||
const cb = () => {
|
||||
onClose()
|
||||
window.removeEventListener('click', cb)
|
||||
}
|
||||
|
||||
window.addEventListener('click', cb)
|
||||
}, [onClose])
|
||||
|
||||
const goBack = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
const newPath = [...path]
|
||||
newPath.pop()
|
||||
setPath(newPath)
|
||||
},
|
||||
[path]
|
||||
)
|
||||
|
||||
const onItemClick = useCallback(
|
||||
(e, item, i) => {
|
||||
e.stopPropagation()
|
||||
if (item.disabled) return
|
||||
if (item.children) {
|
||||
setPath([...path, i])
|
||||
} else if (item.onClick) {
|
||||
item.onClick(e, () => setPath([]))
|
||||
}
|
||||
},
|
||||
[path]
|
||||
)
|
||||
|
||||
let {items} = props
|
||||
|
||||
if (path) {
|
||||
for (const pathIndex of path) {
|
||||
if (items[pathIndex].children) {
|
||||
items = items[pathIndex].children as ItemProps[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'menuable',
|
||||
{
|
||||
'menuable--active': isShowing
|
||||
},
|
||||
props.className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (isShowing) return onClose()
|
||||
onOpen()
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
{isShowing && (
|
||||
<div className={classNames('rounded-xl border border-gray-700 menuable__menu', props.menuClassName)}>
|
||||
{!!path.length && (
|
||||
<div
|
||||
className={classNames(
|
||||
'text-sm whitespace-nowrap cursor-pointer',
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
'text-gray-500 hover:text-gray-300 hover:bg-gray-900 menuable__menu__item'
|
||||
)}
|
||||
onClick={goBack}
|
||||
>
|
||||
<Icon fontAwesome="fas fa-caret-left" />
|
||||
<span className="ml-2">Go back</span>
|
||||
</div>
|
||||
)}
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={classNames(
|
||||
'text-sm whitespace-nowrap',
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
'menuable__menu__item hover:bg-gray-900 ',
|
||||
{ 'cursor-pointer': !item.disabled },
|
||||
item.className
|
||||
)}
|
||||
onClick={(e) => onItemClick(e, item, i)}
|
||||
>
|
||||
{item.component ? (
|
||||
item.component
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={classNames('flex-grow', {
|
||||
'text-gray-500 hover:text-gray-300 hover:font-semibold': !item.disabled,
|
||||
'text-gray-700': item.disabled
|
||||
})}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
{(item.iconUrl || item.iconFA) && (
|
||||
<Icon
|
||||
fontAwesome={item.iconFA}
|
||||
url={item.iconUrl}
|
||||
className={classNames(
|
||||
'ml-4',
|
||||
{
|
||||
'opacity-50': item.disabled
|
||||
},
|
||||
item.iconClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
22
src/ui/components/Menuable/menuable.scss
Normal file
22
src/ui/components/Menuable/menuable.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
@import '../../../util/variables';
|
||||
|
||||
.menuable {
|
||||
position: relative;
|
||||
|
||||
&__menu {
|
||||
position: absolute;
|
||||
margin-top: 0.5rem;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: $black;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
z-index: 300;
|
||||
|
||||
&__item {
|
||||
@extend %row-nowrap;
|
||||
align-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/ui/components/Modal/index.tsx
Normal file
28
src/ui/components/Modal/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { MouseEventHandler, ReactElement, ReactNode } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './modal.scss'
|
||||
|
||||
let modalRoot: HTMLDivElement | null
|
||||
|
||||
export type ModalProps = {
|
||||
onClose?: MouseEventHandler
|
||||
className?: string
|
||||
children?: ReactNode | ReactNode[]
|
||||
}
|
||||
|
||||
export default function Modal(props: ModalProps): ReactElement {
|
||||
const { className = '', onClose, children } = props
|
||||
|
||||
modalRoot = document.querySelector('#modal')
|
||||
|
||||
if (!modalRoot) return <></>
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className="modal__overlay" onClick={onClose}>
|
||||
<div className={`modal__wrapper ${className}`} onClick={(e) => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
modalRoot
|
||||
)
|
||||
}
|
||||
28
src/ui/components/Modal/modal.scss
Normal file
28
src/ui/components/Modal/modal.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@import '../../../util/variables';
|
||||
|
||||
div#modal {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
&__overlay {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba($black, 0.7);
|
||||
z-index: 100;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
max-width: 24rem;
|
||||
margin: 3rem auto;
|
||||
background-color: $bg-black;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 8px 4px rgba($black, 0.2);
|
||||
z-index: 200;
|
||||
}
|
||||
}
|
||||
21
src/ui/components/SwitchButton/index.tsx
Normal file
21
src/ui/components/SwitchButton/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
/* eslint-disable react/require-default-props */
|
||||
import React, { ChangeEventHandler, ReactElement } from 'react'
|
||||
import './switch-button.scss'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type Props = {
|
||||
checked?: boolean
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function SwitchButton(props: Props): ReactElement {
|
||||
return (
|
||||
<div className={classNames('switch-button', props.className)}>
|
||||
<input type="checkbox" onChange={props.onChange} checked={props.checked} />
|
||||
<span className="slider round" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
66
src/ui/components/SwitchButton/switch-button.scss
Normal file
66
src/ui/components/SwitchButton/switch-button.scss
Normal file
@@ -0,0 +1,66 @@
|
||||
@import './src/util/variables';
|
||||
|
||||
.switch-button {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 3.75rem;
|
||||
height: 2rem;
|
||||
}
|
||||
/* Hide default HTML checkbox */
|
||||
.switch-button input {
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 200;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: $border-gray;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: $primary-blue;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px $primary-blue;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
29
src/ui/components/Textarea/index.tsx
Normal file
29
src/ui/components/Textarea/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React, { MouseEventHandler, TextareaHTMLAttributes } from 'react'
|
||||
|
||||
import './textarea.scss'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type Props = {
|
||||
label?: string
|
||||
errorMessage?: string
|
||||
fontAwesome?: string
|
||||
url?: string
|
||||
onIconClick?: MouseEventHandler
|
||||
} & TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
|
||||
export default function Textarea(props: Props) {
|
||||
const { fontAwesome, onIconClick, label, errorMessage, className, ...textareaProps } = props
|
||||
|
||||
return (
|
||||
<div className={classNames(`textarea-group`, className)}>
|
||||
{label && <div className="textarea-group__label">{label}</div>}
|
||||
<div className="textarea-group__group">
|
||||
<textarea className="textarea" {...textareaProps} />
|
||||
</div>
|
||||
{errorMessage && <div className="textarea-group__error-message">{errorMessage}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
64
src/ui/components/Textarea/textarea.scss
Normal file
64
src/ui/components/Textarea/textarea.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
@import './src/util/variables';
|
||||
|
||||
.textarea {
|
||||
@extend %regular-font;
|
||||
margin: 0.5rem 0.5rem;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
color: $primary-text;
|
||||
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
|
||||
resize: none;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba($white, 0.2);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-group {
|
||||
@extend %col-nowrap;
|
||||
|
||||
&__label {
|
||||
@extend %small-font;
|
||||
color: $label-text;
|
||||
padding: 0 0.5rem 0.25rem;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
@extend %small-font;
|
||||
color: $error-red;
|
||||
padding: 0.25rem 0.5rem 0;
|
||||
}
|
||||
|
||||
&__group {
|
||||
@extend %row-nowrap;
|
||||
background-color: lighten($black, 15);
|
||||
border-bottom: 1px solid transparent;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
&:focus-within {
|
||||
background-color: rgba($black, 0.035);
|
||||
border-bottom: 1px solid $primary-blue;
|
||||
}
|
||||
|
||||
.icon {
|
||||
opacity: 0.1;
|
||||
transition: opacity 150ms ease-in-out;
|
||||
margin-right: 0.75rem;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/ui/ducks/app.ts
Normal file
58
src/ui/ducks/app.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Dispatch } from 'redux'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AppRootState } from '@src/ui/store/configureAppStore'
|
||||
import deepEqual from 'fast-deep-equal'
|
||||
|
||||
export enum ActionType {
|
||||
SET_STATUS = 'app/setStatus'
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionType
|
||||
payload?: payload
|
||||
meta?: any
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
initialized: boolean
|
||||
unlocked: boolean
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
initialized: false,
|
||||
unlocked: false
|
||||
}
|
||||
|
||||
export const setStatus = (status: {
|
||||
initialized: boolean
|
||||
unlocked: boolean
|
||||
}): Action<{
|
||||
initialized: boolean
|
||||
unlocked: boolean
|
||||
}> => ({
|
||||
type: ActionType.SET_STATUS,
|
||||
payload: status
|
||||
})
|
||||
|
||||
export const fetchStatus = () => async (dispatch: Dispatch) => {
|
||||
const status = await postMessage({ method: RPCAction.GET_STATUS })
|
||||
dispatch(setStatus(status))
|
||||
}
|
||||
|
||||
export default function app(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionType.SET_STATUS:
|
||||
return {
|
||||
...state,
|
||||
initialized: action.payload.initialized,
|
||||
unlocked: action.payload.unlocked
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const useAppStatus = () => useSelector((state: AppRootState) => state.app, deepEqual)
|
||||
143
src/ui/ducks/identities.ts
Normal file
143
src/ui/ducks/identities.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { IdentityMetadata } from '@src/types'
|
||||
import { Dispatch } from 'redux'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AppRootState } from '@src/ui/store/configureAppStore'
|
||||
import deepEqual from 'fast-deep-equal'
|
||||
|
||||
export enum ActionType {
|
||||
SET_COMMITMENTS = 'app/identities/setCommitments',
|
||||
SET_SELECTED = 'app/identities/setSelected',
|
||||
SET_REQUEST_PENDING = 'app/identities/setRequestPending'
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionType
|
||||
payload?: payload
|
||||
meta?: any
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
identityCommitments: string[]
|
||||
identityMap: {
|
||||
[commitment: string]: IdentityMetadata
|
||||
}
|
||||
requestPending: boolean
|
||||
selected: string
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
identityCommitments: [],
|
||||
identityMap: {},
|
||||
requestPending: false,
|
||||
selected: ''
|
||||
}
|
||||
|
||||
export const createIdentity = (strategy: string, options: any) => async (dispatch: Dispatch) =>
|
||||
postMessage({
|
||||
method: RPCAction.CREATE_IDENTITY,
|
||||
payload: {
|
||||
strategy,
|
||||
options
|
||||
}
|
||||
})
|
||||
|
||||
export const setActiveIdentity = (identityCommitment: string) => async (dispatch: Dispatch) => {
|
||||
if (!identityCommitment) {
|
||||
throw new Error('Identity Commitment not provided!')
|
||||
}
|
||||
return postMessage({
|
||||
method: RPCAction.SET_ACTIVE_IDENTITY,
|
||||
payload: identityCommitment
|
||||
})
|
||||
}
|
||||
|
||||
export const setSelected = (identityCommitment: string) => ({
|
||||
type: ActionType.SET_SELECTED,
|
||||
payload: identityCommitment
|
||||
})
|
||||
|
||||
export const setIdentities = (
|
||||
identities: { commitment: string; metadata: IdentityMetadata }[]
|
||||
): Action<{ commitment: string; metadata: IdentityMetadata }[]> => ({
|
||||
type: ActionType.SET_COMMITMENTS,
|
||||
payload: identities
|
||||
})
|
||||
|
||||
export const setIdentityRequestPending = (requestPending: boolean): Action<boolean> => ({
|
||||
type: ActionType.SET_REQUEST_PENDING,
|
||||
payload: requestPending
|
||||
})
|
||||
|
||||
export const fetchIdentities = () => async (dispatch: Dispatch) => {
|
||||
const identities = await postMessage({ method: RPCAction.GET_IDENTITIES })
|
||||
const selected = await postMessage({ method: RPCAction.GET_ACTIVE_IDENTITY })
|
||||
dispatch(setIdentities(identities))
|
||||
dispatch(setSelected(selected))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/default-param-last
|
||||
export default function identities(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionType.SET_COMMITMENTS:
|
||||
return reduceSetIdentities(state, action)
|
||||
case ActionType.SET_SELECTED:
|
||||
return {
|
||||
...state,
|
||||
selected: action.payload
|
||||
}
|
||||
case ActionType.SET_REQUEST_PENDING:
|
||||
return {
|
||||
...state,
|
||||
requestPending: action.payload
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function reduceSetIdentities(
|
||||
state: State,
|
||||
action: Action<{ commitment: string; metadata: IdentityMetadata }[]>
|
||||
): State {
|
||||
const identityCommitments: string[] = []
|
||||
const identityMap = {}
|
||||
|
||||
if (action.payload) {
|
||||
for (const id of action.payload) {
|
||||
identityMap[id.commitment] = id.metadata
|
||||
identityCommitments.push(id.commitment)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
identityMap,
|
||||
identityCommitments
|
||||
}
|
||||
}
|
||||
|
||||
export const useIdentities = () =>
|
||||
useSelector((state: AppRootState) => {
|
||||
const { identityMap, identityCommitments } = state.identities
|
||||
return identityCommitments.map((commitment) => ({
|
||||
commitment,
|
||||
metadata: identityMap[commitment]
|
||||
}))
|
||||
}, deepEqual)
|
||||
|
||||
export const useSelectedIdentity = () =>
|
||||
useSelector((state: AppRootState) => {
|
||||
const { identityMap, selected } = state.identities
|
||||
return {
|
||||
commitment: selected,
|
||||
metadata: identityMap[selected]
|
||||
}
|
||||
}, deepEqual)
|
||||
|
||||
export const useIdentityRequestPending = () =>
|
||||
useSelector((state: AppRootState) => state.identities.requestPending, deepEqual)
|
||||
68
src/ui/ducks/requests.ts
Normal file
68
src/ui/ducks/requests.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AppRootState } from '@src/ui/store/configureAppStore'
|
||||
import deepEqual from 'fast-deep-equal'
|
||||
import { PendingRequest } from '@src/types'
|
||||
import { Dispatch } from 'redux'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
|
||||
enum ActionType {
|
||||
SET_PENDING_REQUESTS = 'request/setPendingRequests'
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionType
|
||||
payload?: payload
|
||||
meta?: any
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
pendingRequests: PendingRequest[]
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
pendingRequests: []
|
||||
}
|
||||
|
||||
export const setPendingRequest = (pendingRequests: PendingRequest[]): Action<PendingRequest[]> => ({
|
||||
type: ActionType.SET_PENDING_REQUESTS,
|
||||
payload: pendingRequests
|
||||
})
|
||||
|
||||
export const fetchRequestPendingStatus = () => async (dispatch: Dispatch) => {
|
||||
const pendingRequests = await postMessage({ method: RPCAction.GET_PENDING_REQUESTS })
|
||||
dispatch(setPendingRequest(pendingRequests))
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: This pattern that return a function(dispatch, getState) is called a Thunk
|
||||
* You don't need to use a thunk unless you want to dispatch an action after async requests
|
||||
* When calling a thunk, you must dispatch it, e.g.:
|
||||
*
|
||||
* dispatch(finalizeRequest('0', 'dummy'));
|
||||
*/
|
||||
// export const finalizeRequest = (id: string, action: RequestResolutionAction) => async (dispatch: Dispatch) => {
|
||||
// return postMessage({
|
||||
// method: RPCAction.FINALIZE_REQUEST,
|
||||
// payload: {
|
||||
// id,
|
||||
// action,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/default-param-last
|
||||
export default function requests(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionType.SET_PENDING_REQUESTS:
|
||||
return {
|
||||
...state,
|
||||
pendingRequests: action.payload
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const useRequestsPending = () => useSelector((state: AppRootState) => state.requests.pendingRequests, deepEqual)
|
||||
132
src/ui/ducks/web3.ts
Normal file
132
src/ui/ducks/web3.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import deepEqual from 'fast-deep-equal'
|
||||
import { AppRootState } from '@src/ui/store/configureAppStore'
|
||||
import { Dispatch } from 'redux'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import ChainsJSON from '@src/static/chains.json'
|
||||
|
||||
type ChainInfo = {
|
||||
chainId: number
|
||||
infoURL: string
|
||||
name: string
|
||||
nativeCurrency: {
|
||||
name: string
|
||||
symbol: string
|
||||
decimals: number
|
||||
}
|
||||
shortName: string
|
||||
}
|
||||
|
||||
export const chainsMap = ChainsJSON.reduce(
|
||||
(
|
||||
map: {
|
||||
[id: number]: ChainInfo
|
||||
},
|
||||
chainInfo: ChainInfo
|
||||
) => {
|
||||
map[chainInfo.chainId] = chainInfo
|
||||
return map
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
enum ActionTypes {
|
||||
SET_LOADING = 'web3/setLoading',
|
||||
SET_CONNECTING = 'web3/setConnecting',
|
||||
SET_ACCOUNT = 'web3/setAccount',
|
||||
SET_NETWORK = 'web3/setNetwork',
|
||||
SET_CHAIN_ID = 'web3/setChainId'
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionTypes
|
||||
payload?: payload
|
||||
meta?: any
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
account: string
|
||||
networkType: string
|
||||
chainId: number
|
||||
loading: boolean
|
||||
connecting: boolean
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
account: '',
|
||||
networkType: '',
|
||||
chainId: -1,
|
||||
loading: false,
|
||||
connecting: false
|
||||
}
|
||||
|
||||
export const setWeb3Connecting = (connecting: boolean): Action<boolean> => ({
|
||||
type: ActionTypes.SET_CONNECTING,
|
||||
payload: connecting
|
||||
})
|
||||
|
||||
export const setAccount = (account: string): Action<string> => ({
|
||||
type: ActionTypes.SET_ACCOUNT,
|
||||
payload: account
|
||||
})
|
||||
|
||||
export const setNetwork = (network: string): Action<string> => ({
|
||||
type: ActionTypes.SET_NETWORK,
|
||||
payload: network
|
||||
})
|
||||
|
||||
export const setChainId = (chainId: number): Action<number> => ({
|
||||
type: ActionTypes.SET_CHAIN_ID,
|
||||
payload: chainId
|
||||
})
|
||||
|
||||
export const fetchWalletInfo = () => async (dispatch: Dispatch) => {
|
||||
const info = await postMessage({ method: RPCAction.GET_WALLET_INFO })
|
||||
|
||||
if (info) {
|
||||
dispatch(setAccount(info.account))
|
||||
dispatch(setNetwork(info.networkType))
|
||||
dispatch(setChainId(info.chainId))
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/default-param-last
|
||||
export default function web3(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_ACCOUNT:
|
||||
return {
|
||||
...state,
|
||||
account: action.payload
|
||||
}
|
||||
case ActionTypes.SET_NETWORK:
|
||||
return {
|
||||
...state,
|
||||
networkType: action.payload
|
||||
}
|
||||
case ActionTypes.SET_CHAIN_ID:
|
||||
return {
|
||||
...state,
|
||||
chainId: action.payload
|
||||
}
|
||||
case ActionTypes.SET_CONNECTING:
|
||||
return {
|
||||
...state,
|
||||
connecting: action.payload
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const useWeb3Connecting = () => useSelector((state: AppRootState) => state.web3.connecting, deepEqual)
|
||||
|
||||
export const useAccount = () => useSelector((state: AppRootState) => state.web3.account, deepEqual)
|
||||
|
||||
export const useNetwork = (): ChainInfo | null => useSelector((state: AppRootState) => {
|
||||
const chainInfo = chainsMap[state.web3.chainId]
|
||||
return chainInfo || null
|
||||
}, deepEqual)
|
||||
|
||||
export const useChainId = () => useSelector((state: AppRootState) => state.web3.chainId, deepEqual)
|
||||
147
src/ui/pages/Home/home.scss
Normal file
147
src/ui/pages/Home/home.scss
Normal file
@@ -0,0 +1,147 @@
|
||||
@import '../../../util/variables';
|
||||
|
||||
.home {
|
||||
position: relative;
|
||||
|
||||
&__info {
|
||||
@extend %col-nowrap;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
&__scroller {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.home__list__fix-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--fixed-menu {
|
||||
.home__list__header {
|
||||
//display: none;
|
||||
}
|
||||
|
||||
.home__list__fix-header {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: $bg-black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__connection-button {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
padding: 0.25rem;
|
||||
border-radius: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
|
||||
&--connected {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
background-color: $gray-500;
|
||||
border-radius: 50%;
|
||||
margin: 0.25rem 0.5rem 0.25rem 0.25rem;
|
||||
|
||||
&--connected {
|
||||
background-color: $success-green;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: $gray-300;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
@extend %col-nowrap;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&__header {
|
||||
@extend %row-nowrap;
|
||||
|
||||
&__tab {
|
||||
@extend %row-nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
border-bottom: 2px solid $gray-800;
|
||||
|
||||
&--selected {
|
||||
border-color: $primary-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.identity-row {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $gray-900;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-950;
|
||||
}
|
||||
|
||||
&__select-icon {
|
||||
font-size: 0.8125rem !important;
|
||||
border: 2px solid $gray-800;
|
||||
color: $gray-800;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
margin-right: 1rem;
|
||||
padding: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
color: $primary-green;
|
||||
border-color: $primary-green;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
color: $primary-green;
|
||||
border-color: $primary-green;
|
||||
}
|
||||
}
|
||||
|
||||
&__menu-icon {
|
||||
font-size: 0.8125rem !important;
|
||||
color: $gray-600;
|
||||
|
||||
&:hover {
|
||||
color: $gray-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-identity-row {
|
||||
&:hover {
|
||||
color: $gray-300;
|
||||
|
||||
.icon {
|
||||
color: $primary-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
216
src/ui/pages/Home/index.tsx
Normal file
216
src/ui/pages/Home/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import RPCAction from '@src/util/constants'
|
||||
import { fetchWalletInfo, useNetwork } from '@src/ui/ducks/web3'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
import {
|
||||
fetchIdentities,
|
||||
setActiveIdentity,
|
||||
useIdentities,
|
||||
useSelectedIdentity
|
||||
} from '@src/ui/ducks/identities'
|
||||
import Header from '@src/ui/components/Header'
|
||||
import classNames from 'classnames'
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
import './home.scss'
|
||||
import {ellipsify} from '@src/util/account'
|
||||
import CreateIdentityModal from '@src/ui/components/CreateIdentityModal'
|
||||
import ConnectionModal from '@src/ui/components/ConnectionModal'
|
||||
|
||||
export default function Home(): ReactElement {
|
||||
const dispatch = useDispatch()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [fixedTabs, fixTabs] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchIdentities())
|
||||
dispatch(fetchWalletInfo())
|
||||
}, [])
|
||||
|
||||
const onScroll = useCallback(async () => {
|
||||
if (!scrollRef.current) return
|
||||
|
||||
const scrollTop = scrollRef.current?.scrollTop
|
||||
|
||||
fixTabs(scrollTop > 92)
|
||||
}, [scrollRef])
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col home">
|
||||
<Header />
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className={classNames('flex flex-col flex-grow flex-shrink overflow-y-auto home__scroller', {
|
||||
'home__scroller--fixed-menu': fixedTabs
|
||||
})}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<HomeInfo />
|
||||
<HomeList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
var HomeInfo = function(): ReactElement {
|
||||
const network = useNetwork()
|
||||
const [connected, setConnected] = useState(false)
|
||||
const [showingModal, showModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
await refreshConnectionStatus()
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const refreshConnectionStatus = useCallback(async () => {
|
||||
try {
|
||||
const tabs = await browser.tabs.query({ active: true, lastFocusedWindow: true })
|
||||
const [tab] = tabs || []
|
||||
|
||||
if (tab?.url) {
|
||||
const { origin } = new URL(tab.url)
|
||||
const isHostApproved = await postMessage({
|
||||
method: RPCAction.IS_HOST_APPROVED,
|
||||
payload: origin
|
||||
})
|
||||
|
||||
setConnected(isHostApproved)
|
||||
}
|
||||
} catch (e) {
|
||||
setConnected(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showingModal && (
|
||||
<ConnectionModal onClose={() => showModal(false)} refreshConnectionStatus={refreshConnectionStatus} />
|
||||
)}
|
||||
<div className="home__info">
|
||||
<div
|
||||
className={classNames('home__connection-button', {
|
||||
'home__connection-button--connected': connected
|
||||
})}
|
||||
onClick={connected ? () => showModal(true) : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames('home__connection-button__icon', {
|
||||
'home__connection-button__icon--connected': connected
|
||||
})}
|
||||
/>
|
||||
<div className="text-xs home__connection-button__text">
|
||||
{connected ? 'Connected' : 'Not Connected'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-semibold">
|
||||
{network ? `0.0000 ${network.nativeCurrency.symbol}` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
var HomeList = function(): ReactElement {
|
||||
const [selectedTab, selectTab] = useState<'identities' | 'activity'>('identities')
|
||||
|
||||
return (
|
||||
<div className="home__list">
|
||||
<div className="home__list__header">
|
||||
<div
|
||||
className={classNames('home__list__header__tab', {
|
||||
'home__list__header__tab--selected': selectedTab === 'identities'
|
||||
})}
|
||||
onClick={() => selectTab('identities')}
|
||||
>
|
||||
Identities
|
||||
</div>
|
||||
<div
|
||||
className={classNames('home__list__header__tab', {
|
||||
'home__list__header__tab--selected': selectedTab === 'activity'
|
||||
})}
|
||||
onClick={() => selectTab('activity')}
|
||||
>
|
||||
Activity
|
||||
</div>
|
||||
</div>
|
||||
<div className="home__list__fix-header">
|
||||
<div
|
||||
className={classNames('home__list__header__tab', {
|
||||
'home__list__header__tab--selected': selectedTab === 'identities'
|
||||
})}
|
||||
onClick={() => selectTab('identities')}
|
||||
>
|
||||
Identities
|
||||
</div>
|
||||
<div
|
||||
className={classNames('home__list__header__tab', {
|
||||
'home__list__header__tab--selected': selectedTab === 'activity'
|
||||
})}
|
||||
onClick={() => selectTab('activity')}
|
||||
>
|
||||
Activity
|
||||
</div>
|
||||
</div>
|
||||
<div className="home__list__content">
|
||||
{selectedTab === 'identities' ? <IdentityList /> : null}
|
||||
{selectedTab === 'activity' ? <ActivityList /> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
var IdentityList = function(): ReactElement {
|
||||
const identities = useIdentities()
|
||||
const selected = useSelectedIdentity()
|
||||
const dispatch = useDispatch()
|
||||
const selectIdentity = useCallback(async (identityCommitment: string) => {
|
||||
dispatch(setActiveIdentity(identityCommitment))
|
||||
}, [])
|
||||
const [showingModal, showModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchIdentities())
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showingModal && <CreateIdentityModal onClose={() => showModal(false)} />}
|
||||
{identities.map(({ commitment, metadata }, i) => (
|
||||
<div className="p-4 identity-row" key={commitment}>
|
||||
<Icon
|
||||
className={classNames('identity-row__select-icon', {
|
||||
'identity-row__select-icon--selected': selected.commitment === commitment
|
||||
})}
|
||||
fontAwesome="fas fa-check"
|
||||
onClick={() => selectIdentity(commitment)}
|
||||
/>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex flex-row items-center text-lg font-semibold">
|
||||
{`${metadata.name}`}
|
||||
<span className="text-xs py-1 px-2 ml-2 rounded-full bg-gray-500 text-gray-800">
|
||||
{metadata.provider}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-base text-gray-500">{ellipsify(commitment)}</div>
|
||||
</div>
|
||||
<Icon className="identity-row__menu-icon" fontAwesome="fas fa-ellipsis-h" />
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className="create-identity-row flex flex-row items-center justify-center p-4 cursor-pointer text-gray-600"
|
||||
onClick={() => showModal(true)}
|
||||
>
|
||||
<Icon fontAwesome="fas fa-plus" size={1} className="mr-2" />
|
||||
<div>Add Identity</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
var ActivityList = function(): ReactElement {
|
||||
return <div />
|
||||
}
|
||||
66
src/ui/pages/Login/index.tsx
Normal file
66
src/ui/pages/Login/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react'
|
||||
import './login.scss'
|
||||
import Button, { ButtonType } from '@src/ui/components/Button'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
import LogoSVG from '@src/static/icons/logo.svg'
|
||||
import Input from '@src/ui/components/Input'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import RPCAction from '@src/util/constants'
|
||||
|
||||
const Login = function(): ReactElement {
|
||||
const [pw, setPW] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const valid = !!pw
|
||||
|
||||
const login = useCallback(async () => {
|
||||
if (!valid) {
|
||||
setError('Invalid password')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
await postMessage({
|
||||
method: RPCAction.UNLOCK,
|
||||
payload: pw
|
||||
})
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [pw])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap h-full login">
|
||||
<div className="flex flex-col items-center flex-grow p-8 login__content">
|
||||
<Icon url={LogoSVG} />
|
||||
<div className="text-lg pt-8">
|
||||
<b>Welcome Back!</b>
|
||||
</div>
|
||||
<div className="text-base">To continue, please unlock your wallet</div>
|
||||
<div className="py-8 w-full">
|
||||
<Input
|
||||
className="mb-4"
|
||||
type="password"
|
||||
label="Password"
|
||||
value={pw}
|
||||
onChange={(e) => setPW(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="text-red-500 text-sm text-center">{error}</div>}
|
||||
<div className="flex flex-row items-center justify-center flex-shrink p-8 login__footer">
|
||||
<Button btnType={ButtonType.primary} disabled={!pw} onClick={login} loading={loading}>
|
||||
Unlock
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login;
|
||||
12
src/ui/pages/Login/login.scss
Normal file
12
src/ui/pages/Login/login.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@import '../../../util/variables';
|
||||
|
||||
.login {
|
||||
color: $primary-green;
|
||||
|
||||
&__content {
|
||||
.icon {
|
||||
width: 8rem !important;
|
||||
height: 8rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/ui/pages/Onboarding/index.tsx
Normal file
65
src/ui/pages/Onboarding/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react'
|
||||
import './onboarding.scss'
|
||||
import Button, { ButtonType } from '@src/ui/components/Button'
|
||||
import Icon from '@src/ui/components/Icon'
|
||||
import LogoSVG from '@src/static/icons/logo.svg'
|
||||
import Input from '@src/ui/components/Input'
|
||||
import postMessage from '@src/util/postMessage'
|
||||
import RPCAction from '@src/util/constants'
|
||||
|
||||
export default function Onboarding(): ReactElement {
|
||||
const [pw, setPW] = useState('')
|
||||
const [pw2, setPW2] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const valid = !!pw && pw === pw2
|
||||
|
||||
const createPassword = useCallback(async () => {
|
||||
if (!valid) {
|
||||
setError('Invalid password')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await postMessage({
|
||||
method: RPCAction.SETUP_PASSWORD,
|
||||
payload: pw
|
||||
})
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
}
|
||||
}, [pw, pw2])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap h-full onboarding">
|
||||
<div className="flex flex-col items-center flex-grow p-8 onboarding__content">
|
||||
<Icon url={LogoSVG} />
|
||||
<div className="text-lg pt-8">
|
||||
<b>Thanks for using ZKeeper!</b>
|
||||
</div>
|
||||
<div className="text-base">To continue, please setup a password</div>
|
||||
<div className="py-8 w-full">
|
||||
<Input
|
||||
className="mb-4"
|
||||
type="password"
|
||||
label="Password"
|
||||
value={pw}
|
||||
onChange={(e) => setPW(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
value={pw2}
|
||||
onChange={(e) => setPW2(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="text-red-500 text-sm text-center">{error}</div>}
|
||||
<div className="flex flex-row items-center justify-center flex-shrink p-8 onboarding__footer">
|
||||
<Button btnType={ButtonType.primary} disabled={!pw || pw !== pw2} onClick={createPassword}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
src/ui/pages/Onboarding/onboarding.scss
Normal file
12
src/ui/pages/Onboarding/onboarding.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@import '../../../util/variables';
|
||||
|
||||
.onboarding {
|
||||
color: $primary-green;
|
||||
|
||||
&__content {
|
||||
.icon {
|
||||
width: 8rem !important;
|
||||
height: 8rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/ui/pages/Popup/index.tsx
Normal file
64
src/ui/pages/Popup/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
|
||||
import './popup.scss'
|
||||
import { Redirect, Route, Switch } from 'react-router'
|
||||
import Home from '@src/ui/pages/Home'
|
||||
import { useRequestsPending, fetchRequestPendingStatus } from '@src/ui/ducks/requests'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { fetchStatus, useAppStatus } from '@src/ui/ducks/app'
|
||||
import Onboarding from '@src/ui/pages/Onboarding'
|
||||
import Login from '@src/ui/pages/Login'
|
||||
import ConfirmRequestModal from '@src/ui/components/ConfirmRequestModal'
|
||||
|
||||
export default function Popup(): ReactElement {
|
||||
const pendingRequests = useRequestsPending()
|
||||
const dispatch = useDispatch()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { initialized, unlocked } = useAppStatus()
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
try {
|
||||
await dispatch(fetchStatus())
|
||||
await dispatch(fetchRequestPendingStatus())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRequestPendingStatus())
|
||||
}, [unlocked])
|
||||
|
||||
if (loading) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
let content: ReactNode
|
||||
|
||||
if (!initialized) {
|
||||
content = <Onboarding />
|
||||
} else if (!unlocked) {
|
||||
content = <Login />
|
||||
} else if (pendingRequests.length) {
|
||||
const [pendingRequest] = pendingRequests
|
||||
return <ConfirmRequestModal />
|
||||
} else {
|
||||
content = (
|
||||
<Switch>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route>
|
||||
<Redirect to="/" />
|
||||
</Route>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="popup">{content}</div>
|
||||
}
|
||||
56
src/ui/pages/Popup/popup.scss
Normal file
56
src/ui/pages/Popup/popup.scss
Normal file
@@ -0,0 +1,56 @@
|
||||
@import './src/util/variables';
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: $bg-black;
|
||||
color: $white;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-size: 16px;
|
||||
width: 357px;
|
||||
height: 600px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $primary-blue;
|
||||
}
|
||||
|
||||
.popup {
|
||||
@extend %col-nowrap;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
|
||||
&__loading {
|
||||
@extend %col-nowrap;
|
||||
background-color: $header-gray;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 358px) {
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 36rem;
|
||||
height: 40rem;
|
||||
border: 1px solid $gray-700;
|
||||
margin: 3rem auto;
|
||||
box-shadow: 0 2px 4px 0px $header-gray;
|
||||
}
|
||||
}
|
||||
27
src/ui/popup.tsx
Normal file
27
src/ui/popup.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
import Popup from '@src/ui/pages/Popup'
|
||||
import { Provider } from 'react-redux'
|
||||
import configureAppStore from '@src/ui/store/configureAppStore'
|
||||
import { HashRouter } from 'react-router-dom'
|
||||
|
||||
const store = configureAppStore()
|
||||
|
||||
browser.runtime.onMessage.addListener((action) => {
|
||||
if (action?.type) {
|
||||
store.dispatch(action)
|
||||
}
|
||||
})
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }).then(() => {
|
||||
browser.runtime.connect()
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
<Popup />
|
||||
</HashRouter>
|
||||
</Provider>,
|
||||
document.getElementById('popup')
|
||||
)
|
||||
})
|
||||
30
src/ui/store/configureAppStore.ts
Normal file
30
src/ui/store/configureAppStore.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux'
|
||||
import { createLogger } from 'redux-logger'
|
||||
import thunk from 'redux-thunk'
|
||||
import web3 from '@src/ui/ducks/web3'
|
||||
import identities from '@src/ui/ducks/identities'
|
||||
import requests from '@src/ui/ducks/requests'
|
||||
import app from '@src/ui/ducks/app'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
web3,
|
||||
identities,
|
||||
requests,
|
||||
app
|
||||
})
|
||||
|
||||
export type AppRootState = ReturnType<typeof rootReducer>
|
||||
|
||||
export default function configureAppStore() {
|
||||
return createStore(
|
||||
rootReducer,
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? applyMiddleware(
|
||||
thunk,
|
||||
createLogger({
|
||||
collapsed: (getState, action = {}) => [''].includes(action.type)
|
||||
})
|
||||
)
|
||||
: applyMiddleware(thunk)
|
||||
)
|
||||
}
|
||||
1
src/util/account.ts
Normal file
1
src/util/account.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ellipsify = (text: string, start = 6, end = 4) => `${text.slice(0, start)}...${text.slice(-end)}`
|
||||
13
src/util/checkParameter.ts
Normal file
13
src/util/checkParameter.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export default function checkParameter(
|
||||
value: any,
|
||||
name: string,
|
||||
type: 'boolean' | 'number' | 'string' | 'object' | 'function'
|
||||
) {
|
||||
if (value === undefined) {
|
||||
throw new TypeError(`Parameter '${name}' is not defined`)
|
||||
}
|
||||
|
||||
if (typeof value !== type) {
|
||||
throw new TypeError(`Parameter '${name}' is not ${type === 'object' ? 'an' : 'a'} ${type}`)
|
||||
}
|
||||
}
|
||||
32
src/util/constants.ts
Normal file
32
src/util/constants.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
enum RPCAction {
|
||||
UNLOCK = 'rpc/unlock',
|
||||
LOCK = 'rpc/lock',
|
||||
GET_STATUS = 'rpc/getStatus',
|
||||
TRY_INJECT = 'rpc/inject',
|
||||
SETUP_PASSWORD = 'rpc/lock/setupPassword',
|
||||
CONNECT_METAMASK = 'rpc/metamask/connectMetamask',
|
||||
GET_WALLET_INFO = 'rpc/metamask/getWalletInfo',
|
||||
CREATE_IDENTITY = 'rpc/identity/createIdentity',
|
||||
CREATE_IDENTITY_REQ = 'rpc/identity/createIdentityRequest',
|
||||
SET_ACTIVE_IDENTITY = 'rpc/identity/setActiveIdentity',
|
||||
GET_ACTIVE_IDENTITY = 'rpc/identity/getActiveidentity',
|
||||
GET_COMMITMENTS = 'rpc/identity/getIdentityCommitments',
|
||||
GET_IDENTITIES = 'rpc/identity/getIdentities',
|
||||
GET_REQUEST_PENDING_STATUS = 'rpc/identity/getRequestPendingStatus',
|
||||
FINALIZE_REQUEST = 'rpc/requests/finalize',
|
||||
GET_PENDING_REQUESTS = 'rpc/requests/get',
|
||||
SEMAPHORE_PROOF = 'rpc/protocols/semaphore/genProof',
|
||||
RLN_PROOF = 'rpc/protocols/rln/genProof',
|
||||
NRLN_PROOF = 'rpc/protocols/nrln/genProof',
|
||||
DUMMY_REQUEST = 'rpc/protocols/semaphore/dummyReuqest',
|
||||
REQUEST_ADD_REMOVE_APPROVAL = 'rpc/hosts/request',
|
||||
APPROVE_HOST = 'rpc/hosts/approve',
|
||||
IS_HOST_APPROVED = 'rpc/hosts/isHostApprove',
|
||||
GET_HOST_PERMISSIONS = 'rpc/hosts/getHostPermissions',
|
||||
SET_HOST_PERMISSIONS = 'rpc/hosts/setHostPermissions',
|
||||
REMOVE_HOST = 'rpc/hosts/remove',
|
||||
CLOSE_POPUP = 'rpc/popup/close',
|
||||
// DEV RPCS
|
||||
CLEAR_APPROVED_HOSTS = 'rpc/hosts/clear'
|
||||
}
|
||||
export default RPCAction
|
||||
18
src/util/postMessage.ts
Normal file
18
src/util/postMessage.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
|
||||
export type MessageAction = {
|
||||
method: string
|
||||
payload?: any
|
||||
error?: boolean
|
||||
meta?: any
|
||||
}
|
||||
|
||||
export default async function postMessage(message: MessageAction) {
|
||||
const [err, res] = await browser.runtime.sendMessage(message)
|
||||
|
||||
if (err) {
|
||||
throw new Error(err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
16
src/util/pushMessage.ts
Normal file
16
src/util/pushMessage.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { browser } from 'webextension-polyfill-ts'
|
||||
|
||||
export type ReduxAction = {
|
||||
type: string
|
||||
payload?: any
|
||||
error?: boolean
|
||||
meta?: any
|
||||
}
|
||||
|
||||
export default async function pushMessage(message: ReduxAction) {
|
||||
if (chrome && chrome.runtime) {
|
||||
return chrome.runtime.sendMessage(message)
|
||||
}
|
||||
|
||||
return browser.runtime.sendMessage(message)
|
||||
}
|
||||
146
src/util/variables.scss
Normal file
146
src/util/variables.scss
Normal file
@@ -0,0 +1,146 @@
|
||||
$white: #ffffff;
|
||||
$black: #000000;
|
||||
|
||||
$bg-black: #000403;
|
||||
$primary-blue: #2580f8;
|
||||
$primary-green: #94febf;
|
||||
|
||||
$header-gray: rgba($black, 0.05);
|
||||
$border-gray: rgba($black, 0.1);
|
||||
$gray-950: lighten($black, 5);
|
||||
$gray-900: lighten($black, 10);
|
||||
$gray-800: lighten($black, 20);
|
||||
$gray-700: lighten($black, 30);
|
||||
$gray-600: lighten($black, 40);
|
||||
$gray-500: lighten($black, 50);
|
||||
$gray-400: lighten($black, 60);
|
||||
$gray-300: lighten($black, 70);
|
||||
$gray-200: lighten($black, 80);
|
||||
$gray-100: lighten($black, 90);
|
||||
|
||||
$primary-text: $gray-200;
|
||||
$secondary-text: $gray-400;
|
||||
$label-text: lighten($black, 75);
|
||||
|
||||
$error-red: #f52525;
|
||||
$success-green: #2ed272;
|
||||
$warning-orange: #ffa31a;
|
||||
|
||||
%flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
%col-nowrap {
|
||||
@extend %flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
%row-nowrap {
|
||||
@extend %flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
%small-font {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
|
||||
%lite-font {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
|
||||
%regular-font {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
|
||||
%h1-font {
|
||||
font-size: 2rem;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
|
||||
%h2-font {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
|
||||
%h3-font {
|
||||
font-size: 1.17rem;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
|
||||
%h5-font {
|
||||
font-size: 0.83rem;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
|
||||
%h4-font {
|
||||
font-size: 1rem;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
|
||||
%bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
%ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
%clickable {
|
||||
cursor: pointer;
|
||||
transition: 200ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
$breakpoint-tablet: 768px;
|
||||
|
||||
table {
|
||||
@extend %col-nowrap;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
@extend %bold;
|
||||
@extend %lite-font;
|
||||
flex: 0 0 auto;
|
||||
background-color: $black;
|
||||
color: rgba($white, 0.5);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
tr {
|
||||
@extend %row-nowrap;
|
||||
|
||||
&:nth-of-type(odd) {
|
||||
background-color: rgba($black, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
align-items: flex-start;
|
||||
flex: 1 1 auto;
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
tbody {
|
||||
flex: 1 1 auto;
|
||||
td {
|
||||
@extend %lite-font;
|
||||
}
|
||||
}
|
||||
|
||||
small {
|
||||
color: $secondary-text;
|
||||
}
|
||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@src/*": ["src/*"]
|
||||
},
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"rootDir": "./src",
|
||||
"outDir": "dist/js",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2020", "dom"],
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
85
webpack.common.js
Normal file
85
webpack.common.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const webpack = require('webpack')
|
||||
const path = require('path')
|
||||
const CopyPlugin = require('copy-webpack-plugin')
|
||||
|
||||
const envPlugin = new webpack.EnvironmentPlugin(['NODE_ENV'])
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
injected: path.join(__dirname, 'src/contentscripts/injected.ts'),
|
||||
content: path.join(__dirname, 'src/contentscripts/index.ts'),
|
||||
backgroundPage: path.join(__dirname, 'src/background/backgroundPage.ts'),
|
||||
popup: path.join(__dirname, 'src/ui/popup.tsx')
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist/js'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
plugins: [
|
||||
envPlugin,
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer']
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser'
|
||||
})
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader'
|
||||
},
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader' // Creates style nodes from JS strings
|
||||
},
|
||||
{
|
||||
loader: 'css-loader' // Translates CSS into CommonJS
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader' // Compiles Sass to CSS
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(gif|png|jpe?g|svg)$/i,
|
||||
use: [
|
||||
'file-loader',
|
||||
{
|
||||
loader: 'image-webpack-loader',
|
||||
options: {
|
||||
publicPath: 'assets',
|
||||
bypassOnDebug: true, // webpack@1.x
|
||||
disable: true // webpack@2.x and newer
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.png', '.svg'],
|
||||
alias: {
|
||||
'@src': path.resolve(__dirname, 'src/'),
|
||||
buffer: 'buffer'
|
||||
},
|
||||
fallback: {
|
||||
browserify: require.resolve('browserify'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
os: require.resolve('os-browserify/browser'),
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
fs: false
|
||||
}
|
||||
},
|
||||
externals: /^(worker_threads)$/
|
||||
}
|
||||
9
webpack.dev.js
Normal file
9
webpack.dev.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable import/extensions */
|
||||
const merge = require('webpack-merge')
|
||||
const common = require('./webpack.common.js')
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
devtool: 'inline-source-map'
|
||||
})
|
||||
8
webpack.prod.js
Normal file
8
webpack.prod.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/* eslint-disable import/extensions */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const merge = require('webpack-merge')
|
||||
const common = require('./webpack.common.js')
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'production'
|
||||
})
|
||||
Reference in New Issue
Block a user