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 { arr, hashMap } = this.wakuMessages[name]
messages
.map(decodeFunction)
const decodedMessages = messages.map(decodeFunction).filter((e): e is T => !!e)
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))
.forEach((e) => {
if (e) {
@ -117,7 +126,7 @@ export class WakuMessaging {
protected addressesBalances: { [address: string]: BigNumber | undefined } = {}
protected lastBlockBalances = 0
protected async updateBalances(newAddress?: string) {
protected async updateBalances(newAddresses?: string[]) {
const addressesToUpdate: { [addr: string]: boolean } = {}
const addAddressToUpdate = (addr: string) => {
@ -130,22 +139,11 @@ export class WakuMessaging {
if (this.lastBlockBalances != currentBlock) {
Object.keys(this.addressesBalances).forEach(addAddressToUpdate)
if (newAddress) addAddressToUpdate(newAddress)
Object.values(this.wakuMessages).forEach((wakuMessage) =>
wakuMessage.arr.forEach((msg) => wakuMessage.tokenCheckArray.forEach((field) => addAddressToUpdate(msg[field])))
)
newAddresses?.forEach(addAddressToUpdate)
} else {
Object.values(this.wakuMessages).forEach((wakuMessage) =>
wakuMessage.arr.forEach((msg) =>
wakuMessage.tokenCheckArray.forEach((field) => {
const address = msg[field]
if (!this.addressesBalances[address]) {
addAddressToUpdate(address)
}
newAddresses?.forEach((newAddress) => {
if (!this.addressesBalances[newAddress]) addAddressToUpdate(newAddress)
})
)
)
if (newAddress && !this.addressesBalances[newAddress]) addAddressToUpdate(newAddress)
}
const addressesToUpdateArray = Object.keys(addressesToUpdate)
@ -170,7 +168,7 @@ export class WakuMessaging {
}
public async getTokenBalance(address: string) {
await this.updateBalances(address)
await this.updateBalances([address])
return this.addressesBalances[address] ?? undefined
}
}

View File

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

View File

@ -1,11 +1,12 @@
import { VotingContract } from '@status-waku-voting/contracts/abi'
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 { createWaku } from '../utils/createWaku'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { VoteMsg } from '../models/VoteMsg'
import { VotingRoom } from '../types/PollType'
import { DetailedVotingRoom } from '../models/DetailedVotingRoom'
const ABI = [
'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) {
try {
return (await this.getVotingRooms())[id]
} catch {
return undefined
}
}
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)
}
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) {
const msgParams: any = {
domain: {
name: 'Waku proposal',
name: 'Voting Contract',
version: '1',
chainId,
verifyingContract,
@ -43,9 +43,9 @@ export function createSignMsgParams(message: Message, chainId: number, verifying
{ name: 'verifyingContract', type: 'address' },
],
Vote: [
{ name: 'roomIdAndType', type: 'string' },
{ name: 'tokenAmount', type: 'string' },
{ name: 'voter', type: 'string' },
{ name: 'roomIdAndType', type: 'uint256' },
{ name: 'tokenAmount', type: 'uint256' },
{ name: 'voter', type: 'address' },
],
},
}

View File

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

View File

@ -25,6 +25,7 @@ export function ProposalList({ theme, wakuVoting, votes, availableAmount }: Prop
key={votingRoom.id}
mobileVersion={mobileVersion}
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 { useEthers } from '@usedapp/core'
import { FinalBtn, VoteBtnAgainst, VoteBtnFor } from '../Buttons'
@ -9,15 +9,18 @@ import { Modal, Theme } from '@status-waku-voting/react-components'
import { VoteModal } from '../VoteModal'
import { VoteAnimatedModal } from '../VoteAnimatedModal'
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 {
theme: Theme
votingRoom: VotingRoom
availableAmount: number
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 [showVoteModal, setShowVoteModal] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false)
@ -36,24 +39,27 @@ export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunc
setShowConfirmModal(val)
}
const { votes, sum, modifiedVotingRoom } = useRoomWakuVotes(votingRoom, wakuVoting)
return (
<Card>
{showVoteModal && (
<Modal heading={votingRoom.question} setShowModal={setShowVoteModal} theme={theme}>
<VoteModal
votingRoom={votingRoom}
votingRoom={modifiedVotingRoom}
availableAmount={availableAmount}
selectedVote={selectedVoted}
proposingAmount={proposingAmount}
setShowConfirmModal={setNext}
setProposingAmount={setProposingAmount}
wakuVoting={wakuVoting}
/>{' '}
</Modal>
)}
{showConfirmModal && (
<Modal heading={votingRoom.question} setShowModal={hideConfirm} theme={theme}>
<VoteAnimatedModal
votingRoom={votingRoom}
votingRoom={modifiedVotingRoom}
selectedVote={selectedVoted}
setShowModal={hideConfirm}
proposingAmount={proposingAmount}
@ -66,7 +72,7 @@ export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunc
<CardHeading />
)}
<VoteChart votingRoom={votingRoom} selectedVote={selectedVoted} />
<VoteChart votingRoom={modifiedVotingRoom} selectedVote={selectedVoted} />
<CardButtons>
{votingRoom.voteWinner ? (
@ -100,7 +106,7 @@ export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunc
{' '}
<ViewLink address={'#'} />
</CardViewLink>
<VoteSubmitButton votes={15} disabled={!account} />
<VoteSubmitButton votes={sum.toNumber()} disabled={!account} onClick={() => wakuVoting.commitVotes(votes)} />
</CardVoteBottom>
</Card>
)

View File

@ -5,11 +5,17 @@ import { VoteSendingBtn } from '../Buttons'
interface VoteSubmitButtonProps {
votes: number
disabled: boolean
onClick: () => void
}
export function VoteSubmitButton({ votes, disabled }: VoteSubmitButtonProps) {
export function VoteSubmitButton({ votes, disabled, onClick }: VoteSubmitButtonProps) {
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
}

View File

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

View File

@ -69,7 +69,7 @@ export function ProposalVoteMobile({ wakuVoting, availableAmount }: ProposalVote
<CardVoteBottom>
{' '}
<VoteSubmitButton votes={15} disabled={!account} />
<VoteSubmitButton votes={15} disabled={!account} onClick={() => null} />
</CardVoteBottom>
</Card>
)
@ -80,6 +80,8 @@ export const Card = styled.div`
flex-direction: column;
justify-content: space-between;
align-items: center;
width: 100vw;
margin: 0px;
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 { useVotingRoom } from './hooks/useVotingRoom'
import { useVotingRooms } from './hooks/useVotingRooms'
export { useWakuProposal, useVotingRoom, useVotingRooms }
import { useRoomWakuVotes } from './hooks/useRoomWakuVotes'
export { useWakuProposal, useVotingRoom, useVotingRooms, useRoomWakuVotes }