Add proposal voting (#78)
This commit is contained in:
parent
9502bc67b2
commit
e1a1203a58
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export function ProposalList({ theme, wakuVoting, votes, availableAmount }: Prop
|
|||
key={votingRoom.id}
|
||||
mobileVersion={mobileVersion}
|
||||
availableAmount={availableAmount}
|
||||
wakuVoting={wakuVoting}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
`
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
|
|
Loading…
Reference in New Issue