(null)
27 |
28 | if (isLoading) {
29 | return (
30 |
31 | Loading transcript...
32 |
33 |
34 | )
35 | }
36 |
37 | const handleOnClickChevron = () => {
38 | reOrderFormattedData()
39 | setIncreaseOrder(!increaseOrder)
40 | }
41 |
42 | return (
43 |
44 |
45 |
46 | #{' '}
47 | {showChevron ? (
48 |
52 | {'>'}
53 |
54 | ) : (
55 | ''
56 | )}
57 |
58 |
59 |
60 | Participant ID
61 |
62 | Signatures
63 |
64 |
65 | {data.map((record) => (
66 | setSelectedTranscriptItem(record)}
69 | >
70 | {record.position}
71 |
72 | {record.participantName ?? record.participantId}
73 |
74 |
75 | {record.transcripts.map((transcript, i) => (
76 |
84 | ))}
85 |
86 |
87 | ))}
88 | setSelectedTranscriptItem(null)}
91 | onChange={(i: number) => {
92 | let item = data[i]
93 | if (!item) {
94 | for (let j = 0, nj = data.length; j < nj; j++) {
95 | const element = data[j]
96 | if (element.position === i) {
97 | item = element
98 | }
99 | }
100 | }
101 | setSelectedTranscriptItem(item)
102 | }}
103 | />
104 |
105 | )
106 | }
107 |
108 | const Container = styled.div`
109 | margin-top: 20px;
110 | width: 100ch;
111 | max-width: 100%;
112 | `
113 |
114 | const TableHead = styled.div`
115 | border-top: solid 1px ${({ theme }) => theme.text};
116 | padding-top: 35px;
117 | padding-bottom: 45px;
118 | padding-inline: 5px;
119 | display: flex;
120 | height: 60px;
121 | `
122 |
123 | const Chevron = styled.span<{ increaseOrder: boolean }>`
124 | cursor: pointer;
125 | user-select: none;
126 | margin-left: 12px;
127 | ${({ increaseOrder }) =>
128 | increaseOrder
129 | ? css`
130 | transform: rotate(90deg);
131 | height: 18px;
132 | width: 12px;
133 | `
134 | : css`
135 | transform: rotate(270deg);
136 | height: 20px;
137 | width: 10px;
138 | `}
139 | `
140 |
141 | const Row = styled.div`
142 | display: flex;
143 | padding-inline: 5px;
144 | align-items: center;
145 | height: 70px;
146 | border-bottom: solid 1px ${({ theme }) => theme.text};
147 | gap: 1rem;
148 | cursor: pointer;
149 |
150 | :hover:not([disabled]) {
151 | box-shadow: 1px 2px 6px 6px #b4b2b2;
152 | border-bottom: none;
153 | border-right: none;
154 | border-left: none;
155 | }
156 | `
157 |
158 | type ColProps = {
159 | flex?: number
160 | width?: string
161 | atEnd?: boolean
162 | center?: boolean
163 | }
164 |
165 | const Col = styled.div`
166 | ${({ width }) => width && `width: ${width};`}
167 | ${({ flex }) => `flex: ${flex || '1'}`};
168 | font-size: ${FONT_SIZE.M};
169 | display: flex;
170 | ${({ atEnd }) => atEnd && 'justify-content: end'}
171 | ${({ center }) => center && 'justify-content: center'}
172 | `
173 |
174 | const Address = styled.p`
175 | overflow: hidden;
176 | text-overflow: ellipsis;
177 | white-space: nowrap;
178 | `
179 |
180 | export default RecordTable
181 |
--------------------------------------------------------------------------------
/src/components/modals/TranscriptModal.tsx:
--------------------------------------------------------------------------------
1 | import Modal from 'react-modal'
2 | import { Record } from '../../types'
3 | import theme from '../../style/theme'
4 | import styled from 'styled-components'
5 | import { isMobile } from '../../utils'
6 | import { FONT_SIZE } from '../../constants'
7 | import { Bold, Description } from '../Text'
8 | import { textSerif } from '../../style/utils'
9 | import { useEffect } from 'react'
10 | import { Trans, useTranslation } from 'react-i18next'
11 | import BlockiesIdenticon from '../../components/Blockies'
12 |
13 |
14 | type Props = {
15 | record: Record | null
16 | onDeselect: () => void
17 | onChange: (i: number) => void
18 | }
19 |
20 | const TranscriptModal = ({ record, onDeselect, onChange }: Props) => {
21 | const open = !!record
22 | useTranslation()
23 | useEffect(() => {
24 | if (open) document.body.style.overflowY = 'hidden';
25 | else document.body.style.overflowY = 'unset';
26 | }, [open])
27 |
28 | const powers = [12, 13, 14, 15]
29 |
30 | const onArrowClick = (i: number) => {
31 | onChange(record?.position! + i)
32 | }
33 |
34 | return (
35 | <>
36 |
61 |
62 |
63 | CONTRIBUTION DETAILS
64 |
65 |
66 |
67 |
68 |
69 | # {record?.position}
70 |
71 | onArrowClick(-1)}>{'<-'}
72 | onArrowClick(+1)}>{'->'}
73 |
74 |
75 |
76 |
77 |
78 | Participant ID:
79 |
80 |
81 | {record?.participantId}
82 |
83 |
84 |
85 | Powers of Tau Pubkeys:
86 |
87 |
88 |
89 | {record?.transcripts.map((transcript, index) => (
90 |
91 |
98 | {`(2^${powers[index]}): `}{transcript.potPubkeys}
99 |
100 | ))}
101 |
102 | {record?.position === 0 ?
103 | <>
104 |
105 |
106 | What is this is contribution?
107 |
108 |
109 |
110 |
111 | The genesis contribution helps out as a starting point for the entire Ceremony. It does not contain BLS signatures or ECDSA signatures.
112 |
113 |
114 | >
115 | :
116 | <>
117 |
118 |
119 | BLS Signatures:
120 |
121 |
122 |
123 | {record?.transcripts.map((transcript, index) => (
124 | - {transcript.blsSignature}
125 | ))}
126 |
127 | >
128 | }
129 | {record?.participantEcdsaSignature ?
130 | <>
131 |
132 |
133 | ECDSA Signature (optional):
134 |
135 |
136 | {record?.participantEcdsaSignature}
137 | >
138 | :
139 | null
140 | }
141 |
142 | >
143 | )
144 | }
145 |
146 | export const Title = styled.h2`
147 | ${textSerif}
148 | `
149 |
150 | export const SubTitle = styled(Bold)`
151 | ${textSerif}
152 | `
153 |
154 | export const Arrow = styled.div`
155 | cursor: pointer;
156 | padding-right: 4px;
157 |
158 | :hover {
159 | color: #7dbcff;
160 | }
161 | `
162 |
163 | export const ArrowSection = styled.div`
164 | display: inline-flex;
165 | align-items: center;
166 | -webkit-user-select: none; /* Safari */
167 | -ms-user-select: none; /* IE 10 and IE 11 */
168 | user-select: none; /* Standard syntax */
169 | `
170 |
171 | export const Desc = styled(Description)`
172 | word-break: break-all;
173 | font-size: ${FONT_SIZE.S};
174 | margin: 0 0 10px;
175 | `
176 |
177 | export default TranscriptModal
178 |
--------------------------------------------------------------------------------
/src/components/landing/LatestRecords.tsx:
--------------------------------------------------------------------------------
1 | import ROUTES from '../../routes'
2 | import styled from 'styled-components'
3 | import useRecord from '../../hooks/useRecord'
4 | import { useState, useEffect } from 'react'
5 | import { Record, Transcript } from '../../types'
6 | import { PageTitle } from '../Text'
7 | import RecordTable from '../RecordTable'
8 | import { PrimaryButton } from '../Button'
9 | import { useNavigate } from 'react-router-dom'
10 | import { Trans, useTranslation } from 'react-i18next'
11 | import LatestContributionsBorder from '../../assets/latest-contributions-border.svg'
12 | /*
13 | // DISABLING THIS FOR NOW
14 | // Our INFURA NODE reached max limit and it is preventing different wallet providers to signin
15 | // I will activate this again when we find a solution (we need a higher limit in Infura)
16 | =====================================
17 | import { providers, utils } from 'ethers'
18 | import { INFURA_ID } from '../../constants'
19 | */
20 |
21 | const LatestRecords = () => {
22 | useTranslation()
23 | const navigate = useNavigate()
24 | const [isLoading, setIsLoading] = useState(true)
25 | const [formattedData, setFormattedData] = useState([])
26 | // load data from API
27 | const { data } = useRecord()
28 |
29 |
30 |
31 | const onClickViewContributions = () => {
32 | navigate(ROUTES.RECORD)
33 | }
34 |
35 | useEffect(() => {
36 | let active = true
37 | const formatDataFromRecord = async () => {
38 | if (!data) { return }
39 | const { transcripts, participantIds, participantEcdsaSignatures } = data! as Transcript;
40 | const records: Record[] = []
41 | const ni = participantIds.length
42 | for (let i = ni-1; i >= ni-4 && i >=0; i--) {
43 | const participantId = participantIds[i].replace('eth|','')
44 | const participantEcdsaSignature = participantEcdsaSignatures[i]
45 | const record: Record = {
46 | position: i,
47 | participantId,
48 | participantEcdsaSignature,
49 | transcripts: [
50 | {
51 | potPubkeys: transcripts[0].witness.potPubkeys[i],
52 | blsSignature: transcripts[0].witness.blsSignatures[i],
53 | },
54 | {
55 | potPubkeys: transcripts[1].witness.potPubkeys[i],
56 | blsSignature: transcripts[1].witness.blsSignatures[i],
57 | },
58 | {
59 | potPubkeys: transcripts[2].witness.potPubkeys[i],
60 | blsSignature: transcripts[2].witness.blsSignatures[i],
61 | },
62 | {
63 | potPubkeys: transcripts[3].witness.potPubkeys[i],
64 | blsSignature: transcripts[3].witness.blsSignatures[i],
65 | }
66 | ],
67 | }
68 | records.push(record)
69 | }
70 |
71 | /*
72 | // DISABLING THIS FOR NOW
73 | // Our INFURA NODE reached max limit and it is preventing different wallet providers to signin
74 | // I will activate this again when we find a solution (we need a higher limit in Infura)
75 | =====================================
76 | // used to lookup addresses on ens
77 | const provider = new providers.InfuraProvider('homestead', INFURA_ID)
78 | const recordsWithNames = await Promise.all(records.map(async (record) => {
79 | try {
80 | if (!record.participantId || !utils.isAddress(record.participantId)) return record
81 | return { ...record, participantName: await provider.lookupAddress(record.participantId) }
82 | } catch(err) {
83 | console.error(`Failed to query ENS name for ${record.participantId}`, err)
84 | return record
85 | }
86 | }));
87 | */
88 |
89 | if (!active) { return }
90 | setFormattedData(records)
91 | setIsLoading(false)
92 | }
93 | formatDataFromRecord();
94 | return () => { active = false }
95 | }, [data])
96 |
97 | return (
98 |
99 |
100 |
101 |
102 | LATEST CONTRIBUTIONS
103 |
104 |
105 | {}}
110 | />
111 |
112 |
113 | View all contributions
114 |
115 |
116 |
117 |
118 | )
119 | }
120 |
121 | export default LatestRecords
122 |
123 |
124 | const Container = styled.div`
125 | width: 80ch;
126 | max-width: 100%;
127 | margin: 0 auto;
128 | margin-bottom: 5rem;
129 |
130 | border: min(12vw, 7rem) solid;
131 | border-image-source: url(${LatestContributionsBorder});
132 | border-image-slice: 160;
133 | border-image-repeat: round;
134 |
135 | box-sizing: border-box;
136 | `
137 |
138 | const WhiteBackground = styled.div`
139 | background: white;
140 | width: 100%;
141 | padding-block: 5vh;
142 | padding-inline: 5vw;
143 | display: flex;
144 | flex-direction: column;
145 | align-items: center;
146 | `
147 |
148 | const ButtonSection = styled.div`
149 | margin-top: 30px;
150 | `
--------------------------------------------------------------------------------
/src/pages/complete.tsx:
--------------------------------------------------------------------------------
1 | import ROUTES from '../routes'
2 | import styled from 'styled-components'
3 | import { useEffect, useState } from 'react'
4 | import { useNavigate } from 'react-router-dom'
5 | import ErrorMessage from '../components/Error'
6 | import { Trans, useTranslation } from 'react-i18next'
7 | import { Description, PageTitle } from '../components/Text'
8 | import { useContributionStore, Store } from '../store/contribute'
9 | import { ButtonWithLinkOut, PrimaryButton } from '../components/Button'
10 | import HeaderJustGoingBack from '../components/headers/HeaderJustGoingBack'
11 | import ContributionModal from '../components/modals/ContributionModal'
12 | import wasm from '../wasm'
13 | import {
14 | SingleContainer as Container,
15 | SingleWrap as Wrap,
16 | SingleButtonSection,
17 | TextSection,
18 | InnerWrap,
19 | Over,
20 | } from '../components/Layout'
21 | import ShareSocialModal from '../components/modals/ShareSocialModal'
22 |
23 | const CompletePage = () => {
24 | const { t } = useTranslation()
25 | const navigate = useNavigate()
26 | const [error, setError] = useState(null)
27 | const [identity, setIdentity] = useState("")
28 | const [isModalOpen, setIsModalOpen] = useState(false)
29 | const [isSocialModalOpen, setIsSocialModalOpen] = useState(false)
30 | const { receipt, contribution, newContribution, sequencerSignature } = useContributionStore(
31 | (state: Store) => ({
32 | receipt: state.receipt,
33 | contribution: state.contribution,
34 | newContribution: state.newContribution,
35 | sequencerSignature: state.sequencerSignature,
36 | })
37 | )
38 |
39 |
40 | const handleClickShareSocial = () => {
41 | setIsSocialModalOpen(true)
42 | }
43 |
44 | const handleClickViewContribution = async () => {
45 | setIsModalOpen(true)
46 | }
47 |
48 | const handleClickGoToHome = () => {
49 | navigate(ROUTES.ROOT)
50 | }
51 |
52 | useEffect(() => {
53 | (async () => {
54 | if (!contribution || !newContribution){
55 | navigate(ROUTES.ROOT)
56 | return
57 | }
58 | const checks = await wasm.checkContributions(
59 | contribution!,
60 | newContribution!
61 | )
62 | if (!checks.checkContribution){
63 | setError( t('error.pastSubgroupChecksFailed') )
64 | }
65 | if (!checks.checkNewContribution){
66 | setError( t('error.newSubgroupChecksFailed'))
67 | }
68 | })()
69 | // eslint-disable-next-line react-hooks/exhaustive-deps
70 | }, [])
71 |
72 | useEffect(() => {
73 | if (!receipt) return
74 | const { identity } = JSON.parse(receipt!)
75 | setIdentity(identity)
76 | }, [receipt])
77 |
78 | return (
79 | <>
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | Dankshard
draws near
88 |
89 |
90 |
91 |
92 | {error && {error}}
93 |
94 |
95 | Success! Echoes of you are permanently fused with the others
96 | in this Summoning Ceremony. Ceremony credibility is highest
97 | with broad community participation - make sure to share this
98 | with others.
99 |
100 |
101 | The final output of this Ceremony will be used in a future
102 | Ethereum upgrade to enable Danksharding.
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | Share on Social
111 |
112 |
113 |
114 |
115 | View your contribution
116 |
117 |
118 |
119 |
120 | Go back home
121 |
122 |
123 |
124 |
125 |
126 | setIsModalOpen(false)}
132 | />
133 | setIsSocialModalOpen(false)}
137 | />
138 |
139 |
140 |
141 | >
142 | )
143 | }
144 |
145 | const TannedBackground = styled.div`
146 | top: 0px;
147 | z-index: -3;
148 | width: 100%;
149 | height: 100vh;
150 | position: absolute;
151 | background: ${({ theme }) => theme.surface };
152 | `
153 |
154 | export const ButtonSection = styled(SingleButtonSection)`
155 | margin-top: 15px;
156 | height: 120px;
157 | gap: 10px;
158 | `
159 |
160 | export default CompletePage
161 |
--------------------------------------------------------------------------------
/src/pages/lobby.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import ErrorMessage from '../components/Error'
4 | import { Description, PageTitle, Bold } from '../components/Text'
5 | import {
6 | SingleContainer as Container,
7 | SingleWrap as Wrap,
8 | Over,
9 | TextSection,
10 | InnerWrap
11 | } from '../components/Layout'
12 | import { LOBBY_CHECKIN_FREQUENCY, AVERAGE_CONTRIBUTION_TIME } from '../constants'
13 | import useTryContribute from '../hooks/useTryContribute'
14 | import ROUTES from '../routes'
15 | import { useContributionStore, Store } from '../store/contribute'
16 | import { isSuccessRes, sleep } from '../utils'
17 | import { useAuthStore } from '../store/auth'
18 | import HeaderJustGoingBack from '../components/headers/HeaderJustGoingBack'
19 | import useSequencerStatus from '../hooks/useSequencerStatus'
20 | import { Trans, useTranslation } from 'react-i18next'
21 | import styled from 'styled-components'
22 | import { ErrorRes } from '../types'
23 |
24 | const LobbyPage = () => {
25 | const { t } = useTranslation()
26 | const { data } = useSequencerStatus()
27 | const { error, setError } = useAuthStore()
28 | const [showError, setShowError] = useState(error)
29 | const [ lobbySize, setLobbySize] = useState(data?.lobby_size || 0)
30 | const [ chances, setChances] = useState('0.0')
31 |
32 | const tryContribute = useTryContribute()
33 | const updateContribution = useContributionStore(
34 | (state: Store) => state.updateContribution
35 | )
36 | const navigate = useNavigate()
37 |
38 | useEffect(() => {
39 |
40 | async function poll(): Promise {
41 | // periodically post /lobby/try_contribute
42 | let timeToContribute = false
43 | while (!timeToContribute){
44 | const res = await tryContribute.mutateAsync()
45 | if (isSuccessRes(res) && res.hasOwnProperty('contributions')) {
46 | timeToContribute = true
47 | updateContribution(JSON.stringify(res))
48 | navigate(ROUTES.CONTRIBUTING)
49 | return
50 | } else {
51 | const resError = res as ErrorRes
52 | switch (resError.code) {
53 | case 'TryContributeError::RateLimited':
54 | setShowError( t('error.tryContributeError.rateLimited') )
55 | console.log(resError.error)
56 | break
57 | case 'TryContributeError::UnknownSessionId':
58 | setError( t('error.tryContributeError.unknownSessionId') )
59 | console.log(resError.error)
60 | navigate(ROUTES.SIGNIN)
61 | return
62 | case 'TryContributeError::AnotherContributionInProgress':
63 | console.log(resError.error)
64 | break
65 | case 'TryContributeError::LobbyIsFull':
66 | console.log(resError.error)
67 | navigate(ROUTES.LOBBY_FULL)
68 | return
69 | case 'TypeError':
70 | setShowError( t('error.tryContributeError.typeError'))
71 | console.log(resError.error)
72 | break;
73 | default:
74 | // StorageError and TaskError keep you in the lobby until sequencer gets fixed
75 | setShowError( t('error.tryContributeError.unknownError', resError) )
76 | console.log(resError)
77 | break
78 | }
79 | // try again after LOBBY_CHECKIN_FREUQUENCY
80 | await sleep(LOBBY_CHECKIN_FREQUENCY)
81 | setShowError(null)
82 | }
83 | }
84 | }
85 | poll()
86 |
87 | // eslint-disable-next-line react-hooks/exhaustive-deps
88 | }, [])
89 |
90 | useEffect(() => {
91 | setLobbySize(data?.lobby_size || 0)
92 | const slotsInOneHour = (60*60) / AVERAGE_CONTRIBUTION_TIME
93 | let chancesNumber = ((slotsInOneHour / lobbySize) * 100)
94 | if (chancesNumber > 100 || lobbySize === 0){
95 | chancesNumber = 99.9
96 | }
97 | setChances( chancesNumber.toLocaleString('en-US',{maximumFractionDigits: 1}) )
98 | }, [data, lobbySize])
99 |
100 | return (
101 | <>
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | Waiting to be
submitted
110 |
111 |
112 |
113 | {showError && {showError}}
114 |
115 | {lobbySize}
116 |
117 | participants in the lobby
118 |
119 |
120 |
121 | {chances + "%"}
122 |
123 | chance of contributing in the next hour
124 |
125 |
126 |
127 |
128 | Your entropy is ready to be accepted by the Sequencer.
129 | Contributions are chosen randomly from the Lobby.
130 |
131 |
132 | Trying to contribute from multiple tabs may result in errors,
133 | please only use one tab. Leave this guide open with your
134 | computer awake and your contribution will be combined
135 | with the others soon.
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | >
144 | )
145 | }
146 |
147 | const Desc = styled(Description)`
148 | margin-bottom: 5px;
149 | `
150 |
151 | export default LobbyPage
152 |
--------------------------------------------------------------------------------
/src/pages/doubleSign.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { useNavigate } from 'react-router-dom'
3 | import { PrimaryButton } from '../components/Button'
4 | import { Description, PageTitle } from '../components/Text'
5 | import {
6 | SingleContainer as Container,
7 | SingleWrap as Wrap,
8 | SingleButtonSection,
9 | TextSection,
10 | InnerWrap,
11 | Over
12 | } from '../components/Layout'
13 | import { useEntropyStore } from '../store/contribute'
14 | import ROUTES from '../routes'
15 | import { useState, useEffect } from 'react'
16 | import ErrorMessage from '../components/Error'
17 | import { ErrorRes, RequestLinkRes } from '../types'
18 | import { Trans, useTranslation } from 'react-i18next'
19 | import LoadingSpinner from '../components/LoadingSpinner'
20 | import HeaderJustGoingBack from '../components/headers/HeaderJustGoingBack'
21 | import api from '../api'
22 | import { useWeb3Modal } from '@web3modal/react'
23 | import {
24 | useAccount,
25 | useNetwork,
26 | useDisconnect,
27 | useSignTypedData,
28 | useSwitchNetwork
29 | } from 'wagmi'
30 | import { buildEIP712Message } from '../utils'
31 |
32 | const DoubleSignPage = () => {
33 | const [error, setError] = useState(null)
34 | const [isLoading, setIsLoading] = useState(false)
35 | const navigate = useNavigate()
36 | const { t } = useTranslation()
37 | const { potPubkeys } = useEntropyStore()
38 | const { updateECDSASigner, updateECDSASignature } = useEntropyStore()
39 | const { open, close } = useWeb3Modal()
40 | const { chain } = useNetwork()
41 | const { disconnect } = useDisconnect()
42 | const { switchNetwork } = useSwitchNetwork()
43 | const { domain, types, message, primaryType } = buildEIP712Message(potPubkeys)
44 | const { data, signTypedData, reset } = useSignTypedData({
45 | domain,
46 | message,
47 | primaryType,
48 | types
49 | })
50 | const { address, isConnected } = useAccount()
51 |
52 | useEffect(() => {
53 | disconnect()
54 | // eslint-disable-next-line no-restricted-globals
55 | if (self.crossOriginIsolated) {
56 | console.log('refreshing...')
57 | navigate(0)
58 | } else {
59 | console.log(
60 | `${window.crossOriginIsolated ? '' : 'not'} x-origin isolated`
61 | )
62 | }
63 | // eslint-disable-next-line react-hooks/exhaustive-deps
64 | }, [])
65 |
66 | useEffect(() => {
67 | ;(async () => {
68 | if (!data || !address) return
69 | // save signature for later
70 | updateECDSASigner(address)
71 | updateECDSASignature(data)
72 | await onSigninSIWE()
73 | })()
74 | // eslint-disable-next-line react-hooks/exhaustive-deps
75 | }, [data])
76 |
77 | useEffect(() => {
78 | if (address && isConnected) {
79 | if (switchNetwork) {
80 | switchNetwork(1)
81 | }
82 | signTypedData()
83 | } else {
84 | disconnect()
85 | close()
86 | reset()
87 | }
88 | // eslint-disable-next-line react-hooks/exhaustive-deps
89 | }, [address, isConnected])
90 |
91 | const signPotPubkeysWithECDSA = async () => {
92 | //TODO: repeat using different addresses here and in SIWE
93 | disconnect()
94 | await open()
95 | }
96 |
97 | const onSigninSIWE = async () => {
98 | const requestLinks = await api.getRequestLink()
99 | const code = (requestLinks as ErrorRes).code
100 | switch (code) {
101 | case undefined:
102 | window.location.replace((requestLinks as RequestLinkRes).eth_auth_url)
103 | break
104 | case 'AuthErrorPayload::LobbyIsFull':
105 | navigate(ROUTES.LOBBY_FULL)
106 | return
107 | default:
108 | setError(JSON.stringify(requestLinks))
109 | break
110 | }
111 | }
112 |
113 | const handleClickSign = async () => {
114 | setError(null)
115 | setIsLoading(true)
116 | try {
117 | await signPotPubkeysWithECDSA()
118 | } catch (error) {
119 | console.log(error)
120 | setIsLoading(false)
121 | }
122 | }
123 |
124 | return (
125 | <>
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | Bind your
Contribution
134 |
135 |
136 |
137 | {error && {error}}
138 | {chain && chain.name !== 'Ethereum' && (
139 | {t('error.incorrectChainId')}
140 | )}
141 |
142 |
143 | Signing below will bind each Summoner’s entropy contribution
144 | to their Ethereum address. Participants will be redirected
145 | to a "Sign-in with Ethereum" page, and then back to this
146 | interface to complete the final steps of the process.
147 |
148 |
149 |
150 |
151 | {isLoading ? (
152 | <>
153 |
154 |
155 | Check your wallet to sign the contribution
156 |
157 |
158 |
159 | >
160 | ) : (
161 |
162 | Sign
163 |
164 | )}
165 |
166 |
167 |
168 |
169 |
170 | >
171 | )
172 | }
173 |
174 | const CheckWalletDesc = styled(Description)`
175 | margin-bottom: 0px;
176 | font-weight: 700;
177 | `
178 |
179 | const ButtonSection = styled(SingleButtonSection)`
180 | margin-top: 5px;
181 | height: auto;
182 | `
183 |
184 | export default DoubleSignPage
185 |
--------------------------------------------------------------------------------
/src/pages/landing.tsx:
--------------------------------------------------------------------------------
1 | import ROUTES from '../routes'
2 | import { isMobile } from '../utils'
3 | import styled from 'styled-components'
4 | import Footer from '../components/Footer'
5 | import { useRef, useEffect } from 'react'
6 | import { useAuthStore } from '../store/auth'
7 | import { useNavigate } from 'react-router-dom'
8 | import FaqPage from '../components/landing/Faq'
9 | import Header from '../components/headers/Header'
10 | import { TextSection } from '../components/Layout'
11 | import { Trans, useTranslation } from 'react-i18next'
12 | import {
13 | CIRCLE_SIZE,
14 | ENVIRONMENT,
15 | FONT_SIZE,
16 | TRANSCRIPT_HASH
17 | } from '../constants'
18 | import {
19 | Bold,
20 | Description,
21 | ItalicSubTitle,
22 | PageTitle
23 | } from '../components/Text'
24 | import Explanation from '../components/landing/Explanation'
25 | import { BgColoredContainer } from '../components/Background'
26 | import { PrimaryButton } from '../components/Button'
27 | import LatestContributionsBorder from '../assets/latest-contributions-border.svg'
28 |
29 | const LandingPage = () => {
30 | useTranslation()
31 | const ref = useRef(null)
32 | const navigate = useNavigate()
33 | const { signout } = useAuthStore()
34 |
35 | useEffect(() => {
36 | ;(async () => {
37 | signout()
38 | await navigator.serviceWorker.ready
39 | // eslint-disable-next-line no-restricted-globals
40 | if (!self.crossOriginIsolated) {
41 | console.log('refreshing...')
42 | navigate(0)
43 | } else {
44 | console.log(
45 | `${window.crossOriginIsolated ? '' : 'not'} x-origin isolated`
46 | )
47 | }
48 | })()
49 | // eslint-disable-next-line react-hooks/exhaustive-deps
50 | }, [])
51 |
52 | const onClickVerify = () => {
53 | navigate(ROUTES.RECORD)
54 | }
55 |
56 | return (
57 |
58 |
59 |
60 |
61 |
62 |
63 | SUMMONING GUIDES
64 |
65 | {ENVIRONMENT === 'testnet' ? (
66 | ''
67 | ) : (
68 | <>
69 |
70 | The ceremony is over
71 |
72 |
75 | {'Transcript sha256 hash: '}
76 |
77 |
85 | {TRANSCRIPT_HASH}
86 |
87 |
90 |
91 | {' '}
92 |
93 | Total contributions
94 |
95 |
96 |
97 |
105 | {
106 | /*we avoid seq data to load it fast! */
107 | Number(141416).toLocaleString('en-US', {
108 | maximumFractionDigits: 0
109 | })
110 | }{' '}
111 |
112 | >
113 | )}
114 |
115 |
116 |
117 | Whispers from the shadows tell of a powerful spirit Dankshard,
118 | who will open the next chapter of Ethereum scalability.
119 | Contributors have come together to summon its power.
120 |
121 |
122 | Magic math completed - check out your participation in the
123 | ceremony:
124 |
125 |
126 |
127 |
128 | {isMobile() ? (
129 | Proceed on desktop
130 | ) : (
131 | Verify transcript
132 | )}
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | )
141 | }
142 |
143 | const Section = styled.section`
144 | padding: 0 24px;
145 | display: flex;
146 | flex-direction: column;
147 | align-items: center;
148 | `
149 |
150 | const TopSection = styled(Section)`
151 | border: min(12vw, 8rem) solid;
152 | border-image-source: url(${LatestContributionsBorder});
153 | border-image-slice: 150;
154 | border-image-repeat: round;
155 | margin: 6rem auto;
156 | padding: 0px;
157 | box-sizing: border-box;
158 | width: 100%;
159 | max-width: 100ch;
160 | `
161 |
162 | const BgColor = styled.div`
163 | background-color: ${({ theme }) => theme.surface};
164 | height: ${CIRCLE_SIZE}px;
165 | width: ${CIRCLE_SIZE}px;
166 | max-width: 100%;
167 | border-radius: 50%;
168 | box-shadow: 0 0 200px 120px ${({ theme }) => theme.surface};
169 | position: absolute;
170 | z-index: -1;
171 | margin-top: -30px;
172 | `
173 |
174 | const WhiteBackground = styled.div`
175 | background: white;
176 | width: 100%;
177 | padding-block: 5vh;
178 | padding-inline: 5vw;
179 | display: flex;
180 | flex-direction: column;
181 | align-items: center;
182 | `
183 |
184 | export default LandingPage
185 |
--------------------------------------------------------------------------------
/src/pages/contributing.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import styled from 'styled-components'
4 | import {
5 | useContributionStore,
6 | useEntropyStore,
7 | } from '../store/contribute'
8 | import { useAuthStore } from '../store/auth'
9 | import { Description, PageTitle } from '../components/Text'
10 | import { PrimaryButton } from '../components/Button'
11 | import { isSuccessRes, processIdentity } from '../utils'
12 | import ROUTES from '../routes'
13 | import api from '../api'
14 | import HeaderJustGoingBack from '../components/headers/HeaderJustGoingBack'
15 | import { Trans, useTranslation } from 'react-i18next'
16 | import {
17 | SingleContainer as Container,
18 | SingleWrap as Wrap,
19 | Over,
20 | OverRelative,
21 | TextSection,
22 | InnerWrap
23 | } from '../components/Layout'
24 |
25 | type Steps = 'contributing' | 'completed' | 'error'
26 |
27 | const ContributingPage = () => {
28 | const { sessionId, provider, nickname } = useAuthStore()
29 | const { entropy, ECDSASignature } = useEntropyStore()
30 | const {
31 | contribution,
32 | updateReceipt,
33 | updateNewContribution,
34 | updateSequencerSignature,
35 | } = useContributionStore()
36 |
37 | const [step, setStep] = useState('contributing')
38 | const [error, setError] = useState(null)
39 | const navigate = useNavigate()
40 | const { t } = useTranslation()
41 |
42 | useEffect(() => {
43 | (async () => {
44 | try {
45 | if (!sessionId || !contribution) {
46 | throw new Error('invalid sessionId or contribution')
47 | }
48 | const identity = await processIdentity(provider!, nickname!)
49 | const res = await api.contribute(
50 | sessionId!,
51 | contribution!,
52 | entropy!,
53 | identity!,
54 | ECDSASignature
55 | )
56 | if (isSuccessRes(res)) {
57 | setStep('completed')
58 | updateReceipt(res.receipt)
59 | updateNewContribution(res.contribution)
60 | updateSequencerSignature(res.signature)
61 | navigate(ROUTES.COMPLETE)
62 | } else {
63 | console.log(res)
64 | const code = decodeURIComponent(res.code)
65 | switch (code) {
66 | case 'ContributeError::NotUsersTurn':
67 | setError(t('error.contributeError.notUsersTurn'))
68 | break
69 | case 'ContributeError::InvalidContribution::UnexpectedNumContributions':
70 | setError(t('error.contributeError.invalidContribution.unexpectedNumContributions'))
71 | break
72 | case 'ContributeError::Signature::SignatureCreation':
73 | setError(t('error.contributeError.signature.signatureCreation'))
74 | break
75 | case 'ContributeError::Signature::InvalidToken':
76 | setError(t('error.contributeError.signature.invalidToken'))
77 | break
78 | case 'ContributeError::Signature::InvalidSignature':
79 | setError(t('error.contributeError.signature.invalidSignature'))
80 | break
81 | default:
82 | setError(t('error.contributeError.customError', {error: code}))
83 | break
84 | }
85 | setStep('error')
86 | }
87 | } catch (error) {
88 | console.log(error)
89 | setStep('error')
90 | }
91 | })()
92 | // eslint-disable-next-line react-hooks/exhaustive-deps
93 | }, [])
94 |
95 | return (
96 | <>
97 |
98 |
99 |
100 |
101 |
102 |
103 | {step === 'contributing' ? (
104 |
105 |
106 | You have been
107 |
108 | called upon
109 |
110 | Now
111 |
112 |
113 | ) : step === 'completed' ? (
114 |
115 |
116 | Contribution
117 |
118 | Complete
119 |
120 |
121 | ) : (
122 |
123 |
124 | Something
125 |
126 | Went
127 |
128 | Wrong
129 |
130 |
131 | )}
132 |
133 | {step === 'contributing' ? (
134 | <>
135 |
136 |
137 | You are now entrusted with the Powers of Tau. Your
138 | Secret, Sigil and Sample are being fused with those
139 | that came before.
140 |
141 |
142 | Rituals cannot be hastened - time given here creates
143 | timeless artifacts.
144 |
145 |
146 | >
147 | ) : step === 'completed' ? (
148 |
149 |
150 | You have just successfully complete the contribution. Don’t
151 | forget to return for the summoning ending & spread the
152 | word.
153 |
154 |
155 | ) : (
156 |
157 | { t('contributing.description.error', {error: error}) }
158 |
159 | )}
160 |
161 | {step === 'completed' && (
162 |
163 |
164 | View my contribution
165 |
166 |
167 | )}
168 |
169 |
170 |
171 |
172 |
173 | >
174 | )
175 | }
176 |
177 | const ContainerR = styled(Container)<{ complete: boolean }>`
178 | transition: all 2s linear;
179 | ${({ complete, theme }) =>
180 | complete ? `background-color: ${theme.surface};` : ''}
181 | `
182 |
183 | export default ContributingPage
184 |
--------------------------------------------------------------------------------
/src/pages/entropyInput.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useState,
3 | MouseEventHandler,
4 | useEffect,
5 | ChangeEventHandler
6 | } from 'react'
7 | import wasm from '../wasm'
8 | import styled from 'styled-components'
9 | import { useNavigate } from 'react-router-dom'
10 | import { PrimaryButton } from '../components/Button'
11 | import { Description, PageTitle, Bold } from '../components/Text'
12 | import { useEntropyStore } from '../store/contribute'
13 | import {
14 | SingleContainer as Container,
15 | SingleWrap as Wrap,
16 | SingleButtonSection,
17 | TextSection,
18 | Over
19 | } from '../components/Layout'
20 | import ROUTES from '../routes'
21 | import SnakeProgress from '../components/SnakeProgress'
22 | import HeaderJustGoingBack from '../components/headers/HeaderJustGoingBack'
23 | import { CURVE } from '@noble/bls12-381'
24 | import { hkdf } from '@noble/hashes/hkdf'
25 | import { sha256 } from '@noble/hashes/sha256'
26 | import { randomBytes } from '@noble/hashes/utils'
27 | import { Trans, useTranslation } from 'react-i18next'
28 | import { MIN_MOUSE_ENTROPY_SAMPLES, FONT_SIZE } from '../constants'
29 | import 'text-security'
30 | import LoadingSpinner from '../components/LoadingSpinner'
31 | import { isSafari } from '../utils'
32 | import ErrorMessage from '../components/Error'
33 |
34 | type Player = {
35 | play: () => void
36 | pause: () => void
37 | seek: (percent: number) => void
38 | }
39 |
40 | const EntropyInputPage = () => {
41 | const { t } = useTranslation()
42 | const navigate = useNavigate()
43 | const [showError, setShowError] = useState('')
44 | const [isLoading, setIsLoading] = useState(false)
45 | const [keyEntropy, setKeyEntropy] = useState('')
46 | const [mouseEntropy, setMouseEntropy] = useState('')
47 | const [lastMouseEntropyUpdate, setLastMouseEntropyUpdate] = useState(0)
48 | const [mouseEntropyRandomOffset, setMouseEntropyRandomOffset] = useState(0)
49 | const [mouseEntropySamples, setMouseEntropySamples] = useState(0)
50 | const [percentage, setPercentage] = useState(0)
51 | const [player, setPlayer] = useState(null)
52 |
53 | const { updateEntropy, updatePotPubkeys } = useEntropyStore()
54 | const handleSubmit = async () => {
55 | if (percentage !== 100) return
56 | setIsLoading(true)
57 | await processGeneratedEntropy()
58 | navigate(ROUTES.SIGNIN)
59 | }
60 |
61 | const handleCaptureMouseEntropy: MouseEventHandler = (e) => {
62 | /*
63 | Mouse entropy is based off of recording the position and precise timing of mouse movements.
64 | Entropy is only collected every ~128 ms to help reduce correlated mouse locations. The precise period is sampled
65 | from ~ U[0, 255] to increase sample periodicity variance.
66 | performance.now() is used for timestamps due to its sub-millisecond resolution in some browsers.
67 | */
68 | if (performance.now() - lastMouseEntropyUpdate > mouseEntropyRandomOffset) {
69 | setLastMouseEntropyUpdate(performance.now())
70 | setMouseEntropyRandomOffset(randomBytes(1)[0])
71 | setMouseEntropySamples(mouseEntropySamples + 1)
72 | setMouseEntropy(
73 | `${mouseEntropy}${e.movementX}${e.movementY}${e.screenX}${
74 | e.screenY
75 | }${performance.now()}`
76 | )
77 | }
78 | }
79 |
80 | const handleCaptureKeyEntropy: ChangeEventHandler = (e) => {
81 | setKeyEntropy(`${keyEntropy}${e.target.value}${performance.now()}`)
82 | }
83 |
84 | const processGeneratedEntropy = async () => {
85 | const entropyGenerated = mouseEntropy + keyEntropy
86 | const entropyGeneratedAsBytes = Uint8Array.from( entropyGenerated.split('').map((x) => x.charCodeAt(0)) )
87 | const entropyRandomAsBytes = randomBytes(32)
88 | const entropyAsBytes = new Uint8Array(entropyGeneratedAsBytes.length + entropyRandomAsBytes.length)
89 | entropyAsBytes.set(entropyGeneratedAsBytes)
90 | entropyAsBytes.set(entropyRandomAsBytes, entropyGeneratedAsBytes.length)
91 | /*
92 | In order to reduce modulo-bias in the entropy (w.r.t. the curve order):
93 | it is expanded out (and mixed) to at least 48 bytes before being reduced mod curve order.
94 | This exact technique is the RECOMMENDED means of obtaining a ~uniformly random F_r element according to
95 | the IRTF BLS signature specs: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05#section-2.3
96 | */
97 | const salt = randomBytes(32)
98 | const expandedEntropy = hkdf(sha256, entropyAsBytes, salt, '', 48)
99 |
100 | const hex96 = expandedEntropy.reduce(
101 | (str, byte) => str + byte.toString(16).padStart(2, '0'),
102 | ''
103 | )
104 | const expandedEntropyInt = BigInt('0x' + hex96)
105 | const secretInt = expandedEntropyInt % CURVE.r
106 | const secretHex = secretInt.toString(16).padStart(64, '0')
107 | const potPubkeys = await wasm.getPotPubkeys(secretHex)
108 |
109 | updateEntropy(secretHex)
110 | updatePotPubkeys(potPubkeys)
111 | }
112 |
113 | useEffect(() => {
114 | if ( isSafari() ){
115 | setShowError(t('error.safariNotAllowed'))
116 | }
117 | }, [t])
118 |
119 | useEffect(() => {
120 | // MIN_MOUSE_ENTROPY_SAMPLES Chosen to target 128 bits of entropy, assuming 2 bits added per sample.
121 | const percentage = Math.min(
122 | Math.floor((mouseEntropySamples / MIN_MOUSE_ENTROPY_SAMPLES) * 100),
123 | 100
124 | )
125 | setPercentage(percentage)
126 | if (player) player.seek(percentage)
127 | // eslint-disable-next-line react-hooks/exhaustive-deps
128 | }, [mouseEntropy])
129 |
130 | return (
131 | <>
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | Entropy
Entry
140 |
141 |
142 |
143 | {showError && {showError}}
144 |
145 |
146 | The Ceremony requires three random inputs from each Summoner.
147 |
148 |
149 | Secret: Enter a piece of you: a hope for the future,
150 | or the name of someone dear. Be sure to add random characters
151 | - it's important to not remember the exact text.
152 |
153 |
154 | Sigil: Trace some elements of the guide with your
155 | cursor - the interface will capture your unique path.
156 |
157 |
158 | Sample: Your browser will generate its own
159 | randomness in the background.
160 |
161 |
162 |
163 |
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 |
--------------------------------------------------------------------------------