Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { signMessage, getBlockNumber } from './utils/web3';
import { getHash, verify } from './sign/utils';
import gateways from './gateways.json';
import networks from './networks.json';
import voting from './voting';

export const SNAPSHOT_SUBGRAPH_URL = {
'1': 'https://api.thegraph.com/subgraphs/name/snapshot-labs/snapshot',
Expand Down Expand Up @@ -177,6 +178,12 @@ export async function sleep(time) {
});
}

export function getNumberWithOrdinal(n) {
const s = ['th', 'st', 'nd', 'rd'],
v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
}

export default {
call,
multicall,
Expand All @@ -189,6 +196,8 @@ export default {
getSpaceUri,
clone,
sleep,
getNumberWithOrdinal,
voting,
getProvider,
signMessage,
getBlockNumber,
Expand Down
41 changes: 41 additions & 0 deletions src/voting/approval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export default class ApprovalVoting {
public proposal;
public votes;
public strategies;
public selected;

constructor(proposal, votes, strategies, selected) {
this.proposal = proposal;
this.votes = votes;
this.strategies = strategies;
this.selected = selected;
}

resultsByVoteBalance() {
return this.proposal.choices.map((choice, i) =>
this.votes
.filter((vote: any) => vote.choice.includes(i + 1))
.reduce((a, b: any) => a + b.balance, 0)
);
}

resultsByStrategyScore() {
return this.proposal.choices.map((choice, i) =>
this.strategies.map((strategy, sI) =>
this.votes
.filter((vote: any) => vote.choice.includes(i + 1))
.reduce((a, b: any) => a + b.scores[sI], 0)
)
);
}

sumOfResultsBalance() {
return this.votes.reduce((a, b: any) => a + b.balance, 0);
}

getChoiceString() {
return this.proposal.choices
.filter((choice, i) => this.selected.includes(i + 1))
.join(', ');
}
}
14 changes: 14 additions & 0 deletions src/voting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import singleChoice from './singleChoice';
import approval from './approval';
import quadratic from './quadratic';
import rankedChoice from './rankedChoice';
import weighted from './weighted';

export default {
'single-choice': singleChoice,
approval,
quadratic,
'ranked-choice': rankedChoice,
weighted,
basic: singleChoice
};
82 changes: 82 additions & 0 deletions src/voting/quadratic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
export function percentageOfTotal(i, values, total) {
const reducedTotal: any = total.reduce((a: any, b: any) => a + b, 0);
const percent = (values[i] / reducedTotal) * 100;
return isNaN(percent) ? 0 : percent;
}

export function quadraticMath(i, choice, balance) {
return Math.sqrt(
(percentageOfTotal(i + 1, choice, Object.values(choice)) / 100) * balance
);
}

export default class ApprovalVoting {
public proposal;
public votes;
public strategies;
public selected;

constructor(proposal, votes, strategies, selected) {
this.proposal = proposal;
this.votes = votes;
this.strategies = strategies;
this.selected = selected;
}

resultsByVoteBalance() {
const results = this.proposal.choices
.map((choice, i) =>
this.votes
.map(vote => quadraticMath(i, vote.choice, vote.balance))
.reduce((a, b: any) => a + b, 0)
)
.map(sqrt => sqrt * sqrt);

return results
.map((res, i) => percentageOfTotal(i, results, results))
.map(p => (this.sumOfResultsBalance() / 100) * p);
}

resultsByStrategyScore() {
const results = this.proposal.choices
.map((choice, i) =>
this.strategies.map((strategy, sI) =>
this.votes
.map(vote => quadraticMath(i, vote.choice, vote.scores[sI]))
.reduce((a, b: any) => a + b, 0)
)
)
.map(arr => arr.map(sqrt => [sqrt * sqrt]));

return results.map((res, i) =>
this.strategies
.map((strategy, sI) => [
percentageOfTotal(0, results[i][sI], results.flat(2))
])
.map(p => [(this.sumOfResultsBalance() / 100) * p])
);
}

sumOfResultsBalance() {
return this.votes.reduce((a, b: any) => a + b.balance, 0);
}

getChoiceString() {
return this.proposal.choices
.map((choice, i) => {
if (this.selected[i + 1]) {
return `${
Math.round(
percentageOfTotal(
i + 1,
this.selected,
Object.values(this.selected)
) * 10
) / 10
}% for ${choice}`;
}
})
.filter(el => el != null)
.join(', ');
}
}
107 changes: 107 additions & 0 deletions src/voting/rankedChoice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { getNumberWithOrdinal } from '../utils';

function irv(ballots, rounds) {
const candidates: any[] = [...new Set(ballots.map(vote => vote[0]).flat())];
const votes = Object.entries(
ballots.reduce((votes, [v], i, src) => {
votes[v[0]][0] += src[i][1];
votes[v[0]][1].length > 1
? (votes[v[0]][1] = votes[v[0]][1].map(
(score, sI) => score + src[i][2][sI]
))
: (votes[v[0]][1] = src[i][2]);
return votes;
}, Object.assign({}, ...candidates.map(c => ({ [c]: [0, []] }))))
);

const votesWithoutScore = votes.map((vote: any) => [vote[0], vote[1][0]]);

/* eslint-disable @typescript-eslint/no-unused-vars */
const [topCand, topCount] = votesWithoutScore.reduce(
([n, m]: any[], [v, c]: any[]) => (c > m ? [v, c] : [n, m]),
['?', -Infinity]
);
const [bottomCand, bottomCount] = votesWithoutScore.reduce(
([n, m]: any, [v, c]: any) => (c < m ? [v, c] : [n, m]),
['?', Infinity]
);
/* eslint-enable @typescript-eslint/no-unused-vars */

const sortedByHighest = votes.sort((a: any, b: any) => b[1][0] - a[1][0]);

const totalPowerOfVotes = ballots
.map(bal => bal[1])
.reduce((a, b: any) => a + b, 0);

rounds.push({
round: rounds.length + 1,
sortedByHighest
});

return topCount > totalPowerOfVotes / 2 || sortedByHighest.length < 3
? rounds
: irv(
ballots
.map(ballot => [
ballot[0].filter(c => c != bottomCand),
ballot[1],
ballot[2]
])
.filter(b => b[0].length > 0),
rounds
);
}

function getFinalRound(i, votes) {
const results = irv(
votes.map((vote: any) => [vote.choice, vote.balance, vote.scores]),
[]
);
const finalRound = results[results.length - 1];
return finalRound.sortedByHighest.filter((res: any) => res[0] == i + 1);
}

export default class ApprovalVoting {
public proposal;
public votes;
public strategies;
public selected;

constructor(proposal, votes, strategies, selected) {
this.proposal = proposal;
this.votes = votes;
this.strategies = strategies;
this.selected = selected;
}

resultsByVoteBalance() {
return this.proposal.choices.map((choice, i) =>
getFinalRound(i, this.votes).reduce((a, b: any) => a + b[1][0], 0)
);
}

resultsByStrategyScore() {
return this.proposal.choices.map((choice, i) =>
this.strategies.map((strategy, sI) => {
return getFinalRound(i, this.votes).reduce(
(a, b: any) => a + b[1][1][sI],
0
);
})
);
}

sumOfResultsBalance() {
return this.resultsByVoteBalance().reduce((a, b: any) => a + b);
}

getChoiceString() {
return this.selected
.map(choice => {
if (this.proposal.choices[choice - 1])
return this.proposal.choices[choice - 1];
})
.map((el, i) => `(${getNumberWithOrdinal(i + 1)}) ${el}`)
.join(', ');
}
}
44 changes: 44 additions & 0 deletions src/voting/singleChoice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export default class SingleChoiceVoting {
public proposal;
public votes;
public strategies;
public selected;

constructor(proposal, votes, strategies, selected) {
this.proposal = proposal;
this.votes = votes;
this.strategies = strategies;
this.selected = selected;
}

// Returns an array with the results for each choice
resultsByVoteBalance() {
return this.proposal.choices.map((choice, i) =>
this.votes
.filter((vote: any) => vote.choice === i + 1)
.reduce((a, b: any) => a + b.balance, 0)
);
}

// Returns an array with the results for each choice
// and for each strategy
resultsByStrategyScore() {
return this.proposal.choices.map((choice, i) =>
this.strategies.map((strategy, sI) =>
this.votes
.filter((vote: any) => vote.choice === i + 1)
.reduce((a, b: any) => a + b.scores[sI], 0)
)
);
}

// Returns the total amount of the results
sumOfResultsBalance() {
return this.votes.reduce((a, b: any) => a + b.balance, 0);
}

// Returns a string of all choices
getChoiceString() {
return this.proposal.choices[this.selected - 1];
}
}
Loading