Skip to content

Commit

Permalink
Feature/voting verification (pancakeswap#1677)
Browse files Browse the repository at this point in the history
* perf(voting): Verify votes after they have loaded

* refactor: Sort proposals by newest
  • Loading branch information
hachiojidev authored Jul 8, 2021
1 parent 899a63a commit 0d1890e
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/state/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ export const useGetProposal = (proposalId: string) => {

export const useGetVotes = (proposalId: string) => {
const votes = useSelector((state: State) => state.voting.votes[proposalId])
return votes || []
return votes ? votes.filter((vote) => vote._inValid !== true) : []
}

export const useGetVotingStateLoadingStatus = () => {
Expand Down
1 change: 1 addition & 0 deletions src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export interface Vote {
votingPower: string
verificationHash: string
}
_inValid?: boolean
}

export enum VotingStateLoadingStatus {
Expand Down
73 changes: 44 additions & 29 deletions src/state/voting/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ export const getProposals = async (first = 5, skip = 0, state = ProposalState.AC
SNAPSHOT_API,
gql`
query getProposals($first: Int!, $skip: Int!, $state: String!) {
proposals(first: $first, skip: $skip, where: { space_in: "cake.eth", state: $state }) {
proposals(
first: $first
skip: $skip
orderBy: "end"
orderDirection: desc
where: { space_in: "cake.eth", state: $state }
) {
id
title
body
Expand Down Expand Up @@ -83,8 +89,43 @@ export const getVotes = async (first: number, skip: number, where: VoteWhere): P
return response.votes
}

export const getAllVotes = async (proposalId: string, block?: number, votesPerChunk = 1000): Promise<Vote[]> => {
export const getVoteVerificationStatuses = async (
votes: Vote[],
block?: number,
): Promise<{ [key: string]: boolean }> => {
const blockNumber = block || (await simpleRpcProvider.getBlockNumber())

const votesToVerify = votes.map((vote) => ({
address: vote.voter,
verificationHash: vote.metadata?.verificationHash,
total: vote.metadata?.votingPower,
}))
const response = await fetch(`${SNAPSHOT_VOTING_API}/verify`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
block: blockNumber,
votes: votesToVerify,
}),
})

if (!response.ok) {
throw new Error(response.statusText)
}

const data = await response.json()
return votes.reduce((accum, vote) => {
return {
...accum,
[vote.id]: data.data[vote.voter.toLowerCase()]?.isValid === true,
}
}, {})
}

export const getAllVotes = async (proposalId: string, block?: number, votesPerChunk = 1000): Promise<Vote[]> => {
// const blockNumber = block || (await simpleRpcProvider.getBlockNumber())
return new Promise((resolve, reject) => {
let votes: Vote[] = []

Expand All @@ -93,33 +134,7 @@ export const getAllVotes = async (proposalId: string, block?: number, votesPerCh
const voteChunk = await getVotes(votesPerChunk, newSkip, { proposal: proposalId })

if (voteChunk.length === 0) {
// Verify all the votes
const votesToVerify = votes.map((vote) => ({
address: vote.voter,
verificationHash: vote.metadata?.verificationHash,
total: vote.metadata?.votingPower,
}))
const response = await fetch(`${SNAPSHOT_VOTING_API}/verify`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
block: blockNumber,
votes: votesToVerify,
}),
})

if (!response.ok) {
throw new Error(response.statusText)
}

const data = await response.json()
const verifiedVotes = votes.filter((vote) => {
return data.data[vote.voter.toLowerCase()]?.isValid === true
})

resolve(verifiedVotes)
resolve(votes)
} else {
votes = [...votes, ...voteChunk]
fetchVoteChunk(newSkip + votesPerChunk)
Expand Down
29 changes: 27 additions & 2 deletions src/state/voting/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable no-param-reassign */
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { merge } from 'lodash'
import { Proposal, ProposalState, VotingStateLoadingStatus, VotingState, Vote } from 'state/types'
import { getAllVotes, getProposal, getProposals } from './helpers'
import { Proposal, ProposalState, VotingStateLoadingStatus, VotingState, Vote, State } from 'state/types'
import { getAllVotes, getProposal, getProposals, getVoteVerificationStatuses } from './helpers'

const initialState: VotingState = {
proposalLoadingStatus: VotingStateLoadingStatus.INITIAL,
Expand Down Expand Up @@ -33,11 +33,36 @@ export const fetchVotes = createAsyncThunk<
return { votes: response, proposalId }
})

export const verifyVotes = createAsyncThunk<
{ results: { [key: string]: boolean }; proposalId: string },
{ proposalId: string; snapshot?: string },
{ state: State }
>('voting/verifyVotes', async ({ proposalId, snapshot }, { getState }) => {
const state = getState()
const proposalVotes = state.voting.votes[proposalId]
const response = await getVoteVerificationStatuses(proposalVotes, Number(snapshot))
return { results: response, proposalId }
})

export const votingSlice = createSlice({
name: 'voting',
initialState,
reducers: {},
extraReducers: (builder) => {
// Verify Votes
builder.addCase(verifyVotes.fulfilled, (state, action) => {
const { proposalId, results } = action.payload

if (state.votes[proposalId]) {
state.votes[proposalId] = state.votes[proposalId].map((vote) => {
return {
...vote,
_inValid: results[vote.id] === false,
}
})
}
})

// Fetch Proposals
builder.addCase(fetchProposals.pending, (state) => {
state.proposalLoadingStatus = VotingStateLoadingStatus.LOADING
Expand Down
13 changes: 10 additions & 3 deletions src/views/Voting/Proposal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Link, useParams } from 'react-router-dom'
import { useAppDispatch } from 'state'
import { ProposalState, VotingStateLoadingStatus } from 'state/types'
import { useGetProposal, useGetVotingStateLoadingStatus, useGetVotes, useGetProposalLoadingStatus } from 'state/hooks'
import { fetchProposal, fetchVotes } from 'state/voting'
import { fetchProposal, fetchVotes, verifyVotes } from 'state/voting'
import { useTranslation } from 'contexts/Localization'
import Container from 'components/Layout/Container'
import ReactMarkdown from 'components/ReactMarkdown'
Expand Down Expand Up @@ -38,10 +38,17 @@ const Proposal = () => {

// We have to wait for the proposal to load before fetching the votes because we need to include the snapshot
useEffect(() => {
if (proposalId && snapshot) {
if (proposalId && snapshot && votes.length === 0) {
dispatch(fetchVotes({ proposalId, block: Number(snapshot) }))
}
}, [proposalId, snapshot, dispatch])
}, [proposalId, snapshot, votes, dispatch])

// After the votes have been loaded verify
useEffect(() => {
if (voteLoadingStatus === VotingStateLoadingStatus.IDLE) {
dispatch(verifyVotes({ proposalId, snapshot }))
}
}, [voteLoadingStatus, proposalId, snapshot, dispatch])

if (!proposal) {
return <PageLoader />
Expand Down

0 comments on commit 0d1890e

Please sign in to comment.