From e1a1203a58e91d507c6371f2858f0affb2e3ee5c Mon Sep 17 00:00:00 2001 From: Szymon Szlachtowicz <38212223+Szymx95@users.noreply.github.com> Date: Mon, 20 Sep 2021 16:34:54 +0200 Subject: [PATCH] Add proposal voting (#78) --- packages/core/src/classes/WakuMessaging.ts | 36 +++++++------ packages/core/src/classes/WakuPolling.ts | 4 +- packages/core/src/classes/WakuVoting.ts | 51 ++++++++++++++++++- .../core/src/models/DetailedVotingRoom.ts | 23 +++++++++ packages/core/src/models/VoteMsg.ts | 8 +-- .../src/components/ProposalCard.tsx | 6 ++- .../src/components/ProposalList.tsx | 1 + .../ProposalVoteCard/ProposalVote.tsx | 18 ++++--- .../ProposalVoteCard/VoteSubmitButton.tsx | 10 +++- .../src/components/VoteModal.tsx | 25 +++++---- .../components/mobile/ProposalVoteMobile.tsx | 4 +- .../src/hooks/useRoomWakuVotes.ts | 32 ++++++++++++ packages/proposal-hooks/src/index.ts | 3 +- 13 files changed, 172 insertions(+), 49 deletions(-) create mode 100644 packages/core/src/models/DetailedVotingRoom.ts create mode 100644 packages/proposal-hooks/src/hooks/useRoomWakuVotes.ts diff --git a/packages/core/src/classes/WakuMessaging.ts b/packages/core/src/classes/WakuMessaging.ts index 7931be7..df4aca4 100644 --- a/packages/core/src/classes/WakuMessaging.ts +++ b/packages/core/src/classes/WakuMessaging.ts @@ -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) - } - }) - ) - ) - if (newAddress && !this.addressesBalances[newAddress]) addAddressToUpdate(newAddress) + newAddresses?.forEach((newAddress) => { + if (!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 } } diff --git a/packages/core/src/classes/WakuPolling.ts b/packages/core/src/classes/WakuPolling.ts index ad18856..a5ae7c1 100644 --- a/packages/core/src/classes/WakuPolling.ts +++ b/packages/core/src/classes/WakuPolling.ts @@ -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) { diff --git a/packages/core/src/classes/WakuVoting.ts b/packages/core/src/classes/WakuVoting.ts index b734228..f0fefcd 100644 --- a/packages/core/src/classes/WakuVoting.ts +++ b/packages/core/src/classes/WakuVoting.ts @@ -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) { - return (await this.getVotingRooms())[id] + 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 } + } } diff --git a/packages/core/src/models/DetailedVotingRoom.ts b/packages/core/src/models/DetailedVotingRoom.ts new file mode 100644 index 0000000..cad0d87 --- /dev/null +++ b/packages/core/src/models/DetailedVotingRoom.ts @@ -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) + } + }) + } +} diff --git a/packages/core/src/models/VoteMsg.ts b/packages/core/src/models/VoteMsg.ts index 257d7f4..55e42f9 100644 --- a/packages/core/src/models/VoteMsg.ts +++ b/packages/core/src/models/VoteMsg.ts @@ -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' }, ], }, } diff --git a/packages/proposal-components/src/components/ProposalCard.tsx b/packages/proposal-components/src/components/ProposalCard.tsx index b3155e6..8adcb4e 100644 --- a/packages/proposal-components/src/components/ProposalCard.tsx +++ b/packages/proposal-components/src/components/ProposalCard.tsx @@ -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 ( mobileVersion && history.push(`/votingRoom/${votingRoom.id.toString()}`)}> - + ) } diff --git a/packages/proposal-components/src/components/ProposalList.tsx b/packages/proposal-components/src/components/ProposalList.tsx index adfd3c8..7b7efcf 100644 --- a/packages/proposal-components/src/components/ProposalList.tsx +++ b/packages/proposal-components/src/components/ProposalList.tsx @@ -25,6 +25,7 @@ export function ProposalList({ theme, wakuVoting, votes, availableAmount }: Prop key={votingRoom.id} mobileVersion={mobileVersion} availableAmount={availableAmount} + wakuVoting={wakuVoting} /> ) })} diff --git a/packages/proposal-components/src/components/ProposalVoteCard/ProposalVote.tsx b/packages/proposal-components/src/components/ProposalVoteCard/ProposalVote.tsx index 32176bb..291aeda 100644 --- a/packages/proposal-components/src/components/ProposalVoteCard/ProposalVote.tsx +++ b/packages/proposal-components/src/components/ProposalVoteCard/ProposalVote.tsx @@ -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 ( {showVoteModal && ( {' '} )} {showConfirmModal && ( )} - + {votingRoom.voteWinner ? ( @@ -100,7 +106,7 @@ export function ProposalVote({ votingRoom, theme, availableAmount, hideModalFunc {' '} - + wakuVoting.commitVotes(votes)} /> ) diff --git a/packages/proposal-components/src/components/ProposalVoteCard/VoteSubmitButton.tsx b/packages/proposal-components/src/components/ProposalVoteCard/VoteSubmitButton.tsx index 16d3e4b..c6a076d 100644 --- a/packages/proposal-components/src/components/ProposalVoteCard/VoteSubmitButton.tsx +++ b/packages/proposal-components/src/components/ProposalVoteCard/VoteSubmitButton.tsx @@ -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 {addCommas(votes)} votes need saving + return ( + + {' '} + {addCommas(votes)} votes need saving + + ) } return null } diff --git a/packages/proposal-components/src/components/VoteModal.tsx b/packages/proposal-components/src/components/VoteModal.tsx index 9c9a1de..0ea0dbb 100644 --- a/packages/proposal-components/src/components/VoteModal.tsx +++ b/packages/proposal-components/src/components/VoteModal.tsx @@ -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 && Not enought ABC to vote} - {funds && - (selectedVote === 0 ? ( - setShowConfirmModal(true)}> - Vote Against - - ) : ( - setShowConfirmModal(true)}> - Vote For - - ))} + {funds && ( + { + wakuVoting.sendVote(votingRoom.id, selectedVote, BigNumber.from(proposingAmount)) + setShowConfirmModal(true) + }} + > + {selectedVote === 0 ? `Vote Against` : `Vote For`} + + )} ) } diff --git a/packages/proposal-components/src/components/mobile/ProposalVoteMobile.tsx b/packages/proposal-components/src/components/mobile/ProposalVoteMobile.tsx index 8debb4e..348e02d 100644 --- a/packages/proposal-components/src/components/mobile/ProposalVoteMobile.tsx +++ b/packages/proposal-components/src/components/mobile/ProposalVoteMobile.tsx @@ -69,7 +69,7 @@ export function ProposalVoteMobile({ wakuVoting, availableAmount }: ProposalVote {' '} - + null} /> ) @@ -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; ` diff --git a/packages/proposal-hooks/src/hooks/useRoomWakuVotes.ts b/packages/proposal-hooks/src/hooks/useRoomWakuVotes.ts new file mode 100644 index 0000000..c1c74d3 --- /dev/null +++ b/packages/proposal-hooks/src/hooks/useRoomWakuVotes.ts @@ -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([]) + 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 } +} diff --git a/packages/proposal-hooks/src/index.ts b/packages/proposal-hooks/src/index.ts index 318b9e3..74e1411 100644 --- a/packages/proposal-hooks/src/index.ts +++ b/packages/proposal-hooks/src/index.ts @@ -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 }