diff --git a/packages/dai-plugin-governance/src/GovPollingService.js b/packages/dai-plugin-governance/src/GovPollingService.js index 961fb9f28..83b73fab5 100644 --- a/packages/dai-plugin-governance/src/GovPollingService.js +++ b/packages/dai-plugin-governance/src/GovPollingService.js @@ -1,4 +1,5 @@ import { PrivateService } from '@makerdao/services-core'; +import BigNumber from 'bignumber.js'; import { POLLING, BATCH_POLLING } from './utils/constants'; import { MKR } from './utils/constants'; import { fromBuffer, toBuffer, paddedArray } from './utils/helpers'; @@ -310,6 +311,60 @@ export default class GovPollingService extends PrivateService { return votes; } + async getTallyPlurality(pollId) { + const poll = await this._getPoll(pollId); + if (!poll) return null; + + const endUnix = Math.floor(poll.endDate / 1000); + const currentVotes = await this.get( + 'govQueryApi' + ).getMkrSupportRankedChoice(pollId, endUnix); + + const numVoters = currentVotes.length; + + const resultsObject = currentVotes.reduce((acc, cur) => { + if (acc[cur.optionIdRaw]) { + acc[cur.optionIdRaw] = new BigNumber(acc[cur.optionIdRaw]).plus( + cur.mkrSupport + ); + } else { + acc[cur.optionIdRaw] = new BigNumber(cur.mkrSupport); + } + return acc; + }, {}); + + const summedSupport = Object.keys(resultsObject).map(option => ({ + optionId: option, + mkrSupport: resultsObject[option] + })); + + const sorted = summedSupport.sort((prev, next) => + prev.mkrSupport.gt(next.mkrSupport) ? -1 : 1 + ); + + const winner = (sorted[0] ? sorted[0].optionId : 0).toString(); + + const totalMkrParticipation = summedSupport.reduce( + (acc, cur) => new BigNumber(cur.mkrSupport || 0).plus(acc), + new BigNumber(0) + ); + + const options = summedSupport.reduce((a, v) => { + a[v.optionId] = { + mkrSupport: new BigNumber(v.mkrSupport || 0), + winner: v.optionId === winner + }; + return a; + }, {}); + + return { + winner, + totalMkrParticipation, + numVoters, + options + }; + } + async getTallyRankedChoiceIrv(pollId) { const poll = await this._getPoll(pollId); if (!poll) return {}; diff --git a/packages/dai-plugin-governance/test/GovPollingService.test.js b/packages/dai-plugin-governance/test/GovPollingService.test.js index 249965161..1318bca76 100644 --- a/packages/dai-plugin-governance/test/GovPollingService.test.js +++ b/packages/dai-plugin-governance/test/GovPollingService.test.js @@ -26,7 +26,9 @@ import { dummyBallotDontMoveToEliminated, dummyBallotDontMoveToEliminatedExpect, dummyBallotStopWhenOneRemains, - dummyBallotStopWhenOneRemainsExpect + dummyBallotStopWhenOneRemainsExpect, + dummyMkrGetMkrSupportRCForPluralityData, + dummyMkrGetMkrSupportRCForPluralityDataAdjusted } from './fixtures'; import { MKR } from '../src/utils/constants'; @@ -328,6 +330,66 @@ test('should correctly decode ranked choice options from event logs', () => { expect(decodedOptions).toEqual(expectedOptions); }); +test('plurality tally', async () => { + govQueryApiService.getMkrSupportRankedChoice = jest.fn( + () => dummyMkrGetMkrSupportRCForPluralityData + ); + govPollingService._getPoll = jest.fn(() => ({ + endDate: 123 + })); + const tally = await govPollingService.getTallyPlurality(); + + const expectedResult = { + winner: '1', + totalMkrParticipation: '809', + numVoters: 5, + options: { + '0': { + mkrSupport: '109', + winner: false + }, + '1': { + mkrSupport: '700', + winner: true + } + } + }; + + expect(JSON.parse(JSON.stringify(tally))).toEqual(expectedResult); +}); + +test('plurality tally with adjusted votes', async () => { + govQueryApiService.getMkrSupportRankedChoice = jest.fn( + () => dummyMkrGetMkrSupportRCForPluralityDataAdjusted + ); + govPollingService._getPoll = jest.fn(() => ({ + endDate: 123 + })); + const tally = await govPollingService.getTallyPlurality(); + + const expectedResult = { + winner: '2', + totalMkrParticipation: '2041', + numVoters: 7, + options: { + '0': { + mkrSupport: '109', + winner: false + }, + '1': { + mkrSupport: '700', + winner: false + }, + '2': { + mkrSupport: '1232', + winner: true + } + } + }; + + expect(JSON.parse(JSON.stringify(tally))).toEqual(expectedResult); +}); + // IRV algo tests test('ranked choice tally with majority', async () => { diff --git a/packages/dai-plugin-governance/test/fixtures.js b/packages/dai-plugin-governance/test/fixtures.js index 8d86b2d29..380a0dabe 100644 --- a/packages/dai-plugin-governance/test/fixtures.js +++ b/packages/dai-plugin-governance/test/fixtures.js @@ -16,6 +16,59 @@ export const dummyMkrSupportData = [ } ]; +export const dummyMkrGetMkrSupportRCForPluralityData = [ + { + optionIdRaw: '1', + mkrSupport: '40' + }, + { + optionIdRaw: '1', + mkrSupport: '60' + }, + { + optionIdRaw: '0', + mkrSupport: '77' + }, + { + optionIdRaw: '0', + mkrSupport: '32' + }, + { + optionIdRaw: '1', + mkrSupport: '600' + } +]; +export const dummyMkrGetMkrSupportRCForPluralityDataAdjusted = [ + { + optionIdRaw: '1', + mkrSupport: '40' + }, + { + optionIdRaw: '1', + mkrSupport: '60' + }, + { + optionIdRaw: '0', + mkrSupport: '77' + }, + { + optionIdRaw: '0', + mkrSupport: '32' + }, + { + optionIdRaw: '1', + mkrSupport: '600' + }, + { + optionIdRaw: '2', + mkrSupport: '32' + }, + { + optionIdRaw: '2', + mkrSupport: '1200' + } +]; + export const dummyAllPollsData = [ { creator: '0xeda95d1bdb60f901986f43459151b6d1c734b8a2',