Add proposal voting (#78)

This commit is contained in:
Szymon Szlachtowicz 2021-09-20 16:34:54 +02:00 committed by GitHub
parent 9502bc67b2
commit e1a1203a58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 172 additions and 49 deletions

View File

@ -87,8 +87,17 @@ export class WakuMessaging {
) { ) {
const { decodeFunction, filterFunction, name } = setupData const { decodeFunction, filterFunction, name } = setupData
const { arr, hashMap } = this.wakuMessages[name] const { arr, hashMap } = this.wakuMessages[name]
messages const decodedMessages = messages.map(decodeFunction).filter((e): e is T => !!e)
.map(decodeFunction)
const addressesToUpdate: string[] = []
decodedMessages.forEach((message: any) => {
setupData.tokenCheckArray.forEach((property) => {
addressesToUpdate.push(message?.[property])
})
})
this.updateBalances(addressesToUpdate)
decodedMessages
.sort((a, b) => ((a?.timestamp ?? new Date(0)) > (b?.timestamp ?? new Date(0)) ? 1 : -1)) .sort((a, b) => ((a?.timestamp ?? new Date(0)) > (b?.timestamp ?? new Date(0)) ? 1 : -1))
.forEach((e) => { .forEach((e) => {
if (e) { if (e) {
@ -117,7 +126,7 @@ export class WakuMessaging {
protected addressesBalances: { [address: string]: BigNumber | undefined } = {} protected addressesBalances: { [address: string]: BigNumber | undefined } = {}
protected lastBlockBalances = 0 protected lastBlockBalances = 0
protected async updateBalances(newAddress?: string) { protected async updateBalances(newAddresses?: string[]) {
const addressesToUpdate: { [addr: string]: boolean } = {} const addressesToUpdate: { [addr: string]: boolean } = {}
const addAddressToUpdate = (addr: string) => { const addAddressToUpdate = (addr: string) => {
@ -130,22 +139,11 @@ export class WakuMessaging {
if (this.lastBlockBalances != currentBlock) { if (this.lastBlockBalances != currentBlock) {
Object.keys(this.addressesBalances).forEach(addAddressToUpdate) Object.keys(this.addressesBalances).forEach(addAddressToUpdate)
if (newAddress) addAddressToUpdate(newAddress) newAddresses?.forEach(addAddressToUpdate)
Object.values(this.wakuMessages).forEach((wakuMessage) =>
wakuMessage.arr.forEach((msg) => wakuMessage.tokenCheckArray.forEach((field) => addAddressToUpdate(msg[field])))
)
} else { } else {
Object.values(this.wakuMessages).forEach((wakuMessage) => newAddresses?.forEach((newAddress) => {
wakuMessage.arr.forEach((msg) => if (!this.addressesBalances[newAddress]) addAddressToUpdate(newAddress)
wakuMessage.tokenCheckArray.forEach((field) => { })
const address = msg[field]
if (!this.addressesBalances[address]) {
addAddressToUpdate(address)
}
})
)
)
if (newAddress && !this.addressesBalances[newAddress]) addAddressToUpdate(newAddress)
} }
const addressesToUpdateArray = Object.keys(addressesToUpdate) const addressesToUpdateArray = Object.keys(addressesToUpdate)
@ -170,7 +168,7 @@ export class WakuMessaging {
} }
public async getTokenBalance(address: string) { public async getTokenBalance(address: string) {
await this.updateBalances(address) await this.updateBalances([address])
return this.addressesBalances[address] ?? undefined return this.addressesBalances[address] ?? undefined
} }
} }

View File

@ -71,7 +71,7 @@ export class WakuPolling extends WakuMessaging {
endTime?: number endTime?: number
) { ) {
const address = await signer.getAddress() const address = await signer.getAddress()
await this.updateBalances(address) await this.updateBalances([address])
if (this.addressesBalances[address] && this.addressesBalances[address]?.gt(minToken ?? BigNumber.from(0))) { if (this.addressesBalances[address] && this.addressesBalances[address]?.gt(minToken ?? BigNumber.from(0))) {
const pollInit = await PollInitMsg.create(signer, question, answers, pollType, this.chainId, minToken, endTime) const pollInit = await PollInitMsg.create(signer, question, answers, pollType, this.chainId, minToken, endTime)
if (pollInit) { if (pollInit) {
@ -94,7 +94,7 @@ export class WakuPolling extends WakuMessaging {
const address = await signer.getAddress() const address = await signer.getAddress()
const poll = this.wakuMessages['pollInit'].arr.find((poll: PollInitMsg): poll is PollInitMsg => poll.id === pollId) const poll = this.wakuMessages['pollInit'].arr.find((poll: PollInitMsg): poll is PollInitMsg => poll.id === pollId)
if (poll) { if (poll) {
await this.updateBalances(address) await this.updateBalances([address])
if (this.addressesBalances[address] && this.addressesBalances[address]?.gt(poll.minToken ?? BigNumber.from(0))) { if (this.addressesBalances[address] && this.addressesBalances[address]?.gt(poll.minToken ?? BigNumber.from(0))) {
const pollVote = await TimedPollVoteMsg.create(signer, pollId, selectedAnswer, this.chainId, tokenAmount) const pollVote = await TimedPollVoteMsg.create(signer, pollId, selectedAnswer, this.chainId, tokenAmount)
if (pollVote) { if (pollVote) {

View File

@ -1,11 +1,12 @@
import { VotingContract } from '@status-waku-voting/contracts/abi' import { VotingContract } from '@status-waku-voting/contracts/abi'
import { WakuMessaging } from './WakuMessaging' import { WakuMessaging } from './WakuMessaging'
import { Contract, Wallet, BigNumber, ethers } from 'ethers' import { Contract, Wallet, BigNumber, ethers, utils } from 'ethers'
import { Waku, WakuMessage } from 'js-waku' import { Waku, WakuMessage } from 'js-waku'
import { createWaku } from '../utils/createWaku' import { createWaku } from '../utils/createWaku'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers' import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { VoteMsg } from '../models/VoteMsg' import { VoteMsg } from '../models/VoteMsg'
import { VotingRoom } from '../types/PollType' import { VotingRoom } from '../types/PollType'
import { DetailedVotingRoom } from '../models/DetailedVotingRoom'
const ABI = [ const ABI = [
'function aggregate(tuple(address target, bytes callData)[] calls) view returns (uint256 blockNumber, bytes[] returnData)', 'function aggregate(tuple(address target, bytes callData)[] calls) view returns (uint256 blockNumber, bytes[] returnData)',
@ -90,7 +91,11 @@ export class WakuVoting extends WakuMessaging {
} }
public async getVotingRoom(id: number) { public async getVotingRoom(id: number) {
return (await this.getVotingRooms())[id] try {
return (await this.getVotingRooms())[id]
} catch {
return undefined
}
} }
public async sendVote(roomId: number, selectedAnswer: number, tokenAmount: BigNumber) { public async sendVote(roomId: number, selectedAnswer: number, tokenAmount: BigNumber) {
@ -105,4 +110,46 @@ export class WakuVoting extends WakuMessaging {
) )
await this.sendWakuMessage(this.wakuMessages['vote'], vote) await this.sendWakuMessage(this.wakuMessages['vote'], vote)
} }
public async commitVotes(votes: VoteMsg[]) {
const signer = this.provider.getSigner()
const mappedVotes = votes.map((vote) => {
const sig = utils.splitSignature(vote.signature)
return [vote.voter, BigNumber.from(vote.roomId).mul(2).add(vote.answer), vote.tokenAmount, sig.r, sig._vs]
})
this.votingContract = this.votingContract.connect(signer)
this.votingContract.castVotes(mappedVotes)
}
public async getRoomWakuVotes(id: number) {
await this.updateBalances()
const votingRoom = await this.getVotingRoom(id)
if (!votingRoom || votingRoom.timeLeft < 0) {
return undefined
}
const votersHashMap: { [voter: string]: boolean } = {}
votingRoom.voters.forEach((voter) => (votersHashMap[voter] = true))
const newVotingRoom: VotingRoom = { ...votingRoom }
const wakuVotes = this.wakuMessages['vote'].arr.filter((vote: VoteMsg) => {
if (
vote.roomId === id &&
this.addressesBalances[vote.voter] &&
this.addressesBalances[vote.voter]?.gt(vote.tokenAmount)
) {
if (!votersHashMap[vote.voter]) {
votersHashMap[vote.voter] = true
if (vote.answer === 0) {
newVotingRoom.totalVotesAgainst = newVotingRoom.totalVotesAgainst.add(vote.tokenAmount)
} else {
newVotingRoom.totalVotesFor = newVotingRoom.totalVotesFor.add(vote.tokenAmount)
}
return true
}
}
return false
}) as VoteMsg[]
const sum = wakuVotes.reduce((prev, curr) => prev.add(curr.tokenAmount), BigNumber.from(0))
return { sum, wakuVotes, newVotingRoom }
}
} }

View File

@ -0,0 +1,23 @@
import { BigNumber } from 'ethers'
import { VotingRoom } from '../types/PollType'
import { VoteMsg } from './VoteMsg'
export class DetailedVotingRoom {
public messages: VoteMsg[] = []
public votingRoom: VotingRoom
public sum: BigNumber = BigNumber.from(0)
constructor(votingRoom: VotingRoom, voteMessages: VoteMsg[]) {
this.votingRoom = votingRoom
this.sum = voteMessages.reduce((prev, curr) => prev.add(curr.tokenAmount), BigNumber.from(0))
const votersHashMap: { [voter: string]: boolean } = {}
votingRoom.voters.forEach((voter) => (votersHashMap[voter] = true))
voteMessages.forEach((vote) => {
if (!votersHashMap[vote.voter]) {
votersHashMap[vote.voter] = true
this.messages.push(vote)
}
})
}
}

View File

@ -26,7 +26,7 @@ type Message = {
export function createSignMsgParams(message: Message, chainId: number, verifyingContract: string) { export function createSignMsgParams(message: Message, chainId: number, verifyingContract: string) {
const msgParams: any = { const msgParams: any = {
domain: { domain: {
name: 'Waku proposal', name: 'Voting Contract',
version: '1', version: '1',
chainId, chainId,
verifyingContract, verifyingContract,
@ -43,9 +43,9 @@ export function createSignMsgParams(message: Message, chainId: number, verifying
{ name: 'verifyingContract', type: 'address' }, { name: 'verifyingContract', type: 'address' },
], ],
Vote: [ Vote: [
{ name: 'roomIdAndType', type: 'string' }, { name: 'roomIdAndType', type: 'uint256' },
{ name: 'tokenAmount', type: 'string' }, { name: 'tokenAmount', type: 'uint256' },
{ name: 'voter', type: 'string' }, { name: 'voter', type: 'address' },
], ],
}, },
} }

View File

@ -5,6 +5,7 @@ import { Theme } from '@status-waku-voting/react-components'
import { ProposalInfo } from './ProposalInfo' import { ProposalInfo } from './ProposalInfo'
import { ProposalVote } from './ProposalVoteCard/ProposalVote' import { ProposalVote } from './ProposalVoteCard/ProposalVote'
import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType' import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType'
import { WakuVoting } from '@status-waku-voting/core'
interface ProposalCardProps { interface ProposalCardProps {
votingRoom: VotingRoom votingRoom: VotingRoom
@ -12,15 +13,16 @@ interface ProposalCardProps {
theme: Theme theme: Theme
hideModalFunction?: (val: boolean) => void hideModalFunction?: (val: boolean) => void
availableAmount: number availableAmount: number
wakuVoting: WakuVoting
} }
export function ProposalCard({ theme, votingRoom, mobileVersion, availableAmount }: ProposalCardProps) { export function ProposalCard({ theme, votingRoom, mobileVersion, availableAmount, wakuVoting }: ProposalCardProps) {
const history = useHistory() const history = useHistory()
return ( return (
<Card onClick={() => mobileVersion && history.push(`/votingRoom/${votingRoom.id.toString()}`)}> <Card onClick={() => mobileVersion && history.push(`/votingRoom/${votingRoom.id.toString()}`)}>
<ProposalInfo votingRoom={votingRoom} /> <ProposalInfo votingRoom={votingRoom} />
<ProposalVote votingRoom={votingRoom} theme={theme} availableAmount={availableAmount} /> <ProposalVote votingRoom={votingRoom} theme={theme} availableAmount={availableAmount} wakuVoting={wakuVoting} />
</Card> </Card>
) )
} }

View File

@ -25,6 +25,7 @@ export function ProposalList({ theme, wakuVoting, votes, availableAmount }: Prop
key={votingRoom.id} key={votingRoom.id}
mobileVersion={mobileVersion} mobileVersion={mobileVersion}
availableAmount={availableAmount} availableAmount={availableAmount}
wakuVoting={wakuVoting}
/> />
) )
})} })}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { useEthers } from '@usedapp/core' import { useEthers } from '@usedapp/core'
import { FinalBtn, VoteBtnAgainst, VoteBtnFor } from '../Buttons' import { FinalBtn, VoteBtnAgainst, VoteBtnFor } from '../Buttons'
@ -9,15 +9,18 @@ import { Modal, Theme } from '@status-waku-voting/react-components'
import { VoteModal } from '../VoteModal' import { VoteModal } from '../VoteModal'
import { VoteAnimatedModal } from '../VoteAnimatedModal' import { VoteAnimatedModal } from '../VoteAnimatedModal'
import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType' import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType'
import { WakuVoting } from '@status-waku-voting/core'
import { useRoomWakuVotes } from '@status-waku-voting/proposal-hooks'
interface ProposalVoteProps { interface ProposalVoteProps {
theme: Theme theme: Theme
votingRoom: VotingRoom votingRoom: VotingRoom
availableAmount: number availableAmount: number
hideModalFunction?: (val: boolean) => void hideModalFunction?: (val: boolean) => void
wakuVoting: WakuVoting
} }
export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunction }: ProposalVoteProps) { export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunction, wakuVoting }: ProposalVoteProps) {
const { account } = useEthers() const { account } = useEthers()
const [showVoteModal, setShowVoteModal] = useState(false) const [showVoteModal, setShowVoteModal] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false) const [showConfirmModal, setShowConfirmModal] = useState(false)
@ -36,24 +39,27 @@ export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunc
setShowConfirmModal(val) setShowConfirmModal(val)
} }
const { votes, sum, modifiedVotingRoom } = useRoomWakuVotes(votingRoom, wakuVoting)
return ( return (
<Card> <Card>
{showVoteModal && ( {showVoteModal && (
<Modal heading={votingRoom.question} setShowModal={setShowVoteModal} theme={theme}> <Modal heading={votingRoom.question} setShowModal={setShowVoteModal} theme={theme}>
<VoteModal <VoteModal
votingRoom={votingRoom} votingRoom={modifiedVotingRoom}
availableAmount={availableAmount} availableAmount={availableAmount}
selectedVote={selectedVoted} selectedVote={selectedVoted}
proposingAmount={proposingAmount} proposingAmount={proposingAmount}
setShowConfirmModal={setNext} setShowConfirmModal={setNext}
setProposingAmount={setProposingAmount} setProposingAmount={setProposingAmount}
wakuVoting={wakuVoting}
/>{' '} />{' '}
</Modal> </Modal>
)} )}
{showConfirmModal && ( {showConfirmModal && (
<Modal heading={votingRoom.question} setShowModal={hideConfirm} theme={theme}> <Modal heading={votingRoom.question} setShowModal={hideConfirm} theme={theme}>
<VoteAnimatedModal <VoteAnimatedModal
votingRoom={votingRoom} votingRoom={modifiedVotingRoom}
selectedVote={selectedVoted} selectedVote={selectedVoted}
setShowModal={hideConfirm} setShowModal={hideConfirm}
proposingAmount={proposingAmount} proposingAmount={proposingAmount}
@ -66,7 +72,7 @@ export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunc
<CardHeading /> <CardHeading />
)} )}
<VoteChart votingRoom={votingRoom} selectedVote={selectedVoted} /> <VoteChart votingRoom={modifiedVotingRoom} selectedVote={selectedVoted} />
<CardButtons> <CardButtons>
{votingRoom.voteWinner ? ( {votingRoom.voteWinner ? (
@ -100,7 +106,7 @@ export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunc
{' '} {' '}
<ViewLink address={'#'} /> <ViewLink address={'#'} />
</CardViewLink> </CardViewLink>
<VoteSubmitButton votes={15} disabled={!account} /> <VoteSubmitButton votes={sum.toNumber()} disabled={!account} onClick={() => wakuVoting.commitVotes(votes)} />
</CardVoteBottom> </CardVoteBottom>
</Card> </Card>
) )

View File

@ -5,11 +5,17 @@ import { VoteSendingBtn } from '../Buttons'
interface VoteSubmitButtonProps { interface VoteSubmitButtonProps {
votes: number votes: number
disabled: boolean disabled: boolean
onClick: () => void
} }
export function VoteSubmitButton({ votes, disabled }: VoteSubmitButtonProps) { export function VoteSubmitButton({ votes, disabled, onClick }: VoteSubmitButtonProps) {
if (votes > 0) { if (votes > 0) {
return <VoteSendingBtn disabled={disabled}> {addCommas(votes)} votes need saving</VoteSendingBtn> return (
<VoteSendingBtn onClick={onClick} disabled={disabled}>
{' '}
{addCommas(votes)} votes need saving
</VoteSendingBtn>
)
} }
return null return null
} }

View File

@ -4,6 +4,8 @@ import { VoteChart } from './ProposalVoteCard/VoteChart'
import { DisabledButton, VoteBtnAgainst, VoteBtnFor } from './Buttons' import { DisabledButton, VoteBtnAgainst, VoteBtnFor } from './Buttons'
import { VotePropose } from './VotePropose' import { VotePropose } from './VotePropose'
import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType' import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType'
import { WakuVoting } from '@status-waku-voting/core'
import { BigNumber } from 'ethers'
export interface VoteModalProps { export interface VoteModalProps {
votingRoom: VotingRoom votingRoom: VotingRoom
@ -12,6 +14,7 @@ export interface VoteModalProps {
proposingAmount: number proposingAmount: number
setShowConfirmModal: (show: boolean) => void setShowConfirmModal: (show: boolean) => void
setProposingAmount: (val: number) => void setProposingAmount: (val: number) => void
wakuVoting: WakuVoting
} }
export function VoteModal({ export function VoteModal({
@ -21,6 +24,7 @@ export function VoteModal({
proposingAmount, proposingAmount,
setShowConfirmModal, setShowConfirmModal,
setProposingAmount, setProposingAmount,
wakuVoting,
}: VoteModalProps) { }: VoteModalProps) {
const disabled = proposingAmount === 0 const disabled = proposingAmount === 0
const funds = availableAmount > 0 const funds = availableAmount > 0
@ -36,16 +40,17 @@ export function VoteModal({
{!funds && <DisabledButton>Not enought ABC to vote</DisabledButton>} {!funds && <DisabledButton>Not enought ABC to vote</DisabledButton>}
{funds && {funds && (
(selectedVote === 0 ? ( <ModalVoteBtnAgainst
<ModalVoteBtnAgainst disabled={disabled} onClick={() => setShowConfirmModal(true)}> disabled={disabled}
Vote Against onClick={async () => {
</ModalVoteBtnAgainst> wakuVoting.sendVote(votingRoom.id, selectedVote, BigNumber.from(proposingAmount))
) : ( setShowConfirmModal(true)
<ModalVoteBtnFor disabled={disabled} onClick={() => setShowConfirmModal(true)}> }}
Vote For >
</ModalVoteBtnFor> {selectedVote === 0 ? `Vote Against` : `Vote For`}
))} </ModalVoteBtnAgainst>
)}
</Column> </Column>
) )
} }

View File

@ -69,7 +69,7 @@ export function ProposalVoteMobile({ wakuVoting, availableAmount }: ProposalVote
<CardVoteBottom> <CardVoteBottom>
{' '} {' '}
<VoteSubmitButton votes={15} disabled={!account} /> <VoteSubmitButton votes={15} disabled={!account} onClick={() => null} />
</CardVoteBottom> </CardVoteBottom>
</Card> </Card>
) )
@ -80,6 +80,8 @@ export const Card = styled.div`
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100vw;
margin: 0px;
padding: 88px 16px 32px; padding: 88px 16px 32px;
` `

View File

@ -0,0 +1,32 @@
import React, { useState, useRef, useEffect } from 'react'
import { WakuVoting } from '@status-waku-voting/core'
import { VoteMsg } from '@status-waku-voting/core/dist/esm/src/models/VoteMsg'
import { utils, BigNumber } from 'ethers'
import { VotingRoom } from '@status-waku-voting/core/dist/esm/src/types/PollType'
export function useRoomWakuVotes(votingRoom: VotingRoom, wakuVoting: WakuVoting) {
const [votes, setVotes] = useState<VoteMsg[]>([])
const [sum, setSum] = useState(BigNumber.from(0))
const [modifiedVotingRoom, setModifiedVotingRoom] = useState(votingRoom)
const hash = useRef('')
useEffect(() => {
const updateVotes = async () => {
const newVotes = await wakuVoting.getRoomWakuVotes(votingRoom.id)
if (newVotes) {
const newHash = utils.id(newVotes.wakuVotes.map((vote) => vote.id).join(''))
if (newHash != hash.current) {
hash.current = newHash
setVotes(newVotes.wakuVotes)
setSum(newVotes.sum)
setModifiedVotingRoom(newVotes.newVotingRoom)
}
}
}
updateVotes()
const interval = setInterval(updateVotes, 10000)
return () => clearInterval(interval)
}, [wakuVoting])
return { votes, sum, modifiedVotingRoom }
}

View File

@ -1,4 +1,5 @@
import { useWakuProposal } from './hooks/useWakuProposal' import { useWakuProposal } from './hooks/useWakuProposal'
import { useVotingRoom } from './hooks/useVotingRoom' import { useVotingRoom } from './hooks/useVotingRoom'
import { useVotingRooms } from './hooks/useVotingRooms' import { useVotingRooms } from './hooks/useVotingRooms'
export { useWakuProposal, useVotingRoom, useVotingRooms } import { useRoomWakuVotes } from './hooks/useRoomWakuVotes'
export { useWakuProposal, useVotingRoom, useVotingRooms, useRoomWakuVotes }