Refactor packages and update readme (#94)

This commit is contained in:
Szymon Szlachtowicz 2021-09-24 02:57:44 +02:00 committed by GitHub
parent 861feccd67
commit 77e242a110
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 502 additions and 343 deletions

158
README.md
View File

@ -1,2 +1,156 @@
# status-waku-voting
Repository for decentralised voting over waku
# DappConnect Voting & Polling SDKs
A set of DappConnect SDKs to enable gas efficient voting and gasless polling over Waku
## DappConnect Voting SDK
The DappConnect voting SDK allows you to leverage Waku to save gas fees for most voters.
It leverages Waku, a decentralized communication network, to broadcast and aggregates votes.
Most token holders will not need to spend gas to vote.
Only the party that starts an election and submit the end results need to interact with the blockchain.
This can be used for a DAO for example:
- The DAO or a party that needs a proposal voted spend gas,
- The token holders do not spend gas when voting.
### Functional Overview
#### Setting up voting contract
The voting contract needs to be deployed on the blockchain and takes the following input:
- The ERC20 token contract address: Only holder of these tokens will be able to vote.
- Duration of an election: Elections expire after a fixed time.
#### Starting an election
An election can be started by any token holder,
therefore referred to as _moderator_.
An election needs to be started on chain,
the moderator starts the election by setting a question and description.
The moderator also casts the first vote in favour.
#### Running an election
All token holder can cast weighted votes without spending gas.
Votes are sent over Waku and each user can aggregate votes to learn the election forecast.
#### Submitting votes
The _moderator_, or any user, can aggregate the votes and submit them to the contract, spending gas.
Votes can be submitted in several steps, by different users.
This allows any user to submit missing or omitted votes to the blockchain to ensure that the result matches the actual opinion of the token holders.
#### Election end
The election ends when the expiry is reached.
_Moderator_ and token holders need to ensure they submit all votes to the blockchain before the expiry.
Once the expiry is reached, the result cannot be changed.
### Security considerations
#### 1. Who can start an election?
Anyone holding any amount of the specified ERC-20 token can start an election.
#### 2. Who can vote?
Anyone holding any amount of the specified ERC-20 token can vote.
#### 3. How are votes weighted?
Votes are weighted with a an amount of ERC-20 token specified by the voter.
The voter can only vote with the amount of token they hold at the time of submission of the vote to the blockchain.
#### 4. Can votes be re-used?
A vote from a given account MUST only count against a given election for a given voting contract.
#### 5. Can the result be manipulated?
Votes cannot be forged as a voter needs to sign their vote, specific to a given election and voting smart contract,
with their Ethereum account.
A user submitting votes to the contract can accidentally or voluntarily omit votes.
However, any user can do any number of calls to submit votes to the contract.
Hence, if a user is contesting the result because they are aware of votes that are not yet submitted then they can submit said votes themselves.
If Alice starts an election and Bob wishes for the election result to be _no_.
Bob should vote _no_, aggregate _no_ votes and submit them in a timely fashion with appropriate gas fees to ensure all _no_ votes are recorded.
Neither Alice nor Bob should _wait to see_ which way the election is going (by monitoring the Waku network) before deciding to submit votes.
Indeed, if one wishes to wait the last minute to submit vote then there is a risk that the votes do not get mined, and hence counted, in the blockchain in time.
#### 6. Can Alice stop Bob submitting votes to the contract?
Alice cannot stop Bob submitting votes to the contract that are valid and not yet submitted.
Anyone can submit valid votes to the contract.
#### 7. Can a vote be replaced?
Once a vote is submitted (and mined) to the smart contract, it is not possible to submit another vote cast by the same token holder
(in terms of Ethereum account).
Between the time a vote is published over Waku, and **not** yet mined in the blockchain, the vote could be replaced in several ways:
- By publishing a new vote over Waku, the dApp will replace the most recent vote, allowing voters to change their mind on both the vote value and the token weight that backs the vote,
- By submitting a new vote to the blockchain, before another party submit their previous vote,
- By submitting a new vote to the blockchain with a higher gas fee to race their previous vote in a block.
#### 8. Can a vote be cancelled?
A vote can be replaced as described [above](#7-can-a-vote-be-replaced).
Once a vote is published over the Waku network and another peer received the vote,
there is no control to stop the gossiping of the vote.
Once another user receives the vote, they can submit it to the blockchain.
The only way to annul a vote is to reduce one's token balance to make the vote invalid.
For example, if Alice has 10,000 tokens and votes with all 10,000 tokens on Monday.
She decides to cancel her vote, before it is submitting to the blockchain,
by transferring 1000 tokens out of her account on Tuesday, leaving her account with 9000 tokens.
When Bob submit Alice's vote on Wednesday, the vote will not be valid anymore and will be rejected.
This way, Alice effectively cancelled her vote, as long as her token transfer get mined before the vote submission.
#### 9. Can an election be cancelled or annulled?
It is not possible to cancel or annul an election.
Once an election is started, it will automatically end once the expiry is reached.
The only event that terminates and election is the mining a block with a timestamp greater than the election end.
The election always contains one _yes_ vote from the _election moderator_ meaning that if no vote are submitted to the blockchain then
the "default" result is _yes_.
Participants and moderators must ensure they submit votes early enough,
with appropriate gas fees,
for those votes to be mined before the expiry.
#### 10. When do I need to hold tokens to back my votes?
The token balance of the voters is checked when the votes are mined in the blockchain.
Which means that the voter needs to hold the number of tokens they voted for until the vote is mined.
Failing to doing so could result in the vote being cancelled.
See [8. Can a vote be cancelled?](#8-can-a-vote-be-cancelled) for details.
#### 11. Can a submission of votes fail? If so, what happens?
If Alice attempts to submit multiple votes to the smart contracts and one vote is invalid, then the full transaction will revert and no vote will be submitted
Alice MUST verify that the votes are valid just before submitting them to the blockchain.
**Additionally**, but not instead of, Alice MAY verify that is a vote is valid when receiving it.
A vote may become invalid between reception over Waku and submission to the smart contract because:
- The voter has transferred a number of token out of their account, making the account balance lesser than the vote weight,
- A vote for this voter has already been submitted to the smart contract (whether or not it has the same value).
## DappConnect Polling SDK
TODO

View File

@ -53,7 +53,7 @@ export class WakuMessaging {
wakuMessagesSetup.forEach((setupData) => {
this.wakuMessages[setupData.name] = {
topic: `/${this.appName}/0.1/${setupData.name}/proto/`,
topic: `/${this.appName}/0.0.1/${setupData.name}/proto/`,
tokenCheckArray: setupData.tokenCheckArray,
hashMap: {},
arr: [],

View File

@ -16,7 +16,7 @@
},
"dependencies": {
"@status-waku-voting/polling-example": "^0.1.0",
"@status-waku-voting/proposal-example": "^0.1.0",
"@status-waku-voting/voting-example": "^0.1.0",
"assert": "^2.0.0",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",

View File

@ -1,7 +1,7 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { PollingPage } from '@status-waku-voting/polling-example'
import { ProposalPage } from '@status-waku-voting/proposal-example'
import { VotingPage } from '@status-waku-voting/voting-example'
import { BrowserRouter } from 'react-router-dom'
import { Route, Switch } from 'react-router'
@ -10,7 +10,7 @@ ReactDOM.render(
<BrowserRouter>
<Switch>
<Route exact path="/polling" component={PollingPage} />
<Route exact path="/proposal" component={ProposalPage} />
<Route exact path="/voting" component={VotingPage} />
</Switch>
</BrowserRouter>
</div>,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -37,6 +37,7 @@ export function Polling() {
<Wrapper>
<TopBar
logo={pollingIcon}
logoWidth={84}
title={'Polling Dapp'}
theme={orangeTheme}
activate={activateBrowserWallet}

View File

@ -1,133 +0,0 @@
import { blueTheme } from '@status-waku-voting/react-components/dist/esm/src/style/themes'
import React, { useState } from 'react'
import styled from 'styled-components'
import { useHistory } from 'react-router'
import { ProposingBtn } from '../Buttons'
import { CardHeading, CardText } from '../ProposalInfo'
import {
InfoText,
Label,
ProposingData,
ProposingInfo,
ProposingInput,
ProposingTextInput,
} from '../newVoteModal/ProposeModal'
import { VotePropose } from '../VotePropose'
import { BigNumber } from 'ethers'
import { WakuVoting } from '@status-waku-voting/core'
interface ProposeMobileProps {
availableAmount: number
wakuVoting: WakuVoting
}
export function ProposeMobile({ availableAmount, wakuVoting }: ProposeMobileProps) {
const insufficientFunds = availableAmount < 10000
const [proposingAmount, setProposingAmount] = useState(0)
const [title, setTitle] = useState('')
const [text, setText] = useState('')
const [customData, setCustomData] = useState(false)
const history = useHistory()
return (
<ProposingDataMobile>
{insufficientFunds && (
<ProposingInfo>
<span></span>
<InfoText>You need at least 10,000 {wakuVoting.tokenSymbol} to create a proposal!</InfoText>
</ProposingInfo>
)}
<ProposingCardHeading>Create proposal</ProposingCardHeading>
{!customData && (
<ProposingCustomData>
<Label>
Title
<ProposingInput
cols={2}
maxLength={90}
placeholder="E.g. Change the rate of the token issuance"
value={title}
onInput={(e) => {
setTitle(e.currentTarget.value)
}}
required
/>
</Label>
<Label>
Description
<ProposingTextInput
maxLength={440}
placeholder="Describe your proposal as detailed as you can in 440 characters."
value={text}
onInput={(e) => {
setText(e.currentTarget.value)
}}
required
/>
</Label>
<ProposingBtn
disabled={!text || !title || insufficientFunds}
theme={blueTheme}
onClick={() => setCustomData(true)}
>
Continue
</ProposingBtn>
</ProposingCustomData>
)}
{customData && (
<ProposingCustomData>
<CustomProposingHeading>{title}</CustomProposingHeading>
<ProposingCardText>{text}</ProposingCardText>
<VoteProposeWrap>
<VotePropose
availableAmount={availableAmount}
setProposingAmount={setProposingAmount}
proposingAmount={proposingAmount}
wakuVoting={wakuVoting}
/>
</VoteProposeWrap>
<ProposingBtn
disabled={proposingAmount === 0}
onClick={async () => {
await wakuVoting.createVote(title, text, BigNumber.from(proposingAmount))
history.push(`/proposal`), setTitle(''), setText('')
}}
>
Create proposal
</ProposingBtn>
</ProposingCustomData>
)}
</ProposingDataMobile>
)
}
const ProposingDataMobile = styled(ProposingData)`
padding: 88px 16px 32px;
margin-top: 0;
`
export const VoteProposeWrap = styled.div`
margin-bottom: 32px;
width: 100%;
`
const ProposingCustomData = styled.div`
width: 100%;
`
const ProposingCardHeading = styled(CardHeading)`
margin-bottom: 16px;
`
const CustomProposingHeading = styled(ProposingCardHeading)`
font-size: 22px;
margin-top: 24px;
`
const ProposingCardText = styled(CardText)`
margin-bottom: 0;
`

View File

@ -1,22 +0,0 @@
import { ProposeMobile } from './components/mobile/ProposeMobile'
import { ProposalVoteMobile } from './components/mobile/ProposalVoteMobile'
import { VotingEmpty } from './components/VotingEmpty'
import { VoteModal } from './components/VoteModal/VoteModal'
import { ProposalList } from './components/ProposalList'
import { ProposalCard } from './components/ProposalCard'
import { ProposalHeader } from './components/ProposalHeader'
import { NotificationItem } from './components/NotificationItem'
import { NewVoteModal } from './components/newVoteModal/NewVoteModal'
export {
VotingEmpty,
VoteModal,
ProposalList,
ProposalCard,
ProposalHeader,
NotificationItem,
NewVoteModal,
ProposeMobile,
ProposalVoteMobile,
}

View File

@ -1,3 +0,0 @@
Package contains example usage of voting sdk
For connecting with web3 api usedapp is used please refer to usedapp docs for more info

View File

@ -1,14 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M23.3333 31.9999H0.666667C0.298667 31.9999 0 31.7012 0 31.3332C0 30.9652 0.298667 30.6665 0.666667 30.6665H23.3333C23.7013 30.6665 24 30.9652 24 31.3332C24 31.7012 23.7013 31.9999 23.3333 31.9999Z" fill="black"/>
<path d="M28.4908 12.9173C28.4881 12.9173 28.4868 12.9173 28.4841 12.9173C27.9481 12.9159 27.4455 12.7039 27.0681 12.3239L19.7028 4.8746C18.9321 4.0946 18.9361 2.82926 19.7121 2.0546L21.1788 0.58793C21.9575 -0.19207 23.2281 -0.19207 24.0068 0.58793L31.4148 7.9946C32.1948 8.7746 32.1948 10.0439 31.4148 10.8239L29.9055 12.3333C29.5268 12.7093 29.0255 12.9173 28.4908 12.9173ZM22.5921 1.33326C22.4215 1.33326 22.2508 1.39726 22.1201 1.52793L20.6535 2.9946C20.3948 3.25326 20.3935 3.6746 20.6495 3.9346L28.0148 11.3839C28.1401 11.5106 28.3081 11.5813 28.4868 11.5813C28.6415 11.5426 28.8335 11.5133 28.9601 11.3853L30.4695 9.87726C30.7295 9.61726 30.7295 9.1946 30.4695 8.93326L23.0615 1.5266C22.9335 1.3986 22.7628 1.33326 22.5921 1.33326ZM20.1841 2.52393H20.1975H20.1841Z" fill="black"/>
<path d="M7.80551 16.7814C7.51885 16.7814 7.25351 16.5947 7.16685 16.3067C6.93618 15.5401 6.81885 14.7441 6.81885 13.9414C6.81885 12.6814 7.12018 11.4094 7.68685 10.2601C8.76018 8.11473 10.7162 6.54406 13.0548 5.94806L20.3482 4.09472C20.7082 3.99872 21.0668 4.22006 21.1575 4.57606C21.2482 4.93206 21.0322 5.29606 20.6762 5.38539L13.3828 7.23872C11.4202 7.74006 9.78018 9.05739 8.88151 10.8534C8.40551 11.8174 8.15218 12.8854 8.15218 13.9414C8.15218 14.6134 8.24951 15.2801 8.44285 15.9241C8.54951 16.2761 8.34951 16.6481 7.99618 16.7547C7.93218 16.7721 7.86818 16.7814 7.80551 16.7814Z" fill="black"/>
<path d="M14.9786 25.9893C13.9066 25.9893 12.9386 25.4373 12.3879 24.5133C11.6852 23.332 11.9119 21.7853 12.9386 20.7533L17.6466 16.0373C17.0866 15.3667 16.2652 15 15.3639 15.0453C14.4132 15.0947 13.5626 15.6187 13.0906 16.4467C12.9052 16.768 12.4959 16.876 12.1799 16.6947C11.8612 16.512 11.7492 16.1053 11.9306 15.7853C12.6292 14.5627 13.8866 13.788 15.2932 13.7133C16.6946 13.6373 18.0319 14.2787 18.8559 15.4227L19.0639 15.712C19.2506 15.972 19.2266 16.3293 19.0052 16.5613L13.8799 21.6947C13.2932 22.2853 13.1452 23.184 13.5306 23.8307C14.0759 24.7493 15.4386 24.9027 16.1852 24.1573L21.9746 18.2773L26.6159 11.1253C26.8172 10.816 27.2306 10.7293 27.5372 10.9293C27.8452 11.1293 27.9332 11.5427 27.7319 11.8507L23.0532 19.06C23.0292 19.0987 23.0012 19.1333 22.9692 19.1667L17.1306 25.0973C16.5572 25.6733 15.7932 25.9893 14.9786 25.9893Z" fill="black"/>
<path d="M20.6892 32H3.33317C2.96517 32 2.6665 31.7013 2.6665 31.3333V16.6667C2.6665 16.2987 2.96517 16 3.33317 16H17.9612C18.3292 16 18.6278 16.2987 18.6278 16.6667C18.6278 17.0347 18.3292 17.3333 17.9612 17.3333H3.99984V30.6667H20.0225V20.536C20.0225 20.168 20.3212 19.8693 20.6892 19.8693C21.0572 19.8693 21.3558 20.168 21.3558 20.536V31.3333C21.3558 31.7013 21.0572 32 20.6892 32Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,44 +0,0 @@
import React from 'react'
import { Redirect, Route, Switch } from 'react-router'
import { BrowserRouter } from 'react-router-dom'
import styled from 'styled-components'
import { ProposalVoteMobile, ProposeMobile } from '@status-waku-voting/proposal-components'
import { Proposal } from './Proposal'
import { WakuVoting } from '@status-waku-voting/core'
import { useTokenBalance } from '@status-waku-voting/react-components'
type ProposalMobileProps = {
wakuVoting: WakuVoting
account: string | null | undefined
}
export function ProposalMobile({ wakuVoting, account }: ProposalMobileProps) {
const tokenBalance = useTokenBalance(account, wakuVoting)
return (
<BrowserRouter>
<ProposalWrapper>
<Switch>
<Route exact path="/votingRoom/:id">
<ProposalVoteMobile wakuVoting={wakuVoting} availableAmount={tokenBalance} account={account} />
</Route>
<Route exact path="/creation">
<ProposeMobile availableAmount={tokenBalance} wakuVoting={wakuVoting} />
</Route>
<Route exact path="/proposal">
<Proposal wakuVoting={wakuVoting} account={account} />
</Route>
</Switch>
</ProposalWrapper>
</BrowserRouter>
)
}
const ProposalWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
max-width: 1000px;
margin: 0 auto;
width: 100%;
min-height: 100vh;
`

View File

@ -13,16 +13,17 @@ type TopBarProps = {
activate: () => void
deactivate: () => void
account: string | undefined | null
logoWidth?: number
}
export function TopBar({ logo, title, theme, activate, deactivate, account }: TopBarProps) {
export function TopBar({ logo, title, theme, activate, deactivate, account, logoWidth }: TopBarProps) {
const [isOpened, setIsOpened] = useState(false)
const [selectConnect, setSelectConnect] = useState(false)
return (
<Wrapper theme={theme}>
<ContentWrapper>
<Logo src={logo} />
<Logo src={logo} style={{ width: `${logoWidth ?? 32}px` }} />
<TitleWrapper>
{title.split(' ').map((text, idx) => (
<div key={idx}>{text}</div>

View File

@ -1,8 +1,8 @@
Package that contains react components for usage with WakuVoting
-`VotingEmpty` component showing that a list is empty with button. When user is connected button calls `onCreateClick` otherwise call `onConnectClick`.
-`VotingRoomListEmpty` component showing that a list is empty with button. When user is connected button calls `onCreateClick` otherwise call `onConnectClick`.
type VotingEmptyProps = {
type VotingRoomListEmptyProps = {
theme: Theme
account: string | null | undefined
onCreateClick: () => void // callback when user is connected
@ -22,23 +22,23 @@ export interface VoteModalProps {
className?: string // defined to show mobile and tablet versions can be 'mobile' or 'tablet
}
-`ProposalList` shows a list with all voting rooms on blockchain
-`VotingRoomList` shows a list with all voting rooms on blockchain
```
type ProposalListProps = {
type VotingRoomListProps = {
theme: Theme
wakuVoting: WakuVoting
votes: number[] // array of ids of voting rooms to show
availableAmount: number // available token amount of user
account: string | null | undefined // address of user account
mobileOnClick?: (votingRoom: VotingRoom) => void // callback when user clicks proposal card when list is in mobile mode
mobileOnClick?: (votingRoom: VotingRoom) => void // callback when user clicks voting room card when list is in mobile mode
}
```
-`ProposalCard` a card used to show voting room
-`VotingRoomCard` a card used to show voting room
```
interface ProposalCardProps {
interface VotingRoomCardProps {
votingRoomId: number // id of voting room to show
theme: Theme
availableAmount: number // token balance of current user
@ -64,10 +64,10 @@ export interface VoteModalProps {
}
```
-`ProposalHeader` a list header with button. When user is connected button calls `onCreateClick` otherwise call `onConnectClick`.
-`VotingRoomListHeader` a list header with button. When user is connected button calls `onCreateClick` otherwise call `onConnectClick`.
```
type ProposalHeaderProps = {
type VotingRoomListHeaderProps = {
theme: Theme
account: string | null | undefined // address of user account
onCreateClick: () => void // callback when user is connected
@ -75,10 +75,10 @@ type ProposalHeaderProps = {
}
```
-`NewVoteModal` modal that lets people create new voting room.
-`NewVotingRoomModal` modal that lets people create new voting room.
```
type NewVoteModalProps = {
type NewVotingRoomModalProps = {
theme: Theme
showModal: boolean // state defining whether to show modal
setShowModal: (val: boolean) => void // function that updates showModal state
@ -87,19 +87,19 @@ type NewVoteModalProps = {
}
```
-`ProposeMobile` component for smaller width screen that allows to create new voting room
-`NewVotingRoomMobile` component for smaller width screen that allows to create new voting room
```
interface ProposeMobileProps {
interface NewVotingRoomMobileProps {
availableAmount: number // token balance of user
wakuVoting: WakuVoting
}
```
-`ProposalVoteMobile` voting room information for smaller width with ability to vote on given voting room
-`VotingRoomMobile` voting room information for smaller width with ability to vote on given voting room
```
interface ProposalVoteMobileProps {
interface VotingRoomMobileProps {
wakuVoting: WakuVoting
availableAmount: number // token balance of user
account: string | null | undefined // address of user account

View File

@ -1,5 +1,5 @@
{
"name": "@status-waku-voting/proposal-components",
"name": "@status-waku-voting/voting-components",
"version": "0.1.0",
"main": "dist/cjs/src/index.js",
"module": "dist/esm/src/index.js",
@ -33,7 +33,7 @@
},
"dependencies": {
"@status-waku-voting/core": "^0.1.0",
"@status-waku-voting/proposal-hooks": "^0.1.0",
"@status-waku-voting/voting-hooks": "^0.1.0",
"@status-waku-voting/react-components": "^0.1.0",
"ethers": "^5.4.4",
"humanize-duration": "^3.27.0",

View File

Before

Width:  |  Height:  |  Size: 239 B

After

Width:  |  Height:  |  Size: 239 B

View File

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

View File

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 305 B

View File

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 305 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

View File

@ -31,15 +31,9 @@ export function VoteChart({
tabletMode,
className,
}: VoteChartProps) {
const totalVotesFor = useMemo(
() => (isAnimation ? votingRoom.totalVotesFor : votingRoom.wakuTotalVotesFor),
[votingRoom, proposingAmount]
)
const totalVotesFor = useMemo(() => votingRoom.wakuTotalVotesFor, [votingRoom, proposingAmount])
const totalVotesAgainst = useMemo(
() => (isAnimation ? votingRoom.totalVotesAgainst : votingRoom.wakuTotalVotesAgainst),
[votingRoom, proposingAmount]
)
const totalVotesAgainst = useMemo(() => votingRoom.wakuTotalVotesAgainst, [votingRoom, proposingAmount])
const voteSum = useMemo(
() => totalVotesFor.add(totalVotesAgainst),

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType'
import { Modal, Theme } from '@status-waku-voting/react-components'
import { AmountModal } from './AmountModal'
@ -27,6 +27,7 @@ export function VoteModal({
className,
}: VoteModalProps) {
const [screen, setScreen] = useState(0)
const votingRoomSnap = useMemo(() => Object.assign({}, votingRoom), [showModal])
useEffect(() => setScreen(0), [])
const [proposingAmount, setProposingAmount] = useState(0)
useEffect(() => {
@ -50,7 +51,7 @@ export function VoteModal({
/>
) : (
<ConfirmModal
votingRoom={votingRoom}
votingRoom={votingRoomSnap}
selectedVote={selectedVote}
setShowModal={() => {
setShowModal(false)

View File

@ -4,12 +4,12 @@ import { Theme } from '@status-waku-voting/react-components'
import { ProposalInfo } from './ProposalInfo'
import { ProposalVote } from './ProposalVoteCard/ProposalVote'
import { WakuVoting } from '@status-waku-voting/core'
import { useVotingRoom } from '@status-waku-voting/proposal-hooks'
import { useVotingRoom } from '@status-waku-voting/voting-hooks'
import { VoteModal, VoteModalProps } from './VoteModal/VoteModal'
import { useRefMobileVersion } from '@status-waku-voting/react-components'
import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType'
interface ProposalCardProps {
interface VotingRoomCardProps {
votingRoomId: number
theme: Theme
availableAmount: number
@ -21,7 +21,7 @@ interface ProposalCardProps {
customForClick?: () => void
}
export function ProposalCard({
export function VotingRoomCard({
account,
theme,
votingRoomId,
@ -31,7 +31,7 @@ export function ProposalCard({
customAgainstClick,
customForClick,
mobileOnClick,
}: ProposalCardProps) {
}: VotingRoomCardProps) {
const votingRoom = useVotingRoom(votingRoomId, wakuVoting)
const ref = useRef<HTMLHeadingElement>(null)
const mobileVersion = useRefMobileVersion(ref, 568)

View File

@ -1,12 +1,12 @@
import React from 'react'
import styled from 'styled-components'
import { Theme } from '@status-waku-voting/react-components'
import { ProposalCard } from './ProposalCard'
import { VotingRoomCard } from './VotingRoomCard'
import { WakuVoting } from '@status-waku-voting/core'
import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType'
type ProposalListProps = {
type VotingRoomListProps = {
theme: Theme
wakuVoting: WakuVoting
votes: number[]
@ -14,12 +14,19 @@ type ProposalListProps = {
account: string | null | undefined
mobileOnClick?: (votingRoom: VotingRoom) => void
}
export function ProposalList({ theme, wakuVoting, votes, availableAmount, account, mobileOnClick }: ProposalListProps) {
export function VotingRoomList({
theme,
wakuVoting,
votes,
availableAmount,
account,
mobileOnClick,
}: VotingRoomListProps) {
return (
<List>
{votes.map((votingRoom) => {
return (
<ProposalCard
<VotingRoomCard
account={account}
votingRoomId={votingRoom}
theme={theme}

View File

@ -1,15 +1,15 @@
import React, { useEffect } from 'react'
import React from 'react'
import styled from 'styled-components'
import { CreateButton, Theme } from '@status-waku-voting/react-components'
type VotingEmptyProps = {
type VotingRoomListEmptyProps = {
theme: Theme
account: string | null | undefined
onCreateClick: () => void
onConnectClick: () => void
}
export function VotingEmpty({ theme, account, onCreateClick, onConnectClick }: VotingEmptyProps) {
export function VotingRoomListEmpty({ theme, account, onCreateClick, onConnectClick }: VotingRoomListEmptyProps) {
return (
<VotingEmptyWrap>
<EmptyWrap>

View File

@ -3,14 +3,14 @@ import styled from 'styled-components'
import { CreateButton } from '@status-waku-voting/react-components'
import { Theme } from '@status-waku-voting/react-components/dist/esm/src/style/themes'
type ProposalHeaderProps = {
type VotingRoomListHeaderProps = {
theme: Theme
account: string | null | undefined
onCreateClick: () => void
onConnectClick: () => void
}
export function ProposalHeader({ theme, account, onCreateClick, onConnectClick }: ProposalHeaderProps) {
export function VotingRoomListHeader({ theme, account, onCreateClick, onConnectClick }: VotingRoomListHeaderProps) {
return (
<Wrapper>
<Header>

View File

@ -0,0 +1,57 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { CardHeading } from '../ProposalInfo'
import { VotingRoomDetailInput, ProposingData } from '../newVoteModal/VotingRoomDetailInput'
import { TokenAmountScreen } from '../newVoteModal/TokenAmountScreen'
import { WakuVoting } from '@status-waku-voting/core'
interface NewVotingRoomMobileProps {
availableAmount: number
wakuVoting: WakuVoting
callbackAfterVote: () => void
}
export function NewVotingRoomMobile({ availableAmount, wakuVoting, callbackAfterVote }: NewVotingRoomMobileProps) {
const [title, setTitle] = useState('')
const [text, setText] = useState('')
const [screen, setScreen] = useState(1)
return (
<ProposingDataMobile>
<ProposingCardHeading>Create proposal</ProposingCardHeading>
{screen === 1 && (
<VotingRoomDetailInput
availableAmount={availableAmount}
text={text}
setText={setText}
title={title}
setTitle={setTitle}
setShowProposeVoteModal={() => setScreen(2)}
wakuVoting={wakuVoting}
className={'mobile'}
/>
)}
{screen == 2 && (
<TokenAmountScreen
wakuVoting={wakuVoting}
title={title}
text={text}
availableAmount={availableAmount}
setText={setText}
setTitle={setTitle}
className={'mobile'}
callbackAfterVote={callbackAfterVote}
/>
)}
</ProposingDataMobile>
)
}
const ProposingDataMobile = styled(ProposingData)`
padding: 88px 16px 32px;
margin-top: 0;
`
const ProposingCardHeading = styled(CardHeading)`
margin-bottom: 16px;
`

View File

@ -7,16 +7,16 @@ import { VoteChart } from '../ProposalVoteCard/VoteChart'
import { ProposalInfo } from '../ProposalInfo'
import { VotePropose } from '../VotePropose'
import { VotesBtns } from '../ProposalVoteCard/ProposalVote'
import { useVotingRoom } from '@status-waku-voting/proposal-hooks'
import { useVotingRoom } from '@status-waku-voting/voting-hooks'
import { WakuVoting } from '@status-waku-voting/core'
import { BigNumber } from 'ethers'
interface ProposalVoteMobileProps {
interface VotingRoomMobileProps {
wakuVoting: WakuVoting
availableAmount: number
account: string | null | undefined
}
export function ProposalVoteMobile({ wakuVoting, availableAmount, account }: ProposalVoteMobileProps) {
export function VotingRoomMobile({ wakuVoting, availableAmount, account }: VotingRoomMobileProps) {
const { id } = useParams<{ id: string }>()
const [proposingAmount, setProposingAmount] = useState(0)
const [selectedVoted, setSelectedVoted] = useState(0)

View File

@ -1,10 +1,10 @@
import { WakuVoting } from '@status-waku-voting/core'
import { Modal, Theme } from '@status-waku-voting/react-components'
import React, { useCallback, useEffect, useState } from 'react'
import { ProposeModal } from './ProposeModal'
import { ProposeVoteModal } from './ProposeVoteModal'
import React, { useEffect, useState } from 'react'
import { VotingRoomDetailInput } from './VotingRoomDetailInput'
import { TokenAmountScreen } from './TokenAmountScreen'
type NewVoteModalProps = {
type NewVotingRoomModalProps = {
theme: Theme
showModal: boolean
setShowModal: (val: boolean) => void
@ -12,7 +12,13 @@ type NewVoteModalProps = {
wakuVoting: WakuVoting
}
export function NewVoteModal({ theme, showModal, setShowModal, availableAmount, wakuVoting }: NewVoteModalProps) {
export function NewVotingRoomModal({
theme,
showModal,
setShowModal,
availableAmount,
wakuVoting,
}: NewVotingRoomModalProps) {
const [screen, setScreen] = useState(1)
const [title, setTitle] = useState('')
const [text, setText] = useState('')
@ -32,7 +38,7 @@ export function NewVoteModal({ theme, showModal, setShowModal, availableAmount,
return (
<Modal heading="Create proposal" theme={theme} setShowModal={setShowModal}>
{screen === 1 && (
<ProposeModal
<VotingRoomDetailInput
title={title}
text={text}
setText={setText}
@ -43,14 +49,14 @@ export function NewVoteModal({ theme, showModal, setShowModal, availableAmount,
/>
)}
{screen === 2 && (
<ProposeVoteModal
<TokenAmountScreen
wakuVoting={wakuVoting}
title={title}
text={text}
availableAmount={availableAmount}
setShowModal={setShowModal}
setText={setText}
setTitle={setTitle}
callbackAfterVote={() => setShowModal(false)}
/>
)}
</Modal>

View File

@ -4,32 +4,34 @@ import React, { useState } from 'react'
import styled from 'styled-components'
import { ProposingBtn } from '../Buttons'
import { CardHeading, CardText } from '../ProposalInfo'
import { ProposingData } from './ProposeModal'
import { ProposingData } from './VotingRoomDetailInput'
import { VotePropose } from '../VotePropose'
import { BigNumber } from 'ethers'
interface ProposeVoteModalProps {
interface TokenAmountScreenProps {
wakuVoting: WakuVoting
availableAmount: number
title: string
text: string
setShowModal: (val: boolean) => void
setTitle: (val: string) => void
setText: (val: string) => void
className?: string
callbackAfterVote?: () => void
}
export function ProposeVoteModal({
export function TokenAmountScreen({
wakuVoting,
availableAmount,
title,
text,
setShowModal,
setTitle,
setText,
}: ProposeVoteModalProps) {
className,
callbackAfterVote,
}: TokenAmountScreenProps) {
const [proposingAmount, setProposingAmount] = useState(0)
return (
<ProposingData>
<ProposingData className={className ?? ''}>
<ProposingCardHeading>{title}</ProposingCardHeading>
<ProposingCardText>{text}</ProposingCardText>
@ -45,9 +47,11 @@ export function ProposeVoteModal({
<ProposingBtn
onClick={() => {
wakuVoting.createVote(title, text, BigNumber.from(proposingAmount))
setShowModal(false)
setTitle('')
setText('')
if (callbackAfterVote) {
callbackAfterVote()
}
}}
>
Create proposal
@ -63,6 +67,12 @@ export const VoteProposeWrap = styled.div`
const ProposingCardHeading = styled(CardHeading)`
margin-bottom: 16px;
&.mobile {
margin-bottom: 0px;
font-size: 22px;
margin-top: 24px;
}
`
const ProposingCardText = styled(CardText)`
margin-bottom: 0;

View File

@ -5,7 +5,7 @@ import { TextArea } from '../Input'
import { blueTheme } from '@status-waku-voting/react-components/dist/esm/src/style/themes'
import { WakuVoting } from '@status-waku-voting/core'
interface ProposeModalProps {
interface VotingRoomDetailInputProps {
availableAmount: number
title: string
text: string
@ -13,9 +13,10 @@ interface ProposeModalProps {
setTitle: (val: string) => void
setText: (val: string) => void
wakuVoting: WakuVoting
className?: string
}
export function ProposeModal({
export function VotingRoomDetailInput({
availableAmount,
title,
text,
@ -23,13 +24,14 @@ export function ProposeModal({
setTitle,
setText,
wakuVoting,
}: ProposeModalProps) {
className,
}: VotingRoomDetailInputProps) {
const insufficientFunds = useMemo(() => availableAmount < 10000, [availableAmount])
return (
<ProposingData>
<ProposingData className={className ?? ''}>
{availableAmount < 10000 && (
<ProposingInfo>
<ProposingInfo className={className ?? ''}>
<span></span>
<InfoText>You need at least 10,000 {wakuVoting.tokenSymbol} to create a proposal!</InfoText>
</ProposingInfo>
@ -75,8 +77,9 @@ export function ProposeModal({
export const VoteProposeWrap = styled.div`
margin-top: 32px;
@media (max-width: 600px) {
margin-top: 0;
&.mobile {
margin-bottom: 32px;
width: 100%;
}
`
@ -86,6 +89,11 @@ export const ProposingData = styled.div`
flex-direction: column;
align-items: center;
margin-top: 32px;
&.mobile {
margin-top: 0px;
width: 100%;
}
`
export const ProposingInfo = styled.div`
@ -97,7 +105,7 @@ export const ProposingInfo = styled.div`
margin-bottom: 32px;
background: #ffeff2;
@media (max-width: 600px) {
&.mobile {
max-width: 525px;
}

View File

@ -0,0 +1,22 @@
import { NewVotingRoomMobile } from './components/mobile/NewVotingRoomMobile'
import { VotingRoomMobile } from './components/mobile/VotingRoomMobile'
import { VotingRoomListEmpty } from './components/VotingRoomListEmpty'
import { VoteModal } from './components/VoteModal/VoteModal'
import { VotingRoomList } from './components/VotingRoomList'
import { VotingRoomCard } from './components/VotingRoomCard'
import { VotingRoomListHeader } from './components/VotingRoomListHeader'
import { NotificationItem } from './components/NotificationItem'
import { NewVotingRoomModal } from './components/newVoteModal/NewVotingRoomModal'
export {
VotingRoomListEmpty,
VoteModal,
VotingRoomList,
VotingRoomCard,
VotingRoomListHeader,
NotificationItem,
NewVotingRoomModal,
NewVotingRoomMobile,
VotingRoomMobile,
}

View File

@ -0,0 +1,46 @@
Package contains example usage of voting sdk
For more detailed api please see README in voting-hooks and voting-components
For connecting with web3 api usedapp is used please refer to usedapp docs for more info
## Creating WakuVoting
to create waku voting in react you can use `useWakuVoting` hook from voting-hooks
```
const waku = useWakuVoting(
dappName,
votingContractAddress,
library,
multicallAddress
)
```
For more detailed usage you can take a look in `index.tsx`
Page is divided into mobile and normal version.
## display list of voting rooms
to display list of voting rooms you can use already created `VotingRoomList` for more details see voting-components package
`VotingRoomList` needs a list of voting room ids which can be gotten with `useVotingRoomsId` hook
```
const votes = useVotingRoomsId(wakuVoting)
<VotingRoomList
account={account}
theme={blueTheme}
wakuVoting={wakuVoting}
votes={votes}
availableAmount={tokenBalance}
mobileOnClick={(votingRoom: VotingRoom) => history.push(`/votingRoom/${votingRoom.id.toString()}`)}
/>
```
## creating new voting room
To create voting room you can use `NewVotingRoomModal` component.
Component is a modal and expects a state value and state update function (`showModal` and `setShowModal`) to be able show and hide modal

View File

@ -1,5 +1,5 @@
{
"name": "@status-waku-voting/proposal-example",
"name": "@status-waku-voting/voting-example",
"version": "0.1.0",
"main": "dist/cjs/src/index.js",
"module": "dist/esm/src/index.js",
@ -33,8 +33,8 @@
},
"dependencies": {
"@status-waku-voting/core": "^0.1.0",
"@status-waku-voting/proposal-components": "^0.1.0",
"@status-waku-voting/proposal-hooks": "^0.1.0",
"@status-waku-voting/voting-components": "^0.1.0",
"@status-waku-voting/voting-hooks": "^0.1.0",
"@status-waku-voting/react-components": "^0.1.0",
"@usedapp/core": "^0.4.7",
"ethers": "^5.4.4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,16 +1,21 @@
import React, { useCallback, useRef, useState } from 'react'
import styled from 'styled-components'
import { ProposalHeader, ProposalList, VotingEmpty, NewVoteModal } from '@status-waku-voting/proposal-components'
import {
VotingRoomListHeader,
VotingRoomList,
VotingRoomListEmpty,
NewVotingRoomModal,
} from '@status-waku-voting/voting-components'
import { blueTheme } from '@status-waku-voting/react-components/dist/esm/src/style/themes'
import { WakuVoting } from '@status-waku-voting/core'
import { useTokenBalance } from '@status-waku-voting/react-components'
import { useEthers } from '@usedapp/core'
import { Modal, Networks, useMobileVersion, Theme } from '@status-waku-voting/react-components'
import { useVotingRoomsId } from '@status-waku-voting/proposal-hooks'
import { useVotingRoomsId } from '@status-waku-voting/voting-hooks'
import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType'
import { useHistory } from 'react-router'
type ProposalListHeaderProps = {
type VotingListHeaderProps = {
votesLength: number
theme: Theme
wakuVoting: WakuVoting
@ -18,7 +23,7 @@ type ProposalListHeaderProps = {
account: string | null | undefined
}
function ProposalListHeader({ votesLength, theme, wakuVoting, tokenBalance, account }: ProposalListHeaderProps) {
function VotingListHeader({ votesLength, theme, wakuVoting, tokenBalance, account }: VotingListHeaderProps) {
const [showNewVoteModal, setShowNewVoteModal] = useState(false)
const [showConnectionModal, setShowConnectionModal] = useState(false)
const { activateBrowserWallet } = useEthers()
@ -38,7 +43,7 @@ function ProposalListHeader({ votesLength, theme, wakuVoting, tokenBalance, acco
return (
<div ref={ref}>
<NewVoteModal
<NewVotingRoomModal
theme={theme}
availableAmount={tokenBalance}
setShowModal={setShowNewVoteModal}
@ -51,49 +56,57 @@ function ProposalListHeader({ votesLength, theme, wakuVoting, tokenBalance, acco
</Modal>
)}
{votesLength === 0 ? (
<VotingEmpty account={account} theme={theme} onConnectClick={onConnectClick} onCreateClick={onCreateClick} />
<VotingRoomListEmpty
account={account}
theme={theme}
onConnectClick={onConnectClick}
onCreateClick={onCreateClick}
/>
) : (
<ProposalHeader account={account} theme={theme} onConnectClick={onConnectClick} onCreateClick={onCreateClick} />
<VotingRoomListHeader
account={account}
theme={theme}
onConnectClick={onConnectClick}
onCreateClick={onCreateClick}
/>
)}
</div>
)
}
type ProposalProps = {
type VotingProps = {
wakuVoting: WakuVoting
account: string | null | undefined
}
export function Proposal({ wakuVoting, account }: ProposalProps) {
export function Voting({ wakuVoting, account }: VotingProps) {
const votes = useVotingRoomsId(wakuVoting)
const tokenBalance = useTokenBalance(account, wakuVoting)
const history = useHistory()
return (
<ProposalWrapper>
<ProposalVotesWrapper>
<ProposalListHeader
votesLength={votes.length}
tokenBalance={tokenBalance}
theme={blueTheme}
<Wrapper>
<VotingListHeader
votesLength={votes.length}
tokenBalance={tokenBalance}
theme={blueTheme}
account={account}
wakuVoting={wakuVoting}
/>
{votes.length > 0 && (
<VotingRoomList
account={account}
theme={blueTheme}
wakuVoting={wakuVoting}
votes={votes}
availableAmount={tokenBalance}
mobileOnClick={(votingRoom: VotingRoom) => history.push(`/votingRoom/${votingRoom.id.toString()}`)}
/>
{votes.length > 0 && (
<ProposalList
account={account}
theme={blueTheme}
wakuVoting={wakuVoting}
votes={votes}
availableAmount={tokenBalance}
mobileOnClick={(votingRoom: VotingRoom) => history.push(`/votingRoom/${votingRoom.id.toString()}`)}
/>
)}
</ProposalVotesWrapper>
</ProposalWrapper>
)}
</Wrapper>
)
}
const ProposalWrapper = styled.div`
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
@ -111,8 +124,3 @@ const ProposalWrapper = styled.div`
padding: 64px 16px 84px;
}
`
export const ProposalVotesWrapper = styled(ProposalWrapper)`
width: 100%;
padding: 0;
`

View File

@ -0,0 +1,60 @@
import React from 'react'
import { Redirect, Route, Switch, useHistory } from 'react-router'
import { BrowserRouter } from 'react-router-dom'
import styled from 'styled-components'
import { VotingRoomMobile, NewVotingRoomMobile } from '@status-waku-voting/voting-components'
import { Voting } from './Voting'
import { WakuVoting } from '@status-waku-voting/core'
import { useTokenBalance } from '@status-waku-voting/react-components'
type VotingRoomCreationProps = {
tokenBalance: number
wakuVoting: WakuVoting
}
function VotingRoomCreation({ tokenBalance, wakuVoting }: VotingRoomCreationProps) {
const history = useHistory()
return (
<NewVotingRoomMobile
availableAmount={tokenBalance}
wakuVoting={wakuVoting}
callbackAfterVote={() => history.push('/proposal')}
/>
)
}
type VotingMobileProps = {
wakuVoting: WakuVoting
account: string | null | undefined
}
export function VotingMobile({ wakuVoting, account }: VotingMobileProps) {
const tokenBalance = useTokenBalance(account, wakuVoting)
return (
<BrowserRouter>
<Wrapper>
<Switch>
<Route exact path="/votingRoom/:id">
<VotingRoomMobile wakuVoting={wakuVoting} availableAmount={tokenBalance} account={account} />
</Route>
<Route exact path="/creation">
<VotingRoomCreation tokenBalance={tokenBalance} wakuVoting={wakuVoting} />
</Route>
<Route exact path="/voting">
<Voting wakuVoting={wakuVoting} account={account} />
</Route>
</Switch>
</Wrapper>
</BrowserRouter>
)
}
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
max-width: 1000px;
margin: 0 auto;
width: 100%;
min-height: 100vh;
`

View File

@ -1,9 +1,9 @@
import React, { useRef } from 'react'
import { useWakuVoting } from '@status-waku-voting/proposal-hooks'
import { ProposalMobile } from './components/ProposalMobile'
import { Proposal } from './components/Proposal'
import { useWakuVoting } from '@status-waku-voting/voting-hooks'
import { VotingMobile } from './components/VotingMobile'
import { Voting } from './components/Voting'
import { TopBar, GlobalStyle, useMobileVersion } from '@status-waku-voting/react-components'
import votingIcon from './assets/images/voting.svg'
import votingIcon from './assets/images/voting.png'
import styled from 'styled-components'
import { blueTheme } from '@status-waku-voting/react-components/dist/esm/src/style/themes'
import { DAppProvider, ChainId, useEthers, useConfig } from '@usedapp/core'
@ -26,7 +26,7 @@ const config = {
},
}
function Proposals() {
function VotingWrapper() {
const { account, activateBrowserWallet, deactivate, library, chainId } = useEthers()
const config = useConfig()
const waku = useWakuVoting(
@ -42,6 +42,7 @@ function Proposals() {
<Wrapper ref={ref}>
<TopBar
logo={votingIcon}
logoWidth={84}
title={'Proposals Dapp'}
theme={blueTheme}
activate={activateBrowserWallet}
@ -50,20 +51,20 @@ function Proposals() {
/>
{waku &&
(mobileVersion ? (
<ProposalMobile wakuVoting={waku} account={account} />
<VotingMobile wakuVoting={waku} account={account} />
) : (
<Proposal wakuVoting={waku} account={account} />
<Voting wakuVoting={waku} account={account} />
))}
</Wrapper>
)
}
export function ProposalPage() {
export function VotingPage() {
return (
<Wrapper>
<GlobalStyle />
<DAppProvider config={config}>
<Proposals />
<VotingWrapper />
</DAppProvider>
</Wrapper>
)

View File

@ -1,5 +1,5 @@
{
"name": "@status-waku-voting/proposal-hooks",
"name": "@status-waku-voting/voting-hooks",
"version": "0.1.0",
"main": "dist/cjs/src/index.js",
"module": "dist/esm/src/index.js",

View File

@ -4,17 +4,16 @@ import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType
export function useVotingRoom(id: number, wakuVoting: WakuVoting) {
const [votingRoom, setVotingRoom] = useState<VotingRoom | undefined>(undefined)
const lastTimeLeft = useRef(1)
useEffect(() => {
const updateFunction = async () => {
if (lastTimeLeft.current > 0) {
const votingRoom = await wakuVoting.getVotingRoom(id)
setVotingRoom(votingRoom)
lastTimeLeft.current = votingRoom?.timeLeft ?? 1
const votingRoom = await wakuVoting.getVotingRoom(id)
setVotingRoom(votingRoom)
if (votingRoom?.timeLeft && votingRoom.timeLeft < 0) {
clearInterval(interval)
}
}
updateFunction()
const interval = setInterval(updateFunction, 1000)
const interval = setInterval(updateFunction, 2000)
return () => clearInterval(interval)
}, [id, wakuVoting])