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 }