Skip to content
This repository has been archived by the owner on Sep 28, 2022. It is now read-only.

Commit

Permalink
add irv ranked choice tally method to gov polling service
Browse files Browse the repository at this point in the history
  • Loading branch information
jparklev committed May 1, 2020
1 parent 06d4e20 commit 1c81b31
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 7 deletions.
1 change: 1 addition & 0 deletions packages/dai-plugin-governance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@makerdao/currency": "^0.9.5",
"@makerdao/services-core": "^0.9.5",
"assert": "^2.0.0",
"bignumber": "^1.1.0",
"ramda": "^0.25.0",
"web3-utils": "^1.0.0-beta.36"
},
Expand Down
106 changes: 106 additions & 0 deletions packages/dai-plugin-governance/src/GovPollingService.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PrivateService } from '@makerdao/services-core';
import { POLLING } from './utils/constants';
import { MKR } from './utils/constants';
import BigNumber from 'bignumber.js';

const POSTGRES_MAX_INT = 2147483647;

Expand Down Expand Up @@ -92,6 +93,111 @@ export default class GovPollingService extends PrivateService {
return MKR(weights.reduce((acc, cur) => acc + cur.mkrSupport, 0));
}

async getTallyRankedChoiceIrv(pollId) {
const { endDate } = await this._getPoll(pollId);
const endUnix = Math.floor(endDate / 1000);
const votes = await this.get('govQueryApi').getMkrSupportRankedChoice(
pollId,
endUnix
);
const totalMkrParticipation = votes.reduce(
(acc, cur) => BigNumber(cur.mkrSupport || 0).plus(acc),
BigNumber(0)
);

const tally = { rounds: 1, winner: null, options: {} };
const defaultOptionObj = {
firstChoice: BigNumber(0),
transfer: BigNumber(0),
winner: false,
eliminated: false
};

// run the first round
votes.forEach(vote => {
vote.choice = vote.ballot.pop();
if (!tally.options[vote.choice])
tally.options[vote.choice] = { ...defaultOptionObj };

tally.options[vote.choice].firstChoice = BigNumber(
tally.options[vote.choice].firstChoice
).plus(vote.mkrSupport || 0);
});

// does any candidate have the majority after the first round?
Object.entries(tally.options).forEach(([option, { firstChoice }]) => {
if (firstChoice.gt(totalMkrParticipation.div(2))) tally.winner = option;
});

// if so, we're done. Return the winner
if (tally.winner) {
tally.options[tally.winner].winner = true;
return tally;
}

// if we couldn't find a winner based on first preferences, run additionaly irv rounds until we find one
while (!tally.winner) {
tally.rounds++;

// eliminate the weakest candidate
const [optionToEliminate] = Object.entries(tally.options).reduce(
(prv, cur) => {
const [, prvVotes] = prv;
const [, curVotes] = cur;
if (
curVotes.firstChoice
.add(curVotes.transfer)
.lt(prvVotes.firstChoice.add(prvVotes.transfer))
)
return cur;
return prv;
}
);

tally.options[optionToEliminate].eliminated = true;
tally.options[optionToEliminate].transfer = BigNumber(0);

// a vote needs to be moved if...
// 1) it's currently for the eliminated candidate
// 2) there's another choice further down in the voter's preference list
const votesToBeMoved = votes
.filter(vote => vote.choice === optionToEliminate)
.filter(vote => vote.ballot[vote.ballot.length - 1] !== '0');

// move votes to the next choice on their preference list
votesToBeMoved.forEach(vote => {
vote.choice = vote.ballot.pop();
tally.options[vote.choice].transfer = BigNumber(
tally.options[vote.choice].transfer
).plus(vote.mkrSupport || 0);
});

// look for a candidate with the majority
Object.entries(tally.options).forEach(
([option, { firstChoice, transfer }]) => {
if (firstChoice.add(transfer).gt(totalMkrParticipation.div(2)))
tally.winner = option;
}
);

// sanity checks
if (Object.keys(tally.options).length === 2) {
// dead tie. this seems super unlikely, but it should be here for completeness
// return the tally without declaring a winner
return tally;
}
if (Object.keys(tally.options).length === 1) {
// this shouldn't happen
throw new Error(`Invalid ranked choice tally ${tally.options}`);
}

// if we couldn't find one, go for another round
}

tally.options[tally.winner].winner = true;
return tally;
}

async getPercentageMkrVoted(pollId) {
const [voted, total] = await Promise.all([
this.getMkrAmtVoted(pollId),
Expand Down
27 changes: 26 additions & 1 deletion packages/dai-plugin-governance/src/GovQueryApiService.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { PublicService } from '@makerdao/services-core';
import assert from 'assert';
import { netIdtoSpockUrl, netIdtoSpockUrlStaging } from './utils/helpers';
import {
netIdtoSpockUrl,
netIdtoSpockUrlStaging,
toBuffer,
paddedArray
} from './utils/helpers';

export default class QueryApi extends PublicService {
constructor(name = 'govQueryApi') {
Expand Down Expand Up @@ -115,6 +120,26 @@ export default class QueryApi extends PublicService {
return response.timeToBlockNumber.nodes[0];
}

async getMkrSupportRankedChoice(pollId, unixTime) {
const query = `{voteMkrWeightsAtTimeRankedChoice(argPollId: ${pollId}, argUnix: ${unixTime}){
nodes{
optionIdRaw
mkrSupport
}
}
}`;
const response = await this.getQueryResponseMemoized(this.serverUrl, query);

return response.voteMkrWeightsAtTimeRankedChoice.nodes.map(vote => {
const ballotBuffer = toBuffer(vote.optionIdRaw, { endian: 'little' });
const ballot = paddedArray(32 - ballotBuffer.length, ballotBuffer);
return {
...vote,
ballot
};
});
}

async getMkrSupport(pollId, unixTime) {
const query = `{voteOptionMkrWeightsAtTime(argPollId: ${pollId}, argUnix: ${unixTime}){
nodes{
Expand Down
82 changes: 76 additions & 6 deletions packages/dai-plugin-governance/src/utils/helpers.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { createGetCurrency } from '@makerdao/currency';
import {
MKR,
STAGING_MAINNET_URL,
KOVAN_URL,
MAINNET_URL
} from './constants';
import BigNumber from 'bignumber.js';
import { MKR, STAGING_MAINNET_URL, KOVAN_URL, MAINNET_URL } from './constants';

/**
* @desc get network name
Expand Down Expand Up @@ -47,3 +43,77 @@ export const netIdtoSpockUrlStaging = id => {
};

export const getCurrency = createGetCurrency({ MKR });

export const paddedArray = (k, value) =>
Array.from({ length: k })
.map(() => 0)
.concat(...value);

export const toBuffer = (number, opts) => {
let buf;
let len;
let endian;
let hex = new BigNumber(number).toString(16);
let size;
let hx;

if (!opts) {
opts = {};
}

endian = { 1: 'big', '-1': 'little' }[opts.endian] || opts.endian || 'big';

if (hex.charAt(0) === '-') {
throw new Error('Converting negative numbers to Buffers not supported yet');
}

size = opts.size === 'auto' ? Math.ceil(hex.length / 2) : opts.size || 1;

len = Math.ceil(hex.length / (2 * size)) * size;
buf = Buffer.alloc(len);

// Zero-pad the hex string so the chunks are all `size` long
while (hex.length < 2 * len) {
hex = `0${hex}`;
}

hx = hex.split(new RegExp(`(.{${2 * size}})`)).filter(s => s.length > 0);

hx.forEach((chunk, i) => {
for (var j = 0; j < size; j++) {
var ix = i * size + (endian === 'big' ? j : size - j - 1);
buf[ix] = parseInt(chunk.slice(j * 2, j * 2 + 2), 16);
}
});

return buf;
};

export const fromBuffer = (buf, opts) => {
if (!opts) {
opts = {};
}

var endian =
{ 1: 'big', '-1': 'little' }[opts.endian] || opts.endian || 'big';

var size = opts.size === 'auto' ? Math.ceil(buf.length) : opts.size || 1;

if (buf.length % size !== 0) {
throw new RangeError(
`Buffer length (${buf.length}) must be a multiple of size (${size})`
);
}

var hex = [];
for (var i = 0; i < buf.length; i += size) {
var chunk = [];
for (var j = 0; j < size; j++) {
chunk.push(buf[i + (endian === 'big' ? j : size - j - 1)]);
}

hex.push(chunk.map(c => (c < 16 ? '0' : '') + c.toString(16)).join(''));
}

return new BigNumber(hex.join(''), 16);
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ test('get mkr weight by option', async () => {
console.log('weights', weights);
});

test('get ranked choice mkr weight by option', async () => {
const votes = await service.getMkrSupportRankedChoice(1, 999999999);
console.log('votes', votes);
});

test('get block number', async () => {
const num = await service.getBlockNumber(1511634513);
console.log('num', num);
Expand Down

0 comments on commit 1c81b31

Please sign in to comment.