Skip to content

Commit

Permalink
fix: top voters chart (#1251)
Browse files Browse the repository at this point in the history
* fix: use Dayjs for TopVoters chart, add more tests for top voters cases

* chore: update top voters chart copy

* fix: use Dayjs to calculate dates for voting stats

* chore: sort top voters by lastVoted in ascending order as secondary criteria

* refactor: address pr comments
  • Loading branch information
1emu committed Sep 11, 2023
1 parent 6396428 commit 8766945
Show file tree
Hide file tree
Showing 12 changed files with 2,756 additions and 2,308 deletions.
235 changes: 179 additions & 56 deletions src/back/services/vote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,78 +3,201 @@ import { def, get } from 'bdd-lazy-var/getter'
import { SnapshotService } from '../../services/SnapshotService'
import Time from '../../utils/date/Time'
import { getPreviousMonthStartAndEnd } from '../../utils/date/getPreviousMonthStartAndEnd'
import { SNAPSHOT_VOTES_AUGUST_2023 } from '../../utils/votes/utils.testData'
import { SNAPSHOT_VOTES_30_DAYS } from '../../utils/votes/Votes-30days'
import { SNAPSHOT_VOTES_AUGUST_2023 } from '../../utils/votes/Votes-August-2023'

import { VoteService } from './vote'

import clearAllMocks = jest.clearAllMocks

describe('getTopVoters', () => {
const firstOfAugust = Time.utc('2023-08-1T00:00:000Z').toDate()
const { start, end } = getPreviousMonthStartAndEnd(firstOfAugust)
describe('when fetching top voters for August 2023', () => {
const firstOfAugust = Time.utc('2023-08-01T00:00:00.000Z').toDate()
const { start, end } = getPreviousMonthStartAndEnd(firstOfAugust)
beforeEach(() => {
jest.clearAllMocks()
jest.spyOn(SnapshotService, 'getAllVotesBetweenDates').mockResolvedValue(SNAPSHOT_VOTES_AUGUST_2023)
})

beforeAll(() => {
jest.spyOn(SnapshotService, 'getAllVotesBetweenDates').mockResolvedValue(SNAPSHOT_VOTES_AUGUST_2023)
})
describe('when fetching the top 3 voters', () => {
def('topVoters', async () => await VoteService.getTopVoters(start, end, 3))

describe('when fetching the top 3', () => {
def('topVoters', async () => await VoteService.getTopVoters(start, end, 3))
it('should return the top 3 voters sorted by votes in descending order', async () => {
expect(await get.topVoters).toEqual([
{
address: '0x4534c46ea854c9a302d3dc95b2d3253ae6a28abc',
lastVoted: 1693517313,
votes: 9,
},
{
address: '0x2ae9070b029d05d8e6516aec0475002c53595a9d',
lastVoted: 1693523735,
votes: 6,
},
{
address: '0x703b6e7d10f9ab127bcfcb2dd9985b6b24ba1152',
lastVoted: 1693518089,
votes: 3,
},
])
})
})

describe('when called with the default params', () => {
def('topVoters', async () => await VoteService.getTopVoters(start, end))

it('should return the top 5 voters sorted by votes in descending order', async () => {
expect(await get.topVoters).toEqual([
{
address: '0x4534c46ea854c9a302d3dc95b2d3253ae6a28abc',
lastVoted: 1693517313,
votes: 9,
},
{
address: '0x2ae9070b029d05d8e6516aec0475002c53595a9d',
lastVoted: 1693523735,
votes: 6,
},
{
address: '0x703b6e7d10f9ab127bcfcb2dd9985b6b24ba1152',
lastVoted: 1693518089,
votes: 3,
},
{
address: '0x15f51853d17e89d97980883eef4c6aba6ba82ed5',
lastVoted: 1692998273,
votes: 1,
},
{
address: '0xc95ed3844cfc92e68ab7b0cd72e832a3f6eb0259',
lastVoted: 1693516292,
votes: 1,
},
])
})
describe('when there are no votes on the selected time period', () => {
beforeEach(() => {
clearAllMocks()
jest.spyOn(SnapshotService, 'getAllVotesBetweenDates').mockResolvedValue([])
})

it('should return the top 3 voters sorted by votes in descending order', async () => {
expect(await get.topVoters).toEqual([
{
address: '0xa2f8cd45cd7ea14bce6e87f177cf9df928a089a5',
votes: 79,
},
{
address: '0x2684a202a374d87bb321a744482b89bf6deaf8bd',
votes: 75,
},
{
address: '0x8218a2445679e38f358e42f88fe2125c98440d59',
votes: 73,
},
])
it('should return an empty array', async () => {
expect(await get.topVoters).toEqual([])
})
})
})
})

describe('when called with the default params', () => {
def('topVoters', async () => await VoteService.getTopVoters(start, end))

it('should return the top 5 voters sorted by votes in descending order', async () => {
expect(await get.topVoters).toEqual([
{
address: '0xa2f8cd45cd7ea14bce6e87f177cf9df928a089a5',
votes: 79,
},
{
address: '0x2684a202a374d87bb321a744482b89bf6deaf8bd',
votes: 75,
},
{
address: '0x8218a2445679e38f358e42f88fe2125c98440d59',
votes: 73,
},
{
address: '0xa77294828d42b538890fa6e97adffe9305536171',
votes: 53,
},
{
address: '0x4534c46ea854c9a302d3dc95b2d3253ae6a28abc',
votes: 21,
},
])
describe('when fetching the top 10 voters for a random month span', () => {
const septemberSeventh = Time.utc('2023-09-07T00:00:00.000Z')
const aMonthBefore = septemberSeventh.subtract(1, 'month').startOf('day').toDate()
beforeAll(() => {
jest.clearAllMocks()
jest.spyOn(SnapshotService, 'getAllVotesBetweenDates').mockResolvedValue(SNAPSHOT_VOTES_30_DAYS)
})

describe('when there are no votes on the selected time period', () => {
beforeEach(() => {
clearAllMocks()
jest.spyOn(SnapshotService, 'getAllVotesBetweenDates').mockResolvedValue([])
})
describe('when fetching the top 10 voters', () => {
def('topVoters', async () => await VoteService.getTopVoters(aMonthBefore, septemberSeventh.toDate(), 10))

it('should return an empty array', async () => {
expect(await get.topVoters).toEqual([])
it('should return the top 10 voters sorted by votes in descending order', async () => {
const topVoters = await get.topVoters
expect(topVoters).toEqual([
{
address: '0x338571a641d8c43f9e5a306300c5d89e0cb2cfaf',
lastVoted: 1691429934,
votes: 11,
},
{
address: '0x2f30998c61b21179aa237f77fd578ba8679eb43d',
lastVoted: 1691434211,
votes: 11,
},
{
address: '0xa8f98b7b2039256ba66a12fead20e750ecf9670d',
lastVoted: 1691415396,
votes: 8,
},
{
address: '0xed7461fd98758a84b76d6e941cbb92891443c36f',
lastVoted: 1691432207,
votes: 6,
},
{
address: '0x26871b48edad35f81901ab9002e656e594397e7d',
lastVoted: 1691420208,
votes: 5,
},
{
address: '0x613e052555ac74ff6af0fc64e40e8035c1e9dcf8',
lastVoted: 1691417293,
votes: 4,
},
{
address: '0x1d7886346175e34c614b71d0e2369c7f0e350d07',
lastVoted: 1691438203,
votes: 4,
},
{
address: '0xca204bba80813f79c97a7240d662773d626d23ba',
lastVoted: 1691369051,
votes: 3,
},
{
address: '0x501956ace74edb360c56d0c0fa74b0066fd6f486',
lastVoted: 1691432175,
votes: 3,
},
{
address: '0x7bbea9c18cd0541acab8c19da2b11d0c03faef1c',
lastVoted: 1691432462,
votes: 3,
},
])
expect(topVoters[0].lastVoted).toBeLessThan(topVoters[1].lastVoted)
})
})
})
})

describe('getSortedCountPerUser', () => {
it('sorts the vote count per user and chronologically', () => {
const sortedVotes = VoteService.getSortedVoteCountPerUser(SNAPSHOT_VOTES_AUGUST_2023)
expect(sortedVotes).toEqual([
{
address: '0x4534c46ea854c9a302d3dc95b2d3253ae6a28abc',
lastVoted: 1693517313,
votes: 9,
},
{
address: '0x2ae9070b029d05d8e6516aec0475002c53595a9d',
lastVoted: 1693523735,
votes: 6,
},
{
address: '0x703b6e7d10f9ab127bcfcb2dd9985b6b24ba1152',
lastVoted: 1693518089,
votes: 3,
},
{
address: '0x15f51853d17e89d97980883eef4c6aba6ba82ed5',
lastVoted: 1692998273,
votes: 1,
},
{
address: '0xc95ed3844cfc92e68ab7b0cd72e832a3f6eb0259',
lastVoted: 1693516292,
votes: 1,
},
{
address: '0x003a3eb1a1d2ad3bea19ae06324727beeeec2e34',
lastVoted: 1693521196,
votes: 1,
},
])

expect(sortedVotes[3].votes).toEqual(sortedVotes[4].votes)
expect(sortedVotes[3].lastVoted).toBeLessThan(sortedVotes[4].lastVoted)
expect(sortedVotes[4].votes).toEqual(sortedVotes[5].votes)
expect(sortedVotes[4].lastVoted).toBeLessThan(sortedVotes[5].lastVoted)
})
})
33 changes: 25 additions & 8 deletions src/back/services/vote.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes'
import { VOTES_VP_THRESHOLD } from '../../constants'
import VoteModel from '../../entities/Votes/model'
import { Voter } from '../../entities/Votes/types'
import { VoteCount, Voter } from '../../entities/Votes/types'
import { SnapshotService } from '../../services/SnapshotService'

const DEFAULT_TOP_VOTERS_LIMIT = 5
Expand All @@ -14,20 +14,37 @@ export class VoteService {

static async getTopVoters(start: Date, end: Date, limit = DEFAULT_TOP_VOTERS_LIMIT) {
const votes = await SnapshotService.getAllVotesBetweenDates(new Date(start), new Date(end))
return this.countVotesByUser(votes)
.sort((a, b) => b['votes'] - a['votes'])
.slice(0, limit)
return this.getSortedVoteCountPerUser(votes).slice(0, limit)
}

private static countVotesByUser(votes: SnapshotVote[]) {
public static getSortedVoteCountPerUser(votes: SnapshotVote[]) {
const votesByUser = votes
.filter((vote) => vote.vp && vote.vp > VOTES_VP_THRESHOLD)
.reduce((acc, vote) => {
const address = vote.voter.toLowerCase()
acc[address] = (acc[address] || 0) + 1
if (!acc[address]) {
acc[address] = {
votes: 1,
lastVoted: vote.created,
}
} else {
acc[address] = {
votes: acc[address].votes + 1,
lastVoted: acc[address].lastVoted < vote.created ? vote.created : acc[address].lastVoted,
}
}
return acc
}, {} as Record<string, number>)
}, {} as Record<string, VoteCount>)

return Object.entries(votesByUser).map<Voter>(([address, votes]) => ({ address, votes }))
const voteCountPerUser = Object.entries(votesByUser).map<Voter>(([address, voteCount]) => ({
address,
...voteCount,
}))
return voteCountPerUser.sort((a, b) => {
if (b.votes !== a.votes) {
return b.votes - a.votes
}
return a.lastVoted - b.lastVoted
})
}
}
22 changes: 11 additions & 11 deletions src/clients/SnapshotGraphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,19 +258,19 @@ export class SnapshotGraphql extends API {

try {
while (hasNext) {
const variables = {
space: SNAPSHOT_SPACE,
first: batchSize,
start: created,
end: getQueryTimestamp(end.getTime()),
}

const result = await this.fetch<SnapshotVoteResponse>(
GRAPHQL_ENDPOINT,
this.options()
.method('POST')
.json({
query,
variables: {
space: SNAPSHOT_SPACE,
first: batchSize,
start: created,
end: getQueryTimestamp(end.getTime()),
},
})
this.options().method('POST').json({
query,
variables,
})
)

const results = result?.data?.votes
Expand Down
2 changes: 1 addition & 1 deletion src/clients/SnapshotGraphqlTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type SnapshotVote = {
metadata?: Record<string, unknown>
proposal?: {
id: string
title: string
title?: string
choices: string[]
scores?: number[]
state?: string | 'active' | 'closed'
Expand Down
12 changes: 7 additions & 5 deletions src/components/Home/TopVoters.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useMemo } from 'react'

import { Card } from 'decentraland-ui/dist/components/Card/Card'
import { Header } from 'decentraland-ui/dist/components/Header/Header'
Expand All @@ -7,6 +7,8 @@ import { Table } from 'decentraland-ui/dist/components/Table/Table'
import { VOTES_VP_THRESHOLD } from '../../constants'
import useFormatMessage from '../../hooks/useFormatMessage'
import useTopVoters from '../../hooks/useTopVoters'
import Time from '../../utils/date/Time'
import { getAMonthAgo } from '../../utils/date/aMonthAgo'
import Helper from '../Helper/Helper'

import HomeLoader from './HomeLoader'
Expand All @@ -17,12 +19,12 @@ const createRow = ({ address, votes }: { address: string; votes: number }, idx:
return <TopVotersRow key={idx} address={address} votes={votes} rank={idx + 1} />
}

const now = new Date()
const start = new Date(now.getFullYear(), now.getMonth() - 1, now.getDay())

function TopVoters() {
const now = useMemo(() => Time().toDate(), [])
const aMonthAgo = useMemo(() => getAMonthAgo(now), [])

const t = useFormatMessage()
const { topVoters, isLoadingTopVoters } = useTopVoters(start, now)
const { topVoters, isLoadingTopVoters } = useTopVoters(aMonthAgo, now)

return (
<Card className="TopVoters">
Expand Down
8 changes: 6 additions & 2 deletions src/entities/Votes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export type ChoiceColor = 'approve' | 'reject' | number

export type SelectedVoteChoice = { choice?: string; choiceIndex?: number }

export type Voter = {
address: string
export type VoteCount = {
votes: number
lastVoted: number
}

export type Voter = {
address: string
} & VoteCount
Loading

0 comments on commit 8766945

Please sign in to comment.