├── .dockerignore ├── .prettierignore ├── src ├── react-app-env.d.ts ├── style │ ├── utils.ts │ ├── theme.ts │ └── global.ts ├── components │ ├── Error.tsx │ ├── TextInput.tsx │ ├── ExternalLink.tsx │ ├── SearchInput.tsx │ ├── helper │ │ └── RequireAuth.tsx │ ├── ProgressBar.tsx │ ├── Tooltip.tsx │ ├── QueueProgressBar.tsx │ ├── WalletConnectConfig.tsx │ ├── LoadingSpinner.tsx │ ├── headers │ │ ├── HeaderJustGoingBack.tsx │ │ └── Header.tsx │ ├── FlowerAnimation.tsx │ ├── Logo.tsx │ ├── Image.ts │ ├── Text.ts │ ├── landing │ │ ├── FaqItem.tsx │ │ ├── OtherResources.tsx │ │ ├── Explanation.tsx │ │ └── LatestRecords.tsx │ ├── Blockies.tsx │ ├── Layout.ts │ ├── Footer.tsx │ ├── Pagination.tsx │ ├── Button.tsx │ ├── LanguageSelector.tsx │ ├── Background.tsx │ ├── modals │ │ ├── ShareSocialModal.tsx │ │ └── TranscriptModal.tsx │ └── RecordTable.tsx ├── i18n.ts ├── hooks │ ├── useSequencerStatus.ts │ ├── useTryContribute.ts │ ├── usePreloadAllImages.ts │ ├── useRecord.ts │ ├── useWindowDimensions.ts │ ├── useCountdown.ts │ └── useBackgroundVisibility.ts ├── setupTests.ts ├── assets │ ├── star.svg │ ├── search.svg │ ├── eth-logo-purple.svg │ ├── images.ts │ ├── github.svg │ ├── translator.svg │ ├── left-arrow.svg │ ├── open-flower.svg │ ├── explanation-border.svg │ ├── logo.svg │ ├── eth.svg │ ├── closed-hug-flower.svg │ └── open-hug-flower.svg ├── routes.ts ├── store │ ├── language.ts │ ├── auth.ts │ └── contribute.ts ├── index.tsx ├── pages │ ├── index.ts │ ├── home.tsx │ ├── lobbyFull.tsx │ ├── signinRedirect.tsx │ ├── signin.tsx │ ├── complete.tsx │ ├── lobby.tsx │ ├── doubleSign.tsx │ ├── landing.tsx │ ├── contributing.tsx │ └── entropyInput.tsx ├── locales │ └── index.ts ├── types.ts ├── wasm.ts ├── constants.ts ├── api.ts ├── utils.ts └── App.tsx ├── public ├── robots.txt ├── favicon.ico ├── og-image.png ├── wasm │ ├── pkg │ │ ├── wrapper_small_pot_bg.wasm │ │ ├── wrapper_small_pot_bg.wasm.d.ts │ │ ├── snippets │ │ │ └── wasm-bindgen-rayon-7afa899f36665473 │ │ │ │ └── src │ │ │ │ └── workerHelpers.no-bundler.js │ │ └── wrapper_small_pot.d.ts │ └── wasm-worker.js ├── manifest.json ├── sign.html ├── sw.js ├── index.html └── module-workers-polyfill.min.js ├── .prettierrc ├── .babelrc ├── .env.prod ├── customHttp.yml ├── types ├── styled-components.d.ts └── blockies-identicon.d.ts ├── craco.config.js ├── .gitignore ├── tsconfig.json ├── docker-build.sh ├── validate_receipt ├── validate.js ├── data.json └── README.md ├── compose.yaml ├── package.json └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | .env 3 | build/** -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | package-lock.json -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zkparty/trusted-setup-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zkparty/trusted-setup-frontend/HEAD/public/og-image.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80 6 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["i18next-extract"], 3 | "presets": ["@babel/preset-react", "@babel/preset-typescript"] 4 | } 5 | -------------------------------------------------------------------------------- /public/wasm/pkg/wrapper_small_pot_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zkparty/trusted-setup-frontend/HEAD/public/wasm/pkg/wrapper_small_pot_bg.wasm -------------------------------------------------------------------------------- /src/style/utils.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components' 2 | 3 | const textSerif = css` 4 | font-family: 'Taviraj', serif; 5 | ` 6 | 7 | export { textSerif } 8 | -------------------------------------------------------------------------------- /src/components/Error.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const ErrorMessage = styled.p` 4 | color: ${({ theme }) => theme.error}; 5 | ` 6 | 7 | export default ErrorMessage 8 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | REACT_APP_API_ROOT=https://seq.ceremony.ethereum.org 2 | REACT_APP_ENVIRONMENT=production 3 | REACT_APP_END_DATE=1692850103 4 | REACT_APP_TRANSCRIPT_HASH=0x8ed1c73857e77ae98ea23e36cdcf828ccbf32b423fddc7480de658f9d116c848 5 | -------------------------------------------------------------------------------- /customHttp.yml: -------------------------------------------------------------------------------- 1 | customHeaders: 2 | # crossOriginIsolated for WASM 3 | - pattern: '**' 4 | headers: 5 | - key: 'Cross-Origin-Embedder-Policy' 6 | value: 'require-corp' 7 | - key: 'Cross-Origin-Opener-Policy' 8 | value: 'same-origin' -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | 4 | import { locales } from './locales' 5 | 6 | i18next.use(initReactI18next).init({ 7 | fallbackLng: 'en', 8 | resources: locales 9 | }) 10 | -------------------------------------------------------------------------------- /src/hooks/useSequencerStatus.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import api from '../api' 3 | 4 | export default function useSequencerStatus() { 5 | return useQuery(['status'], async () => { 6 | return api.getStatus() 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /src/assets/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import 'styled-components' 2 | import type { Theme } from '../src/style/theme' 3 | 4 | declare module 'styled-components' { 5 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 6 | export interface DefaultTheme extends Theme {} 7 | } 8 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 2 | module.exports = { 3 | webpack: { 4 | plugins: { 5 | add: [ 6 | new NodePolyfillPlugin({ 7 | excludeAliases: ['console'], 8 | }) 9 | ], 10 | }, 11 | configure: { 12 | ignoreWarnings: [/Failed to parse source map/], 13 | } 14 | }, 15 | }; -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "KZG Ceremony", 3 | "name": "KZG Trusted Setup Ceremony", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /types/blockies-identicon.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'blockies-identicon' { 2 | const api = { 3 | render: function ( 4 | opts: { 5 | seed?: string 6 | color?: string 7 | bgcolor?: string 8 | size?: number 9 | scale?: number 10 | spotcolor?: string 11 | }, 12 | ref: HTMLCanvasElement 13 | ): void {} 14 | } 15 | 16 | export default api 17 | } 18 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | const ROUTES = { 2 | ROOT: '/', 3 | SIGNIN: '/signin', 4 | ENTROPY_INPUT: '/entropy_input', 5 | DOUBLE_SIGN: '/double_sign', 6 | LOBBY_FULL: '/lobby_full', 7 | LOBBY: '/lobby', 8 | CONTRIBUTING: '/contributing', 9 | COMPLETE: '/complete', 10 | RECORD: '/record', 11 | } 12 | 13 | export const MOBILE_FRIENDLY_ROUTES = [ 14 | ROUTES.ROOT, 15 | ROUTES.RECORD 16 | ] 17 | 18 | export default ROUTES 19 | -------------------------------------------------------------------------------- /src/style/theme.ts: -------------------------------------------------------------------------------- 1 | export type Theme = { 2 | background: string 3 | surface: string 4 | surface2: string 5 | text: string 6 | primary: string 7 | loader: string 8 | disabled: string 9 | error: string 10 | } 11 | 12 | const theme: Theme = { 13 | background: '#F0F0F0', 14 | surface: '#FFF8E7', 15 | surface2: '#FCFCFC', 16 | text: '#000000', 17 | primary: '#DAECFF', 18 | loader: '#3A4682', 19 | disabled: '#9EA3A7', 20 | error: '#E36049' 21 | } 22 | 23 | export default theme 24 | -------------------------------------------------------------------------------- /src/assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /ipfs 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .env 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | *initialContribution.json 28 | 29 | # Intl translation extraction 30 | /extractedTranslations -------------------------------------------------------------------------------- /src/assets/eth-logo-purple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { BREAKPOINT, FONT_SIZE } from '../constants' 3 | 4 | const TextInput = styled.input` 5 | font-size: ${FONT_SIZE.M}; 6 | font-weight: 400; 7 | padding: 8px 16px 8px 16px; 8 | border: solid 1px ${({ theme }) => theme.text}; 9 | border-radius: 8px; 10 | background-color: ${({ theme }) => theme.primary}; 11 | color: ${({ theme }) => theme.text}; 12 | width: 95%; 13 | 14 | @media (max-width: ${BREAKPOINT.M}) { 15 | width: 100%; 16 | } 17 | ` 18 | 19 | export default TextInput 20 | -------------------------------------------------------------------------------- /src/store/language.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { persist, createJSONStorage } from 'zustand/middleware' 3 | 4 | export type Store = { 5 | selectedLanguage: string | null 6 | updateSelectedLanguage: (data: string | null) => void 7 | } 8 | export const useLanguageStore = create()( 9 | persist( 10 | (set) => ({ 11 | selectedLanguage: null, 12 | updateSelectedLanguage: (data: string | null) => set({ selectedLanguage: data }), 13 | }), 14 | { 15 | name: 'kzg-language', 16 | storage: createJSONStorage(() => sessionStorage), 17 | }) 18 | ) 19 | -------------------------------------------------------------------------------- /src/hooks/useTryContribute.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query' 2 | import api from '../api' 3 | import { useAuthStore } from '../store/auth' 4 | 5 | /// when user is in the lobby, 6 | /// user needs to periodically post try_contribute request to sequencer. 7 | /// if successfully took the spot, now the user is ready to contribute. 8 | export default function useTryContribute() { 9 | const { sessionId } = useAuthStore() 10 | 11 | return useMutation(async () => { 12 | if (!sessionId) throw new Error('Session id does not exist') 13 | 14 | return await api.tryContribute(sessionId) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /public/sign.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | KZG Summoning Ceremony 10 | 11 | 12 | 13 | 14 |
Signing ...
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | type ExternalLinkProps = { 4 | href: string 5 | children: React.ReactNode 6 | style?: React.CSSProperties 7 | } 8 | 9 | const ExternalLink = ({ 10 | href, 11 | children, 12 | style, 13 | ...props 14 | }: ExternalLinkProps) => ( 15 | 22 | {children} 23 | 24 | ) 25 | 26 | const ArrowLink = styled.a` 27 | ::after { 28 | content: '↗'; 29 | font-size: 0.875em; 30 | } 31 | ` 32 | 33 | export default ExternalLink 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "preserve" 22 | }, 23 | "include": [ 24 | "src", 25 | "types" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 4 | import { ThemeProvider } from 'styled-components' 5 | import { setAppElement } from 'react-modal' 6 | import App from './App' 7 | import theme from './style/theme' 8 | import './i18n' 9 | 10 | setAppElement('#root') 11 | 12 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) 13 | const client = new QueryClient() 14 | 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | -------------------------------------------------------------------------------- /src/components/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { BREAKPOINT, FONT_SIZE } from '../constants' 3 | import SearchIcon from '../assets/search.svg' 4 | 5 | const SearchInput = styled.input` 6 | font-size: ${FONT_SIZE.M}; 7 | font-weight: 400; 8 | padding: 8px 40px 8px 16px; 9 | border: solid 1px ${({ theme }) => theme.text}; 10 | border-radius: 8px; 11 | background-color: ${({ theme }) => theme.primary}; 12 | color: ${({ theme }) => theme.text}; 13 | background: url(${SearchIcon}) no-repeat scroll right 12px bottom 50%; 14 | width: 70%; 15 | 16 | @media (max-width: ${BREAKPOINT.M}) { 17 | width: 100%; 18 | } 19 | ` 20 | 21 | export default SearchInput -------------------------------------------------------------------------------- /src/hooks/usePreloadAllImages.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import images from '../assets/images' 3 | 4 | export default function usePreloadAllImages() { 5 | const [loaded, setLoaded] = useState(false) 6 | useEffect(() => { 7 | // load all images 8 | ;(async () => { 9 | await Promise.all( 10 | images.map((image) => { 11 | return new Promise((resolve) => { 12 | const i = new Image() 13 | i.src = image 14 | i.onload = () => { 15 | resolve() 16 | } 17 | }) 18 | }) 19 | ) 20 | 21 | setLoaded(true) 22 | })() 23 | }, []) 24 | 25 | return loaded 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import HomePage from './home' 2 | import LandingPage from './landing' 3 | import SigninPage from './signin' 4 | import EntropyInputPage from './entropyInput' 5 | import LobbyFullPage from './lobbyFull' 6 | import LobbyPage from './lobby' 7 | import ContributingPage from './contributing' 8 | import CompletePage from './complete' 9 | import RecordPage from './record' 10 | import DoubleSignPage from './doubleSign' 11 | import SigninRedirectPage from './signinRedirect' 12 | 13 | export { 14 | HomePage, 15 | LandingPage, 16 | SigninPage, 17 | EntropyInputPage, 18 | LobbyFullPage, 19 | LobbyPage, 20 | ContributingPage, 21 | CompletePage, 22 | DoubleSignPage, 23 | RecordPage, 24 | SigninRedirectPage 25 | } 26 | -------------------------------------------------------------------------------- /src/components/helper/RequireAuth.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router-dom' 2 | import { shallow } from 'zustand/shallow' 3 | import { useAuthStore } from '../../store/auth' 4 | import ROUTES from '../../routes' 5 | 6 | /** 7 | * check signin status and navigate to 8 | * appropriate route 9 | * accept same props type with Route element of react-router-dom 10 | */ 11 | const RequireAuth = ({ children }: { children: JSX.Element }) => { 12 | const { authenticated } = useAuthStore( 13 | (state) => ({ 14 | authenticated: !!state.provider 15 | }), 16 | shallow 17 | ) 18 | 19 | if (!authenticated) { 20 | return 21 | } 22 | 23 | return children 24 | } 25 | 26 | export default RequireAuth 27 | -------------------------------------------------------------------------------- /src/assets/images.ts: -------------------------------------------------------------------------------- 1 | import BgImg from './img-graphic-base-xl.svg' 2 | import InnerWhite from './inner-white.svg' 3 | import SnakeWhite from './snake-white.svg' 4 | import OuterWhite from './outer-white.svg' 5 | import PizzaInner from './crust.svg' 6 | import PizzaOuter from './fig.svg' 7 | import BgImgColor from './img-base-color-xl.svg' 8 | import InnerColor from './inner-color.svg' 9 | import SnakeColor from './snake-color.svg' 10 | import OuterColor from './outer-color.svg' 11 | import SnakeProgress from './snake-progressbar.svg' 12 | 13 | const images = [ 14 | BgImg, 15 | InnerWhite, 16 | SnakeWhite, 17 | OuterWhite, 18 | PizzaInner, 19 | PizzaOuter, 20 | BgImgColor, 21 | InnerColor, 22 | SnakeColor, 23 | OuterColor, 24 | SnakeProgress 25 | ] 26 | 27 | export default images 28 | -------------------------------------------------------------------------------- /src/components/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { RADIUS } from '../constants' 3 | 4 | type Props = { 5 | percentage: number 6 | } 7 | 8 | const ProgressBar = ({ percentage }: Props) => { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | const Background = styled.div` 17 | border-radius: ${RADIUS.M}; 18 | height: 24px; 19 | width: 320px; 20 | background-color: ${({ theme }) => theme.text}; 21 | ` 22 | 23 | const Progress = styled.div<{ percentage: number }>` 24 | border-radius: ${RADIUS.M}; 25 | height: 24px; 26 | width: ${({ percentage }) => `${320 * (percentage / 100)}px`}; 27 | background-color: ${({ theme }) => theme.surface}; 28 | transition: width ease 0.1s; 29 | ` 30 | 31 | export default ProgressBar 32 | -------------------------------------------------------------------------------- /src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { Outlet, useLocation, useNavigate } from 'react-router-dom' 3 | import Background from '../components/Background' 4 | import ROUTES, { MOBILE_FRIENDLY_ROUTES } from '../routes' 5 | import { isMobile } from '../utils' 6 | 7 | const HomePage = () => { 8 | const location = useLocation() 9 | const navigate = useNavigate() 10 | useEffect(() => { 11 | // check device UA on initial render 12 | const mobile = isMobile() 13 | // If mobile and location is not mobile friendly, redirect to root route 14 | if (mobile && !MOBILE_FRIENDLY_ROUTES.includes(location.pathname)) { 15 | navigate(ROUTES.ROOT) 16 | } 17 | }, [location.pathname, navigate]) 18 | return ( 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default HomePage 26 | -------------------------------------------------------------------------------- /src/style/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | import { FONT_SIZE } from '../constants' 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | body { 6 | font-family: 'Inter', sans-serif; 7 | font-size: ${FONT_SIZE.M}; 8 | color: ${({ theme }) => theme.text}; 9 | margin: 0; 10 | min-width: 100vw; 11 | overflow-x: hidden; 12 | background-color: ${({ theme }) => theme.background} 13 | } 14 | input { 15 | appearance: textfield; 16 | } 17 | input::-webkit-outer-spin-button, 18 | input::-webkit-inner-spin-button { 19 | -webkit-appearance: none; 20 | margin: 0; 21 | } 22 | input:invalid { 23 | box-shadow: none; 24 | } 25 | input:focus { 26 | outline: none; 27 | } 28 | a { 29 | color: inherit; 30 | text-decoration: none; 31 | } 32 | * { 33 | box-sizing: border-box; 34 | } 35 | ` 36 | 37 | export default GlobalStyle 38 | -------------------------------------------------------------------------------- /src/hooks/useRecord.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import api from '../api' 3 | 4 | export default function useRecord() { 5 | return useQuery( 6 | ['record'], 7 | async () => { 8 | return api.getCurrentState() 9 | }, 10 | { 11 | cacheTime: Infinity, 12 | networkMode: 'offlineFirst', 13 | staleTime: Infinity, 14 | refetchOnReconnect: false, 15 | refetchOnWindowFocus: false 16 | } 17 | ) 18 | } 19 | 20 | export function useRecordAsString() { 21 | return useQuery( 22 | ['recordAsString'], 23 | async () => { 24 | return api.getCurrentStateAsString() 25 | }, 26 | { 27 | cacheTime: Infinity, 28 | networkMode: 'offlineFirst', 29 | staleTime: Infinity, 30 | refetchOnReconnect: false, 31 | refetchOnWindowFocus: false 32 | } 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Build WASM 4 | docker run -it --rm \ 5 | -v "${PWD}/public/wasm/:/root/source/wasm/" \ 6 | --entrypoint ./build.sh \ 7 | zkparty/wasm-pack-wrapper 8 | 9 | # Build frontend 10 | docker run -it --rm \ 11 | -v "${PWD}:/work" \ 12 | -w /work \ 13 | -e REACT_APP_API_ROOT="${REACT_APP_API_ROOT}" \ 14 | -e REACT_APP_ENVIRONMENT="${REACT_APP_ENVIRONMENT}" \ 15 | -e REACT_APP_END_DATE="${REACT_APP_END_DATE}" \ 16 | -e REACT_APP_TRANSCRIPT_HASH="${REACT_APP_TRANSCRIPT_HASH}" \ 17 | --entrypoint ./build.sh \ 18 | node:19-bullseye 19 | 20 | # Start IPFS node 21 | docker run -d --rm \ 22 | --name ipfs-host \ 23 | -v "${PWD}/build:/export" \ 24 | -v "${PWD}/ipfs:/data/ipfs" \ 25 | -e IPFS_PROFILE=server \ 26 | -p 4001:4001 -p 4001:4001/udp \ 27 | -p 127.0.0.1:8080:8080 -p 127.0.0.1:5001:5001 \ 28 | ipfs/kubo:latest 29 | -------------------------------------------------------------------------------- /src/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Tippy from '@tippyjs/react' 4 | 5 | type ToolTipProps = { 6 | children: any 7 | explanation: string 8 | } 9 | 10 | const ToolTip = ({ children, explanation }: ToolTipProps) => { 11 | return ( 12 | { explanation }} 19 | > 20 | {children} 21 | 22 | ) 23 | } 24 | 25 | const ToolTipContent = styled.div` 26 | word-break: break-word; 27 | background-color: black; 28 | border: solid black 1px; 29 | border-radius: 5px; 30 | padding: 12px; 31 | color: white; 32 | width: 40ch; 33 | ` 34 | 35 | export default ToolTip -------------------------------------------------------------------------------- /src/components/QueueProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { FONT_SIZE } from '../constants' 3 | 4 | type Props = { 5 | total: number 6 | current: number 7 | } 8 | 9 | const QueueProgressBar = ({ total, current }: Props) => { 10 | return ( 11 | 12 | You are in the queue. 13 | 14 | Kindly stay online while the magic math is being processed. 15 | 16 | 17 | ) 18 | } 19 | 20 | const Container = styled.div` 21 | background-color: ${({ theme }) => theme.surface}; 22 | width: 400px; 23 | padding: 16px 24px; 24 | border-radius: 4px; 25 | ` 26 | 27 | const QueueTitle = styled.p` 28 | margin: 0 0 12px; 29 | font-weight: 600; 30 | font-size: ${FONT_SIZE.L}; 31 | ` 32 | 33 | const QueueDesc = styled.p` 34 | margin: 0; 35 | font-size: ${FONT_SIZE.XS}; 36 | ` 37 | 38 | export default QueueProgressBar 39 | -------------------------------------------------------------------------------- /src/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import ar from './ar/translation.json' 2 | import bn from './bn/translation.json' 3 | import de from './de/translation.json' 4 | import en from './en/translation.json' 5 | import es from './es/translation.json' 6 | import sim_en from './sim_en/translation.json' 7 | import fr from './fr/translation.json' 8 | import hi from './hi/translation.json' 9 | import id from './id/translation.json' 10 | import it from './it/translation.json' 11 | import ja from './ja/translation.json' 12 | import ko from './ko/translation.json' 13 | import pt from './pt/translation.json' 14 | import ru from './ru/translation.json' 15 | import sw from './sw/translation.json' 16 | import tr from './tr/translation.json' 17 | import zh from './zh/translation.json' 18 | 19 | 20 | export const locales = { 21 | ar, 22 | bn, 23 | de, 24 | en, 25 | sim_en, 26 | es, 27 | fr, 28 | hi, 29 | id, 30 | it, 31 | ja, 32 | ko, 33 | pt, 34 | ru, 35 | sw, 36 | tr, 37 | zh 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/useWindowDimensions.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/36862334/get-viewport-window-height-in-reactjs 2 | import { useState, useEffect } from 'react' 3 | 4 | 5 | type WindowDimensions = { 6 | width: number 7 | height: number 8 | } 9 | 10 | const getWindowDimensions = (): WindowDimensions => { 11 | const { innerWidth, innerHeight } = window 12 | 13 | return { 14 | width: innerWidth, 15 | height: innerHeight 16 | } 17 | } 18 | 19 | const useWindowDimensions = (): WindowDimensions => { 20 | const [windowDimensions, setWindowDimensions] = useState({width: 0, height: 0}) 21 | 22 | const handleResize = () => { 23 | setWindowDimensions(getWindowDimensions()) 24 | } 25 | 26 | useEffect(() => { 27 | handleResize() 28 | window.addEventListener('resize', handleResize) 29 | return () => window.removeEventListener('resize', handleResize) 30 | }, []) 31 | 32 | return windowDimensions 33 | } 34 | 35 | export default useWindowDimensions -------------------------------------------------------------------------------- /validate_receipt/validate.js: -------------------------------------------------------------------------------- 1 | const ethers = require('ethers'); 2 | const data = require('./data.json'); 3 | 4 | async function main(){ 5 | 6 | let { signature, receipt } = data; 7 | 8 | // 1. check that signature address is equal to sequencer address 9 | const hash = ethers.utils.hashMessage(receipt); 10 | const signer = ethers.utils.recoverAddress(hash, '0x' + signature); 11 | // get sequencer address from https://ceremony.ethereum.org/#/record 12 | const sequencer = '0x1C5c1289620f4f02D7cC29dda63C71106c04a7EC'; 13 | console.log('Receipt signature check: ' + (signer === sequencer) + '\n'); 14 | console.log(signer); 15 | console.log(sequencer); 16 | console.log('\n'); 17 | 18 | // 2. check that the witnesses are equal to your PoT pubkeys 19 | console.log('Receipt witnesses: \n') 20 | const { witness } = JSON.parse(receipt); 21 | for (let i = 0, ni=witness.length; i < ni; i++) { 22 | const w = witness[i]; 23 | console.log('Witness ' + i + ': ' + w); 24 | } 25 | } 26 | 27 | main(); -------------------------------------------------------------------------------- /src/components/WalletConnectConfig.tsx: -------------------------------------------------------------------------------- 1 | import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum' 2 | import { Web3Modal } from '@web3modal/react' 3 | import { configureChains, createConfig, WagmiConfig } from 'wagmi' 4 | import { mainnet } from 'wagmi/chains' 5 | import { WALLET_CONNECT_PROJECT_ID } from '../constants' 6 | 7 | const chains = [mainnet] 8 | const projectId = WALLET_CONNECT_PROJECT_ID 9 | 10 | const { publicClient } = configureChains(chains, [w3mProvider({ projectId })]) 11 | const wagmiConfig = createConfig({ 12 | autoConnect: true, 13 | connectors: w3mConnectors({ projectId, chains }), 14 | publicClient 15 | }) 16 | const ethereumClient = new EthereumClient(wagmiConfig, chains) 17 | 18 | const WalletConnectConfig = ({ children }: { children: JSX.Element }) => { 19 | return ( 20 | <> 21 | {children} 22 | 23 | 24 | ) 25 | } 26 | 27 | export default WalletConnectConfig 28 | -------------------------------------------------------------------------------- /validate_receipt/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "receipt": "{\"identity\":\"eth|0xe4721a80c6e56f4ebeed6acee91b3ee715e7dd64\",\"witness\":[\"0xb0090f8efb71d921f4f03e5f25eed1bf20b4fb5d717b4f619b2f51d532858737b8100f83aea5c859de980efd86a826cd0c12cb6fe6b6ea5aebf05f1f9dfba17cad911057ef2d4b06f45ed58106faf22f02b60eef912f4e00486383dd363799e5\",\"0x9075546c0dc01e4ae95fd4ed1f6e920af9b0e46d119916c244e72a66d015e147a92acdb7f8bb9fd87e7c3ab6bbc62268034cdd1f0be57e5a9159642caaf2c454cfaa73a28d0ec280868c623df68a350e7a783ed4d26d18210109365673fad6a9\",\"0xb480bb7909671808cf374d21990f14c226fc7193c6d30b3e6dec33c1b801e9c3929da1a30633f8b4527a109009485f30165825e7d5b6638b1126b3001cc3db888bc575a549db02b896984907b65e18dddb7e1d44f45f4b33fba67fb5fbc1b578\",\"0x966c50aacbe81edabd6fa6761af208db4669a83ca564cba659bc770b689f02f19b12c478bba01bc7ffea7286040c69440f7883f5e2c1ddba35ff321c9c39578d3bcdd6b9c61d2ec5755902a57e15cc81be78ca27ba2d0fe6ede3716cdab55f2a\"]}", 3 | "signature": "ca3bae06720081af824bd12b53f1dcdaa6ee1f2cea8b9472a950cbd36024f2f84f797430e5afc91d3c41ac3692ed51ff7617d5aa621c216bba540f3fef08489b1c" 4 | } 5 | -------------------------------------------------------------------------------- /src/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | import styled, { CSSProperties, keyframes } from "styled-components"; 2 | 3 | type Props = { 4 | style?: CSSProperties 5 | } 6 | 7 | const LoadingSpinner = ({style = {}}: Props) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | const Spin = keyframes` 16 | 0% { transform: rotate(0deg); } 17 | 100% { transform: rotate(360deg); } 18 | `; 19 | const SpinnerLoading = styled.div` 20 | width: 50px; 21 | height: 50px; 22 | margin: 8px; 23 | border-radius: 50%; 24 | border: 6px solid ${({ theme }) => theme.loader}; 25 | border-color: ${({ theme }) => theme.loader} transparent ${({ theme }) => theme.loader} transparent; 26 | 27 | animation: ${Spin} 1.5s linear infinite; 28 | `; 29 | const SpinnerContainer = styled.div` 30 | display: grid; 31 | justify-content: center; 32 | align-items: center; 33 | height: 80px; 34 | `; 35 | 36 | export default LoadingSpinner; -------------------------------------------------------------------------------- /validate_receipt/README.md: -------------------------------------------------------------------------------- 1 | # Verify and validate the sequencer receipt 2 | 3 | After computing your contribution, the sequencer would send a receipt with important and verifiable information. The frontend web application would perform checks automatically but any participant would be able to download the receipt and run their own checks. The receipt looks like: 4 | 5 | ``` 6 | { 7 | "receipt": { 8 | "identity": "eth|0x123...", 9 | "witness": [ 10 | "0x123...", 11 | "0x456...", 12 | "0x789...", 13 | "0x012..." 14 | ] 15 | }, 16 | "signature": "ca3..." 17 | } 18 | 19 | ``` 20 | 21 | ## Checks 22 | 23 | 1. **Receipt signature address is equal to sequencer address:** to prove that the receipt was sent by the official sequencer, a participant can recover the public address from the signature and compare it with the official public address. 24 | 25 | 2. **Check that witnesses are equal to PoT Pubkeys:** the receipt sends back the PoT Pubkeys as "witnesses". A participant can check that each witness is equal to one PoT Pubkey. 26 | 27 | 28 | # More info 29 | If you are interested on learning more about the KZG Ceremony checkout this repository [https://github.com/ethereum/kzg-ceremony](). -------------------------------------------------------------------------------- /src/assets/translator.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/headers/HeaderJustGoingBack.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { textSerif } from '../../style/utils' 3 | import { FONT_SIZE, ENVIRONMENT } from '../../constants' 4 | import { Trans, useTranslation } from 'react-i18next' 5 | import LanguageSelector from '../LanguageSelector' 6 | 7 | const HeaderJustGoingBack = () => { 8 | useTranslation() 9 | 10 | return (<>{ ENVIRONMENT === 'testnet' ? 11 | 12 | { ENVIRONMENT === 'testnet' ? 13 | TEST CEREMONY 14 | : 15 | <> 16 | } 17 | 18 | : 19 | // This keeps the selected language between reloads 20 | 21 | } 22 | ) 23 | } 24 | 25 | const Container = styled.header` 26 | z-index: 3; 27 | position: absolute; 28 | top: 0; 29 | width: 100vw; 30 | background-color: ${({ theme }) => theme.surface2}; 31 | height: 40px; 32 | display: flex; 33 | align-items: center; 34 | padding: 0 24px; 35 | color: #3e70bc; 36 | ${textSerif} 37 | font-weight: 800; 38 | letter-spacing: 2px; 39 | justify-content: center; 40 | font-size: ${FONT_SIZE.XXL}; 41 | ` 42 | 43 | export default HeaderJustGoingBack 44 | -------------------------------------------------------------------------------- /src/hooks/useCountdown.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | 4 | const getReturnValues = (countDown: number) => { 5 | // calculate time left 6 | const days = String( 7 | Math.floor(countDown / (1000 * 60 * 60 * 24)) 8 | ).padStart(2,'0') 9 | const hours = String( 10 | Math.floor((countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) 11 | ).padStart(2, '0') 12 | const minutes = String( 13 | Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60)) 14 | ).padStart(2, '0') 15 | const seconds = String( 16 | Math.floor((countDown % (1000 * 60)) / 1000) 17 | ).padStart(2, '0') 18 | 19 | return [days, hours, minutes, seconds]; 20 | } 21 | 22 | export default function useCountdown(targetDate: number) { 23 | const countDownDate = new Date(targetDate * 1000).getTime() 24 | 25 | const [countDown, setCountDown] = useState( 26 | countDownDate - new Date().getTime() 27 | ) 28 | 29 | useEffect(() => { 30 | const interval = setInterval(() => { 31 | setCountDown(countDownDate - new Date().getTime()) 32 | }, 1000) 33 | 34 | return () => clearInterval(interval) 35 | }, [countDownDate]) 36 | 37 | return getReturnValues(countDown); 38 | } 39 | -------------------------------------------------------------------------------- /public/wasm/pkg/wrapper_small_pot_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export function init_threads(a: number): number; 4 | export function contribute_wasm(a: number, b: number, c: number, d: number, e: number, f: number): number; 5 | export function subgroup_check_wasm(a: number, b: number): number; 6 | export function get_pot_pubkeys_wasm(a: number, b: number): number; 7 | export function verify_wasm(a: number, b: number): number; 8 | export function __wbg_wbg_rayon_poolbuilder_free(a: number): void; 9 | export function wbg_rayon_poolbuilder_mainJS(a: number): number; 10 | export function wbg_rayon_poolbuilder_numThreads(a: number): number; 11 | export function wbg_rayon_poolbuilder_receiver(a: number): number; 12 | export function wbg_rayon_poolbuilder_build(a: number): void; 13 | export function initThreadPool(a: number): number; 14 | export function wbg_rayon_start_worker(a: number): void; 15 | export const memory: WebAssembly.Memory; 16 | export function __wbindgen_malloc(a: number): number; 17 | export function __wbindgen_realloc(a: number, b: number, c: number): number; 18 | export function __wbindgen_free(a: number, b: number): void; 19 | export function __wbindgen_exn_store(a: number): void; 20 | export function __wbindgen_thread_destroy(): void; 21 | export function __wbindgen_start(): void; 22 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | name: wasm-pack-pot-wrapper 2 | services: 3 | wasm: 4 | image: glamperd/wasm-pack-wrapper 5 | command: wasm-pack build --target web -d wasm/pkg 6 | working_dir: /root 7 | volumes: 8 | - ./public/wasm/:/root/wasm/ 9 | frontend: 10 | build: 11 | context: frontend 12 | target: development 13 | networks: 14 | - client-side 15 | ports: 16 | - 3000:3000 17 | volumes: 18 | - ./:/work 19 | 20 | ipfs: 21 | build: 22 | context: backend 23 | target: development 24 | environment: 25 | - ADDRESS=0.0.0.0:8000 26 | - RUST_LOG=debug 27 | - PG_DBNAME=postgres 28 | - PG_HOST=db 29 | - PG_USER=postgres 30 | - PG_PASSWORD=mysecretpassword 31 | networks: 32 | - client-side 33 | - server-side 34 | volumes: 35 | - ./backend/src:/code/src 36 | - backend-cache:/code/target 37 | depends_on: 38 | - db 39 | 40 | db: 41 | image: postgres:12-alpine 42 | restart: always 43 | environment: 44 | - POSTGRES_PASSWORD=mysecretpassword 45 | networks: 46 | - server-side 47 | ports: 48 | - 5432:5432 49 | volumes: 50 | - db-data:/var/lib/postgresql/data 51 | 52 | networks: 53 | client-side: {} 54 | server-side: {} 55 | 56 | volumes: 57 | backend-cache: {} 58 | db-data: {} -------------------------------------------------------------------------------- /src/assets/left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/store/auth.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { persist, createJSONStorage } from 'zustand/middleware' 3 | 4 | export type OAuthProvider = 'github' | 'eth' 5 | 6 | export type OAuthRes = { 7 | exp: string 8 | nickname: string 9 | provider: string 10 | session_id: string 11 | sub: string 12 | } 13 | 14 | type Store = { 15 | provider: string | null 16 | sessionId: string | null 17 | nickname: string | null 18 | exp: string | null 19 | sub: string | null 20 | error: string | null 21 | signin: (res: OAuthRes) => void 22 | signout: () => void 23 | setError: (msg: string) => void 24 | } 25 | 26 | export const useAuthStore = create()( 27 | persist( 28 | (set) => ({ 29 | provider: null, 30 | sessionId: null, 31 | nickname: null, 32 | exp: null, 33 | sub: null, 34 | error: null, 35 | signin: (res: OAuthRes) => 36 | set({ 37 | ...res, 38 | sessionId: res.session_id, 39 | error: null 40 | }), 41 | signout: () => 42 | set({ 43 | provider: null, 44 | sessionId: null, 45 | nickname: null, 46 | exp: null, 47 | sub: null, 48 | error: null 49 | }), 50 | setError: (msg: string) => set({ error: msg }) 51 | }), 52 | { 53 | name: 'kzg-temporary-session', 54 | storage: createJSONStorage(() => sessionStorage), 55 | } 56 | ) 57 | ) 58 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type TryContributeRes = { 2 | ValidContribution: number 3 | } 4 | 5 | export type ContributeRes = { 6 | receipt: string | null 7 | signature: string | null 8 | contribution: string | null 9 | } 10 | 11 | export type ErrorRes = { 12 | code: string 13 | error: string 14 | message?: string 15 | } 16 | 17 | export type RequestLinkRes = { 18 | eth_auth_url: string 19 | github_auth_url: string 20 | } 21 | 22 | export type SubgroupCheckResWasm = { 23 | checkContribution: boolean | null 24 | checkNewContribution: boolean | null 25 | } 26 | 27 | export type Transcript = { 28 | transcripts: SubTranscript[], 29 | participantIds: string[], 30 | participantEcdsaSignatures: string[], 31 | } 32 | 33 | export type SubTranscript = { 34 | numG1Powers: number, 35 | numG2Powers: number, 36 | powersOfTau: { 37 | G1Powers: string[] 38 | G2Powers: string[] 39 | }, 40 | witness: { 41 | potPubkeys: string[], 42 | blsSignatures: string[], 43 | runningProducts: string[], 44 | } 45 | } 46 | 47 | export type Record = { 48 | position: number 49 | participantId: string | null 50 | participantName?: string | null 51 | participantEcdsaSignature: string | null 52 | transcripts: TranscriptDetails[] 53 | } 54 | 55 | export type TranscriptDetails = { 56 | potPubkeys: string, 57 | blsSignature: string 58 | } 59 | 60 | export type SequencerStatus = { 61 | lobby_size: number, 62 | num_contributions: number, 63 | sequencer_address: string, 64 | status: string, 65 | } -------------------------------------------------------------------------------- /src/assets/open-flower.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/FlowerAnimation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Lottie from 'react-lottie' 3 | import { isMobile } from '../utils' 4 | import styled, { css } from 'styled-components' 5 | import animationData from '../lotties/flower.json' 6 | 7 | type Props = { 8 | inverse?: boolean 9 | } 10 | 11 | const FlowerAnimation = ({ inverse = false }: Props) => { 12 | const defaultOptions = { 13 | loop: true, 14 | autoplay: true, 15 | animationData: animationData, 16 | rendererSettings: { 17 | preserveAspectRatio: "xMidYMid slice" 18 | } 19 | } 20 | 21 | return ( 22 | 23 | 31 | 32 | ) 33 | } 34 | 35 | const Container = styled.div<{ isMobile: boolean, inverse: boolean}>` 36 | ${({ isMobile }) => isMobile ? 37 | css` 38 | margin-top: -130px; 39 | margin-bottom: 90px; 40 | height: 200px; 41 | ` : '' 42 | } 43 | ${({ inverse }) => inverse ? 44 | css` 45 | -moz-transform: scale(-1, 1); 46 | -webkit-transform: scale(-1, 1); 47 | -o-transform: scale(-1, 1); 48 | -ms-transform: scale(-1, 1); 49 | transform: scale(-1, 1); 50 | ` : '' 51 | } 52 | ` 53 | 54 | export default FlowerAnimation; -------------------------------------------------------------------------------- /src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import LogoImg from '../assets/logo.svg' 3 | import { textSerif } from '../style/utils' 4 | import { BREAKPOINT, FONT_SIZE } from '../constants' 5 | 6 | type Props = { 7 | withVersion?: boolean 8 | centerOnMobile?: boolean 9 | onClick: () => void 10 | } 11 | 12 | const Logo = ({ withVersion, centerOnMobile, onClick }: Props) => { 13 | return ( 14 | 15 | Logo 16 | 17 | KZG 18 | Ceremony 19 | {withVersion && Alpha} 20 | 21 | 22 | ) 23 | } 24 | 25 | const LogoSection = styled.div<{ centerOnMobile?: boolean }>` 26 | display: flex; 27 | align-items: center; 28 | cursor: pointer; 29 | gap: 0.5rem; 30 | ${({ centerOnMobile }) => centerOnMobile && ` 31 | @media (max-width: ${BREAKPOINT.M}) { 32 | flex-direction: column; 33 | & > div { 34 | align-items: center; 35 | } 36 | } 37 | `} 38 | ` 39 | 40 | const LogoTextGroup = styled.div` 41 | display: flex; 42 | flex-direction: column; 43 | ` 44 | 45 | const LogoTextMain = styled.span` 46 | font-size: ${FONT_SIZE.M}; 47 | font-weight: 600; 48 | ${textSerif}; 49 | line-height: 14px; 50 | user-select: none; 51 | ` 52 | 53 | const LogoTextSub = styled.span` 54 | font-size: ${FONT_SIZE.XS}; 55 | color: #494e53; 56 | ${textSerif}; 57 | ` 58 | 59 | export default Logo 60 | -------------------------------------------------------------------------------- /src/components/Image.ts: -------------------------------------------------------------------------------- 1 | import styled, { css, keyframes } from 'styled-components' 2 | import { CIRCLE_SIZE } from '../constants' 3 | 4 | const Base = styled.img<{ visible?: boolean }>` 5 | opacity: ${({ visible }) => (visible ? 1 : 0)}; 6 | transform: scale(0.85); 7 | transition: all 1s ease; 8 | ` 9 | 10 | export const Bg = styled(Base)` 11 | z-index: -2; 12 | position: absolute; 13 | top: -9999px; 14 | bottom: -9999px; 15 | left: -9999px; 16 | right: -9999px; 17 | margin: auto; 18 | height: auto; 19 | width: ${CIRCLE_SIZE + 1244}px; 20 | ` 21 | 22 | export const Img = styled(Base)` 23 | position: absolute; 24 | top: -9999px; 25 | bottom: -9999px; 26 | left: -9999px; 27 | right: -9999px; 28 | margin: auto; 29 | height: auto; 30 | ` 31 | 32 | const r = keyframes` 33 | 0% { transform: rotate(0deg) scale(0.85); } 34 | 100% { transform: rotate(360deg) scale(0.85); } 35 | ` 36 | 37 | const o = keyframes` 38 | to { opacity: 1; } 39 | ` 40 | 41 | export const PizzaImg = styled(Img)<{ rounding: boolean }>` 42 | height: auto; 43 | transition: all 3s ease; 44 | ${({ rounding }) => 45 | rounding 46 | ? css` 47 | animation: ${r} 40s linear infinite, ${o} 3s forwards; 48 | ` 49 | : ''} 50 | ` 51 | 52 | const p = keyframes` 53 | 0% { opacity: 0; } 54 | 50% {opacity: 1;} 55 | 100% { opacity: 0; } 56 | ` 57 | 58 | export const BgPulse = styled(Bg)` 59 | opacity: ${({ visible }) => (visible ? 1 : 0)}; 60 | animation: ${p} 10s ease-in-out infinite; 61 | height: auto; 62 | z-index: 0; 63 | width: ${CIRCLE_SIZE + 1313}px; 64 | ` 65 | -------------------------------------------------------------------------------- /src/components/Text.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Link } from 'react-router-dom' 3 | import { FONT_SIZE } from '../constants' 4 | import { textSerif } from '../style/utils' 5 | 6 | const PageTitle = styled.h1` 7 | text-align: center; 8 | text-transform: uppercase; 9 | line-height: 34px; 10 | font-size: ${FONT_SIZE.XXL}; 11 | ${textSerif} 12 | font-weight: 700; 13 | margin-top: 0px; 14 | margin-bottom: 15px; 15 | ` 16 | 17 | const SectionTitle = styled.h2` 18 | text-transform: uppercase; 19 | font-size: ${FONT_SIZE.XXL}; 20 | line-height: 153px; 21 | ${textSerif} 22 | font-weight: 700; 23 | margin: 24px 0; 24 | overflow-wrap: break-word; 25 | ` 26 | 27 | const ItalicSubTitle = styled.h3` 28 | font-size ${FONT_SIZE.L}; 29 | font-style: italic; 30 | font-weight: 100; 31 | text-align: center; 32 | margin-top: 4px; 33 | ` 34 | 35 | const MessageText = styled.p` 36 | font-size ${FONT_SIZE.SM}; 37 | font-weight: 100; 38 | color: red; 39 | text-align: center; 40 | margin-top: 4px; 41 | ` 42 | 43 | const Description = styled.p` 44 | font-weight: 400; 45 | font-size: ${FONT_SIZE.SM}; 46 | margin: 0 0 20px; 47 | ` 48 | 49 | const LinkText = styled(Link)` 50 | color: ${({ theme }) => theme.text}; 51 | border: none; 52 | background-color: transparent; 53 | cursor: pointer; 54 | font-weight: 600; 55 | font-size: ${FONT_SIZE.L}; 56 | line-height: 24px; 57 | padding-bottom: 4px; 58 | :hover { 59 | border-bottom: solid 2px ${({ theme }) => theme.primary}; 60 | } 61 | transition: all 0.2s ease; 62 | ` 63 | 64 | const Bold = styled.span` 65 | font-weight: 700; 66 | ` 67 | 68 | export { PageTitle, SectionTitle, ItalicSubTitle, MessageText, Description, LinkText, Bold } 69 | -------------------------------------------------------------------------------- /src/components/landing/FaqItem.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { useState } from 'react' 3 | import { BREAKPOINT, FONT_SIZE } from '../../constants' 4 | 5 | type Props = { 6 | title: string | React.ReactNode | JSX.Element 7 | content: string | React.ReactNode | JSX.Element 8 | } 9 | 10 | const FaqItem = ({ title, content }: Props) => { 11 | const [open, setOpen] = useState(false) 12 | 13 | return ( 14 | 15 | setOpen((prev) => !prev)} open={open}> 16 | {title} 17 | 18 | {open && {content}} 19 | 20 | ) 21 | } 22 | 23 | const Container = styled.div` 24 | border-bottom: solid 1px ${({ theme }) => theme.text}; 25 | transition: background-color 0.1s linear; 26 | padding: 0.6rem 2rem; 27 | :hover:not([disabled]) { 28 | box-shadow: 1px 2px 6px 6px #b4b2b2; 29 | border-bottom: none; 30 | border-right: none; 31 | border-left: none; 32 | } 33 | ` 34 | 35 | const Title = styled.p<{ open: boolean }>` 36 | font-size: ${FONT_SIZE.M}; 37 | font-weight: 600; 38 | display: flex; 39 | justify-content: space-between; 40 | gap: 1rem; 41 | padding-block: 1rem; 42 | margin-block: 0; 43 | :hover { 44 | cursor: pointer; 45 | } 46 | ::after { 47 | content: "${({ open }) => open ? '-' : '+'}"; 48 | } 49 | @media (max-width: ${BREAKPOINT.M}) { 50 | font-size: ${FONT_SIZE.L}; 51 | } 52 | ` 53 | 54 | const Content = styled.div` 55 | font-size: ${FONT_SIZE.SM}; 56 | ol, li { 57 | margin-block-end: 0.5rem; 58 | } 59 | blockquote { 60 | border-inline-start: 2px solid ${({ theme }) => theme.disabled}; 61 | margin-inline: 1.5rem; 62 | padding-inline: 1rem; 63 | } 64 | a { 65 | text-decoration: underline; 66 | } 67 | ` 68 | 69 | export default FaqItem 70 | -------------------------------------------------------------------------------- /src/components/Blockies.tsx: -------------------------------------------------------------------------------- 1 | import ToolTip from './Tooltip' 2 | import styled from 'styled-components' 3 | import { stringToColor } from '../utils' 4 | import { useEffect, useRef } from 'react' 5 | import blockies from 'blockies-identicon' 6 | import { useTranslation } from 'react-i18next' 7 | 8 | type Props = { 9 | opts: { 10 | seed?: string 11 | size?: number 12 | scale?: number 13 | } 14 | onClick?: () => void 15 | clickable?: boolean 16 | } 17 | 18 | const BlockiesIdenticon = ({ 19 | opts: { seed = 'foo', size = 15, scale = 3 }, 20 | onClick, 21 | clickable = false, 22 | }: Props) => { 23 | const { t } = useTranslation(); 24 | const canvasRef = useRef(null!) 25 | 26 | useEffect(() => { 27 | const color = stringToColor(seed + 'main') 28 | const bgcolor = stringToColor(seed + 'bg') 29 | 30 | // render identicon after load 31 | blockies.render( 32 | { 33 | seed, 34 | color, 35 | bgcolor, 36 | size, 37 | scale, 38 | spotcolor: bgcolor 39 | }, 40 | canvasRef.current 41 | ) 42 | // eslint-disable-next-line 43 | }, []) 44 | 45 | return ( 46 | <> 47 | 48 | 53 | 54 | 55 | ) 56 | } 57 | 58 | const Canvas = styled.canvas<{clickable: boolean}>` 59 | cursor: ${ ({ clickable }) => clickable ? 'pointer' : ''}; 60 | border-radius: 6px; 61 | transition: all linear 0.1s; 62 | height: 30px; 63 | width: 30px; 64 | margin-right: 5px; 65 | :hover { 66 | transform: translateY(-2px) scale(1.05); 67 | box-shadow: 0px 6px 6px 2px #00000033; 68 | } 69 | ` 70 | 71 | export default BlockiesIdenticon 72 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | // sw.js 3 | self.addEventListener("install", function () { 4 | self.skipWaiting(); 5 | }); 6 | 7 | self.addEventListener("activate", (event) => { 8 | event.waitUntil(self.clients.claim()); 9 | }); 10 | 11 | self.addEventListener("fetch", async function (event) { 12 | if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") { 13 | return; 14 | } 15 | 16 | let origin = self.location.origin; 17 | let regex = new RegExp("^" + origin); 18 | let url = event.request.url; 19 | // discard requests coming outside from the origin (google fonts, apis, etc) 20 | if ( !regex.test(url) ) return; 21 | 22 | // Brave requires to use the url string rather than the whole object 23 | let request = event.request; 24 | if (url.includes('session_id') || url.includes("code")){ 25 | request = event.request.url; 26 | } 27 | 28 | event.respondWith( 29 | fetch(request) 30 | .then(function (response) { 31 | const newHeaders = new Headers(response.headers); 32 | 33 | if (url && url.includes('double_sign')) { 34 | newHeaders.delete("Cross-Origin-Embedder-Policy"); 35 | newHeaders.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups"); 36 | } else { 37 | newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp"); 38 | newHeaders.set("Cross-Origin-Opener-Policy", "same-origin"); 39 | } 40 | 41 | const moddedResponse = new Response(response.body, { 42 | status: response.status, 43 | statusText: response.statusText, 44 | headers: newHeaders, 45 | }); 46 | 47 | return moddedResponse; 48 | }) 49 | .catch(async function (e) { 50 | console.error(e); 51 | }) 52 | ); 53 | }); -------------------------------------------------------------------------------- /src/pages/lobbyFull.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { useNavigate } from 'react-router-dom' 3 | import { Trans, useTranslation } from 'react-i18next' 4 | import { PrimaryButtonLarge } from '../components/Button' 5 | import { Description, PageTitle } from '../components/Text' 6 | import HeaderJustGoingBack from '../components/headers/HeaderJustGoingBack' 7 | import { 8 | SingleContainer as Container, 9 | SingleWrap as Wrap, 10 | OverRelative, 11 | TextSection, 12 | InnerWrap, 13 | Over, 14 | } from '../components/Layout' 15 | import ROUTES from '../routes' 16 | 17 | const LobbyFullPage = () => { 18 | useTranslation() 19 | const navigate = useNavigate() 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Too much
magic
31 |
32 | 33 | 34 | 35 | There are too many summoners at this time, please come back 36 | in a few hours! 37 | 38 | 39 | 40 | 41 | { 43 | navigate(ROUTES.ROOT) 44 | }} 45 | > 46 | Return to home 47 | 48 | 49 |
50 |
51 |
52 |
53 |
54 | 55 | ) 56 | } 57 | 58 | const ButtonSection = styled.div` 59 | padding-bottom: 24px; 60 | ` 61 | 62 | export default LobbyFullPage 63 | -------------------------------------------------------------------------------- /src/components/Layout.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { CIRCLE_SIZE, FONT_SIZE } from '../constants' 3 | 4 | // Single section page 5 | export const SingleContainer = styled.div` 6 | color: ${({ theme }) => theme.text}; 7 | height: 100vh; 8 | width: 100vw; 9 | 10 | transform: scale(0.85); 11 | ` 12 | 13 | export const OverRelative = styled.div` 14 | height: 100vh; 15 | width: 100vw; 16 | 17 | position: relative; 18 | ` 19 | 20 | export const Over = styled.div` 21 | height: 100vh; 22 | width: 100vw; 23 | 24 | overflow: hidden; 25 | position: absolute; 26 | ` 27 | 28 | export const SingleWrap = styled.div` 29 | position: absolute; 30 | left: -20000px; 31 | right: -20000px; 32 | top: -20000px; 33 | bottom: -20000px; 34 | 35 | width: ${CIRCLE_SIZE}px; 36 | height: ${CIRCLE_SIZE}px; 37 | background-color: transparent; 38 | border-radius: 50%; 39 | margin: auto; 40 | display: flex; 41 | flex-direction: column; 42 | align-items: center; 43 | justify-content: center; 44 | ` 45 | 46 | export const ColorWrap = styled(SingleWrap)` 47 | background-color: ${({ theme }) => theme.surface}; 48 | ` 49 | 50 | export const InnerWrap = styled.div` 51 | margin: auto; 52 | display: flex; 53 | flex-direction: column; 54 | align-items: center; 55 | ` 56 | 57 | export const SingleButtonSection = styled.div` 58 | display: flex; 59 | flex-direction: column; 60 | height: 200px; 61 | align-items: center; 62 | justify-content: space-around; 63 | margin-top: 40px; 64 | ` 65 | 66 | export const TextSection = styled.div` 67 | width: 36ch; 68 | font-size: ${FONT_SIZE.SM}; 69 | max-width: 100%; 70 | ` 71 | 72 | export const Bg = styled.img` 73 | z-index: 0; 74 | position: absolute; 75 | top: -9999px; 76 | bottom: -9999px; 77 | left: -9999px; 78 | right: -9999px; 79 | margin: auto; 80 | ` 81 | 82 | export const Img = styled.img` 83 | position: absolute; 84 | top: -9999px; 85 | bottom: -9999px; 86 | left: -9999px; 87 | right: -9999px; 88 | margin: auto; 89 | ` 90 | -------------------------------------------------------------------------------- /src/store/contribute.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { persist, createJSONStorage } from 'zustand/middleware' 3 | 4 | export type Store = { 5 | receipt: string | null 6 | contribution: string | null 7 | newContribution: string | null 8 | sequencerSignature: string | null 9 | updateReceipt: (data: string | null) => void 10 | updateContribution: (data: string | null) => void 11 | updateNewContribution: (data: string | null) => void 12 | updateSequencerSignature: (data: string | null) => void 13 | } 14 | 15 | export type EntropyStore = { 16 | entropy: string | null 17 | potPubkeys: string[] | null 18 | ECDSASigner: string | null 19 | ECDSASignature: string | null 20 | updateEntropy: (data: string | null) => void 21 | updatePotPubkeys: (data: string[] | null) => void 22 | updateECDSASigner: (data: string | null) => void 23 | updateECDSASignature: (data: string | null) => void 24 | } 25 | 26 | export const useContributionStore = create((set, _get) => ({ 27 | receipt: null, 28 | contribution: null, 29 | newContribution: null, 30 | sequencerSignature: null, 31 | updateReceipt: (data: string | null) => set({ receipt: data }), 32 | updateContribution: (data: string | null) => set({ contribution: data }), 33 | updateNewContribution: (data: string | null) => set({ newContribution: data }), 34 | updateSequencerSignature: (data: string | null) => set({ sequencerSignature: data }), 35 | })) 36 | 37 | export const useEntropyStore = create()( 38 | persist( 39 | (set) => ({ 40 | entropy: null, 41 | potPubkeys: null, 42 | ECDSASigner: null, 43 | ECDSASignature: null, 44 | updateEntropy: (data: string | null) => set({ entropy: data }), 45 | updatePotPubkeys: (data: string[] | null) => set({ potPubkeys: data }), 46 | updateECDSASigner: (data: string | null) => set({ ECDSASigner: data }), 47 | updateECDSASignature: (data: string | null) => set({ ECDSASignature: data }), 48 | }), 49 | { 50 | name: 'kzg-temporary-entropy', 51 | storage: createJSONStorage(() => sessionStorage), 52 | }) 53 | ) 54 | -------------------------------------------------------------------------------- /src/components/headers/Header.tsx: -------------------------------------------------------------------------------- 1 | // Import libraries 2 | import { Trans, useTranslation } from 'react-i18next' 3 | import { textSerif } from '../../style/utils' 4 | import styled from 'styled-components' 5 | // Import components 6 | import Logo from '../Logo' 7 | import LanguageSelector from '../LanguageSelector' 8 | // Import constants 9 | import { FONT_SIZE, BREAKPOINT, ENVIRONMENT } from '../../constants' 10 | // Import hooks 11 | import { useNavigate } from 'react-router-dom' 12 | import { useAuthStore } from '../../store/auth' 13 | import { isMobile } from '../../utils' 14 | import ROUTES from '../../routes' 15 | 16 | const Header = () => { 17 | useTranslation() 18 | const navigate = useNavigate() 19 | const { nickname } = useAuthStore() 20 | return ( 21 | 22 | navigate(ROUTES.ROOT)} /> 23 | 24 | {ENVIRONMENT === 'testnet' ? ( 25 | 26 | TEST CEREMONY 27 | 28 | ) : ( 29 | <> 30 | )} 31 |
{nickname}
32 | 33 |
34 | ) 35 | } 36 | 37 | const Container = styled.header<{ isMobile: boolean }>` 38 | background-color: ${({ theme }) => theme.surface2}; 39 | height: 75px; 40 | display: flex; 41 | align-items: center; 42 | justify-content: space-between; 43 | box-shadow: 1px 2px 6px 1px #b4b2b2; 44 | padding-inline: ${({ isMobile }) => (isMobile ? '5vw' : '21vw')}; 45 | width: 100%; 46 | z-index: 3; 47 | position: absolute; 48 | top: 0; 49 | ` 50 | 51 | const CenterSection = styled.div<{ isMobile: boolean }>` 52 | display: flex; 53 | color: #3e70bc; 54 | align-items: start; 55 | font-size: ${({ isMobile }) => (isMobile ? FONT_SIZE.S : FONT_SIZE.XXL)}; 56 | letter-spacing: ${({ isMobile }) => (isMobile ? '0.5px' : '2px')}; 57 | ${textSerif} 58 | font-weight: 800; 59 | ` 60 | 61 | const Address = styled.div` 62 | max-width: 11ch; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | white-space: nowrap; 66 | @media (max-width: ${BREAKPOINT.M}) { 67 | display: none; 68 | } 69 | ` 70 | 71 | export default Header 72 | -------------------------------------------------------------------------------- /public/wasm/wasm-worker.js: -------------------------------------------------------------------------------- 1 | import init, { 2 | init_threads, 3 | verify_wasm, 4 | contribute_wasm, 5 | subgroup_check_wasm, 6 | get_pot_pubkeys_wasm 7 | } from './pkg/wrapper_small_pot.js' 8 | 9 | onmessage = async (event) => { 10 | await init() 11 | await init_threads(navigator.hardwareConcurrency) 12 | console.log('available threads:', navigator.hardwareConcurrency) 13 | const { action } = event.data 14 | switch (action) { 15 | case 'contribute': 16 | contribute(event.data) 17 | break 18 | case 'subgroupCheck': 19 | subgroupChecks(event.data) 20 | break 21 | case 'getPotPubkeys': 22 | getPotPubkeys(event.data) 23 | break 24 | case 'verify': 25 | verify(event.data) 26 | break 27 | default: 28 | break 29 | } 30 | } 31 | 32 | async function contribute(data) { 33 | const { contributionString, entropy, identity } = data 34 | console.log('start contributing') 35 | const startTime = performance.now() 36 | const result = contribute_wasm(contributionString, entropy, identity) 37 | const endTime = performance.now() 38 | console.log(`Contribution took ${endTime - startTime} milliseconds`) 39 | postMessage(result) 40 | } 41 | 42 | function subgroupChecks(data) { 43 | let { contribution, newContribution } = data 44 | 45 | console.log('start subgroup checks') 46 | const startTime = performance.now() 47 | const checkContribution = subgroup_check_wasm(contribution) 48 | const checkNewContribution = subgroup_check_wasm(newContribution) 49 | const endTime = performance.now() 50 | console.log(`Subgroups checks took ${endTime - startTime} milliseconds`) 51 | const result = { 52 | checkContribution, 53 | checkNewContribution 54 | } 55 | postMessage(result) 56 | } 57 | 58 | function getPotPubkeys(data) { 59 | const { entropy } = data 60 | console.log('start get potPubkeys') 61 | const startTime = performance.now() 62 | const potPubkeys = get_pot_pubkeys_wasm(entropy) 63 | const endTime = performance.now() 64 | console.log(`Get PotPubkeys took ${endTime - startTime} milliseconds`) 65 | postMessage(potPubkeys) 66 | } 67 | 68 | function verify(data) { 69 | const { transcript } = data 70 | console.log('start verifying') 71 | const startTime = performance.now() 72 | const result = verify_wasm(transcript) 73 | const endTime = performance.now() 74 | console.log(`Verification took ${endTime - startTime} milliseconds`) 75 | postMessage(result) 76 | } 77 | -------------------------------------------------------------------------------- /src/assets/explanation-border.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Logo from './Logo' 2 | import ROUTES from '../routes' 3 | import { Trans } from 'react-i18next' 4 | import styled from 'styled-components' 5 | import { useNavigate } from 'react-router-dom' 6 | import { BREAKPOINT, FONT_SIZE } from '../constants' 7 | 8 | const Footer = () => { 9 | const navigate = useNavigate() 10 | return ( 11 | 12 | 13 | navigate(ROUTES.ROOT)}/> 14 | 15 | 16 | Built by the Ethereum Community - 2022 17 | 18 | 19 | 20 | 21 | 22 | 23 | GitHub 24 | 25 | 26 | 27 | Documentation 28 | 29 | 30 | 31 | 32 | Audit report 33 | 34 | 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | const Container = styled.footer` 42 | background-color: ${({ theme }) => theme.surface2}; 43 | /* height: 360px; */ 44 | padding: 60px 10%; 45 | display: flex; 46 | justify-content: space-between; 47 | gap: 2.5rem; 48 | @media (max-width: ${BREAKPOINT.M}) { 49 | flex-direction: column; 50 | align-items: center; 51 | } 52 | ` 53 | 54 | const LeftSection = styled.div` 55 | display: flex; 56 | flex-direction: column; 57 | @media (max-width: ${BREAKPOINT.M}) { 58 | align-items: center; 59 | } 60 | p { 61 | text-align: center; 62 | } 63 | ` 64 | 65 | const RightSection = styled.div` 66 | display: flex; 67 | ` 68 | 69 | const Copyright = styled.p` 70 | color: ${({ theme }) => theme.text}; 71 | ` 72 | 73 | const LinkGroup = styled.div` 74 | display: flex; 75 | flex-direction: column; 76 | @media (max-width: ${BREAKPOINT.M}) { 77 | align-items: center; 78 | } 79 | ` 80 | 81 | const LinkItem = styled.a` 82 | font-size: ${FONT_SIZE.L}; 83 | font-weight: 600; 84 | cursor: pointer; 85 | margin-bottom: 24px; 86 | ` 87 | 88 | export default Footer 89 | -------------------------------------------------------------------------------- /src/wasm.ts: -------------------------------------------------------------------------------- 1 | import type { SubgroupCheckResWasm } from './types' 2 | 3 | class Wasm { 4 | async contribute( 5 | contribution: string, 6 | entropy: string, 7 | identity: string 8 | ): Promise { 9 | return new Promise((resolve) => { 10 | const worker = new Worker('./wasm/wasm-worker.js', { 11 | type: 'module' 12 | }) 13 | const data = { 14 | action: 'contribute', 15 | contributionString: contribution, 16 | entropy: entropy, 17 | identity: identity 18 | } 19 | worker.onmessage = async (event) => { 20 | resolve(event.data as any) 21 | worker.terminate() 22 | } 23 | worker.postMessage(data) 24 | }) 25 | } 26 | async checkContributions( 27 | contribution: string, 28 | newContribution: string 29 | ): Promise { 30 | return new Promise((resolve) => { 31 | const worker = new Worker('./wasm/wasm-worker.js', { 32 | type: 'module' 33 | }) 34 | const data = { 35 | action: 'subgroupCheck', 36 | contribution: contribution, 37 | newContribution: newContribution 38 | } 39 | worker.onmessage = async (event) => { 40 | const { checkContribution, checkNewContribution } = event.data 41 | resolve({ 42 | checkContribution, 43 | checkNewContribution 44 | }) 45 | worker.terminate() 46 | } 47 | worker.postMessage(data) 48 | }) 49 | } 50 | async getPotPubkeys(entropy: string) { 51 | return new Promise((resolve) => { 52 | const worker = new Worker('./wasm/wasm-worker.js', { 53 | type: 'module' 54 | }) 55 | const data = { 56 | action: 'getPotPubkeys', 57 | entropy: entropy 58 | } 59 | worker.onmessage = async (event) => { 60 | resolve(event.data) 61 | worker.terminate() 62 | } 63 | worker.postMessage(data) 64 | }) 65 | } 66 | async verify(transcript: string) { 67 | return new Promise((resolve) => { 68 | const worker = new Worker('./wasm/wasm-worker.js', { 69 | type: 'module' 70 | }) 71 | const data = { 72 | action: 'verify', 73 | transcript: transcript 74 | } 75 | worker.onmessage = async (event) => { 76 | resolve(event.data) 77 | worker.terminate() 78 | } 79 | worker.postMessage(data) 80 | }) 81 | } 82 | } 83 | 84 | const wasm = new Wasm() 85 | export default wasm 86 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | const CIRCLE_SIZE = 490 2 | 3 | const PAGE_SIZE = 20 4 | 5 | const FONT_SIZE = { 6 | XXS: '9px', 7 | XS: '11px', 8 | S: '13px', 9 | SM: '14px', 10 | M: '15px', 11 | L: '19px', 12 | XL: '23px', 13 | XXL: '25px' 14 | } as const 15 | 16 | const SPACE = { 17 | XS: '4px', 18 | S: '8px', 19 | M: '16px', 20 | L: '20px', 21 | XL: '24px', 22 | XXL: '32px' 23 | } as const 24 | 25 | const RADIUS = { 26 | S: '4px', 27 | M: '8px', 28 | L: '12px' 29 | } as const 30 | 31 | const BREAKPOINT = { 32 | S: '480px', 33 | M: '768px', 34 | L: '1024px', 35 | XL: '1280px' 36 | } as const 37 | 38 | const BACKGROUND_DARKNESS = 0.7 39 | 40 | const ENVIRONMENT = process.env.REACT_APP_ENVIRONMENT || 'testnet' 41 | 42 | const API_ROOT = process.env.REACT_APP_API_ROOT || 'http://127.0.0.1:3000' 43 | const COMPUTE_DEADLINE = 44 | parseInt(process.env.REACT_APP_COMPUTE_DEADLINE as string) || 180 45 | const LOBBY_CHECKIN_FREQUENCY = 46 | parseInt(process.env.REACT_APP_LOBBY_CHECKIN_FREQUENCY as string) || 32000 47 | const MIN_MOUSE_ENTROPY_SAMPLES = 48 | parseInt(process.env.REACT_APP_MIN_MOUSE_ENTROPY_SAMPLES as string) || 64 49 | const AVERAGE_CONTRIBUTION_TIME = 50 | parseInt(process.env.REACT_APP_AVERAGE_CONTRIBUTION_TIME as string) || 52 51 | const ETH_MIN_NONCE = 52 | parseInt(process.env.REACT_APP_ETH_MIN_NONCE as string) || 3 53 | 54 | const START_DATE = 55 | parseInt(process.env.REACT_APP_START_DATE as string) || 1678713180 56 | const END_DATE = 57 | parseInt(process.env.REACT_APP_END_DATE as string) || 1681668780 58 | 59 | const INFURA_ID = 60 | process.env.REACT_APP_INFURA_ID || 'cd82571d19ab490e828dd0f86ec3cbf0' 61 | const PORTIS_ID = 62 | process.env.REACT_APP_PORTIS_ID || 'd6418a0a-18ae-4dfd-a206-3398012907ec' 63 | const FORTMATIC_KEY = 64 | process.env.REACT_APP_FORTMATIC_KEY || 'pk_live_AAE763E3E8AC097E' 65 | 66 | const WALLET_CONNECT_PROJECT_ID = 67 | process.env.REACT_APP_WALLET_CONNECT_PROJECT_ID || 68 | 'c8abd7e451b16bf3f3cdf58aaea89f60' 69 | 70 | const TRANSCRIPT_HASH = 71 | process.env.REACT_APP_TRANSCRIPT_HASH || 72 | '0x8ed1c73857e77ae98ea23e36cdcf828ccbf32b423fddc7480de658f9d116c848' 73 | 74 | export { 75 | FONT_SIZE, 76 | CIRCLE_SIZE, 77 | PAGE_SIZE, 78 | SPACE, 79 | RADIUS, 80 | BREAKPOINT, 81 | ENVIRONMENT, 82 | BACKGROUND_DARKNESS, 83 | API_ROOT, 84 | COMPUTE_DEADLINE, 85 | LOBBY_CHECKIN_FREQUENCY, 86 | MIN_MOUSE_ENTROPY_SAMPLES, 87 | AVERAGE_CONTRIBUTION_TIME, 88 | ETH_MIN_NONCE, 89 | START_DATE, 90 | END_DATE, 91 | PORTIS_ID, 92 | FORTMATIC_KEY, 93 | INFURA_ID, 94 | WALLET_CONNECT_PROJECT_ID, 95 | TRANSCRIPT_HASH 96 | } 97 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/hooks/useBackgroundVisibility.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom' 2 | import ROUTES from '../routes' 3 | import { isBgRoute } from '../utils' 4 | 5 | type ColorState = 'hidden' | 'white' | 'color' | 'white-no-pizza-animate' 6 | type AnimateState = ColorState | 'animate' 7 | 8 | type InnerState = ColorState 9 | type OuterState = ColorState 10 | type BgState = AnimateState 11 | type DarkState = 'hidden' | 'true' 12 | type SnakeState = ColorState | 'progress' 13 | type PizzaState = AnimateState 14 | 15 | type Visibilities = { 16 | bg: BgState 17 | dark: DarkState 18 | inner: InnerState 19 | outer: OuterState 20 | snake: SnakeState 21 | pizza: PizzaState 22 | } 23 | 24 | const initial: Visibilities = { 25 | bg: 'hidden', 26 | dark: 'hidden', 27 | inner: 'hidden', 28 | outer: 'hidden', 29 | snake: 'hidden', 30 | pizza: 'hidden' 31 | } 32 | 33 | export default function useBackgroundVisibility(): Visibilities { 34 | const location = useLocation() 35 | 36 | if (!isBgRoute(location.pathname)) return initial 37 | 38 | if (location.pathname === ROUTES.SIGNIN) 39 | return { 40 | bg: 'white', 41 | dark: 'hidden', 42 | inner: 'color', 43 | outer: 'color', 44 | snake: 'color', 45 | pizza: 'hidden' 46 | } 47 | 48 | if (location.pathname === ROUTES.ENTROPY_INPUT) 49 | return { 50 | bg: 'white', 51 | dark: 'hidden', 52 | inner: 'hidden', 53 | outer: 'hidden', 54 | snake: 'progress', 55 | pizza: 'hidden' 56 | } 57 | 58 | if (location.pathname === ROUTES.LOBBY) 59 | return { 60 | bg: 'white-no-pizza-animate', 61 | dark: 'hidden', 62 | inner: 'color', 63 | outer: 'color', 64 | snake: 'color', 65 | pizza: 'animate' 66 | } 67 | if (location.pathname === ROUTES.LOBBY_FULL) 68 | return { 69 | bg: 'white', 70 | dark: 'hidden', 71 | inner: 'color', 72 | outer: 'color', 73 | snake: 'color', 74 | pizza: 'hidden' 75 | } 76 | 77 | if (location.pathname === ROUTES.DOUBLE_SIGN) 78 | return { 79 | bg: 'white', 80 | dark: 'hidden', 81 | inner: 'color', 82 | outer: 'color', 83 | snake: 'color', 84 | pizza: 'color' 85 | } 86 | 87 | if (location.pathname === ROUTES.CONTRIBUTING) 88 | return { 89 | bg: 'animate', 90 | dark: 'hidden', 91 | inner: 'color', 92 | outer: 'color', 93 | snake: 'color', 94 | pizza: 'color' 95 | } 96 | 97 | if (location.pathname === ROUTES.COMPLETE) 98 | return { 99 | bg: 'color', 100 | dark: 'hidden', 101 | inner: 'color', 102 | outer: 'color', 103 | snake: 'color', 104 | pizza: 'color' 105 | } 106 | 107 | return initial 108 | } 109 | -------------------------------------------------------------------------------- /src/pages/signinRedirect.tsx: -------------------------------------------------------------------------------- 1 | import ROUTES from '../routes' 2 | import { useEffect } from 'react' 3 | import { useAuthStore } from '../store/auth' 4 | import { useTranslation } from 'react-i18next' 5 | import { useNavigate } from 'react-router-dom' 6 | import { useEntropyStore } from '../store/contribute' 7 | import { toParams, validateSigninParams } from '../utils' 8 | 9 | const SigninRedirect = (props: any) => { 10 | const navigate = useNavigate() 11 | const { t } = useTranslation() 12 | const { ECDSASigner } = useEntropyStore() 13 | const { signin, setError } = useAuthStore() 14 | 15 | useEffect(() => { 16 | const isSameWallet = async (nickname: string): Promise => { 17 | if (ECDSASigner && ECDSASigner.toLowerCase() === nickname.toLowerCase()) { 18 | return true 19 | } else { 20 | return false 21 | } 22 | } 23 | 24 | ;(async () => { 25 | const params = toParams(props.search.replace(/^\?/, '')) 26 | window.history.replaceState(null, '', window.location.pathname) 27 | // check if login was succesful 28 | if (validateSigninParams(params)) { 29 | const notSameWallet = !(await isSameWallet(params.nickname)) 30 | if (params.provider === 'Ethereum' && notSameWallet) { 31 | setError(t('error.notSameWallet')) 32 | navigate(ROUTES.SIGNIN) 33 | return 34 | } 35 | // store signin data and redirect to entropy input page 36 | signin(params) 37 | navigate(ROUTES.LOBBY) 38 | } else { 39 | const code = decodeURIComponent(params.code) 40 | switch (code) { 41 | case 'AuthErrorPayload::LobbyIsFull': 42 | navigate(ROUTES.LOBBY_FULL) 43 | return 44 | case 'AuthErrorPayload::UserAlreadyContributed': 45 | setError(t('error.authErrorPayload.userAlreadyContributed')) 46 | break 47 | case 'AuthErrorPayload::InvalidAuthCode': 48 | setError(t('error.authErrorPayload.invalidAuthCode')) 49 | break 50 | case 'AuthErrorPayload::FetchUserDataError': 51 | setError(t('error.authErrorPayload.fetchUserDataError')) 52 | break 53 | case 'AuthErrorPayload::CouldNotExtractUserData': 54 | setError(t('error.authErrorPayload.couldNotExtractUserData')) 55 | break 56 | case 'AuthErrorPayload::UserCreatedAfterDeadline': 57 | setError(t('error.authErrorPayload.userCreatedAfterDeadline')) 58 | break 59 | default: 60 | setError(t('error.authErrorPayload.customError', { error: code })) 61 | break 62 | } 63 | navigate(ROUTES.SIGNIN) 64 | } 65 | })() 66 | }, [ECDSASigner, navigate, setError, signin, t, props.search]) 67 | 68 | return
Signin processing
69 | } 70 | 71 | export default SigninRedirect 72 | -------------------------------------------------------------------------------- /src/assets/eth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | KZG Summoning Ceremony 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /public/wasm/pkg/snippets/wasm-bindgen-rayon-7afa899f36665473/src/workerHelpers.no-bundler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | // This file is kept similar to workerHelpers.js, but intended to be used in 15 | // a bundlerless ES module environment (which has a few differences). 16 | 17 | function waitForMsgType(target, type) { 18 | return new Promise(resolve => { 19 | target.addEventListener('message', function onMsg({ data }) { 20 | if (data == null || data.type !== type) return; 21 | target.removeEventListener('message', onMsg); 22 | resolve(data); 23 | }); 24 | }); 25 | } 26 | 27 | waitForMsgType(self, 'wasm_bindgen_worker_init').then(async data => { 28 | const pkg = await import(data.mainJS); 29 | await pkg.default(data.module, data.memory); 30 | postMessage({ type: 'wasm_bindgen_worker_ready' }); 31 | pkg.wbg_rayon_start_worker(data.receiver); 32 | }); 33 | 34 | // Note: this is never used, but necessary to prevent a bug in Firefox 35 | // (https://bugzilla.mozilla.org/show_bug.cgi?id=1702191) where it collects 36 | // Web Workers that have a shared WebAssembly memory with the main thread, 37 | // but are not explicitly rooted via a `Worker` instance. 38 | // 39 | // By storing them in a variable, we can keep `Worker` objects around and 40 | // prevent them from getting GC-d. 41 | let _workers; 42 | 43 | export async function startWorkers(module, memory, builder) { 44 | const workerInit = { 45 | type: 'wasm_bindgen_worker_init', 46 | module, 47 | memory, 48 | receiver: builder.receiver(), 49 | mainJS: builder.mainJS() 50 | }; 51 | 52 | _workers = await Promise.all( 53 | Array.from({ length: builder.numThreads() }, async () => { 54 | // Self-spawn into a new Worker. 55 | // The script is fetched as a blob so it works even if this script is 56 | // hosted remotely (e.g. on a CDN). This avoids a cross-origin 57 | // security error. 58 | let scriptBlob = await fetch(import.meta.url).then(r => r.blob()); 59 | let url = URL.createObjectURL(scriptBlob); 60 | const worker = new Worker(url, { 61 | type: 'module' 62 | }); 63 | worker.postMessage(workerInit); 64 | await waitForMsgType(worker, 'wasm_bindgen_worker_ready'); 65 | URL.revokeObjectURL(url); 66 | return worker; 67 | }) 68 | ); 69 | builder.build(); 70 | } 71 | -------------------------------------------------------------------------------- /public/module-workers-polyfill.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if(!e||!0!==e._$P){if(e){var n,r=Object.defineProperty({},"type",{get:function(){n=!0}});try{var t=URL.createObjectURL(new Blob([""],{type:"text/javascript"}));new e(t,r).terminate(),URL.revokeObjectURL(t)}catch(e){}if(!n)try{new e("data:text/javascript,",r).terminate()}catch(e){}if(n)return;(self.Worker=function(n,r){return r&&"module"==r.type&&(r={name:n+"\n"+(r.name||"")},n="undefined"==typeof document?location.href:document.currentScript&&document.currentScript.src||(new Error).stack.match(/[(@]((file|https?):\/\/[^)]+?):\d+(:\d+)?(?:\)|$)/m)[1]),new e(n,r)})._$P=!0}"undefined"==typeof document&&function(){var e={},n={};function r(e,n){for(n=n.replace(/^(\.\.\/|\.\/)/,e.replace(/[^/]+$/g,"")+"$1");n!==(n=n.replace(/[^/]+\/\.\.\//g,"")););return n.replace(/\.\//g,"")}var t=[],s=t.push.bind(t);addEventListener("message",s);var a=self.name.match(/^[^\n]+/)[0];self.name=self.name.replace(/^[^\n]*\n/g,""),function t(s,a){var u,o=s;return a&&(s=r(a,s)),e[s]||(e[s]=fetch(s).then((function(a){if((o=a.url)!==s){if(null!=e[o])return e[o];e[o]=e[s]}return a.text().then((function(e){if(!a.ok)throw e;var c={exports:{}};u=n[o]||(n[o]=c.exports);var i=function(e){return t(e,o)},f=[];return e=function(e,n){n=n||[];var r,t=[],a=0;function u(e,n){for(var s,a=/(?:^|,)\s*([\w$]+)(?:\s+as\s+([\w$]+))?\s*/g,u=[];s=a.exec(e);)n?t.push((s[2]||s[1])+":"+s[1]):u.push((s[2]||s[1])+"="+r+"."+s[1]);return u}return(e=e.replace(/(^\s*|[;}\s\n]\s*)import\s*(?:(?:([\w$]+)(?:\s*\,\s*\{([^}]+)\})?|(?:\*\s*as\s+([\w$]+))|\{([^}]*)\})\s*from)?\s*(['"])(.+?)\6/g,(function(e,t,s,o,c,i,f,p){return n.push(p),t+="var "+(r="$im$"+ ++a)+"=$require("+f+p+f+")",s&&(t+=";var "+s+" = 'default' in "+r+" ? "+r+".default : "+r),c&&(t+=";var "+c+" = "+r),(o=o||i)&&(t+=";var "+u(o,!1)),t})).replace(/((?:^|[;}\s\n])\s*)export\s*(?:\s+(default)\s+|((?:async\s+)?function\s*\*?|class|const\s|let\s|var\s)\s*([a-zA-Z0-9$_{[]+))/g,(function(e,n,r,s,u){if(r){var o="$im$"+ ++a;return t.push("default:"+o),n+"var "+o+"="}return t.push(u+":"+u),n+s+" "+u})).replace(/((?:^|[;}\s\n])\s*)export\s*\{([^}]+)\}\s*;?/g,(function(e,n,r){return u(r,!0),n})).replace(/((?:^|[^a-zA-Z0-9$_@`'".])\s*)(import\s*\([\s\S]+?\))/g,"$1$$$2")).replace(/((?:^|[^a-zA-Z0-9$_@`'".])\s*)import\.meta\.url/g,"$1"+JSON.stringify(s))+"\n$module.exports={"+t.join(",")+"}"}(e,f),Promise.all(f.map((function(e){var s=r(o,e);return s in n?n[s]:t(s)}))).then((function(n){e+="\n//# sourceURL="+s;try{var r=new Function("$import","$require","$module","$exports",e)}catch(n){var t=n.line-1,a=n.column,o=e.split("\n"),p=(o[t-2]||"")+"\n"+o[t-1]+"\n"+(null==a?"":new Array(a).join("-")+"^\n")+(o[t]||""),l=new Error(n.message+"\n\n"+p,s,t);throw l.sourceURL=l.fileName=s,l.line=t,l.column=a,l}var m=r(i,(function(e){return n[f.indexOf(e)]}),c,c.exports);return null!=m&&(c.exports=m),Object.assign(u,c.exports),c.exports}))}))})))}(a).then((function(){removeEventListener("message",s),t.map(dispatchEvent)})).catch((function(e){setTimeout((function(){throw e}))}))}()}}(self.Worker); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trusted-setup-frontend", 3 | "version": "0.2.0", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@coinbase/wallet-sdk": "^3.5.4", 8 | "@noble/bls12-381": "^1.4.0", 9 | "@noble/hashes": "^1.1.3", 10 | "@portis/web3": "^4.0.7", 11 | "@spruceid/siwe-web3modal": "^0.1.10", 12 | "@tanstack/react-query": "^4.2.3", 13 | "@testing-library/jest-dom": "^5.14.1", 14 | "@testing-library/react": "^13.0.0", 15 | "@testing-library/user-event": "^13.2.1", 16 | "@tippyjs/react": "^4.2.6", 17 | "@toruslabs/torus-embed": "^1.37.2", 18 | "@types/jest": "^27.0.1", 19 | "@types/node": "^16.7.13", 20 | "@types/react": "^18.0.0", 21 | "@types/react-dom": "^18.0.0", 22 | "@walletconnect/web3-provider": "^1.8.0", 23 | "@web3modal/ethereum": "^2.6.2", 24 | "@web3modal/react": "^2.6.2", 25 | "blockies-identicon": "^0.1.0", 26 | "ethereumjs-abi": "^0.6.8", 27 | "fortmatic": "^2.4.0", 28 | "husky": "^8.0.1", 29 | "i18next": "^21.10.0", 30 | "lint-staged": "^13.0.3", 31 | "node-fetch": "^3.3.0", 32 | "node-polyfill-webpack-plugin": "^2.0.1", 33 | "prettier": "^2.7.1", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-i18next": "^11.18.6", 37 | "react-lottie": "^1.2.3", 38 | "react-modal": "^3.15.1", 39 | "react-router-dom": "^6.4.1", 40 | "react-scripts": "5.0.1", 41 | "react-select": "^5.5.7", 42 | "styled-components": "^5.3.5", 43 | "text-security": "^3.2.1", 44 | "viem": "^1.9.3", 45 | "wagmi": "^1.3.7", 46 | "web-vitals": "^2.1.0", 47 | "zustand": "^4.2.0" 48 | }, 49 | "scripts": { 50 | "start": "craco start", 51 | "build": "craco build", 52 | "test": "craco test", 53 | "eject": "react-scripts eject", 54 | "extract": "babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}'" 55 | }, 56 | "eslintConfig": { 57 | "extends": [ 58 | "react-app", 59 | "react-app/jest" 60 | ] 61 | }, 62 | "browserslist": { 63 | "production": [ 64 | "chrome >= 67", 65 | "edge >= 79", 66 | "firefox >= 68", 67 | "opera >= 54", 68 | "safari >= 14" 69 | ], 70 | "development": [ 71 | "last 1 chrome version", 72 | "last 1 firefox version", 73 | "last 1 safari version" 74 | ] 75 | }, 76 | "lint-staged": { 77 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 78 | "prettier --write" 79 | ] 80 | }, 81 | "husky": { 82 | "hooks": { 83 | "pre-commit": "lint-staged" 84 | } 85 | }, 86 | "devDependencies": { 87 | "@babel/cli": "^7.19.3", 88 | "@babel/core": "^7.20.2", 89 | "@babel/preset-react": "^7.18.6", 90 | "@babel/preset-typescript": "^7.18.6", 91 | "@craco/craco": "^7.0.0", 92 | "@types/react-lottie": "^1.2.6", 93 | "@types/react-modal": "^3.13.1", 94 | "@types/styled-components": "^5.1.26", 95 | "babel-plugin-i18next-extract": "^0.9.0", 96 | "typescript": "^5.2.2" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/components/landing/OtherResources.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { isMobile } from '../../utils' 3 | import { FONT_SIZE } from '../../constants' 4 | import { Trans, useTranslation } from 'react-i18next' 5 | import OpenHugFlower from '../../assets/open-hug-flower.svg' 6 | import ClosedHugFlower from '../../assets/closed-hug-flower.svg' 7 | 8 | const OtherResources = () => { 9 | useTranslation() 10 | const mobile = isMobile() 11 | 12 | const onClickIPFSInterface = () => { 13 | window.open("https://latest.kzgceremony.eth") 14 | } 15 | 16 | const onClickOtherClients = () => { 17 | window.open("https://github.com/ethereum/kzg-ceremony#client-implementations") 18 | } 19 | 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | { mobile ? 27 | Proceed on desktop 28 | : 29 | IPFS 30 | } 31 | { " " } 32 | 33 | closed hug flower icon 34 | 35 | 36 | 37 | 38 | 39 | 40 | CLI Clients 41 | 42 | { " " } 43 | 44 | open hug flower icon 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | const Row = styled.div<{isMobile: boolean}>` 52 | display: flex; 53 | margin-top: 20px; 54 | margin-bottom: 10px; 55 | ${({ isMobile }) => isMobile ? 56 | `overflow-x: scroll; 57 | width: 100%;` 58 | : 'width: 100%;' 59 | } 60 | ` 61 | 62 | const InternalCol = styled.div` 63 | margin: 7px; 64 | padding-block: 5px; 65 | height: auto; 66 | 67 | 68 | :hover:not([disabled]) { 69 | border: solid 1px ${({ theme }) => theme.loader}; 70 | } 71 | ` 72 | 73 | const Col = styled.button` 74 | flex: 4; 75 | width: 100%; 76 | height: auto; 77 | border: none; 78 | background: transparent; 79 | cursor: pointer; 80 | text-align: center; 81 | padding: 5px; 82 | 83 | :hover:not([disabled]) { 84 | z-index: 1; 85 | padding: 4px; /* -1px added from the border in InternalCol */ 86 | box-shadow: 1px 2px 6px 6px #b4b2b2; 87 | background: ${({ theme }) => theme.surface} 88 | } 89 | ` 90 | 91 | const Link = styled.div` 92 | text-decoration-line: underline; 93 | font-family: 'Inter', sans-serif; 94 | font-size: ${FONT_SIZE.SM}; 95 | font-weight: 100; 96 | 97 | ::after { 98 | content: "↗"; 99 | font-size: 0.875em; 100 | } 101 | ` 102 | 103 | export default OtherResources -------------------------------------------------------------------------------- /src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | // Library imports 2 | import { Dispatch, SetStateAction, useMemo } from 'react' 3 | import styled from 'styled-components' 4 | // Component imports 5 | import { ReactComponent as LeftArrowIcon } from '../assets/left-arrow.svg' 6 | // Utility imports 7 | import { isMobile } from '../utils' 8 | 9 | // Constants 10 | const MAX_PAGE_SHORTCUTS = 6 11 | const MAX_PAGE_SHORTCUTS_MOBILE = 4 12 | 13 | // Type declaration and Pagination component 14 | type Props = { 15 | page: number 16 | setPage: Dispatch> 17 | totalPages: number 18 | } 19 | const Pagination = ({ page, setPage, totalPages }: Props) => { 20 | // Memorized values 21 | const pageNumbersToDisplay = useMemo(() => { 22 | const maxPages = isMobile() ? MAX_PAGE_SHORTCUTS_MOBILE : MAX_PAGE_SHORTCUTS 23 | let startPage = Math.max(1, page - Math.floor(maxPages / 2)) 24 | let endPage = Math.min(totalPages, startPage + maxPages - 1) 25 | // If we're at the end, pad to the left 26 | if (endPage === totalPages) { 27 | startPage = Math.max(1, endPage - maxPages + 1) 28 | } 29 | // If we're at the start, pad to the right 30 | if (startPage === 1) { 31 | endPage = Math.min(totalPages, startPage + maxPages - 1) 32 | } 33 | return Array.from({ length: endPage + 1 - startPage }, (_, i) => i + startPage) 34 | }, [page, totalPages]) 35 | 36 | return ( 37 | 38 | 1 ? "visible" : "hidden", "cursor": "pointer" }} 40 | onClick={() => { 41 | setPage(prev => prev - 1) 42 | window.scrollTo({ 43 | top: 0, 44 | behavior: 'smooth', 45 | }) 46 | }} 47 | /> 48 | {pageNumbersToDisplay.map((pageNumber) => ( 49 | { 53 | setPage(pageNumber) 54 | window.scrollTo({ 55 | top: 0, 56 | behavior: 'smooth', 57 | }) 58 | }} 59 | > 60 | {pageNumber} 61 | 62 | ))} 63 | { 66 | setPage(page + 1) 67 | window.scrollTo({ 68 | top: 0, 69 | behavior: 'smooth', 70 | }) 71 | }} 72 | /> 73 | 74 | ) 75 | } 76 | 77 | const PaginationContainer = styled.div` 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | gap: 0.25rem; 82 | margin-top: 4rem; 83 | ` 84 | 85 | const PageIndicator = styled.button<{ active?: boolean }>` 86 | display: grid; 87 | cursor: pointer; 88 | place-items: center; 89 | height: 2rem; 90 | min-width: 2rem; 91 | border-radius: 1rem; 92 | background: ${({ theme }) => theme.surface}; 93 | border: solid ${({ theme }) => theme.text}; 94 | border-width: ${({ active }) => (active ? '1px' : '0px')}; 95 | ` 96 | 97 | const RightArrowIcon = styled(LeftArrowIcon)` 98 | transform: rotate(180deg); 99 | ` 100 | 101 | export default Pagination -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { FONT_SIZE } from '../constants' 3 | import { textSerif } from '../style/utils' 4 | 5 | const Button = styled.button` 6 | cursor: pointer; 7 | /* border-radius: 100px; */ 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | 12 | font-size: ${FONT_SIZE.M}; 13 | font-weight: 600; 14 | 15 | :disabled { 16 | cursor: default; 17 | } 18 | ` 19 | 20 | const PrimaryButtonInner = styled(Button)<{ 21 | variant: string 22 | disabled?: boolean 23 | }>` 24 | background-color: ${({ theme, variant }) => 25 | variant === 'white' ? theme.surface2 : theme.primary}; 26 | color: ${({ theme, disabled }) => (disabled ? theme.disabled : theme.text)}; 27 | border: none; 28 | min-width: 120px; 29 | padding: 8px 24px; 30 | line-height: 20px; 31 | height: 48px; 32 | clip-path: polygon( 33 | 25px 2px, 34 | 3px 50%, 35 | 25px calc(100% - 2px), 36 | calc(100% - 25px) calc(100% - 2px), 37 | calc(100% - 3px) 50%, 38 | calc(100% - 25px) 2px 39 | ); 40 | ${textSerif} 41 | ` 42 | 43 | const PrimaryButtonOuter = styled.div<{ disabled?: boolean }>` 44 | background-color: ${({ theme, disabled }) => 45 | disabled ? theme.disabled : 'black'}; 46 | clip-path: polygon( 47 | 24px 0, 48 | 0 50%, 49 | 24px 100%, 50 | calc(100% - 24px) 100%, 51 | 100% 50%, 52 | calc(100% - 24px) 0 53 | ); 54 | display: inline; 55 | width: fit-content; 56 | ` 57 | 58 | const PrimaryButtonWrapper = styled.div<{ disabled?: boolean }>` 59 | width: fit-content; 60 | display: flex; 61 | transition: all 0.2s ease; 62 | 63 | :hover:not([disabled]) { 64 | filter: drop-shadow(2px 6px 6px #b4b2b2); 65 | } 66 | ` 67 | 68 | const PrimaryButton = ({ disabled, variant, ...props }: any) => { 69 | return ( 70 | 71 | 72 | 73 | 74 | 75 | ) 76 | } 77 | 78 | const PrimaryButtonLarge = styled(PrimaryButton)` 79 | padding: 24px 32px; 80 | min-width: 240px; 81 | font-size: ${FONT_SIZE.L}; 82 | ` 83 | 84 | const BorderedButton = styled(Button)` 85 | background: transparent; 86 | border: solid 1px ${({ theme }) => theme.primary}; 87 | color: ${({ theme }) => theme.primary}; 88 | 89 | min-width: 120px; 90 | ` 91 | 92 | const BorderedButtonLarge = styled(BorderedButton)` 93 | padding: 24px 32px; 94 | border-radius: 38px; 95 | min-width: 240px; 96 | font-size: ${FONT_SIZE.L}; 97 | ` 98 | 99 | const SecondaryButton = styled.button` 100 | border: none; 101 | cursor: pointer; 102 | background: transparent; 103 | 104 | font-size: ${FONT_SIZE.M}; 105 | font-style: italic; 106 | font-weight: 500; 107 | 108 | :disabled { 109 | cursor: default; 110 | } 111 | :hover:not([disabled]) { 112 | filter: drop-shadow(0px 10px 12px #00000226); 113 | } 114 | ${textSerif} 115 | ` 116 | 117 | const ButtonWithLinkOut = styled(SecondaryButton)` 118 | ::after { 119 | content: "↗"; 120 | padding-left: 5px; 121 | font-size: 0.875em; 122 | } 123 | ` 124 | 125 | export { 126 | PrimaryButton, 127 | PrimaryButtonLarge, 128 | BorderedButton, 129 | BorderedButtonLarge, 130 | SecondaryButton, 131 | ButtonWithLinkOut 132 | } 133 | -------------------------------------------------------------------------------- /src/components/landing/Explanation.tsx: -------------------------------------------------------------------------------- 1 | import { isMobile } from '../../utils' 2 | import styled from 'styled-components' 3 | import { TextSection } from '../Layout' 4 | import { Trans, useTranslation } from 'react-i18next' 5 | import { Description, PageTitle } from '../Text' 6 | import ExplanationBorder from '../../assets/explanation-border.svg' 7 | 8 | 9 | const Explanation = ({ refFromLanding }: any) => { 10 | useTranslation() 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | PROTO-DANKSHARDING
AND THE CEREMONY 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | Proto-danksharding (aka EIP-4844) is a planned change to the 26 | Ethereum protocol which introduces ephemeral data storage. 27 | Because the data does not need to be stored by the network forever, 28 | it will be cheaper to use than on-chain storage (i.e. CALLDATA). 29 | Rollups (Layer 2s) can use this storage to post transaction data or 30 | proofs back to Layer 1 (mainnet). The benefits are lower transaction 31 | fees on the L2, greater scalability and more accessibility 32 | to more people! 33 | 34 | 35 | Proto-danksharding requires a new cryptographic scheme: KZG 36 | Commitments. This ceremony, sometimes called a "Trusted Setup", 37 | will generate a structured reference string (SRS) which is needed 38 | for the commitments to work. An SRS is secure as long as at least 39 | one participant in the ceremony successfully conceals their secret. 40 | 41 | 42 | This is a multi-party ceremony: each contributor creates a secret 43 | and runs a computation to mix in with previous contributions. Then, 44 | the output is made public and passed to the next contributor. 45 | To guard against attempts to corrupt the ceremony, participants need an 46 | Ethereum address or GitHub account with an established history to 47 | participate. The final output of the Ceremony will be included in a 48 | future upgrade to help scale the Ethereum network. 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | ) 57 | } 58 | 59 | const Container = styled.div` 60 | display : ${ isMobile() ? '' : 'flex'}; 61 | justify-content: center; 62 | height: 100%; 63 | width: 100%; 64 | ` 65 | 66 | const WhiteBackground = styled.div` 67 | background: white; 68 | width: 100%; 69 | padding-block: 5vh; 70 | padding-inline: 1vw; 71 | display: flex; 72 | flex-direction: column; 73 | align-items: center; 74 | ` 75 | 76 | const SecondSection = styled.section` 77 | width: 64ch; 78 | max-width: 100%; 79 | margin: 0 auto; 80 | margin-bottom: 5rem; 81 | 82 | border: min(4vw,5rem) solid; 83 | border-image-source: url(${ExplanationBorder}); 84 | border-image-slice: 60; 85 | border-image-repeat: round; 86 | 87 | box-sizing: border-box; 88 | ` 89 | 90 | const SecondTextSection = styled(TextSection)` 91 | margin-top: 40px; 92 | width: 55ch; 93 | ` 94 | 95 | export default Explanation 96 | -------------------------------------------------------------------------------- /src/components/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import TranslatorImg from '../assets/translator.svg' 2 | import { useLanguageStore } from '../store/language' 3 | import { BREAKPOINT, FONT_SIZE } from '../constants' 4 | import Select, { StylesConfig } from 'react-select' 5 | import { useTranslation } from 'react-i18next' 6 | import styled, { CSSProperties } from 'styled-components' 7 | import { locales } from '../locales' 8 | import { isMobile } from '../utils' 9 | import theme from '../style/theme' 10 | import { useEffect } from 'react' 11 | 12 | type Props = { 13 | style?: CSSProperties 14 | } 15 | 16 | const LanguageSelector = ({ style = {} }: Props) => { 17 | const { i18n, t } = useTranslation() 18 | const { selectedLanguage, updateSelectedLanguage } = useLanguageStore() 19 | 20 | const handleChange = (event: any) => { 21 | i18n.changeLanguage(event.value) 22 | updateSelectedLanguage(event.value) 23 | } 24 | 25 | const MAIN_COLOR = 'black' 26 | 27 | const getOptions = () => { 28 | const options = Object.keys(locales).map((language) => ({ 29 | value: language, 30 | label: t(`language.${language}`, { lng: language }) 31 | })) 32 | return options 33 | } 34 | 35 | const selectedLanguageToUse = selectedLanguage || 'en' 36 | 37 | useEffect(() => { 38 | if (selectedLanguage){ 39 | i18n.changeLanguage(selectedLanguage) 40 | } 41 | }, [i18n, selectedLanguage]) 42 | 43 | const selectStyles: StylesConfig = { 44 | control: (styles: any) => ({ 45 | ...styles, 46 | boxShadow: 'none !important', 47 | border: 'none !important', 48 | fontSize: FONT_SIZE.M, 49 | alignItems: 'center', 50 | color: MAIN_COLOR, 51 | cursor: 'pointer', 52 | display: 'flex', 53 | width: '125px' 54 | }), 55 | menuList: (styles: any) => ({ 56 | ...styles, 57 | position: 'absolute', 58 | right: isMobile() ? '-55px' : '-110px', 59 | background: '#FFFFFF', 60 | borderRadius: '0px 0px 5px 5px', 61 | minWidth: '135px !important' 62 | }), 63 | indicatorSeparator: (styles: any) => ({ 64 | ...styles, 65 | display: 'none' 66 | }), 67 | dropdownIndicator: (styles: any) => ({ 68 | ...styles, 69 | color: MAIN_COLOR 70 | }), 71 | singleValue: (styles: any) => ({ 72 | ...styles, 73 | color: MAIN_COLOR 74 | }), 75 | option: (styles: any) => ({ 76 | ...styles, 77 | cursor: 'pointer', 78 | backgroundColor: 'transparent', 79 | width: '100% !important', 80 | color: MAIN_COLOR, 81 | ':hover': { 82 | ...styles[':hover'], 83 | backgroundColor: theme.surface 84 | } 85 | }) 86 | } 87 | 88 | return ( 89 | 90 | translator logo 91 | 168 | 169 | 170 | {isLoading ? 171 | 172 | : 173 | 177 | Submit 178 | 179 | } 180 | 181 | 182 | 183 | 184 | 185 | ) 186 | } 187 | 188 | const SubDesc = styled(Description)` 189 | margin: 0 0 15px; 190 | ` 191 | 192 | const Input = styled.input<{ keyEntropy: string }>` 193 | font-family: ${({ keyEntropy }) => 194 | keyEntropy === '' ? 'inherit' : 'text-security-disc'}; 195 | text-align: center; 196 | text-security: disc; 197 | -moz-text-security: disc; 198 | -webkit-text-security: disc; 199 | font-size: ${FONT_SIZE.M}; 200 | margin-top: 3px; 201 | padding: 4px 8px; 202 | border: solid 1px ${({ theme }) => theme.text}; 203 | border-radius: 4px; 204 | background-color: ${({ theme }) => theme.surface}; 205 | min-height: 29px; 206 | width: 300px; 207 | ` 208 | 209 | const ButtonSection = styled(SingleButtonSection)` 210 | margin-top: 12px; 211 | height: auto; 212 | ` 213 | 214 | export default EntropyInputPage 215 | --------------------------------------------------------------------------------